mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-10-02 10:56:22 -04:00
Merge branch 'TwilitRealm:main' into hold-to-mash
This commit is contained in:
@@ -157,10 +157,11 @@ f32 daAlink_c::damageMagnification(BOOL i_checkZoraMag, int param_1) {
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
base_mag *= dusk::getSettings().game.damageMultiplier;
|
||||
if (dusk::getSettings().game.instantDeath) {
|
||||
base_mag = 9999.0f;
|
||||
return 9999.0f; // don't need to multiply this further, 9999 is plenty
|
||||
}
|
||||
|
||||
base_mag *= dusk::getSettings().game.damageMultiplier;
|
||||
#endif
|
||||
|
||||
if (checkWolf() && !checkCargoCarry() && param_1 == 0) {
|
||||
|
||||
@@ -113,6 +113,12 @@ void daAlink_c::handleQuickTransform() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure Link is not underwater
|
||||
if (!checkNoResetFlg0(FLG0_SWIM_UP)) {
|
||||
Z2GetAudioMgr()->seStart(Z2SE_SYS_ERROR, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the game's default checks for if the player can currently transform
|
||||
if (!m_midnaActor->checkMetamorphoseEnableBase()) {
|
||||
Z2GetAudioMgr()->seStart(Z2SE_SYS_ERROR, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0);
|
||||
|
||||
@@ -462,7 +462,7 @@ int daDitem_c::create() {
|
||||
m_itemNo = daDitem_prm::getNo(this);
|
||||
|
||||
#if TARGET_PC
|
||||
const auto [item, displayItem] = dusk::mods::item_check_resolve(mItemGiveTag, m_itemNo, this);
|
||||
const auto [item, displayItem, _] = dusk::mods::item_check_resolve(mItemGiveTag, m_itemNo, this);
|
||||
m_itemNo = item;
|
||||
setDisplayItemNo(displayItem);
|
||||
const char* arc_name = dItem_data::getArcName(displayItem);
|
||||
@@ -550,7 +550,7 @@ static int daDitem_Execute(daDitem_c* i_this) {
|
||||
}
|
||||
|
||||
int daDitem_c::draw() {
|
||||
switch (m_itemNo) {
|
||||
switch (DUSK_IF_ELSE(getDisplayItemNo(), m_itemNo)) {
|
||||
case dItemNo_WOOD_STICK_e:
|
||||
draw_WOOD_STICK();
|
||||
break;
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
#include "d/actor/d_a_e_po.h"
|
||||
#include "d/actor/d_a_obj_poFire.h"
|
||||
#if TARGET_PC
|
||||
#include "d/actor/d_a_alink.h"
|
||||
#endif
|
||||
#include "d/d_cc_d.h"
|
||||
#include "d/d_cc_uty.h"
|
||||
#include "f_op/f_op_actor_enemy.h"
|
||||
|
||||
@@ -3300,7 +3300,7 @@ int daMidna_c::execute() {
|
||||
if (!checkStateFlg0(FLG0_UNK_8000)) {
|
||||
offStateFlg0((daMidna_FLG0)(FLG0_NPC_NEAR | FLG0_NPC_FAR));
|
||||
BOOL far_;
|
||||
if (fopAcIt_Judge((fopAcIt_JudgeFunc)daMidna_searchNpc, &far_)) {
|
||||
if (fopAcIt_Judge((fopAcIt_JudgeFunc)daMidna_searchNpc, &far_) IF_DUSK(&& !dusk::getSettings().game.canTransformAnywhere)) {
|
||||
if (!far_) {
|
||||
onStateFlg0(FLG0_NPC_NEAR);
|
||||
} else {
|
||||
|
||||
@@ -4022,16 +4022,53 @@ BOOL daNpc_grA_c::talk(void*) {
|
||||
if (r26 && talkProc(NULL, TRUE, NULL)) {
|
||||
if (mFlow.getEventId(&sp8) == 1) {
|
||||
#if TARGET_PC
|
||||
const u8 originalItem = sp8;
|
||||
u32 itemGiveTag = 0;
|
||||
if (sp8 == dItemNo_BOMB_IN_BAG_e) {
|
||||
bool isItemCheck = false;
|
||||
if (originalItem == dItemNo_BOMB_IN_BAG_e) {
|
||||
const auto itemCheck =
|
||||
dusk::mods::item_check_commit("goron_reward:F_SP113", sp8, this);
|
||||
dusk::mods::item_check_commit("goron_reward:F_SP113", originalItem, this);
|
||||
sp8 = itemCheck.itemNo;
|
||||
itemGiveTag = itemCheck.tag;
|
||||
isItemCheck = true;
|
||||
} else {
|
||||
const char* stage = dComIfGp_getStartStageName();
|
||||
const bool isAdultGoronShop = (strcmp(stage, "R_SP160") == 0 && originalItem == dItemNo_HYLIA_SHIELD_e) ||
|
||||
(strcmp(stage, "F_SP116") == 0 && originalItem == dItemNo_ARROW_30_e);
|
||||
if (isAdultGoronShop) {
|
||||
const auto itemCheck = dusk::mods::item_check_commit(
|
||||
dusk::mods::item_give_tag_shop(originalItem), originalItem, this);
|
||||
sp8 = itemCheck.itemNo;
|
||||
itemGiveTag = itemCheck.tag;
|
||||
isItemCheck = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (isItemCheck && sp8 == dItemNo_NONE_e) {
|
||||
dusk::mods::item_check_complete({itemGiveTag, dItemNo_NONE_e}, this);
|
||||
r29 = 1;
|
||||
if (mType == 0xb) {
|
||||
field_0x1691 = 1;
|
||||
}
|
||||
} else {
|
||||
field_0x1480 = fopAcM_createItemForPresentDemo(
|
||||
¤t.pos, sp8, 0, -1, -1, NULL, NULL, itemGiveTag);
|
||||
if (field_0x1480 != fpcM_ERROR_PROCESS_ID_e) {
|
||||
s16 r25 =
|
||||
dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xff);
|
||||
dComIfGp_getEvent()->reset(this);
|
||||
fopAcM_orderChangeEventId(this, r25, 1, -1);
|
||||
field_0x9ec = 1;
|
||||
r29 = 1;
|
||||
mOrderNewEvt = 1;
|
||||
if (mType == 0xb) {
|
||||
field_0x1691 = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
field_0x1480 = fopAcM_createItemForPresentDemo(¤t.pos, sp8, 0, -1, -1, NULL,
|
||||
NULL IF_DUSK_ARG(itemGiveTag));
|
||||
NULL);
|
||||
if (field_0x1480 != fpcM_ERROR_PROCESS_ID_e) {
|
||||
s16 r25 = dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xff);
|
||||
dComIfGp_getEvent()->reset(this);
|
||||
@@ -4043,6 +4080,7 @@ BOOL daNpc_grA_c::talk(void*) {
|
||||
field_0x1691 = 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
if (mType == 0xa && field_0x1486 == 0 && daNpcF_chkEvtBit(0x187)) {
|
||||
dComIfGp_getEvent()->reset(this);
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#include "d/actor/d_a_npc_grc.h"
|
||||
#include "d/actor/d_a_npc.h"
|
||||
#include "Z2AudioLib/Z2Instances.h"
|
||||
#if TARGET_PC
|
||||
#include "mods/items.h"
|
||||
#endif
|
||||
#include <cstring>
|
||||
|
||||
enum grC_RES_File_ID {
|
||||
@@ -1476,8 +1479,27 @@ BOOL daNpc_grC_c::talk(void* param_1) {
|
||||
rv = TRUE;
|
||||
|
||||
if (mFlow.getEventId(&i_itemNo) == 1) {
|
||||
mItemID = fopAcM_createItemForPresentDemo(¤t.pos, i_itemNo, 0, -1, -1, NULL, NULL);
|
||||
#if TARGET_PC
|
||||
const bool isCastleTownShop =
|
||||
strcmp(dComIfGp_getStartStageName(), "R_SP160") == 0 &&
|
||||
(i_itemNo == dItemNo_RED_BOTTLE_e || i_itemNo == dItemNo_OIL_BOTTLE_e);
|
||||
u32 itemGiveTag = 0;
|
||||
if (isCastleTownShop) {
|
||||
const auto itemCheck = dusk::mods::item_check_commit(
|
||||
dusk::mods::item_give_tag_shop(i_itemNo), i_itemNo, this);
|
||||
i_itemNo = itemCheck.itemNo;
|
||||
itemGiveTag = itemCheck.tag;
|
||||
}
|
||||
|
||||
if (isCastleTownShop && i_itemNo == dItemNo_NONE_e) {
|
||||
dusk::mods::item_check_complete({itemGiveTag, dItemNo_NONE_e}, this);
|
||||
} else {
|
||||
mItemID = fopAcM_createItemForPresentDemo(
|
||||
¤t.pos, i_itemNo, 0, -1, -1, NULL, NULL, itemGiveTag);
|
||||
}
|
||||
#else
|
||||
mItemID = fopAcM_createItemForPresentDemo(¤t.pos, i_itemNo, 0, -1, -1, NULL, NULL);
|
||||
#endif
|
||||
if (mItemID != fpcM_ERROR_PROCESS_ID_e) {
|
||||
s16 i_eventID = dComIfGp_getEventManager().getEventIdx(this, "DEFAULT_GETITEM", 0xFF);
|
||||
dComIfGp_getEvent()->reset(this);
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
#include "d/actor/d_a_npc_kkri.h"
|
||||
#include "d/actor/d_a_e_ym.h"
|
||||
#if TARGET_PC
|
||||
#include "mods/items.h"
|
||||
#endif
|
||||
#include <cstring>
|
||||
|
||||
static DUSK_CONSTEXPR int l_bmdData[2][2] = {
|
||||
@@ -1183,16 +1186,39 @@ int daNpc_Kkri_c::talk(void*) {
|
||||
if (mItemPartnerId == fpcM_ERROR_PROCESS_ID_e) {
|
||||
#if TARGET_PC
|
||||
u32 itemGiveTag = 0;
|
||||
if (item_no == dItemNo_OIL_BOTTLE3_e) {
|
||||
const auto itemCheck =
|
||||
dusk::mods::item_check_commit("coro_bottle", item_no, this);
|
||||
const char* itemCheckName = nullptr;
|
||||
switch (item_no) {
|
||||
case dItemNo_OIL_BOTTLE3_e:
|
||||
itemCheckName = ITEM_CHECK_CORO_BOTTLE;
|
||||
break;
|
||||
case dItemNo_KANTERA_e:
|
||||
itemCheckName = ITEM_CHECK_CORO_LANTERN;
|
||||
break;
|
||||
case dItemNo_KEY_OF_FILONE_e:
|
||||
itemCheckName = ITEM_CHECK_CORO_GATE_KEY;
|
||||
break;
|
||||
}
|
||||
|
||||
if (itemCheckName != nullptr) {
|
||||
const auto itemCheck = dusk::mods::item_check_commit(
|
||||
itemCheckName, item_no, this);
|
||||
item_no = itemCheck.itemNo;
|
||||
itemGiveTag = itemCheck.tag;
|
||||
}
|
||||
|
||||
if (item_no == dItemNo_NONE_e && itemCheckName != nullptr) {
|
||||
dusk::mods::item_check_complete({itemGiveTag, dItemNo_NONE_e}, this);
|
||||
field_0xfd5 = 1;
|
||||
mEvtNo = 1;
|
||||
evtChange();
|
||||
} else {
|
||||
mItemPartnerId = fopAcM_createItemForPresentDemo(
|
||||
¤t.pos, item_no, 0, -1, -1, NULL, NULL, itemGiveTag);
|
||||
}
|
||||
#else
|
||||
mItemPartnerId = fopAcM_createItemForPresentDemo(
|
||||
¤t.pos, item_no, 0, -1, -1, NULL, NULL);
|
||||
#endif
|
||||
mItemPartnerId = fopAcM_createItemForPresentDemo(¤t.pos, item_no,
|
||||
0, -1, -1, NULL,
|
||||
NULL IF_DUSK_ARG(itemGiveTag));
|
||||
}
|
||||
|
||||
if (fopAcM_IsExecuting(mItemPartnerId)) {
|
||||
|
||||
@@ -268,7 +268,7 @@ int daItem_c::_daItem_create() {
|
||||
const u32 params = fopAcM_GetParam(this);
|
||||
mOriginalItemNo = params & 0xFF;
|
||||
mItemGiveTag = dusk::mods::item_give_tag_freestanding(daItem_prm::getItemBitNo(this));
|
||||
const auto [item, displayItem] =
|
||||
const auto [item, displayItem, _] =
|
||||
dusk::mods::item_check_resolve(mItemGiveTag, mOriginalItemNo, this);
|
||||
mItemOverridden = item != mOriginalItemNo;
|
||||
setDisplayItemNo(displayItem);
|
||||
@@ -935,10 +935,6 @@ void daItem_c::itemGet() {
|
||||
mItemOverridden = m_itemNo != mOriginalItemNo;
|
||||
#endif
|
||||
switch (m_itemNo) {
|
||||
#if TARGET_PC
|
||||
case dItemNo_UTAWA_HEART_e:
|
||||
case dItemNo_KAKERA_HEART_e:
|
||||
#endif
|
||||
case dItemNo_HEART_e:
|
||||
mDoAud_seStart(Z2SE_HEART_PIECE_GET, NULL, 0, 0);
|
||||
execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this));
|
||||
@@ -980,9 +976,6 @@ void daItem_c::itemGet() {
|
||||
case dItemNo_PACHINKO_SHOT_e:
|
||||
mDoAud_seStart(Z2SE_CONSUMP_ITEM_GET, NULL, 0, 0);
|
||||
execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this));
|
||||
#if TARGET_PC
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
#if TARGET_PC
|
||||
if (mItemOverridden) {
|
||||
|
||||
@@ -99,6 +99,15 @@ int daObjLife_c::Create() {
|
||||
mRotateSpeed = 7000;
|
||||
|
||||
#if TARGET_PC
|
||||
// Restore speeds that were skipped if created from a boss with create instead of fastCreate
|
||||
auto bossItemSpeeds = dusk::mods::get_boss_item_actor_speeds();
|
||||
if (bossItemSpeeds.f != 0.f || bossItemSpeeds.y != 0.f) {
|
||||
mOverrideHover = false;
|
||||
speedF = bossItemSpeeds.f;
|
||||
speed.y = bossItemSpeeds.y;
|
||||
dusk::mods::set_boss_item_actor_speeds(0.f, 0.f);
|
||||
}
|
||||
|
||||
if (mOverrideHover) {
|
||||
fopAcM_SetGravity(this, 0.0f);
|
||||
mRotateSpeed = 550;
|
||||
@@ -153,20 +162,20 @@ int daObjLife_c::create() {
|
||||
if (mItemGiveOriginalNo == dItemNo_NONE_e) {
|
||||
mOriginalItemNo = parameterItemNo;
|
||||
mItemGiveTag = dusk::mods::item_give_tag_freestanding(getSaveBitNo());
|
||||
const auto [item, displayItem] =
|
||||
const auto [item, displayItem, was_resolved] =
|
||||
dusk::mods::item_check_resolve(mItemGiveTag, mOriginalItemNo, this);
|
||||
setDisplayItemNo(displayItem);
|
||||
mItemOverridden = item != mOriginalItemNo;
|
||||
mItemOverridden = was_resolved;
|
||||
if (mItemOverridden) {
|
||||
fopAcM_SetParam(this, (params & 0xFFFFFF00) | item);
|
||||
}
|
||||
} else if (mGoldenWolfItem) {
|
||||
mOriginalItemNo = mItemGiveOriginalNo;
|
||||
mItemGiveTag = dusk::mods::item_give_tag_golden_wolf(static_cast<u16>(field_0x938));
|
||||
const auto [item, displayItem] =
|
||||
const auto [item, displayItem, was_resolved] =
|
||||
dusk::mods::item_check_resolve(mItemGiveTag, mOriginalItemNo, this);
|
||||
setDisplayItemNo(displayItem);
|
||||
mItemOverridden = item != mOriginalItemNo;
|
||||
mItemOverridden = was_resolved;
|
||||
if (item != parameterItemNo) {
|
||||
fopAcM_SetParam(this, (params & 0xFFFFFF00) | item);
|
||||
}
|
||||
|
||||
@@ -151,11 +151,7 @@ void daObjMasterSword_c::create_init() {
|
||||
int daObjMasterSword_c::create() {
|
||||
fopAcM_ct(this, daObjMasterSword_c);
|
||||
|
||||
#if TARGET_PC
|
||||
if (dComIfGs_isEventBit(dSv_event_flag_c::F_0264)) {
|
||||
#else
|
||||
if (dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[getFlagNo()])) {
|
||||
#endif
|
||||
return cPhs_ERROR_e;
|
||||
}
|
||||
|
||||
@@ -199,7 +195,7 @@ int daObjMasterSword_c::execute() {
|
||||
#if TARGET_PC
|
||||
const auto masterSword = dusk::mods::item_check_commit(
|
||||
ITEM_CHECK_MASTER_SWORD, dItemNo_MASTER_SWORD_e, this);
|
||||
if (masterSword.itemNo == dItemNo_MASTER_SWORD_e) {
|
||||
if (!masterSword.was_resolved) {
|
||||
dComIfGs_onItemFirstBit(dItemNo_MASTER_SWORD_e);
|
||||
dMeter2Info_setSword(dItemNo_MASTER_SWORD_e, false);
|
||||
dComIfGs_setSelectEquipSword(dItemNo_MASTER_SWORD_e);
|
||||
@@ -214,7 +210,7 @@ int daObjMasterSword_c::execute() {
|
||||
|
||||
const auto shadowCrystal = dusk::mods::item_check_commit(
|
||||
ITEM_CHECK_SHADOW_CRYSTAL, dItemNo_SHADOW_CRYSTAL_e, this);
|
||||
if (shadowCrystal.itemNo == dItemNo_SHADOW_CRYSTAL_e) {
|
||||
if (!shadowCrystal.was_resolved) {
|
||||
execItemGet(shadowCrystal.itemNo, shadowCrystal.tag, this);
|
||||
} else if (shadowCrystal.itemNo == dItemNo_NONE_e) {
|
||||
dusk::mods::item_check_complete(shadowCrystal, this);
|
||||
@@ -222,15 +218,14 @@ int daObjMasterSword_c::execute() {
|
||||
dusk::mods::item_check_enqueue(shadowCrystal, dusk::mods::ItemGiveMode::Demo);
|
||||
}
|
||||
|
||||
dComIfGs_onEventBit(dSv_event_flag_c::F_0264);
|
||||
#else
|
||||
dComIfGs_onItemFirstBit(dItemNo_MASTER_SWORD_e);
|
||||
dMeter2Info_setSword(dItemNo_MASTER_SWORD_e, false);
|
||||
dComIfGs_setSelectEquipSword(dItemNo_MASTER_SWORD_e);
|
||||
|
||||
dComIfGp_setItemLifeCount(dComIfGs_getMaxLife(), 0);
|
||||
dComIfGs_onEventBit(dSv_event_flag_c::saveBitLabels[getFlagNo()]);
|
||||
#endif
|
||||
dComIfGs_onEventBit(dSv_event_flag_c::saveBitLabels[getFlagNo()]);
|
||||
fopAcM_delete(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ int daKey_c::create() {
|
||||
|
||||
#if TARGET_PC
|
||||
mItemGiveTag = dusk::mods::item_give_tag_freestanding(getSaveBitNo());
|
||||
const auto [item, displayItem] =
|
||||
const auto [item, displayItem, _] =
|
||||
dusk::mods::item_check_resolve(mItemGiveTag, dItemNo_SMALL_KEY_e, this);
|
||||
m_itemNo = item;
|
||||
if (m_itemNo == dItemNo_NONE_e) {
|
||||
|
||||
+27
-25
@@ -367,31 +367,7 @@ JKRHeap* daPy_anmHeap_c::setAnimeHeap() {
|
||||
}
|
||||
|
||||
#if !PLATFORM_WII
|
||||
#if TARGET_PC
|
||||
#include "dusk/dvd_asset.hpp"
|
||||
using GameVersion = dusk::version::GameVersion;
|
||||
static const u8* l_sightDL_get() {
|
||||
static u8 buf[0x89];
|
||||
static bool _ = (
|
||||
dusk::LoadDolAsset(
|
||||
buf,
|
||||
{
|
||||
{GameVersion::GcnUsa, 0x803BA0C0},
|
||||
{GameVersion::GcnPal, 0x803BBDA0},
|
||||
{GameVersion::GcnJpn, 0x803B4220},
|
||||
{GameVersion::WiiUsaRev0, 0x803F63C0},
|
||||
{GameVersion::WiiUsa, 0x803E1640},
|
||||
{GameVersion::WiiPal, 0x803E23A0},
|
||||
{GameVersion::WiiJpn, 0x803DF600}
|
||||
},
|
||||
0x89
|
||||
),
|
||||
true
|
||||
);
|
||||
return buf;
|
||||
}
|
||||
#define l_sightDL (l_sightDL_get())
|
||||
#else
|
||||
#if !TARGET_PC
|
||||
#include "assets/l_sightDL__d_a_player.h"
|
||||
#endif
|
||||
|
||||
@@ -435,7 +411,33 @@ void daPy_sightPacket_c::draw() {
|
||||
GXLoadPosMtxImm(mProjMtx, GX_PNMTX0);
|
||||
GXSetCurrentMtx(0);
|
||||
GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR_NULL);
|
||||
#if TARGET_PC
|
||||
GXSetNumTexGens(1);
|
||||
GXSetNumTevStages(1);
|
||||
GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY);
|
||||
GXSetCullMode(GX_CULL_NONE);
|
||||
GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_C1, GX_CC_C0, GX_CC_TEXC, GX_CC_ZERO);
|
||||
GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
|
||||
GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_A0, GX_CA_TEXA, GX_CA_ZERO);
|
||||
GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
|
||||
GXSetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_INVSRCALPHA, GX_LO_CLEAR);
|
||||
GXSetZMode(GX_FALSE, GX_LEQUAL, GX_FALSE);
|
||||
GXSetColorUpdate(GX_TRUE);
|
||||
GXSetAlphaUpdate(GX_FALSE);
|
||||
GXSetDither(GX_TRUE);
|
||||
GXBegin(GX_TRIANGLESTRIP, GX_VTXFMT0, 4);
|
||||
GXPosition3u8(1, 1, 0);
|
||||
GXTexCoord2u8(1, 1);
|
||||
GXPosition3u8(255, 1, 0);
|
||||
GXTexCoord2u8(0, 1);
|
||||
GXPosition3u8(1, 255, 0);
|
||||
GXTexCoord2u8(1, 0);
|
||||
GXPosition3u8(255, 255, 0);
|
||||
GXTexCoord2u8(0, 0);
|
||||
GXEnd();
|
||||
#else
|
||||
GXCallDisplayList(l_sightDL, 0x80);
|
||||
#endif
|
||||
J3DShape::resetVcdVatCache();
|
||||
}
|
||||
|
||||
|
||||
@@ -106,8 +106,9 @@ const char* daShopItem_c::getShopArcname() {
|
||||
if (m_itemNo != dItemNo_NONE_e && mItemGiveOriginalNo == dItemNo_NONE_e) {
|
||||
mItemGiveOriginalNo = m_itemNo;
|
||||
mItemGiveTag = dusk::mods::item_give_tag_shop(mItemGiveOriginalNo);
|
||||
const auto [_, displayItem] =
|
||||
const auto [_, displayItem, was_resolved] =
|
||||
dusk::mods::item_check_resolve(mItemGiveTag, mItemGiveOriginalNo, this);
|
||||
setDisplayItemNo(displayItem);
|
||||
mItemOverridden = displayItem != mItemGiveOriginalNo;
|
||||
if (mItemOverridden) {
|
||||
mOverrideData = mData[mShopItemID];
|
||||
|
||||
@@ -191,6 +191,14 @@ void dBrightCheck_c::modeMove() {
|
||||
mDoAud_seStart(Z2SE_ENTER_GAME, NULL, 0, 0);
|
||||
#ifdef TARGET_PC
|
||||
toggleAutoSave(true);
|
||||
|
||||
if (!dusk::getSettings().game.hideTvSettingsScreen) {
|
||||
const dusk::gamemode::GameMode* gameMode =
|
||||
dusk::gamemode::getGameModeManager().getCurrentGameMode();
|
||||
if (gameMode) {
|
||||
gameMode->invokeOnSaveLoadedFunction();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
mCompleteCheck = true;
|
||||
mMode = MODE_WAIT_e;
|
||||
|
||||
+10
-1
@@ -30,6 +30,7 @@
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/camera_operators.hpp"
|
||||
#include "dusk/commands.hpp"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/action_bindings.h"
|
||||
@@ -1054,6 +1055,11 @@ void dCamera_c::debugDrawInit() {
|
||||
bool dCamera_c::Run() {
|
||||
#if TARGET_PC
|
||||
ResetView();
|
||||
if (dusk::isCameraDetached()) {
|
||||
mFrameCounter++;
|
||||
mTicks++;
|
||||
return true;
|
||||
}
|
||||
if (executeDebugFlyCam() || dusk::mods::camera_run_operators(this)) {
|
||||
mFrameCounter++;
|
||||
mTicks++;
|
||||
@@ -7595,7 +7601,7 @@ bool dCamera_c::executeDebugFlyCam() {
|
||||
if (ImGui::IsKeyDown(ImGuiKey_Q)) rollInput -= 1.0f;
|
||||
if (ImGui::IsKeyDown(ImGuiKey_E)) rollInput += 1.0f;
|
||||
}
|
||||
bool mouseValid = !io.WantCaptureMouse && io.MousePos.x >= 0.0f && io.MousePos.y >= 0.0f;
|
||||
bool mouseValid = !io.WantCaptureMouse && io.MousePos.x >= 0.0f && io.MousePos.y >= 0.0f && ImGui::IsMouseDown(ImGuiMouseButton_Right);
|
||||
if (mouseValid && sFlyCamLastMousePos.x >= 0.0f) {
|
||||
cStickX -= (io.MousePos.x - sFlyCamLastMousePos.x) * 2.0f;
|
||||
cStickY -= (io.MousePos.y - sFlyCamLastMousePos.y) * 2.0f;
|
||||
@@ -11123,6 +11129,9 @@ camera_class* dCam_getCamera() {
|
||||
|
||||
dCamera_c* dCam_getBody() {
|
||||
camera_process_class* camera = (camera_process_class*)dCam_getCamera();
|
||||
#if TARGET_PC
|
||||
if (camera == nullptr) { return nullptr; }
|
||||
#endif
|
||||
return &camera->mCamera;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
#include <typeindex>
|
||||
#include "JSystem/JKernel/JKRHeap.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "client/TracyScoped.hpp"
|
||||
#include "tracy/Tracy.hpp"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "helpers/gx_helper.h"
|
||||
#include "dusk/logging.h"
|
||||
|
||||
@@ -1789,10 +1789,16 @@ void dFile_select_c::nameInput2() {
|
||||
#if TARGET_PC
|
||||
dusk::mods::svc::save_slot_new(mSelectNum);
|
||||
const dusk::gamemode::GameMode* gameMode =
|
||||
dusk::gamemode::getGameModeManager().getCurrentGameMode();
|
||||
dusk::gamemode::getGameModeManager().getCurrentGameMode();
|
||||
if (gameMode) {
|
||||
gameMode->invokeOnNewSaveFunction();
|
||||
gameMode->invokeOnSaveLoadedFunction();
|
||||
}
|
||||
|
||||
// only do OnSaveLoaded callback here if hiding the brightness check screen
|
||||
if (dusk::getSettings().game.hideTvSettingsScreen) {
|
||||
if (gameMode) {
|
||||
gameMode->invokeOnSaveLoadedFunction();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
mDataSelProc = DATASELPROC_NEXT_MODE_WAIT;
|
||||
|
||||
+4
-1
@@ -269,7 +269,10 @@ static void (*item_func_ptr[256])() = {
|
||||
item_func_noentry,
|
||||
};
|
||||
|
||||
inline void getItemFunc(u8 i_itemNo) {
|
||||
#if !TARGET_PC
|
||||
inline
|
||||
#endif
|
||||
void getItemFunc(u8 i_itemNo) {
|
||||
dComIfGs_onItemFirstBit(i_itemNo);
|
||||
item_func_ptr[i_itemNo]();
|
||||
}
|
||||
|
||||
+2
-1
@@ -606,7 +606,8 @@ void dMeter2_c::moveLife() {
|
||||
life_count = (dComIfGs_getMaxLife() / 5) * 4;
|
||||
}
|
||||
|
||||
s16 new_life = dComIfGs_getLife() + dComIfGp_getItemLifeCount();
|
||||
DUSK_IF_ELSE(int, s16) new_life = dComIfGs_getLife() + dComIfGp_getItemLifeCount(); // prevent an overflow with really large damage values
|
||||
|
||||
if (new_life > life_count) {
|
||||
new_life = life_count;
|
||||
} else if (new_life < 0) {
|
||||
|
||||
+24
-3
@@ -198,6 +198,20 @@ dMeter2Draw_c::dMeter2Draw_c(JKRExpHeap* mp_heap) {
|
||||
IF_DUSK_BLOCK_END
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
if (dusk::version::isLessThanWiiJpn()) {
|
||||
if (J2DPicture* b_btn = static_cast<J2DPicture*>(mpScreen->search(MULTI_CHAR('b_btn')))) {
|
||||
b_btn->setAlpha(255);
|
||||
b_btn->setWhite(JUtility::TColor(195, 63, 63, 255));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
static_cast<J2DTextBox*>(mpXYText[i][0]->getPanePtr())->setCharSpace(0.0f);
|
||||
static_cast<J2DTextBox*>(mpXYText[i][1]->getPanePtr())->setCharSpace(0.0f);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
init();
|
||||
field_0xa8 = 0;
|
||||
field_0x1e4 = 0;
|
||||
@@ -2460,7 +2474,7 @@ void dMeter2Draw_c::drawButtonA(u8 i_action, f32 i_posX, f32 i_posY, f32 i_textP
|
||||
mpButtonA->paneTrans(i_posX, i_posY);
|
||||
mpTextA->scale(var_f30 * i_scale, var_f30 * i_scale);
|
||||
mpTextA->paneTrans(g_drawHIO.mButtonATextPosX + i_textPosX,
|
||||
g_drawHIO.mButtonATextPosY + i_textPosY);
|
||||
g_drawHIO.mButtonATextPosY + i_textPosY IF_DUSK(+ (dusk::version::isLessThanWiiJpn() ? 6.0f : 0.0f)));
|
||||
}
|
||||
|
||||
void dMeter2Draw_c::drawButtonB(u8 i_action, bool param_1, f32 i_posX, f32 i_posY, f32 i_textPosX,
|
||||
@@ -2658,6 +2672,11 @@ void dMeter2Draw_c::drawButtonXY(int i_no, u8 i_itemNo, u8 i_action, bool param_
|
||||
|
||||
static u64 const tag[] = {MULTI_CHAR('item_x_n'), MULTI_CHAR('item_y_n')};
|
||||
|
||||
#if TARGET_PC
|
||||
const float btnXOffsetX = dusk::version::isLessThanWiiJpn() ? 2.0f : 0.0f;
|
||||
const float btnXOffsetY = dusk::version::isLessThanWiiJpn() ? 38.0f : 0.0f;
|
||||
#endif
|
||||
|
||||
if (!param_3) {
|
||||
mpScreen->search(tag[i_no])->hide();
|
||||
|
||||
@@ -2704,7 +2723,8 @@ void dMeter2Draw_c::drawButtonXY(int i_no, u8 i_itemNo, u8 i_action, bool param_
|
||||
|
||||
if (i_no == SELECT_X_e) {
|
||||
mpTextXY[i_no]->scale(g_drawHIO.mButtonXYTextScale, g_drawHIO.mButtonXYTextScale);
|
||||
mpTextXY[i_no]->paneTrans(g_drawHIO.mButtonXYTextPosX, g_drawHIO.mButtonXYTextPosY);
|
||||
mpTextXY[i_no]->paneTrans(g_drawHIO.mButtonXYTextPosX IF_DUSK(- btnXOffsetX),
|
||||
g_drawHIO.mButtonXYTextPosY IF_DUSK(+ btnXOffsetY));
|
||||
} else if (i_no == SELECT_Y_e) {
|
||||
mpTextXY[i_no]->scale(g_drawHIO.mButtonXYTextScale, g_drawHIO.mButtonXYTextScale);
|
||||
mpTextXY[i_no]->paneTrans(g_drawHIO.mButtonXYTextPosX, g_drawHIO.mButtonXYTextPosY);
|
||||
@@ -2766,7 +2786,8 @@ void dMeter2Draw_c::drawButtonXY(int i_no, u8 i_itemNo, u8 i_action, bool param_
|
||||
mpLightXY[0]->setAlphaRate(mButtonXItemBaseAlpha[var_r29] * field_0x7f0);
|
||||
|
||||
mpTextXY[i_no]->scale(g_drawHIO.mButtonXYTextScale, g_drawHIO.mButtonXYTextScale);
|
||||
mpTextXY[i_no]->paneTrans(g_drawHIO.mButtonXYTextPosX, g_drawHIO.mButtonXYTextPosY);
|
||||
mpTextXY[i_no]->paneTrans(g_drawHIO.mButtonXYTextPosX IF_DUSK(- btnXOffsetX),
|
||||
g_drawHIO.mButtonXYTextPosY IF_DUSK(+ btnXOffsetY));
|
||||
} else if (i_no == SELECT_Y_e) {
|
||||
mpButtonXY[1]->scale(g_drawHIO.mButtonYScale, g_drawHIO.mButtonYScale);
|
||||
mpButtonXY[1]->paneTrans(g_drawHIO.mButtonYPosX, g_drawHIO.mButtonYPosY);
|
||||
|
||||
@@ -1762,7 +1762,7 @@ u16 dMsgFlow_c::query042(mesg_flow_node_branch* i_flowNode_p, fopAc_ac_c* i_spea
|
||||
daMidna_c* midna_p = daPy_py_c::getMidnaActor();
|
||||
|
||||
u8 ret = 0;
|
||||
if (strcmp("F_SP116", dComIfGp_getStartStageName()) == 0 && dComIfGs_isSaveDunSwitch(60)) {
|
||||
if (strcmp("F_SP116", dComIfGp_getStartStageName()) == 0 && dComIfGs_isSaveDunSwitch(60) IF_DUSK(&& !dusk::getSettings().game.canTransformAnywhere)) {
|
||||
ret = 4;
|
||||
} else if (midna_p->checkNpcNear()) {
|
||||
ret = 1;
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#include "dusk/autosave.h"
|
||||
#include "dusk/memory.h"
|
||||
#include "dusk/mods/item.hpp"
|
||||
#include "dusk/trigger_viewer.h"
|
||||
#include "dusk/ui/ui.hpp"
|
||||
#include "mods/items.h"
|
||||
#endif
|
||||
@@ -677,6 +678,10 @@ static int dScnPly_Draw(dScnPly_c* i_this) {
|
||||
dComIfG_Ccsp()->Draw();
|
||||
dComIfG_Bgsp().Draw();
|
||||
|
||||
#if TARGET_PC
|
||||
dusk::TriggerView::execute();
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
dPath_Draw();
|
||||
#endif
|
||||
|
||||
@@ -1537,6 +1537,27 @@ dStage_objectNameInf* dStage_searchName(char const* objName) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
dStage_objectNameInf* dStage_searchNameCI(char const* objName) {
|
||||
dStage_objectNameInf* obj = l_objectName;
|
||||
|
||||
for (u32 i = 0; i < ARRAY_SIZEU(l_objectName); i++) {
|
||||
const char* a = obj->name;
|
||||
const char* b = objName;
|
||||
while (*a && *b && tolower((unsigned char)*a) == tolower((unsigned char)*b)) {
|
||||
++a;
|
||||
++b;
|
||||
}
|
||||
if (*a == '\0' && *b == '\0') {
|
||||
return obj;
|
||||
}
|
||||
obj++;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
const char* dStage_getName(s16 procName, s8 argument) {
|
||||
static char tmp_name[dStage_NAME_LENGTH];
|
||||
|
||||
|
||||
@@ -0,0 +1,880 @@
|
||||
#include "commands.hpp"
|
||||
|
||||
#include "JSystem/JUtility/JUTGamePad.h"
|
||||
#include "SSystem/SComponent/c_math.h"
|
||||
#include "SSystem/SComponent/c_sxyz.h"
|
||||
#include "SSystem/SComponent/c_xyz.h"
|
||||
#include "c/c_damagereaction.h"
|
||||
#include "d/actor/d_a_alink.h"
|
||||
#include "d/d_com_inf_actor.h"
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include "d/d_camera.h"
|
||||
#include "d/d_kankyo.h"
|
||||
#include "d/d_stage.h"
|
||||
#include "dusk/game_clock.h"
|
||||
#include "f_op/f_op_actor_mng.h"
|
||||
#include "f_pc/f_pc_layer.h"
|
||||
#include "f_pc/f_pc_layer_iter.h"
|
||||
#include "f_pc/f_pc_manager.h"
|
||||
#include "f_pc/f_pc_node.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "fmt/format.h"
|
||||
|
||||
namespace dusk {
|
||||
namespace {
|
||||
|
||||
static constexpr int kMaxHistory = 64;
|
||||
|
||||
static std::vector<std::string> SplitArgs(std::string_view input) {
|
||||
std::vector<std::string> args;
|
||||
std::string cur;
|
||||
for (char c : input) {
|
||||
if (c == ' ' || c == '\t') {
|
||||
if (!cur.empty()) {
|
||||
args.push_back(std::move(cur));
|
||||
cur.clear();
|
||||
}
|
||||
} else {
|
||||
cur += c;
|
||||
}
|
||||
}
|
||||
if (!cur.empty()) {
|
||||
args.push_back(std::move(cur));
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
static std::optional<long> ParseLong(const std::string& s) {
|
||||
if (s.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
char* end = nullptr;
|
||||
const long v = std::strtol(s.c_str(), &end, 0);
|
||||
return (end != s.c_str() && *end == '\0') ? std::optional<long>{v} : std::nullopt;
|
||||
}
|
||||
|
||||
static std::optional<float> ParseFloat(const std::string& s) {
|
||||
if (s.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
char* end = nullptr;
|
||||
const float v = std::strtof(s.c_str(), &end);
|
||||
return (end != s.c_str() && *end == '\0') ? std::optional<float>{v} : std::nullopt;
|
||||
}
|
||||
|
||||
static const char* ActorShortName(s16 profname) {
|
||||
const char* n = dStage_getName(profname, -1);
|
||||
return n ? n : "?";
|
||||
}
|
||||
|
||||
// Resolves @found / @link / @<id> to a process pointer, outputting an error on failure
|
||||
static base_process_class* ParseProcArg(
|
||||
const std::string& s, unsigned int foundProcId, const CommandOutput& output) {
|
||||
if (s.empty() || s[0] != '@') {
|
||||
output("Error: proc reference must start with @");
|
||||
return nullptr;
|
||||
}
|
||||
const std::string inner = s.substr(1);
|
||||
unsigned int id;
|
||||
if (inner == "found") {
|
||||
if (foundProcId == 0) {
|
||||
output("Error: @found is not set");
|
||||
return nullptr;
|
||||
}
|
||||
id = foundProcId;
|
||||
} else if (inner == "link") {
|
||||
auto* player = dComIfGp_getPlayer(0);
|
||||
if (player == nullptr) {
|
||||
output("Error: player not available");
|
||||
return nullptr;
|
||||
}
|
||||
id = (unsigned int)fpcM_GetID(player);
|
||||
} else {
|
||||
const auto v = ParseLong(inner);
|
||||
if (!v) {
|
||||
output("Error: invalid proc ID");
|
||||
return nullptr;
|
||||
}
|
||||
id = (unsigned int)*v;
|
||||
}
|
||||
auto* proc = fpcM_SearchByID(id);
|
||||
if (proc == nullptr) {
|
||||
output(fmt::format(FMT_STRING("Error: proc {} not found"), id));
|
||||
return nullptr;
|
||||
}
|
||||
return proc;
|
||||
}
|
||||
|
||||
// Like ParseProcArg but ensures the proc is an actor
|
||||
static fopAc_ac_c* ParseActorArg(
|
||||
const std::string& s, unsigned int foundProcId, const CommandOutput& output) {
|
||||
auto* proc = ParseProcArg(s, foundProcId, output);
|
||||
if (proc == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!fopAcM_IsActor(proc)) {
|
||||
output("Error: proc is not an actor");
|
||||
return nullptr;
|
||||
}
|
||||
return static_cast<fopAc_ac_c*>(proc);
|
||||
}
|
||||
|
||||
static std::optional<s16> ParseActorId(const std::string& s) {
|
||||
if (const auto v = ParseLong(s)) {
|
||||
return (s16)*v;
|
||||
}
|
||||
const auto* entry = dStage_searchNameCI(s.c_str());
|
||||
return entry ? std::optional<s16>{entry->procname} : std::nullopt;
|
||||
}
|
||||
|
||||
static std::optional<cXyz> ParseXYZ(const std::vector<std::string>& args, size_t i) {
|
||||
const auto x = ParseFloat(args[i]), y = ParseFloat(args[i + 1]), z = ParseFloat(args[i + 2]);
|
||||
return (x && y && z) ? std::optional<cXyz>{cXyz(*x, *y, *z)} : std::nullopt;
|
||||
}
|
||||
|
||||
static bool TryAngle(
|
||||
s16& out, const std::vector<std::string>& args, size_t i, const CommandOutput& output) {
|
||||
if (args.size() <= i) {
|
||||
return true;
|
||||
}
|
||||
const auto a = ParseLong(args[i]);
|
||||
if (!a) {
|
||||
output("Error: invalid angle");
|
||||
return false;
|
||||
}
|
||||
out = (s16)*a;
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::string actorLine(const base_process_class* proc) {
|
||||
const auto* ac = static_cast<const fopAc_ac_c*>(proc);
|
||||
return fmt::format(FMT_STRING("procId={} 0x{:04X} ({}) @ ({:.2f}, {:.2f}, {:.2f}) room={}"),
|
||||
(unsigned int)proc->id, (unsigned int)(u16)proc->profname, ActorShortName(proc->profname),
|
||||
ac->current.pos.x, ac->current.pos.y, ac->current.pos.z, (int)ac->current.roomNo);
|
||||
}
|
||||
|
||||
static void recurseLayer(void* p, int (*callback)(void*, void*), void* ctx) {
|
||||
auto* proc = static_cast<base_process_class*>(p);
|
||||
if (fpcBs_Is_JustOfType(g_fpcNd_type, proc->subtype)) {
|
||||
fpcLyIt_OnlyHere(&static_cast<process_node_class*>(p)->layer, callback, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
struct ListContext {
|
||||
s16 targetId;
|
||||
std::vector<std::string>* output;
|
||||
};
|
||||
struct FindContext {
|
||||
s16 targetId;
|
||||
std::vector<base_process_class*> matches;
|
||||
};
|
||||
|
||||
static int ListActorCallback(void* p, void* ctx) {
|
||||
auto* proc = static_cast<base_process_class*>(p);
|
||||
auto* context = static_cast<ListContext*>(ctx);
|
||||
if (fopAcM_IsActor(proc) && (context->targetId < 0 || proc->profname == context->targetId)) {
|
||||
context->output->push_back(" " + actorLine(proc));
|
||||
}
|
||||
recurseLayer(p, ListActorCallback, ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int FindActorCallback(void* p, void* ctx) {
|
||||
auto* proc = static_cast<base_process_class*>(p);
|
||||
auto* context = static_cast<FindContext*>(ctx);
|
||||
if (fopAcM_IsActor(proc) && proc->profname == context->targetId) {
|
||||
context->matches.push_back(proc);
|
||||
}
|
||||
recurseLayer(p, FindActorCallback, ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct CameraFlyState {
|
||||
bool active = false;
|
||||
cXyz startEye;
|
||||
cXyz startCenter;
|
||||
cXyz endEye;
|
||||
cXyz endCenter;
|
||||
float duration = 0.0f;
|
||||
float elapsed = 0.0f;
|
||||
};
|
||||
CameraFlyState s_cameraFly;
|
||||
|
||||
static dCamera_c* getCamera() { return dCam_getBody(); }
|
||||
|
||||
static cXyz anglesToDir(s16 h, s16 v) {
|
||||
return cXyz(cM_scos(v) * cM_ssin(h), cM_ssin(v), cM_scos(v) * cM_scos(h));
|
||||
}
|
||||
|
||||
static void cameraToAngles(dCamera_c* cam, s16& h, s16& v) {
|
||||
const cXyz eye = cam->iEye();
|
||||
const cXyz ctr = cam->iCenter();
|
||||
const float dx = ctr.x - eye.x;
|
||||
const float dy = ctr.y - eye.y;
|
||||
const float dz = ctr.z - eye.z;
|
||||
const float hDist = sqrtf(dx * dx + dz * dz);
|
||||
h = cM_atan2s(dx, dz);
|
||||
v = cM_atan2s(dy, hDist);
|
||||
}
|
||||
|
||||
static float smoothstep(float t) {
|
||||
t = std::clamp(t, 0.0f, 1.0f);
|
||||
return t * t * (3.0f - 2.0f * t);
|
||||
}
|
||||
|
||||
static constexpr float kCamTargetDist = 100.0f;
|
||||
|
||||
} // namespace
|
||||
|
||||
bool isCameraDetached() { return s_cameraFly.active; }
|
||||
|
||||
void processCameraCommands() {
|
||||
if (!s_cameraFly.active) { return; }
|
||||
dCamera_c* cam = getCamera();
|
||||
if (cam == nullptr) { s_cameraFly.active = false; return; }
|
||||
|
||||
cXyz eye, center;
|
||||
|
||||
if (s_cameraFly.duration <= 0.0f) {
|
||||
eye = s_cameraFly.endEye;
|
||||
center = s_cameraFly.endCenter;
|
||||
} else {
|
||||
s_cameraFly.elapsed += dusk::game_clock::kSimPeriod;
|
||||
const float t = smoothstep(s_cameraFly.elapsed / s_cameraFly.duration);
|
||||
eye = s_cameraFly.startEye + (s_cameraFly.endEye - s_cameraFly.startEye) * t;
|
||||
center = s_cameraFly.startCenter + (s_cameraFly.endCenter - s_cameraFly.startCenter) * t;
|
||||
|
||||
if (s_cameraFly.elapsed >= s_cameraFly.duration) {
|
||||
eye = s_cameraFly.endEye;
|
||||
center = s_cameraFly.endCenter;
|
||||
s_cameraFly.startEye = s_cameraFly.endEye;
|
||||
s_cameraFly.startCenter = s_cameraFly.endCenter;
|
||||
s_cameraFly.duration = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
cam->Reset(center, eye);
|
||||
cam->mDebugFlyCam.initialized = false;
|
||||
}
|
||||
|
||||
void runCommand(std::string_view cmdLine, CommandState& state, const CommandOutput& output) {
|
||||
if (state.echoEnabled) {
|
||||
output(fmt::format(FMT_STRING("> {}"), cmdLine));
|
||||
}
|
||||
|
||||
if (!cmdLine.empty()) {
|
||||
if (state.history.empty() || state.history.back() != cmdLine) {
|
||||
state.history.push_back(std::string(cmdLine));
|
||||
if ((int)state.history.size() > kMaxHistory) {
|
||||
state.history.erase(state.history.begin());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto args = SplitArgs(cmdLine);
|
||||
if (args.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto& cmd = args[0];
|
||||
|
||||
auto requirePlayer = [&]() -> daAlink_c* {
|
||||
auto* p = (daAlink_c*)dComIfGp_getPlayer(0);
|
||||
if (p == nullptr) {
|
||||
output("Error: player not available");
|
||||
}
|
||||
return p;
|
||||
};
|
||||
|
||||
if (cmd == "tp") {
|
||||
auto* player = requirePlayer();
|
||||
if (player == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.size() >= 2 && args[1].starts_with('@')) {
|
||||
auto* ac = ParseActorArg(args[1], state.foundProcId, output);
|
||||
if (ac == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.size() >= 3 && args[2].starts_with('@')) {
|
||||
auto* destAc = ParseActorArg(args[2], state.foundProcId, output);
|
||||
if (destAc == nullptr) {
|
||||
return;
|
||||
}
|
||||
const cXyz destPos = destAc->current.pos;
|
||||
ac->current.pos = destPos;
|
||||
output(fmt::format(FMT_STRING("Moved actor {} to ({:.2f}, {:.2f}, {:.2f})"), ac->id,
|
||||
destPos.x, destPos.y, destPos.z));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.size() >= 5) {
|
||||
const auto pos = ParseXYZ(args, 2);
|
||||
if (!pos) {
|
||||
output("Error: invalid coordinates");
|
||||
return;
|
||||
}
|
||||
ac->current.pos = *pos;
|
||||
if (!TryAngle(ac->shape_angle.y, args, 5, output)) {
|
||||
return;
|
||||
}
|
||||
output(fmt::format(FMT_STRING("Moved actor {} to ({:.2f}, {:.2f}, {:.2f})"), ac->id,
|
||||
pos->x, pos->y, pos->z));
|
||||
return;
|
||||
}
|
||||
|
||||
player->current.pos = ac->current.pos;
|
||||
output(fmt::format(FMT_STRING("Teleported to actor {} ({:.2f}, {:.2f}, {:.2f})"),
|
||||
ac->id, ac->current.pos.x, ac->current.pos.y, ac->current.pos.z));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.size() < 4) {
|
||||
output("Usage: tp <x> <y> <z> [angle] | tp @<procId> [<x> <y> <z> [angle] | @link]");
|
||||
return;
|
||||
}
|
||||
const auto pos = ParseXYZ(args, 1);
|
||||
if (!pos) {
|
||||
output("Error: invalid coordinates");
|
||||
return;
|
||||
}
|
||||
player->current.pos = *pos;
|
||||
if (!TryAngle(player->shape_angle.y, args, 4, output)) {
|
||||
return;
|
||||
}
|
||||
output(fmt::format(
|
||||
FMT_STRING("Teleported to ({:.2f}, {:.2f}, {:.2f})"), pos->x, pos->y, pos->z));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "spawn") {
|
||||
if (args.size() < 2) {
|
||||
output("Usage: spawn <actorId> [params] [x y z] [angle]");
|
||||
return;
|
||||
}
|
||||
auto* player = requirePlayer();
|
||||
if (player == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto actorId = ParseActorId(args[1]);
|
||||
if (!actorId) {
|
||||
output("Error: unknown actor ID or name");
|
||||
return;
|
||||
}
|
||||
|
||||
long paramsL = -1;
|
||||
if (args.size() >= 3) {
|
||||
const auto p = ParseLong(args[2]);
|
||||
if (!p) {
|
||||
output("Error: invalid params");
|
||||
return;
|
||||
}
|
||||
paramsL = *p;
|
||||
}
|
||||
|
||||
cXyz pos = player->current.pos;
|
||||
if (args.size() >= 6) {
|
||||
const auto spawnPos = ParseXYZ(args, 3);
|
||||
if (!spawnPos) {
|
||||
output("Error: invalid spawn coordinates");
|
||||
return;
|
||||
}
|
||||
pos = *spawnPos;
|
||||
}
|
||||
|
||||
s16 angleY = 0;
|
||||
if (!TryAngle(angleY, args, 6, output)) {
|
||||
return;
|
||||
}
|
||||
csXyz angle;
|
||||
angle.set(0, angleY, 0);
|
||||
cXyz scale(1.0f, 1.0f, 1.0f);
|
||||
|
||||
layer_class* savedLayer = fpcLy_CurrentLayer();
|
||||
base_process_class* playScene = fpcM_SearchByName(fpcNm_PLAY_SCENE_e);
|
||||
if (playScene != nullptr) {
|
||||
fpcLy_SetCurrentLayer(&((process_node_class*)playScene)->layer);
|
||||
}
|
||||
unsigned int result = fopAcM_create(
|
||||
*actorId, (u32)paramsL, &pos, player->current.roomNo, &angle, &scale, (s8)-1);
|
||||
fpcLy_SetCurrentLayer(savedLayer);
|
||||
|
||||
output(result != 0 ? fmt::format(FMT_STRING("Spawned actorId=0x{:04X} procId={}"),
|
||||
(unsigned int)(u16)*actorId, result) :
|
||||
fmt::format(FMT_STRING("Failed to spawn actorId=0x{:04X}"),
|
||||
(unsigned int)(u16)*actorId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "reset") {
|
||||
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
|
||||
output("Soft reset triggered");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "warp") {
|
||||
if (args.size() < 4) {
|
||||
output("Usage: warp <stageName> <point> <roomNo> [layer=-1]");
|
||||
output(" e.g. warp F_SP121 0 0");
|
||||
return;
|
||||
}
|
||||
const auto pointL = ParseLong(args[2]), roomL = ParseLong(args[3]);
|
||||
if (!pointL || !roomL) {
|
||||
output("Error: invalid point or room number");
|
||||
return;
|
||||
}
|
||||
long layerL = -1;
|
||||
if (args.size() >= 5) {
|
||||
const auto l = ParseLong(args[4]);
|
||||
if (!l) {
|
||||
output("Error: invalid layer");
|
||||
return;
|
||||
}
|
||||
layerL = *l;
|
||||
}
|
||||
state.lastWarpStage = args[1];
|
||||
dComIfGp_setNextStage(state.lastWarpStage.c_str(), (s16)*pointL, (s8)*roomL, (s8)layerL);
|
||||
output(fmt::format(FMT_STRING("Warping to {} point={} room={} layer={}"), args[1],
|
||||
(int)(s16)*pointL, (int)(s8)*roomL, (int)(s8)layerL));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "list") {
|
||||
s16 targetId = -1;
|
||||
if (args.size() >= 2) {
|
||||
const auto id = ParseActorId(args[1]);
|
||||
if (!id) {
|
||||
output("Error: unknown actor ID or name");
|
||||
return;
|
||||
}
|
||||
targetId = *id;
|
||||
}
|
||||
std::vector<std::string> results;
|
||||
ListContext ctx{targetId, &results};
|
||||
fpcLyIt_OnlyHere(fpcLy_RootLayer(), ListActorCallback, &ctx);
|
||||
output(results.empty() ? "No matching actors found" :
|
||||
fmt::format(FMT_STRING("Found {} actor(s):"), results.size()));
|
||||
for (const auto& r : results) {
|
||||
output(r);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "killall") {
|
||||
if (args.size() < 2) {
|
||||
output("Usage: killall <actorId|name>");
|
||||
return;
|
||||
}
|
||||
const auto targetId = ParseActorId(args[1]);
|
||||
if (!targetId) {
|
||||
output("Error: unknown actor ID or name");
|
||||
return;
|
||||
}
|
||||
FindContext ctx{*targetId, {}};
|
||||
fpcLyIt_OnlyHere(fpcLy_RootLayer(), FindActorCallback, &ctx);
|
||||
for (auto* proc : ctx.matches) {
|
||||
fpcM_Delete(proc);
|
||||
}
|
||||
output(fmt::format(FMT_STRING("Deleted {} actor(s) of type {} ({})"),
|
||||
(int)ctx.matches.size(), (unsigned int)(u16)*targetId, ActorShortName(*targetId)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "heal") {
|
||||
auto* player = requirePlayer();
|
||||
if (player == nullptr) {
|
||||
return;
|
||||
}
|
||||
const u16 maxLife = dComIfGs_getMaxLife() / 5 * 4;
|
||||
u16 newLife = maxLife;
|
||||
if (args.size() >= 2) {
|
||||
const auto amount = ParseLong(args[1]);
|
||||
if (!amount) {
|
||||
output("Error: invalid amount");
|
||||
return;
|
||||
}
|
||||
newLife =
|
||||
(u16)std::max(0L, std::min((long)maxLife, (long)dComIfGs_getLife() + *amount));
|
||||
}
|
||||
dComIfGs_setLife(newLife);
|
||||
output(fmt::format(FMT_STRING("Health: {}/{}"), (int)newLife, (int)maxLife));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "kill") {
|
||||
if (args.size() >= 2) {
|
||||
auto* proc = ParseProcArg(args[1], state.foundProcId, output);
|
||||
if (proc == nullptr) {
|
||||
return;
|
||||
}
|
||||
fpcM_Delete(proc);
|
||||
output(fmt::format(FMT_STRING("Deleted proc {}"), proc->id));
|
||||
} else {
|
||||
auto* player = requirePlayer();
|
||||
if (player == nullptr) {
|
||||
return;
|
||||
}
|
||||
dComIfGs_setLife(0);
|
||||
output("Set Link's health to 0");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "freeze" || cmd == "unfreeze") {
|
||||
const bool doFreeze = (cmd == "freeze");
|
||||
if (args.size() < 2) {
|
||||
g_dComIfAc_gameInfo.mPause = doFreeze;
|
||||
output(doFreeze ? "Global freeze on" : "Global freeze off");
|
||||
return;
|
||||
}
|
||||
auto* ac = ParseActorArg(args[1], state.foundProcId, output);
|
||||
if (ac == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (doFreeze) {
|
||||
fpcM_PauseEnable(ac, 1);
|
||||
output(fmt::format(FMT_STRING("Froze actor {}"), (unsigned int)ac->id));
|
||||
} else {
|
||||
fpcM_PauseDisable(ac, 1);
|
||||
output(fmt::format(FMT_STRING("Unfroze actor {}"), (unsigned int)ac->id));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "rate") {
|
||||
if (args.size() >= 2) {
|
||||
const auto hz = ParseLong(args[1]);
|
||||
if (!hz || *hz <= 0) {
|
||||
output("Error: rate must be a positive integer");
|
||||
return;
|
||||
}
|
||||
dusk::game_clock::set_sim_rate((float)*hz);
|
||||
}
|
||||
output(fmt::format(FMT_STRING("Sim rate: {:.0f} hz"), dusk::game_clock::get_sim_rate()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "ebf") {
|
||||
if (args.size() >= 2) {
|
||||
const auto val = ParseLong(args[1]);
|
||||
if (!val || *val < 0 || *val > 255) {
|
||||
output("Error: value must be 0-255");
|
||||
return;
|
||||
}
|
||||
cDmr_SkipInfo = (u8)*val;
|
||||
}
|
||||
output(fmt::format(FMT_STRING("EBF = {}"), (int)cDmr_SkipInfo));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "time") {
|
||||
if (args.size() >= 2) {
|
||||
const auto t = ParseFloat(args[1]);
|
||||
if (!t || *t < 0.0f || *t > 360.0f) {
|
||||
output("Error: time must be a float between 0 and 360");
|
||||
return;
|
||||
}
|
||||
dKy_instant_timechg(*t);
|
||||
}
|
||||
output(fmt::format(FMT_STRING("Time: {:.2f} ({}:{:02d})"), dComIfGs_getTime(),
|
||||
dKy_getdaytime_hour(), dKy_getdaytime_minute()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "rupees") {
|
||||
const u16 maxRupees = dComIfGs_getRupeeMax();
|
||||
if (args.size() >= 2) {
|
||||
const auto amount = ParseLong(args[1]);
|
||||
if (!amount) {
|
||||
output("Error: invalid amount");
|
||||
return;
|
||||
}
|
||||
dComIfGs_setRupee((u16)std::max(0L, std::min((long)maxRupees, *amount)));
|
||||
}
|
||||
output(fmt::format(FMT_STRING("Rupees: {}/{}"), (int)dComIfGs_getRupee(), (int)maxRupees));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "find") {
|
||||
if (args.size() < 2) {
|
||||
if (state.foundProcId == 0) {
|
||||
output("@found is not set");
|
||||
return;
|
||||
}
|
||||
auto* proc = fpcM_SearchByID(state.foundProcId);
|
||||
output(proc != nullptr && fopAcM_IsActor(proc) ?
|
||||
"@found = " + actorLine(proc) :
|
||||
fmt::format(FMT_STRING("@found = {} (no longer exists)"),
|
||||
(unsigned int)state.foundProcId));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto targetId = ParseActorId(args[1]);
|
||||
if (!targetId) {
|
||||
output("Error: unknown actor ID or name");
|
||||
return;
|
||||
}
|
||||
|
||||
int targetN = 1;
|
||||
if (args.size() >= 3) {
|
||||
const auto n = ParseLong(args[2]);
|
||||
if (!n || *n < 1) {
|
||||
output("Error: index must be >= 1");
|
||||
return;
|
||||
}
|
||||
targetN = (int)*n;
|
||||
}
|
||||
|
||||
FindContext ctx{*targetId, {}};
|
||||
fpcLyIt_OnlyHere(fpcLy_RootLayer(), FindActorCallback, &ctx);
|
||||
|
||||
if (ctx.matches.empty()) {
|
||||
output(fmt::format(FMT_STRING("No actors found for '{}'"), args[1]));
|
||||
return;
|
||||
}
|
||||
|
||||
std::sort(ctx.matches.begin(), ctx.matches.end(),
|
||||
[](const base_process_class* a, const base_process_class* b) { return a->id < b->id; });
|
||||
|
||||
if (targetN > (int)ctx.matches.size()) {
|
||||
output(fmt::format(
|
||||
FMT_STRING("Error: only {} actor(s) of that type exist"), (int)ctx.matches.size()));
|
||||
return;
|
||||
}
|
||||
|
||||
auto* picked = ctx.matches[(size_t)(targetN - 1)];
|
||||
state.foundProcId = picked->id;
|
||||
output(fmt::format(
|
||||
FMT_STRING("@found [{}/{}] {}"), targetN, (int)ctx.matches.size(), actorLine(picked)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "camera") {
|
||||
const std::string sub = args.size() >= 2 ? args[1] : "";
|
||||
|
||||
if (sub == "attach") {
|
||||
s_cameraFly.active = false;
|
||||
output("Camera attached");
|
||||
return;
|
||||
}
|
||||
|
||||
auto* cam = getCamera();
|
||||
if (cam == nullptr) { output("Error: camera not available"); return; }
|
||||
|
||||
if (sub == "detach") {
|
||||
s_cameraFly.active = true;
|
||||
s_cameraFly.duration = 0.0f;
|
||||
s_cameraFly.startEye = s_cameraFly.endEye = cam->iEye();
|
||||
s_cameraFly.startCenter = s_cameraFly.endCenter = cam->iCenter();
|
||||
output("Camera detached");
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub == "tp") {
|
||||
// camera tp @<ref>
|
||||
if (args.size() >= 3 && !args[2].empty() && args[2][0] == '@') {
|
||||
auto* ac = ParseActorArg(args[2], state.foundProcId, output);
|
||||
if (ac == nullptr) { return; }
|
||||
const cXyz actorPos = ac->current.pos;
|
||||
const cXyz dir = anglesToDir(ac->shape_angle.y, 0);
|
||||
const cXyz newEye = cXyz(actorPos.x - dir.x * kCamTargetDist,
|
||||
actorPos.y - dir.y * kCamTargetDist + 50.0f,
|
||||
actorPos.z - dir.z * kCamTargetDist);
|
||||
s_cameraFly.active = true; s_cameraFly.duration = 0.0f;
|
||||
s_cameraFly.startEye = s_cameraFly.endEye = newEye;
|
||||
s_cameraFly.startCenter = s_cameraFly.endCenter = actorPos;
|
||||
output(fmt::format(FMT_STRING("Camera moved to actor {}"), ac->id));
|
||||
return;
|
||||
}
|
||||
// camera tp x y z [h] [v]
|
||||
if (args.size() < 5) {
|
||||
output("Usage: camera tp <x> <y> <z> [h_angle] [v_angle] | camera tp @<ref>");
|
||||
return;
|
||||
}
|
||||
const auto pos = ParseXYZ(args, 2);
|
||||
if (!pos) { output("Error: invalid coordinates"); return; }
|
||||
s16 h = 0, v = 0;
|
||||
cameraToAngles(cam, h, v);
|
||||
if (args.size() >= 6) {
|
||||
const auto hv = ParseLong(args[5]);
|
||||
if (!hv) { output("Error: invalid h_angle"); return; }
|
||||
h = (s16)*hv;
|
||||
}
|
||||
if (args.size() >= 7) {
|
||||
const auto vv = ParseLong(args[6]);
|
||||
if (!vv) { output("Error: invalid v_angle"); return; }
|
||||
v = (s16)*vv;
|
||||
}
|
||||
const cXyz dir = anglesToDir(h, v);
|
||||
const cXyz center = cXyz(pos->x + dir.x * kCamTargetDist,
|
||||
pos->y + dir.y * kCamTargetDist,
|
||||
pos->z + dir.z * kCamTargetDist);
|
||||
s_cameraFly.active = true; s_cameraFly.duration = 0.0f;
|
||||
s_cameraFly.startEye = s_cameraFly.endEye = *pos;
|
||||
s_cameraFly.startCenter = s_cameraFly.endCenter = center;
|
||||
output(fmt::format(FMT_STRING("Camera teleported to ({:.2f}, {:.2f}, {:.2f})"),
|
||||
pos->x, pos->y, pos->z));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub == "pos") {
|
||||
s16 h = 0, v = 0;
|
||||
cameraToAngles(cam, h, v);
|
||||
const cXyz eye = cam->iEye();
|
||||
output(fmt::format(FMT_STRING("Camera: ({:.2f}, {:.2f}, {:.2f}) h={} v={}"),
|
||||
eye.x, eye.y, eye.z, (int)h, (int)v));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub == "fly") {
|
||||
// camera fly <time> <x> <y> <z> [h] [v] | camera fly <time> @<ref>
|
||||
if (args.size() < 4) {
|
||||
output("Usage: camera fly <time> <x> <y> <z> [h_angle] [v_angle] | camera fly <time> @<ref>");
|
||||
return;
|
||||
}
|
||||
const auto t = ParseFloat(args[2]);
|
||||
if (!t || *t <= 0) { output("Error: invalid time"); return; }
|
||||
const float flyTime = *t;
|
||||
|
||||
cXyz targetPos;
|
||||
s16 h = 0, v = 0;
|
||||
|
||||
if (!args[3].empty() && args[3][0] == '@') {
|
||||
auto* ac = ParseActorArg(args[3], state.foundProcId, output);
|
||||
if (ac == nullptr) { return; }
|
||||
targetPos = ac->current.pos;
|
||||
h = ac->shape_angle.y;
|
||||
v = 0;
|
||||
} else {
|
||||
if (args.size() < 6) {
|
||||
output("Usage: camera fly <time> <x> <y> <z> [h_angle] [v_angle] | camera fly <time> @<ref>");
|
||||
return;
|
||||
}
|
||||
const auto pos = ParseXYZ(args, 3);
|
||||
if (!pos) { output("Error: invalid coordinates"); return; }
|
||||
targetPos = *pos;
|
||||
cameraToAngles(cam, h, v);
|
||||
if (args.size() >= 7) {
|
||||
const auto hv = ParseLong(args[6]); if (!hv) { output("Error: invalid h_angle"); return; } h = (s16)*hv;
|
||||
}
|
||||
if (args.size() >= 8) {
|
||||
const auto vv = ParseLong(args[7]); if (!vv) { output("Error: invalid v_angle"); return; } v = (s16)*vv;
|
||||
}
|
||||
}
|
||||
|
||||
const cXyz dir = anglesToDir(h, v);
|
||||
const cXyz endCenter = cXyz(targetPos.x + dir.x * kCamTargetDist,
|
||||
targetPos.y + dir.y * kCamTargetDist,
|
||||
targetPos.z + dir.z * kCamTargetDist);
|
||||
|
||||
s_cameraFly.active = true;
|
||||
s_cameraFly.startEye = cam->iEye();
|
||||
s_cameraFly.startCenter = cam->iCenter();
|
||||
s_cameraFly.endEye = targetPos;
|
||||
s_cameraFly.endCenter = endCenter;
|
||||
s_cameraFly.duration = flyTime;
|
||||
s_cameraFly.elapsed = 0.0f;
|
||||
output(fmt::format(FMT_STRING("Camera flying to ({:.2f}, {:.2f}, {:.2f}) over {:.1f}s"),
|
||||
targetPos.x, targetPos.y, targetPos.z, flyTime));
|
||||
return;
|
||||
}
|
||||
|
||||
output("Usage: camera detach | attach | tp ... | fly ... | pos");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "echo") {
|
||||
if (args.size() >= 2 && args[1] == "off") {
|
||||
state.echoEnabled = false;
|
||||
output("Echo disabled");
|
||||
} else if (args.size() >= 2 && args[1] == "on") {
|
||||
state.echoEnabled = true;
|
||||
output("Echo enabled");
|
||||
} else {
|
||||
output(state.echoEnabled ? "Echo: on" : "Echo: off");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "transform") {
|
||||
auto* player = requirePlayer();
|
||||
if (player == nullptr) { return; }
|
||||
player->procCoMetamorphoseInit();
|
||||
output("Transforming");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "angle") {
|
||||
auto* player = requirePlayer();
|
||||
if (player == nullptr) { return; }
|
||||
if (args.size() >= 2) {
|
||||
const auto a = ParseLong(args[1]);
|
||||
if (!a) { output("Error: invalid angle"); return; }
|
||||
player->shape_angle.y = (s16)*a;
|
||||
output(fmt::format(FMT_STRING("Angle set to {}"), (int)(s16)*a));
|
||||
} else {
|
||||
output(fmt::format(FMT_STRING("Angle: {}"), (int)player->shape_angle.y));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "pos") {
|
||||
auto* player = requirePlayer();
|
||||
if (player == nullptr) {
|
||||
return;
|
||||
}
|
||||
output(fmt::format(FMT_STRING("pos: {:.4f} {:.4f} {:.4f}"), player->current.pos.x,
|
||||
player->current.pos.y, player->current.pos.z));
|
||||
output(
|
||||
fmt::format(FMT_STRING("stage: {} room: {} entry: {}"), dComIfGp_getStartStageName(),
|
||||
(int)player->current.roomNo, (int)dComIfGp_getStartStagePoint()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == "help") {
|
||||
output("@<ref> = @<procId> | @found | @link");
|
||||
output("");
|
||||
output("angle [value] Get or set Link's facing angle");
|
||||
output("camera detach | attach Detach or reattach camera");
|
||||
output("camera pos Print camera position and angles");
|
||||
output("camera tp <x> <y> <z> [h] [v] Teleport camera (h/v are s16 angles)");
|
||||
output("camera tp @<ref> Teleport camera to actor");
|
||||
output("camera fly <t> <x> <y> <z> [h] [v] Fly camera over t seconds (h/v are s16 angles)");
|
||||
output("camera fly <t> @<ref> Fly camera to actor");
|
||||
output("ebf [0-255] Get or set cDmr_SkipInfo");
|
||||
output("echo on | off Enable or disable command echo");
|
||||
output("find <id|name> [n=1] Store nth actor as @found");
|
||||
output("freeze [@<ref>] Freeze globally, or freeze actor");
|
||||
output("heal [amount] Heal to max, or by relative amount");
|
||||
output("kill Set Link health to 0");
|
||||
output("kill @<ref> Delete proc");
|
||||
output("killall <id|name> Delete all actors of a type");
|
||||
output("list [id|name] List actors in scene");
|
||||
output("pos Print player position and stage");
|
||||
output("rate [hz] Get or set sim rate (1-480, default 30)");
|
||||
output("reset Soft reset");
|
||||
output("rupees [amount] Get or set rupee count");
|
||||
output("spawn <id|name> [params] [x y z] [angle] Spawn actor");
|
||||
output("time [0-360] Get or set time of day");
|
||||
output("tp <x> <y> <z> [angle] Teleport Link to coords");
|
||||
output("tp @<ref> Teleport Link to actor");
|
||||
output("tp @<ref> <x> <y> <z> [angle] Move actor to coords");
|
||||
output("tp @<ref> @<ref> Move actor to actor");
|
||||
output("transform Force transform");
|
||||
output("unfreeze [@<ref>] Unfreeze globally, or unfreeze actor");
|
||||
output("warp <stage> <point> <room> [layer] Warp to stage");
|
||||
return;
|
||||
}
|
||||
|
||||
output(fmt::format(FMT_STRING("Unknown command '{}' (try 'help')"), cmd));
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk {
|
||||
|
||||
using CommandOutput = std::function<void(std::string)>;
|
||||
|
||||
struct CommandState {
|
||||
std::vector<std::string> history;
|
||||
unsigned int foundProcId = 0;
|
||||
std::string lastWarpStage;
|
||||
bool echoEnabled = true;
|
||||
};
|
||||
|
||||
// Execute a single command line. Calls output() for every line of response.
|
||||
// Manages history internally; callers need only hold a CommandState.
|
||||
void runCommand(std::string_view cmdLine, CommandState& state, const CommandOutput& output);
|
||||
|
||||
// Per-sim-tick update for camera fly animation.
|
||||
void processCameraCommands();
|
||||
|
||||
// Returns true when the dev camera is detached (suppresses the game's camera update).
|
||||
bool isCameraDetached();
|
||||
|
||||
} // namespace dusk
|
||||
@@ -50,6 +50,16 @@ void reset() {
|
||||
s_simTickActive = false;
|
||||
}
|
||||
|
||||
void set_sim_rate(float hz) {
|
||||
const float maximumHz = aurora::time::kMaximumTimeScale / kSimPeriod;
|
||||
aurora::time::set_scale(std::clamp(hz, 1.0f, maximumHz) * kSimPeriod);
|
||||
reset();
|
||||
}
|
||||
|
||||
float get_sim_rate() {
|
||||
return aurora::time::scale() / kSimPeriod;
|
||||
}
|
||||
|
||||
const FrameTiming& advance() {
|
||||
const auto nativeNow = native_clock::now();
|
||||
const auto gameNow = game_clock::now();
|
||||
|
||||
@@ -28,4 +28,8 @@ float sample_interpolation_step();
|
||||
|
||||
float consume_interval(const void* consumer);
|
||||
|
||||
// Sets the effective simulation rate through the game clock time scale.
|
||||
void set_sim_rate(float hz);
|
||||
float get_sim_rate();
|
||||
|
||||
} // namespace dusk::game_clock
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
#include "dusk/game_combos.h"
|
||||
|
||||
#include "SSystem/SComponent/c_API_controller_pad.h"
|
||||
#include "SSystem/SComponent/c_xyz.h"
|
||||
#include "d/actor/d_a_alink.h"
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "m_Do/m_Do_controller_pad.h"
|
||||
|
||||
namespace dusk {
|
||||
namespace {
|
||||
|
||||
cXyz s_savedTeleportPos{};
|
||||
s16 s_savedTeleportAngle = 0;
|
||||
bool s_hasTeleportPos = false;
|
||||
|
||||
static daAlink_c* getPlayer() {
|
||||
return (daAlink_c*)dComIfGp_getPlayer(0);
|
||||
}
|
||||
|
||||
static void consumeButtons(u16 mask) {
|
||||
mDoCPd_c::getCpadInfo(PAD_1).mPressedButtonFlags &= ~mask;
|
||||
mDoCPd_c::getCpadInfo(PAD_1).mButtonFlags &= ~mask;
|
||||
}
|
||||
|
||||
// Table: holdMask, trigMask, strict, condition, action, consumeMask, exclusive
|
||||
static const GameCombo kCombos[] = {
|
||||
// Move Link (L+R+Y), pass-through, non-exclusive
|
||||
{
|
||||
PAD_TRIGGER_R | PAD_TRIGGER_L,
|
||||
PAD_BUTTON_Y,
|
||||
false,
|
||||
[] { return (bool)getSettings().game.enableMoveLinkCombo; },
|
||||
[] { getTransientSettings().moveLinkActive = !getTransientSettings().moveLinkActive; },
|
||||
0,
|
||||
false,
|
||||
},
|
||||
// Quick Transform (R+Y, strictly only R held)
|
||||
{
|
||||
PAD_TRIGGER_R,
|
||||
PAD_BUTTON_Y,
|
||||
true,
|
||||
[] { return getPlayer() != nullptr; },
|
||||
[] { getPlayer()->handleQuickTransform(); },
|
||||
PAD_BUTTON_Y,
|
||||
false,
|
||||
},
|
||||
// Wolf Howl (R+X)
|
||||
{
|
||||
PAD_TRIGGER_R,
|
||||
PAD_BUTTON_X,
|
||||
false,
|
||||
[] { return getPlayer() != nullptr; },
|
||||
[] { getPlayer()->handleWolfHowl(); },
|
||||
PAD_BUTTON_X,
|
||||
false,
|
||||
},
|
||||
// Teleport save (R+D-pad Up), consumes D-pad Up, exclusive
|
||||
{
|
||||
PAD_TRIGGER_R,
|
||||
PAD_BUTTON_UP,
|
||||
false,
|
||||
[] { return getSettings().game.enableTeleportCombo && getPlayer() != nullptr; },
|
||||
[] {
|
||||
auto* p = getPlayer();
|
||||
s_savedTeleportPos = p->current.pos;
|
||||
s_savedTeleportAngle = p->shape_angle.y;
|
||||
s_hasTeleportPos = true;
|
||||
},
|
||||
PAD_BUTTON_UP,
|
||||
true,
|
||||
},
|
||||
// Teleport load (R+D-pad Down), consumes D-pad Down, exclusive
|
||||
{
|
||||
PAD_TRIGGER_R,
|
||||
PAD_BUTTON_DOWN,
|
||||
false,
|
||||
[] {
|
||||
return getSettings().game.enableTeleportCombo && s_hasTeleportPos &&
|
||||
getPlayer() != nullptr;
|
||||
},
|
||||
[] {
|
||||
auto* p = getPlayer();
|
||||
p->current.pos = s_savedTeleportPos;
|
||||
p->shape_angle.y = s_savedTeleportAngle;
|
||||
p->mNormalSpeed = 0.0f;
|
||||
},
|
||||
PAD_BUTTON_DOWN,
|
||||
true,
|
||||
},
|
||||
// Moon Jump (R+A, hold), continuous, pass-through, non-exclusive
|
||||
{
|
||||
PAD_TRIGGER_R | PAD_BUTTON_A,
|
||||
0,
|
||||
false,
|
||||
[] { return getSettings().game.moonJump && getPlayer() != nullptr; },
|
||||
[] { getPlayer()->speed.y = 56.0f; },
|
||||
0,
|
||||
false,
|
||||
},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void processGameCombos() {
|
||||
if (!getSettings().game.enableMoveLinkCombo) {
|
||||
getTransientSettings().moveLinkActive = false;
|
||||
}
|
||||
|
||||
const u16 held = mDoCPd_c::getHold(PAD_1) & 0xFFFF;
|
||||
const u16 trig = mDoCPd_c::getTrig(PAD_1) & 0xFFFF;
|
||||
|
||||
for (const auto& combo : kCombos) {
|
||||
if ((held & combo.holdMask) != combo.holdMask) {
|
||||
continue;
|
||||
}
|
||||
if (combo.strict && (held & ~combo.trigMask) != combo.holdMask) {
|
||||
continue;
|
||||
}
|
||||
if (combo.trigMask != 0 && !(trig & combo.trigMask)) {
|
||||
continue;
|
||||
}
|
||||
if (combo.condition != nullptr && !combo.condition()) {
|
||||
continue;
|
||||
}
|
||||
combo.action();
|
||||
if (combo.consumeMask != 0) {
|
||||
consumeButtons(combo.consumeMask);
|
||||
}
|
||||
if (combo.exclusive) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <dolphin/types.h>
|
||||
|
||||
namespace dusk {
|
||||
|
||||
struct GameCombo {
|
||||
using ConditionFn = bool(*)();
|
||||
using ActionFn = void(*)();
|
||||
|
||||
u16 holdMask; // all of these must be held (getHold)
|
||||
u16 trigMask; // at least one must be newly triggered (getTrig); 0 = fires every frame while holdMask is held
|
||||
bool strict; // if true: (held & ~trigMask) must equal holdMask exactly, no extra buttons held
|
||||
ConditionFn condition; // extra game-state guard; nullptr = no extra check
|
||||
ActionFn action; // runs when the combo is fired
|
||||
u16 consumeMask; // buttons to clear after firing; 0 = pass-through
|
||||
bool exclusive; // stop evaluating future combos if this fires
|
||||
};
|
||||
|
||||
void processGameCombos();
|
||||
|
||||
} // namespace dusk
|
||||
@@ -64,10 +64,15 @@ namespace dusk {
|
||||
if (eventRunning) {
|
||||
ImGui::SetTooltip("Cannot enable while paused or during an active event.");
|
||||
} else {
|
||||
ImGui::SetTooltip("Detach camera and fly freely.\n"
|
||||
"WASD/Arrows/Left stick: move, Mouse/C-stick: look\n"
|
||||
"Ctrl/L: down, Space/R: up, Shift/Z: fast\n"
|
||||
"Q Key/Y: roll left, R Key/X: roll right");
|
||||
ImGui::SetTooltip("Detach camera and fly freely.\n\n"
|
||||
"Controls:\n"
|
||||
"WASD/Arrows/Left stick - Move\n"
|
||||
"Right Click+Mouse/C-stick - Look\n"
|
||||
"Ctrl/L - Down\n"
|
||||
"Space/R - Up\n"
|
||||
"Shift/Z - Faster\n"
|
||||
"Q Key/Y - Roll Left\n"
|
||||
"R Key/X - Roll Right");
|
||||
}
|
||||
}
|
||||
if (eventRunning) {
|
||||
|
||||
@@ -264,13 +264,6 @@ namespace dusk {
|
||||
}
|
||||
previousTurboActive = turboActive;
|
||||
previousSlowActive = slowDown;
|
||||
|
||||
if (frame_interp::get_ui_tick_pending() && mDoMain::developmentMode == 1 && (mDoCPd_c::getHold(PAD_1) & (PAD_TRIGGER_R | PAD_TRIGGER_L)) == (PAD_TRIGGER_R | PAD_TRIGGER_L) && mDoCPd_c::getTrigY(PAD_1)) {
|
||||
getTransientSettings().moveLinkActive = !getTransientSettings().moveLinkActive;
|
||||
}
|
||||
if (mDoMain::developmentMode != 1) {
|
||||
getTransientSettings().moveLinkActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiConsole::PreDraw() {
|
||||
|
||||
@@ -89,6 +89,26 @@ namespace dusk {
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
auto& triggerView = getTransientSettings().triggerView;
|
||||
if (ImGui::BeginMenu("Trigger View")) {
|
||||
ImGui::Checkbox("Load Zones", &triggerView.loadZones);
|
||||
ImGui::Checkbox("Event Areas", &triggerView.eventAreas);
|
||||
ImGui::Checkbox("Event Tags", &triggerView.eventTags);
|
||||
ImGui::Checkbox("Switch Areas", &triggerView.switchAreas);
|
||||
ImGui::Checkbox("Midna Stops", &triggerView.midnaStops);
|
||||
ImGui::Checkbox("Twilight Gates", &triggerView.twilightGates);
|
||||
ImGui::Checkbox("Checkpoints", &triggerView.checkpoints);
|
||||
ImGui::Checkbox("Paths", &triggerView.paths);
|
||||
ImGui::Separator();
|
||||
ImGui::Checkbox("Transform Distances", &triggerView.transformDists);
|
||||
ImGui::Checkbox("Attention Distances", &triggerView.attentionDists);
|
||||
ImGui::Checkbox("Purple Mist Avoid", &triggerView.purpleMistAvoid);
|
||||
ImGui::Checkbox("Leever Ranges", &triggerView.leevers);
|
||||
ImGui::Separator();
|
||||
ImGui::SliderFloat("Opacity##triggers", &triggerView.opacity, 0.0f, 100.0f);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (!dusk::IsGameLaunched) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ constexpr std::string_view kStubFragments[] = {
|
||||
"Unimplemented: BP register"sv,
|
||||
"Unhandled BP register"sv,
|
||||
"Unhandled XF register"sv,
|
||||
"Unhandled XF memory write"sv,
|
||||
"but selective updates are not implemented"sv,
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ struct ItemCheckResult {
|
||||
uint32_t tag = 0;
|
||||
uint8_t itemNo = 0;
|
||||
uint8_t displayItemNo = 0;
|
||||
bool was_resolved = false;
|
||||
};
|
||||
|
||||
ItemCheckResolution item_check_resolve(const char* name, uint8_t itemNo, fopAc_ac_c* giver);
|
||||
@@ -57,6 +58,13 @@ uint32_t item_check_message(uint16_t group, uint32_t messageId);
|
||||
bool item_give_queue_dispatching();
|
||||
uint32_t item_give_queue_take_tag();
|
||||
|
||||
struct BossItemActorSpeeds {
|
||||
f32 f;
|
||||
f32 y;
|
||||
};
|
||||
void set_boss_item_actor_speeds(f32 f, f32 y);
|
||||
BossItemActorSpeeds get_boss_item_actor_speeds();
|
||||
|
||||
namespace detail {
|
||||
struct CommittedCheck {
|
||||
uint8_t vanillaItem = 0;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#include "item.hpp"
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
static BossItemActorSpeeds bossItemActorSpeeds{};
|
||||
void set_boss_item_actor_speeds(f32 f, f32 y) {
|
||||
bossItemActorSpeeds.f = f;
|
||||
bossItemActorSpeeds.y = y;
|
||||
}
|
||||
|
||||
BossItemActorSpeeds get_boss_item_actor_speeds() {
|
||||
return bossItemActorSpeeds;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -120,6 +120,12 @@ constexpr std::array kMessageChecks{
|
||||
.vanillaItem = dItemNo_HORSE_FLUTE_e,
|
||||
.enqueueAtDisplay = false,
|
||||
},
|
||||
MessageCheck{
|
||||
.group = 2,
|
||||
.messageId = 6531,
|
||||
.name = ITEM_CHECK_SHAD_DOMINION_ROD,
|
||||
.vanillaItem = dItemNo_COPY_ROD_2_e,
|
||||
}
|
||||
};
|
||||
|
||||
std::unordered_map<LoadedMod*, ModItemChecks> s_modChecks;
|
||||
@@ -152,7 +158,7 @@ std::string poe_check_name(uint8_t bitNo) {
|
||||
}
|
||||
|
||||
std::string shop_check_name(uint8_t itemNo) {
|
||||
return fmt::format("shop:{}:{}", current_stage_name(), itemNo);
|
||||
return fmt::format("shop:{}:{}:{}", current_stage_name(), dStage_roomControl_c::getStayNo(), itemNo);
|
||||
}
|
||||
|
||||
std::string bug_check_name(uint8_t insectId) {
|
||||
@@ -229,6 +235,7 @@ ItemCheckResolution item_check_resolve(const char* name, uint8_t itemNo, fopAc_a
|
||||
.vanilla_item = itemNo,
|
||||
.current_item = itemNo,
|
||||
.current_display_item = itemNo,
|
||||
.was_resolved = false,
|
||||
};
|
||||
for (const auto& resolve : resolves) {
|
||||
if (!resolve.mod->active) {
|
||||
@@ -237,6 +244,7 @@ ItemCheckResolution item_check_resolve(const char* name, uint8_t itemNo, fopAc_a
|
||||
if (resolve.fixedValue) {
|
||||
info.current_item = resolve.itemNo;
|
||||
info.current_display_item = resolve.itemNo;
|
||||
info.was_resolved = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -251,6 +259,7 @@ ItemCheckResolution item_check_resolve(const char* name, uint8_t itemNo, fopAc_a
|
||||
}
|
||||
info.current_item = resolution.item;
|
||||
info.current_display_item = resolution.display_item;
|
||||
info.was_resolved = true;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
fail_mod(*resolve.mod, MOD_ERROR,
|
||||
@@ -263,6 +272,7 @@ ItemCheckResolution item_check_resolve(const char* name, uint8_t itemNo, fopAc_a
|
||||
return {
|
||||
.item = info.current_item,
|
||||
.display_item = info.current_display_item,
|
||||
.was_resolved = info.was_resolved,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -265,6 +265,7 @@ ItemCheckResult item_check_commit(uint32_t giveTag, uint8_t itemNo, fopAc_ac_c*
|
||||
.tag = giveTag,
|
||||
.itemNo = committed->resolution.item,
|
||||
.displayItemNo = committed->resolution.display_item,
|
||||
.was_resolved = committed->resolution.was_resolved,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -274,6 +275,7 @@ ItemCheckResult item_check_commit(uint32_t giveTag, uint8_t itemNo, fopAc_ac_c*
|
||||
.tag = giveTag,
|
||||
.itemNo = committed.resolution.item,
|
||||
.displayItemNo = committed.resolution.display_item,
|
||||
.was_resolved = committed.resolution.was_resolved,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,6 @@
|
||||
#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) {
|
||||
@@ -39,11 +31,7 @@ std::string pl_dlerror() {
|
||||
#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);
|
||||
return dlopen(p.c_str(), RTLD_LAZY | RTLD_LOCAL);
|
||||
}
|
||||
void* pl_dlsym(void* h, const char* name) {
|
||||
return dlsym(h, name);
|
||||
|
||||
@@ -75,12 +75,12 @@ __declspec(allocate(".symdbh"))
|
||||
#if defined(__clang__)
|
||||
__attribute__((used))
|
||||
#endif
|
||||
constinit const SymdbDescriptor s_symdbDescriptor{kDescriptorMagic, 0, 0};
|
||||
constinit SymdbDescriptor s_symdbDescriptor{kDescriptorMagic, 0, 0};
|
||||
#elif defined(__APPLE__)
|
||||
__attribute__((section("__DATA,__symdbh"), used)) constinit const SymdbDescriptor
|
||||
s_symdbDescriptor{kDescriptorMagic, 0, 0};
|
||||
__attribute__((section("__DATA,__symdbh"), used)) constinit SymdbDescriptor s_symdbDescriptor{
|
||||
kDescriptorMagic, 0, 0};
|
||||
#else
|
||||
__attribute__((section("symdbh"), used)) constinit const SymdbDescriptor s_symdbDescriptor{
|
||||
__attribute__((section("symdbh"), used)) constinit SymdbDescriptor s_symdbDescriptor{
|
||||
kDescriptorMagic, 0, 0};
|
||||
#endif
|
||||
|
||||
@@ -92,7 +92,7 @@ struct State {
|
||||
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;
|
||||
std::vector<std::pair<uint64_t, uint32_t>> inlineSites;
|
||||
bool loaded = false;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,703 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include <aurora/lib/window.hpp>
|
||||
#include <borealis/file_select.hpp>
|
||||
#include <borealis/io.hpp>
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/file.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
constexpr borealis::Log Log{"dusk::mods::file"};
|
||||
|
||||
static_assert(static_cast<int>(FILE_OPEN_READ) == static_cast<int>(borealis::io::File::Mode::Read));
|
||||
static_assert(
|
||||
static_cast<int>(FILE_OPEN_TRUNCATE) == static_cast<int>(borealis::io::File::Mode::Truncate));
|
||||
static_assert(
|
||||
static_cast<int>(FILE_OPEN_APPEND) == static_cast<int>(borealis::io::File::Mode::Append));
|
||||
|
||||
SlotMap<borealis::io::File> s_streams;
|
||||
std::unordered_map<void*, LoadedMod*> s_buffers;
|
||||
std::unordered_map<LoadedMod*, std::string> s_joinResults;
|
||||
ConfigVar<std::string> s_pickerOverride{"file.pickerOverride", ""};
|
||||
|
||||
struct PendingPick {
|
||||
LoadedMod* owner = nullptr;
|
||||
FilePickFn callback = nullptr;
|
||||
void* userData = nullptr;
|
||||
std::optional<std::string> overrideLocation;
|
||||
std::optional<std::string> exportSource;
|
||||
};
|
||||
|
||||
std::shared_ptr<PendingPick> s_pendingPick;
|
||||
|
||||
ModResult map_status(borealis::io::Status status) {
|
||||
switch (status) {
|
||||
case borealis::io::Status::Ok:
|
||||
return MOD_OK;
|
||||
case borealis::io::Status::NotFound:
|
||||
return MOD_UNAVAILABLE;
|
||||
case borealis::io::Status::Unsupported:
|
||||
return MOD_UNSUPPORTED;
|
||||
case borealis::io::Status::AlreadyExists:
|
||||
return MOD_CONFLICT;
|
||||
case borealis::io::Status::Failed:
|
||||
default:
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
ModResult map_picker_status(borealis::file_select::Status status) {
|
||||
switch (status) {
|
||||
case borealis::file_select::Status::Selected:
|
||||
return MOD_OK;
|
||||
case borealis::file_select::Status::Canceled:
|
||||
return MOD_UNAVAILABLE;
|
||||
case borealis::file_select::Status::Unsupported:
|
||||
return MOD_UNSUPPORTED;
|
||||
case borealis::file_select::Status::Busy:
|
||||
return MOD_CONFLICT;
|
||||
case borealis::file_select::Status::Failed:
|
||||
default:
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
void invoke_pick(const std::shared_ptr<PendingPick>& pending, ModResult status,
|
||||
const std::vector<std::string>& locations, const std::string& error) {
|
||||
if (s_pendingPick == pending) {
|
||||
s_pendingPick.reset();
|
||||
}
|
||||
auto* owner = pending->owner;
|
||||
const auto callback = pending->callback;
|
||||
if (owner == nullptr || callback == nullptr || !owner->active) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<const char*> rawLocations;
|
||||
rawLocations.reserve(locations.size());
|
||||
for (const auto& location : locations) {
|
||||
rawLocations.push_back(location.c_str());
|
||||
}
|
||||
try {
|
||||
callback(owner->context.get(), status, rawLocations.empty() ? nullptr : rawLocations.data(),
|
||||
static_cast<uint32_t>(rawLocations.size()), error.c_str(), pending->userData);
|
||||
} catch (const std::exception& exception) {
|
||||
fail_mod(*owner, MOD_ERROR,
|
||||
std::string{"exception in file picker callback: "} + exception.what());
|
||||
} catch (...) {
|
||||
fail_mod(*owner, MOD_ERROR, "unknown exception in file picker callback");
|
||||
}
|
||||
}
|
||||
|
||||
bool valid_pick_options(const FilePickOptions* options) {
|
||||
if (options == nullptr || options->struct_size < sizeof(FilePickOptions) ||
|
||||
(options->filter_count != 0 && options->filters == nullptr))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = 0; i < options->filter_count; ++i) {
|
||||
if (options->filters[i].name == nullptr || options->filters[i].pattern == nullptr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ModResult begin_pick(ModContext* context, const FilePickOptions* options, FilePickFn callback,
|
||||
void* userData, bool folder) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || callback == nullptr || !valid_pick_options(options)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (s_pendingPick != nullptr || borealis::file_select::busy()) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
|
||||
auto pending = std::make_shared<PendingPick>(PendingPick{
|
||||
.owner = mod,
|
||||
.callback = callback,
|
||||
.userData = userData,
|
||||
});
|
||||
const auto& overrideLocation = s_pickerOverride.getValue();
|
||||
if (!overrideLocation.empty()) {
|
||||
pending->overrideLocation = overrideLocation;
|
||||
s_pendingPick = std::move(pending);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
s_pendingPick = pending;
|
||||
const std::string defaultLocation =
|
||||
options->default_location != nullptr ? options->default_location : "";
|
||||
if (folder) {
|
||||
borealis::file_select::open_folder(
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.defaultLocation = defaultLocation,
|
||||
},
|
||||
[pending](borealis::file_select::Result result) {
|
||||
invoke_pick(
|
||||
pending, map_picker_status(result.status), result.locations, result.message);
|
||||
});
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
std::vector<borealis::file_select::Filter> filters;
|
||||
filters.reserve(options->filter_count);
|
||||
for (uint32_t i = 0; i < options->filter_count; ++i) {
|
||||
filters.push_back({options->filters[i].name, options->filters[i].pattern});
|
||||
}
|
||||
borealis::file_select::open_file(
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.filters = std::move(filters),
|
||||
.defaultLocation = defaultLocation,
|
||||
},
|
||||
[pending](borealis::file_select::Result result) {
|
||||
invoke_pick(
|
||||
pending, map_picker_status(result.status), result.locations, result.message);
|
||||
});
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult begin_export(ModContext* context, const char* sourceLocation, const char* suggestedName,
|
||||
FilePickFn callback, void* userData) {
|
||||
auto* mod = mod_from_context(context);
|
||||
const std::string_view name = suggestedName != nullptr ? suggestedName : "";
|
||||
if (mod == nullptr || sourceLocation == nullptr || callback == nullptr || name.empty() ||
|
||||
name == "." || name == ".." || name.find_first_of("/\\") != std::string_view::npos)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (s_pendingPick != nullptr || borealis::file_select::busy()) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
const auto available = borealis::io::check(sourceLocation);
|
||||
if (available != borealis::io::Status::Ok) {
|
||||
return map_status(available);
|
||||
}
|
||||
|
||||
auto pending = std::make_shared<PendingPick>(PendingPick{
|
||||
.owner = mod,
|
||||
.callback = callback,
|
||||
.userData = userData,
|
||||
});
|
||||
const auto& overrideLocation = s_pickerOverride.getValue();
|
||||
if (!overrideLocation.empty()) {
|
||||
pending->overrideLocation = overrideLocation;
|
||||
pending->exportSource = sourceLocation;
|
||||
s_pendingPick = std::move(pending);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
borealis::file_select::ExportOptions options{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.sourceLocation = sourceLocation,
|
||||
.suggestedName = suggestedName,
|
||||
};
|
||||
s_pendingPick = pending;
|
||||
try {
|
||||
borealis::file_select::export_file(
|
||||
std::move(options), [pending](borealis::file_select::Result result) {
|
||||
invoke_pick(
|
||||
pending, map_picker_status(result.status), result.locations, result.message);
|
||||
});
|
||||
} catch (...) {
|
||||
if (s_pendingPick == pending) {
|
||||
s_pendingPick.reset();
|
||||
}
|
||||
throw;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult file_pick_file(
|
||||
ModContext* context, const FilePickOptions* options, FilePickFn callback, void* userData) {
|
||||
return begin_pick(context, options, callback, userData, false);
|
||||
}
|
||||
|
||||
ModResult file_pick_folder(
|
||||
ModContext* context, const FilePickOptions* options, FilePickFn callback, void* userData) {
|
||||
return begin_pick(context, options, callback, userData, true);
|
||||
}
|
||||
|
||||
ModResult file_export_file(ModContext* context, const char* sourceLocation,
|
||||
const char* suggestedName, FilePickFn callback, void* userData) {
|
||||
try {
|
||||
return begin_export(context, sourceLocation, suggestedName, callback, userData);
|
||||
} catch (...) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
ModResult file_display_name(
|
||||
ModContext* context, const char* location, char* buffer, uint32_t bufferSize) {
|
||||
if (mod_from_context(context) == nullptr || location == nullptr || buffer == nullptr ||
|
||||
bufferSize == 0)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const std::string name = borealis::io::display_name(location);
|
||||
if (name.size() + 1 > bufferSize) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
std::memcpy(buffer, name.c_str(), name.size() + 1);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult file_check(ModContext* context, const char* location) {
|
||||
if (mod_from_context(context) == nullptr || location == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return map_status(borealis::io::check(location));
|
||||
}
|
||||
|
||||
ModResult file_open(
|
||||
ModContext* context, const char* location, FileOpenMode mode, FileStreamHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || location == nullptr || outHandle == nullptr || mode < FILE_OPEN_READ ||
|
||||
mode > FILE_OPEN_APPEND)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
try {
|
||||
const auto ioMode = static_cast<borealis::io::File::Mode>(mode);
|
||||
if (mode != FILE_OPEN_READ) {
|
||||
const auto available = borealis::io::check(location);
|
||||
if (available != borealis::io::Status::Ok) {
|
||||
return map_status(available);
|
||||
}
|
||||
}
|
||||
auto result = borealis::io::open(location, ioMode);
|
||||
if (result.status != borealis::io::Status::Ok) {
|
||||
Log.warn("[{}] open '{}' failed: {}", mod->metadata.id,
|
||||
borealis::io::display_name(location), result.message);
|
||||
return map_status(result.status);
|
||||
}
|
||||
*outHandle = s_streams.emplace(*mod, std::move(result.file));
|
||||
return MOD_OK;
|
||||
} catch (...) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
borealis::io::File* find_stream(ModContext* context, FileStreamHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* entry = s_streams.find_owned(handle, *mod);
|
||||
return entry != nullptr ? &entry->value : nullptr;
|
||||
}
|
||||
|
||||
ModResult file_size(ModContext* context, FileStreamHandle handle, uint64_t* outSize) {
|
||||
if (outSize == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outSize = 0;
|
||||
auto* file = find_stream(context, handle);
|
||||
if (file == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outSize = file->size();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult file_read(ModContext* context, FileStreamHandle handle, void* buffer, uint64_t length,
|
||||
uint64_t* outRead) {
|
||||
if (outRead == nullptr || (buffer == nullptr && length != 0)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outRead = 0;
|
||||
auto* file = find_stream(context, handle);
|
||||
if (file == nullptr || file->writable()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outRead = file->read(buffer, length);
|
||||
return file->error().empty() ? MOD_OK : MOD_ERROR;
|
||||
}
|
||||
|
||||
ModResult file_seek(ModContext* context, FileStreamHandle handle, uint64_t offset) {
|
||||
auto* file = find_stream(context, handle);
|
||||
if (file == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return file->seek(offset) ? MOD_OK : MOD_ERROR;
|
||||
}
|
||||
|
||||
ModResult file_close(ModContext* context, FileStreamHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auto entry = s_streams.take_owned(handle, *mod);
|
||||
if (!entry.has_value()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return entry->value.close() ? MOD_OK : MOD_ERROR;
|
||||
}
|
||||
|
||||
ModResult file_write(
|
||||
ModContext* context, FileStreamHandle handle, const void* buffer, uint64_t length) {
|
||||
if ((buffer == nullptr && length != 0) || length > std::numeric_limits<size_t>::max()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auto* file = find_stream(context, handle);
|
||||
if (file == nullptr || !file->writable()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const auto bytes =
|
||||
std::span{static_cast<const std::byte*>(buffer), static_cast<size_t>(length)};
|
||||
return file->write(bytes) ? MOD_OK : MOD_ERROR;
|
||||
}
|
||||
|
||||
ModResult file_flush(ModContext* context, FileStreamHandle handle) {
|
||||
auto* file = find_stream(context, handle);
|
||||
if (file == nullptr || !file->writable()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return file->flush() ? MOD_OK : MOD_ERROR;
|
||||
}
|
||||
|
||||
ModResult file_write_all(ModContext* context, const char* location, const void* data, size_t size) {
|
||||
if (mod_from_context(context) == nullptr || location == nullptr ||
|
||||
(data == nullptr && size != 0))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
try {
|
||||
const auto available = borealis::io::check(location);
|
||||
if (available != borealis::io::Status::Ok) {
|
||||
return map_status(available);
|
||||
}
|
||||
auto opened = borealis::io::open(location, borealis::io::File::Mode::Truncate);
|
||||
if (opened.status != borealis::io::Status::Ok) {
|
||||
return map_status(opened.status);
|
||||
}
|
||||
if (!opened.file.write(std::span{static_cast<const std::byte*>(data), size})) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
return opened.file.close() ? MOD_OK : MOD_ERROR;
|
||||
} catch (...) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
ModResult file_read_all(ModContext* context, const char* location, FileBuffer* outBuffer) {
|
||||
if (outBuffer == nullptr || outBuffer->struct_size < sizeof(FileBuffer)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
outBuffer->data = nullptr;
|
||||
outBuffer->size = 0;
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || location == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
try {
|
||||
auto opened = borealis::io::open(location);
|
||||
if (opened.status != borealis::io::Status::Ok) {
|
||||
return map_status(opened.status);
|
||||
}
|
||||
const uint64_t expectedSize = opened.file.size();
|
||||
if (expectedSize > std::numeric_limits<size_t>::max()) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
std::vector<unsigned char> bytes;
|
||||
bytes.resize(static_cast<size_t>(expectedSize));
|
||||
uint64_t total = 0;
|
||||
while (total < expectedSize) {
|
||||
const uint64_t count = opened.file.read(bytes.data() + total, expectedSize - total);
|
||||
if (count == 0) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
total += count;
|
||||
}
|
||||
if (expectedSize == 0) {
|
||||
unsigned char chunk[64 * 1024];
|
||||
while (true) {
|
||||
const uint64_t count = opened.file.read(chunk, sizeof(chunk));
|
||||
if (count == 0) {
|
||||
if (!opened.file.error().empty()) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
bytes.insert(bytes.end(), chunk, chunk + count);
|
||||
}
|
||||
}
|
||||
if (bytes.empty()) {
|
||||
return MOD_OK;
|
||||
}
|
||||
std::unique_ptr<void, decltype(&std::free)> data{std::malloc(bytes.size()), &std::free};
|
||||
if (!data) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
std::memcpy(data.get(), bytes.data(), bytes.size());
|
||||
s_buffers.emplace(data.get(), mod);
|
||||
outBuffer->data = data.release();
|
||||
outBuffer->size = bytes.size();
|
||||
return MOD_OK;
|
||||
} catch (...) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
void file_free(ModContext* context, FileBuffer* buffer) {
|
||||
if (buffer == nullptr || buffer->struct_size < sizeof(FileBuffer) || buffer->data == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
const auto found = s_buffers.find(buffer->data);
|
||||
if (mod == nullptr || found == s_buffers.end() || found->second != mod) {
|
||||
Log.error("[{}] file free: buffer is not owned by this mod", mod_id_from_context(context));
|
||||
return;
|
||||
}
|
||||
s_buffers.erase(found);
|
||||
std::free(buffer->data);
|
||||
buffer->data = nullptr;
|
||||
buffer->size = 0;
|
||||
}
|
||||
|
||||
ModResult file_list(
|
||||
ModContext* context, const char* folderLocation, FileListFn callback, void* userData) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || folderLocation == nullptr || callback == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
borealis::io::ListResult result;
|
||||
try {
|
||||
result = borealis::io::list(folderLocation);
|
||||
} catch (...) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
if (result.status != borealis::io::Status::Ok) {
|
||||
return map_status(result.status);
|
||||
}
|
||||
try {
|
||||
for (const auto& entry : result.entries) {
|
||||
const FileEntry raw{
|
||||
.name = entry.name.c_str(),
|
||||
.location = entry.location.c_str(),
|
||||
.is_directory = entry.isDirectory,
|
||||
};
|
||||
callback(mod->context.get(), &raw, userData);
|
||||
if (!mod->active) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
callback(mod->context.get(), nullptr, userData);
|
||||
} catch (const std::exception& exception) {
|
||||
fail_mod(
|
||||
*mod, MOD_ERROR, std::string{"exception in file list callback: "} + exception.what());
|
||||
return MOD_ERROR;
|
||||
} catch (...) {
|
||||
fail_mod(*mod, MOD_ERROR, "unknown exception in file list callback");
|
||||
return MOD_ERROR;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult file_join(ModContext* context, const char* folderLocation, const char* relativePath,
|
||||
const char** outLocation) {
|
||||
if (outLocation != nullptr) {
|
||||
*outLocation = nullptr;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || folderLocation == nullptr || relativePath == nullptr ||
|
||||
outLocation == nullptr)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auto result = borealis::io::join(folderLocation, relativePath);
|
||||
if (result.status != borealis::io::Status::Ok) {
|
||||
return map_status(result.status);
|
||||
}
|
||||
auto& saved = s_joinResults[mod];
|
||||
saved = std::move(result.location);
|
||||
*outLocation = saved.c_str();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult file_create_child(
|
||||
ModContext* context, const char* folderLocation, const char* name, const char** outLocation) {
|
||||
if (outLocation != nullptr) {
|
||||
*outLocation = nullptr;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || folderLocation == nullptr || name == nullptr || outLocation == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
try {
|
||||
auto result = borealis::io::create_child(folderLocation, name);
|
||||
if (result.status != borealis::io::Status::Ok) {
|
||||
return map_status(result.status);
|
||||
}
|
||||
auto& saved = s_joinResults[mod];
|
||||
saved = std::move(result.location);
|
||||
*outLocation = saved.c_str();
|
||||
return MOD_OK;
|
||||
} catch (...) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
void file_initialize() {
|
||||
config::Register(s_pickerOverride);
|
||||
}
|
||||
|
||||
bool file_available() {
|
||||
const auto capabilities = borealis::file_select::capabilities();
|
||||
return capabilities.canOpenFile || capabilities.canOpenFolder || capabilities.canExportFile;
|
||||
}
|
||||
|
||||
ModResult copy_picker_override(
|
||||
std::string_view source, std::string_view destination, std::string& error) {
|
||||
try {
|
||||
if (source == destination) {
|
||||
return MOD_OK;
|
||||
}
|
||||
auto input = borealis::io::open(source);
|
||||
if (input.status != borealis::io::Status::Ok) {
|
||||
error = std::move(input.message);
|
||||
return map_status(input.status);
|
||||
}
|
||||
auto output = borealis::io::open(destination, borealis::io::File::Mode::Truncate);
|
||||
if (output.status != borealis::io::Status::Ok) {
|
||||
error = std::move(output.message);
|
||||
return map_status(output.status);
|
||||
}
|
||||
std::array<std::byte, 64 * 1024> buffer{};
|
||||
while (true) {
|
||||
const uint64_t read = input.file.read(buffer.data(), buffer.size());
|
||||
if (read == 0) {
|
||||
if (!input.file.error().empty()) {
|
||||
error = input.file.error();
|
||||
return MOD_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!output.file.write(std::span{buffer.data(), static_cast<size_t>(read)})) {
|
||||
error = output.file.error();
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
if (!output.file.close()) {
|
||||
error = output.file.error();
|
||||
return MOD_ERROR;
|
||||
}
|
||||
return MOD_OK;
|
||||
} catch (const std::exception& exception) {
|
||||
error = exception.what();
|
||||
return MOD_ERROR;
|
||||
} catch (...) {
|
||||
error = "Unable to copy export source";
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
void file_frame_begin() {
|
||||
auto pending = s_pendingPick;
|
||||
if (pending == nullptr || !pending->overrideLocation.has_value()) {
|
||||
return;
|
||||
}
|
||||
if (!pending->exportSource.has_value()) {
|
||||
invoke_pick(pending, MOD_OK, {*pending->overrideLocation}, "");
|
||||
return;
|
||||
}
|
||||
std::string error;
|
||||
const ModResult result =
|
||||
copy_picker_override(*pending->exportSource, *pending->overrideLocation, error);
|
||||
invoke_pick(pending, result,
|
||||
result == MOD_OK ? std::vector<std::string>{*pending->overrideLocation} :
|
||||
std::vector<std::string>{},
|
||||
error);
|
||||
}
|
||||
|
||||
void file_remove_mod(LoadedMod& mod) {
|
||||
const size_t streams = s_streams.erase_all(mod);
|
||||
size_t buffers = 0;
|
||||
std::erase_if(s_buffers, [&](const auto& entry) {
|
||||
if (entry.second != &mod) {
|
||||
return false;
|
||||
}
|
||||
std::free(entry.first);
|
||||
++buffers;
|
||||
return true;
|
||||
});
|
||||
s_joinResults.erase(&mod);
|
||||
if (s_pendingPick != nullptr && s_pendingPick->owner == &mod) {
|
||||
s_pendingPick->owner = nullptr;
|
||||
s_pendingPick->callback = nullptr;
|
||||
s_pendingPick.reset();
|
||||
}
|
||||
if (streams != 0 || buffers != 0) {
|
||||
Log.warn("[{}] reclaimed {} open stream(s) and {} file buffer(s)", mod.metadata.id, streams,
|
||||
buffers);
|
||||
}
|
||||
}
|
||||
|
||||
void file_shutdown() {
|
||||
config::unregister(s_pickerOverride);
|
||||
s_pendingPick.reset();
|
||||
}
|
||||
|
||||
constexpr FileService s_fileService{
|
||||
.header = SERVICE_HEADER(FileService, FILE_SERVICE_MAJOR, FILE_SERVICE_MINOR),
|
||||
.pick_file = file_pick_file,
|
||||
.pick_folder = file_pick_folder,
|
||||
.export_file = file_export_file,
|
||||
.display_name = file_display_name,
|
||||
.check = file_check,
|
||||
.open = file_open,
|
||||
.size = file_size,
|
||||
.read = file_read,
|
||||
.write = file_write,
|
||||
.seek = file_seek,
|
||||
.flush = file_flush,
|
||||
.close = file_close,
|
||||
.read_all = file_read_all,
|
||||
.write_all = file_write_all,
|
||||
.free = file_free,
|
||||
.list = file_list,
|
||||
.join = file_join,
|
||||
.create_child = file_create_child,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_fileModule{
|
||||
.id = FILE_SERVICE_ID,
|
||||
.majorVersion = FILE_SERVICE_MAJOR,
|
||||
.minorVersion = FILE_SERVICE_MINOR,
|
||||
.service = &s_fileService,
|
||||
.available = file_available,
|
||||
.initialize = file_initialize,
|
||||
.modDetached = file_remove_mod,
|
||||
.frameBegin = file_frame_begin,
|
||||
.shutdown = file_shutdown,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,577 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/http.h"
|
||||
|
||||
#include <borealis/http.hpp>
|
||||
#include <borealis/io.hpp>
|
||||
#include <borealis/version.h>
|
||||
#include <fmt/format.h>
|
||||
#include <xxhash.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
constexpr size_t MaxRequestsPerMod = 16;
|
||||
constexpr size_t MaxUrlBytes = 8 * 1024;
|
||||
constexpr size_t MaxHeaders = 64;
|
||||
constexpr size_t MaxHeaderBytes = 16 * 1024;
|
||||
constexpr size_t MaxRequestBodyBytes = 16 * 1024 * 1024;
|
||||
constexpr size_t DefaultResponseBodyBytes = 1024 * 1024;
|
||||
constexpr size_t MaxResponseBodyBytes = 64 * 1024 * 1024;
|
||||
constexpr std::chrono::milliseconds DefaultTimeout{10000};
|
||||
|
||||
struct PendingRequest {
|
||||
HttpCompleteFn callback = nullptr;
|
||||
void* userData = nullptr;
|
||||
borealis::Task<borealis::http::Result> task;
|
||||
std::filesystem::path stagingPath;
|
||||
std::filesystem::path downloadPath;
|
||||
bool completing = false;
|
||||
};
|
||||
|
||||
static_assert(std::is_nothrow_move_constructible_v<PendingRequest>);
|
||||
|
||||
SlotMap<PendingRequest> s_requests;
|
||||
|
||||
bool ascii_iequals(std::string_view left, std::string_view right) {
|
||||
return left.size() == right.size() && std::ranges::equal(left, right, [](char a, char b) {
|
||||
return std::tolower(static_cast<unsigned char>(a)) ==
|
||||
std::tolower(static_cast<unsigned char>(b));
|
||||
});
|
||||
}
|
||||
|
||||
bool is_reserved_header(std::string_view name) {
|
||||
constexpr std::string_view reserved[]{
|
||||
"User-Agent",
|
||||
"Host",
|
||||
"Content-Length",
|
||||
"Connection",
|
||||
"Accept-Encoding",
|
||||
"Range",
|
||||
"If-Range",
|
||||
};
|
||||
return std::ranges::any_of(
|
||||
reserved, [&](std::string_view value) { return ascii_iequals(name, value); });
|
||||
}
|
||||
|
||||
bool valid_header_name(std::string_view name) {
|
||||
constexpr std::string_view separators{"()<>@,;:\\\"/[]?={} \t"};
|
||||
return !name.empty() && std::ranges::all_of(name, [&](unsigned char value) {
|
||||
return value > 32 && value < 127 &&
|
||||
separators.find(static_cast<char>(value)) == std::string_view::npos;
|
||||
});
|
||||
}
|
||||
|
||||
bool valid_url(std::string_view url) {
|
||||
constexpr std::string_view scheme{"https://"};
|
||||
if (!url.starts_with(scheme) || url.size() <= scheme.size() || url.size() > MaxUrlBytes) {
|
||||
return false;
|
||||
}
|
||||
if (std::ranges::any_of(url, [](unsigned char value) { return value <= 32 || value == 127; })) {
|
||||
return false;
|
||||
}
|
||||
const auto authorityEnd = url.find_first_of("/?#", scheme.size());
|
||||
const auto authority = url.substr(scheme.size(), authorityEnd - scheme.size());
|
||||
return !authority.empty();
|
||||
}
|
||||
|
||||
bool declares_http_import(const LoadedMod& mod) {
|
||||
return std::ranges::any_of(
|
||||
mod.manifestInfo.imports, [](const ModManifestInfo::Import& serviceImport) {
|
||||
return serviceImport.id == HTTP_SERVICE_ID;
|
||||
});
|
||||
}
|
||||
|
||||
std::filesystem::path normalized_absolute(const std::filesystem::path& path, std::error_code& ec) {
|
||||
auto result = std::filesystem::absolute(path, ec);
|
||||
return ec ? std::filesystem::path{} : result.lexically_normal();
|
||||
}
|
||||
|
||||
bool path_is_below(const std::filesystem::path& path, const std::filesystem::path& directory) {
|
||||
const auto [directoryEnd, pathPosition] =
|
||||
std::mismatch(directory.begin(), directory.end(), path.begin(), path.end());
|
||||
return directoryEnd == directory.end() && pathPosition != path.end();
|
||||
}
|
||||
|
||||
bool path_is_at_or_below(
|
||||
const std::filesystem::path& path, const std::filesystem::path& directory) {
|
||||
const auto [directoryEnd, pathPosition] =
|
||||
std::mismatch(directory.begin(), directory.end(), path.begin(), path.end());
|
||||
(void)pathPosition;
|
||||
return directoryEnd == directory.end();
|
||||
}
|
||||
|
||||
std::filesystem::path data_root(const LoadedMod& mod, std::error_code& ec) {
|
||||
if (!mod.dataDirUtf8.empty()) {
|
||||
return normalized_absolute(borealis::io::fs_path_from_utf8(mod.dataDirUtf8), ec);
|
||||
}
|
||||
return normalized_absolute(ConfigPath / "mod_data" / mod.metadata.id, ec);
|
||||
}
|
||||
|
||||
std::optional<std::filesystem::path> validate_download_path(
|
||||
const LoadedMod& mod, const char* rawPath) {
|
||||
if (rawPath == nullptr) {
|
||||
return std::filesystem::path{};
|
||||
}
|
||||
const auto supplied = borealis::io::fs_path_from_utf8(rawPath);
|
||||
if (!supplied.is_absolute()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
const auto path = normalized_absolute(supplied, ec);
|
||||
if (ec) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto modRoot = normalized_absolute(mod.dir, ec);
|
||||
if (ec) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto dataRoot = data_root(mod, ec);
|
||||
if (ec) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto stagingRoot = (modRoot / "downloads").lexically_normal();
|
||||
if ((!path_is_below(path, modRoot) && !path_is_below(path, dataRoot)) ||
|
||||
path_is_at_or_below(path, stagingRoot))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
std::filesystem::path staging_path(const LoadedMod& mod, std::string_view url) {
|
||||
const auto& modId = mod.metadata.id;
|
||||
const auto modHash = XXH64(modId.data(), modId.size(), 0);
|
||||
const auto hash = XXH64(url.data(), url.size(), modHash);
|
||||
return mod.dir / "downloads" / fmt::format("{:016x}.part", hash);
|
||||
}
|
||||
|
||||
HttpError map_error(borealis::http::Error error) {
|
||||
switch (error) {
|
||||
case borealis::http::Error::None:
|
||||
return HTTP_ERROR_NONE;
|
||||
case borealis::http::Error::InvalidUrl:
|
||||
return HTTP_ERROR_INVALID_URL;
|
||||
case borealis::http::Error::UnsupportedScheme:
|
||||
return HTTP_ERROR_UNSUPPORTED_SCHEME;
|
||||
case borealis::http::Error::Timeout:
|
||||
return HTTP_ERROR_TIMEOUT;
|
||||
case borealis::http::Error::TooLarge:
|
||||
return HTTP_ERROR_TOO_LARGE;
|
||||
case borealis::http::Error::Canceled:
|
||||
return HTTP_ERROR_CANCELED;
|
||||
case borealis::http::Error::Io:
|
||||
return HTTP_ERROR_IO;
|
||||
case borealis::http::Error::NoBackend:
|
||||
case borealis::http::Error::NotInitialized:
|
||||
case borealis::http::Error::Network:
|
||||
return HTTP_ERROR_NETWORK;
|
||||
default:
|
||||
return HTTP_ERROR_NETWORK;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<borealis::http::Method> borealis_method(HttpMethod method) {
|
||||
switch (method) {
|
||||
case HTTP_METHOD_GET:
|
||||
return borealis::http::Method::Get;
|
||||
case HTTP_METHOD_POST:
|
||||
return borealis::http::Method::Post;
|
||||
case HTTP_METHOD_HEAD:
|
||||
return borealis::http::Method::Head;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
borealis::http::Result publish_download(borealis::http::Result result,
|
||||
const std::filesystem::path& staging, const std::filesystem::path& destination) noexcept {
|
||||
if (result.error != borealis::http::Error::None) {
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
std::string renameError;
|
||||
if (borealis::io::atomic_replace(staging, destination, renameError)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::filesystem::path temporary = destination;
|
||||
temporary += "." + borealis::io::fs_path_to_string(staging.filename()) + ".part";
|
||||
std::error_code ec;
|
||||
std::filesystem::copy_file(
|
||||
staging, temporary, std::filesystem::copy_options::overwrite_existing, ec);
|
||||
if (ec) {
|
||||
const std::string copyError = ec.message();
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(temporary, ignored);
|
||||
result.error = borealis::http::Error::Io;
|
||||
result.message = "Failed to publish download: " + copyError;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string replaceError;
|
||||
if (!borealis::io::atomic_replace(temporary, destination, replaceError)) {
|
||||
std::filesystem::remove(temporary, ec);
|
||||
result.error = borealis::http::Error::Io;
|
||||
result.message = "Failed to publish download: " + replaceError;
|
||||
return result;
|
||||
}
|
||||
std::filesystem::remove(staging, ec);
|
||||
return result;
|
||||
} catch (const std::exception& exception) {
|
||||
result.error = borealis::http::Error::Io;
|
||||
result.message = std::string{"Failed to publish download: "} + exception.what();
|
||||
return result;
|
||||
} catch (...) {
|
||||
result.error = borealis::http::Error::Io;
|
||||
result.message = "Failed to publish download";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
void http_frame_begin() {
|
||||
std::vector<HttpRequestHandle> ready;
|
||||
s_requests.for_each([&](const HttpRequestHandle handle, const auto& entry) {
|
||||
const auto& pending = entry.value;
|
||||
if (!pending.completing && pending.task.ready()) {
|
||||
ready.push_back(handle);
|
||||
}
|
||||
});
|
||||
|
||||
for (const auto handle : ready) {
|
||||
auto* entry = s_requests.find(handle);
|
||||
if (entry == nullptr || !entry->owner->active || entry->value.completing) {
|
||||
continue;
|
||||
}
|
||||
auto& pending = entry->value;
|
||||
borealis::http::Result result;
|
||||
try {
|
||||
auto completed = pending.task.try_take();
|
||||
if (!completed.has_value()) {
|
||||
continue;
|
||||
}
|
||||
result = std::move(*completed);
|
||||
} catch (const std::exception& exception) {
|
||||
result = {
|
||||
.error = borealis::http::Error::Io,
|
||||
.message = exception.what(),
|
||||
};
|
||||
} catch (...) {
|
||||
result = {
|
||||
.error = borealis::http::Error::Io,
|
||||
.message = "HTTP request completion failed",
|
||||
};
|
||||
}
|
||||
|
||||
auto* owner = entry->owner;
|
||||
const auto callback = pending.callback;
|
||||
const auto userData = pending.userData;
|
||||
pending.completing = true;
|
||||
|
||||
std::vector<HttpHeader> headers;
|
||||
headers.reserve(result.response.headers.size());
|
||||
for (const auto& header : result.response.headers) {
|
||||
headers.push_back({.name = header.name.c_str(), .value = header.value.c_str()});
|
||||
}
|
||||
const bool downloadSucceeded =
|
||||
!pending.downloadPath.empty() && result.error == borealis::http::Error::None;
|
||||
const auto publishedPath = downloadSucceeded ?
|
||||
borealis::io::fs_path_to_string(pending.downloadPath) :
|
||||
std::string{};
|
||||
const HttpResult snapshot{
|
||||
.struct_size = sizeof(HttpResult),
|
||||
.error = map_error(result.error),
|
||||
.error_message = result.message.c_str(),
|
||||
.status_code = result.response.statusCode,
|
||||
.headers = headers.empty() ? nullptr : headers.data(),
|
||||
.header_count = static_cast<uint32_t>(headers.size()),
|
||||
.body = pending.downloadPath.empty() && !result.response.body.empty() ?
|
||||
result.response.body.data() :
|
||||
nullptr,
|
||||
.body_size = pending.downloadPath.empty() ? result.response.body.size() : 0,
|
||||
.download_path = downloadSucceeded ? publishedPath.c_str() : nullptr,
|
||||
};
|
||||
|
||||
try {
|
||||
callback(owner->context.get(), handle, &snapshot, userData);
|
||||
} catch (const std::exception& exception) {
|
||||
fail_mod(*owner, MOD_ERROR,
|
||||
std::string{"exception in HTTP completion callback: "} + exception.what());
|
||||
} catch (...) {
|
||||
fail_mod(*owner, MOD_ERROR, "unknown exception in HTTP completion callback");
|
||||
}
|
||||
s_requests.erase(handle);
|
||||
}
|
||||
}
|
||||
|
||||
size_t active_request_count(const LoadedMod& mod) {
|
||||
size_t count = 0;
|
||||
s_requests.for_each([&](HttpRequestHandle, const auto& entry) {
|
||||
if (entry.owner == &mod && !entry.value.completing) {
|
||||
++count;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
bool staging_path_in_use(const LoadedMod& mod, const std::filesystem::path& path) {
|
||||
bool inUse = false;
|
||||
s_requests.for_each([&](HttpRequestHandle, const auto& entry) {
|
||||
if (entry.owner == &mod && !entry.value.completing && entry.value.stagingPath == path) {
|
||||
inUse = true;
|
||||
}
|
||||
});
|
||||
return inUse;
|
||||
}
|
||||
|
||||
std::string user_agent_version(std::string_view version) {
|
||||
std::string result{version};
|
||||
for (char& ch : result) {
|
||||
const auto value = static_cast<unsigned char>(ch);
|
||||
if (value <= 32 || value >= 127) {
|
||||
ch = '_';
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ModResult start_request(LoadedMod& mod, const HttpRequestDesc& desc, HttpCompleteFn callback,
|
||||
void* userData, HttpRequestHandle& outHandle) {
|
||||
const std::string_view url{desc.url};
|
||||
const auto method = borealis_method(desc.method);
|
||||
if (!valid_url(url) || !method.has_value() ||
|
||||
(desc.header_count != 0 && desc.headers == nullptr) ||
|
||||
(desc.body_size != 0 && desc.body == nullptr) || desc.body_size > MaxRequestBodyBytes ||
|
||||
((desc.method == HTTP_METHOD_GET || desc.method == HTTP_METHOD_HEAD) &&
|
||||
desc.body_size != 0) ||
|
||||
(desc.method == HTTP_METHOD_HEAD && desc.download_path != nullptr) ||
|
||||
desc.header_count > MaxHeaders)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
size_t headerBytes = 0;
|
||||
for (uint32_t i = 0; i < desc.header_count; ++i) {
|
||||
const auto& header = desc.headers[i];
|
||||
if (header.name == nullptr || header.value == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const std::string_view name{header.name};
|
||||
const std::string_view value{header.value};
|
||||
const bool invalidValue = std::ranges::any_of(
|
||||
value, [](unsigned char ch) { return (ch < 32 && ch != '\t') || ch == 127; });
|
||||
if (!valid_header_name(name) || invalidValue || is_reserved_header(name) ||
|
||||
name.size() > MaxHeaderBytes - headerBytes)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
headerBytes += name.size();
|
||||
if (value.size() > MaxHeaderBytes - headerBytes) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
headerBytes += value.size();
|
||||
}
|
||||
|
||||
auto downloadPath = validate_download_path(mod, desc.download_path);
|
||||
if (!downloadPath.has_value() ||
|
||||
(downloadPath->empty() && desc.max_body_bytes > MaxResponseBodyBytes))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (active_request_count(mod) >= MaxRequestsPerMod) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
|
||||
std::filesystem::path staging;
|
||||
if (!downloadPath->empty()) {
|
||||
staging = staging_path(mod, url);
|
||||
if (staging_path_in_use(mod, staging)) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(staging.parent_path(), ec);
|
||||
if (ec) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
std::filesystem::create_directories(downloadPath->parent_path(), ec);
|
||||
if (ec) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
PendingRequest pending{
|
||||
.callback = callback,
|
||||
.userData = userData,
|
||||
.stagingPath = staging,
|
||||
.downloadPath = *downloadPath,
|
||||
};
|
||||
|
||||
if (!borealis::http::available()) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
borealis::http::Request request{
|
||||
.method = *method,
|
||||
.url = std::string{url},
|
||||
.body = desc.body_size != 0 ?
|
||||
std::string{static_cast<const char*>(desc.body), desc.body_size} :
|
||||
std::string{},
|
||||
.downloadTo = staging,
|
||||
.connectTimeout = desc.connect_timeout_ms != 0 ?
|
||||
std::chrono::milliseconds{desc.connect_timeout_ms} :
|
||||
DefaultTimeout,
|
||||
.idleTimeout = desc.idle_timeout_ms != 0 ?
|
||||
std::chrono::milliseconds{desc.idle_timeout_ms} :
|
||||
DefaultTimeout,
|
||||
.totalTimeout = desc.total_timeout_ms != 0 ?
|
||||
std::optional{std::chrono::milliseconds{desc.total_timeout_ms}} :
|
||||
std::nullopt,
|
||||
.maxBodyBytes =
|
||||
desc.max_body_bytes != 0 ? desc.max_body_bytes : DefaultResponseBodyBytes,
|
||||
};
|
||||
request.headers.reserve(desc.header_count + 1);
|
||||
for (uint32_t i = 0; i < desc.header_count; ++i) {
|
||||
request.headers.push_back({desc.headers[i].name, desc.headers[i].value});
|
||||
}
|
||||
request.headers.push_back({
|
||||
.name = "User-Agent",
|
||||
.value = fmt::format("{}/{} {}/{}", AppName, BOREALIS_APP_VERSION, mod.metadata.id,
|
||||
user_agent_version(mod.metadata.version)),
|
||||
});
|
||||
|
||||
auto task = borealis::http::start(std::move(request));
|
||||
if (task.ready()) {
|
||||
auto immediate = task.try_take();
|
||||
if (!immediate.has_value()) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
if (immediate->error == borealis::http::Error::NoBackend ||
|
||||
immediate->error == borealis::http::Error::NotInitialized)
|
||||
{
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
task = borealis::detail::make_ready_task(std::move(*immediate));
|
||||
}
|
||||
if (!downloadPath->empty()) {
|
||||
task = std::move(task).map([staging, destination = *downloadPath](auto&& result) {
|
||||
return publish_download(std::move(result), staging, destination);
|
||||
});
|
||||
}
|
||||
pending.task = std::move(task);
|
||||
|
||||
outHandle = s_requests.emplace(mod, std::move(pending));
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult http_request(ModContext* context, const HttpRequestDesc* desc, HttpCompleteFn callback,
|
||||
void* userData, HttpRequestHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(HttpRequestDesc) ||
|
||||
desc->url == nullptr || callback == nullptr || outHandle == nullptr)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!declares_http_import(*mod)) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
try {
|
||||
return start_request(*mod, *desc, callback, userData, *outHandle);
|
||||
} catch (...) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
ModResult http_progress(ModContext* context, HttpRequestHandle handle, HttpProgress* outProgress) {
|
||||
const uint32_t structSize = outProgress != nullptr ? outProgress->struct_size : 0;
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || outProgress == nullptr || structSize < sizeof(HttpProgress)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outProgress = HttpProgress{.struct_size = structSize};
|
||||
const auto* entry = s_requests.find_owned(handle, *mod);
|
||||
if (entry == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
const auto progress = entry->value.task.progress();
|
||||
outProgress->completed_bytes = progress.completed;
|
||||
outProgress->total_bytes = progress.total.value_or(0);
|
||||
outProgress->total_known = progress.total.has_value();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult http_cancel(ModContext* context, HttpRequestHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auto* entry = s_requests.find_owned(handle, *mod);
|
||||
if (entry == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
entry->value.task.cancel();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void http_mod_deactivating(LoadedMod& mod) {
|
||||
(void)s_requests.take_all(mod);
|
||||
}
|
||||
|
||||
void http_mod_detached(LoadedMod& mod) {
|
||||
bool found = false;
|
||||
s_requests.for_each(
|
||||
[&](HttpRequestHandle, const auto& entry) { found = found || entry.owner == &mod; });
|
||||
assert(!found);
|
||||
}
|
||||
|
||||
void http_shutdown() {
|
||||
s_requests = {};
|
||||
}
|
||||
|
||||
bool http_available() {
|
||||
return borealis::http::available() && borealis::http::initialize();
|
||||
}
|
||||
|
||||
constexpr HttpService s_httpService{
|
||||
.header = SERVICE_HEADER(HttpService, HTTP_SERVICE_MAJOR, HTTP_SERVICE_MINOR),
|
||||
.request = http_request,
|
||||
.progress = http_progress,
|
||||
.cancel = http_cancel,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_httpModule{
|
||||
.id = HTTP_SERVICE_ID,
|
||||
.majorVersion = HTTP_SERVICE_MAJOR,
|
||||
.minorVersion = HTTP_SERVICE_MINOR,
|
||||
.service = &s_httpService,
|
||||
.available = http_available,
|
||||
.modDeactivating = http_mod_deactivating,
|
||||
.modDetached = http_mod_detached,
|
||||
.frameBegin = http_frame_begin,
|
||||
.shutdown = http_shutdown,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -75,6 +75,17 @@ ModResult item_resolve_check(
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult item_resolve_check_full(ModContext* context, const char* name, uint8_t originalItemNo,
|
||||
ItemCheckResolution* outResolution) {
|
||||
if (mod_from_context(context) == nullptr || !is_valid_check_name(name) ||
|
||||
outResolution == nullptr)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outResolution = item_check_resolve(name, originalItemNo, nullptr);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult item_give_item(
|
||||
ModContext* context, const char* checkName, uint8_t itemNo, uint32_t flags) {
|
||||
auto* mod = mod_from_context(context);
|
||||
@@ -130,6 +141,7 @@ constexpr ItemService s_itemService{
|
||||
.give_item = item_give_item,
|
||||
.observe_gives = item_observe_gives,
|
||||
.unobserve_gives = item_unobserve_gives,
|
||||
.resolve_check_full = item_resolve_check_full,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -138,6 +138,9 @@ const ServiceRecord* find_service_record(const char* serviceId, const uint16_t m
|
||||
}
|
||||
|
||||
ModResult register_module(const ServiceModule& module) {
|
||||
if (module.available != nullptr && !module.available()) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
const auto result = register_service(
|
||||
module.id, module.majorVersion, module.minorVersion, module.service, nullptr, false);
|
||||
if (result != MOD_OK) {
|
||||
@@ -210,6 +213,8 @@ void ModLoader::init_services() {
|
||||
&svc::g_hostModule,
|
||||
&svc::g_logModule,
|
||||
&svc::g_resourceModule,
|
||||
&svc::g_fileModule,
|
||||
&svc::g_httpModule,
|
||||
&svc::g_hookModule,
|
||||
&svc::g_overlayModule,
|
||||
&svc::g_textureModule,
|
||||
|
||||
@@ -26,6 +26,8 @@ struct ServiceModule {
|
||||
uint16_t minorVersion = 0;
|
||||
const void* service = nullptr;
|
||||
|
||||
// False prevents registration when a platform dependency is unavailable.
|
||||
bool (*available)() = nullptr;
|
||||
// One-time setup, at registration (ModLoader::init_services).
|
||||
void (*initialize)() = nullptr;
|
||||
// A mod is beginning deactivation: stop callbacks that may execute concurrently. Service state
|
||||
@@ -68,6 +70,8 @@ void modules_shutdown();
|
||||
extern const ServiceModule g_hostModule;
|
||||
extern const ServiceModule g_logModule;
|
||||
extern const ServiceModule g_resourceModule;
|
||||
extern const ServiceModule g_fileModule;
|
||||
extern const ServiceModule g_httpModule;
|
||||
extern const ServiceModule g_hookModule;
|
||||
extern const ServiceModule g_overlayModule;
|
||||
extern const ServiceModule g_textureModule;
|
||||
|
||||
@@ -200,6 +200,7 @@ void save_slot_written(uint32_t slot, const void* slotData) {
|
||||
store.snapshotCrc = utils::crc32(slotData, kQuestLogSize);
|
||||
}
|
||||
flush_sidecar();
|
||||
s_currentSlot = static_cast<int32_t>(slot);
|
||||
notify(slot, &SaveObserverRecord::onWritten, "save-written");
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ constexpr size_t kUiControlSelectedSize =
|
||||
offsetof(UiControlDesc, is_selected) + sizeof(UiPredicateFn);
|
||||
constexpr size_t kUiControlStringSetModeSize =
|
||||
offsetof(UiControlDesc, string_set_mode) + sizeof(UiStringSetMode);
|
||||
constexpr size_t kUiControlFilePickerSize =
|
||||
offsetof(UiControlDesc, directory_mode) + sizeof(bool);
|
||||
constexpr size_t kUiListItemV21Size = offsetof(UiListItem, label) + sizeof(const char*);
|
||||
constexpr size_t kUiListDescV21Size = offsetof(UiListDesc, user_data) + sizeof(void*);
|
||||
|
||||
@@ -303,6 +305,7 @@ void wire_callback_binding(
|
||||
break;
|
||||
case UI_CONTROL_STRING:
|
||||
case UI_CONTROL_COLOR:
|
||||
case UI_CONTROL_FILE_PICKER:
|
||||
spec.getString = [getValue]() -> Rml::String {
|
||||
const UiControlValue value = getValue();
|
||||
return value.string_value != nullptr ? value.string_value : "";
|
||||
@@ -382,7 +385,8 @@ bool wire_config_var_binding(LoadedMod& mod, const UiControlDesc& desc, ui::ModC
|
||||
return true;
|
||||
}
|
||||
case UI_CONTROL_STRING:
|
||||
case UI_CONTROL_COLOR: {
|
||||
case UI_CONTROL_COLOR:
|
||||
case UI_CONTROL_FILE_PICKER: {
|
||||
const auto find = [modPtr, varHandle] {
|
||||
return static_cast<ConfigVar<std::string>*>(
|
||||
config_find_var(*modPtr, varHandle, CONFIG_VAR_STRING));
|
||||
@@ -642,6 +646,14 @@ ModResult ui_pane_add_control(
|
||||
spec.colorPresets.emplace_back(desc.color_presets[i]);
|
||||
}
|
||||
break;
|
||||
case UI_CONTROL_FILE_PICKER:
|
||||
spec.kind = ui::ModControlSpec::Kind::FilePicker;
|
||||
spec.directoryMode = desc.directory_mode;
|
||||
for (size_t i = 0; i < desc.file_filter_count; ++i) {
|
||||
spec.fileFilters.push_back(
|
||||
{desc.file_filters[i].name, desc.file_filters[i].pattern});
|
||||
}
|
||||
break;
|
||||
case UI_CONTROL_SELECT:
|
||||
spec.kind = ui::ModControlSpec::Kind::Select;
|
||||
if (slot->helpPane == nullptr) {
|
||||
@@ -1257,6 +1269,18 @@ bool valid_control_desc(const UiControlDesc& desc) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case UI_CONTROL_FILE_PICKER:
|
||||
if (desc.struct_size < kUiControlFilePickerSize ||
|
||||
(desc.file_filter_count != 0 && desc.file_filters == nullptr))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < desc.file_filter_count; ++i) {
|
||||
if (desc.file_filters[i].name == nullptr || desc.file_filters[i].pattern == nullptr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
+47
-5
@@ -1,8 +1,12 @@
|
||||
#include "dusk/settings.h"
|
||||
#include <aurora/aurora.h>
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/ui/ui.hpp"
|
||||
#include "dusk/game_mode.hpp"
|
||||
#include "dusk/texture_replacements.hpp"
|
||||
#include "dusk/ui/ui.hpp"
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
#include <dolphin/vi.h>
|
||||
|
||||
namespace dusk {
|
||||
|
||||
@@ -161,6 +165,8 @@ UserSettings g_userSettings = {
|
||||
.removeQuestMapMarkers {"game.removeQuestMapMarkers", false},
|
||||
.showInputViewer {"game.showInputViewer", false},
|
||||
.showInputViewerGyro {"game.showInputViewerGyro", false},
|
||||
.enableMoveLinkCombo {"game.enableMoveLinkCombo", false},
|
||||
.enableTeleportCombo {"game.enableTeleportCombo", false},
|
||||
.lastSelectedGameModeId {"game.lastSelectedGameModeId", gamemode::kVanillaGameModeId}
|
||||
},
|
||||
|
||||
@@ -220,6 +226,22 @@ UserSettings& getSettings() {
|
||||
return g_userSettings;
|
||||
}
|
||||
|
||||
void applyInternalResolutionScale(int scale) {
|
||||
VISetFrameBufferScale(static_cast<float>(scale));
|
||||
}
|
||||
|
||||
void applyResampler(Resampler resampler) {
|
||||
switch (resampler) {
|
||||
case Resampler::Area:
|
||||
aurora_set_resampler(SAMPLER_AREA);
|
||||
break;
|
||||
case Resampler::Bilinear:
|
||||
default:
|
||||
aurora_set_resampler(SAMPLER_BILINEAR);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void registerSettings() {
|
||||
// Video
|
||||
Register(g_userSettings.video.enableFullscreen);
|
||||
@@ -286,9 +308,12 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.bloomMultiplier);
|
||||
Register(g_userSettings.game.depthOfFieldMode);
|
||||
Register(g_userSettings.game.disableWaterRefraction);
|
||||
Register(g_userSettings.game.enableTextureReplacements);
|
||||
Register(g_userSettings.game.internalResolutionScale);
|
||||
Register(g_userSettings.game.resampler);
|
||||
Register(g_userSettings.game.enableTextureReplacements,
|
||||
[](const bool&, const bool&) { texture_replacements::reload(); });
|
||||
Register(g_userSettings.game.internalResolutionScale,
|
||||
[](const int& value, const int&) { applyInternalResolutionScale(value); });
|
||||
Register(g_userSettings.game.resampler,
|
||||
[](const Resampler& value, const Resampler&) { applyResampler(value); });
|
||||
Register(g_userSettings.game.shadowResolutionMultiplier);
|
||||
Register(g_userSettings.game.enableMapBackground);
|
||||
Register(g_userSettings.game.disableCutscenePillarboxing);
|
||||
@@ -314,6 +339,8 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.removeQuestMapMarkers);
|
||||
Register(g_userSettings.game.showInputViewer);
|
||||
Register(g_userSettings.game.showInputViewerGyro);
|
||||
Register(g_userSettings.game.enableMoveLinkCombo);
|
||||
Register(g_userSettings.game.enableTeleportCombo);
|
||||
Register(g_userSettings.game.lastSelectedGameModeId);
|
||||
Register(g_userSettings.game.fastSpinner);
|
||||
Register(g_userSettings.game.infiniteHearts);
|
||||
@@ -405,6 +432,21 @@ static TransientSettings g_transientSettings = {
|
||||
.colliderViewOpacity = 50.0f,
|
||||
.drawRange = 100.0f,
|
||||
},
|
||||
.triggerView = {
|
||||
.loadZones = false,
|
||||
.eventAreas = false,
|
||||
.switchAreas = false,
|
||||
.eventTags = false,
|
||||
.midnaStops = false,
|
||||
.twilightGates = false,
|
||||
.checkpoints = false,
|
||||
.paths = false,
|
||||
.transformDists = false,
|
||||
.attentionDists = false,
|
||||
.purpleMistAvoid = false,
|
||||
.leevers = false,
|
||||
.opacity = 75.0f,
|
||||
},
|
||||
.turboMode = false,
|
||||
};
|
||||
|
||||
|
||||
@@ -287,6 +287,8 @@ struct UserSettings {
|
||||
ConfigVar<bool> removeQuestMapMarkers;
|
||||
ConfigVar<bool> showInputViewer;
|
||||
ConfigVar<bool> showInputViewerGyro;
|
||||
ConfigVar<bool> enableMoveLinkCombo;
|
||||
ConfigVar<bool> enableTeleportCombo;
|
||||
|
||||
ConfigVar<std::string> lastSelectedGameModeId;
|
||||
} game;
|
||||
@@ -317,6 +319,9 @@ UserSettings& getSettings();
|
||||
|
||||
void registerSettings();
|
||||
|
||||
void applyInternalResolutionScale(int scale);
|
||||
void applyResampler(Resampler resampler);
|
||||
|
||||
// Transient settings
|
||||
|
||||
struct CollisionViewSettings {
|
||||
@@ -330,8 +335,25 @@ struct CollisionViewSettings {
|
||||
float drawRange;
|
||||
};
|
||||
|
||||
struct TriggerViewSettings {
|
||||
bool loadZones;
|
||||
bool eventAreas;
|
||||
bool switchAreas;
|
||||
bool eventTags;
|
||||
bool midnaStops;
|
||||
bool twilightGates;
|
||||
bool checkpoints;
|
||||
bool paths;
|
||||
bool transformDists;
|
||||
bool attentionDists;
|
||||
bool purpleMistAvoid;
|
||||
bool leevers;
|
||||
float opacity;
|
||||
};
|
||||
|
||||
struct TransientSettings {
|
||||
CollisionViewSettings collisionView;
|
||||
TriggerViewSettings triggerView;
|
||||
bool turboMode;
|
||||
bool moveLinkActive;
|
||||
bool stateShareLoadActive;
|
||||
|
||||
@@ -19,6 +19,8 @@ static void onSpeedrunModeDeactive() {
|
||||
if (getSettings().game.liveSplitEnabled) {
|
||||
speedrun::disconnectLiveSplit();
|
||||
}
|
||||
g_speedrunInfo.reset();
|
||||
reset();
|
||||
}
|
||||
|
||||
void registerSpeedrunGameMode() {
|
||||
@@ -83,6 +85,9 @@ void resetForSpeedrunMode() {
|
||||
getSettings().backend.enableAdvancedSettings.setSpeedrunValue(false);
|
||||
getSettings().game.recordingMode.setSpeedrunValue(false);
|
||||
getSettings().game.debugFlyCam.setSpeedrunValue(false);
|
||||
|
||||
getSettings().game.enableMoveLinkCombo.setSpeedrunValue(false);
|
||||
getSettings().game.enableTeleportCombo.setSpeedrunValue(false);
|
||||
}
|
||||
|
||||
static void clearSpeedrunOverrides() {
|
||||
|
||||
@@ -26,11 +26,6 @@ void reload() {
|
||||
s_directoryGroup.registrations.size());
|
||||
}
|
||||
|
||||
void set_enabled(bool enabled) {
|
||||
getSettings().game.enableTextureReplacements.setValue(enabled);
|
||||
reload();
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
aurora::texture::unregister_replacements(s_directoryGroup);
|
||||
s_directoryGroup.registrations.clear();
|
||||
|
||||
@@ -8,7 +8,6 @@ namespace dusk::texture_replacements {
|
||||
inline constexpr int32_t kUserTextureReplacementPriority = -1'000'000;
|
||||
|
||||
void reload();
|
||||
void set_enabled(bool enabled);
|
||||
void shutdown();
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
#include "JSystem/JMath/JMath.h"
|
||||
#include "SSystem/SComponent/c_list.h"
|
||||
#include "SSystem/SComponent/c_tag.h"
|
||||
#include "d/actor/d_a_alink.h"
|
||||
#include "d/actor/d_a_e_rb.h"
|
||||
#include "d/actor/d_a_e_rd.h"
|
||||
#include "d/actor/d_a_kytag08.h"
|
||||
#include "d/actor/d_a_scene_exit.h"
|
||||
#include "d/actor/d_a_swc00.h"
|
||||
#include "d/actor/d_a_tag_chgrestart.h"
|
||||
#include "d/actor/d_a_tag_mstop.h"
|
||||
#include "d/d_attention.h"
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include "d/d_debug_viewer.h"
|
||||
#include "d/d_path.h"
|
||||
#include "d/d_stage.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/trigger_viewer.h"
|
||||
#include "f_op/f_op_actor_mng.h"
|
||||
#include "f_op/f_op_actor_tag.h"
|
||||
#include "f_pc/f_pc_name.h"
|
||||
#include "m_Do/m_Do_mtx.h"
|
||||
|
||||
namespace dusk {
|
||||
namespace TriggerView {
|
||||
|
||||
static u8 s_opacity;
|
||||
|
||||
typedef void (*DrawCallback)(fopAc_ac_c*);
|
||||
|
||||
static void searchActorForCallback(s16 actorName, DrawCallback callback) {
|
||||
node_class* node = g_fopAcTg_Queue.mpHead;
|
||||
|
||||
for (int i = 0; i < g_fopAcTg_Queue.mSize; i++) {
|
||||
if (node != NULL) {
|
||||
create_tag_class* tag = (create_tag_class*)node;
|
||||
fopAc_ac_c* actorData = (fopAc_ac_c*)tag->mpTagData;
|
||||
|
||||
bool checkAll = actorName == -1;
|
||||
if (actorData != NULL && (fopAcM_GetName(actorData) == actorName || checkAll)) {
|
||||
callback(actorData);
|
||||
}
|
||||
node = node->mpNextNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void drawSceneExit(fopAc_ac_c* actor) {
|
||||
daScex_c* scex = (daScex_c*)actor;
|
||||
|
||||
cXyz points[8];
|
||||
points[0].set(-actor->scale.x, actor->scale.y, -actor->scale.z);
|
||||
points[1].set(actor->scale.x, actor->scale.y, -actor->scale.z);
|
||||
points[2].set(-actor->scale.x, actor->scale.y, actor->scale.z);
|
||||
points[3].set(actor->scale.x, actor->scale.y, actor->scale.z);
|
||||
points[4].set(-actor->scale.x, 0.0f, -actor->scale.z);
|
||||
points[5].set(actor->scale.x, 0.0f, -actor->scale.z);
|
||||
points[6].set(-actor->scale.x, 0.0f, actor->scale.z);
|
||||
points[7].set(actor->scale.x, 0.0f, actor->scale.z);
|
||||
|
||||
mDoMtx_inverse(scex->mMatrix, mDoMtx_stack_c::get());
|
||||
mDoMtx_multVecArray(mDoMtx_stack_c::get(), points, points, 8);
|
||||
|
||||
GXColor color = {0xFF, 0x00, 0xFF, s_opacity};
|
||||
dDbVw_drawCube8pXlu(points, color);
|
||||
}
|
||||
|
||||
static void drawMidnaStop(fopAc_ac_c* actor) {
|
||||
daTagMstop_c* mstop = (daTagMstop_c*)actor;
|
||||
|
||||
GXColor color = {0x4A, 0x36, 0xBA, s_opacity};
|
||||
dDbVw_drawCylinderXlu(mstop->current.pos, mstop->scale.x * 100.0f, mstop->scale.y, color, 1);
|
||||
}
|
||||
|
||||
static void drawPlumTag(fopAc_ac_c* actor) {
|
||||
GXColor color = {0x00, 0xFF, 0x00, s_opacity};
|
||||
dDbVw_drawCylinderXlu(actor->current.pos, actor->scale.x * 100.0f, 1000000.0f, color, 1);
|
||||
}
|
||||
|
||||
static void drawPlumSearch(fopAc_ac_c* actor) {
|
||||
GXColor color = {0xFF, 0x00, 0x00, s_opacity};
|
||||
const f32 search_dist = 500.0f;
|
||||
dDbVw_drawCircleXlu(actor->attention_info.position, search_dist + 160.0f, color, 1, 12);
|
||||
}
|
||||
|
||||
static void drawSwitchArea(fopAc_ac_c* actor) {
|
||||
daSwc00_c* swc = (daSwc00_c*)actor;
|
||||
int shape_type = (fopAcM_GetParam(actor) >> 0x12) & 3;
|
||||
|
||||
GXColor color = {0x00, 0x00, 0xFF, s_opacity};
|
||||
if (shape_type == 3) {
|
||||
dDbVw_drawCylinderXlu(swc->current.pos, JMAFastSqrt(swc->scale.x) - 30.0f, swc->scale.y, color, 1);
|
||||
} else if (shape_type == 0) {
|
||||
cXyz size = swc->field_0x574 - swc->field_0x568;
|
||||
size *= 0.5f;
|
||||
cXyz pos = swc->field_0x568 + size;
|
||||
csXyz angle(swc->current.angle.x, swc->current.angle.y, swc->current.angle.z);
|
||||
dDbVw_drawCubeXlu(pos, size, angle, color);
|
||||
}
|
||||
}
|
||||
|
||||
static void drawEventArea(fopAc_ac_c* actor) {
|
||||
u8 type = (actor->shape_angle.z & 0xFF);
|
||||
if (type == 0xFF) {
|
||||
type = 0;
|
||||
}
|
||||
|
||||
if (type == 15 || type == 16) {
|
||||
GXColor color = {0xFF, 0xFF, 0x00, s_opacity};
|
||||
cXyz points[8];
|
||||
points[0].set(-actor->scale.x, actor->scale.y, -actor->scale.z);
|
||||
points[1].set(actor->scale.x, actor->scale.y, -actor->scale.z);
|
||||
points[2].set(-actor->scale.x, actor->scale.y, actor->scale.z);
|
||||
points[3].set(actor->scale.x, actor->scale.y, actor->scale.z);
|
||||
points[4].set(-actor->scale.x, 0.0f, -actor->scale.z);
|
||||
points[5].set(actor->scale.x, 0.0f, -actor->scale.z);
|
||||
points[6].set(-actor->scale.x, 0.0f, actor->scale.z);
|
||||
points[7].set(actor->scale.x, 0.0f, actor->scale.z);
|
||||
|
||||
mDoMtx_stack_c::transS(actor->home.pos.x, actor->home.pos.y, actor->home.pos.z);
|
||||
mDoMtx_stack_c::YrotS(actor->current.angle.y);
|
||||
mDoMtx_multVecArray(mDoMtx_stack_c::get(), points, points, 8);
|
||||
|
||||
dDbVw_drawCube8pXlu(points, color);
|
||||
} else {
|
||||
GXColor outer_color = {0xFF, 0x00, 0x00, s_opacity};
|
||||
GXColor inner_color = {0x00, 0xFF, 0x00, s_opacity};
|
||||
cXyz pos = actor->current.pos;
|
||||
|
||||
daAlink_c* player = (daAlink_c*)dComIfGp_getPlayer(0);
|
||||
if (player != NULL && pos.y < player->mLinkAcch.GetGroundH()) {
|
||||
pos.y = player->mLinkAcch.GetGroundH() + 100.0f;
|
||||
}
|
||||
|
||||
const f32 inner_scale = 0.83f;
|
||||
dDbVw_drawCircleXlu(pos, actor->scale.x * inner_scale, inner_color, 1, 20);
|
||||
dDbVw_drawCircleXlu(pos, actor->scale.x, outer_color, 1, 20);
|
||||
}
|
||||
}
|
||||
|
||||
static void drawEventTag(fopAc_ac_c* actor) {
|
||||
GXColor color = {0x00, 0xC8, 0xFF, s_opacity};
|
||||
u16 area_type = actor->home.angle.x & 0x8000;
|
||||
|
||||
if (area_type == 0x8000) {
|
||||
cXyz start(actor->current.pos.x - (actor->scale.x * 0.5f), actor->current.pos.y,
|
||||
actor->current.pos.z - (actor->scale.z * 0.5f));
|
||||
cXyz end(actor->current.pos.x + (actor->scale.x * 0.5f),
|
||||
actor->current.pos.y + actor->scale.y,
|
||||
actor->current.pos.z + (actor->scale.z * 0.5f));
|
||||
|
||||
cXyz points[8];
|
||||
points[0].set(start.x, start.y, start.z);
|
||||
points[1].set(start.x, start.y, end.z);
|
||||
points[2].set(end.x, start.y, end.z);
|
||||
points[3].set(end.x, start.y, start.z);
|
||||
points[4].set(start.x, end.y, start.z);
|
||||
points[5].set(start.x, end.y, end.z);
|
||||
points[6].set(end.x, end.y, end.z);
|
||||
points[7].set(end.x, end.y, start.z);
|
||||
|
||||
dDbVw_drawCube8pXlu(points, color);
|
||||
} else {
|
||||
cXyz pos = actor->current.pos;
|
||||
pos.y -= actor->scale.y;
|
||||
dDbVw_drawCylinderXlu(pos, actor->scale.x, actor->scale.y * 2, color, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void drawTWGate(fopAc_ac_c* actor) {
|
||||
GXColor color = {0xFF, 0xFF, 0xFF, s_opacity};
|
||||
dDbVw_drawCylinderXlu(actor->current.pos, actor->scale.x * 100.0f, actor->scale.y * 100.0f, color, 1);
|
||||
}
|
||||
|
||||
static u8 s_pathColorIndex = 0;
|
||||
|
||||
static void drawPaths(dStage_dPath_c* paths) {
|
||||
static const GXColor colors[8] = {
|
||||
{0xFF, 0xFF, 0xFF}, {0x00, 0x00, 0x00}, {0xFF, 0x00, 0x00}, {0x00, 0xFF, 0x00},
|
||||
{0x00, 0x00, 0xFF}, {0xFF, 0xFF, 0x00}, {0xFF, 0x00, 0xFF}, {0x00, 0xFF, 0xFF},
|
||||
};
|
||||
|
||||
cXyz cubeSize = {30.0f, 30.0f, 30.0f};
|
||||
csXyz cubeAngle = {0, 0, 0};
|
||||
|
||||
for (int i = 0; i < (int)paths->num; i++) {
|
||||
dPath* path = &paths->m_path[i];
|
||||
GXColor color = colors[(s_pathColorIndex++) & 7];
|
||||
color.a = s_opacity;
|
||||
cXyz a, b;
|
||||
|
||||
if (dPath_ChkClose(path) && path->m_num > 2) {
|
||||
a = (Vec)path->m_points[0].m_position;
|
||||
b = (Vec)path->m_points[(int)path->m_num - 1].m_position;
|
||||
dDbVw_drawLineXlu(a, b, color, 1, 10);
|
||||
}
|
||||
|
||||
for (int j = 0; j < (int)path->m_num - 1; j++) {
|
||||
a = (Vec)path->m_points[j].m_position;
|
||||
b = (Vec)path->m_points[j + 1].m_position;
|
||||
dDbVw_drawLineXlu(a, b, color, 1, 10);
|
||||
dDbVw_drawCubeXlu(a, cubeSize, cubeAngle, color);
|
||||
}
|
||||
dDbVw_drawCubeXlu(b, cubeSize, cubeAngle, color);
|
||||
}
|
||||
}
|
||||
|
||||
static void drawStagePaths() {
|
||||
dStage_dPath_c* stagePaths = g_dComIfG_gameInfo.play.getStage().getPath2Inf();
|
||||
if (stagePaths != nullptr) {
|
||||
drawPaths(stagePaths);
|
||||
}
|
||||
}
|
||||
|
||||
static void drawCurrentRoomPaths() {
|
||||
fopAc_ac_c* player = dComIfGp_getPlayer(0);
|
||||
if (player == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
s32 roomNo = fopAcM_GetRoomNo(player);
|
||||
if (roomNo < 0 || roomNo >= 64) {
|
||||
return;
|
||||
}
|
||||
|
||||
dStage_dPath_c* roomPaths = dStage_roomControl_c::mStatus[roomNo].mRoomDt.getPath2Inf();
|
||||
if (roomPaths != nullptr) {
|
||||
drawPaths(roomPaths);
|
||||
}
|
||||
}
|
||||
|
||||
static void drawCheckpointTag(fopAc_ac_c* actor) {
|
||||
daTagChgRestart_c* chk = (daTagChgRestart_c*)actor;
|
||||
|
||||
GXColor color = {0x29, 0xF0, 0xFF, s_opacity};
|
||||
cXyz points[8];
|
||||
|
||||
mDoMtx_stack_c::transS(actor->current.pos.x, actor->current.pos.y, actor->current.pos.z);
|
||||
mDoMtx_stack_c::YrotM(actor->current.angle.y);
|
||||
|
||||
points[0] = chk->mVertices[0];
|
||||
points[1] = chk->mVertices[1];
|
||||
points[2] = chk->mVertices[3];
|
||||
points[3] = chk->mVertices[2];
|
||||
points[4] = chk->mVertices[0];
|
||||
points[5] = chk->mVertices[1];
|
||||
points[6] = chk->mVertices[3];
|
||||
points[7] = chk->mVertices[2];
|
||||
|
||||
mDoMtx_multVecArray(mDoMtx_stack_c::get(), points, points, 8);
|
||||
|
||||
fopAc_ac_c* player = dComIfGp_getPlayer(0);
|
||||
if (player != nullptr) {
|
||||
for (int i = 0; i < 8; i++) {
|
||||
points[i].y = player->current.pos.y;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
points[i].y += 1000.0f;
|
||||
}
|
||||
|
||||
dDbVw_drawCube8pXlu(points, color);
|
||||
}
|
||||
|
||||
static void drawTransformDists(fopAc_ac_c* actor) {
|
||||
if (fopAcM_GetGroup(actor) == 4 && !(actor->actor_status & fopAcStts_UNK_0x8000000_e)) {
|
||||
GXColor near_color = {0x00, 0xFF, 0x00, s_opacity};
|
||||
GXColor far_color = {0xFF, 0x00, 0x00, s_opacity};
|
||||
|
||||
const f32 near_dist = 400.0f;
|
||||
const f32 far_dist = 5000.0f;
|
||||
|
||||
dDbVw_drawCircleXlu(actor->eyePos, near_dist, near_color, 1, 20);
|
||||
dDbVw_drawCircleXlu(actor->eyePos, far_dist, far_color, 1, 20);
|
||||
|
||||
const s16 view_range = 0x4000;
|
||||
cXyz offset(0.0f, 0.0f, far_dist);
|
||||
cXyz endpos;
|
||||
|
||||
mDoMtx_stack_c::transS(actor->eyePos.x, actor->eyePos.y, actor->eyePos.z);
|
||||
mDoMtx_stack_c::YrotM(actor->shape_angle.y);
|
||||
mDoMtx_stack_c::YrotM(-view_range);
|
||||
mDoMtx_stack_c::multVec(&offset, &endpos);
|
||||
dDbVw_drawLineXlu(actor->eyePos, endpos, far_color, 1, 10);
|
||||
|
||||
mDoMtx_stack_c::transS(actor->eyePos.x, actor->eyePos.y, actor->eyePos.z);
|
||||
mDoMtx_stack_c::YrotM(actor->shape_angle.y);
|
||||
mDoMtx_stack_c::YrotM(view_range);
|
||||
mDoMtx_stack_c::multVec(&offset, &endpos);
|
||||
dDbVw_drawLineXlu(actor->eyePos, endpos, far_color, 1, 10);
|
||||
|
||||
mDoMtx_stack_c::transS(actor->eyePos.x, actor->eyePos.y, actor->eyePos.z);
|
||||
mDoMtx_stack_c::YrotM(actor->shape_angle.y);
|
||||
mDoMtx_stack_c::multVec(&offset, &endpos);
|
||||
dDbVw_drawLineXlu(actor->eyePos, endpos, far_color, 1, 10);
|
||||
}
|
||||
}
|
||||
|
||||
static void drawAttentionDists(fopAc_ac_c* actor) {
|
||||
if (fopAcM_GetGroup(actor) != 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
GXColor lock_color = {0x00, 0x00, 0xFF, s_opacity};
|
||||
GXColor talk_color = {0x00, 0xFF, 0x00, s_opacity};
|
||||
|
||||
dist_entry& lock_inf = dAttention_c::getDistTable(actor->attention_info.distances[fopAc_attn_LOCK_e]);
|
||||
dist_entry& talk_inf = dAttention_c::getDistTable(actor->attention_info.distances[fopAc_attn_TALK_e]);
|
||||
|
||||
dDbVw_drawCircleXlu(actor->attention_info.position, lock_inf.mDistMax, lock_color, 1, 20);
|
||||
dDbVw_drawCircleXlu(actor->attention_info.position, talk_inf.mDistMax, talk_color, 1, 20);
|
||||
}
|
||||
|
||||
static void drawPurpleMistAvoid(fopAc_ac_c* actor) {
|
||||
kytag08_class* tag = (kytag08_class*)actor;
|
||||
|
||||
GXColor avoidColor = {0x00, 0xFF, 0x00, s_opacity};
|
||||
GXColor targetColor = {0xFF, 0x00, 0xFF, s_opacity};
|
||||
|
||||
dDbVw_drawCircleXlu(tag->mAvoidPos, tag->mSize.x * 45.0f * tag->mSizeScale, avoidColor, 1, 20);
|
||||
|
||||
cXyz cubeSize(10.0f, 10.0f, 10.0f);
|
||||
csXyz cubeAngle(0, 0, 0);
|
||||
dDbVw_drawCubeXlu(tag->mAvoidPos, cubeSize, cubeAngle, avoidColor);
|
||||
dDbVw_drawCubeXlu(tag->mTargetAvoidPos, cubeSize, cubeAngle, targetColor);
|
||||
}
|
||||
|
||||
static void drawLeeverData(fopAc_ac_c* actor) {
|
||||
e_rb_class* leever = (e_rb_class*)actor;
|
||||
|
||||
if (leever->isChild) {
|
||||
return;
|
||||
}
|
||||
|
||||
GXColor color = {0xFF, 0x00, 0x00, s_opacity};
|
||||
GXColor color2 = {0x00, 0x00, 0xFF, s_opacity};
|
||||
|
||||
cXyz pos = actor->current.pos;
|
||||
daAlink_c* player = (daAlink_c*)dComIfGp_getPlayer(0);
|
||||
if (player != nullptr && pos.y < player->mLinkAcch.GetGroundH()) {
|
||||
pos.y = player->mLinkAcch.GetGroundH() + 100.0f;
|
||||
}
|
||||
|
||||
dDbVw_drawCircleXlu(pos, leever->appearRange * 100.0f, color, 1, 20);
|
||||
dDbVw_drawCircleXlu(pos, leever->field_0xa69 * 100.0f, color2, 1, 20);
|
||||
}
|
||||
|
||||
void execute() {
|
||||
const auto& settings = getTransientSettings().triggerView;
|
||||
s_opacity = (u8)(255.0f * (settings.opacity / 100.0f));
|
||||
|
||||
if (settings.loadZones) {
|
||||
searchActorForCallback(fpcNm_SCENE_EXIT_e, drawSceneExit);
|
||||
}
|
||||
|
||||
if (settings.midnaStops) {
|
||||
searchActorForCallback(fpcNm_Tag_Mstop_e, drawMidnaStop);
|
||||
}
|
||||
|
||||
if (settings.switchAreas) {
|
||||
searchActorForCallback(fpcNm_SWC00_e, drawSwitchArea);
|
||||
}
|
||||
|
||||
if (settings.eventAreas) {
|
||||
searchActorForCallback(fpcNm_TAG_EVENT_e, drawEventTag);
|
||||
searchActorForCallback(fpcNm_TAG_EVTAREA_e, drawEventArea);
|
||||
searchActorForCallback(fpcNm_TAG_MYNA2_e, drawPlumTag);
|
||||
searchActorForCallback(fpcNm_MYNA2_e, drawPlumSearch);
|
||||
}
|
||||
|
||||
if (settings.twilightGates) {
|
||||
searchActorForCallback(fpcNm_Tag_TWGate_e, drawTWGate);
|
||||
}
|
||||
|
||||
if (settings.paths) {
|
||||
s_pathColorIndex = 0;
|
||||
drawStagePaths();
|
||||
drawCurrentRoomPaths();
|
||||
}
|
||||
|
||||
if (settings.checkpoints) {
|
||||
searchActorForCallback(fpcNm_Tag_ChgRestart_e, drawCheckpointTag);
|
||||
}
|
||||
|
||||
if (settings.transformDists) {
|
||||
searchActorForCallback(-1, drawTransformDists);
|
||||
}
|
||||
|
||||
if (settings.attentionDists) {
|
||||
searchActorForCallback(-1, drawAttentionDists);
|
||||
}
|
||||
|
||||
if (settings.purpleMistAvoid) {
|
||||
searchActorForCallback(fpcNm_KYTAG08_e, drawPurpleMistAvoid);
|
||||
}
|
||||
|
||||
if (settings.leevers) {
|
||||
searchActorForCallback(fpcNm_E_RB_e, drawLeeverData);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace TriggerView
|
||||
} // namespace dusk
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk {
|
||||
namespace TriggerView {
|
||||
void execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
#include "command_console.hpp"
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
#include <RmlUi/Core/Elements/ElementFormControlInput.h>
|
||||
#include <aurora/rmlui.hpp>
|
||||
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
<head>
|
||||
<link type="text/rcss" href="res/rml/command_console.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<console id="console">
|
||||
<output id="console-output"></output>
|
||||
<input id="console-input" type="text" maxlength="255" />
|
||||
</console>
|
||||
</body>
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
bool is_command(std::string_view text) {
|
||||
return text.size() >= 2 && text[0] == '>' && text[1] == ' ';
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CommandConsole::CommandConsole() : Document(kDocumentSource, false, DocumentScope::CommandConsole) {
|
||||
mConsole = mDocument ? mDocument->GetElementById("console") : nullptr;
|
||||
mOutput = mDocument ? mDocument->GetElementById("console-output") : nullptr;
|
||||
auto* rawInput = mDocument ? mDocument->GetElementById("console-input") : nullptr;
|
||||
mInput = rmlui_dynamic_cast<Rml::ElementFormControlInput*>(rawInput);
|
||||
|
||||
listen(
|
||||
Rml::EventId::Keydown,
|
||||
[this](Rml::Event& event) {
|
||||
if (!mInputActive) {
|
||||
return;
|
||||
}
|
||||
const auto key = static_cast<Rml::Input::KeyIdentifier>(
|
||||
event.GetParameter<int>("key_identifier", Rml::Input::KI_UNKNOWN));
|
||||
if (key == Rml::Input::KI_RETURN) {
|
||||
execute_from_input();
|
||||
event.StopImmediatePropagation();
|
||||
} else if (key == Rml::Input::KI_ESCAPE) {
|
||||
hide(false);
|
||||
event.StopImmediatePropagation();
|
||||
} else if (key == Rml::Input::KI_UP) {
|
||||
navigate_history(-1);
|
||||
event.StopImmediatePropagation();
|
||||
} else if (key == Rml::Input::KI_DOWN) {
|
||||
navigate_history(1);
|
||||
event.StopImmediatePropagation();
|
||||
} else if (key == Rml::Input::KI_PRIOR) {
|
||||
scroll_messages(-1);
|
||||
event.StopImmediatePropagation();
|
||||
} else if (key == Rml::Input::KI_NEXT) {
|
||||
scroll_messages(1);
|
||||
event.StopImmediatePropagation();
|
||||
}
|
||||
},
|
||||
true);
|
||||
|
||||
listen(mOutput, Rml::EventId::Scroll, [this](Rml::Event&) {
|
||||
const float bottom =
|
||||
std::max(0.0f, mOutput->GetScrollHeight() - mOutput->GetClientHeight());
|
||||
mStickToBottom = mOutput->GetScrollTop() >= bottom - 4.0f;
|
||||
});
|
||||
}
|
||||
|
||||
bool CommandConsole::handle_nav_command(Rml::Event&, NavCommand) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void CommandConsole::update() {
|
||||
if (!getSettings().backend.enableAdvancedSettings || is_prelaunch_open()) {
|
||||
close_input();
|
||||
Document::hide(false);
|
||||
return;
|
||||
}
|
||||
|
||||
update_message_lifetimes();
|
||||
if (!mInputActive && !has_onscreen_messages()) {
|
||||
Document::hide(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Document::visible()) {
|
||||
Document::show();
|
||||
}
|
||||
if (mInputActive && mStickToBottom && mOutput != nullptr) {
|
||||
mOutput->SetScrollTop(mOutput->GetScrollHeight() - mOutput->GetClientHeight());
|
||||
}
|
||||
}
|
||||
|
||||
void CommandConsole::show() {
|
||||
if (mInputActive || !getSettings().backend.enableAdvancedSettings || is_prelaunch_open()) {
|
||||
return;
|
||||
}
|
||||
mInputActive = true;
|
||||
mHistoryPos = -1;
|
||||
mStickToBottom = true;
|
||||
if (mConsole != nullptr) {
|
||||
mConsole->SetAttribute("open", "");
|
||||
}
|
||||
if (mOutput != nullptr) {
|
||||
mOutput->SetAttribute("open", "");
|
||||
}
|
||||
if (mInput != nullptr) {
|
||||
mInput->SetValue("");
|
||||
}
|
||||
Document::show();
|
||||
focus();
|
||||
}
|
||||
|
||||
bool CommandConsole::focus() {
|
||||
if (mInputActive && mInput != nullptr) {
|
||||
aurora::rmlui::set_input_type(aurora::rmlui::InputType::Text);
|
||||
return mInput->Focus(true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CommandConsole::hide(bool close) {
|
||||
close_input();
|
||||
if (close) {
|
||||
Document::hide(true);
|
||||
} else if (!has_onscreen_messages()) {
|
||||
Document::hide(false);
|
||||
}
|
||||
|
||||
if (auto* doc = top_document()) {
|
||||
doc->focus();
|
||||
}
|
||||
}
|
||||
|
||||
bool CommandConsole::visible() const {
|
||||
return mInputActive && Document::visible();
|
||||
}
|
||||
|
||||
bool CommandConsole::active() const {
|
||||
return mInputActive && Document::active();
|
||||
}
|
||||
|
||||
void CommandConsole::close_input() {
|
||||
if (!mInputActive) {
|
||||
return;
|
||||
}
|
||||
mInputActive = false;
|
||||
mHistoryPos = -1;
|
||||
if (mConsole != nullptr) {
|
||||
mConsole->RemoveAttribute("open");
|
||||
}
|
||||
if (mInput != nullptr) {
|
||||
mInput->SetValue("");
|
||||
mInput->Blur();
|
||||
}
|
||||
if (mOutput != nullptr) {
|
||||
mOutput->RemoveAttribute("open");
|
||||
}
|
||||
}
|
||||
|
||||
void CommandConsole::execute_from_input() {
|
||||
if (mInput == nullptr) {
|
||||
return;
|
||||
}
|
||||
const Rml::String value = mInput->GetValue();
|
||||
hide(false);
|
||||
if (!value.empty()) {
|
||||
runCommand(value, mState, [this](std::string text) { append_message(std::move(text)); });
|
||||
}
|
||||
}
|
||||
|
||||
void CommandConsole::navigate_history(int direction) {
|
||||
if (mState.history.empty() || mInput == nullptr) {
|
||||
return;
|
||||
}
|
||||
const int prev = mHistoryPos;
|
||||
if (direction < 0) {
|
||||
if (mHistoryPos == -1) {
|
||||
mHistoryPos = (int)mState.history.size() - 1;
|
||||
} else if (mHistoryPos > 0) {
|
||||
--mHistoryPos;
|
||||
}
|
||||
} else {
|
||||
if (mHistoryPos != -1 && ++mHistoryPos >= (int)mState.history.size()) {
|
||||
mHistoryPos = -1;
|
||||
}
|
||||
}
|
||||
if (prev != mHistoryPos) {
|
||||
const char* str = mHistoryPos >= 0 ? mState.history[mHistoryPos].c_str() : "";
|
||||
mInput->SetValue(str);
|
||||
const int end = static_cast<int>(Rml::StringUtilities::LengthUTF8(mInput->GetValue()));
|
||||
mInput->SetSelectionRange(end, end);
|
||||
}
|
||||
}
|
||||
|
||||
void CommandConsole::scroll_messages(int pages) {
|
||||
if (mOutput == nullptr) {
|
||||
return;
|
||||
}
|
||||
const float bottom = std::max(0.0f, mOutput->GetScrollHeight() - mOutput->GetClientHeight());
|
||||
const float pageHeight = std::max(1.0f, mOutput->GetClientHeight() * 0.9f);
|
||||
const float scrollTop =
|
||||
std::clamp(mOutput->GetScrollTop() + static_cast<float>(pages) * pageHeight, 0.0f, bottom);
|
||||
mOutput->SetScrollTop(scrollTop);
|
||||
mStickToBottom = scrollTop >= bottom - 4.0f;
|
||||
}
|
||||
|
||||
void CommandConsole::append_message(std::string text) {
|
||||
auto* element = append(mOutput, "line");
|
||||
if (element != nullptr) {
|
||||
element->SetClass("cmd", is_command(text));
|
||||
append_text(element, text);
|
||||
}
|
||||
|
||||
const auto now = clock::now();
|
||||
mMessages.push_back({
|
||||
.text = std::move(text),
|
||||
.element = element,
|
||||
.fadeAt = now + kMessageDuration - kFadeDuration,
|
||||
.hideAt = now + kMessageDuration,
|
||||
});
|
||||
|
||||
while (mMessages.size() > kMaxMessageHistory) {
|
||||
if (auto* oldElement = mMessages.front().element; oldElement != nullptr) {
|
||||
if (auto* parent = oldElement->GetParentNode()) {
|
||||
parent->RemoveChild(oldElement);
|
||||
}
|
||||
}
|
||||
mMessages.pop_front();
|
||||
}
|
||||
limit_visible_messages();
|
||||
if (mConsole != nullptr) {
|
||||
mConsole->RemoveAttribute("fading");
|
||||
}
|
||||
|
||||
if (getSettings().backend.enableAdvancedSettings && !is_prelaunch_open() &&
|
||||
!Document::visible())
|
||||
{
|
||||
Document::show();
|
||||
}
|
||||
}
|
||||
|
||||
void CommandConsole::limit_visible_messages() {
|
||||
std::size_t visibleCount = 0;
|
||||
for (auto it = mMessages.rbegin(); it != mMessages.rend(); ++it) {
|
||||
if (it->expired) {
|
||||
continue;
|
||||
}
|
||||
if (++visibleCount <= kMaxVisibleLines) {
|
||||
continue;
|
||||
}
|
||||
it->expired = true;
|
||||
if (it->element != nullptr) {
|
||||
it->element->SetAttribute("expired", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CommandConsole::update_message_lifetimes() {
|
||||
const auto now = clock::now();
|
||||
for (auto& message : mMessages) {
|
||||
if (!message.fading && now >= message.fadeAt) {
|
||||
message.fading = true;
|
||||
if (message.element != nullptr) {
|
||||
message.element->SetAttribute("fading", "");
|
||||
}
|
||||
}
|
||||
if (!message.expired && now >= message.hideAt) {
|
||||
message.expired = true;
|
||||
if (message.element != nullptr) {
|
||||
message.element->SetAttribute("expired", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bool hasVisibleMessages = has_onscreen_messages();
|
||||
const bool hasOpaqueMessages = std::ranges::any_of(
|
||||
mMessages, [](const auto& message) { return !message.fading && !message.expired; });
|
||||
if (mConsole != nullptr) {
|
||||
if (hasVisibleMessages && !hasOpaqueMessages) {
|
||||
mConsole->SetAttribute("fading", "");
|
||||
} else {
|
||||
mConsole->RemoveAttribute("fading");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CommandConsole::has_onscreen_messages() const {
|
||||
return std::ranges::any_of(mMessages, [](const auto& message) { return !message.expired; });
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include "document.hpp"
|
||||
#include "dusk/commands.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
|
||||
namespace Rml {
|
||||
class ElementFormControlInput;
|
||||
}
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class CommandConsole : public Document {
|
||||
public:
|
||||
CommandConsole();
|
||||
|
||||
void update() override;
|
||||
void show() override;
|
||||
bool focus() override;
|
||||
void hide(bool close) override;
|
||||
bool visible() const override;
|
||||
bool active() const override;
|
||||
bool permanent() const override { return true; }
|
||||
|
||||
bool input_active() const { return mInputActive; }
|
||||
|
||||
private:
|
||||
struct MessageLine {
|
||||
std::string text;
|
||||
Rml::Element* element = nullptr;
|
||||
clock::time_point fadeAt;
|
||||
clock::time_point hideAt;
|
||||
bool fading = false;
|
||||
bool expired = false;
|
||||
};
|
||||
|
||||
static constexpr auto kMessageDuration = std::chrono::seconds{6};
|
||||
static constexpr auto kFadeDuration = std::chrono::milliseconds{800};
|
||||
static constexpr std::size_t kMaxVisibleLines = 24;
|
||||
static constexpr std::size_t kMaxMessageHistory = 500;
|
||||
|
||||
Rml::Element* mConsole = nullptr;
|
||||
Rml::Element* mOutput = nullptr;
|
||||
Rml::ElementFormControlInput* mInput = nullptr;
|
||||
|
||||
std::deque<MessageLine> mMessages;
|
||||
int mHistoryPos = -1;
|
||||
bool mInputActive = false;
|
||||
bool mStickToBottom = true;
|
||||
|
||||
CommandState mState;
|
||||
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
void append_message(std::string text);
|
||||
void close_input();
|
||||
void execute_from_input();
|
||||
bool has_onscreen_messages() const;
|
||||
void limit_visible_messages();
|
||||
void navigate_history(int direction);
|
||||
void scroll_messages(int pages);
|
||||
void update_message_lifetimes();
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -84,6 +84,7 @@ void Document::show() {
|
||||
focus();
|
||||
}
|
||||
}
|
||||
mPendingClose = false;
|
||||
}
|
||||
|
||||
void Document::hide(bool close) {
|
||||
|
||||
@@ -23,6 +23,7 @@ public:
|
||||
bool has_focus() const;
|
||||
virtual bool visible() const;
|
||||
virtual bool active() const;
|
||||
virtual bool permanent() const { return false; }
|
||||
virtual bool obscures_game() const { return false; }
|
||||
virtual void cover() {
|
||||
mWasVisible = visible();
|
||||
@@ -70,6 +71,7 @@ public:
|
||||
Document::hide(close);
|
||||
}
|
||||
|
||||
bool pending_close() const { return mPendingClose; }
|
||||
bool closed() const { return mClosed; }
|
||||
|
||||
bool handle_nav_event(Rml::Event& event);
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#include "file_button.hpp"
|
||||
|
||||
#include <aurora/lib/window.hpp>
|
||||
#include <borealis/io.hpp>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
FileButton::FileButton(Rml::Element* parent, Props props)
|
||||
: BaseControlledSelectButton{parent, {.key = std::move(props.key)}},
|
||||
mGetValue{std::move(props.getValue)}, mSetValue{std::move(props.setValue)},
|
||||
mIsDisabled{std::move(props.isDisabled)}, mIsModified{std::move(props.isModified)},
|
||||
mFilters{std::move(props.filters)}, mDirectoryMode{props.directoryMode} {}
|
||||
|
||||
bool FileButton::modified() const {
|
||||
return mIsModified ? mIsModified() : BaseControlledSelectButton::modified();
|
||||
}
|
||||
|
||||
bool FileButton::disabled() const {
|
||||
return borealis::file_select::busy() || (mIsDisabled && mIsDisabled());
|
||||
}
|
||||
|
||||
Rml::String FileButton::format_value() {
|
||||
const Rml::String location = mGetValue ? mGetValue() : "";
|
||||
if (location.empty()) {
|
||||
return "(none)";
|
||||
}
|
||||
const auto name = borealis::io::display_name(location);
|
||||
return name.empty() ? location : name;
|
||||
}
|
||||
|
||||
bool FileButton::handle_nav_command(NavCommand command) {
|
||||
if (command != NavCommand::Confirm) {
|
||||
return false;
|
||||
}
|
||||
open_picker();
|
||||
return true;
|
||||
}
|
||||
|
||||
void FileButton::open_picker() {
|
||||
if (disabled()) {
|
||||
return;
|
||||
}
|
||||
const std::string defaultLocation = mGetValue ? mGetValue() : "";
|
||||
auto complete = [setValue = mSetValue](borealis::file_select::Result result) {
|
||||
if (result.status == borealis::file_select::Status::Selected && !result.locations.empty() &&
|
||||
setValue)
|
||||
{
|
||||
setValue(std::move(result.locations.front()));
|
||||
}
|
||||
};
|
||||
if (mDirectoryMode) {
|
||||
borealis::file_select::open_folder(
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.defaultLocation = defaultLocation,
|
||||
},
|
||||
std::move(complete));
|
||||
return;
|
||||
}
|
||||
borealis::file_select::open_file(
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.filters = mFilters,
|
||||
.defaultLocation = defaultLocation,
|
||||
},
|
||||
std::move(complete));
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "select_button.hpp"
|
||||
|
||||
#include <borealis/file_select.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class FileButton : public BaseControlledSelectButton {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
std::function<Rml::String()> getValue;
|
||||
std::function<void(Rml::String)> setValue;
|
||||
std::function<bool()> isDisabled;
|
||||
std::function<bool()> isModified;
|
||||
std::vector<borealis::file_select::Filter> filters;
|
||||
bool directoryMode = false;
|
||||
};
|
||||
|
||||
FileButton(Rml::Element* parent, Props props);
|
||||
bool modified() const override;
|
||||
bool disabled() const override;
|
||||
|
||||
protected:
|
||||
Rml::String format_value() override;
|
||||
bool handle_nav_command(NavCommand command) override;
|
||||
|
||||
private:
|
||||
void open_picker();
|
||||
|
||||
std::function<Rml::String()> mGetValue;
|
||||
std::function<void(Rml::String)> mSetValue;
|
||||
std::function<bool()> mIsDisabled;
|
||||
std::function<bool()> mIsModified;
|
||||
std::vector<borealis::file_select::Filter> mFilters;
|
||||
bool mDirectoryMode = false;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
+146
-127
@@ -1,20 +1,18 @@
|
||||
#include "graphics_tuner.hpp"
|
||||
|
||||
#include "Z2AudioLib/Z2SeMgr.h"
|
||||
#include "m_Do/m_Do_audio.h"
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
#include <aurora/gfx.h>
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
#include <dolphin/vi.h>
|
||||
#include <fmt/format.h>
|
||||
#include "button.hpp"
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/texture_replacements.hpp"
|
||||
#include "m_Do/m_Do_audio.h"
|
||||
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
@@ -40,70 +38,84 @@ const Rml::String kDocumentSource = R"RML(
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
int get_value(GraphicsOption option) {
|
||||
switch (option) {
|
||||
case GraphicsOption::InternalResolution:
|
||||
return getSettings().game.internalResolutionScale.getValue();
|
||||
case GraphicsOption::ShadowResolution:
|
||||
return getSettings().game.shadowResolutionMultiplier.getValue();
|
||||
case GraphicsOption::Resampler:
|
||||
return static_cast<int>(getSettings().game.resampler.getValue());
|
||||
case GraphicsOption::BloomMode:
|
||||
return static_cast<int>(getSettings().game.bloomMode.getValue());
|
||||
case GraphicsOption::BloomMultiplier:
|
||||
return std::clamp(
|
||||
static_cast<int>(getSettings().game.bloomMultiplier.getValue() * 100.0f + 0.5f), 0,
|
||||
100);
|
||||
case GraphicsOption::DepthOfFieldMode:
|
||||
return static_cast<int>(getSettings().game.depthOfFieldMode.getValue());
|
||||
case GraphicsOption::TextureReplacements:
|
||||
return getSettings().game.enableTextureReplacements.getValue();
|
||||
Rml::String format_internal_resolution(int value) {
|
||||
u32 width = 0;
|
||||
u32 height = 0;
|
||||
AuroraGetRenderSize(&width, &height);
|
||||
if (value <= 0) {
|
||||
return fmt::format("Auto ({}×{})", width, height);
|
||||
}
|
||||
return 0;
|
||||
return fmt::format("{}× ({}×{})", value, width, height);
|
||||
}
|
||||
|
||||
void set_value(GraphicsOption option, int value) {
|
||||
switch (option) {
|
||||
case GraphicsOption::InternalResolution:
|
||||
getSettings().game.internalResolutionScale.setValue(value);
|
||||
VISetFrameBufferScale(static_cast<float>(value));
|
||||
break;
|
||||
case GraphicsOption::ShadowResolution:
|
||||
getSettings().game.shadowResolutionMultiplier.setValue(value);
|
||||
break;
|
||||
case GraphicsOption::Resampler: {
|
||||
const auto sampler = static_cast<Resampler>(std::clamp(value,
|
||||
static_cast<int>(Resampler::Bilinear),
|
||||
static_cast<int>(Resampler::Area)));
|
||||
getSettings().game.resampler.setValue(sampler);
|
||||
switch (sampler) {
|
||||
case Resampler::Area:
|
||||
aurora_set_resampler(SAMPLER_AREA);
|
||||
break;
|
||||
case Resampler::Bilinear:
|
||||
default:
|
||||
aurora_set_resampler(SAMPLER_BILINEAR);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
Rml::String format_resampler(int value) {
|
||||
switch (static_cast<Resampler>(value)) {
|
||||
case Resampler::Bilinear:
|
||||
return "Bilinear";
|
||||
case Resampler::Area:
|
||||
return "Area";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
case GraphicsOption::BloomMode:
|
||||
getSettings().game.bloomMode.setValue(static_cast<BloomMode>(std::clamp(
|
||||
value, static_cast<int>(BloomMode::Off), static_cast<int>(BloomMode::Dusk))));
|
||||
break;
|
||||
case GraphicsOption::DepthOfFieldMode:
|
||||
getSettings().game.depthOfFieldMode.setValue(static_cast<DepthOfFieldMode>(std::clamp(
|
||||
value, static_cast<int>(DepthOfFieldMode::Off), static_cast<int>(DepthOfFieldMode::Dusk))));
|
||||
break;
|
||||
case GraphicsOption::BloomMultiplier:
|
||||
getSettings().game.bloomMultiplier.setValue(std::clamp(value, 0, 100) / 100.0f);
|
||||
break;
|
||||
case GraphicsOption::TextureReplacements:
|
||||
texture_replacements::set_enabled(static_cast<bool>(value));
|
||||
break;
|
||||
}
|
||||
|
||||
Rml::String format_post_process_mode(int value) {
|
||||
switch (static_cast<BloomMode>(value)) {
|
||||
case BloomMode::Off:
|
||||
return "Off";
|
||||
case BloomMode::Classic:
|
||||
return "Classic";
|
||||
case BloomMode::Dusk:
|
||||
return "Dusklight";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
Rml::String format_times(int value) { return fmt::format("{}×", value); }
|
||||
|
||||
Rml::String format_percent(int value) { return fmt::format("{}%", value); }
|
||||
|
||||
Rml::String format_bool(int value) { return value ? "On" : "Off"; }
|
||||
|
||||
template <typename T>
|
||||
int read_cvar(const ConfigVar<T>& var) {
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
return static_cast<int>(var.getValue() * 100.0f + 0.5f);
|
||||
} else {
|
||||
return static_cast<int>(var.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void write_cvar(ConfigVar<T>& var, int value) {
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
var.setValue(static_cast<float>(value) / 100.0f);
|
||||
} else if constexpr (std::is_same_v<T, bool>) {
|
||||
var.setValue(static_cast<bool>(value));
|
||||
} else {
|
||||
var.setValue(static_cast<T>(value));
|
||||
}
|
||||
}
|
||||
|
||||
template <auto Var, typename Min, typename Max, typename Def>
|
||||
const GraphicsSetting& bind(Min min, Max max, Def def, int step, Rml::String (*label)(int),
|
||||
bool watchSize = false) {
|
||||
static const GraphicsSetting desc{
|
||||
.min = static_cast<int>(min),
|
||||
.max = static_cast<int>(max),
|
||||
.defaultValue = static_cast<int>(def),
|
||||
.step = step,
|
||||
.watchesRenderSize = watchSize,
|
||||
.read = []() -> int { return read_cvar(Var()); },
|
||||
.write = [](int value) { write_cvar(Var(), value); },
|
||||
.label = label,
|
||||
.cvarName = []() -> const char* { return Var().getName(); },
|
||||
.isModified = []() -> bool { return Var().getValue() != Var().getDefaultValue(); },
|
||||
};
|
||||
return desc;
|
||||
}
|
||||
|
||||
Rml::Element* create_stepped_carousel_root(Rml::Element* parent) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto root = doc->CreateElement("div");
|
||||
@@ -129,6 +141,35 @@ void update_carousel_arrow_color(Rml::Element* arrow, bool dim) {
|
||||
|
||||
} // namespace
|
||||
|
||||
const GraphicsSetting& GraphicsSetting::of(GraphicsOption option) {
|
||||
switch (option) {
|
||||
case GraphicsOption::InternalResolution:
|
||||
return bind<[]() -> auto& { return getSettings().game.internalResolutionScale; }>(
|
||||
0, 12, 0, 1, format_internal_resolution, true);
|
||||
case GraphicsOption::ShadowResolution:
|
||||
return bind<[]() -> auto& { return getSettings().game.shadowResolutionMultiplier; }>(
|
||||
1, 8, 1, 1, format_times);
|
||||
case GraphicsOption::Resampler:
|
||||
return bind<[]() -> auto& { return getSettings().game.resampler; }>(
|
||||
Resampler::Bilinear, Resampler::Area, Resampler::Bilinear, 1, format_resampler);
|
||||
case GraphicsOption::BloomMode:
|
||||
return bind<[]() -> auto& { return getSettings().game.bloomMode; }>(
|
||||
BloomMode::Off, BloomMode::Dusk, BloomMode::Classic, 1, format_post_process_mode);
|
||||
case GraphicsOption::BloomMultiplier:
|
||||
return bind<[]() -> auto& { return getSettings().game.bloomMultiplier; }>(
|
||||
0, 100, 100, 10, format_percent);
|
||||
case GraphicsOption::DepthOfFieldMode:
|
||||
return bind<[]() -> auto& { return getSettings().game.depthOfFieldMode; }>(
|
||||
DepthOfFieldMode::Off, DepthOfFieldMode::Dusk, DepthOfFieldMode::Classic, 1,
|
||||
format_post_process_mode);
|
||||
case GraphicsOption::TextureReplacements:
|
||||
return bind<[]() -> auto& { return getSettings().game.enableTextureReplacements; }>(
|
||||
0, 1, 0, 1, format_bool);
|
||||
}
|
||||
DuskLog.error("{} is an invalid GraphicsOption", static_cast<int>(option));
|
||||
abort();
|
||||
}
|
||||
|
||||
SteppedCarousel::SteppedCarousel(Rml::Element* parent, Props props)
|
||||
: Component(create_stepped_carousel_root(parent)), mProps(std::move(props)) {
|
||||
mPrevElem = create_stepped_carousel_arrow(mRoot, "prev", "");
|
||||
@@ -152,7 +193,9 @@ bool SteppedCarousel::focus() {
|
||||
return Component::focus();
|
||||
}
|
||||
|
||||
void SteppedCarousel::update() {
|
||||
void SteppedCarousel::update() {}
|
||||
|
||||
void SteppedCarousel::refresh() {
|
||||
if (mValueElem == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -194,59 +237,9 @@ void SteppedCarousel::apply(int value) {
|
||||
}
|
||||
}
|
||||
|
||||
Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
|
||||
switch (option) {
|
||||
case GraphicsOption::InternalResolution: {
|
||||
u32 width = 0;
|
||||
u32 height = 0;
|
||||
AuroraGetRenderSize(&width, &height);
|
||||
if (value <= 0) {
|
||||
return fmt::format("Auto ({}×{})", width, height);
|
||||
} else {
|
||||
return fmt::format("{}× ({}×{})", value, width, height);
|
||||
}
|
||||
}
|
||||
case GraphicsOption::ShadowResolution:
|
||||
return fmt::format("{}×", value);
|
||||
case GraphicsOption::Resampler:
|
||||
switch (static_cast<Resampler>(value)) {
|
||||
case Resampler::Bilinear:
|
||||
return "Bilinear";
|
||||
case Resampler::Area:
|
||||
return "Area";
|
||||
}
|
||||
break;
|
||||
case GraphicsOption::BloomMode:
|
||||
switch (static_cast<BloomMode>(value)) {
|
||||
case BloomMode::Off:
|
||||
return "Off";
|
||||
case BloomMode::Classic:
|
||||
return "Classic";
|
||||
case BloomMode::Dusk:
|
||||
return "Dusklight";
|
||||
}
|
||||
break;
|
||||
case GraphicsOption::DepthOfFieldMode:
|
||||
switch (static_cast<DepthOfFieldMode>(value)) {
|
||||
case DepthOfFieldMode::Off:
|
||||
return "Off";
|
||||
case DepthOfFieldMode::Classic:
|
||||
return "Classic";
|
||||
case DepthOfFieldMode::Dusk:
|
||||
return "Dusklight";
|
||||
}
|
||||
break;
|
||||
case GraphicsOption::BloomMultiplier:
|
||||
return fmt::format("{}%", value);
|
||||
case GraphicsOption::TextureReplacements:
|
||||
return static_cast<bool>(value) ? "On" : "Off";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
GraphicsTuner::GraphicsTuner(GraphicsTunerProps props)
|
||||
: Document(kDocumentSource, false, DocumentScope::GraphicsTuner), mOption(props.option),
|
||||
mValueMin(props.valueMin), mValueMax(props.valueMax), mDefaultValue(props.defaultValue) {
|
||||
: Document(kDocumentSource, false, DocumentScope::GraphicsTuner),
|
||||
mSetting(GraphicsSetting::of(props.option)) {
|
||||
if (mDocument == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -260,13 +253,12 @@ GraphicsTuner::GraphicsTuner(GraphicsTunerProps props)
|
||||
if (auto* carouselParent = mDocument->GetElementById("carousel-container")) {
|
||||
mCarousel = &add_component<SteppedCarousel>(carouselParent,
|
||||
SteppedCarousel::Props{
|
||||
.min = mValueMin,
|
||||
.max = mValueMax,
|
||||
.step = props.step,
|
||||
.getValue = [this] { return get_value(mOption); },
|
||||
.onChange = [this](int value) { set_value(mOption, value); },
|
||||
.formatValue =
|
||||
[this](int value) { return format_graphics_setting_value(mOption, value); },
|
||||
.min = mSetting.min,
|
||||
.max = mSetting.max,
|
||||
.step = mSetting.step,
|
||||
.getValue = [this] { return mSetting.read(); },
|
||||
.onChange = [this](int value) { mSetting.set(value); },
|
||||
.formatValue = [this](int value) { return mSetting.label(value); },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -282,6 +274,17 @@ GraphicsTuner::GraphicsTuner(GraphicsTunerProps props)
|
||||
resetButton.root()->SetClass("reset", true);
|
||||
}
|
||||
|
||||
if (mCarousel != nullptr) {
|
||||
if (const char* name = mSetting.cvarName()) {
|
||||
mSubscription = config::subscribe(name,
|
||||
[this](config::ConfigVarBase&, const void*) { mCarousel->refresh(); });
|
||||
}
|
||||
mCarousel->refresh();
|
||||
if (mSetting.watchesRenderSize) {
|
||||
AuroraGetRenderSize(&mLastRenderWidth, &mLastRenderHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Hide document after transition completion
|
||||
mRoot = mDocument->GetElementById("root");
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
@@ -293,6 +296,12 @@ GraphicsTuner::GraphicsTuner(GraphicsTunerProps props)
|
||||
});
|
||||
}
|
||||
|
||||
GraphicsTuner::~GraphicsTuner() {
|
||||
if (mSubscription != 0) {
|
||||
config::unsubscribe(mSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
void GraphicsTuner::show() {
|
||||
Document::show();
|
||||
mRoot->SetAttribute("open", "");
|
||||
@@ -309,6 +318,16 @@ void GraphicsTuner::hide(bool close) {
|
||||
}
|
||||
|
||||
void GraphicsTuner::update() {
|
||||
if (mSetting.watchesRenderSize && mCarousel != nullptr) {
|
||||
u32 width = 0;
|
||||
u32 height = 0;
|
||||
AuroraGetRenderSize(&width, &height);
|
||||
if (width != mLastRenderWidth || height != mLastRenderHeight) {
|
||||
mLastRenderWidth = width;
|
||||
mLastRenderHeight = height;
|
||||
mCarousel->refresh();
|
||||
}
|
||||
}
|
||||
for (const auto& component : mComponents) {
|
||||
component->update();
|
||||
}
|
||||
@@ -342,7 +361,7 @@ bool GraphicsTuner::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
}
|
||||
|
||||
void GraphicsTuner::reset_default() {
|
||||
set_value(mOption, mDefaultValue);
|
||||
mSetting.set(mSetting.defaultValue);
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "component.hpp"
|
||||
#include "document.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
@@ -28,6 +28,7 @@ public:
|
||||
|
||||
bool focus() override;
|
||||
void update() override;
|
||||
void refresh();
|
||||
bool handle_nav_command(NavCommand cmd);
|
||||
|
||||
private:
|
||||
@@ -49,21 +50,34 @@ enum class GraphicsOption {
|
||||
TextureReplacements,
|
||||
};
|
||||
|
||||
Rml::String format_graphics_setting_value(GraphicsOption option, int value);
|
||||
struct GraphicsSetting {
|
||||
int min = 0;
|
||||
int max = 0;
|
||||
int defaultValue = 0;
|
||||
int step = 1;
|
||||
bool watchesRenderSize = false;
|
||||
int (*read)() = nullptr;
|
||||
void (*write)(int) = nullptr;
|
||||
Rml::String (*label)(int) = nullptr;
|
||||
const char* (*cvarName)() = nullptr;
|
||||
bool (*isModified)() = nullptr;
|
||||
|
||||
static const GraphicsSetting& of(GraphicsOption option);
|
||||
|
||||
void set(int value) const { write(std::clamp(value, min, max)); }
|
||||
Rml::String text() const { return label(read()); }
|
||||
};
|
||||
|
||||
struct GraphicsTunerProps {
|
||||
GraphicsOption option;
|
||||
Rml::String title;
|
||||
Rml::String helpText;
|
||||
int valueMin = 0;
|
||||
int valueMax = 0;
|
||||
int defaultValue = 0;
|
||||
int step = 1;
|
||||
};
|
||||
|
||||
class GraphicsTuner : public Document {
|
||||
public:
|
||||
explicit GraphicsTuner(GraphicsTunerProps props);
|
||||
~GraphicsTuner() override;
|
||||
|
||||
void show() override;
|
||||
void hide(bool close) override;
|
||||
@@ -85,13 +99,13 @@ private:
|
||||
|
||||
void reset_default();
|
||||
|
||||
GraphicsOption mOption;
|
||||
int mValueMin = 0;
|
||||
int mValueMax = 0;
|
||||
int mDefaultValue = 0;
|
||||
GraphicsSetting mSetting;
|
||||
std::vector<std::unique_ptr<Component> > mComponents;
|
||||
SteppedCarousel* mCarousel;
|
||||
SteppedCarousel* mCarousel = nullptr;
|
||||
Rml::Element* mRoot;
|
||||
u64 mSubscription = 0;
|
||||
u32 mLastRenderWidth = 0;
|
||||
u32 mLastRenderHeight = 0;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "dusk/speedrun.h"
|
||||
#include "dusk/ui/prelaunch.hpp"
|
||||
#include "editor.hpp"
|
||||
#include "f_op/f_op_scene_mng.h"
|
||||
#include "f_pc/f_pc_manager.h"
|
||||
#include "f_pc/f_pc_name.h"
|
||||
#include "imgui.h"
|
||||
@@ -159,13 +160,12 @@ void MenuBar::build_tabs() {
|
||||
});
|
||||
|
||||
if (dusk::speedrun::isActive()) {
|
||||
mTabBar->add_tab("Reset Timer", [this] {
|
||||
mTabBar->add_tab("Reset Run", [this] {
|
||||
mTabBar->set_active_tab(-1);
|
||||
mDoAud_seStartMenu(kSoundClick);
|
||||
dusk::speedrun::g_speedrunInfo.reset();
|
||||
if (getSettings().game.liveSplitEnabled) {
|
||||
dusk::speedrun::reset();
|
||||
}
|
||||
dusk::speedrun::reset();
|
||||
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
|
||||
hide(false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ public:
|
||||
void update() override;
|
||||
bool focus() override;
|
||||
bool visible() const override;
|
||||
bool permanent() const override { return true; }
|
||||
|
||||
static void refresh_tabs();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "bool_button.hpp"
|
||||
#include "color_input.hpp"
|
||||
#include "file_button.hpp"
|
||||
#include "number_button.hpp"
|
||||
#include "string_button.hpp"
|
||||
|
||||
@@ -83,6 +84,17 @@ Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec) {
|
||||
.alpha = s.colorAlpha,
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::FilePicker:
|
||||
control = &pane.add_child<FileButton>(FileButton::Props{
|
||||
.key = s.label,
|
||||
.getValue = s.getString,
|
||||
.setValue = s.setString,
|
||||
.isDisabled = s.isDisabled,
|
||||
.isModified = s.isModified,
|
||||
.filters = s.fileFilters,
|
||||
.directoryMode = s.directoryMode,
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::Select:
|
||||
if (helpPane == nullptr || s.options.empty()) {
|
||||
return nullptr;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "pane.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include <borealis/file_select.hpp>
|
||||
|
||||
#include <climits>
|
||||
|
||||
namespace dusk::ui {
|
||||
@@ -16,6 +18,7 @@ struct ModControlSpec {
|
||||
String,
|
||||
Select,
|
||||
Color,
|
||||
FilePicker,
|
||||
};
|
||||
|
||||
Kind kind = Kind::Button;
|
||||
@@ -42,6 +45,8 @@ struct ModControlSpec {
|
||||
bool stringSetOnChange = false;
|
||||
std::vector<Rml::String> colorPresets;
|
||||
bool colorAlpha = false;
|
||||
std::vector<borealis::file_select::Filter> fileFilters;
|
||||
bool directoryMode = false;
|
||||
};
|
||||
|
||||
Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec);
|
||||
|
||||
@@ -5,12 +5,14 @@
|
||||
#include "fmt/format.h"
|
||||
#include "logs_window.hpp"
|
||||
#include "mod_texture_provider.hpp"
|
||||
#include "mods/svc/http.h"
|
||||
#include "pane.hpp"
|
||||
|
||||
#include "Z2AudioLib/Z2SeMgr.h"
|
||||
#include "m_Do/m_Do_audio.h"
|
||||
|
||||
#include <memory>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -39,6 +41,13 @@ ModStatus mod_status(const mods::LoadedMod& mod) {
|
||||
return {"", "Disabled"};
|
||||
}
|
||||
|
||||
bool mod_uses_network(const mods::LoadedMod& mod) {
|
||||
return std::ranges::any_of(
|
||||
mod.manifestInfo.imports, [](const mods::ModManifestInfo::Import& serviceImport) {
|
||||
return serviceImport.id == HTTP_SERVICE_ID;
|
||||
});
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -63,16 +72,19 @@ public:
|
||||
iconRml = R"(<icon class="mod-icon placeholder"/>)";
|
||||
}
|
||||
const auto status = mod_status(mod);
|
||||
const auto networkBadge = mod_uses_network(mod) ?
|
||||
R"(<span class="mod-entry-network">Network</span>)" :
|
||||
Rml::String{};
|
||||
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-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(mod.metadata.author), status.badgeClass, status.text, networkBadge,
|
||||
escape(snippet(mod.metadata.description, 90))));
|
||||
mRoot->SetClass("inactive", !mod.active);
|
||||
mRoot->SetClass("failed", mod.loadFailed);
|
||||
@@ -225,6 +237,9 @@ void ModsWindow::build_detail(Pane& pane, mods::LoadedMod& mod) {
|
||||
statusBadge = fmt::format(
|
||||
R"( <span class="status-badge {}">{}</span>)", status.badgeClass, status.text);
|
||||
}
|
||||
if (mod_uses_network(mod)) {
|
||||
statusBadge += R"( <span class="status-badge network">Network</span>)";
|
||||
}
|
||||
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>)",
|
||||
|
||||
+10
-48
@@ -18,6 +18,7 @@
|
||||
#include <SDL3/SDL_misc.h>
|
||||
#include <aurora/lib/window.hpp>
|
||||
#include <borealis/file_select.hpp>
|
||||
#include <borealis/io.hpp>
|
||||
#include <borealis/log.hpp>
|
||||
#include <borealis/update.hpp>
|
||||
#include <borealis/version.h>
|
||||
@@ -130,42 +131,7 @@ struct DiscVerificationTask {
|
||||
std::unique_ptr<DiscVerificationTask> sDiscVerificationTask;
|
||||
bool sDiscVerificationModalPushed = false;
|
||||
|
||||
struct UpdateCheckTask {
|
||||
UpdateCheckTask() {
|
||||
worker = std::thread([this] {
|
||||
try {
|
||||
result = borealis::update::check_latest_github_release(AppInfo);
|
||||
} catch (const std::exception& e) {
|
||||
result = {
|
||||
.status = borealis::update::Status::Failed,
|
||||
.message = fmt::format("Update check failed with exception: {}", e.what()),
|
||||
};
|
||||
} catch (...) {
|
||||
result = {
|
||||
.status = borealis::update::Status::Failed,
|
||||
.message = "Update check failed with an unknown exception",
|
||||
};
|
||||
}
|
||||
done.store(true, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
~UpdateCheckTask() { join(); }
|
||||
|
||||
void join() {
|
||||
if (worker.joinable()) {
|
||||
worker.join();
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool finished() const { return done.load(std::memory_order_acquire); }
|
||||
|
||||
borealis::update::Result result;
|
||||
std::atomic_bool done = false;
|
||||
std::thread worker;
|
||||
};
|
||||
|
||||
std::unique_ptr<UpdateCheckTask> sUpdateCheckTask;
|
||||
borealis::Task<borealis::update::Result> sUpdateCheck;
|
||||
std::optional<borealis::update::Result> sUpdateCheckResult;
|
||||
|
||||
bool verification_state_allows_launch(iso::ValidationError validation) noexcept {
|
||||
@@ -243,20 +209,19 @@ void begin_update_check() {
|
||||
if (!getSettings().backend.checkForUpdates.getValue()) {
|
||||
return;
|
||||
}
|
||||
if (sUpdateCheckTask != nullptr || sUpdateCheckResult.has_value()) {
|
||||
if (sUpdateCheck || sUpdateCheckResult.has_value()) {
|
||||
return;
|
||||
}
|
||||
sUpdateCheckTask = std::make_unique<UpdateCheckTask>();
|
||||
sUpdateCheck = borealis::update::check_latest_github_release(AppInfo);
|
||||
}
|
||||
|
||||
std::optional<borealis::update::Result> take_finished_update_check() {
|
||||
if (sUpdateCheckTask == nullptr || !sUpdateCheckTask->finished()) {
|
||||
if (!sUpdateCheck || !sUpdateCheck.ready()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
sUpdateCheckTask->join();
|
||||
auto result = std::move(sUpdateCheckTask->result);
|
||||
sUpdateCheckTask.reset();
|
||||
auto result = sUpdateCheck.try_take();
|
||||
sUpdateCheck = {};
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -479,7 +444,7 @@ private:
|
||||
}
|
||||
|
||||
if (mFileName != nullptr) {
|
||||
std::string fileName = borealis::file_select::display_name(sDiscVerificationTask->path);
|
||||
std::string fileName = borealis::io::display_name(sDiscVerificationTask->path);
|
||||
if (fileName.empty()) {
|
||||
fileName = sDiscVerificationTask->path;
|
||||
}
|
||||
@@ -1167,7 +1132,7 @@ void Prelaunch::update() {
|
||||
sUpdateCheckResult = std::move(*result);
|
||||
}
|
||||
|
||||
if (sUpdateCheckTask != nullptr) {
|
||||
if (sUpdateCheck) {
|
||||
mUpdateStatus->SetAttribute("state", "checking");
|
||||
mUpdateMessage->SetInnerRML("Checking for updates...");
|
||||
} else if (!sUpdateCheckResult.has_value() ||
|
||||
@@ -1192,10 +1157,7 @@ void Prelaunch::update() {
|
||||
}
|
||||
|
||||
void return_to_prelaunch() noexcept {
|
||||
close_documents_except(DocumentScope::MenuBar);
|
||||
if (auto* menuBar = find_document(DocumentScope::MenuBar)) {
|
||||
menuBar->force_hide(false);
|
||||
}
|
||||
close_all_documents();
|
||||
push_document(std::make_unique<Prelaunch>(), true);
|
||||
}
|
||||
|
||||
|
||||
+26
-45
@@ -231,7 +231,7 @@ Rml::String configured_data_path_display_name() {
|
||||
return "(none)";
|
||||
}
|
||||
|
||||
auto display = borealis::file_select::display_name(path);
|
||||
auto display = borealis::io::display_name(path);
|
||||
if (display.empty()) {
|
||||
return path;
|
||||
}
|
||||
@@ -440,24 +440,15 @@ SelectButton& config_int_select(Pane& leftPane, Pane& rightPane, ConfigVar<int>&
|
||||
return button;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, ConfigVar<T>& var,
|
||||
void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane,
|
||||
const GraphicsTunerProps& props) {
|
||||
const auto setting = GraphicsSetting::of(props.option);
|
||||
leftPane.register_control(
|
||||
leftPane
|
||||
.add_select_button({
|
||||
.key = props.title,
|
||||
.getValue =
|
||||
[&var, option = props.option] {
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
return format_graphics_setting_value(
|
||||
option, float_setting_percent(var));
|
||||
} else {
|
||||
return format_graphics_setting_value(
|
||||
option, static_cast<int>(var.getValue()));
|
||||
}
|
||||
},
|
||||
.isModified = [&var] { return var.getValue() != var.getDefaultValue(); },
|
||||
.getValue = [setting] { return setting.text(); },
|
||||
.isModified = [setting] { return setting.isModified(); },
|
||||
.submit = false,
|
||||
})
|
||||
.on_nav_command([&window, props](Rml::Event&, NavCommand cmd) {
|
||||
@@ -493,7 +484,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
if (path.empty()) {
|
||||
display = "(none)";
|
||||
} else {
|
||||
display = borealis::file_select::display_name(path);
|
||||
display = borealis::io::display_name(path);
|
||||
if (display.empty()) {
|
||||
display = path;
|
||||
}
|
||||
@@ -539,6 +530,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.defaultLocation = defaultLocation,
|
||||
.requireRealPath = true,
|
||||
},
|
||||
&data_folder_dialog_callback);
|
||||
});
|
||||
@@ -778,75 +770,50 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
|
||||
leftPane.add_section("Resolution");
|
||||
graphics_tuner_control(*this, leftPane, rightPane,
|
||||
getSettings().game.internalResolutionScale,
|
||||
GraphicsTunerProps{
|
||||
.option = GraphicsOption::InternalResolution,
|
||||
.title = "Internal Resolution",
|
||||
.helpText = kInternalResolutionHelpText,
|
||||
.valueMin = 0,
|
||||
.valueMax = 12,
|
||||
.defaultValue = 0,
|
||||
});
|
||||
graphics_tuner_control(*this, leftPane, rightPane,
|
||||
getSettings().game.shadowResolutionMultiplier,
|
||||
GraphicsTunerProps{
|
||||
.option = GraphicsOption::ShadowResolution,
|
||||
.title = "Shadow Resolution",
|
||||
.helpText = kShadowResolutionHelpText,
|
||||
.valueMin = 1,
|
||||
.valueMax = 8,
|
||||
.defaultValue = 1,
|
||||
});
|
||||
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.resampler,
|
||||
graphics_tuner_control(*this, leftPane, rightPane,
|
||||
GraphicsTunerProps{
|
||||
.option = GraphicsOption::Resampler,
|
||||
.title = "Output Resampling",
|
||||
.helpText = kResamplerHelpText,
|
||||
.valueMin = static_cast<int>(Resampler::Bilinear),
|
||||
.valueMax = static_cast<int>(Resampler::Area),
|
||||
.defaultValue = static_cast<int>(Resampler::Bilinear),
|
||||
});
|
||||
|
||||
leftPane.add_section("Post-Processing");
|
||||
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMode,
|
||||
graphics_tuner_control(*this, leftPane, rightPane,
|
||||
GraphicsTunerProps{
|
||||
.option = GraphicsOption::BloomMode,
|
||||
.title = "Bloom",
|
||||
.helpText = kBloomHelpText,
|
||||
.valueMin = static_cast<int>(BloomMode::Off),
|
||||
.valueMax = static_cast<int>(BloomMode::Dusk),
|
||||
.defaultValue = static_cast<int>(BloomMode::Classic),
|
||||
});
|
||||
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMultiplier,
|
||||
graphics_tuner_control(*this, leftPane, rightPane,
|
||||
GraphicsTunerProps{
|
||||
.option = GraphicsOption::BloomMultiplier,
|
||||
.title = "Bloom Brightness",
|
||||
.helpText = kBloomBrightnessHelpText,
|
||||
.valueMin = 0,
|
||||
.valueMax = 100,
|
||||
.defaultValue = 100,
|
||||
.step = 10,
|
||||
});
|
||||
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.depthOfFieldMode,
|
||||
graphics_tuner_control(*this, leftPane, rightPane,
|
||||
GraphicsTunerProps{
|
||||
.option = GraphicsOption::DepthOfFieldMode,
|
||||
.title = "Depth of Field",
|
||||
.helpText = kDepthOfFieldHelpText,
|
||||
.valueMin = static_cast<int>(DepthOfFieldMode::Off),
|
||||
.valueMax = static_cast<int>(DepthOfFieldMode::Dusk),
|
||||
.defaultValue = static_cast<int>(DepthOfFieldMode::Classic),
|
||||
});
|
||||
|
||||
leftPane.add_section("Rendering");
|
||||
graphics_tuner_control(*this, leftPane, rightPane,
|
||||
getSettings().game.enableTextureReplacements,
|
||||
GraphicsTunerProps{
|
||||
.option = GraphicsOption::TextureReplacements,
|
||||
.title = "Enable Texture Replacements",
|
||||
.helpText = kTextureReplacementHelpText,
|
||||
.valueMin = static_cast<int>(false),
|
||||
.valueMax = static_cast<int>(true),
|
||||
.defaultValue = static_cast<int>(false),
|
||||
});
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
@@ -1241,7 +1208,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
{
|
||||
.key = "Speedrun Mode",
|
||||
.helpText =
|
||||
"Enables speedrunning options while restricting certain gameplay modifiers.",
|
||||
"Enables Speedrun game mode option in the Dusklight launch menu.",
|
||||
.onChange =
|
||||
[this](bool enabled) {
|
||||
if (enabled) {
|
||||
@@ -1558,6 +1525,20 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
"Recording Mode",
|
||||
"Disables the game HUD and all background music.<br/><br/>Useful for recording footage.");
|
||||
});
|
||||
|
||||
add_tab("Tools", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
leftPane.add_section("Link");
|
||||
add_speedrun_disabled_option(leftPane, rightPane, getSettings().game.enableMoveLinkCombo,
|
||||
"Move Link (L+R+Y)",
|
||||
"Enables the L+R+Y button combo to toggle freely repositioning Link.");
|
||||
add_speedrun_disabled_option(leftPane, rightPane, getSettings().game.enableTeleportCombo,
|
||||
"Teleport (R+D-pad Up/Down)",
|
||||
"R+D-pad Up stores Link's current position.<br/>"
|
||||
"R+D-pad Down teleports Link back to it.");
|
||||
});
|
||||
}
|
||||
|
||||
void SettingsWindow::update() {
|
||||
|
||||
+40
-4
@@ -18,6 +18,7 @@
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/io.hpp"
|
||||
#include <borealis/io.hpp>
|
||||
#include "command_console.hpp"
|
||||
#include "icon_provider.hpp"
|
||||
#include "input.hpp"
|
||||
#include "mod_texture_provider.hpp"
|
||||
@@ -68,6 +69,7 @@ void restyle_scope(DocumentScope scope) {
|
||||
|
||||
std::deque<Toast> sToasts;
|
||||
bool sMenuNotificationRequested = false;
|
||||
bool sConsoleShortcutHeld = false;
|
||||
|
||||
// Sometimes gamepads can connect and disconnect quickly, especially during
|
||||
// connection negotiation. In this case, we'll receive an _ADDED event for a
|
||||
@@ -106,6 +108,7 @@ void shutdown() noexcept {
|
||||
sDocumentStack.clear();
|
||||
sPassiveDocuments.clear();
|
||||
sConnectedGamepads.clear();
|
||||
sConsoleShortcutHeld = false;
|
||||
input::reset_input_state();
|
||||
input::release_input_block();
|
||||
sInitialized = false;
|
||||
@@ -217,6 +220,28 @@ void handle_event(const SDL_Event& event) noexcept {
|
||||
} else if (event.type == SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED) {
|
||||
apply_scale();
|
||||
}
|
||||
if (event.type == SDL_EVENT_WINDOW_FOCUS_LOST) {
|
||||
sConsoleShortcutHeld = false;
|
||||
}
|
||||
if (event.type == SDL_EVENT_KEY_UP && event.key.key == SDLK_SLASH && sConsoleShortcutHeld) {
|
||||
sConsoleShortcutHeld = false;
|
||||
return;
|
||||
}
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_SLASH &&
|
||||
getSettings().backend.enableAdvancedSettings)
|
||||
{
|
||||
auto* console = static_cast<CommandConsole*>(find_document(DocumentScope::CommandConsole));
|
||||
if (sConsoleShortcutHeld) {
|
||||
return;
|
||||
}
|
||||
if (console != nullptr && !console->input_active() && !event.key.repeat) {
|
||||
sConsoleShortcutHeld = true;
|
||||
bring_document_to_front(*console);
|
||||
console->show();
|
||||
input::sync_input_block();
|
||||
return;
|
||||
}
|
||||
}
|
||||
input::handle_event(event);
|
||||
}
|
||||
|
||||
@@ -262,6 +287,17 @@ Document& push_document(std::unique_ptr<Document> doc, bool show, bool passive)
|
||||
return ret;
|
||||
}
|
||||
|
||||
void bring_document_to_front(Document& doc) noexcept {
|
||||
const auto it = std::ranges::find_if(
|
||||
sDocumentStack, [&doc](const auto& entry) { return entry.get() == &doc; });
|
||||
if (it == sDocumentStack.end() || std::next(it) == sDocumentStack.end()) {
|
||||
return;
|
||||
}
|
||||
auto entry = std::move(*it);
|
||||
sDocumentStack.erase(it);
|
||||
sDocumentStack.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
void uncover_top_document() noexcept {
|
||||
if (auto* doc = top_document()) {
|
||||
doc->uncover();
|
||||
@@ -278,10 +314,10 @@ Document* find_document(DocumentScope scope) noexcept {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void close_documents_except(DocumentScope scope) noexcept {
|
||||
void close_all_documents() noexcept {
|
||||
for (auto& doc : sDocumentStack) {
|
||||
if (!doc->closed() && doc->scope() != scope) {
|
||||
doc->force_hide(true);
|
||||
if (!doc->closed()) {
|
||||
doc->force_hide(!doc->permanent());
|
||||
}
|
||||
}
|
||||
input::sync_input_block();
|
||||
@@ -289,7 +325,7 @@ void close_documents_except(DocumentScope scope) noexcept {
|
||||
|
||||
bool any_document_visible() noexcept {
|
||||
return std::any_of(sDocumentStack.begin(), sDocumentStack.end(),
|
||||
[](const auto& doc) { return doc && doc->visible(); });
|
||||
[](const auto& doc) { return doc && doc->visible() && !doc->pending_close(); });
|
||||
}
|
||||
|
||||
bool is_prelaunch_open() noexcept {
|
||||
|
||||
+3
-1
@@ -19,6 +19,7 @@ using clock = std::chrono::steady_clock;
|
||||
|
||||
enum class DocumentScope : u8 {
|
||||
None,
|
||||
CommandConsole,
|
||||
Prelaunch,
|
||||
Window,
|
||||
MenuBar,
|
||||
@@ -87,12 +88,13 @@ void update() noexcept;
|
||||
|
||||
Document& push_document(
|
||||
std::unique_ptr<Document> doc, bool show = true, bool passive = false) noexcept;
|
||||
void bring_document_to_front(Document& doc) 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;
|
||||
Document* find_document(DocumentScope scope) noexcept;
|
||||
void close_documents_except(DocumentScope scope) noexcept;
|
||||
void close_all_documents() noexcept;
|
||||
bool any_document_visible() noexcept;
|
||||
bool is_prelaunch_open() noexcept;
|
||||
bool game_obscured_below(const Document& doc) noexcept;
|
||||
|
||||
@@ -60,8 +60,12 @@ bool isWii() {
|
||||
|| getGameVersion() == GameVersion::WiiKor;
|
||||
}
|
||||
|
||||
bool isLessThanWiiJpn() {
|
||||
return getGameVersion() < GameVersion::WiiJpn;
|
||||
}
|
||||
|
||||
bool isJpnOrLessThanWiiJpn() {
|
||||
return isRegionJpn() || getGameVersion() < GameVersion::WiiJpn;
|
||||
return isRegionJpn() || isLessThanWiiJpn();
|
||||
}
|
||||
|
||||
bool isPalOrAtLeastWiiR2() {
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace dusk::version {
|
||||
|
||||
bool isGcn();
|
||||
bool isWii();
|
||||
bool isLessThanWiiJpn();
|
||||
bool isJpnOrLessThanWiiJpn();
|
||||
bool isPalOrAtLeastWiiR2();
|
||||
|
||||
|
||||
@@ -760,23 +760,7 @@ static void duskExecute() {
|
||||
isRecording = false;
|
||||
}
|
||||
|
||||
if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigX(PAD_1)) {
|
||||
if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) {
|
||||
dynamic_cast<daAlink_c*>(link)->handleWolfHowl();
|
||||
}
|
||||
}
|
||||
|
||||
if ((mDoCPd_c::getHold(PAD_1) & (PAD_TRIGGER_R | PAD_TRIGGER_L)) == PAD_TRIGGER_R && mDoCPd_c::getTrigY(PAD_1)) {
|
||||
if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) {
|
||||
dynamic_cast<daAlink_c*>(link)->handleQuickTransform();
|
||||
}
|
||||
}
|
||||
|
||||
if (dusk::getSettings().game.moonJump && (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1))) {
|
||||
if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) {
|
||||
link->speed.y = 56.0f;
|
||||
}
|
||||
}
|
||||
|
||||
if (dusk::getSettings().game.fastSpinner && mDoCPd_c::getHoldR(PAD_1)) {
|
||||
if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) {
|
||||
|
||||
@@ -1621,6 +1621,7 @@ fpc_ProcID fopAcM_createItemForBoss(const cXyz* i_pos, int i_itemNo, int i_roomN
|
||||
|
||||
if (i_itemNo != originalItemNo) {
|
||||
const u32 params = 0xFFFF0000 | param_8 << 8 | (i_itemNo & 0xFF);
|
||||
dusk::mods::set_boss_item_actor_speeds(i_speedF, i_speedY);
|
||||
return fopAcM_create(fpcNm_Obj_LifeContainer_e, params, i_pos, i_roomNo, i_angle,
|
||||
i_scale, -1, giveTag, originalItemNo);
|
||||
}
|
||||
|
||||
+52
-31
@@ -47,6 +47,7 @@
|
||||
#include <borealis/aurora_log.h>
|
||||
#include <borealis/cli.hpp>
|
||||
#include <borealis/crash.hpp>
|
||||
#include <borealis/http.hpp>
|
||||
#include <borealis/io.hpp>
|
||||
#include <borealis/sentry.hpp>
|
||||
#include <borealis/version.h>
|
||||
@@ -60,6 +61,8 @@
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/game_clock.h"
|
||||
#include "dusk/gyro.h"
|
||||
#include "dusk/commands.hpp"
|
||||
#include "dusk/game_combos.h"
|
||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||
#include "dusk/imgui/ImGuiEngine.hpp"
|
||||
#include "dusk/iso_validate.hpp"
|
||||
@@ -71,6 +74,7 @@
|
||||
#include "dusk/mouse.h"
|
||||
#include "dusk/os.h"
|
||||
#include "dusk/presentation.hpp"
|
||||
#include "dusk/ui/command_console.hpp"
|
||||
#include "dusk/ui/menu_bar.hpp"
|
||||
#include "dusk/ui/overlay.hpp"
|
||||
#include "dusk/ui/prelaunch.hpp"
|
||||
@@ -304,7 +308,9 @@ void main01(void) {
|
||||
mDoCPd_c::read();
|
||||
dusk::mouse::read();
|
||||
dusk::gyro::read(dusk::game_clock::kSimPeriod);
|
||||
dusk::processGameCombos();
|
||||
fapGm_Execute();
|
||||
dusk::processCameraCommands();
|
||||
mDoAud_Execute();
|
||||
dusk::game_clock::commit_sim_tick();
|
||||
}
|
||||
@@ -333,10 +339,12 @@ void main01(void) {
|
||||
mDoCPd_c::read();
|
||||
dusk::mouse::read();
|
||||
dusk::gyro::read(timing.dt);
|
||||
dusk::processGameCombos();
|
||||
|
||||
// EXECUTE GAME LOGIC & RENDER
|
||||
// This calls mDoGph_Painter -> JFWDisplay -> GX Functions
|
||||
fapGm_Execute();
|
||||
dusk::processCameraCommands();
|
||||
|
||||
mDoAud_Execute();
|
||||
dusk::game_clock::commit_sim_tick();
|
||||
@@ -740,16 +748,8 @@ int game_main(int argc, char* argv[]) {
|
||||
} else {
|
||||
AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH);
|
||||
}
|
||||
VISetFrameBufferScale(dusk::getSettings().game.internalResolutionScale.getValue());
|
||||
switch (dusk::getSettings().game.resampler.getValue()) {
|
||||
case dusk::Resampler::Area:
|
||||
aurora_set_resampler(SAMPLER_AREA);
|
||||
break;
|
||||
case dusk::Resampler::Bilinear:
|
||||
default:
|
||||
aurora_set_resampler(SAMPLER_BILINEAR);
|
||||
break;
|
||||
}
|
||||
dusk::applyInternalResolutionScale(dusk::getSettings().game.internalResolutionScale.getValue());
|
||||
dusk::applyResampler(dusk::getSettings().game.resampler.getValue());
|
||||
|
||||
dusk::audio::SetMasterVolume(dusk::audio::MasterVolumeToLinear(dusk::getSettings().audio.masterVolume / 100.0f));
|
||||
dusk::audio::SetEnableReverb(dusk::getSettings().audio.enableReverb);
|
||||
@@ -770,6 +770,10 @@ int game_main(int argc, char* argv[]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (borealis::http::available() && !borealis::http::initialize()) {
|
||||
DuskLog.warn("Failed to initialize the HTTP worker pool");
|
||||
}
|
||||
|
||||
if (dusk::getSettings().game.enableHighQualityMinimapTextures.getValue()) {
|
||||
dusk::hq_minimap::set_active(true);
|
||||
}
|
||||
@@ -780,18 +784,26 @@ int game_main(int argc, char* argv[]) {
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::Overlay>(), true, true);
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::TouchControls>(), false, true);
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::MenuBar>(), false);
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::CommandConsole>(), false);
|
||||
|
||||
// Invalidate a bad saved isoPath so that Dusklight can't get blocked from starting up.
|
||||
// This is only a metadata check; full hash verification is handled by the prelaunch UI.
|
||||
bool forcePreLaunchUI = false;
|
||||
bool saveConfigBeforePrelaunch = false;
|
||||
|
||||
const std::string p = dusk::getSettings().backend.isoPath;
|
||||
borealis::io::PathAccess dvdPathAccess;
|
||||
const auto resolveDvdLocation = [&dvdPathAccess](const std::string& location) {
|
||||
dvdPathAccess = borealis::io::access_path(location);
|
||||
return dvdPathAccess ? borealis::io::fs_path_to_string(dvdPathAccess.path()) : location;
|
||||
};
|
||||
|
||||
const std::string savedLocation = dusk::getSettings().backend.isoPath;
|
||||
dusk::iso::DiscInfo discInfo{};
|
||||
if (!p.empty() &&
|
||||
dusk::iso::inspect(p.c_str(), discInfo) != dusk::iso::ValidationError::Success)
|
||||
if (!savedLocation.empty() &&
|
||||
dusk::iso::inspect(savedLocation.c_str(), discInfo) != dusk::iso::ValidationError::Success)
|
||||
{
|
||||
DuskLog.warn("Saved DVD image path failed validation, clearing configured path: {}", p);
|
||||
DuskLog.warn("Saved DVD image location failed validation, clearing it: {}",
|
||||
borealis::io::display_name(savedLocation));
|
||||
dusk::getSettings().backend.isoPath.setValue("");
|
||||
dusk::getSettings().backend.isoVerification.setValue(dusk::DiscVerificationState::Unknown);
|
||||
forcePreLaunchUI = true;
|
||||
@@ -800,18 +812,23 @@ int game_main(int argc, char* argv[]) {
|
||||
|
||||
bool skipPreLaunchUI = dusk::getSettings().backend.skipPreLaunchUI.getValue();
|
||||
|
||||
std::string dvd_path = dusk::getSettings().backend.isoPath;
|
||||
std::string dvdLocation = dusk::getSettings().backend.isoPath;
|
||||
std::string dvdPath = resolveDvdLocation(dvdLocation);
|
||||
bool dvd_opened = false;
|
||||
if (parsed_arg_options.count("dvd")) {
|
||||
dvd_path = parsed_arg_options["dvd"].as<std::string>();
|
||||
if (dusk::iso::inspect(dvd_path.c_str(), discInfo) == dusk::iso::ValidationError::Success) {
|
||||
DuskLog.info("Loading DVD image from command line: {}", dvd_path);
|
||||
dvd_opened = aurora_dvd_open(dvd_path.c_str());
|
||||
dvdLocation = parsed_arg_options["dvd"].as<std::string>();
|
||||
dvdPath = resolveDvdLocation(dvdLocation);
|
||||
if (dusk::iso::inspect(dvdLocation.c_str(), discInfo) ==
|
||||
dusk::iso::ValidationError::Success)
|
||||
{
|
||||
DuskLog.info("Loading DVD image from command line: {}", dvdPath);
|
||||
dvd_opened = aurora_dvd_open(dvdPath.c_str());
|
||||
if (!dvd_opened) {
|
||||
DuskLog.warn("Failed to open DVD image from command line: {}, opening prelaunch UI", dvd_path);
|
||||
DuskLog.warn("Failed to open DVD image from command line: {}, opening prelaunch UI",
|
||||
dvdPath);
|
||||
forcePreLaunchUI = true;
|
||||
} else {
|
||||
dusk::getSettings().backend.isoPath.setValue(dvd_path);
|
||||
dusk::getSettings().backend.isoPath.setValue(dvdLocation);
|
||||
dusk::getSettings().backend.isoVerification.setValue(
|
||||
dusk::DiscVerificationState::Unknown);
|
||||
dusk::config::save();
|
||||
@@ -819,13 +836,14 @@ int game_main(int argc, char* argv[]) {
|
||||
skipPreLaunchUI = true;
|
||||
}
|
||||
} else {
|
||||
DuskLog.warn("DVD image from command line failed validation: {}, opening prelaunch UI", dvd_path);
|
||||
DuskLog.warn(
|
||||
"DVD image from command line failed validation: {}, opening prelaunch UI", dvdPath);
|
||||
forcePreLaunchUI = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If we can't load right into the game, stop requesting to load a stage or save
|
||||
if (forcePreLaunchUI || dvd_path.empty()) {
|
||||
if (forcePreLaunchUI || dvdPath.empty()) {
|
||||
if (dusk::StageRequested.set) {
|
||||
DuskLog.warn("Cannot load stage {} because no iso path is set, opening prelaunch UI",dusk::StageRequested.stage);
|
||||
dusk::StageRequested = {};
|
||||
@@ -872,6 +890,7 @@ int game_main(int argc, char* argv[]) {
|
||||
|
||||
// pre game launch ui main loop
|
||||
if (!launchUILoop()) {
|
||||
borealis::http::shutdown();
|
||||
borealis::sentry::shutdown();
|
||||
borealis::log::shutdown();
|
||||
fflush(stdout);
|
||||
@@ -885,18 +904,19 @@ int game_main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
dvd_path = dusk::getSettings().backend.isoPath;
|
||||
if (dvd_path.empty()) {
|
||||
dvdLocation = dusk::getSettings().backend.isoPath;
|
||||
dvdPath = resolveDvdLocation(dvdLocation);
|
||||
if (dvdPath.empty()) {
|
||||
DuskLog.fatal("No DVD image specified, unable to boot!");
|
||||
}
|
||||
if (!dusk::IsGameLaunched &&
|
||||
dusk::iso::inspect(dvd_path.c_str(), discInfo) != dusk::iso::ValidationError::Success)
|
||||
if (!dusk::IsGameLaunched && dusk::iso::inspect(dvdLocation.c_str(), discInfo) !=
|
||||
dusk::iso::ValidationError::Success)
|
||||
{
|
||||
DuskLog.fatal("DVD image failed validation: {}", dvd_path);
|
||||
DuskLog.fatal("DVD image failed validation: {}", dvdPath);
|
||||
}
|
||||
DuskLog.info("Loading DVD image: {}", dvd_path);
|
||||
if (!aurora_dvd_open(dvd_path.c_str())) {
|
||||
DuskLog.fatal("Failed to open DVD image: {}", dvd_path);
|
||||
DuskLog.info("Loading DVD image: {}", dvdPath);
|
||||
if (!aurora_dvd_open(dvdPath.c_str())) {
|
||||
DuskLog.fatal("Failed to open DVD image: {}", dvdPath);
|
||||
}
|
||||
|
||||
dusk::IsGameLaunched = true;
|
||||
@@ -961,6 +981,7 @@ int game_main(int argc, char* argv[]) {
|
||||
OSReport("Starting main01 (Game Loop)...\n");
|
||||
|
||||
main01();
|
||||
borealis::http::shutdown();
|
||||
|
||||
// We need to cleanly shut down the threads to avoid crashes on shutdown.
|
||||
if (daMP_c::m_myObj) {
|
||||
|
||||
Reference in New Issue
Block a user