mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-04 19:25:43 -04:00
Compare commits
165 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27274e7341 | |||
| 1cb8b19520 | |||
| d109cd891e | |||
| 63c4002ca6 | |||
| 6954efcd15 | |||
| 78c2147771 | |||
| c938d5468e | |||
| 35590c5312 | |||
| e976b10e2a | |||
| 3949706b28 | |||
| ce55916845 | |||
| 9b252cbdd2 | |||
| 97fa09f6ee | |||
| 0e7a7cccb9 | |||
| 1c0cdcc176 | |||
| 280305c2ba | |||
| d1f1d579bc | |||
| eeeb3ffe25 | |||
| 1ee0f862e1 | |||
| 6b327c9f61 | |||
| f148e0ebc1 | |||
| 6f20f4d629 | |||
| 94670270d6 | |||
| 8e21247a97 | |||
| aa84004cb4 | |||
| e1c201f4bd | |||
| 425a1d15a0 | |||
| e3ad41792a | |||
| c1ba10fc8b | |||
| 1c5686f71b | |||
| b9e0f2b9ca | |||
| ede175f141 | |||
| a301874e30 | |||
| 9b4a9a6bd9 | |||
| 04274b1884 | |||
| ca783b8424 | |||
| 2e4c2b5b46 | |||
| 4c09d8b910 | |||
| fb1b260d09 | |||
| 98eb8f718e | |||
| 68b2e0ee2d | |||
| 81771a0522 | |||
| 93c8bcc210 | |||
| 176bf5f0c4 | |||
| 65a5945778 | |||
| 8f7b9cdfdd | |||
| acecba7ff7 | |||
| 507e0aadbc | |||
| 8406d9b192 | |||
| 6f3170cb56 | |||
| 835582224e | |||
| 52879f50f0 | |||
| 2b505f1be4 | |||
| f089f9024d | |||
| 43fb421a93 | |||
| f06d6b50a9 | |||
| 2c987b0211 | |||
| 3d860ad454 | |||
| b5bf19569b | |||
| 5a7f5cb4a7 | |||
| 1affefbbfc | |||
| e4ff38a712 | |||
| f2ac4d6f44 | |||
| 48b98a5432 | |||
| a4752154f7 | |||
| 0d973a497b | |||
| ab4eccf1df | |||
| e0c449f28e | |||
| ce9a5c06d5 | |||
| 2bbba1e4e8 | |||
| 39298f8d8f | |||
| 32b4c0567a | |||
| 7ff1b5332e | |||
| 5e77a60bd6 | |||
| 9629c000bd | |||
| 9dc5fed686 | |||
| 2cc9db77dd | |||
| 8aa08c9443 | |||
| dca3e2eba6 | |||
| 57061fba38 | |||
| cee6a24309 | |||
| 62edb3abc6 | |||
| 1c9e1c0a66 | |||
| a06aeb10c1 | |||
| b84b309e00 | |||
| 62eecb3ccd | |||
| ec07572ced | |||
| b45e2fa34d | |||
| 289a718446 | |||
| 292a2a6c34 | |||
| 2e8415b950 | |||
| dbf1f6e354 | |||
| 9a2fe9745d | |||
| b5ca343fac | |||
| 88c7ff63ff | |||
| 7a438ad30f | |||
| 89649de7c3 | |||
| a1960eaa33 | |||
| d6820c9233 | |||
| 5c4bb8d33d | |||
| e3ce1f01c9 | |||
| 9b6b344ecf | |||
| 3db85d5b44 | |||
| b5871d72d9 | |||
| b70a714f88 | |||
| cad5a8d1bc | |||
| 93f8a5fa8f | |||
| b0809ea78c | |||
| b0e9033736 | |||
| ce0d89058a | |||
| 1ac6df8de7 | |||
| 5899b2157a | |||
| 3185f578fb | |||
| 4fc09799b6 | |||
| fe0e3cad72 | |||
| 37d1aa7f40 | |||
| 4a12554bf4 | |||
| fecd1d5683 | |||
| bce9bf6fd9 | |||
| fbf63b075a | |||
| b86d6e90e2 | |||
| 6425b452e7 | |||
| 1657fe8083 | |||
| ecd74a4cbd | |||
| 36dc43c602 | |||
| d92515f0d4 | |||
| f147dcac0c | |||
| ee4c84f39b | |||
| b8a83c6f59 | |||
| 4462c0ef69 | |||
| 3cb7fbd030 | |||
| 1e372a856d | |||
| b48d9aa052 | |||
| d899706208 | |||
| 9a7b62cbc6 | |||
| 8e0f0e878e | |||
| 79344edf0d | |||
| e59bfd1a9c | |||
| f7b880c5ea | |||
| ff78bc8d6c | |||
| 6503b4e7eb | |||
| 8fb4ba8924 | |||
| 5f675c6f2b | |||
| b3333241c5 | |||
| e39079c0f8 | |||
| c3317d9232 | |||
| 36092f1fdb | |||
| 5eb3184174 | |||
| 025cb58493 | |||
| b3dee825e8 | |||
| f6c5aac3c8 | |||
| 25e9064d09 | |||
| 3e1e8f1244 | |||
| 0bf663141a | |||
| d7dced7ddf | |||
| 78b0563c0e | |||
| 871d18e294 | |||
| c157564da6 | |||
| ecc3b00c51 | |||
| 8afb1141ab | |||
| 8c5673d9b8 | |||
| 916dfcd9da | |||
| 842210e539 | |||
| 39d951d0cb | |||
| a4be0841e5 |
+1
-1
@@ -2,7 +2,7 @@
|
||||
Language: Cpp
|
||||
Standard: C++03
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignAfterOpenBracket: DontAlign
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignOperands: true
|
||||
|
||||
@@ -100,6 +100,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL Linux)
|
||||
endif ()
|
||||
set(AURORA_ENABLE_DVD ON CACHE BOOL "Enable DVD API support" FORCE)
|
||||
set(AURORA_ENABLE_CARD ON CACHE BOOL "Enable CARD API support" FORCE)
|
||||
set(AURORA_ENABLE_RMLUI ON CACHE BOOL "Enable RmlUi UI support" FORCE)
|
||||
add_subdirectory(extern/aurora EXCLUDE_FROM_ALL)
|
||||
|
||||
add_subdirectory(libs/freeverb)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
|
||||
- ### **[Official Website](https://twilitrealm.dev)**
|
||||
- ### **[Discord](https://discord.gg/QACynxeyna)**
|
||||
|
||||
Vendored
+1
-1
Submodule extern/aurora updated: c77a4d0c3c...41d5c9c5a2
+42
-1
@@ -1,7 +1,7 @@
|
||||
|
||||
set(DOLZEL_FILES
|
||||
src/m_Do/m_Do_main.cpp
|
||||
src/m_Do/m_Do_printf.cpp
|
||||
#src/m_Do/m_Do_printf.cpp
|
||||
src/m_Do/m_Do_audio.cpp
|
||||
src/m_Do/m_Do_controller_pad.cpp
|
||||
#src/m_Do/m_Re_controller_pad.cpp
|
||||
@@ -1429,6 +1429,7 @@ set(DUSK_FILES
|
||||
src/dusk/globals.cpp
|
||||
src/dusk/gyro.cpp
|
||||
src/dusk/gamepad_color.cpp
|
||||
src/dusk/autosave.cpp
|
||||
src/dusk/io.cpp
|
||||
src/dusk/layout.cpp
|
||||
src/dusk/logging.cpp
|
||||
@@ -1462,11 +1463,51 @@ set(DUSK_FILES
|
||||
src/dusk/imgui/ImGuiStateShare.cpp
|
||||
src/dusk/imgui/ImGuiAchievements.hpp
|
||||
src/dusk/imgui/ImGuiAchievements.cpp
|
||||
src/dusk/ui/bool_button.cpp
|
||||
src/dusk/ui/bool_button.hpp
|
||||
src/dusk/ui/button.cpp
|
||||
src/dusk/ui/button.hpp
|
||||
src/dusk/ui/component.cpp
|
||||
src/dusk/ui/component.hpp
|
||||
src/dusk/ui/document.cpp
|
||||
src/dusk/ui/document.hpp
|
||||
src/dusk/ui/editor.cpp
|
||||
src/dusk/ui/editor.hpp
|
||||
src/dusk/ui/event.cpp
|
||||
src/dusk/ui/event.hpp
|
||||
src/dusk/ui/input.cpp
|
||||
src/dusk/ui/input.hpp
|
||||
src/dusk/ui/nav_types.hpp
|
||||
src/dusk/ui/number_button.cpp
|
||||
src/dusk/ui/number_button.hpp
|
||||
src/dusk/ui/overlay.cpp
|
||||
src/dusk/ui/overlay.hpp
|
||||
src/dusk/ui/pane.cpp
|
||||
src/dusk/ui/pane.hpp
|
||||
src/dusk/ui/popup.cpp
|
||||
src/dusk/ui/popup.hpp
|
||||
src/dusk/ui/prelaunch.cpp
|
||||
src/dusk/ui/prelaunch.hpp
|
||||
src/dusk/ui/prelaunch_options.cpp
|
||||
src/dusk/ui/prelaunch_options.hpp
|
||||
src/dusk/ui/select_button.cpp
|
||||
src/dusk/ui/select_button.hpp
|
||||
src/dusk/ui/settings.cpp
|
||||
src/dusk/ui/settings.hpp
|
||||
src/dusk/ui/string_button.cpp
|
||||
src/dusk/ui/string_button.hpp
|
||||
src/dusk/ui/tab_bar.cpp
|
||||
src/dusk/ui/tab_bar.hpp
|
||||
src/dusk/ui/ui.cpp
|
||||
src/dusk/ui/ui.hpp
|
||||
src/dusk/ui/window.cpp
|
||||
src/dusk/ui/window.hpp
|
||||
src/dusk/achievements.cpp
|
||||
src/dusk/iso_validate.cpp
|
||||
src/dusk/livesplit.cpp
|
||||
src/dusk/offset_ptr.cpp
|
||||
src/dusk/OSContext.cpp
|
||||
src/dusk/OSReport.cpp
|
||||
src/dusk/OSThread.cpp
|
||||
src/dusk/OSMutex.cpp
|
||||
src/dusk/discord_presence.cpp
|
||||
|
||||
@@ -27,6 +27,7 @@ public:
|
||||
/* 0x17C */ cXyz mViewScale;
|
||||
#if TARGET_PC
|
||||
bool mbReset = false;
|
||||
bool mbHadEntry = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -91,6 +91,10 @@ public:
|
||||
void calcCursor();
|
||||
void drawCursor();
|
||||
|
||||
#if TARGET_PC
|
||||
void dMapBgWide();
|
||||
#endif
|
||||
|
||||
void setDPDFloorSelCurPos(s8 i_pos) { field_0xdd6 = i_pos; }
|
||||
|
||||
f32 getMapWidth() { return mMapWidth; }
|
||||
|
||||
@@ -81,6 +81,10 @@ public:
|
||||
void calcDrawPriority();
|
||||
void setArrowPosAxis(f32, f32);
|
||||
|
||||
#if TARGET_PC
|
||||
void fMapBackWide();
|
||||
#endif
|
||||
|
||||
virtual void draw();
|
||||
virtual ~dMenu_Fmap2DBack_c();
|
||||
|
||||
@@ -330,6 +334,10 @@ public:
|
||||
void setHIO(bool);
|
||||
bool isWarpAccept();
|
||||
|
||||
#if TARGET_PC
|
||||
void fMapTopWide();
|
||||
#endif
|
||||
|
||||
virtual void draw();
|
||||
virtual ~dMenu_Fmap2DTop_c();
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef AUTOSAVE_H
|
||||
#define AUTOSAVE_H
|
||||
|
||||
#include <m_Do/m_Do_MemCardRWmng.h>
|
||||
#include <m_Do/m_Do_MemCard.h>
|
||||
|
||||
void noAutoSave();
|
||||
void triggerAutoSave();
|
||||
void updateAutoSave();
|
||||
void enterAutoSave();
|
||||
void autoSaving();
|
||||
void waitingForWrite();
|
||||
void endAutoSave();
|
||||
|
||||
#endif
|
||||
@@ -55,6 +55,7 @@ struct UserSettings {
|
||||
ConfigVar<int> soundEffectsVolume;
|
||||
ConfigVar<int> fanfareVolume;
|
||||
ConfigVar<bool> enableReverb;
|
||||
ConfigVar<bool> enableHrtf;
|
||||
} audio;
|
||||
|
||||
// Game settings
|
||||
@@ -71,6 +72,7 @@ struct UserSettings {
|
||||
ConfigVar<bool> disableRupeeCutscenes;
|
||||
ConfigVar<bool> noSwordRecoil;
|
||||
ConfigVar<int> damageMultiplier;
|
||||
ConfigVar<bool> hyperEnemies;
|
||||
ConfigVar<bool> noHeartDrops;
|
||||
ConfigVar<bool> instantDeath;
|
||||
ConfigVar<bool> fastClimbing;
|
||||
@@ -80,6 +82,7 @@ struct UserSettings {
|
||||
ConfigVar<bool> instantSaves;
|
||||
ConfigVar<bool> instantText;
|
||||
ConfigVar<bool> sunsSong;
|
||||
ConfigVar<bool> autoSave;
|
||||
|
||||
// Preferences
|
||||
ConfigVar<bool> enableMirrorMode;
|
||||
|
||||
@@ -59,6 +59,9 @@ public:
|
||||
bool isActive() const { return mSeqList.getNumLinks() != 0; }
|
||||
int getNumActiveSeqs() const { return mSeqList.getNumLinks(); }
|
||||
void pause(bool paused) { mActivity.field_0x0.flags.flag2 = paused; }
|
||||
#if TARGET_PC
|
||||
JSUList<JAISeq>* getSeqList() { return &mSeqList; }
|
||||
#endif
|
||||
|
||||
private:
|
||||
/* 0x08 */ JAIAudience* mAudience;
|
||||
|
||||
@@ -207,4 +207,11 @@ void JPARegistAlphaEnv(JPAEmitterWorkData*, JPABaseParticle*);
|
||||
void JPARegistPrmAlpha(JPAEmitterWorkData*, JPABaseParticle*);
|
||||
void JPARegistPrmAlphaEnv(JPAEmitterWorkData*, JPABaseParticle*);
|
||||
|
||||
#if TARGET_PC
|
||||
void JPAInterpBillboard(JPAEmitterWorkData*, JPABaseParticle*);
|
||||
void JPAInterpRotBillboard(JPAEmitterWorkData*, JPABaseParticle*);
|
||||
void JPAInterpDirection(JPAEmitterWorkData*, JPABaseParticle*);
|
||||
void JPAInterpRotDirection(JPAEmitterWorkData*, JPABaseParticle*);
|
||||
#endif
|
||||
|
||||
#endif /* JPABASESHAPE_H */
|
||||
|
||||
@@ -24,6 +24,9 @@ public:
|
||||
void init_c(JPAEmitterWorkData*, JPABaseParticle*);
|
||||
bool calc_p(JPAEmitterWorkData*);
|
||||
bool calc_c(JPAEmitterWorkData*);
|
||||
#if TARGET_PC
|
||||
void interp(JPAEmitterWorkData*, void const* drawFunc);
|
||||
#endif
|
||||
bool canCreateChild(JPAEmitterWorkData*);
|
||||
f32 getWidth(JPABaseEmitter const*) const;
|
||||
f32 getHeight(JPABaseEmitter const*) const;
|
||||
|
||||
@@ -40,6 +40,9 @@ public:
|
||||
JUTTransparency getTransparency() const { return JUTTransparency(mTransparency); }
|
||||
u16 getNumColors() const { return mNumColors; }
|
||||
ResTLUT* getColorTable() const { return mColorTable; }
|
||||
#if TARGET_PC
|
||||
void dataUploaded();
|
||||
#endif
|
||||
|
||||
private:
|
||||
/* 0x00 */ GXTlutObj mTlutObj;
|
||||
|
||||
@@ -75,6 +75,7 @@ public:
|
||||
s32 getTransparency() const { return mTexInfo->alphaEnabled; }
|
||||
s32 getWidth() const { return mTexInfo->width; }
|
||||
s32 getHeight() const { return mTexInfo->height; }
|
||||
JUTPalette* getPalette() const { return mPalette; }
|
||||
void setCaptureFlag(bool flag) { mFlags &= 2 | flag; }
|
||||
bool getCaptureFlag() const { return mFlags & 1; }
|
||||
bool getEmbPaletteDelFlag() const { return mFlags & 2; }
|
||||
@@ -82,7 +83,7 @@ public:
|
||||
int getTlutName() const { return mTlutName; }
|
||||
bool operator==(const JUTTexture& other) {
|
||||
return mTexInfo == other.mTexInfo
|
||||
&& field_0x2c == other.field_0x2c
|
||||
&& mPalette == other.mPalette
|
||||
&& mWrapS == other.mWrapS
|
||||
&& mWrapT == other.mWrapT
|
||||
&& mMinFilter == other.mMinFilter
|
||||
@@ -100,7 +101,7 @@ private:
|
||||
/* 0x20 */ const ResTIMG* mTexInfo;
|
||||
/* 0x24 */ void* mTexData;
|
||||
/* 0x28 */ JUTPalette* mEmbPalette;
|
||||
/* 0x2C */ JUTPalette* field_0x2c;
|
||||
/* 0x2C */ JUTPalette* mPalette;
|
||||
/* 0x30 */ u8 mWrapS;
|
||||
/* 0x31 */ u8 mWrapT;
|
||||
/* 0x32 */ u8 mMinFilter;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "JSystem/JSystem.h" // IWYU pragma: keep
|
||||
|
||||
#include "JSystem/JAudio2/JASChannel.h"
|
||||
#if TARGET_PC
|
||||
#include "dusk/audio/DuskDsp.hpp"
|
||||
#endif
|
||||
#include "JSystem/JAudio2/JASAiCtrl.h"
|
||||
#include "JSystem/JAudio2/JASCalc.h"
|
||||
#include "JSystem/JAudio2/JASDriverIF.h"
|
||||
@@ -170,7 +173,12 @@ void JASChannel::updateEffectorParam(JASDsp::TChannel* i_channel, u16* i_mixerVo
|
||||
|
||||
f32 pan = 0.5f;
|
||||
f32 dolby = 0.0f;
|
||||
switch (JASDriver::getOutputMode()) {
|
||||
#if TARGET_PC
|
||||
u32 effectiveOutputMode = dusk::audio::EnableHrtf ? JAS_OUTPUT_SURROUND : JASDriver::getOutputMode();
|
||||
#else
|
||||
u32 effectiveOutputMode = JASDriver::getOutputMode();
|
||||
#endif
|
||||
switch (effectiveOutputMode) {
|
||||
case JAS_OUTPUT_MONO:
|
||||
break;
|
||||
case JAS_OUTPUT_STEREO:
|
||||
|
||||
@@ -302,7 +302,6 @@ void JASKernel::setupRootHeap(JKRSolidHeap* heap, u32 size) {
|
||||
JKRHEAP_NAME(sSystemHeap, "JASKernel::sSystemHeap");
|
||||
JUT_ASSERT(787, sSystemHeap);
|
||||
sCommandHeap = JKR_NEW_ARGS (heap, 0) JASMemChunkPool<1024, JASThreadingModel::ObjectLevelLockable>;
|
||||
JKRHEAP_NAME(sSystemHeap, "JASKernel::sCommandHeap");
|
||||
JUT_ASSERT(790, sCommandHeap);
|
||||
JASDram = heap;
|
||||
}
|
||||
|
||||
@@ -442,6 +442,7 @@ static JAUSectionHeap* JAUNewSectionHeap(JKRSolidHeap* heap, bool param_1) {
|
||||
JAUSectionHeap* JAUNewSectionHeap(bool param_0) {
|
||||
s32 freeSize = JASDram->getFreeSize();
|
||||
JKRSolidHeap* sectionHeap = JKRCreateSolidHeap(freeSize, JASDram, true);
|
||||
JKRHEAP_NAME(sectionHeap, "sectionHeap");
|
||||
JUT_ASSERT(821, sectionHeap);
|
||||
return JAUNewSectionHeap(sectionHeap, param_0);
|
||||
}
|
||||
|
||||
@@ -222,16 +222,11 @@ void* JKRExpHeap::do_alloc(u32 size, int alignment) {
|
||||
OSReport_Error("Free block list as follows:\n");
|
||||
OSReport_Error("Start | End | Size \n");
|
||||
|
||||
int i = 0;
|
||||
for (const CMemBlock* block = mHeadFreeList; block; block = block->mNext) {
|
||||
if (block->mMagic) {
|
||||
// Allocated, ignore.
|
||||
continue;
|
||||
}
|
||||
if (i++ > 10) {
|
||||
OSReport_Error("<more>\n");
|
||||
break;
|
||||
}
|
||||
|
||||
auto blockStart = (uintptr_t)block - (uintptr_t)mStart;
|
||||
auto blockEnd = (uintptr_t)block + block->size - (uintptr_t)mStart;
|
||||
@@ -239,6 +234,14 @@ void* JKRExpHeap::do_alloc(u32 size, int alignment) {
|
||||
OSReport_Error("%08X | %08X | %08X\n", (u32) blockStart, (u32) blockEnd, (u32) blockSize);
|
||||
}
|
||||
|
||||
OSReport_Error("Child heaps as follows:\n");
|
||||
OSReport_Error("Start | End | Name \n");
|
||||
|
||||
const JSUTree<JKRHeap>& tree = getHeapTree();
|
||||
for (JSUTreeIterator iter(tree.getFirstChild()); iter != tree.getEndChild(); ++iter) {
|
||||
OSReport_Error("%08X | %08X | %s\n", iter->getStartAddr(), iter->getEndAddr(), iter->getName());
|
||||
}
|
||||
|
||||
CRASH("Aborting due to allocation failure!");
|
||||
}
|
||||
#else
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
#include <mtx.h>
|
||||
#include <gx.h>
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#endif
|
||||
#include "tracy/Tracy.hpp"
|
||||
|
||||
void JPASetPointSize(JPAEmitterWorkData* work) {
|
||||
@@ -418,50 +421,95 @@ static projectionFunc p_prj[3] = {
|
||||
loadPrjAnm,
|
||||
};
|
||||
|
||||
void JPADrawBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) {
|
||||
if (param_1->checkStatus(JPAPtclStts_Invisible)) {
|
||||
#if TARGET_PC
|
||||
void JPAInterpBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
|
||||
Mtx ptclPosMtx;
|
||||
MTXTrans(ptclPosMtx, ptcl->mPosition.x, ptcl->mPosition.y, ptcl->mPosition.z);
|
||||
dusk::frame_interp::record_final_mtx(ptclPosMtx, ptcl);
|
||||
}
|
||||
|
||||
void JPAInterpRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
|
||||
Mtx ptclPosMtx;
|
||||
f32 sinRot = JMASSin(ptcl->mRotateAngle);
|
||||
f32 cosRot = JMASCos(ptcl->mRotateAngle);
|
||||
MTXTrans(ptclPosMtx, ptcl->mPosition.x, ptcl->mPosition.y, ptcl->mPosition.z);
|
||||
ptclPosMtx[0][0] = cosRot;
|
||||
ptclPosMtx[0][1] = -sinRot;
|
||||
ptclPosMtx[1][0] = sinRot;
|
||||
ptclPosMtx[1][1] = cosRot;
|
||||
dusk::frame_interp::record_final_mtx(ptclPosMtx, ptcl);
|
||||
}
|
||||
#endif
|
||||
|
||||
void JPADrawBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
|
||||
if (ptcl->checkStatus(JPAPtclStts_Invisible)) {
|
||||
return;
|
||||
}
|
||||
|
||||
JGeometry::TVec3<f32> local_48;
|
||||
MTXMultVec(work->mPosCamMtx, ¶m_1->mPosition, &local_48);
|
||||
Mtx local_38;
|
||||
local_38[0][0] = work->mGlobalPtclScl.x * param_1->mParticleScaleX;
|
||||
local_38[0][3] = local_48.x;
|
||||
local_38[1][1] = work->mGlobalPtclScl.y * param_1->mParticleScaleY;
|
||||
local_38[1][3] = local_48.y;
|
||||
local_38[2][2] = 1.0f;
|
||||
local_38[2][3] = local_48.z;
|
||||
local_38[0][1] = local_38[0][2] = local_38[1][0] = local_38[1][2] = local_38[2][0] = local_38[2][1] = 0.0f;
|
||||
GXLoadPosMtxImm(local_38, 0);
|
||||
p_prj[work->mPrjType](work, local_38);
|
||||
JGeometry::TVec3<f32> pos;
|
||||
#if TARGET_PC
|
||||
Mtx ptclPosMtx;
|
||||
if (dusk::frame_interp::lookup_replacement(ptcl, ptclPosMtx)) {
|
||||
pos.set(ptclPosMtx[0][3], ptclPosMtx[1][3], ptclPosMtx[2][3]);
|
||||
MTXMultVec(work->mPosCamMtx, &pos, &pos);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
MTXMultVec(work->mPosCamMtx, &ptcl->mPosition, &pos);
|
||||
}
|
||||
Mtx posMtx;
|
||||
posMtx[0][0] = work->mGlobalPtclScl.x * ptcl->mParticleScaleX;
|
||||
posMtx[0][3] = pos.x;
|
||||
posMtx[1][1] = work->mGlobalPtclScl.y * ptcl->mParticleScaleY;
|
||||
posMtx[1][3] = pos.y;
|
||||
posMtx[2][2] = 1.0f;
|
||||
posMtx[2][3] = pos.z;
|
||||
posMtx[0][1] = posMtx[0][2] = posMtx[1][0] = posMtx[1][2] = posMtx[2][0] = posMtx[2][1] = 0.0f;
|
||||
GXLoadPosMtxImm(posMtx, GX_PNMTX0);
|
||||
p_prj[work->mPrjType](work, posMtx);
|
||||
GXCallDisplayList(jpa_dl, sizeof(jpa_dl));
|
||||
}
|
||||
|
||||
void JPADrawRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) {
|
||||
if (param_1->checkStatus(JPAPtclStts_Invisible)) {
|
||||
void JPADrawRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
|
||||
if (ptcl->checkStatus(JPAPtclStts_Invisible)) {
|
||||
return;
|
||||
}
|
||||
|
||||
JGeometry::TVec3<f32> local_48;
|
||||
MTXMultVec(work->mPosCamMtx, ¶m_1->mPosition, &local_48);
|
||||
f32 sinRot = JMASSin(param_1->mRotateAngle);
|
||||
f32 cosRot = JMASCos(param_1->mRotateAngle);
|
||||
f32 particleX = work->mGlobalPtclScl.x * param_1->mParticleScaleX;
|
||||
f32 particleY = work->mGlobalPtclScl.y * param_1->mParticleScaleY;
|
||||
if (work->mpRes->getUsrIdx() == 0x89d7) {
|
||||
int a = 0;
|
||||
}
|
||||
|
||||
Mtx local_38;
|
||||
local_38[0][0] = cosRot * particleX;
|
||||
local_38[0][1] = -sinRot * particleY;
|
||||
local_38[0][3] = local_48.x;
|
||||
local_38[1][0] = sinRot * particleX;
|
||||
local_38[1][1] = cosRot * particleY;
|
||||
local_38[1][3] = local_48.y;
|
||||
local_38[2][2] = 1.0f;
|
||||
local_38[2][3] = local_48.z;
|
||||
local_38[0][2] = local_38[1][2] = local_38[2][0] = local_38[2][1] = 0.0f;
|
||||
GXLoadPosMtxImm(local_38, 0);
|
||||
p_prj[work->mPrjType](work, local_38);
|
||||
JGeometry::TVec3<f32> pos;
|
||||
f32 sinRot, cosRot;
|
||||
#if TARGET_PC
|
||||
Mtx ptclPosMtx;
|
||||
MTXTrans(ptclPosMtx, ptcl->mPosition.x, ptcl->mPosition.y, ptcl->mPosition.z);
|
||||
if (dusk::frame_interp::lookup_replacement(ptcl, ptclPosMtx)) {
|
||||
pos.set(ptclPosMtx[0][3], ptclPosMtx[1][3], ptclPosMtx[2][3]);
|
||||
sinRot = ptclPosMtx[1][0];
|
||||
cosRot = ptclPosMtx[0][0];
|
||||
MTXMultVec(work->mPosCamMtx, &pos, &pos);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
MTXMultVec(work->mPosCamMtx, &ptcl->mPosition, &pos);
|
||||
sinRot = JMASSin(ptcl->mRotateAngle);
|
||||
cosRot = JMASCos(ptcl->mRotateAngle);
|
||||
}
|
||||
f32 particleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX;
|
||||
f32 particleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY;
|
||||
Mtx posMtx;
|
||||
posMtx[0][0] = cosRot * particleX;
|
||||
posMtx[0][1] = -sinRot * particleY;
|
||||
posMtx[0][3] = pos.x;
|
||||
posMtx[1][0] = sinRot * particleX;
|
||||
posMtx[1][1] = cosRot * particleY;
|
||||
posMtx[1][3] = pos.y;
|
||||
posMtx[2][2] = 1.0f;
|
||||
posMtx[2][3] = pos.z;
|
||||
posMtx[0][2] = posMtx[1][2] = posMtx[2][0] = posMtx[2][1] = 0.0f;
|
||||
GXLoadPosMtxImm(posMtx, GX_PNMTX0);
|
||||
p_prj[work->mPrjType](work, posMtx);
|
||||
GXCallDisplayList(jpa_dl, sizeof(jpa_dl));
|
||||
}
|
||||
|
||||
@@ -484,7 +532,7 @@ void JPADrawYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) {
|
||||
local_38[2][2] = work->mYBBCamMtx[2][2];
|
||||
local_38[2][3] = local_48.z;
|
||||
local_38[0][1] = local_38[0][2] = local_38[1][0] = local_38[2][0] = 0.0f;
|
||||
GXLoadPosMtxImm(local_38, 0);
|
||||
GXLoadPosMtxImm(local_38, GX_PNMTX0);
|
||||
p_prj[work->mPrjType](work, local_38);
|
||||
GXCallDisplayList(jpa_dl, sizeof(jpa_dl));
|
||||
}
|
||||
@@ -517,7 +565,7 @@ void JPADrawRotYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) {
|
||||
local_38[2][1] = local_94 * fVar1;
|
||||
local_38[2][2] = local_90;
|
||||
local_38[2][3] = local_48.z;
|
||||
GXLoadPosMtxImm(local_38, 0);
|
||||
GXLoadPosMtxImm(local_38, GX_PNMTX0);
|
||||
p_prj[work->mPrjType](work, local_38);
|
||||
GXCallDisplayList(jpa_dl, sizeof(jpa_dl));
|
||||
}
|
||||
@@ -681,103 +729,197 @@ static u8* p_dl[2] = {
|
||||
jpa_dl_x,
|
||||
};
|
||||
|
||||
void JPADrawDirection(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) {
|
||||
if (param_1->checkStatus(JPAPtclStts_Invisible)) {
|
||||
#if TARGET_PC
|
||||
void JPAInterpDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
|
||||
JGeometry::TVec3<f32> axisY;
|
||||
JGeometry::TVec3<f32> axisZ;
|
||||
p_direction[work->mDirType](work, ptcl, &axisY);
|
||||
|
||||
if (axisY.isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ZoneScoped;
|
||||
axisY.normalize();
|
||||
axisZ.cross(ptcl->mBaseAxis, axisY);
|
||||
|
||||
JGeometry::TVec3<f32> local_6c;
|
||||
JGeometry::TVec3<f32> local_78;
|
||||
p_direction[param_0->mDirType](param_0, param_1, &local_6c);
|
||||
|
||||
if (local_6c.isZero()) {
|
||||
if (axisZ.isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
local_6c.normalize();
|
||||
local_78.cross(param_1->mBaseAxis, local_6c);
|
||||
|
||||
if (local_78.isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
local_78.normalize();
|
||||
param_1->mBaseAxis.cross(local_6c, local_78);
|
||||
param_1->mBaseAxis.normalize();
|
||||
Mtx local_60;
|
||||
f32 fVar1 = param_0->mGlobalPtclScl.x * param_1->mParticleScaleX;
|
||||
f32 fVar2 = param_0->mGlobalPtclScl.y * param_1->mParticleScaleY;
|
||||
local_60[0][0] = param_1->mBaseAxis.x;
|
||||
local_60[0][1] = local_6c.x;
|
||||
local_60[0][2] = local_78.x;
|
||||
local_60[0][3] = param_1->mPosition.x;
|
||||
local_60[1][0] = param_1->mBaseAxis.y;
|
||||
local_60[1][1] = local_6c.y;
|
||||
local_60[1][2] = local_78.y;
|
||||
local_60[1][3] = param_1->mPosition.y;
|
||||
local_60[2][0] = param_1->mBaseAxis.z;
|
||||
local_60[2][1] = local_6c.z;
|
||||
local_60[2][2] = local_78.z;
|
||||
local_60[2][3] = param_1->mPosition.z;
|
||||
p_plane[param_0->mPlaneType](local_60, fVar1, fVar2);
|
||||
MTXConcat(param_0->mPosCamMtx, local_60, local_60);
|
||||
GXLoadPosMtxImm(local_60, 0);
|
||||
p_prj[param_0->mPrjType](param_0, local_60);
|
||||
GXCallDisplayList(p_dl[param_0->mDLType], sizeof(jpa_dl));
|
||||
axisZ.normalize();
|
||||
ptcl->mBaseAxis.cross(axisY, axisZ);
|
||||
ptcl->mBaseAxis.normalize();
|
||||
Mtx posMtx;
|
||||
f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX;
|
||||
f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY;
|
||||
posMtx[0][0] = ptcl->mBaseAxis.x;
|
||||
posMtx[0][1] = axisY.x;
|
||||
posMtx[0][2] = axisZ.x;
|
||||
posMtx[0][3] = ptcl->mPosition.x;
|
||||
posMtx[1][0] = ptcl->mBaseAxis.y;
|
||||
posMtx[1][1] = axisY.y;
|
||||
posMtx[1][2] = axisZ.y;
|
||||
posMtx[1][3] = ptcl->mPosition.y;
|
||||
posMtx[2][0] = ptcl->mBaseAxis.z;
|
||||
posMtx[2][1] = axisY.z;
|
||||
posMtx[2][2] = axisZ.z;
|
||||
posMtx[2][3] = ptcl->mPosition.z;
|
||||
p_plane[work->mPlaneType](posMtx, scaleX, scaleY);
|
||||
dusk::frame_interp::record_final_mtx(posMtx, ptcl);
|
||||
}
|
||||
|
||||
void JPADrawRotDirection(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) {
|
||||
if (param_1->checkStatus(JPAPtclStts_Invisible)) {
|
||||
void JPAInterpRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
|
||||
f32 sinRot = JMASSin(ptcl->mRotateAngle);
|
||||
f32 cosRot = JMASCos(ptcl->mRotateAngle);
|
||||
JGeometry::TVec3<f32> axisY;
|
||||
JGeometry::TVec3<f32> axisZ;
|
||||
p_direction[work->mDirType](work, ptcl, &axisY);
|
||||
|
||||
if (axisY.isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
axisY.normalize();
|
||||
axisZ.cross(ptcl->mBaseAxis, axisY);
|
||||
|
||||
if (axisZ.isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
axisZ.normalize();
|
||||
ptcl->mBaseAxis.cross(axisY, axisZ);
|
||||
ptcl->mBaseAxis.normalize();
|
||||
f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX;
|
||||
f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY;
|
||||
Mtx mtx1;
|
||||
Mtx mtx2;
|
||||
p_rot[work->mRotType](sinRot, cosRot, mtx1);
|
||||
p_plane[work->mPlaneType](mtx1, scaleX, scaleY);
|
||||
mtx2[0][0] = ptcl->mBaseAxis.x;
|
||||
mtx2[0][1] = axisY.x;
|
||||
mtx2[0][2] = axisZ.x;
|
||||
mtx2[0][3] = ptcl->mPosition.x;
|
||||
mtx2[1][0] = ptcl->mBaseAxis.y;
|
||||
mtx2[1][1] = axisY.y;
|
||||
mtx2[1][2] = axisZ.y;
|
||||
mtx2[1][3] = ptcl->mPosition.y;
|
||||
mtx2[2][0] = ptcl->mBaseAxis.z;
|
||||
mtx2[2][1] = axisY.z;
|
||||
mtx2[2][2] = axisZ.z;
|
||||
mtx2[2][3] = ptcl->mPosition.z;
|
||||
MTXConcat(mtx2, mtx1, mtx1);
|
||||
dusk::frame_interp::record_final_mtx(mtx1, ptcl);
|
||||
}
|
||||
#endif
|
||||
|
||||
void JPADrawDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
|
||||
if (ptcl->checkStatus(JPAPtclStts_Invisible)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ZoneScoped;
|
||||
|
||||
f32 sinRot = JMASSin(param_1->mRotateAngle);
|
||||
f32 cosRot = JMASCos(param_1->mRotateAngle);
|
||||
JGeometry::TVec3<f32> local_6c;
|
||||
JGeometry::TVec3<f32> local_78;
|
||||
p_direction[param_0->mDirType](param_0, param_1, &local_6c);
|
||||
Mtx posMtx;
|
||||
#if TARGET_PC
|
||||
if (!dusk::frame_interp::lookup_replacement(ptcl, posMtx))
|
||||
#endif
|
||||
{
|
||||
JGeometry::TVec3<f32> axisY;
|
||||
JGeometry::TVec3<f32> axisZ;
|
||||
p_direction[work->mDirType](work, ptcl, &axisY);
|
||||
|
||||
if (local_6c.isZero()) {
|
||||
if (axisY.isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
axisY.normalize();
|
||||
axisZ.cross(ptcl->mBaseAxis, axisY);
|
||||
|
||||
if (axisZ.isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
axisZ.normalize();
|
||||
ptcl->mBaseAxis.cross(axisY, axisZ);
|
||||
ptcl->mBaseAxis.normalize();
|
||||
f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX;
|
||||
f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY;
|
||||
posMtx[0][0] = ptcl->mBaseAxis.x;
|
||||
posMtx[0][1] = axisY.x;
|
||||
posMtx[0][2] = axisZ.x;
|
||||
posMtx[0][3] = ptcl->mPosition.x;
|
||||
posMtx[1][0] = ptcl->mBaseAxis.y;
|
||||
posMtx[1][1] = axisY.y;
|
||||
posMtx[1][2] = axisZ.y;
|
||||
posMtx[1][3] = ptcl->mPosition.y;
|
||||
posMtx[2][0] = ptcl->mBaseAxis.z;
|
||||
posMtx[2][1] = axisY.z;
|
||||
posMtx[2][2] = axisZ.z;
|
||||
posMtx[2][3] = ptcl->mPosition.z;
|
||||
p_plane[work->mPlaneType](posMtx, scaleX, scaleY);
|
||||
}
|
||||
|
||||
MTXConcat(work->mPosCamMtx, posMtx, posMtx);
|
||||
GXLoadPosMtxImm(posMtx, GX_PNMTX0);
|
||||
p_prj[work->mPrjType](work, posMtx);
|
||||
GXCallDisplayList(p_dl[work->mDLType], sizeof(jpa_dl));
|
||||
}
|
||||
|
||||
void JPADrawRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
|
||||
if (ptcl->checkStatus(JPAPtclStts_Invisible)) {
|
||||
return;
|
||||
}
|
||||
|
||||
local_6c.normalize();
|
||||
local_78.cross(param_1->mBaseAxis, local_6c);
|
||||
ZoneScoped;
|
||||
|
||||
if (local_78.isZero()) {
|
||||
return;
|
||||
Mtx mtx1;
|
||||
Mtx mtx2;
|
||||
#if TARGET_PC
|
||||
if (!dusk::frame_interp::lookup_replacement(ptcl, mtx1))
|
||||
#endif
|
||||
{
|
||||
f32 sinRot = JMASSin(ptcl->mRotateAngle);
|
||||
f32 cosRot = JMASCos(ptcl->mRotateAngle);
|
||||
JGeometry::TVec3<f32> axisY;
|
||||
JGeometry::TVec3<f32> axisZ;
|
||||
p_direction[work->mDirType](work, ptcl, &axisY);
|
||||
|
||||
if (axisY.isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
axisY.normalize();
|
||||
axisZ.cross(ptcl->mBaseAxis, axisY);
|
||||
|
||||
if (axisZ.isZero()) {
|
||||
return;
|
||||
}
|
||||
|
||||
axisZ.normalize();
|
||||
ptcl->mBaseAxis.cross(axisY, axisZ);
|
||||
ptcl->mBaseAxis.normalize();
|
||||
f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX;
|
||||
f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY;
|
||||
p_rot[work->mRotType](sinRot, cosRot, mtx1);
|
||||
p_plane[work->mPlaneType](mtx1, scaleX, scaleY);
|
||||
mtx2[0][0] = ptcl->mBaseAxis.x;
|
||||
mtx2[0][1] = axisY.x;
|
||||
mtx2[0][2] = axisZ.x;
|
||||
mtx2[0][3] = ptcl->mPosition.x;
|
||||
mtx2[1][0] = ptcl->mBaseAxis.y;
|
||||
mtx2[1][1] = axisY.y;
|
||||
mtx2[1][2] = axisZ.y;
|
||||
mtx2[1][3] = ptcl->mPosition.y;
|
||||
mtx2[2][0] = ptcl->mBaseAxis.z;
|
||||
mtx2[2][1] = axisY.z;
|
||||
mtx2[2][2] = axisZ.z;
|
||||
mtx2[2][3] = ptcl->mPosition.z;
|
||||
MTXConcat(mtx2, mtx1, mtx1);
|
||||
}
|
||||
|
||||
local_78.normalize();
|
||||
param_1->mBaseAxis.cross(local_6c, local_78);
|
||||
param_1->mBaseAxis.normalize();
|
||||
f32 particleX = param_0->mGlobalPtclScl.x * param_1->mParticleScaleX;
|
||||
f32 particleY = param_0->mGlobalPtclScl.y * param_1->mParticleScaleY;
|
||||
Mtx auStack_80;
|
||||
Mtx local_60;
|
||||
p_rot[param_0->mRotType](sinRot, cosRot, auStack_80);
|
||||
p_plane[param_0->mPlaneType](auStack_80, particleX, particleY);
|
||||
local_60[0][0] = param_1->mBaseAxis.x;
|
||||
local_60[0][1] = local_6c.x;
|
||||
local_60[0][2] = local_78.x;
|
||||
local_60[0][3] = param_1->mPosition.x;
|
||||
local_60[1][0] = param_1->mBaseAxis.y;
|
||||
local_60[1][1] = local_6c.y;
|
||||
local_60[1][2] = local_78.y;
|
||||
local_60[1][3] = param_1->mPosition.y;
|
||||
local_60[2][0] = param_1->mBaseAxis.z;
|
||||
local_60[2][1] = local_6c.z;
|
||||
local_60[2][2] = local_78.z;
|
||||
local_60[2][3] = param_1->mPosition.z;
|
||||
MTXConcat(local_60, auStack_80, auStack_80);
|
||||
MTXConcat(param_0->mPosCamMtx, auStack_80, local_60);
|
||||
GXLoadPosMtxImm(local_60, 0);
|
||||
p_prj[param_0->mPrjType](param_0, local_60);
|
||||
GXCallDisplayList(p_dl[param_0->mDLType], sizeof(jpa_dl));
|
||||
MTXConcat(work->mPosCamMtx, mtx1, mtx2);
|
||||
GXLoadPosMtxImm(mtx2, GX_PNMTX0);
|
||||
p_prj[work->mPrjType](work, mtx2);
|
||||
GXCallDisplayList(p_dl[work->mDLType], sizeof(jpa_dl));
|
||||
}
|
||||
|
||||
void JPADrawDBillboard(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) {
|
||||
|
||||
@@ -204,6 +204,28 @@ void JPABaseParticle::init_c(JPAEmitterWorkData* work, JPABaseParticle* parent)
|
||||
mTexAnmIdx = 0;
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
void JPABaseParticle::interp(JPAEmitterWorkData* work, void const* drawFunc) {
|
||||
static bool enable = false;
|
||||
if (!enable)
|
||||
return;
|
||||
|
||||
// don't interpolate the first frame
|
||||
if (mAge == 0)
|
||||
return;
|
||||
|
||||
if (drawFunc == JPADrawBillboard) {
|
||||
JPAInterpBillboard(work, this);
|
||||
} else if (drawFunc == JPADrawRotBillboard) {
|
||||
JPAInterpRotBillboard(work, this);
|
||||
} else if (drawFunc == JPADrawDirection) {
|
||||
JPAInterpDirection(work, this);
|
||||
} else if (drawFunc == JPADrawRotDirection) {
|
||||
JPAInterpRotDirection(work, this);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool JPABaseParticle::calc_p(JPAEmitterWorkData* work) {
|
||||
if (++mAge >= mLifeTime) {
|
||||
return true;
|
||||
@@ -247,6 +269,17 @@ bool JPABaseParticle::calc_p(JPAEmitterWorkData* work) {
|
||||
mOffsetPosition.y + mLocalPosition.y * work->mPublicScale.y,
|
||||
mOffsetPosition.z + mLocalPosition.z * work->mPublicScale.z);
|
||||
|
||||
#if TARGET_PC
|
||||
JPABaseShape* pBsp = work->mpRes->getBsp();
|
||||
work->mGlobalPtclScl.x = work->mpEmtr->mGlobalPScl.x * pBsp->getBaseSizeX();
|
||||
work->mGlobalPtclScl.y = work->mpEmtr->mGlobalPScl.y * pBsp->getBaseSizeY();
|
||||
work->mDirType = pBsp->getDirType();
|
||||
work->mRotType = pBsp->getRotType();
|
||||
work->mDLType = pBsp->getType() == 4 || pBsp->getType() == 8;
|
||||
work->mPlaneType = work->mDLType ? 2 : pBsp->getBasePlaneType();
|
||||
interp(work, (void const*)work->mpRes->mpDrawParticleFuncList[0]);
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -289,6 +322,23 @@ bool JPABaseParticle::calc_c(JPAEmitterWorkData* work) {
|
||||
mOffsetPosition.y + mLocalPosition.y * work->mPublicScale.y,
|
||||
mOffsetPosition.z + mLocalPosition.z * work->mPublicScale.z);
|
||||
|
||||
#if TARGET_PC
|
||||
JPABaseShape* pBsp = work->mpRes->getBsp();
|
||||
JPAChildShape* pCsp = work->mpRes->getCsp();
|
||||
if (pCsp->isScaleInherited()) {
|
||||
work->mGlobalPtclScl.x = work->mpEmtr->mGlobalPScl.x * pBsp->getBaseSizeX();
|
||||
work->mGlobalPtclScl.y = work->mpEmtr->mGlobalPScl.y * pBsp->getBaseSizeY();
|
||||
} else {
|
||||
work->mGlobalPtclScl.x = work->mpEmtr->mGlobalPScl.x * pCsp->getScaleX();
|
||||
work->mGlobalPtclScl.y = work->mpEmtr->mGlobalPScl.y * pCsp->getScaleY();
|
||||
}
|
||||
work->mDirType = pCsp->getDirType();
|
||||
work->mRotType = pCsp->getRotType();
|
||||
work->mDLType = pCsp->getType() == 4 || pCsp->getType() == 8;
|
||||
work->mPlaneType = work->mDLType ? 2 : pCsp->getBasePlaneType();
|
||||
interp(work, (void const*)work->mpRes->mpDrawParticleChildFuncList[0]);
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,3 +38,9 @@ bool JUTPalette::load() {
|
||||
|
||||
return check;
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
void JUTPalette::dataUploaded() {
|
||||
GXInitTlutObjData(&mTlutObj, (void*)mColorTable);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -27,7 +27,7 @@ void JUTTexture::storeTIMG(ResTIMG const* param_0, u8 param_1) {
|
||||
mTexData = (void*)((intptr_t)mTexInfo + 0x20);
|
||||
}
|
||||
|
||||
field_0x2c = NULL;
|
||||
mPalette = NULL;
|
||||
mTlutName = 0;
|
||||
mWrapS = mTexInfo->wrapS;
|
||||
mWrapT = mTexInfo->wrapT;
|
||||
@@ -95,7 +95,7 @@ void JUTTexture::storeTIMG(ResTIMG const* param_0, JUTPalette* param_1, GXTlut p
|
||||
}
|
||||
mEmbPalette = param_1;
|
||||
setEmbPaletteDelFlag(false);
|
||||
field_0x2c = NULL;
|
||||
mPalette = NULL;
|
||||
if (param_1 != NULL) {
|
||||
mTlutName = param_2;
|
||||
if (param_2 != param_1->getTlutName()) {
|
||||
@@ -120,11 +120,11 @@ void JUTTexture::storeTIMG(ResTIMG const* param_0, JUTPalette* param_1, GXTlut p
|
||||
void JUTTexture::attachPalette(JUTPalette* param_0) {
|
||||
if (mTexInfo->indexTexture) {
|
||||
if (param_0 == NULL && mEmbPalette != NULL) {
|
||||
field_0x2c = mEmbPalette;
|
||||
mPalette = mEmbPalette;
|
||||
} else {
|
||||
field_0x2c = param_0;
|
||||
mPalette = param_0;
|
||||
}
|
||||
initTexObj(field_0x2c->getTlutName());
|
||||
initTexObj(mPalette->getTlutName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,9 +133,9 @@ void JUTTexture::init() {
|
||||
initTexObj();
|
||||
} else {
|
||||
if (mEmbPalette != NULL) {
|
||||
field_0x2c = mEmbPalette;
|
||||
mPalette = mEmbPalette;
|
||||
|
||||
initTexObj(field_0x2c->getTlutName());
|
||||
initTexObj(mPalette->getTlutName());
|
||||
} else {
|
||||
OS_REPORT("This texture is CI-Format, but EmbPalette is NULL.\n");
|
||||
}
|
||||
@@ -179,8 +179,8 @@ void JUTTexture::initTexObj(GXTlut param_0) {
|
||||
}
|
||||
|
||||
void JUTTexture::load(GXTexMapID param_0) {
|
||||
if (field_0x2c) {
|
||||
field_0x2c->load();
|
||||
if (mPalette) {
|
||||
mPalette->load();
|
||||
}
|
||||
GXLoadTexObj(&mTexObj, param_0);
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 457 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 188 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,147 @@
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-size: 24dp;
|
||||
color: #FFFFFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.overlay-root {
|
||||
width: 100%;
|
||||
min-height: 45%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: stretch;
|
||||
decorator: vertical-gradient(#00000000 #151610F2);
|
||||
padding: 48dp 0 40dp 0;
|
||||
filter: opacity(0);
|
||||
transition: filter 0.2s linear-in-out;
|
||||
}
|
||||
|
||||
.overlay-root[open] {
|
||||
filter: opacity(1);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
width: 100%;
|
||||
max-width: 1216dp;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24dp;
|
||||
padding: 0 32dp;
|
||||
}
|
||||
|
||||
@media (max-height: 800dp) {
|
||||
.overlay-root {
|
||||
min-height: 38%;
|
||||
padding: 32dp 0 28dp 0;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
gap: 16dp;
|
||||
padding: 0 24dp;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 24dp;
|
||||
}
|
||||
|
||||
.carousel-container {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 18dp;
|
||||
line-height: 22dp;
|
||||
color: rgba(255, 255, 255, 50%);
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 1dp 0;
|
||||
border-top: 1dp rgba(217, 217, 217, 50%);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 24dp;
|
||||
}
|
||||
|
||||
footer-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 220dp;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
font-size: 20dp;
|
||||
line-height: 24dp;
|
||||
text-transform: uppercase;
|
||||
color: #FFFFFF;
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
footer-button.return {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
footer-button.reset {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.stepped-carousel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16dp;
|
||||
width: auto;
|
||||
min-width: 246dp;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stepped-carousel-value {
|
||||
line-height: 29dp;
|
||||
min-width: 166dp;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.stepped-carousel-arrow {
|
||||
width: 24dp;
|
||||
height: 24dp;
|
||||
min-width: 24dp;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
font-size: 18dp;
|
||||
color: #E0DBC8;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
focus: auto;
|
||||
}
|
||||
|
||||
popup {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 64dp;
|
||||
background-color: rgba(21, 22, 16, 80%);
|
||||
border-bottom: 2dp #92875B;
|
||||
backdrop-filter: blur(5dp);
|
||||
transform: translateY(-64dp);
|
||||
transition: transform 0.2s cubic-in-out;
|
||||
}
|
||||
|
||||
popup[open] {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
popup tab-bar {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
popup tab-bar tab {
|
||||
opacity: 0.35;
|
||||
color: #E0DBC8;
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "Fira Sans";
|
||||
font-weight: normal;
|
||||
font-size: 20dp;
|
||||
color: #FFFFFF;
|
||||
background-color: #000000;
|
||||
decorator: image(../prelaunch-bg.png cover left center);
|
||||
filter: opacity(0);
|
||||
transition: filter 1s 0.1s linear-in-out;
|
||||
}
|
||||
|
||||
body[open] {
|
||||
filter: opacity(1);
|
||||
}
|
||||
|
||||
content {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: opacity(0);
|
||||
transition: filter 0.2s linear-in-out;
|
||||
}
|
||||
|
||||
content[open] {
|
||||
filter: opacity(1);
|
||||
}
|
||||
|
||||
menu {
|
||||
position: absolute;
|
||||
left: 96dp;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
/* Scale based on a reference screen width, 428/1216 */
|
||||
width: 35.230264vw;
|
||||
min-width: 428dp;
|
||||
max-width: 856dp;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 48dp;
|
||||
}
|
||||
|
||||
hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: 8dp;
|
||||
}
|
||||
|
||||
hero img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-family: "Alegreya SC";
|
||||
font-size: 32dp;
|
||||
}
|
||||
|
||||
@media (min-width: 1216dp) {
|
||||
.eyebrow {
|
||||
/* Same logic as .menu, 32/1216 */
|
||||
font-size: 2.631579vw;
|
||||
}
|
||||
}
|
||||
|
||||
.eyebrow span {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#menu-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12dp;
|
||||
}
|
||||
|
||||
#menu-list button {
|
||||
width: 428dp;
|
||||
height: 54dp;
|
||||
padding: 8dp 16dp;
|
||||
border-radius: 8dp;
|
||||
text-transform: uppercase;
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-size: 32dp;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
/* Define a fully transparent gradient as the default state, otherwise a white flash occurs */
|
||||
decorator: horizontal-gradient(#00000000 #00000000);
|
||||
}
|
||||
|
||||
#menu-list button.anim-done {
|
||||
transition: decorator color 0.1s linear-in-out;
|
||||
}
|
||||
|
||||
#menu-list button:hover,
|
||||
#menu-list button:focus-visible {
|
||||
color: black;
|
||||
decorator: horizontal-gradient(#FEE685FF #FEE68500);
|
||||
}
|
||||
|
||||
disk-status {
|
||||
position: absolute;
|
||||
left: 96dp;
|
||||
bottom: 72dp;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8dp;
|
||||
}
|
||||
|
||||
version-info {
|
||||
position: absolute;
|
||||
right: 96dp;
|
||||
bottom: 72dp;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8dp;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.status,
|
||||
.version {
|
||||
font-size: 24dp;
|
||||
}
|
||||
|
||||
.status,
|
||||
.update {
|
||||
color: #D8F999;
|
||||
}
|
||||
|
||||
.status[bad] {
|
||||
color: #FFC9C9;
|
||||
}
|
||||
|
||||
/* TODO: Hidden until an actual update checker is introduced */
|
||||
.update {
|
||||
display: none;
|
||||
font-size: 16dp;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.detail,
|
||||
.update span {
|
||||
color: #A6A09B;
|
||||
}
|
||||
|
||||
/* Startup animation */
|
||||
.intro-item {
|
||||
opacity: 0;
|
||||
transform: translateY(10dp);
|
||||
transition: opacity transform 0.3s 0.1s cubic-in-out;
|
||||
}
|
||||
|
||||
body.animate-in .intro-item {
|
||||
opacity: 1;
|
||||
transform: translateY(0dp);
|
||||
}
|
||||
|
||||
.delay-0 {
|
||||
transition: opacity transform 0.3s 0.1s cubic-in-out;
|
||||
}
|
||||
|
||||
.delay-1 {
|
||||
transition: opacity transform 0.3s 0.2s cubic-in-out;
|
||||
}
|
||||
|
||||
.delay-2 {
|
||||
transition: opacity transform 0.3s 0.3s cubic-in-out;
|
||||
}
|
||||
|
||||
.delay-3 {
|
||||
transition: opacity transform 0.3s 0.4s cubic-in-out;
|
||||
}
|
||||
|
||||
.delay-4 {
|
||||
transition: opacity transform 0.3s 0.5s cubic-in-out;
|
||||
}
|
||||
|
||||
.delay-5 {
|
||||
transition: opacity transform 0.3s 0.6s cubic-in-out;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
tab-bar {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
overflow: auto hidden;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
tab-bar tab {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 24dp;
|
||||
line-height: 64dp;
|
||||
white-space: nowrap;
|
||||
decorator: vertical-gradient(#c2a42d00 #c2a42d00);
|
||||
transition: decorator 0.1s linear-in-out, opacity 0.1s linear-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
tab-bar tab:selected {
|
||||
opacity: 1;
|
||||
border-bottom: 4dp #C2A42D;
|
||||
font-effect: glow(0dp 4dp 0dp 4dp black);
|
||||
}
|
||||
|
||||
tab-bar tab:focus-visible,
|
||||
tab-bar tab:hover {
|
||||
opacity: 1;
|
||||
font-effect: glow(0dp 4dp 0dp 4dp black);
|
||||
decorator: vertical-gradient(#c2a42d00 #c2a42d26);
|
||||
}
|
||||
|
||||
tab-bar tab:active {
|
||||
decorator: vertical-gradient(#c2a42d10 #c2a42d40);
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 64dp;
|
||||
font-family: "Fira Sans";
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 15dp;
|
||||
color: #E0DBC8;
|
||||
}
|
||||
|
||||
window {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
max-width: 1088dp;
|
||||
max-height: 768dp;
|
||||
margin: auto;
|
||||
border-radius: 14dp;
|
||||
overflow: hidden;
|
||||
border: 2dp #92875B;
|
||||
backdrop-filter: blur(5dp);
|
||||
box-shadow: 0 0 25dp 5dp;
|
||||
background-color: rgba(21, 22, 16, 90%);
|
||||
filter: opacity(0);
|
||||
transform: scale(0.9);
|
||||
transform-origin: center;
|
||||
transition: filter transform 0.2s cubic-in-out;
|
||||
}
|
||||
|
||||
window[open] {
|
||||
filter: opacity(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
@media (max-height: 640dp) {
|
||||
body {
|
||||
padding: 16dp;
|
||||
}
|
||||
window {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
window tab-bar {
|
||||
flex: 0 0 64dp;
|
||||
height: 64dp;
|
||||
background-color: rgba(217, 217, 217, 10%);
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
font-size: 18dp;
|
||||
border-bottom: 2dp #92875B;
|
||||
}
|
||||
|
||||
window tab-bar tab {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
window content {
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
window content pane {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex: 1 1 0;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 24dp;
|
||||
padding-bottom: 0dp;
|
||||
gap: 8dp;
|
||||
overflow: hidden auto;
|
||||
font-size: 20dp;
|
||||
}
|
||||
|
||||
window content pane:not(:last-of-type) {
|
||||
border-right: 1dp #92875B;
|
||||
}
|
||||
|
||||
window content pane > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
window content pane > spacer {
|
||||
display: block;
|
||||
/* Completes the 24dp bottom inset after the pane's 8dp gap. */
|
||||
flex: 0 0 16dp;
|
||||
height: 16dp;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
scrollbarvertical {
|
||||
width: 8dp;
|
||||
margin: 4dp 4dp 4dp 0;
|
||||
}
|
||||
|
||||
scrollbarvertical sliderarrowdec,
|
||||
scrollbarvertical sliderarrowinc {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
scrollbarvertical slidertrack {
|
||||
width: 8dp;
|
||||
}
|
||||
|
||||
scrollbarvertical sliderbar {
|
||||
width: 8dp;
|
||||
min-height: 24dp;
|
||||
background-color: rgba(224, 219, 200, 45%);
|
||||
border-radius: 2dp;
|
||||
transition: background-color 0.2s cubic-in-out;
|
||||
}
|
||||
|
||||
scrollbarvertical sliderbar:hover,
|
||||
scrollbarvertical sliderbar:active {
|
||||
background-color: rgba(194, 164, 45, 80%);
|
||||
}
|
||||
|
||||
scrollbarhorizontal {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
scrollbarhorizontal sliderarrowdec,
|
||||
scrollbarhorizontal sliderarrowinc {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
scrollbarhorizontal slidertrack,
|
||||
scrollbarhorizontal sliderbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-size: 22dp;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.section-heading:not(:first-of-type) {
|
||||
padding-top: 12dp;
|
||||
}
|
||||
|
||||
button {
|
||||
text-align: center;
|
||||
background-color: rgba(17, 16, 10, 20%);
|
||||
opacity: 0.9;
|
||||
padding: 8dp 16dp;
|
||||
border-radius: 14dp;
|
||||
box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp;
|
||||
font-size: 20dp;
|
||||
transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out;
|
||||
cursor: pointer;
|
||||
focus: auto;
|
||||
}
|
||||
|
||||
button:not(:disabled):hover,
|
||||
button:not(:disabled):focus-visible {
|
||||
background-color: rgba(204, 184, 119, 20%);
|
||||
box-shadow: #C2A42D 0 0 0 2dp;
|
||||
}
|
||||
|
||||
button:not(:disabled):selected {
|
||||
opacity: 1;
|
||||
background-color: rgba(204, 184, 119, 40%);
|
||||
}
|
||||
|
||||
button:not(:disabled):active {
|
||||
opacity: 1;
|
||||
background-color: rgba(204, 184, 119, 40%);
|
||||
box-shadow: #C2A42D 0 0 0 2dp;
|
||||
}
|
||||
|
||||
select-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8dp;
|
||||
background-color: rgba(17, 16, 10, 20%);
|
||||
opacity: 0.9;
|
||||
padding: 8dp 16dp;
|
||||
border-radius: 14dp;
|
||||
box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp;
|
||||
transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out;
|
||||
cursor: pointer;
|
||||
focus: auto;
|
||||
}
|
||||
|
||||
select-button:not(:disabled):hover,
|
||||
select-button:not(:disabled):focus-visible {
|
||||
background-color: rgba(204, 184, 119, 20%);
|
||||
box-shadow: #C2A42D 0 0 0 2dp;
|
||||
}
|
||||
|
||||
select-button:not(:disabled):selected {
|
||||
opacity: 1;
|
||||
background-color: rgba(204, 184, 119, 40%);
|
||||
}
|
||||
|
||||
select-button:not(:disabled):active {
|
||||
opacity: 1;
|
||||
background-color: rgba(204, 184, 119, 40%);
|
||||
box-shadow: #C2A42D 0 0 0 2dp;
|
||||
}
|
||||
|
||||
select-button:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
select-button key {
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
font-size: 18dp;
|
||||
text-transform: uppercase;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
select-button value {
|
||||
margin-left: auto;
|
||||
font-size: 20dp;
|
||||
}
|
||||
|
||||
select-button input {
|
||||
text-align: right;
|
||||
font-size: 20dp;
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
#include "Z2AudioLib/Z2Audience.h"
|
||||
#include "Z2AudioLib/Z2SoundInfo.h"
|
||||
#if TARGET_PC
|
||||
#include "dusk/audio/DuskDsp.hpp"
|
||||
#include <cmath>
|
||||
#endif
|
||||
#include "Z2AudioLib/Z2Calc.h"
|
||||
#include "Z2AudioLib/Z2Param.h"
|
||||
#include "JSystem/JAudio2/JAISound.h"
|
||||
@@ -734,9 +738,22 @@ f32 Z2Audience::calcRelPosPan(const Vec& param_0, int camID) {
|
||||
|
||||
f32 Z2Audience::calcRelPosDolby(const Vec& param_0, int camID) {
|
||||
f32 fVar1 = param_0.z + mAudioCamera[camID].getDolbyCenterZ();
|
||||
#if TARGET_PC
|
||||
if (dusk::audio::EnableHrtf) {
|
||||
// Normalize the direction so result is purely front/back orientation,
|
||||
// independent of how far away the sound is
|
||||
f32 lenSq = param_0.x * param_0.x + param_0.y * param_0.y + param_0.z * param_0.z;
|
||||
if (lenSq < 0.0001f) {
|
||||
return 0.5f;
|
||||
}
|
||||
f32 zNorm = param_0.z / sqrtf(lenSq);
|
||||
f32 t = (zNorm + 1.0f) * 0.5f;
|
||||
return 0.5f - 0.5f * cosf(t * static_cast<f32>(M_PI));
|
||||
}
|
||||
#endif
|
||||
if (fVar1 > mSetting.field_0x48) {
|
||||
return 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
if (fVar1 < mSetting.field_0x44) {
|
||||
return 0.0f;
|
||||
|
||||
@@ -4962,13 +4962,16 @@ int daAlink_c::create() {
|
||||
|
||||
setArcName(checkWolf());
|
||||
setOriginalHeap(&mpArcHeap, 0xA2800);
|
||||
JKRHEAP_NAME(mpArcHeap, "Alink ArcHeap");
|
||||
if (dComIfG_resLoad(&mPhaseReq, mArcName, mpArcHeap) != cPhs_COMPLEATE_e) {
|
||||
return cPhs_INIT_e;
|
||||
}
|
||||
|
||||
setShieldArcName();
|
||||
setOriginalHeap(&mpShieldArcHeap, 0x7000);
|
||||
if (dComIfG_resLoad(&mShieldPhaseReq, mShieldArcName, mpShieldArcHeap) != cPhs_COMPLEATE_e) {
|
||||
JKRHEAP_NAME(mpShieldArcHeap, "Alink ShieldArcHeap");
|
||||
if (dComIfG_resLoad(&mShieldPhaseReq, mShieldArcName, mpShieldArcHeap) != cPhs_COMPLEATE_e)
|
||||
{
|
||||
return cPhs_INIT_e;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ void daAlink_c::setOriginalHeap(JKRExpHeap** i_ppheap, u32 i_size) {
|
||||
JKRHeap* parent = mDoExt_getGameHeap();
|
||||
|
||||
JKRExpHeap* heap = JKRExpHeap::create(size + (var_r29 + var_r28), parent, true);
|
||||
JKRHEAP_NAME(heap, "Alink original");
|
||||
*i_ppheap = heap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#if TARGET_PC
|
||||
#include <f_ap/f_ap_game.h>
|
||||
#include <dusk/autosave.h>
|
||||
#endif
|
||||
|
||||
char* daDoor20_c::getStopBmdName() {
|
||||
switch (door_param2_c::getKind(this)) {
|
||||
case 3:
|
||||
@@ -196,6 +201,7 @@ void daDoor20_c::setEventPrm() {
|
||||
} else {
|
||||
roomNo = FRoomNo;
|
||||
}
|
||||
|
||||
if (dComIfGp_roomControl_checkStatusFlag(roomNo, 1)) {
|
||||
if (door_param2_c::getKind(this) == 9) {
|
||||
if (daPy_py_c::checkNowWolf()) {
|
||||
@@ -564,6 +570,11 @@ int daDoor20_c::openEnd(int param_1) {
|
||||
openEnd_1();
|
||||
break;
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
triggerAutoSave();
|
||||
#endif
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -463,6 +463,23 @@ int daMidna_c::createHeap() {
|
||||
JKRReadIdxResource(mBckHeap[0].getBuffer(), mBckHeap[0].getBufferSize(), 0x1DC, dComIfGp_getAnmArchive());
|
||||
J3DAnmTransform* md_anm = (J3DAnmTransform*)J3DAnmLoaderDataBase::load(mBckHeap[0].getBuffer());
|
||||
modelData = (J3DModelData*)dComIfG_getObjectRes(l_arcName, 14);
|
||||
|
||||
#if TARGET_PC
|
||||
J3DTexture* tex = modelData->getTexture();
|
||||
JUTNameTab* nametable = modelData->getTextureName();
|
||||
if (tex != NULL && nametable != NULL) {
|
||||
for (u16 i = 0; i < tex->getNum(); i++) {
|
||||
const char* name = nametable->getName(i);
|
||||
if (name != NULL && strcmp(name, "midona_eye") == 0) {
|
||||
ResTIMG* timg = tex->getResTIMG(i);
|
||||
timg->mipmapEnabled = false;
|
||||
tex->loadGXTexObj(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
JUT_ASSERT(852, modelData != NULL);
|
||||
mpMorf = JKR_NEW mDoExt_McaMorfSO(modelData, &mMorfCB, NULL, md_anm, J3DFrameCtrl::EMode_LOOP, 1.0f, 0, -1, NULL, 0, 0x11000284);
|
||||
if (mpMorf == NULL || mpMorf->getModel() == NULL) {
|
||||
|
||||
@@ -40,6 +40,7 @@ dMirror_packet_c::dMirror_packet_c() {
|
||||
void dMirror_packet_c::reset() {
|
||||
#if TARGET_PC
|
||||
mbReset = true;
|
||||
mbHadEntry = false;
|
||||
#else
|
||||
mModelCount = 0;
|
||||
#endif
|
||||
@@ -84,11 +85,21 @@ void dMirror_packet_c::calcMinMax() {
|
||||
}
|
||||
|
||||
int dMirror_packet_c::entryModel(J3DModel* i_model) {
|
||||
#if TARGET_PC
|
||||
if (mbReset) {
|
||||
mModelCount = 0;
|
||||
mbReset = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (mModelCount >= 0x40) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
mModels[mModelCount++] = i_model;
|
||||
#if TARGET_PC
|
||||
mbHadEntry = true;
|
||||
#endif
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -592,13 +603,6 @@ int daMirror_c::execute() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
if (mPacket.mbReset) {
|
||||
mPacket.mModelCount = 0;
|
||||
mPacket.mbReset = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
daPy_py_c* player = daPy_getLinkPlayerActorClass();
|
||||
JUT_ASSERT(0, player != NULL);
|
||||
|
||||
@@ -624,6 +628,12 @@ int daMirror_c::draw() {
|
||||
mDoExt_modelUpdateDL(mpModel);
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
if (mPacket.mbReset && !mPacket.mbHadEntry) {
|
||||
mPacket.mModelCount = 0;
|
||||
}
|
||||
mPacket.mbHadEntry = true;
|
||||
#endif
|
||||
dComIfGd_getOpaListBG()->entryImm(&mPacket, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,16 @@ void daObj_Balloon_c::saveBestScore() {
|
||||
dComIfGp_setMessageCountNumber(m_balloon_score);
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
static void minigameReset() {
|
||||
// !@bug d_a_obj_balloon.rel unload used to zero these file-statics; with static linking they dangle across scenes.
|
||||
m_combo_type = 0xFFFFFFFF;
|
||||
m_combo_count = 0;
|
||||
m_combo_next_score = 0;
|
||||
m_balloon_score = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static u8 hio_set;
|
||||
|
||||
static daObj_Balloon_HIO_c l_HIO;
|
||||
@@ -205,13 +215,6 @@ int daObj_Balloon_c::_delete() {
|
||||
Z2GetAudioMgr()->seStop(Z2SE_OBJ_WATERMILL_ROUND, 0);
|
||||
if (mHIOInit) {
|
||||
hio_set = false;
|
||||
#ifdef TARGET_PC
|
||||
// !@bug d_a_obj_balloon.rel unload used to zero these file-statics; with static linking they dangle across scenes.
|
||||
m_combo_type = 0xFFFFFFFF;
|
||||
m_combo_count = 0;
|
||||
m_combo_next_score = 0;
|
||||
m_balloon_score = 0;
|
||||
#endif
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -253,6 +256,7 @@ int daObj_Balloon_c::create() {
|
||||
}
|
||||
|
||||
if (!hio_set) {
|
||||
IF_DUSK(minigameReset());
|
||||
mHIOInit = true;
|
||||
hio_set = true;
|
||||
l_HIO.field_0x04 = -1;
|
||||
|
||||
+55
-43
@@ -1175,6 +1175,12 @@ bool dCamera_c::Run() {
|
||||
clrFlag(0x200000);
|
||||
}
|
||||
} else {
|
||||
#if TARGET_PC
|
||||
if (mCamParam.Algorythmn(mCamStyle) != 1) {
|
||||
mCamParam.mManualMode = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
sp0F = (this->*engine_tbl[mCamParam.Algorythmn(mCamStyle)])(mCamStyle);
|
||||
|
||||
field_0x170++;
|
||||
@@ -1481,7 +1487,7 @@ void dCamera_c::CalcTrimSize() {
|
||||
mTrimHeight += -mTrimHeight * 0.25f;
|
||||
break;
|
||||
case 2:
|
||||
#if WIDESCREEN_SUPPORT
|
||||
#if !TARGET_PC && WIDESCREEN_SUPPORT
|
||||
if (mDoGph_gInf_c::isWide() && mDoGph_gInf_c::isWideZoom()) {
|
||||
mTrimHeight += (16.0f - mTrimHeight) * 0.25f;
|
||||
break;
|
||||
@@ -3089,10 +3095,6 @@ bool dCamera_c::bumpCheck(u32 i_flags) {
|
||||
field_0x968 *= mMonitor.field_0xc / 5.0f;
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
if (!dusk::getSettings().game.freeCamera || !mCamParam.mManualMode) {
|
||||
#endif
|
||||
|
||||
f32 tmp = field_0x96c * (mIsWolf == 1 ? 30.0f : 30.0f);
|
||||
center += vec3.norm() * (tmp * globe.V().Sin());
|
||||
cSGlobe globe2(vec2 - center);
|
||||
@@ -3106,10 +3108,6 @@ bool dCamera_c::bumpCheck(u32 i_flags) {
|
||||
vec = lin_chk1.GetCross();
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
}
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
if (mCamSetup.CheckFlag(0x8000)) {
|
||||
dDbVw_Report(20, 235, " U");
|
||||
@@ -3520,12 +3518,6 @@ void dCamera_c::checkGroundInfo() {
|
||||
}
|
||||
|
||||
bool dCamera_c::chaseCamera(s32 param_0) {
|
||||
#if TARGET_PC
|
||||
if (freeCamera()) {
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
static f32 JumpCushion = 0.9f;
|
||||
f32 charge_latitude = mCamSetup.ChargeLatitude();
|
||||
int charge_timer = mCamSetup.ChargeTimer();
|
||||
@@ -4207,6 +4199,11 @@ bool dCamera_c::chaseCamera(s32 param_0) {
|
||||
chase->field_0x8 -= chase->field_0xc;
|
||||
chase->field_0x8c = 0;
|
||||
chase->field_0x90 = false;
|
||||
|
||||
#if TARGET_PC
|
||||
freeCamera();
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -4644,6 +4641,11 @@ bool dCamera_c::chaseCamera(s32 param_0) {
|
||||
if (chase->field_0x1c != 0) {
|
||||
chase->field_0x1c--;
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
freeCamera();
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7091,10 +7093,12 @@ bool dCamera_c::subjectCamera(s32 param_0) {
|
||||
cXyz sp1E0(val0, val2, val1);
|
||||
|
||||
#if TARGET_PC
|
||||
f32 aspect = mDoGph_gInf_c::getAspect();
|
||||
f32 baseAspect = FB_WIDTH / FB_HEIGHT;
|
||||
if (aspect > baseAspect) {
|
||||
sp1E0.z += (aspect - baseAspect) * 4;
|
||||
if (sp13) {
|
||||
f32 aspect = mDoGph_gInf_c::getAspect();
|
||||
f32 baseAspect = FB_WIDTH / FB_HEIGHT;
|
||||
if (aspect > baseAspect) {
|
||||
sp1E0.z += (aspect - baseAspect) * 4;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -7472,52 +7476,47 @@ bool dCamera_c::test2Camera(s32 param_0) {
|
||||
|
||||
#if TARGET_PC
|
||||
bool dCamera_c::freeCamera() {
|
||||
if (!dusk::getSettings().game.freeCamera) {
|
||||
if (dusk::getSettings().game.freeCamera && mGear == 1) {
|
||||
mGear = 0;
|
||||
}
|
||||
|
||||
if (!dusk::getSettings().game.freeCamera || mCamStyle == 70)
|
||||
{
|
||||
mCamParam.mManualMode = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
mCamParam.freeXAngle = mViewCache.mDirection.mAzimuth.Degree();
|
||||
mCamParam.freeYAngle = mViewCache.mDirection.mInclination.Degree();
|
||||
if (!mCamParam.mManualMode) {
|
||||
mCamParam.freeXAngle = mViewCache.mDirection.mAzimuth.Degree();
|
||||
mCamParam.freeYAngle = mViewCache.mDirection.mInclination.Degree();
|
||||
}
|
||||
|
||||
cXyz camMovement = {mPadInfo.mCStick.mLastPosX, mPadInfo.mCStick.mLastPosY, 0.0f};
|
||||
f32 magnitude = sqrt(mPadInfo.mCStick.mLastPosX * mPadInfo.mCStick.mLastPosX + mPadInfo.mCStick.mLastPosY * mPadInfo.mCStick.mLastPosY);
|
||||
|
||||
if (mPadInfo.mCStick.mLastPosX != 0 || mPadInfo.mCStick.mLastPosY != 0) {
|
||||
if (!mCamParam.mManualMode) {
|
||||
mCamParam.mManualMode = 1;
|
||||
mCamParam.freeXAngle = mViewCache.mDirection.mAzimuth.Degree();
|
||||
mCamParam.freeYAngle = mViewCache.mDirection.mInclination.Degree();
|
||||
}
|
||||
|
||||
mCamParam.mManualMode = 1;
|
||||
camMovement = camMovement.normalize();
|
||||
camMovement.y *= dusk::getSettings().game.invertCameraYAxis ? 1.0f : -1.0f;
|
||||
mCamParam.freeXAngle += camMovement.x * magnitude * dusk::getSettings().game.freeCameraSensitivity * 4.0f;
|
||||
mCamParam.freeYAngle += camMovement.y * magnitude * dusk::getSettings().game.freeCameraSensitivity * 4.0f;
|
||||
mCamParam.freeXAngle += camMovement.x * magnitude * dusk::getSettings().game.freeCameraSensitivity * 5.0f;
|
||||
mCamParam.freeYAngle += camMovement.y * magnitude * dusk::getSettings().game.freeCameraSensitivity * 5.0f;
|
||||
}
|
||||
|
||||
if (!mCamParam.mManualMode) {
|
||||
fopAc_ac_c* player = dComIfGp_getPlayer(0);
|
||||
if (!mCamParam.mManualMode || player == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
f32 minYAngle = -10.0f;
|
||||
f32 minYAngle = -30.0f;
|
||||
f32 maxAngle = 50.0f;
|
||||
|
||||
mCamParam.freeYAngle = std::clamp(mCamParam.freeYAngle, minYAngle, maxAngle);
|
||||
mViewCache.mDirection.mAzimuth = cSAngle(mCamParam.freeXAngle);
|
||||
mViewCache.mDirection.mInclination = cSAngle(mCamParam.freeYAngle);
|
||||
f32 currentLerp = (mCamParam.freeYAngle - minYAngle) / (maxAngle - minYAngle);
|
||||
mViewCache.mDirection.mRadius = std::lerp(200.0f, 1000.0f, currentLerp);
|
||||
|
||||
cXyz finalCenter = mpPlayerActor->current.pos;
|
||||
finalCenter.y += mIsWolf ? 90.0f : 100.0f;
|
||||
mViewCache.mCenter = finalCenter;
|
||||
|
||||
cXyz finalEye = finalCenter + mViewCache.mDirection.Xyz();
|
||||
cXyz finalEye = mViewCache.mCenter + mViewCache.mDirection.Xyz();
|
||||
mViewCache.mEye = finalEye;
|
||||
|
||||
mViewCache.mFovy = 60.0f;
|
||||
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -11161,12 +11160,25 @@ static int camera_draw(camera_process_class* i_this) {
|
||||
}
|
||||
#endif
|
||||
|
||||
int trim_height = body->TrimHeight();
|
||||
|
||||
#if TARGET_PC
|
||||
auto trim_height = body->TrimHeight();
|
||||
|
||||
if (mDoGph_gInf_c::isWideZoom()) {
|
||||
const auto target_ar = FB_WIDTH / (FB_HEIGHT - trim_height * 2.0f);
|
||||
const auto current_ar = mDoGph_gInf_c::m_safeWidthF / mDoGph_gInf_c::m_safeHeightF;
|
||||
|
||||
if (current_ar < target_ar) {
|
||||
trim_height = FB_HEIGHT / 2.0f * (1.0f - current_ar / target_ar);
|
||||
} else {
|
||||
trim_height = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
trim_height *= viewport->height / FB_HEIGHT;
|
||||
window->setScissor(0.0f, trim_height, viewport->width, viewport->height - trim_height * 2.0f);
|
||||
#else
|
||||
int trim_height = body->TrimHeight();
|
||||
|
||||
window->setScissor(0.0f, trim_height, FB_WIDTH, FB_HEIGHT - trim_height * 2.0f);
|
||||
#endif
|
||||
|
||||
|
||||
+45
-9
@@ -22,6 +22,10 @@
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/gx_helper.h"
|
||||
#include "dusk/logging.h"
|
||||
|
||||
static const void* getInterpKey(const void* base, int idx) {
|
||||
return reinterpret_cast<const void*>(reinterpret_cast<uintptr_t>(base) ^ idx);
|
||||
}
|
||||
#endif
|
||||
|
||||
class dDlst_2Dm_c {
|
||||
@@ -1062,7 +1066,15 @@ void dDlst_shadowReal_c::reset() {
|
||||
}
|
||||
|
||||
void dDlst_shadowReal_c::imageDraw(Mtx param_0) {
|
||||
GXSetProjection(mRenderProjMtx, GX_ORTHOGRAPHIC);
|
||||
#ifdef TARGET_PC
|
||||
Mtx render_proj_mtx;
|
||||
if (dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 2), render_proj_mtx)) {
|
||||
GXSetProjection(render_proj_mtx, GX_ORTHOGRAPHIC);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
GXSetProjection(mRenderProjMtx, GX_ORTHOGRAPHIC);
|
||||
}
|
||||
JUT_ASSERT(1916, mModelNum);
|
||||
J3DModelData* model_data;
|
||||
J3DModel** models = mpModels;
|
||||
@@ -1075,7 +1087,15 @@ void dDlst_shadowReal_c::imageDraw(Mtx param_0) {
|
||||
for (u16 j = 0; j < model_data->getShapeNum(); j++) {
|
||||
if (!model_data->getShapeNodePointer(j)->checkFlag(1)) {
|
||||
shape_pkt = (*models)->getShapePacket(j);
|
||||
shape_pkt->setBaseMtxPtr(&mViewMtx);
|
||||
#ifdef TARGET_PC
|
||||
Mtx view_mtx;
|
||||
if (dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 1), view_mtx)) {
|
||||
shape_pkt->setBaseMtxPtr(&view_mtx);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
shape_pkt->setBaseMtxPtr(&mViewMtx);
|
||||
}
|
||||
shape_pkt->drawFast();
|
||||
shape_pkt->setBaseMtxPtr((Mtx*)param_0);
|
||||
}
|
||||
@@ -1096,7 +1116,18 @@ void dDlst_shadowReal_c::draw() {
|
||||
GXSetVtxDesc(GX_VA_POS, GX_DIRECT);
|
||||
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0);
|
||||
GXSetCurrentMtx(GX_PNMTX0);
|
||||
GXLoadTexMtxImm(mReceiverProjMtx, GX_TEXMTX0, GX_MTX3x4);
|
||||
#ifdef TARGET_PC
|
||||
Mtx view_mtx, recv_proj_mtx;
|
||||
const auto have_view_mtx = dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 1), view_mtx);
|
||||
const auto have_recv_proj_mtx = dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 3), recv_proj_mtx);
|
||||
if (have_view_mtx && have_recv_proj_mtx) {
|
||||
cMtx_concat(recv_proj_mtx, view_mtx, recv_proj_mtx);
|
||||
GXLoadTexMtxImm(recv_proj_mtx, GX_TEXMTX0, GX_MTX3x4);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
GXLoadTexMtxImm(mReceiverProjMtx, GX_TEXMTX0, GX_MTX3x4);
|
||||
}
|
||||
mShadowRealPoly.draw();
|
||||
}
|
||||
|
||||
@@ -1253,6 +1284,13 @@ u8 dDlst_shadowReal_c::setShadowRealMtx(cXyz* param_0, cXyz* param_1, f32 param_
|
||||
cMtx_lookAt(mViewMtx, &local_64, param_1, 0);
|
||||
C_MTXOrtho(mRenderProjMtx, param_2, -param_2, -param_2, param_2, 1.0f, 10000.0f);
|
||||
C_MTXLightOrtho(mReceiverProjMtx, param_2, -param_2, -param_2, param_2, 0.5f, -0.5f, 0.5f, 0.5f);
|
||||
|
||||
#ifdef TARGET_PC
|
||||
const auto keybase = mpModels[0];
|
||||
dusk::frame_interp::record_final_mtx(mViewMtx, getInterpKey(keybase, 1));
|
||||
dusk::frame_interp::record_final_mtx(mRenderProjMtx, getInterpKey(keybase, 2));
|
||||
dusk::frame_interp::record_final_mtx(mReceiverProjMtx, getInterpKey(keybase, 3));
|
||||
#endif
|
||||
cMtx_concat(mReceiverProjMtx, mViewMtx, mReceiverProjMtx);
|
||||
return r29;
|
||||
}
|
||||
@@ -1277,6 +1315,10 @@ u32 dDlst_shadowReal_c::set(u32 i_key, J3DModel* i_model, cXyz* param_2, f32 par
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef TARGET_PC
|
||||
// provide a stable key for interpolation
|
||||
mpModels[0] = i_model;
|
||||
#endif
|
||||
field_0x1 = setShadowRealMtx(&sp60, param_2, param_3, param_4, param_7, param_5);
|
||||
|
||||
if (!field_0x1) {
|
||||
@@ -1370,12 +1412,6 @@ void dDlst_shadowSimple_c::draw() {
|
||||
GXCallDisplayList(l_shadowVolumeDL, 0x40);
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
static const void* getInterpKey(const void* base, int idx) {
|
||||
return reinterpret_cast<const void*>(reinterpret_cast<uintptr_t>(base) ^ idx);
|
||||
}
|
||||
#endif
|
||||
|
||||
void dDlst_shadowSimple_c::set(cXyz* param_0, f32 param_1, f32 param_2, cXyz* param_3,
|
||||
s16 param_4, f32 param_5, TGXTexObj* param_6) {
|
||||
if (param_5 < 0.0f) {
|
||||
|
||||
@@ -856,7 +856,46 @@ void dMenu_DmapBg_c::decGoldFrameAlphaRate() {
|
||||
setGoldFrameAlphaRate(rate);
|
||||
}
|
||||
|
||||
void dMenu_DmapBg_c::dMapBgWide() {
|
||||
// Scale Base HUD
|
||||
mBaseScreen->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f);
|
||||
mBaseScreen->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f);
|
||||
|
||||
// Boss Key, Compass & Map icons
|
||||
mBaseScreen->search(MULTI_CHAR('key_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
|
||||
mBaseScreen->search(MULTI_CHAR('con_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
|
||||
mBaseScreen->search(MULTI_CHAR('map_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
|
||||
|
||||
// Text Header
|
||||
mBaseScreen->search(MULTI_CHAR('t_t00'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
|
||||
mBaseScreen->search(MULTI_CHAR('f_t_00'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
|
||||
|
||||
// C Button
|
||||
mBaseScreen->search(MULTI_CHAR('c_btn2'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
|
||||
|
||||
// Scale Buttons HUD
|
||||
mButtonScreen->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f);
|
||||
mButtonScreen->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f);
|
||||
|
||||
// Buttons
|
||||
mButtonScreen->search(MULTI_CHAR('cont_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
|
||||
|
||||
// C Button
|
||||
mButtonScreen->search(MULTI_CHAR('c_btn'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
|
||||
mButtonScreen->search(MULTI_CHAR('c_text_s'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
|
||||
mButtonScreen->search(MULTI_CHAR('c_text'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
|
||||
mButtonScreen->search(MULTI_CHAR('f_text_s'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
|
||||
mButtonScreen->search(MULTI_CHAR('f_text'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
|
||||
|
||||
// Decorations
|
||||
mButtonScreen->search(MULTI_CHAR('kazari_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
|
||||
}
|
||||
|
||||
void dMenu_DmapBg_c::draw() {
|
||||
#if TARGET_PC
|
||||
dMapBgWide();
|
||||
#endif
|
||||
|
||||
u32 scissor_left;
|
||||
u32 scissor_top;
|
||||
u32 scissor_width;
|
||||
|
||||
+34
-1
@@ -20,6 +20,15 @@
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include <cstring>
|
||||
|
||||
#if TARGET_PC
|
||||
void dMenu_Fmap2DBack_c::fMapBackWide() {
|
||||
mpBaseScreen->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f);
|
||||
mpBaseScreen->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f);
|
||||
mpBackScreen->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f);
|
||||
mpBackScreen->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
dMenu_Fmap2DBack_c::dMenu_Fmap2DBack_c() {
|
||||
dMeter2Info_setMapDrugFlag(0);
|
||||
|
||||
@@ -267,6 +276,10 @@ dMenu_Fmap2DBack_c::~dMenu_Fmap2DBack_c() {
|
||||
}
|
||||
|
||||
void dMenu_Fmap2DBack_c::draw() {
|
||||
#if TARGET_PC
|
||||
fMapBackWide();
|
||||
#endif
|
||||
|
||||
calcBlink();
|
||||
|
||||
J2DGrafContext* grafPort = dComIfGp_getCurrentGrafPort();
|
||||
@@ -1199,7 +1212,7 @@ f32 dMenu_Fmap2DBack_c::getMapScissorAreaSizeX() {
|
||||
}
|
||||
|
||||
f32 dMenu_Fmap2DBack_c::getMapScissorAreaSizeRealX() {
|
||||
#if PLATFORM_GCN && !TARGET_PC
|
||||
#if PLATFORM_GCN
|
||||
return getMapScissorAreaSizeX();
|
||||
#else
|
||||
return getMapScissorAreaSizeX() * mDoGph_gInf_c::getScale();
|
||||
@@ -1407,6 +1420,11 @@ void dMenu_Fmap2DBack_c::stageTextureDraw() {
|
||||
mpSpotTexture->setAlpha(mAlphaRate * 255.0f * field_0xfa8 * mSpotTextureFadeAlpha);
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
JUTPalette* pPalette = mpSpotTexture->getTexture(0)->getPalette();
|
||||
pPalette->dataUploaded();
|
||||
#endif
|
||||
|
||||
mpSpotTexture->draw(mTransX + getMapScissorAreaLX(), mTransZ + getMapScissorAreaLY(),
|
||||
getMapScissorAreaSizeRealX(), getMapScissorAreaSizeRealY(), false, false,
|
||||
false);
|
||||
@@ -2179,6 +2197,17 @@ void dMenu_Fmap2DBack_c::setArrowPosAxis(f32 i_posX, f32 i_posZ) {
|
||||
control_ypos = 0.0f;
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
void dMenu_Fmap2DTop_c::fMapTopWide() {
|
||||
mpTitleScreen->search(MULTI_CHAR('spot0_n'))->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f);
|
||||
mpTitleScreen->search(MULTI_CHAR('spot2_n'))->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f);
|
||||
mpTitleScreen->search(MULTI_CHAR('name_n'))->translate(mDoGph_gInf_c::ScaleHUDXLeft(-243.0f), -169.0f);
|
||||
mpTitleScreen->search(MULTI_CHAR('sub_n_n'))->translate(mDoGph_gInf_c::ScaleHUDXLeft(-80.0f), -154.0f);
|
||||
mpTitleScreen->search(MULTI_CHAR('btn_i_n'))->translate(mDoGph_gInf_c::ScaleHUDXLeft(-241.0f), 177.0f);
|
||||
mpTitleScreen->search(MULTI_CHAR('cont_n'))->translate(mDoGph_gInf_c::ScaleHUDXRight(515.0f), 83.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
dMenu_Fmap2DTop_c::dMenu_Fmap2DTop_c(JKRExpHeap* i_heap, STControl* i_stick) {
|
||||
mpHeap = i_heap;
|
||||
mTransX = 0.0f;
|
||||
@@ -2572,6 +2601,10 @@ void dMenu_Fmap2DTop_c::setAllAlphaRate(f32 i_rate, bool i_init) {
|
||||
}
|
||||
|
||||
void dMenu_Fmap2DTop_c::draw() {
|
||||
#if TARGET_PC
|
||||
fMapTopWide();
|
||||
#endif
|
||||
|
||||
u32 scissor_left, scissor_top, scissor_width, scissor_height;
|
||||
J2DOrthoGraph* ctx = static_cast<J2DOrthoGraph*>(dComIfGp_getCurrentGrafPort());
|
||||
ctx->setup2D();
|
||||
|
||||
@@ -2306,6 +2306,10 @@ void dMeter_drawHIO_c::updateOnWide() {
|
||||
// River Canoe Minigame
|
||||
g_drawHIO.mMiniGame.mCounterPosX[1] = mDoGph_gInf_c::ScaleHUDXRight(g_drawHIO.mMiniGame.mCounterPosX[1]);
|
||||
g_drawHIO.mMiniGame.mIconPosX[1] = mDoGph_gInf_c::ScaleHUDXRight(g_drawHIO.mMiniGame.mIconPosX[1]);
|
||||
|
||||
// Bulblin Count in Hidden Village
|
||||
g_drawHIO.mMiniGame.mCounterPosX[2] = mDoGph_gInf_c::ScaleHUDXRight(g_drawHIO.mMiniGame.mCounterPosX[2]);
|
||||
g_drawHIO.mMiniGame.mIconPosX[2] = mDoGph_gInf_c::ScaleHUDXRight(g_drawHIO.mMiniGame.mIconPosX[2]);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/memory.h"
|
||||
#include <dusk/autosave.h>
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
@@ -700,6 +701,10 @@ static u8 lbl_8074CAE4;
|
||||
static u32 l_sceneChangeStartTick;
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
static BOOL autoSaved;
|
||||
#endif
|
||||
|
||||
static int dScnPly_Execute(dScnPly_c* i_this) {
|
||||
#if DEBUG
|
||||
fapGm_HIO_c::startCpuTimer();
|
||||
@@ -742,6 +747,15 @@ static int dScnPly_Execute(dScnPly_c* i_this) {
|
||||
}
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
if (!dComIfGp_event_runCheck() && !fopOvlpM_IsPeek() && !dComIfG_resetToOpening(i_this) &&
|
||||
!dComIfGp_isEnableNextStage() && autoSaved == FALSE)
|
||||
{
|
||||
triggerAutoSave();
|
||||
autoSaved = TRUE;
|
||||
}
|
||||
#endif
|
||||
|
||||
dKy_itudemo_se();
|
||||
|
||||
#if DEBUG
|
||||
@@ -1593,6 +1607,11 @@ static int dScnPly_Create(scene_class* i_this) {
|
||||
|
||||
dScnPly_c* a_this = (dScnPly_c*)i_this;
|
||||
int phase_state = dComLbG_PhaseHandler(&a_this->field_0x1c4, l_method, a_this);
|
||||
|
||||
#if TARGET_PC
|
||||
autoSaved = FALSE;
|
||||
#endif
|
||||
|
||||
return phase_state;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,11 @@
|
||||
#include "lingcod/lingcod.h"
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/settings.h"
|
||||
#include <f_ap/f_ap_game.h>
|
||||
#include <dusk/autosave.h>
|
||||
#endif
|
||||
|
||||
static u8 dSv_item_rename(u8 i_itemNo) {
|
||||
switch (i_itemNo) {
|
||||
@@ -345,6 +349,10 @@ void dSv_player_item_c::setItem(int i_slotNo, u8 i_itemNo) {
|
||||
dComIfGp_setSelectItem(i);
|
||||
}
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
triggerAutoSave();
|
||||
#endif
|
||||
}
|
||||
|
||||
u8 dSv_player_item_c::getItem(int i_slotNo, bool i_checkCombo) const {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "os_report.h"
|
||||
|
||||
aurora::Module Log("dusk::osReport");
|
||||
|
||||
bool dusk::OSReportReallyForceEnable = false;
|
||||
|
||||
u8 __OSReport_disable;
|
||||
|
||||
void OSReportDisable() {
|
||||
__OSReport_disable = true;
|
||||
}
|
||||
|
||||
void OSReportEnable() {
|
||||
__OSReport_disable = false;
|
||||
}
|
||||
|
||||
static bool checkEnabled() {
|
||||
return !__OSReport_disable || dusk::OSReportReallyForceEnable;
|
||||
}
|
||||
|
||||
static std::string FormatToString(const char* msg, va_list list) {
|
||||
int ret = vsnprintf(nullptr, 0, msg, list);
|
||||
std::string buf(ret, '\0');
|
||||
vsnprintf(buf.data(), buf.size(), msg, list);
|
||||
buf.pop_back();
|
||||
return buf;
|
||||
}
|
||||
|
||||
void OSReport_Error(const char* fmt, ...) {
|
||||
if (!checkEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
const auto str = FormatToString(fmt, args);
|
||||
va_end(args);
|
||||
|
||||
Log.error("{}", str);
|
||||
}
|
||||
|
||||
void OSReport_FatalError(const char* fmt, ...) {
|
||||
if (!checkEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
const auto str = FormatToString(fmt, args);
|
||||
va_end(args);
|
||||
|
||||
Log.fatal("{}", str);
|
||||
}
|
||||
|
||||
void OSReport_Warning(const char* fmt, ...) {
|
||||
if (!checkEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
const auto str = FormatToString(fmt, args);
|
||||
va_end(args);
|
||||
|
||||
Log.warn("{}", str);
|
||||
}
|
||||
|
||||
void OSReport_System(const char* fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
OSVAttention(fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void OSVAttention(const char* fmt, va_list args) {
|
||||
if (!checkEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto str = FormatToString(fmt, args);
|
||||
|
||||
Log.info("{}", str);
|
||||
}
|
||||
|
||||
void OSAttention(const char* fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
OSVAttention(fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
@@ -48,6 +48,20 @@ f32 dusk::audio::MasterVolume = 1.0f;
|
||||
f32 dusk::audio::PrevMasterVolume = 1.0f;
|
||||
bool dusk::audio::EnableReverb = true;
|
||||
bool dusk::audio::DumpAudio = false;
|
||||
bool dusk::audio::EnableHrtf = false;
|
||||
f32 dusk::audio::HrtfGain = 0.5f;
|
||||
|
||||
|
||||
// 3dB at 5kHz.
|
||||
static constexpr f32 HRTF_LP_K = 0.75f;
|
||||
static constexpr f32 HRTF_ALLPASS_G = 0.3f;
|
||||
// Front never drops below (1 - HRTF_EXTRACT_MAX).
|
||||
static constexpr f32 HRTF_EXTRACT_MAX = 0.6f;
|
||||
|
||||
static f32 sHrtfLp1 = 0.0f;
|
||||
static f32 sHrtfLp2 = 0.0f;
|
||||
static f32 sHrtfApIn1 = 0.0f;
|
||||
static f32 sHrtfApOut1 = 0.0f;
|
||||
|
||||
/**
|
||||
* Validate that a DSP channel's format is actually something we know how to play.
|
||||
@@ -283,6 +297,9 @@ void dusk::audio::DspRender(OutputSubframe& subframe) {
|
||||
DspSubframe reverbInputR = {};
|
||||
bool anyReverbInput = false;
|
||||
|
||||
DspSubframe surroundBus = {};
|
||||
bool anySurroundInput = false;
|
||||
|
||||
for (int i = 0; i < channels.size(); i++) {
|
||||
auto& channel = channels[i];
|
||||
auto& channelAux = ChannelAux[i];
|
||||
@@ -324,6 +341,21 @@ void dusk::audio::DspRender(OutputSubframe& subframe) {
|
||||
}
|
||||
}
|
||||
|
||||
if (EnableHrtf && channel.mAutoMixerBeenSet) {
|
||||
f32 dolby = (channel.mAutoMixerPanDolby & 0xFF) / 127.0f;
|
||||
if (dolby > 0.0f) {
|
||||
anySurroundInput = true;
|
||||
f32 extract = dolby * HRTF_EXTRACT_MAX;
|
||||
f32 frontScale = 1.0f - extract;
|
||||
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
|
||||
f32 mono = (channelSubframe.channels[0][j] + channelSubframe.channels[1][j]) * 0.5f;
|
||||
surroundBus[j] += mono * extract;
|
||||
channelSubframe.channels[0][j] *= frontScale;
|
||||
channelSubframe.channels[1][j] *= frontScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (DumpAudio && sChannelDumpFiles[i]) {
|
||||
f32 interleaved[DSP_SUBFRAME_SIZE * 2];
|
||||
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
|
||||
@@ -349,6 +381,28 @@ void dusk::audio::DspRender(OutputSubframe& subframe) {
|
||||
ReverbHasTail = wetEnergy >= REVERB_ENERGY_EPSILON;
|
||||
}
|
||||
|
||||
if (EnableHrtf && anySurroundInput) {
|
||||
// Two-pole LPF: -12 dB/oct above 3 kHz
|
||||
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
|
||||
sHrtfLp1 = (1.0f - HRTF_LP_K) * sHrtfLp1 + HRTF_LP_K * surroundBus[j];
|
||||
sHrtfLp2 = (1.0f - HRTF_LP_K) * sHrtfLp2 + HRTF_LP_K * sHrtfLp1;
|
||||
surroundBus[j] = sHrtfLp2;
|
||||
}
|
||||
|
||||
// Mix into L and R
|
||||
// L gets the filtered signal directly; R gets it allpass for mild decorrelation
|
||||
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
|
||||
f32 s = surroundBus[j];
|
||||
|
||||
subframe.channels[0][j] += s * HrtfGain;
|
||||
|
||||
f32 r = -HRTF_ALLPASS_G * s + sHrtfApIn1 + HRTF_ALLPASS_G * sHrtfApOut1;
|
||||
sHrtfApIn1 = s;
|
||||
sHrtfApOut1 = r;
|
||||
subframe.channels[1][j] += r * HrtfGain;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& channel : subframe.channels) {
|
||||
ApplyVolume(channel, channel, PrevMasterVolume, MasterVolume);
|
||||
}
|
||||
|
||||
@@ -133,4 +133,6 @@ namespace dusk::audio {
|
||||
extern f32 PrevMasterVolume;
|
||||
extern bool EnableReverb;
|
||||
extern bool DumpAudio;
|
||||
extern bool EnableHrtf;
|
||||
extern f32 HrtfGain;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#include "dusk/autosave.h"
|
||||
#include "imgui/ImGuiConsole.hpp"
|
||||
|
||||
u8 mSaveBuffer[QUEST_LOG_SIZE * 3];
|
||||
u8 mAutoSaveProc = 0;
|
||||
int autoSaveWriteState = 0;
|
||||
|
||||
typedef void (*AutoSaveFuncs)();
|
||||
static AutoSaveFuncs AutoSaveFuncsProc[] = {
|
||||
noAutoSave, enterAutoSave, autoSaving, waitingForWrite, endAutoSave,
|
||||
};
|
||||
|
||||
void noAutoSave() {}
|
||||
|
||||
void triggerAutoSave() {
|
||||
if (dusk::getSettings().game.autoSave && mAutoSaveProc == 0 &&
|
||||
strcmp(dComIfGp_getStartStageName(), "F_SP102") != 0)
|
||||
{
|
||||
mAutoSaveProc = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void updateAutoSave() {
|
||||
(AutoSaveFuncsProc[mAutoSaveProc])();
|
||||
}
|
||||
|
||||
void writeAutoSave() {
|
||||
int stageNo = dStage_stagInfo_GetSaveTbl(dComIfGp_getStageStagInfo());
|
||||
|
||||
dComIfGs_putSave(stageNo);
|
||||
dComIfGs_setMemoryToCard(mSaveBuffer, dComIfGs_getDataNum());
|
||||
mDoMemCdRWm_SetCheckSumGameData(mSaveBuffer, dComIfGs_getDataNum());
|
||||
|
||||
u8* save = mSaveBuffer;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
mDoMemCdRWm_TestCheckSumGameData(save);
|
||||
save += QUEST_LOG_SIZE;
|
||||
}
|
||||
|
||||
g_mDoMemCd_control.save(mSaveBuffer, sizeof(mSaveBuffer), 0);
|
||||
}
|
||||
|
||||
void autoSaving() {
|
||||
int cardState = g_mDoMemCd_control.LoadSync(mSaveBuffer, sizeof(mSaveBuffer), 0);
|
||||
if (cardState != 0) {
|
||||
if (cardState == 2) {
|
||||
mAutoSaveProc = 1;
|
||||
} else if (cardState == 1) {
|
||||
writeAutoSave();
|
||||
mAutoSaveProc = 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void enterAutoSave() {
|
||||
u32 cardStatus = g_mDoMemCd_control.getStatus(0);
|
||||
|
||||
if (cardStatus != 14) {
|
||||
switch (cardStatus) {
|
||||
case 2:
|
||||
g_mDoMemCd_control.load();
|
||||
mAutoSaveProc = 2;
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
break;
|
||||
default:
|
||||
mAutoSaveProc = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void waitingForWrite() {
|
||||
autoSaveWriteState = g_mDoMemCd_control.SaveSync();
|
||||
|
||||
if (autoSaveWriteState == 2) {
|
||||
mAutoSaveProc = 0;
|
||||
} else if (autoSaveWriteState == 1) {
|
||||
mAutoSaveProc = 4;
|
||||
}
|
||||
}
|
||||
|
||||
void endAutoSave() {
|
||||
dusk::g_imguiConsole.AddToast("Saving...", 2.0f);
|
||||
mAutoSaveProc = 0;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "ImGuiConsole.hpp"
|
||||
#include "ImGuiMenuTools.hpp"
|
||||
#include <cmath>
|
||||
#include "JSystem/JAudio2/JAISeq.h"
|
||||
#include "JSystem/JAudio2/JAISeMgr.h"
|
||||
#include "JSystem/JAudio2/JAISeqMgr.h"
|
||||
#include "JSystem/JAudio2/JAIStreamMgr.h"
|
||||
@@ -15,6 +17,24 @@ static std::array<u32, DSP_CHANNELS> lastResetCounts = {};
|
||||
|
||||
static bool sortUpdateCount = true;
|
||||
|
||||
static void DrawDirectionGauge(float pan, float dolby) {
|
||||
constexpr float R = 20.0f;
|
||||
constexpr float SIZE = R * 2.0f + 4.0f;
|
||||
|
||||
ImVec2 origin = ImGui::GetCursorScreenPos();
|
||||
ImGui::Dummy(ImVec2(SIZE, SIZE));
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImVec2 c = ImVec2(origin.x + SIZE * 0.5f, origin.y + SIZE * 0.5f);
|
||||
|
||||
dl->AddCircle(c, R, IM_COL32(90, 90, 90, 255), 32);
|
||||
|
||||
float dx = (pan - 0.5f) * 2.0f;
|
||||
float dy = dolby * 2.0f - 1.0f;
|
||||
float len = sqrtf(dx * dx + dy * dy);
|
||||
if (len > 1.0f) { dx /= len; dy /= len; }
|
||||
dl->AddLine(c, ImVec2(c.x + dx * R, c.y + dy * R), IM_COL32(255, 200, 50, 255), 1.5f);
|
||||
}
|
||||
|
||||
static void DisplayDspChannel(int i) {
|
||||
using namespace dusk::audio;
|
||||
|
||||
@@ -52,8 +72,10 @@ static void DisplayDspChannel(int i) {
|
||||
auto fxMix = (channel.mAutoMixerFxMix >> 8) / 127.5f;
|
||||
auto volume = VolumeFromU16(channel.mAutoMixerVolume);
|
||||
auto pitch = channel.mPitch / 4096.0f;
|
||||
DrawDirectionGauge(pan, dolby);
|
||||
ImGui::SameLine();
|
||||
ImGui::Text(
|
||||
"Auto mixer active (pan: %f, dolby: %f, fx: %f, volume: %f, pitch %f)",
|
||||
"pan: %.2f dolby: %.2f\nfx: %.2f vol: %.2f pitch: %.2f",
|
||||
pan, dolby, fxMix, volume, pitch);
|
||||
} else {
|
||||
ImGui::Text(
|
||||
@@ -183,6 +205,10 @@ static void ShowAllJAISes() {
|
||||
if (ImGui::Button("Pause All")) {
|
||||
category->pause(true);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Resume All")) {
|
||||
category->pause(false);
|
||||
}
|
||||
|
||||
for (auto seLink = category->getSeList()->getFirst(); seLink != nullptr; seLink = seLink->getNext()) {
|
||||
const auto se = seLink->getObject();
|
||||
@@ -196,6 +222,33 @@ static void ShowAllJAISes() {
|
||||
}
|
||||
|
||||
|
||||
static void ShowSeqTracks(JAISeq& seq) {
|
||||
JASTrack& root = seq.inner_.outputTrack;
|
||||
|
||||
for (int group = 0; group < 2; group++) {
|
||||
JASTrack* groupTrack = root.getChild(group);
|
||||
if (groupTrack == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int j = 0; j < JASTrack::MAX_CHILDREN; j++) {
|
||||
JASTrack* track = groupTrack->getChild(j);
|
||||
if (track == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int trackIdx = group * 16 + j;
|
||||
char label[64];
|
||||
snprintf(label, sizeof(label), "Track %d (bank %hu, prog %hu)##%p",
|
||||
trackIdx, track->getBankNumber(), track->getProgNumber(), track);
|
||||
bool muted = track->mFlags.mute;
|
||||
if (ImGui::Checkbox(label, &muted)) {
|
||||
track->mute(muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void ShowAllJAISeqs() {
|
||||
auto& mgr = *JAISeqMgr::getInstance();
|
||||
|
||||
@@ -206,6 +259,26 @@ static void ShowAllJAISeqs() {
|
||||
if (ImGui::Button("Unpause")) {
|
||||
mgr.pause(false);
|
||||
}
|
||||
|
||||
ImGui::Text("Active sequences: %d", mgr.getNumActiveSeqs());
|
||||
|
||||
auto* seqList = mgr.getSeqList();
|
||||
for (auto* link = seqList->getFirst(); link != nullptr; link = link->getNext()) {
|
||||
JAISeq* seq = link->getObject();
|
||||
if (seq == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%p", seq);
|
||||
|
||||
if (ImGui::BeginChild(buf, ImVec2(), ImGuiChildFlags_Border | ImGuiChildFlags_AutoResizeY)) {
|
||||
ImGui::Text("Seq [%p]", seq);
|
||||
ShowSeqTracks(*seq);
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
}
|
||||
|
||||
void dusk::ImGuiMenuTools::ShowAudioDebug() {
|
||||
|
||||
@@ -324,18 +324,18 @@ namespace dusk {
|
||||
ImGuiMenuGame::ToggleFullscreen();
|
||||
}
|
||||
|
||||
if (!dusk::IsGameLaunched) {
|
||||
m_preLaunchWindow.draw();
|
||||
}
|
||||
// if (!dusk::IsGameLaunched) {
|
||||
// m_preLaunchWindow.draw();
|
||||
// }
|
||||
|
||||
m_isHidden = !getSettings().backend.duskMenuOpen;
|
||||
bool showMenu = !dusk::IsGameLaunched || !CheckMenuViewToggle(ImGuiKey_F1, m_isHidden);
|
||||
if (dusk::IsGameLaunched) {
|
||||
const bool menuOpen = !m_isHidden;
|
||||
if (getSettings().backend.duskMenuOpen != menuOpen) {
|
||||
getSettings().backend.duskMenuOpen.setValue(menuOpen);
|
||||
Save();
|
||||
}
|
||||
if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) {
|
||||
m_isHidden = !m_isHidden;
|
||||
}
|
||||
bool showMenu = !m_isHidden;
|
||||
if (getSettings().backend.duskMenuOpen != showMenu) {
|
||||
getSettings().backend.duskMenuOpen.setValue(showMenu);
|
||||
Save();
|
||||
}
|
||||
|
||||
// The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg,
|
||||
@@ -365,10 +365,10 @@ namespace dusk {
|
||||
}
|
||||
|
||||
if (dusk::IsGameLaunched && !m_isLaunchInitialized) {
|
||||
m_toasts.emplace_back(ImGui::GetIO().MouseSource == ImGuiMouseSource_TouchScreen ?
|
||||
AddToast(ImGui::GetIO().MouseSource == ImGuiMouseSource_TouchScreen ?
|
||||
"Tap to toggle menu"s :
|
||||
"Press F1 to toggle menu"s,
|
||||
2.5f);
|
||||
4.f);
|
||||
m_isLaunchInitialized = true;
|
||||
if (getSettings().game.liveSplitEnabled) {
|
||||
dusk::speedrun::connectLiveSplit();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,7 @@ static void ApplyPresetDusk() {
|
||||
s.game.enableFrameInterpolation.setValue(true);
|
||||
s.game.sunsSong.setValue(true);
|
||||
s.game.bloomMode.setValue(BloomMode::Dusk);
|
||||
s.game.autoSave.setValue(true);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -180,9 +180,9 @@ namespace dusk {
|
||||
void ShowHeapDetailed(JKRHeap* heap, OpenHeapData& data, bool& open) {
|
||||
char title[128];
|
||||
const char* name = data.Safe ? heap->getName() : "INVALID";
|
||||
snprintf(title, sizeof(title), "Heap %s##%p", heap->getName(), static_cast<const void*>(heap));
|
||||
snprintf(title, sizeof(title), "Heap %s##%p", name, static_cast<const void*>(heap));
|
||||
|
||||
if (!ImGui::Begin(name, &open)) {
|
||||
if (!ImGui::Begin(title, &open)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
@@ -195,7 +195,7 @@ namespace dusk {
|
||||
|
||||
heap->lock();
|
||||
|
||||
ImGui::Text("Name: %s", heap->getName());
|
||||
ImGui::Text("Name: %s", name);
|
||||
const auto size = BytesToString(heap->getSize());
|
||||
const auto freeSize = BytesToString(heap->getFreeSize());
|
||||
ImGui::Text("Size: %08X (%s), free: %08X (%s)", heap->getSize(), size.c_str(), heap->getFreeSize(), freeSize.c_str());
|
||||
|
||||
@@ -3,31 +3,13 @@
|
||||
|
||||
#include "ImGuiEngine.hpp"
|
||||
#include "ImGuiConsole.hpp"
|
||||
#include "ImGuiMenuGame.hpp"
|
||||
#include "ImGuiConfig.hpp"
|
||||
|
||||
#include "JSystem/JUtility/JUTGamePad.h"
|
||||
#include "dusk/audio/DuskAudioSystem.h"
|
||||
#include "dusk/audio/DuskDsp.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/hotkeys.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/livesplit.h"
|
||||
#include "m_Do/m_Do_controller_pad.h"
|
||||
#include "m_Do/m_Do_graphic.h"
|
||||
|
||||
#include <aurora/gfx.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
|
||||
#include "m_Do/m_Do_main.h"
|
||||
|
||||
namespace {
|
||||
constexpr int kInternalResolutionScaleMax = 12;
|
||||
} // namespace
|
||||
|
||||
namespace aurora::gx {
|
||||
extern bool enableLodBias;
|
||||
}
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
|
||||
namespace dusk {
|
||||
void ImGuiMenuGame::ToggleFullscreen() {
|
||||
@@ -40,475 +22,17 @@ namespace dusk {
|
||||
|
||||
void ImGuiMenuGame::draw() {
|
||||
if (ImGui::BeginMenu("Settings")) {
|
||||
drawAudioMenu();
|
||||
drawCheatsMenu();
|
||||
drawGameplayMenu();
|
||||
drawGraphicsMenu();
|
||||
drawInputMenu();
|
||||
drawInterfaceMenu();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::MenuItem("Reset", hotkeys::DO_RESET)) {
|
||||
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
|
||||
}
|
||||
|
||||
if (!IsMobile && ImGui::MenuItem("Exit")) {
|
||||
dusk::IsRunning = false;
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiMenuGame::drawGraphicsMenu() {
|
||||
if (ImGui::BeginMenu("Graphics")) {
|
||||
ImGui::SeparatorText("Display");
|
||||
|
||||
if (!IsMobile) {
|
||||
if (ImGui::MenuItem("Toggle Fullscreen", hotkeys::TOGGLE_FULLSCREEN)) {
|
||||
ToggleFullscreen();
|
||||
}
|
||||
|
||||
if (ImGui::Button("Restore Default Window Size")) {
|
||||
getSettings().video.enableFullscreen.setValue(false);
|
||||
VISetWindowFullscreen(false);
|
||||
VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2);
|
||||
VICenterWindow();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
bool vsync = getSettings().video.enableVsync;
|
||||
if (ImGui::Checkbox("Enable VSync", &vsync)) {
|
||||
getSettings().video.enableVsync.setValue(vsync);
|
||||
aurora_enable_vsync(vsync);
|
||||
config::Save();
|
||||
}
|
||||
|
||||
bool lockAspect = getSettings().video.lockAspectRatio;
|
||||
if (ImGui::Checkbox("Force 4:3 Aspect Ratio", &lockAspect)) {
|
||||
getSettings().video.lockAspectRatio.setValue(lockAspect);
|
||||
|
||||
if (lockAspect) {
|
||||
AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT);
|
||||
} else {
|
||||
AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH);
|
||||
}
|
||||
|
||||
config::Save();
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Resolution");
|
||||
|
||||
u32 internalResolutionWidth = 0;
|
||||
u32 internalResolutionHeight = 0;
|
||||
AuroraGetRenderSize(&internalResolutionWidth, &internalResolutionHeight);
|
||||
ImGui::TextDisabled("Current internal resolution: %ux%u", internalResolutionWidth,
|
||||
internalResolutionHeight);
|
||||
|
||||
int scale = std::clamp(getSettings().game.internalResolutionScale.getValue(), 0,
|
||||
kInternalResolutionScaleMax);
|
||||
if (ImGui::SliderInt("Internal Resolution", &scale, 0, kInternalResolutionScaleMax,
|
||||
scale == 0 ? "Auto" : "%dx"))
|
||||
{
|
||||
getSettings().game.internalResolutionScale.setValue(scale);
|
||||
VISetFrameBufferScale(static_cast<float>(scale));
|
||||
config::Save();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Auto renders at the native window resolution.\n"
|
||||
"Higher values scale the game's internal framebuffer.");
|
||||
}
|
||||
|
||||
config::ImGuiSliderInt("Shadow Resolution", getSettings().game.shadowResolutionMultiplier, 1, 8, "x%d");
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Improves the shadow resolution, making them higher quality.");
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Post-Processing");
|
||||
|
||||
constexpr const char* bloomModeNames[] = {"Off", "Classic", "Dusk"};
|
||||
int bloomMode = static_cast<int>(getSettings().game.bloomMode.getValue());
|
||||
if (ImGui::BeginCombo("Bloom", bloomModeNames[bloomMode])) {
|
||||
for (int i = 0; i < IM_ARRAYSIZE(bloomModeNames); i++) {
|
||||
const bool selected = bloomMode == i;
|
||||
if (ImGui::Selectable(bloomModeNames[i], selected)) {
|
||||
getSettings().game.bloomMode.setValue(static_cast<BloomMode>(i));
|
||||
config::Save();
|
||||
}
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
bool bloomOff = bloomMode == static_cast<int>(BloomMode::Off);
|
||||
if (bloomOff) ImGui::BeginDisabled();
|
||||
float mult = getSettings().game.bloomMultiplier.getValue();
|
||||
if (ImGui::SliderFloat("Bloom Brightness", &mult, 0.0f, 1.0f, "%.2f")) {
|
||||
getSettings().game.bloomMultiplier.setValue(mult);
|
||||
config::Save();
|
||||
}
|
||||
if (bloomOff) ImGui::EndDisabled();
|
||||
|
||||
ImGui::SeparatorText("Rendering");
|
||||
|
||||
config::ImGuiCheckbox("Unlock Framerate", getSettings().game.enableFrameInterpolation);
|
||||
const bool frameInterpolationHovered = ImGui::IsItemHovered();
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.72f, 0.2f, 1.0f));
|
||||
ImGui::TextUnformatted("[EXPERIMENTAL]");
|
||||
ImGui::PopStyleColor();
|
||||
if (frameInterpolationHovered || ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Uses inter-frame interpolation to enable higher frame rates.\nVisual artifacts, animation glitches, or instability may occur.");
|
||||
}
|
||||
|
||||
ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias);
|
||||
|
||||
config::ImGuiCheckbox("Enable Depth of Field", getSettings().game.enableDepthOfField);
|
||||
|
||||
config::ImGuiCheckbox("Enable Mini-Map Shadows", getSettings().game.enableMapBackground);
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiMenuGame::drawGameplayMenu() {
|
||||
if (ImGui::BeginMenu("Gameplay")) {
|
||||
ImGui::SeparatorText("General");
|
||||
|
||||
config::ImGuiCheckbox("Mirror Mode", getSettings().game.enableMirrorMode);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Mirrors the world horizontally, matching the Wii version of the game.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Disable Main HUD", getSettings().game.disableMainHUD);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Disables the main HUD of the game.\n"
|
||||
"Useful for recording or a more immersive experience!");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Restores patched glitches from Wii USA 1.0,\n"
|
||||
"the first released version.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Enable Rotating Link Doll", getSettings().game.enableLinkDollRotation);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Enables rotating Link in the collection menu with the C-Stick");
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Difficulty");
|
||||
|
||||
ImGui::BeginDisabled(getSettings().game.speedrunMode);
|
||||
config::ImGuiSliderInt("Damage Multiplier", getSettings().game.damageMultiplier, 1, 8, "x%d");
|
||||
|
||||
config::ImGuiCheckbox("Instant Death", getSettings().game.instantDeath);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Any hit will instantly kill you.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("No Heart Drops", getSettings().game.noHeartDrops);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Hearts will never drop from enemies,\n"
|
||||
"pots and various other places.");
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::SeparatorText("Quality of Life");
|
||||
|
||||
config::ImGuiCheckbox("Bigger Wallets", getSettings().game.biggerWallets);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Wallet sizes are like in the HD version. (500, 1000, 2000)");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Rupees won't play cutscenes after you've collected them the first time.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Faster Climbing", getSettings().game.fastClimbing);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Quicker climbing on ladders and vines like the HD version.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Faster Tears of Light", getSettings().game.fastTears);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Tears of Light dropped by Shadow Insects pop out faster like the HD version.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Instant Saves", getSettings().game.instantSaves);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Skip the delay when writing to the Memory Card.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Hold B for Instant Text", getSettings().game.instantText);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Make text scroll immediately by holding B.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("No Climbing Miss Animation", getSettings().game.noMissClimbing);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Prevents Link from playing a struggle animation\n"
|
||||
"when grabbing ledges or climbing on vines.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("No Rupee Returns", getSettings().game.noReturnRupees);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Always collect Rupees even if your Wallet is too full.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("No Sword Recoil", getSettings().game.noSwordRecoil);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Link won't recoil when his sword hits walls.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("No 2nd Fish for Cat", getSettings().game.no2ndFishForCat);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Only need to fish once for Sera's cat to return.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Skip TV Settings Screen", getSettings().game.hideTvSettingsScreen);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Skip the TV calibration screen shown when loading a save.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Skip Warning Screen", getSettings().game.skipWarningScreen);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Skip the warning screen shown when starting the game.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Sun's Song (R+X)", getSettings().game.sunsSong);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Allows Wolf Link to howl and change the time of day.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Quick Transform (R+Y)", getSettings().game.enableQuickTransform);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Transform instantly by pressing R and Y simultaneously.");
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Speedrunning");
|
||||
if (config::ImGuiCheckbox("Speedrun Mode", getSettings().game.speedrunMode)) {
|
||||
resetForSpeedrunMode();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Enables Speedrunning options, while restricting certain gameplay modifiers.");
|
||||
}
|
||||
|
||||
ImGui::BeginDisabled(!getSettings().game.speedrunMode);
|
||||
bool prevLiveSplit = getSettings().game.liveSplitEnabled;
|
||||
config::ImGuiCheckbox("LiveSplit Connection", getSettings().game.liveSplitEnabled);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Connect to LiveSplit server on localhost:16834.");
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
if ((bool)getSettings().game.liveSplitEnabled != prevLiveSplit) {
|
||||
if (getSettings().game.liveSplitEnabled) {
|
||||
dusk::speedrun::connectLiveSplit();
|
||||
} else {
|
||||
dusk::speedrun::disconnectLiveSplit();
|
||||
DuskToast("LiveSplit disconnected", 3.f);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiMenuGame::drawCheatsMenu() {
|
||||
if (ImGui::BeginMenu("Cheats")) {
|
||||
ImGui::BeginDisabled(getSettings().game.speedrunMode);
|
||||
|
||||
ImGui::SeparatorText("Resources");
|
||||
config::ImGuiCheckbox("Infinite Hearts", getSettings().game.infiniteHearts);
|
||||
config::ImGuiCheckbox("Infinite Arrows", getSettings().game.infiniteArrows);
|
||||
config::ImGuiCheckbox("Infinite Bombs", getSettings().game.infiniteBombs);
|
||||
config::ImGuiCheckbox("Infinite Oil", getSettings().game.infiniteOil);
|
||||
config::ImGuiCheckbox("Infinite Oxygen", getSettings().game.infiniteOxygen);
|
||||
config::ImGuiCheckbox("Infinite Rupees", getSettings().game.infiniteRupees);
|
||||
config::ImGuiCheckbox("No Item Timer", getSettings().game.enableIndefiniteItemDrops);
|
||||
ImGui::SetItemTooltip("Item drops such as Rupees, Hearts, etc. will never disappear after they drop.");
|
||||
|
||||
ImGui::SeparatorText("Abilities");
|
||||
config::ImGuiCheckbox("Moon Jump (R+A)", getSettings().game.moonJump);
|
||||
config::ImGuiCheckbox("Super Clawshot", getSettings().game.superClawshot);
|
||||
config::ImGuiCheckbox("Always Greatspin", getSettings().game.alwaysGreatspin);
|
||||
config::ImGuiCheckbox("Fast Iron Boots", getSettings().game.enableFastIronBoots);
|
||||
|
||||
config::ImGuiCheckbox("Can Transform Anywhere", getSettings().game.canTransformAnywhere);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Allows you to transform even if NPCs are looking.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Fast Spinner", getSettings().game.fastSpinner);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Speeds up Spinner movement when holding R.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Free Magic Armor", getSettings().game.freeMagicArmor);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Makes the magic armor work without rupees.");
|
||||
}
|
||||
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiMenuGame::drawAudioMenu() {
|
||||
if (ImGui::BeginMenu("Audio")) {
|
||||
|
||||
ImGui::SeparatorText("Volume");
|
||||
|
||||
ImGui::Text("Master Volume");
|
||||
if (config::ImGuiSliderInt("##masterVolume", getSettings().audio.masterVolume, 0, 100)) {
|
||||
dusk::audio::SetMasterVolume(getSettings().audio.masterVolume / 100.0f);
|
||||
}
|
||||
|
||||
/*
|
||||
// TODO: Implement additional settings
|
||||
ImGui::Text("Main Music Volume");
|
||||
ImGui::SliderFloat("##mainMusicVolume", &getSettings().audio.mainMusicVolume, 0, 100);
|
||||
|
||||
ImGui::Text("Sub Music Volume");
|
||||
ImGui::SliderFloat("##subMusicVolume", &getSettings().audio.subMusicVolume, 0, 100);
|
||||
|
||||
ImGui::Text("Sound Effects Volume");
|
||||
ImGui::SliderFloat("##soundEffectsVolume", &getSettings().audio.soundEffectsVolume, 0, 100);
|
||||
|
||||
ImGui::Text("Fanfare Volume");
|
||||
ImGui::SliderFloat("##fanfareVolume", &getSettings().audio.fanfareVolume, 0, 100);
|
||||
|
||||
Z2AudioMgr* audioMgr = Z2AudioMgr::getInterface();
|
||||
if (audioMgr != nullptr) {
|
||||
}
|
||||
*/
|
||||
|
||||
ImGui::SeparatorText("Effects");
|
||||
|
||||
if (config::ImGuiCheckbox("Enable Reverb", getSettings().audio.enableReverb)) {
|
||||
dusk::audio::SetEnableReverb(getSettings().audio.enableReverb);
|
||||
}
|
||||
|
||||
|
||||
ImGui::SeparatorText("Tweaks");
|
||||
|
||||
config::ImGuiCheckbox("No Low HP Sound", getSettings().game.noLowHpSound);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Disable the beeping sound when having low health.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Non-Stop Midna's Lament", getSettings().game.midnasLamentNonStop);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Prevents enemy music while Midna's Lament is playing.");
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiMenuGame::drawInputMenu() {
|
||||
if (ImGui::BeginMenu("Input")) {
|
||||
ImGui::SeparatorText("Controller");
|
||||
|
||||
// TODO: Remove this once Controller Config exists in RmlUi
|
||||
if (ImGui::Button("Configure Controller")){
|
||||
m_showControllerConfig = !m_showControllerConfig;
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Camera");
|
||||
|
||||
config::ImGuiCheckbox("Free Camera", getSettings().game.freeCamera);
|
||||
|
||||
if (getSettings().game.freeCamera) {
|
||||
config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis);
|
||||
config::ImGuiCheckbox("Invert Camera Y Axis", getSettings().game.invertCameraYAxis);
|
||||
config::ImGuiSliderFloat("Free Camera Sensitivity", getSettings().game.freeCameraSensitivity, 0.5f, 2.0f, "%.1f");
|
||||
} else {
|
||||
config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis);
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Gyro");
|
||||
|
||||
config::ImGuiCheckbox("Gyro Aim", getSettings().game.enableGyroAim);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Enables the gyroscope on supported controllers\n"
|
||||
"while in look mode (C-Up) and while aiming the\n"
|
||||
"Slingshot, Gale Boomerang, Hero's Bow, Clawshot(s),\n"
|
||||
"Ball and Chain, and Dominion Rod.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Gyro Rollgoal", getSettings().game.enableGyroRollgoal);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Enables the gyroscope on supported controllers to\n"
|
||||
"tilt the Rollgoal table in Hena's Cabin.");
|
||||
}
|
||||
|
||||
if (getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal) {
|
||||
config::ImGuiSliderFloat("Gyro Pitch Sensitivity", getSettings().game.gyroSensitivityY, 0.25f, 4.0f, "%.2f");
|
||||
config::ImGuiSliderFloat("Gyro Yaw Sensitivity", getSettings().game.gyroSensitivityX, 0.25f, 4.0f, "%.2f");
|
||||
|
||||
if (getSettings().game.enableGyroRollgoal) {
|
||||
config::ImGuiSliderFloat("Rollgoal Sensitivity", getSettings().game.gyroSensitivityRollgoal, 0.25f, 4.0f, "%.2f");
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Additional multiplier for scaling how strongly\n"
|
||||
"the gyroscope affects the Rollgoal table.");
|
||||
}
|
||||
}
|
||||
|
||||
config::ImGuiSliderFloat("Gyro Deadband", getSettings().game.gyroDeadband, 0.0f, 0.5f, "%.3f");
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Angular rates below this magnitude are treated as zero,\n"
|
||||
"reducing drift and jitter when the controller is still.");
|
||||
}
|
||||
|
||||
config::ImGuiSliderFloat("Gyro Smoothing", getSettings().game.gyroSmoothing, 0.0f, 1.0f, "%.2f");
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Low values track raw gyro input more closely,\n"
|
||||
"while higher values smooth out input over time.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Invert Gyro Pitch", getSettings().game.gyroInvertPitch);
|
||||
config::ImGuiCheckbox("Invert Gyro Yaw", getSettings().game.gyroInvertYaw);
|
||||
}
|
||||
|
||||
ImGui::SeparatorText("Tools");
|
||||
|
||||
ImGui::BeginDisabled(getSettings().game.speedrunMode);
|
||||
config::ImGuiCheckbox("Turbo Key", getSettings().game.enableTurboKeybind);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Hold TAB to increase game speed by up to 4x.");
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::Checkbox("Show Input Viewer", &m_showInputViewer);
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiMenuGame::drawInterfaceMenu() {
|
||||
if (ImGui::BeginMenu("Interface")) {
|
||||
config::ImGuiCheckbox("Achievement Notifications", getSettings().game.enableAchievementNotifications);
|
||||
config::ImGuiCheckbox("Skip Pre-Launch UI", getSettings().backend.skipPreLaunchUI);
|
||||
config::ImGuiCheckbox("Show Pipeline Compilation", getSettings().backend.showPipelineCompilation);
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
config::ImGuiCheckbox("Enable Crash Reporting", getSettings().backend.enableCrashReporting);
|
||||
#endif
|
||||
if (!IsMobile) {
|
||||
config::ImGuiCheckbox("Pause on Focus Lost", getSettings().game.pauseOnFocusLost);
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
static void drawVirtualStick(const char* id, const ImVec2& stick) {
|
||||
float scale = ImGuiScale();
|
||||
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x + 45 * scale, ImGui::GetCursorPos().y + 10));
|
||||
@@ -1059,6 +583,7 @@ namespace dusk {
|
||||
getSettings().game.damageMultiplier.setValue(1);
|
||||
getSettings().game.instantDeath.setValue(false);
|
||||
getSettings().game.noHeartDrops.setValue(false);
|
||||
getSettings().game.hyperEnemies.setValue(false);
|
||||
|
||||
getSettings().game.infiniteHearts.setValue(false);
|
||||
getSettings().game.infiniteArrows.setValue(false);
|
||||
|
||||
@@ -55,13 +55,6 @@ namespace dusk {
|
||||
static void resetForSpeedrunMode();
|
||||
|
||||
private:
|
||||
void drawAudioMenu();
|
||||
void drawInputMenu();
|
||||
void drawGraphicsMenu();
|
||||
void drawGameplayMenu();
|
||||
void drawCheatsMenu();
|
||||
void drawInterfaceMenu();
|
||||
|
||||
struct {
|
||||
int m_selectedPort = 0;
|
||||
bool m_isReading = false;
|
||||
|
||||
@@ -41,6 +41,10 @@ static void OpenDataFolder() {
|
||||
#define DUSK_CAN_OPEN_DATA_FOLDER 0
|
||||
#endif
|
||||
|
||||
namespace aurora::gx {
|
||||
extern bool enableLodBias;
|
||||
}
|
||||
|
||||
namespace dusk {
|
||||
ImGuiMenuTools::ImGuiMenuTools() {}
|
||||
|
||||
@@ -91,6 +95,7 @@ namespace dusk {
|
||||
getSettings().game.disableWaterRefraction.setValue(disableWaterRefraction);
|
||||
config::Save();
|
||||
}
|
||||
ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "d/actor/d_a_player.h"
|
||||
|
||||
#include <map>
|
||||
#include <bit>
|
||||
|
||||
namespace dusk {
|
||||
enum ItemType {
|
||||
@@ -1295,8 +1296,33 @@ namespace dusk {
|
||||
membit.offDungeonItem(flag);
|
||||
}
|
||||
}
|
||||
|
||||
static void genCommonAreaFlags(dSv_memBit_c& membit) {
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 10.0f);
|
||||
|
||||
void genMembitFlags(const char* id, dSv_memBit_c& membit) {
|
||||
genDungeonItemCheckbox(membit, "Got Map", dSv_memBit_c::MAP);
|
||||
ImGui::SameLine(230.0f);
|
||||
genDungeonItemCheckbox(membit, "Got Compass", dSv_memBit_c::COMPASS);
|
||||
|
||||
genDungeonItemCheckbox(membit, "Got Boss Key", dSv_memBit_c::BOSS_KEY);
|
||||
ImGui::SameLine(230.0f);
|
||||
genDungeonItemCheckbox(membit, "Saw Boss Demo", dSv_memBit_c::STAGE_BOSS_DEMO);
|
||||
|
||||
genDungeonItemCheckbox(membit, "Got Heart Container", dSv_memBit_c::STAGE_LIFE);
|
||||
ImGui::SameLine(230.0f);
|
||||
genDungeonItemCheckbox(membit, "Defeated Boss", dSv_memBit_c::STAGE_BOSS_ENEMY);
|
||||
|
||||
genDungeonItemCheckbox(membit, "Defeated Miniboss", dSv_memBit_c::STAGE_BOSS_ENEMY_2);
|
||||
ImGui::SameLine(230.0f);
|
||||
genDungeonItemCheckbox(membit, "Got Ooccoo", dSv_memBit_c::OOCCOO_NOTE);
|
||||
|
||||
int keyTemp = membit.getKeyNum();
|
||||
if (ImGui::SliderInt("Keys", &keyTemp, 0, 5)) {
|
||||
membit.setKeyNum(keyTemp);
|
||||
}
|
||||
}
|
||||
|
||||
static void genMembitFlags(const char* id, dSv_memBit_c& membit) {
|
||||
ImGuiBeginGroupPanel("Chest", { 100, 100 });
|
||||
for (int j = 0; j < 2; j++) {
|
||||
drawFlagList(fmt::format("##_tbox{}", j).c_str(), membit.mTbox[j]);
|
||||
@@ -1322,29 +1348,10 @@ namespace dusk {
|
||||
drawFlagList(fmt::format("##_item{}", j).c_str(), membit.mItem[j]);
|
||||
}
|
||||
ImGuiEndGroupPanel();
|
||||
ImVec2 post_item_custor = ImGui::GetCursorPos();
|
||||
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 10.0f);
|
||||
|
||||
genDungeonItemCheckbox(membit, "Got Map", dSv_memBit_c::MAP);
|
||||
ImGui::SameLine(230.0f);
|
||||
genDungeonItemCheckbox(membit, "Got Compass", dSv_memBit_c::COMPASS);
|
||||
|
||||
genDungeonItemCheckbox(membit, "Got Boss Key", dSv_memBit_c::BOSS_KEY);
|
||||
ImGui::SameLine(230.0f);
|
||||
genDungeonItemCheckbox(membit, "Saw Boss Demo", dSv_memBit_c::STAGE_BOSS_DEMO);
|
||||
|
||||
genDungeonItemCheckbox(membit, "Got Heart Container", dSv_memBit_c::STAGE_LIFE);
|
||||
ImGui::SameLine(230.0f);
|
||||
genDungeonItemCheckbox(membit, "Defeated Boss", dSv_memBit_c::STAGE_BOSS_ENEMY);
|
||||
|
||||
genDungeonItemCheckbox(membit, "Defeated Miniboss", dSv_memBit_c::STAGE_BOSS_ENEMY_2);
|
||||
ImGui::SameLine(230.0f);
|
||||
genDungeonItemCheckbox(membit, "Got Ooccoo", dSv_memBit_c::OOCCOO_NOTE);
|
||||
|
||||
int keyTemp = membit.getKeyNum();
|
||||
if (ImGui::SliderInt("Keys", &keyTemp, 0, 5)) {
|
||||
membit.setKeyNum(keyTemp);
|
||||
}
|
||||
ImGui::SetCursorPos({post_item_custor.x, post_switch_cursor.y});
|
||||
// genCommonAreaFlags(membit);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
@@ -1392,74 +1399,326 @@ namespace dusk {
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiSaveEditor::drawFlagsTab() {
|
||||
if (ImGui::TreeNode("Current Region Flags")) {
|
||||
dSv_memBit_c& membit = g_dComIfG_gameInfo.info.mMemory.mBit;
|
||||
genMembitFlags("##TempSceneFlags", membit);
|
||||
|
||||
static void genAreaFlagTable(uint8_t areaIndex, dSv_memBit_c& membit) {
|
||||
constexpr auto makeMask = [](uint8_t size) -> uint16_t { return (1 << size) - 1; };
|
||||
constexpr auto getByteIndexFromFlag = [](uint16_t f) -> uint8_t { return f >> 8; };
|
||||
constexpr auto getBitMaskFromFlag = [](uint16_t f) -> uint8_t { return f & 0xff; };
|
||||
constexpr auto getValueSize = [getBitMaskFromFlag](uint16_t f) -> uint8_t {
|
||||
return std::popcount(getBitMaskFromFlag(f));
|
||||
};
|
||||
|
||||
constexpr auto makeEventFlag = [](uint8_t byteIndex, uint8_t bitIndices) -> uint16_t {
|
||||
return (byteIndex << 8) | bitIndices;
|
||||
};
|
||||
|
||||
stage_stag_info_class* pstag = dComIfGp_getStageStagInfo();
|
||||
if (pstag != nullptr) {
|
||||
int stageNo = dStage_stagInfo_GetSaveTbl(pstag);
|
||||
if (ImGui::Button("Save##SaveTempFlags")) {
|
||||
dComIfGs_putSave(stageNo);
|
||||
const auto eventFlagToAreaFlag = [&](uint16_t areaFlag) -> int {
|
||||
auto byteInd = getByteIndexFromFlag(areaFlag);
|
||||
constexpr size_t areaIndexSize = 5;
|
||||
// if we're looking at 0x580, that would be byte 5, and check if 0x80 is set on that byte
|
||||
// the event flags are structured differently than area flags
|
||||
// B is byte index, b is the flag mask to check
|
||||
// event flags are BBBBBBBB bbbbbbbb
|
||||
// for area flags, they check bitIndex, not mask, i is index
|
||||
// also area uses u32 index, not byte index
|
||||
// area flags are BBBiiiii
|
||||
// so we need to convert from bit mask to index
|
||||
// also our byte index has to become a u32 index
|
||||
|
||||
// dividing byte index by sizeof(u32) gets us the u32 index
|
||||
// but in big endian, the first byte is the highest order byte of the u32
|
||||
// so we skip 24 bytes for the first byte, 16 for the second, etc
|
||||
// essentially (3 - (x % 4)), reversing the modulus, 0=3, 1=2
|
||||
auto bitsToSkip = 8 * ((sizeof(u32) - 1) - (byteInd % sizeof(u32)));
|
||||
return ((byteInd / sizeof(u32)) << areaIndexSize) | ((std::countr_zero(areaFlag) + bitsToSkip) & makeMask(areaIndexSize));
|
||||
};
|
||||
|
||||
constexpr uint8_t validTbox = sizeof(membit.mTbox);
|
||||
constexpr uint8_t validSwitch = validTbox + sizeof(membit.mSwitch);
|
||||
constexpr uint8_t validItem = validSwitch + sizeof(membit.mItem);
|
||||
constexpr uint16_t tboxConvert = 0;
|
||||
constexpr uint16_t switchConvert = sizeof(membit.mTbox) << 8;
|
||||
constexpr uint16_t itemConvert = switchConvert + (sizeof(membit.mItem) << 8);
|
||||
|
||||
const auto LoadFlag = [&](uint16_t flag) -> bool {
|
||||
const auto byteIndex = getByteIndexFromFlag(flag);
|
||||
|
||||
if (byteIndex < validTbox) {
|
||||
return membit.isTbox(eventFlagToAreaFlag(flag - tboxConvert));
|
||||
} else if (byteIndex < validSwitch) {
|
||||
return membit.isSwitch(eventFlagToAreaFlag(flag - switchConvert));
|
||||
} else if (byteIndex < validItem) {
|
||||
return membit.isItem(eventFlagToAreaFlag(flag - itemConvert));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const auto SetFlag = [&](uint16_t flag, bool set) -> void {
|
||||
const auto byteIndex = getByteIndexFromFlag(flag);
|
||||
if (set) {
|
||||
if (byteIndex < validTbox) {
|
||||
membit.onTbox(eventFlagToAreaFlag(flag - tboxConvert));
|
||||
} else if (byteIndex < validSwitch) {
|
||||
membit.onSwitch(eventFlagToAreaFlag(flag - switchConvert));
|
||||
} else if (byteIndex < validItem) {
|
||||
membit.onItem(eventFlagToAreaFlag(flag - itemConvert));
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button("Load##LoadSaveFlags")) {
|
||||
dComIfGs_getSave(stageNo);
|
||||
} else {
|
||||
if (byteIndex < validTbox) {
|
||||
membit.offTbox(eventFlagToAreaFlag(flag - tboxConvert));
|
||||
} else if (byteIndex < validSwitch) {
|
||||
membit.offSwitch(eventFlagToAreaFlag(flag - switchConvert));
|
||||
} else if (byteIndex < validItem) {
|
||||
membit.offItem(eventFlagToAreaFlag(flag - itemConvert));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const auto LoadMultiByteFlag = [&](uint16_t flag) -> uint8_t {
|
||||
const auto bitInds = getBitMaskFromFlag(flag);
|
||||
const auto byteIndex = getByteIndexFromFlag(flag);
|
||||
|
||||
const uint16_t startingMask = std::bit_floor(bitInds);
|
||||
uint8_t val = 0;
|
||||
for (uint16_t bitIndexMask = startingMask; (bitInds & bitIndexMask) != 0;
|
||||
bitIndexMask >>= 1)
|
||||
{
|
||||
val <<= 1;
|
||||
if (LoadFlag(makeEventFlag(byteIndex, bitInds & bitIndexMask))) {
|
||||
val |= 1;
|
||||
}
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
const auto SetMultiByteFlag = [&](uint16_t flag, uint8_t val) -> void {
|
||||
const auto bitInds = getBitMaskFromFlag(flag);
|
||||
const auto byteIndex = getByteIndexFromFlag(flag);
|
||||
|
||||
const uint16_t startingMask = std::bit_floor(bitInds);
|
||||
uint16_t valueMask = 1 << (getValueSize(flag) - 1);
|
||||
|
||||
for (uint16_t bitIndexMask = startingMask; (bitInds & bitIndexMask) != 0;
|
||||
bitIndexMask >>= 1, valueMask >>= 1)
|
||||
{
|
||||
SetFlag(makeEventFlag(byteIndex, bitInds & bitIndexMask), (val & valueMask) != 0);
|
||||
}
|
||||
};
|
||||
|
||||
const auto LoadSpreadMultiByte = [&](uint16_t high, uint16_t low) -> uint8_t {
|
||||
if (low == AREA_FLAG_NONE)
|
||||
return LoadMultiByteFlag(high);
|
||||
return (LoadMultiByteFlag(high) << getValueSize(low)) | LoadMultiByteFlag(low);
|
||||
};
|
||||
|
||||
const auto SetSpreadMultiByte = [&](uint16_t high, uint16_t low, uint8_t value) -> void {
|
||||
if (low == AREA_FLAG_NONE)
|
||||
return SetMultiByteFlag(high, value);
|
||||
const auto lowerSize = getValueSize(low);
|
||||
SetMultiByteFlag(high, value >> lowerSize);
|
||||
SetMultiByteFlag(low, value & makeMask(lowerSize));
|
||||
};
|
||||
|
||||
auto iter = imguiAreaFlagLookup.find(areaIndex);
|
||||
if (iter == imguiAreaFlagLookup.end()) return;
|
||||
|
||||
auto& areaFlags = iter->second;
|
||||
|
||||
static ImGuiTextFilter filter;
|
||||
filter.Draw(); // Search bar
|
||||
|
||||
ImVec2 flagTableSize = {700, 400};
|
||||
if (ImGui::BeginTable("Area Flags", 3,
|
||||
ImGuiTableFlags_ScrollY | ImGuiTableFlags_ScrollX |
|
||||
ImGuiTableFlags_Sortable,
|
||||
flagTableSize))
|
||||
{
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
constexpr int COLUMN_FLAG = 0, COLUMN_BIT = 1, COLUMN_DESC = 2;
|
||||
ImGui::TableSetupColumn("Flag");
|
||||
ImGui::TableSetupColumn("Byte:Bit");
|
||||
ImGui::TableSetupColumn("Description");
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
// if we're sorting by whether the flag is set or not,
|
||||
// we want to re-sort whenever a flag updates, which means every frame cuz we don't
|
||||
// know when it changes. otherwise only re-sort when the sort is dirty
|
||||
if (auto* sort = ImGui::TableGetSortSpecs();
|
||||
sort != nullptr && sort->SpecsCount > 0 &&
|
||||
(sort->SpecsDirty || sort->Specs[0].ColumnIndex == COLUMN_FLAG))
|
||||
{
|
||||
const auto column = sort->Specs[0].ColumnIndex;
|
||||
const auto direction = sort->Specs[0].SortDirection;
|
||||
|
||||
// if we're sorting by flags, do special sort, regular sort is bad for sorting
|
||||
// bools it can swap values that are the same, and that causes constant
|
||||
// reordering
|
||||
if (column == COLUMN_FLAG) {
|
||||
if (direction == ImGuiSortDirection_Ascending) {
|
||||
sortByFlags(std::begin(areaFlags.bitFlags), std::end(areaFlags.bitFlags),
|
||||
LoadFlag);
|
||||
} else {
|
||||
sortByFlags(std::rbegin(areaFlags.bitFlags), std::rend(areaFlags.bitFlags),
|
||||
LoadFlag);
|
||||
}
|
||||
} else {
|
||||
const auto cmp = [column](const EventAreaFlags& l,
|
||||
const EventAreaFlags& r) -> bool {
|
||||
switch (column) {
|
||||
case COLUMN_DESC:
|
||||
return l.description < r.description;
|
||||
case COLUMN_BIT:
|
||||
return l.flagID < r.flagID;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (direction == ImGuiSortDirection_Ascending) {
|
||||
std::sort(std::begin(areaFlags.bitFlags), std::end(areaFlags.bitFlags),
|
||||
cmp);
|
||||
} else {
|
||||
std::sort(std::rbegin(areaFlags.bitFlags), std::rend(areaFlags.bitFlags),
|
||||
cmp);
|
||||
}
|
||||
}
|
||||
|
||||
sort->SpecsDirty = false;
|
||||
}
|
||||
|
||||
for (const auto& e : areaFlags.bitFlags) {
|
||||
std::string formattedBitLocation =
|
||||
fmt::format("{0:02X}:{1:02X}", e.byteIndex, e.bitIndex);
|
||||
|
||||
if (!filter.PassFilter(e.description.c_str()) &&
|
||||
!filter.PassFilter(formattedBitLocation.c_str()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
bool flag = LoadFlag(e.flagID);
|
||||
if (ImGui::Checkbox(fmt::format("##_unused_area_flag_{}", e.flagID).c_str(), &flag)) {
|
||||
SetFlag(e.flagID, flag);
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(formattedBitLocation.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(e.description.c_str());
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
for (const auto& multiByteFlag : areaFlags.multibyteFlags) {
|
||||
auto flagValue = LoadSpreadMultiByte(multiByteFlag.highOrderflag, multiByteFlag.lowOrderflag);
|
||||
|
||||
const char* currentVal = "UNKNOWN";
|
||||
|
||||
auto enumValIter = multiByteFlag.enumValues.find(flagValue);
|
||||
if (enumValIter != multiByteFlag.enumValues.end()) {
|
||||
currentVal = enumValIter->second;
|
||||
}
|
||||
|
||||
if (ImGui::BeginCombo(multiByteFlag.name, currentVal)) {
|
||||
for (const auto& [val, name] : multiByteFlag.enumValues) {
|
||||
if (ImGui::Selectable(name)) {
|
||||
SetSpreadMultiByte(multiByteFlag.highOrderflag, multiByteFlag.lowOrderflag, val);
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
|
||||
genCommonAreaFlags(membit);
|
||||
}
|
||||
|
||||
static void drawCurrentRegionFlags()
|
||||
{
|
||||
dSv_memBit_c& membit = g_dComIfG_gameInfo.info.mMemory.mBit;
|
||||
auto* stageData = dComIfGp_getStageStagInfo();
|
||||
if (!stageData)
|
||||
return;
|
||||
uint8_t stageIndex = dStage_stagInfo_GetSaveTbl(stageData);
|
||||
|
||||
genAreaFlagTable(stageIndex, membit);
|
||||
|
||||
if (ImGui::TreeNode("Flag Matrix")) {
|
||||
genMembitFlags("##TempSceneFlags", membit);
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
stage_stag_info_class* pstag = dComIfGp_getStageStagInfo();
|
||||
if (pstag != nullptr) {
|
||||
int stageNo = dStage_stagInfo_GetSaveTbl(pstag);
|
||||
if (ImGui::Button("Save##SaveTempFlags")) {
|
||||
dComIfGs_putSave(stageNo);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::Button("Load##LoadSaveFlags")) {
|
||||
dComIfGs_getSave(stageNo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiSaveEditor::drawFlagsTab() {
|
||||
if (ImGui::TreeNode("Current Region Flags")) {
|
||||
drawCurrentRegionFlags();
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
if (ImGui::TreeNode("Region Saved Flags")) {
|
||||
static std::array<const char*, 27> regionNames = {
|
||||
"Ordon",
|
||||
"Hyrule Sewers",
|
||||
"Faron",
|
||||
"Eldin",
|
||||
"Lanayru",
|
||||
"Reserved",
|
||||
"Hyrule Field",
|
||||
"Sacred Grove",
|
||||
"Snowpeak",
|
||||
"Castle Town",
|
||||
"Gerudo Desert",
|
||||
"Fishing Pond",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Reserved",
|
||||
"Forest Temple",
|
||||
"Goron Mines",
|
||||
"Lakebed Temple",
|
||||
"Arbiter's Grounds",
|
||||
"Snowpeak Ruins",
|
||||
"Temple of Time",
|
||||
"City in the Sky",
|
||||
"Palace of Twilight",
|
||||
"Hyrule Castle",
|
||||
"Caves",
|
||||
"Grottos",
|
||||
static const std::map<uint8_t, const char*> regionNames = {
|
||||
{ 0x00, "Ordon" },
|
||||
{ 0x01, "Hyrule Sewers" },
|
||||
{ 0x02, "Faron" },
|
||||
{ 0x03, "Eldin" },
|
||||
{ 0x04, "Lanayru" },
|
||||
{ 0x06, "Hyrule Field" },
|
||||
{ 0x07, "Sacred Grove" },
|
||||
{ 0x08, "Snowpeak" },
|
||||
{ 0x09, "Castle Town" },
|
||||
{ 0x0A, "Gerudo Desert" },
|
||||
{ 0x0B, "Fishing Pond" },
|
||||
{ 0x10, "Forest Temple" },
|
||||
{ 0x11, "Goron Mines" },
|
||||
{ 0x12, "Lakebed Temple" },
|
||||
{ 0x13, "Arbiter's Grounds" },
|
||||
{ 0x14, "Snowpeak Ruins" },
|
||||
{ 0x15, "Temple of Time" },
|
||||
{ 0x16, "City in the Sky" },
|
||||
{ 0x17, "Palace of Twilight" },
|
||||
{ 0x18, "Hyrule Castle" },
|
||||
{ 0x19, "Caves" },
|
||||
{ 0x1A, "Lake Hylia Long Cave"},
|
||||
{ 0x1B, "Grottos" }
|
||||
};
|
||||
|
||||
if (ImGui::BeginCombo("Region", regionNames[m_selectedRegion])) {
|
||||
for (int i = 0; i < regionNames.size(); i++) {
|
||||
if (strcmp(regionNames[i], "Reserved") == 0) continue;
|
||||
if (m_selectedRegion.name == nullptr)
|
||||
{
|
||||
const auto& firstRegion = *regionNames.find(0);
|
||||
m_selectedRegion = { firstRegion.first, firstRegion.second };
|
||||
}
|
||||
|
||||
if (ImGui::Selectable(regionNames[i])) {
|
||||
m_selectedRegion = i;
|
||||
if (ImGui::BeginCombo("Region", m_selectedRegion.name)) {
|
||||
for (const auto& [id, name] : regionNames) {
|
||||
if (ImGui::Selectable(name)) {
|
||||
m_selectedRegion = {id, name};
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
dSv_memBit_c* membit = &dComIfGs_getSaveData()->mSave[m_selectedRegion].mBit;
|
||||
if (membit != nullptr) {
|
||||
genMembitFlags("##SaveSceneFlags", *membit);
|
||||
dSv_memBit_c& membit = dComIfGs_getSaveData()->mSave[m_selectedRegion.id].mBit;
|
||||
|
||||
genAreaFlagTable(m_selectedRegion.id, membit);
|
||||
|
||||
if (ImGui::TreeNode("Flag Matrix")) {
|
||||
genMembitFlags("##SaveSceneFlags", membit);
|
||||
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
ImGui::TreePop();
|
||||
@@ -1530,7 +1789,9 @@ namespace dusk {
|
||||
}
|
||||
|
||||
for (const auto& e : duskImguiEventFlags) {
|
||||
if (!filter.PassFilter((e.location + "\n" + e.description + "\n" + e.flagName).c_str()))
|
||||
if (!filter.PassFilter(e.location.c_str()) &&
|
||||
!filter.PassFilter(e.description.c_str()) &&
|
||||
!filter.PassFilter(e.flagName.c_str()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,10 @@ namespace dusk {
|
||||
void drawConfigTab();
|
||||
|
||||
private:
|
||||
int m_selectedRegion = 0;
|
||||
struct {
|
||||
uint8_t id;
|
||||
const char* name;
|
||||
} m_selectedRegion = {0, nullptr};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,10 @@ ValidationError validate(const char* path) {
|
||||
NodHandleWrapper disc;
|
||||
|
||||
const auto sdlStream = SDL_IOFromFile(path, "rb");
|
||||
if (sdlStream == nullptr) {
|
||||
return ValidationError::IOError;
|
||||
}
|
||||
|
||||
const NodDiscStream nod_stream {
|
||||
.user_data = sdlStream,
|
||||
.read_at = StreamReadAt,
|
||||
|
||||
+48
-21
@@ -1,5 +1,6 @@
|
||||
#include "dusk/logging.h"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -32,9 +33,33 @@ static constexpr std::string_view StubFragments[] = {
|
||||
};
|
||||
|
||||
namespace {
|
||||
std::mutex g_logMutex;
|
||||
FILE* g_logFile = nullptr;
|
||||
std::string g_logFilePath;
|
||||
// On macOS, std::mutex becomes poisoned when its dtor is run.
|
||||
// We use this to check if the LogState is destroyed before attempting to acquire it.
|
||||
std::atomic g_logStateAlive(true);
|
||||
|
||||
struct LogState {
|
||||
std::mutex mutex;
|
||||
FILE* file = nullptr;
|
||||
std::string filePath;
|
||||
|
||||
~LogState() {
|
||||
CloseFile();
|
||||
g_logStateAlive.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
void CloseFile() {
|
||||
if (!g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard lock(mutex);
|
||||
if (file != nullptr) {
|
||||
std::fflush(file);
|
||||
std::fclose(file);
|
||||
file = nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
LogState g_logState;
|
||||
|
||||
const char* LogLevelString(AuroraLogLevel level) {
|
||||
switch (level) {
|
||||
@@ -152,10 +177,10 @@ void aurora_log_callback(AuroraLogLevel level, const char* module, const char* m
|
||||
FILE* out = LogStreamForLevel(level);
|
||||
WriteLogLine(out, levelStr, module, message, len);
|
||||
|
||||
{
|
||||
std::lock_guard lock(g_logMutex);
|
||||
if (g_logFile != nullptr) {
|
||||
WriteLogLine(g_logFile, levelStr, module, message, len);
|
||||
if (g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
std::lock_guard lock(g_logState.mutex);
|
||||
if (g_logState.file != nullptr) {
|
||||
WriteLogLine(g_logState.file, levelStr, module, message, len);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,8 +194,11 @@ void aurora_log_callback(AuroraLogLevel level, const char* module, const char* m
|
||||
aurora::Module DuskLog("dusk");
|
||||
|
||||
void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraLogLevel logLevel) {
|
||||
std::lock_guard lock(g_logMutex);
|
||||
if (g_logFile != nullptr || configDir.empty()) {
|
||||
if (!g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard lock(g_logState.mutex);
|
||||
if (g_logState.file != nullptr || configDir.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -184,31 +212,30 @@ void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraL
|
||||
}
|
||||
|
||||
const std::filesystem::path logPath = logsDir / MakeTimestampedLogName();
|
||||
g_logFile = std::fopen(logPath.string().c_str(), "wb");
|
||||
if (g_logFile == nullptr) {
|
||||
g_logState.file = std::fopen(logPath.string().c_str(), "wb");
|
||||
if (g_logState.file == nullptr) {
|
||||
std::fprintf(stderr, "[WARNING | dusk] Failed to open log file '%s'\n",
|
||||
logPath.string().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
g_logFilePath = logPath.string();
|
||||
g_logState.filePath = logPath.string();
|
||||
aurora::g_config.logCallback = &aurora_log_callback;
|
||||
aurora::g_config.logLevel = logLevel;
|
||||
WriteLogLine(g_logFile, "INFO", "dusk", "File logging initialized", 24);
|
||||
WriteLogLine(g_logState.file, "INFO", "dusk", "File logging initialized", 24);
|
||||
}
|
||||
|
||||
void dusk::ShutdownFileLogging() {
|
||||
std::lock_guard lock(g_logMutex);
|
||||
if (g_logFile == nullptr) {
|
||||
if (!g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::fflush(g_logFile);
|
||||
std::fclose(g_logFile);
|
||||
g_logFile = nullptr;
|
||||
g_logState.CloseFile();
|
||||
}
|
||||
|
||||
const char* dusk::GetLogFilePath() {
|
||||
std::lock_guard lock(g_logMutex);
|
||||
return g_logFilePath.empty() ? nullptr : g_logFilePath.c_str();
|
||||
if (!g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard lock(g_logState.mutex);
|
||||
return g_logState.filePath.empty() ? nullptr : g_logState.filePath.c_str();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ UserSettings g_userSettings = {
|
||||
.soundEffectsVolume {"audio.soundEffectsVolume", 100},
|
||||
.fanfareVolume {"audio.fanfareVolume", 100},
|
||||
.enableReverb {"audio.enableReverb", true},
|
||||
.enableHrtf {"audio.enableHrtf", false},
|
||||
},
|
||||
|
||||
.game = {
|
||||
@@ -31,7 +32,8 @@ UserSettings g_userSettings = {
|
||||
.disableRupeeCutscenes {"game.disableRupeeCutscenes", false},
|
||||
.noSwordRecoil {"game.noSwordRecoil", false},
|
||||
.damageMultiplier {"game.damageMultiplier", 1},
|
||||
.noHeartDrops{"game.noHeartDrops", false},
|
||||
.hyperEnemies {"game.hyperEnemies", false},
|
||||
.noHeartDrops {"game.noHeartDrops", false},
|
||||
.instantDeath {"game.instantDeath", false},
|
||||
.fastClimbing {"game.fastClimbing", false},
|
||||
.noMissClimbing {"game.noMissClimbing", false},
|
||||
@@ -40,6 +42,7 @@ UserSettings g_userSettings = {
|
||||
.instantSaves {"game.instantSaves", false},
|
||||
.instantText {"game.instantText", false},
|
||||
.sunsSong {"game.sunsSong", false},
|
||||
.autoSave {"game.autoSave", false},
|
||||
|
||||
// Preferences
|
||||
.enableMirrorMode {"game.enableMirrorMode", false},
|
||||
@@ -52,7 +55,7 @@ UserSettings g_userSettings = {
|
||||
.bloomMode {"game.bloomMode", BloomMode::Classic},
|
||||
.bloomMultiplier {"game.bloomMultiplier", 1.0f},
|
||||
.disableWaterRefraction {"game.disableWaterRefraction", false},
|
||||
.enableFrameInterpolation = {"game.enableFrameInterpolation", false},
|
||||
.enableFrameInterpolation {"game.enableFrameInterpolation", false},
|
||||
.internalResolutionScale {"game.internalResolutionScale", 0},
|
||||
.shadowResolutionMultiplier {"game.shadowResolutionMultiplier", 1},
|
||||
.enableDepthOfField {"game.enableDepthOfField", true},
|
||||
@@ -133,6 +136,7 @@ void registerSettings() {
|
||||
Register(g_userSettings.audio.soundEffectsVolume);
|
||||
Register(g_userSettings.audio.fanfareVolume);
|
||||
Register(g_userSettings.audio.enableReverb);
|
||||
Register(g_userSettings.audio.enableHrtf);
|
||||
|
||||
// Game
|
||||
Register(g_userSettings.game.language);
|
||||
@@ -144,6 +148,7 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.disableRupeeCutscenes);
|
||||
Register(g_userSettings.game.noSwordRecoil);
|
||||
Register(g_userSettings.game.damageMultiplier);
|
||||
Register(g_userSettings.game.hyperEnemies);
|
||||
Register(g_userSettings.game.noHeartDrops);
|
||||
Register(g_userSettings.game.instantDeath);
|
||||
Register(g_userSettings.game.fastClimbing);
|
||||
@@ -152,6 +157,7 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.instantSaves);
|
||||
Register(g_userSettings.game.instantText);
|
||||
Register(g_userSettings.game.sunsSong);
|
||||
Register(g_userSettings.game.autoSave);
|
||||
Register(g_userSettings.game.enableMirrorMode);
|
||||
Register(g_userSettings.game.invertCameraXAxis);
|
||||
Register(g_userSettings.game.invertCameraYAxis);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#include "bool_button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
BoolButton::BoolButton(Rml::Element* parent, Props props)
|
||||
: BaseControlledSelectButton(parent, {std::move(props.key)}),
|
||||
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)),
|
||||
mIsDisabled(std::move(props.isDisabled)) {}
|
||||
|
||||
bool BoolButton::disabled() const {
|
||||
if (mIsDisabled) {
|
||||
return mIsDisabled();
|
||||
}
|
||||
return BaseControlledSelectButton::disabled();
|
||||
}
|
||||
|
||||
Rml::String BoolButton::format_value() {
|
||||
return mGetValue() ? "On" : "Off";
|
||||
}
|
||||
|
||||
bool BoolButton::handle_nav_command(NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || cmd == NavCommand::Right) {
|
||||
mSetValue(!mGetValue());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
#include "select_button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class BoolButton : public BaseControlledSelectButton {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
std::function<bool()> getValue;
|
||||
std::function<void(bool)> setValue;
|
||||
std::function<bool()> isDisabled;
|
||||
};
|
||||
|
||||
BoolButton(Rml::Element* parent, Props props);
|
||||
|
||||
bool disabled() const override;
|
||||
|
||||
protected:
|
||||
Rml::String format_value() override;
|
||||
bool handle_nav_command(NavCommand cmd) override;
|
||||
|
||||
private:
|
||||
std::function<int()> mGetValue;
|
||||
std::function<void(int)> mSetValue;
|
||||
std::function<bool()> mIsDisabled;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,64 @@
|
||||
#include "button.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
Rml::Element* createRoot(Rml::Element* parent, const Rml::String& tagName) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto elem = doc->CreateElement(tagName);
|
||||
return parent->AppendChild(std::move(elem));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Button::Button(Rml::Element* parent, Props props, const Rml::String& tagName)
|
||||
: FluentComponent(createRoot(parent, tagName)) {
|
||||
update_props(std::move(props));
|
||||
}
|
||||
|
||||
void Button::set_text(const Rml::String& text) {
|
||||
if (mProps.text != text) {
|
||||
mRoot->SetInnerRML(escape(text));
|
||||
mProps.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
Button& Button::on_pressed(ButtonCallback callback) {
|
||||
if (!callback) {
|
||||
return *this;
|
||||
}
|
||||
// TODO: convert this to a FluentComponent method?
|
||||
on_nav_command([callback = std::move(callback)](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
callback();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Button::update_props(Props props) {
|
||||
set_text(props.text);
|
||||
mProps = std::move(props);
|
||||
}
|
||||
|
||||
void ControlledButton::update() {
|
||||
if (mIsSelected) {
|
||||
set_selected(mIsSelected());
|
||||
}
|
||||
Button::update();
|
||||
}
|
||||
|
||||
bool ControlledButton::selected() const {
|
||||
if (mIsSelected) {
|
||||
return mIsSelected();
|
||||
}
|
||||
return Button::selected();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "component.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
using ButtonCallback = std::function<void()>;
|
||||
|
||||
class Button : public FluentComponent<Button> {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String text;
|
||||
};
|
||||
|
||||
Button(Rml::Element* parent, Props props, const Rml::String& tagName = "button");
|
||||
Button(Rml::Element* parent, Rml::String text, const Rml::String& tagName = "button")
|
||||
: Button(parent, Props{std::move(text)}, tagName) {}
|
||||
|
||||
void set_text(const Rml::String& text);
|
||||
Button& on_pressed(ButtonCallback callback);
|
||||
|
||||
const Rml::String& get_text() const { return mProps.text; }
|
||||
|
||||
private:
|
||||
void update_props(Props props);
|
||||
|
||||
Props mProps;
|
||||
};
|
||||
|
||||
class ControlledButton : public Button {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String text;
|
||||
std::function<bool()> isSelected;
|
||||
};
|
||||
|
||||
ControlledButton(Rml::Element* parent, Props props, const Rml::String& tagName = "button")
|
||||
: Button(parent, {std::move(props.text)}, tagName),
|
||||
mIsSelected(std::move(props.isSelected)) {}
|
||||
|
||||
void update() override;
|
||||
bool selected() const override;
|
||||
|
||||
private:
|
||||
std::function<bool()> mIsSelected;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "component.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
Component::Component(Rml::Element* root) : mRoot(root) {}
|
||||
|
||||
Component::~Component() = default;
|
||||
|
||||
void Component::update() {
|
||||
for (const auto& child : mChildren) {
|
||||
child->update();
|
||||
}
|
||||
}
|
||||
|
||||
bool Component::focus() {
|
||||
if (disabled()) {
|
||||
return false;
|
||||
}
|
||||
// Can we focus self?
|
||||
if (mRoot->Focus(true)) {
|
||||
mRoot->ScrollIntoView(Rml::ScrollIntoViewOptions{
|
||||
Rml::ScrollAlignment::Center,
|
||||
Rml::ScrollAlignment::Center,
|
||||
Rml::ScrollBehavior::Smooth,
|
||||
Rml::ScrollParentage::Closest,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
// Otherwise, try to focus a child
|
||||
for (const auto& child : mChildren) {
|
||||
if (child->focus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Component::set_selected(bool value) {
|
||||
// Subclasses may override selected() to return a dynamic value, but
|
||||
// we're only interested in if the pseudoclass is set or not, so we
|
||||
// use Component::selected() directly rather than selected().
|
||||
if (Component::selected() == value) {
|
||||
return;
|
||||
}
|
||||
mRoot->SetPseudoClass("selected", value);
|
||||
}
|
||||
|
||||
void Component::set_disabled(bool value) {
|
||||
if (Component::disabled() == value) {
|
||||
return;
|
||||
}
|
||||
if (value) {
|
||||
mRoot->SetAttribute("disabled", "");
|
||||
mRoot->SetPseudoClass("disabled", true);
|
||||
mRoot->Blur();
|
||||
} else {
|
||||
mRoot->RemoveAttribute("disabled");
|
||||
mRoot->SetPseudoClass("disabled", false);
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Element* Component::append(Rml::Element* parent, const Rml::String& tag) {
|
||||
if (parent == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
if (doc == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return parent->AppendChild(doc->CreateElement(tag));
|
||||
}
|
||||
void Component::listen(Rml::Element* element, Rml::EventId event,
|
||||
ScopedEventListener::Callback callback, bool capture) {
|
||||
if (element == nullptr) {
|
||||
element = mRoot;
|
||||
}
|
||||
mListeners.emplace_back(
|
||||
std::make_unique<ScopedEventListener>(element, event, std::move(callback), capture));
|
||||
}
|
||||
|
||||
bool Component::contains(Rml::Element* element) const {
|
||||
for (const auto* node = element; node != nullptr; node = node->GetParentNode()) {
|
||||
if (node == mRoot) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Component::clear_children() {
|
||||
mChildren.clear();
|
||||
while (mRoot->GetNumChildren() > 0) {
|
||||
mRoot->RemoveChild(mRoot->GetFirstChild());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include "event.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Rml {
|
||||
class Element;
|
||||
}
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Component {
|
||||
public:
|
||||
Component() = default;
|
||||
explicit Component(Rml::Element* root);
|
||||
virtual ~Component();
|
||||
|
||||
Component(const Component&) = delete;
|
||||
Component& operator=(const Component&) = delete;
|
||||
|
||||
virtual void update();
|
||||
virtual bool focus();
|
||||
|
||||
virtual bool selected() const { return mRoot->IsPseudoClassSet("selected"); }
|
||||
virtual void set_selected(bool selected);
|
||||
virtual bool disabled() const { return mRoot->IsPseudoClassSet("disabled"); }
|
||||
virtual void set_disabled(bool disabled);
|
||||
|
||||
void listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback,
|
||||
bool capture = false);
|
||||
bool contains(Rml::Element* element) const;
|
||||
|
||||
template <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T> T& add_child(Args&&... args) {
|
||||
auto child = std::make_unique<T>(mRoot, std::forward<Args>(args)...);
|
||||
T& ref = *child;
|
||||
mChildren.emplace_back(std::move(child));
|
||||
return ref;
|
||||
}
|
||||
|
||||
Rml::Element* root() const { return mRoot; }
|
||||
|
||||
protected:
|
||||
static Rml::Element* append(Rml::Element* parent, const Rml::String& tag);
|
||||
void clear_children();
|
||||
|
||||
Rml::Element* mRoot = nullptr;
|
||||
std::vector<std::unique_ptr<Component> > mChildren;
|
||||
std::vector<std::unique_ptr<ScopedEventListener> > mListeners;
|
||||
};
|
||||
|
||||
template <class Derived>
|
||||
class FluentComponent : public Component {
|
||||
public:
|
||||
using Component::Component;
|
||||
|
||||
Derived& listen(
|
||||
Rml::EventId event, ScopedEventListener::Callback callback, bool capture = false) {
|
||||
Component::listen(mRoot, event, std::move(callback), capture);
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
|
||||
Derived& on_focus(ScopedEventListener::Callback callback) {
|
||||
return listen(
|
||||
Rml::EventId::Focus, [this, callback = std::move(callback)](Rml::Event& event) {
|
||||
if (!disabled()) {
|
||||
callback(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Derived& on_nav_command(std::function<bool(Rml::Event&, NavCommand)> callback) {
|
||||
listen(Rml::EventId::Click, [this, callback](Rml::Event& event) {
|
||||
if (!disabled() && callback(event, NavCommand::Confirm)) {
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
listen(Rml::EventId::Keydown, [this, callback = std::move(callback)](Rml::Event& event) {
|
||||
if (disabled()) {
|
||||
return;
|
||||
}
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd != NavCommand::None && callback(event, cmd)) {
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
return static_cast<Derived&>(*this);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,109 @@
|
||||
#include "document.hpp"
|
||||
|
||||
#include "aurora/rmlui.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
Rml::ElementDocument* load_document(const Rml::String& source) {
|
||||
auto* context = aurora::rmlui::get_context();
|
||||
if (context == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return context->LoadDocumentFromMemory(source);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Document::Document(const Rml::String& source) : mDocument(load_document(source)) {
|
||||
// Block events while hidden (except for Menu command)
|
||||
listen(
|
||||
Rml::EventId::Keydown,
|
||||
[this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd != NavCommand::Menu && !visible()) {
|
||||
event.StopImmediatePropagation();
|
||||
}
|
||||
},
|
||||
true);
|
||||
const auto blockUnlessVisible = [this](Rml::Event& event) {
|
||||
if (!visible()) {
|
||||
event.StopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
listen(Rml::EventId::Mouseover, blockUnlessVisible, true);
|
||||
listen(Rml::EventId::Click, blockUnlessVisible, true);
|
||||
listen(Rml::EventId::Scroll, blockUnlessVisible, true);
|
||||
|
||||
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd != NavCommand::None && handle_nav_command(event, cmd)) {
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Document::~Document() {
|
||||
mListeners.clear();
|
||||
if (mDocument != nullptr) {
|
||||
mDocument->Close();
|
||||
mDocument = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Document::show() {
|
||||
if (mDocument != nullptr) {
|
||||
// Attempt to preserve the previously focused element
|
||||
mDocument->Show(Rml::ModalFlag::None, Rml::FocusFlag::Keep, Rml::ScrollFlag::None);
|
||||
// If nothing is focused, let the document decide the initial focus
|
||||
auto* leaf = mDocument->GetFocusLeafNode();
|
||||
if (leaf == nullptr || leaf == mDocument) {
|
||||
focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Document::hide(bool close) {
|
||||
if (mDocument != nullptr) {
|
||||
mDocument->Hide();
|
||||
}
|
||||
if (close) {
|
||||
mClosed = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Document::update() {}
|
||||
|
||||
bool Document::focus() {
|
||||
return false;
|
||||
}
|
||||
|
||||
void Document::listen(Rml::Element* element, Rml::EventId event,
|
||||
ScopedEventListener::Callback callback, bool capture) {
|
||||
if (element == nullptr) {
|
||||
element = mDocument;
|
||||
}
|
||||
if (element == nullptr || !callback) {
|
||||
return;
|
||||
}
|
||||
mListeners.emplace_back(
|
||||
std::make_unique<ScopedEventListener>(element, event, std::move(callback), capture));
|
||||
}
|
||||
|
||||
bool Document::visible() const {
|
||||
if (mDocument == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return *mDocument->GetProperty(Rml::PropertyId::Visibility) == Rml::Style::Visibility::Visible;
|
||||
}
|
||||
|
||||
bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Menu) {
|
||||
toggle();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "component.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Document {
|
||||
public:
|
||||
Document(const Rml::String& source);
|
||||
virtual ~Document();
|
||||
|
||||
Document(const Document&) = delete;
|
||||
Document& operator=(const Document&) = delete;
|
||||
|
||||
virtual void show();
|
||||
virtual void hide(bool close);
|
||||
virtual void update();
|
||||
virtual bool focus();
|
||||
virtual bool visible() const;
|
||||
|
||||
void listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback,
|
||||
bool capture = false);
|
||||
void listen(Rml::EventId event, ScopedEventListener::Callback callback, bool capture = false) {
|
||||
listen(mDocument, event, std::move(callback), capture);
|
||||
}
|
||||
void toggle() {
|
||||
if (visible()) {
|
||||
hide(false);
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
}
|
||||
void pop() {
|
||||
hide(true);
|
||||
show_top_document();
|
||||
}
|
||||
|
||||
bool pending_close() const { return mPendingClose; }
|
||||
bool closed() const { return mClosed; }
|
||||
|
||||
protected:
|
||||
virtual bool handle_nav_command(Rml::Event& event, NavCommand cmd);
|
||||
|
||||
Rml::ElementDocument* mDocument;
|
||||
std::vector<std::unique_ptr<ScopedEventListener> > mListeners;
|
||||
bool mPendingClose = false;
|
||||
bool mClosed = false;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include "window.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class EditorWindow : public Window {
|
||||
public:
|
||||
EditorWindow();
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "event.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
ScopedEventListener::ScopedEventListener(
|
||||
Rml::Element* element, Rml::EventId event, Callback callback, bool capture)
|
||||
: mElement(element), mEvent(event), mCapture(capture), mCallback(std::move(callback)) {
|
||||
mElement->AddEventListener(mEvent, this, mCapture);
|
||||
}
|
||||
|
||||
ScopedEventListener::~ScopedEventListener() {
|
||||
if (mElement != nullptr) {
|
||||
mElement->RemoveEventListener(mEvent, this, mCapture);
|
||||
mElement = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ScopedEventListener::ProcessEvent(Rml::Event& event) {
|
||||
if (mCallback) {
|
||||
mCallback(event);
|
||||
}
|
||||
}
|
||||
|
||||
void ScopedEventListener::OnDetach(Rml::Element* element) {
|
||||
if (element == mElement) {
|
||||
mElement = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class ScopedEventListener final : public Rml::EventListener {
|
||||
public:
|
||||
using Callback = std::function<void(Rml::Event&)>;
|
||||
|
||||
ScopedEventListener(
|
||||
Rml::Element* element, Rml::EventId event, Callback callback, bool capture = false);
|
||||
~ScopedEventListener() override;
|
||||
|
||||
ScopedEventListener(const ScopedEventListener&) = delete;
|
||||
ScopedEventListener& operator=(const ScopedEventListener&) = delete;
|
||||
ScopedEventListener(ScopedEventListener&&) = delete;
|
||||
ScopedEventListener& operator=(ScopedEventListener&&) = delete;
|
||||
|
||||
void ProcessEvent(Rml::Event& event) override;
|
||||
void OnDetach(Rml::Element* element) override;
|
||||
|
||||
private:
|
||||
Rml::Element* mElement = nullptr;
|
||||
Rml::EventId mEvent = Rml::EventId::Invalid;
|
||||
bool mCapture = false;
|
||||
Callback mCallback;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,610 @@
|
||||
#include "input.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <SDL3/SDL_timer.h>
|
||||
#include <aurora/rmlui.hpp>
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
constexpr double kGamepadRepeatInitialDelay = 0.32;
|
||||
constexpr double kGamepadRepeatStartInterval = 0.12;
|
||||
constexpr double kGamepadRepeatMinInterval = 0.045;
|
||||
constexpr double kGamepadRepeatRampDuration = 1.0;
|
||||
constexpr double kGamepadMenuChordGraceDuration = 0.12;
|
||||
constexpr Sint16 kGamepadAxisPressThreshold = 16384;
|
||||
constexpr Sint16 kGamepadAxisReleaseThreshold = 12000;
|
||||
constexpr int kGamepadAxisDirectionCount = SDL_GAMEPAD_AXIS_COUNT * 2;
|
||||
|
||||
struct GamepadRepeatState {
|
||||
Rml::Input::KeyIdentifier key = Rml::Input::KI_UNKNOWN;
|
||||
double pressedAt = 0.0;
|
||||
double nextRepeatAt = 0.0;
|
||||
bool held = false;
|
||||
bool repeatable = false;
|
||||
bool pending = false;
|
||||
};
|
||||
|
||||
bool sPadInputBlocked = false;
|
||||
std::array<GamepadRepeatState, SDL_GAMEPAD_BUTTON_COUNT> sGamepadButtonRepeats;
|
||||
std::array<GamepadRepeatState, kGamepadAxisDirectionCount> sGamepadAxisRepeats;
|
||||
std::array<u32, PAD_MAX_CONTROLLERS> sPadHoldMasks;
|
||||
std::array<bool, PAD_MAX_CONTROLLERS> sMenuChordConsumed;
|
||||
|
||||
double now_seconds() noexcept {
|
||||
return static_cast<double>(SDL_GetTicksNS()) / 1000000000.0;
|
||||
}
|
||||
|
||||
bool is_menu_chord_part(PADButton button) noexcept {
|
||||
return button == PAD_TRIGGER_R || button == PAD_BUTTON_START;
|
||||
}
|
||||
|
||||
bool has_menu_chord_part_held(u32 port) noexcept {
|
||||
if (port >= sPadHoldMasks.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const u32 held = sPadHoldMasks[port];
|
||||
return (held & (PAD_TRIGGER_R | PAD_BUTTON_START)) != 0;
|
||||
}
|
||||
|
||||
bool should_block_pad_for_menu_chord() noexcept {
|
||||
for (u32 port = 0; port < sPadHoldMasks.size(); ++port) {
|
||||
if (sMenuChordConsumed[port] && has_menu_chord_part_held(port)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
PADButton pad_button_from_axis(PADAxis axis) noexcept {
|
||||
switch (axis) {
|
||||
case PAD_AXIS_TRIGGER_R:
|
||||
return PAD_TRIGGER_R;
|
||||
case PAD_AXIS_TRIGGER_L:
|
||||
return PAD_TRIGGER_L;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void set_pad_button_held(u32 port, PADButton button, bool held) noexcept {
|
||||
if (port >= sPadHoldMasks.size() || button == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (held) {
|
||||
sPadHoldMasks[port] |= button;
|
||||
} else {
|
||||
sPadHoldMasks[port] &= ~button;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_menu_chord(u32 port) noexcept {
|
||||
if (port >= sPadHoldMasks.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const u32 held = sPadHoldMasks[port];
|
||||
return (held & PAD_TRIGGER_R) != 0 && (held & PAD_BUTTON_START) != 0;
|
||||
}
|
||||
|
||||
bool any_menu_chord() noexcept {
|
||||
return std::any_of(sPadHoldMasks.begin(), sPadHoldMasks.end(),
|
||||
[](u32 held) { return (held & PAD_TRIGGER_R) != 0 && (held & PAD_BUTTON_START) != 0; });
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_pad_button(PADButton button) noexcept {
|
||||
switch (button) {
|
||||
case PAD_BUTTON_UP:
|
||||
return Rml::Input::KI_UP;
|
||||
case PAD_BUTTON_DOWN:
|
||||
return Rml::Input::KI_DOWN;
|
||||
case PAD_BUTTON_LEFT:
|
||||
return Rml::Input::KI_LEFT;
|
||||
case PAD_BUTTON_RIGHT:
|
||||
return Rml::Input::KI_RIGHT;
|
||||
case PAD_BUTTON_B:
|
||||
return Rml::Input::KI_ESCAPE;
|
||||
case PAD_BUTTON_A:
|
||||
return Rml::Input::KI_RETURN;
|
||||
case PAD_TRIGGER_R:
|
||||
return Rml::Input::KI_NEXT;
|
||||
case PAD_TRIGGER_L:
|
||||
return Rml::Input::KI_PRIOR;
|
||||
default:
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_pad_axis(PADAxis axis) noexcept {
|
||||
switch (axis) {
|
||||
case PAD_AXIS_LEFT_X_POS:
|
||||
return Rml::Input::KI_RIGHT;
|
||||
case PAD_AXIS_LEFT_X_NEG:
|
||||
return Rml::Input::KI_LEFT;
|
||||
case PAD_AXIS_LEFT_Y_POS:
|
||||
return Rml::Input::KI_UP;
|
||||
case PAD_AXIS_LEFT_Y_NEG:
|
||||
return Rml::Input::KI_DOWN;
|
||||
case PAD_AXIS_TRIGGER_R:
|
||||
return Rml::Input::KI_NEXT;
|
||||
case PAD_AXIS_TRIGGER_L:
|
||||
return Rml::Input::KI_PRIOR;
|
||||
default:
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_raw_gamepad_button(SDL_GamepadButton button) noexcept {
|
||||
switch (button) {
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_UP:
|
||||
return Rml::Input::KI_UP;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
|
||||
return Rml::Input::KI_DOWN;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
|
||||
return Rml::Input::KI_LEFT;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
|
||||
return Rml::Input::KI_RIGHT;
|
||||
case SDL_GAMEPAD_BUTTON_EAST:
|
||||
return Rml::Input::KI_ESCAPE;
|
||||
case SDL_GAMEPAD_BUTTON_SOUTH:
|
||||
return Rml::Input::KI_RETURN;
|
||||
case SDL_GAMEPAD_BUTTON_BACK:
|
||||
return Rml::Input::KI_F1;
|
||||
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
|
||||
return Rml::Input::KI_NEXT;
|
||||
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
|
||||
return Rml::Input::KI_PRIOR;
|
||||
default:
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_raw_button_alias(SDL_GamepadButton button) noexcept {
|
||||
switch (button) {
|
||||
case SDL_GAMEPAD_BUTTON_BACK:
|
||||
return Rml::Input::KI_F1;
|
||||
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
|
||||
return Rml::Input::KI_NEXT;
|
||||
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
|
||||
return Rml::Input::KI_PRIOR;
|
||||
default:
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_raw_gamepad_axis(SDL_GamepadAxis axis, PADAxisSign sign) noexcept {
|
||||
switch (axis) {
|
||||
case SDL_GAMEPAD_AXIS_LEFTX:
|
||||
return sign == AXIS_SIGN_POSITIVE ? Rml::Input::KI_RIGHT : Rml::Input::KI_LEFT;
|
||||
case SDL_GAMEPAD_AXIS_LEFTY:
|
||||
return sign == AXIS_SIGN_NEGATIVE ? Rml::Input::KI_UP : Rml::Input::KI_DOWN;
|
||||
case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
|
||||
return sign == AXIS_SIGN_POSITIVE ? Rml::Input::KI_NEXT : Rml::Input::KI_UNKNOWN;
|
||||
case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
|
||||
return sign == AXIS_SIGN_POSITIVE ? Rml::Input::KI_PRIOR : Rml::Input::KI_UNKNOWN;
|
||||
default:
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
bool find_event_port(SDL_JoystickID instance, u32& port) noexcept {
|
||||
for (u32 candidate = 0; candidate < PAD_MAX_CONTROLLERS; ++candidate) {
|
||||
const s32 index = PADGetIndexForPort(candidate);
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SDL_Gamepad* gamepad = PADGetSDLGamepadForIndex(static_cast<u32>(index));
|
||||
if (gamepad == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad);
|
||||
if (joystick != nullptr && SDL_GetJoystickID(joystick) == instance) {
|
||||
port = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool find_mapped_pad_button(u32 port, SDL_GamepadButton nativeButton, PADButton& button) noexcept {
|
||||
u32 buttonCount = 0;
|
||||
PADButtonMapping* buttons = PADGetButtonMappings(port, &buttonCount);
|
||||
if (buttons != nullptr) {
|
||||
for (u32 i = 0; i < buttonCount; ++i) {
|
||||
if (buttons[i].nativeButton == static_cast<u32>(nativeButton)) {
|
||||
button = buttons[i].padButton;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
u32 axisCount = 0;
|
||||
PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount);
|
||||
if (axes != nullptr) {
|
||||
for (u32 i = 0; i < axisCount; ++i) {
|
||||
if (axes[i].nativeButton == nativeButton) {
|
||||
button = pad_button_from_axis(axes[i].padAxis);
|
||||
return button != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool find_mapped_pad_axis(
|
||||
u32 port, SDL_GamepadAxis nativeAxis, PADAxisSign sign, PADAxis& axis) noexcept {
|
||||
u32 buttonCount = 0;
|
||||
PADGetButtonMappings(port, &buttonCount);
|
||||
|
||||
u32 axisCount = 0;
|
||||
PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount);
|
||||
if (axes == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < axisCount; ++i) {
|
||||
const PADSignedNativeAxis mappedAxis = axes[i].nativeAxis;
|
||||
if (mappedAxis.nativeAxis == nativeAxis && mappedAxis.sign == sign) {
|
||||
axis = axes[i].padAxis;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool find_event_pad_button(
|
||||
const SDL_GamepadButtonEvent& event, u32& port, PADButton& button) noexcept {
|
||||
return find_event_port(event.which, port) &&
|
||||
find_mapped_pad_button(port, static_cast<SDL_GamepadButton>(event.button), button);
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_gamepad_button(const SDL_GamepadButtonEvent& event) noexcept {
|
||||
const auto nativeButton = static_cast<SDL_GamepadButton>(event.button);
|
||||
if (nativeButton == SDL_GAMEPAD_BUTTON_BACK) {
|
||||
return Rml::Input::KI_F1;
|
||||
}
|
||||
|
||||
u32 port = 0;
|
||||
if (!find_event_port(event.which, port)) {
|
||||
return map_raw_gamepad_button(nativeButton);
|
||||
}
|
||||
|
||||
PADButton button = 0;
|
||||
if (find_mapped_pad_button(port, nativeButton, button)) {
|
||||
const auto key = map_pad_button(button);
|
||||
return key == Rml::Input::KI_UNKNOWN ? map_raw_button_alias(nativeButton) : key;
|
||||
}
|
||||
|
||||
return map_raw_button_alias(nativeButton);
|
||||
}
|
||||
|
||||
Rml::Input::KeyIdentifier map_gamepad_axis(
|
||||
const SDL_GamepadAxisEvent& event, PADAxisSign sign) noexcept {
|
||||
u32 port = 0;
|
||||
if (!find_event_port(event.which, port)) {
|
||||
return map_raw_gamepad_axis(static_cast<SDL_GamepadAxis>(event.axis), sign);
|
||||
}
|
||||
|
||||
PADAxis axis = 0;
|
||||
if (find_mapped_pad_axis(port, static_cast<SDL_GamepadAxis>(event.axis), sign, axis)) {
|
||||
return map_pad_axis(axis);
|
||||
}
|
||||
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
|
||||
bool is_repeatable_key(Rml::Input::KeyIdentifier key) noexcept {
|
||||
switch (key) {
|
||||
case Rml::Input::KI_UP:
|
||||
case Rml::Input::KI_DOWN:
|
||||
case Rml::Input::KI_LEFT:
|
||||
case Rml::Input::KI_RIGHT:
|
||||
case Rml::Input::KI_NEXT:
|
||||
case Rml::Input::KI_PRIOR:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
double repeat_interval(double heldFor) noexcept {
|
||||
const double ramp = std::clamp(heldFor / kGamepadRepeatRampDuration, 0.0, 1.0);
|
||||
return kGamepadRepeatStartInterval +
|
||||
(kGamepadRepeatMinInterval - kGamepadRepeatStartInterval) * ramp;
|
||||
}
|
||||
|
||||
GamepadRepeatState* button_repeat_state(SDL_GamepadButton button) noexcept {
|
||||
const auto index = static_cast<int>(button);
|
||||
if (index < 0 || index >= static_cast<int>(sGamepadButtonRepeats.size())) {
|
||||
return nullptr;
|
||||
}
|
||||
return &sGamepadButtonRepeats[index];
|
||||
}
|
||||
|
||||
GamepadRepeatState* axis_repeat_state(SDL_GamepadAxis axis, PADAxisSign sign) noexcept {
|
||||
const auto axisIndex = static_cast<int>(axis);
|
||||
if (axisIndex < 0 || axisIndex >= SDL_GAMEPAD_AXIS_COUNT) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const int directionOffset = sign == AXIS_SIGN_POSITIVE ? 0 : 1;
|
||||
return &sGamepadAxisRepeats[axisIndex * 2 + directionOffset];
|
||||
}
|
||||
|
||||
void clear_gamepad_repeats() noexcept {
|
||||
for (auto& repeat : sGamepadButtonRepeats) {
|
||||
repeat = {};
|
||||
}
|
||||
for (auto& repeat : sGamepadAxisRepeats) {
|
||||
repeat = {};
|
||||
}
|
||||
sPadHoldMasks.fill(0);
|
||||
sMenuChordConsumed.fill(false);
|
||||
}
|
||||
|
||||
void begin_gamepad_key(GamepadRepeatState& repeat, Rml::Input::KeyIdentifier key) noexcept {
|
||||
if (repeat.held) {
|
||||
return;
|
||||
}
|
||||
|
||||
const double now = now_seconds();
|
||||
repeat.key = key;
|
||||
repeat.pressedAt = now;
|
||||
repeat.held = true;
|
||||
repeat.repeatable = is_repeatable_key(key);
|
||||
repeat.nextRepeatAt = repeat.repeatable ? now + kGamepadRepeatInitialDelay : 0.0;
|
||||
repeat.pending = false;
|
||||
}
|
||||
|
||||
void begin_pending_gamepad_key(GamepadRepeatState& repeat, Rml::Input::KeyIdentifier key) noexcept {
|
||||
if (repeat.held) {
|
||||
return;
|
||||
}
|
||||
|
||||
const double now = now_seconds();
|
||||
repeat.key = key;
|
||||
repeat.pressedAt = now;
|
||||
repeat.held = true;
|
||||
repeat.repeatable = is_repeatable_key(key);
|
||||
repeat.nextRepeatAt = 0.0;
|
||||
repeat.pending = true;
|
||||
}
|
||||
|
||||
void consume_menu_chord(u32 port, Rml::Context& context) noexcept {
|
||||
if (port < sMenuChordConsumed.size()) {
|
||||
sMenuChordConsumed[port] = true;
|
||||
}
|
||||
|
||||
auto cancel_next = [&context](GamepadRepeatState& repeat) {
|
||||
if (!repeat.held || repeat.key != Rml::Input::KI_NEXT) {
|
||||
return;
|
||||
}
|
||||
if (!repeat.pending) {
|
||||
context.ProcessKeyUp(repeat.key, 0);
|
||||
}
|
||||
repeat = {};
|
||||
};
|
||||
|
||||
for (auto& repeat : sGamepadButtonRepeats) {
|
||||
cancel_next(repeat);
|
||||
}
|
||||
for (auto& repeat : sGamepadAxisRepeats) {
|
||||
cancel_next(repeat);
|
||||
}
|
||||
}
|
||||
|
||||
void update_menu_chord_release(u32 port) noexcept {
|
||||
if (port >= sMenuChordConsumed.size() || has_menu_chord_part_held(port)) {
|
||||
return;
|
||||
}
|
||||
|
||||
sMenuChordConsumed[port] = false;
|
||||
}
|
||||
|
||||
bool should_defer_menu_chord_part(PADButton button, Rml::Input::KeyIdentifier key) noexcept {
|
||||
return button == PAD_TRIGGER_R && key == Rml::Input::KI_NEXT;
|
||||
}
|
||||
|
||||
void process_axis_direction(
|
||||
Rml::Context& context, const SDL_GamepadAxisEvent& event, PADAxisSign sign) noexcept {
|
||||
GamepadRepeatState* repeat = axis_repeat_state(static_cast<SDL_GamepadAxis>(event.axis), sign);
|
||||
if (repeat == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool active = sign == AXIS_SIGN_POSITIVE ? event.value >= kGamepadAxisPressThreshold :
|
||||
event.value <= -kGamepadAxisPressThreshold;
|
||||
const bool released = sign == AXIS_SIGN_POSITIVE ? event.value <= kGamepadAxisReleaseThreshold :
|
||||
event.value >= -kGamepadAxisReleaseThreshold;
|
||||
|
||||
u32 port = 0;
|
||||
PADAxis padAxis = 0;
|
||||
const bool hasPadAxis =
|
||||
find_event_port(event.which, port) &&
|
||||
find_mapped_pad_axis(port, static_cast<SDL_GamepadAxis>(event.axis), sign, padAxis);
|
||||
const PADButton heldPadButton = hasPadAxis ? pad_button_from_axis(padAxis) : 0;
|
||||
|
||||
if (repeat->held) {
|
||||
if (released) {
|
||||
if (!repeat->pending) {
|
||||
context.ProcessKeyUp(repeat->key, 0);
|
||||
}
|
||||
set_pad_button_held(port, heldPadButton, false);
|
||||
*repeat = {};
|
||||
update_menu_chord_release(port);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
set_pad_button_held(port, heldPadButton, true);
|
||||
const bool chorded = heldPadButton == PAD_TRIGGER_R && is_menu_chord(port);
|
||||
if (chorded) {
|
||||
consume_menu_chord(port, context);
|
||||
}
|
||||
const auto key = chorded ? Rml::Input::KI_F1 : map_gamepad_axis(event, sign);
|
||||
if (key == Rml::Input::KI_UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chorded && should_defer_menu_chord_part(heldPadButton, key)) {
|
||||
begin_pending_gamepad_key(*repeat, key);
|
||||
return;
|
||||
}
|
||||
|
||||
begin_gamepad_key(*repeat, key);
|
||||
context.ProcessMouseLeave();
|
||||
context.ProcessKeyDown(key, 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void sync_input_block() noexcept {
|
||||
const bool shouldBlock = any_document_visible() || should_block_pad_for_menu_chord();
|
||||
if (sPadInputBlocked == shouldBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
PADBlockInput(shouldBlock);
|
||||
sPadInputBlocked = shouldBlock;
|
||||
}
|
||||
|
||||
void release_input_block() noexcept {
|
||||
if (!sPadInputBlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
PADBlockInput(false);
|
||||
sPadInputBlocked = false;
|
||||
}
|
||||
|
||||
void reset_input_state() noexcept {
|
||||
clear_gamepad_repeats();
|
||||
}
|
||||
|
||||
void handle_event(const SDL_Event& event) noexcept {
|
||||
if (event.type == SDL_EVENT_GAMEPAD_REMOVED || event.type == SDL_EVENT_WINDOW_FOCUS_LOST) {
|
||||
reset_input_state();
|
||||
sync_input_block();
|
||||
return;
|
||||
}
|
||||
if (event.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN && event.type != SDL_EVENT_GAMEPAD_BUTTON_UP &&
|
||||
event.type != SDL_EVENT_GAMEPAD_AXIS_MOTION)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto* context = aurora::rmlui::get_context();
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
|
||||
process_axis_direction(*context, event.gaxis, AXIS_SIGN_POSITIVE);
|
||||
process_axis_direction(*context, event.gaxis, AXIS_SIGN_NEGATIVE);
|
||||
sync_input_block();
|
||||
return;
|
||||
}
|
||||
|
||||
auto* repeat = button_repeat_state(static_cast<SDL_GamepadButton>(event.gbutton.button));
|
||||
u32 port = 0;
|
||||
PADButton button = 0;
|
||||
const bool hasPadButton = find_event_pad_button(event.gbutton, port, button);
|
||||
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
|
||||
set_pad_button_held(port, button, true);
|
||||
const bool chorded = hasPadButton && is_menu_chord_part(button) && is_menu_chord(port);
|
||||
if (chorded) {
|
||||
consume_menu_chord(port, *context);
|
||||
}
|
||||
const auto key = chorded ? Rml::Input::KI_F1 : map_gamepad_button(event.gbutton);
|
||||
if (key != Rml::Input::KI_UNKNOWN) {
|
||||
bool deferred = false;
|
||||
if (repeat != nullptr) {
|
||||
if (!chorded && should_defer_menu_chord_part(button, key)) {
|
||||
begin_pending_gamepad_key(*repeat, key);
|
||||
deferred = true;
|
||||
} else {
|
||||
begin_gamepad_key(*repeat, key);
|
||||
}
|
||||
}
|
||||
if (!deferred) {
|
||||
context->ProcessMouseLeave();
|
||||
context->ProcessKeyDown(key, 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const auto key = repeat != nullptr && repeat->held ? repeat->key : Rml::Input::KI_UNKNOWN;
|
||||
const bool wasPending = repeat != nullptr && repeat->pending;
|
||||
set_pad_button_held(port, button, false);
|
||||
update_menu_chord_release(port);
|
||||
if (key != Rml::Input::KI_UNKNOWN) {
|
||||
if (repeat != nullptr) {
|
||||
*repeat = {};
|
||||
}
|
||||
if (!wasPending) {
|
||||
context->ProcessKeyUp(key, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
sync_input_block();
|
||||
}
|
||||
|
||||
void update_input() noexcept {
|
||||
auto* context = aurora::rmlui::get_context();
|
||||
if (context != nullptr) {
|
||||
const double now = now_seconds();
|
||||
auto process_repeats = [context, now](auto& repeats) {
|
||||
for (auto& repeat : repeats) {
|
||||
if (!repeat.held) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (repeat.pending) {
|
||||
if (now < repeat.pressedAt + kGamepadMenuChordGraceDuration) {
|
||||
continue;
|
||||
}
|
||||
|
||||
repeat.pending = false;
|
||||
repeat.pressedAt = now;
|
||||
repeat.nextRepeatAt =
|
||||
repeat.repeatable ? now + kGamepadRepeatInitialDelay : 0.0;
|
||||
context->ProcessMouseLeave();
|
||||
context->ProcessKeyDown(repeat.key, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!repeat.repeatable || now < repeat.nextRepeatAt ||
|
||||
(repeat.key == Rml::Input::KI_NEXT && any_menu_chord()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
context->ProcessKeyDown(repeat.key, 0);
|
||||
const double heldFor = now - repeat.pressedAt;
|
||||
repeat.nextRepeatAt = now + repeat_interval(heldFor);
|
||||
}
|
||||
};
|
||||
process_repeats(sGamepadButtonRepeats);
|
||||
process_repeats(sGamepadAxisRepeats);
|
||||
} else {
|
||||
reset_input_state();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
void update_input() noexcept;
|
||||
void reset_input_state() noexcept;
|
||||
void sync_input_block() noexcept;
|
||||
void release_input_block() noexcept;
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
enum class NavCommand {
|
||||
None,
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Next, // R1
|
||||
Previous, // L1
|
||||
Confirm, // A
|
||||
Cancel, // B
|
||||
Menu, // Back/Minus, or R + Start
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "number_button.hpp"
|
||||
|
||||
#include <charconv>
|
||||
#include <fmt/format.h>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
NumberButton::NumberButton(Rml::Element* parent, Props props)
|
||||
: BaseStringButton(parent, {.key = std::move(props.key), .type = "number"}),
|
||||
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)),
|
||||
mIsDisabled(std::move(props.isDisabled)), mMin(props.min), mMax(props.max), mStep(props.step),
|
||||
mPrefix(std::move(props.prefix)), mSuffix(std::move(props.suffix)) {}
|
||||
|
||||
bool NumberButton::disabled() const {
|
||||
if (mIsDisabled) {
|
||||
return mIsDisabled();
|
||||
}
|
||||
return BaseStringButton::disabled();
|
||||
}
|
||||
|
||||
Rml::String NumberButton::format_value() {
|
||||
return fmt::format("{}{}{}", mPrefix, mGetValue(), mSuffix);
|
||||
}
|
||||
|
||||
Rml::String NumberButton::input_value() {
|
||||
return fmt::to_string(mGetValue());
|
||||
}
|
||||
|
||||
void NumberButton::set_value(Rml::String value) {
|
||||
if (!mSetValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
int parsedValue = 0;
|
||||
const char* begin = value.data();
|
||||
const char* end = begin + value.size();
|
||||
const auto result = std::from_chars(begin, end, parsedValue);
|
||||
if (result.ec != std::errc() || result.ptr != end) {
|
||||
return;
|
||||
}
|
||||
|
||||
mSetValue(std::clamp(parsedValue, mMin, mMax));
|
||||
}
|
||||
|
||||
bool NumberButton::handle_nav_command(NavCommand cmd) {
|
||||
if (cmd == NavCommand::Left) {
|
||||
mSetValue(std::clamp(mGetValue() - mStep, mMin, mMax));
|
||||
return true;
|
||||
} else if (cmd == NavCommand::Right) {
|
||||
mSetValue(std::clamp(mGetValue() + mStep, mMin, mMax));
|
||||
return true;
|
||||
}
|
||||
return BaseStringButton::handle_nav_command(cmd);
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "string_button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class NumberButton : public BaseStringButton {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
std::function<int()> getValue;
|
||||
std::function<void(int)> setValue;
|
||||
std::function<bool()> isDisabled;
|
||||
int min = 0;
|
||||
int max = INT_MAX;
|
||||
int step = 1;
|
||||
Rml::String prefix;
|
||||
Rml::String suffix;
|
||||
};
|
||||
|
||||
NumberButton(Rml::Element* parent, Props props);
|
||||
|
||||
bool disabled() const override;
|
||||
|
||||
protected:
|
||||
Rml::String format_value() override;
|
||||
Rml::String input_value() override;
|
||||
void set_value(Rml::String value) override;
|
||||
bool handle_nav_command(NavCommand cmd) override;
|
||||
|
||||
private:
|
||||
std::function<int()> mGetValue;
|
||||
std::function<void(int)> mSetValue;
|
||||
std::function<bool()> mIsDisabled;
|
||||
int mMin;
|
||||
int mMax;
|
||||
int mStep;
|
||||
Rml::String mPrefix;
|
||||
Rml::String mSuffix;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,275 @@
|
||||
#include "overlay.hpp"
|
||||
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
#include <dolphin/vi.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
<head>
|
||||
<link type="text/rcss" href="res/rml/overlay.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" class="overlay-root">
|
||||
<div class="overlay">
|
||||
<div class="header">
|
||||
<div id="title"></div>
|
||||
<div id="carousel-container" class="carousel-container"></div>
|
||||
</div>
|
||||
<div id="description" class="description"></div>
|
||||
<div class="divider"></div>
|
||||
<div id="footer" class="footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</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::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);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
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::BloomMode:
|
||||
getSettings().game.bloomMode.setValue(static_cast<BloomMode>(std::clamp(
|
||||
value, static_cast<int>(BloomMode::Off), static_cast<int>(BloomMode::Dusk))));
|
||||
break;
|
||||
case GraphicsOption::BloomMultiplier:
|
||||
getSettings().game.bloomMultiplier.setValue(std::clamp(value, 0, 100) / 100.0f);
|
||||
break;
|
||||
}
|
||||
config::Save();
|
||||
}
|
||||
|
||||
Rml::Element* create_stepped_carousel_root(Rml::Element* parent) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto root = doc->CreateElement("div");
|
||||
root->SetClass("stepped-carousel", true);
|
||||
root->SetAttribute("tabindex", "0");
|
||||
return parent->AppendChild(std::move(root));
|
||||
}
|
||||
|
||||
Rml::Element* create_stepped_carousel_arrow(
|
||||
Rml::Element* parent, const Rml::String& className, const Rml::String& label) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto button = doc->CreateElement("button");
|
||||
button->SetClass("stepped-carousel-arrow", true);
|
||||
button->SetClass(className, true);
|
||||
button->SetInnerRML(escape(label));
|
||||
return parent->AppendChild(std::move(button));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SteppedCarousel::SteppedCarousel(Rml::Element* parent, Props props)
|
||||
: Component(create_stepped_carousel_root(parent)), mProps(std::move(props)) {
|
||||
Rml::Element* prevElem = create_stepped_carousel_arrow(mRoot, "prev", "<");
|
||||
mValueElem = append(mRoot, "div");
|
||||
mValueElem->SetClass("stepped-carousel-value", true);
|
||||
Rml::Element* nextElem = create_stepped_carousel_arrow(mRoot, "next", ">");
|
||||
|
||||
listen(prevElem, Rml::EventId::Click,
|
||||
[this](Rml::Event&) { handle_nav_command(NavCommand::Left); });
|
||||
listen(nextElem, Rml::EventId::Click,
|
||||
[this](Rml::Event&) { handle_nav_command(NavCommand::Right); });
|
||||
listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd != NavCommand::None && handle_nav_command(cmd)) {
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool SteppedCarousel::focus() {
|
||||
return Component::focus();
|
||||
}
|
||||
|
||||
void SteppedCarousel::update() {
|
||||
if (mValueElem == nullptr) {
|
||||
return;
|
||||
}
|
||||
const int value = std::clamp(mProps.getValue ? mProps.getValue() : 0, mProps.min, mProps.max);
|
||||
if (mProps.formatValue) {
|
||||
mValueElem->SetInnerRML(mProps.formatValue(value));
|
||||
} else {
|
||||
mValueElem->SetInnerRML(std::to_string(value));
|
||||
}
|
||||
}
|
||||
|
||||
bool SteppedCarousel::handle_nav_command(NavCommand cmd) {
|
||||
if (cmd == NavCommand::Left) {
|
||||
const int value = mProps.getValue ? mProps.getValue() : 0;
|
||||
apply(std::clamp(value - mProps.step, mProps.min, mProps.max));
|
||||
return true;
|
||||
}
|
||||
if (cmd == NavCommand::Right) {
|
||||
const int value = mProps.getValue ? mProps.getValue() : 0;
|
||||
apply(std::clamp(value + mProps.step, mProps.min, mProps.max));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SteppedCarousel::apply(int value) {
|
||||
const int nextValue = std::clamp(value, mProps.min, mProps.max);
|
||||
const int currentValue =
|
||||
std::clamp(mProps.getValue ? mProps.getValue() : 0, mProps.min, mProps.max);
|
||||
if (nextValue == currentValue) {
|
||||
return;
|
||||
}
|
||||
if (mProps.onChange) {
|
||||
mProps.onChange(nextValue);
|
||||
}
|
||||
}
|
||||
|
||||
Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
|
||||
switch (option) {
|
||||
case GraphicsOption::InternalResolution:
|
||||
if (value <= 0) {
|
||||
return "Auto";
|
||||
} else {
|
||||
u32 width = 0;
|
||||
u32 height = 0;
|
||||
AuroraGetRenderSize(&width, &height);
|
||||
return fmt::format("{}x ({}x{})", value, width, height);
|
||||
}
|
||||
case GraphicsOption::ShadowResolution:
|
||||
return fmt::format("{}x", value);
|
||||
case GraphicsOption::BloomMode:
|
||||
switch (static_cast<BloomMode>(value)) {
|
||||
case BloomMode::Off:
|
||||
return "Off";
|
||||
case BloomMode::Classic:
|
||||
return "Classic";
|
||||
case BloomMode::Dusk:
|
||||
return "Dusk";
|
||||
}
|
||||
break;
|
||||
case GraphicsOption::BloomMultiplier:
|
||||
return fmt::format("{}%", value);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
Overlay::Overlay(OverlayProps props)
|
||||
: Document(kDocumentSource), mOption(props.option), mValueMin(props.valueMin),
|
||||
mValueMax(props.valueMax), mDefaultValue(props.defaultValue) {
|
||||
if (mDocument == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto* title = mDocument->GetElementById("title")) {
|
||||
title->SetInnerRML(escape(props.title));
|
||||
}
|
||||
if (auto* description = mDocument->GetElementById("description")) {
|
||||
description->SetInnerRML(escape(props.helpText));
|
||||
}
|
||||
if (auto* carouselParent = mDocument->GetElementById("carousel-container")) {
|
||||
add_component<SteppedCarousel>(carouselParent,
|
||||
SteppedCarousel::Props{
|
||||
.min = mValueMin,
|
||||
.max = mValueMax,
|
||||
.step = 1,
|
||||
.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); },
|
||||
});
|
||||
}
|
||||
|
||||
if (auto* footer = mDocument->GetElementById("footer")) {
|
||||
auto& returnButton = add_component<Button>(footer, "\xE2\x86\x90 Return", "footer-button")
|
||||
.on_pressed([this] { pop(); });
|
||||
returnButton.root()->SetClass("return", true);
|
||||
auto& resetButton =
|
||||
add_component<Button>(footer, "Reset to default", "footer-button").on_pressed([this] {
|
||||
reset_default();
|
||||
});
|
||||
resetButton.root()->SetClass("reset", true);
|
||||
}
|
||||
|
||||
// Hide document after transition completion
|
||||
mRoot = mDocument->GetElementById("root");
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") &&
|
||||
Document::visible())
|
||||
{
|
||||
Document::hide(mPendingClose);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Overlay::show() {
|
||||
Document::show();
|
||||
mRoot->SetAttribute("open", "");
|
||||
}
|
||||
|
||||
void Overlay::hide(bool close) {
|
||||
mRoot->RemoveAttribute("open");
|
||||
if (close) {
|
||||
mPendingClose = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Overlay::update() {
|
||||
for (const auto& component : mComponents) {
|
||||
component->update();
|
||||
}
|
||||
Document::update();
|
||||
}
|
||||
|
||||
bool Overlay::focus() {
|
||||
for (const auto& component : mComponents) {
|
||||
if (component->focus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Overlay::visible() const {
|
||||
return mRoot->HasAttribute("open");
|
||||
}
|
||||
|
||||
bool Overlay::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Cancel) {
|
||||
pop();
|
||||
return true;
|
||||
}
|
||||
return Document::handle_nav_command(event, cmd);
|
||||
}
|
||||
|
||||
void Overlay::reset_default() {
|
||||
set_value(mOption, mDefaultValue);
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "component.hpp"
|
||||
#include "document.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class SteppedCarousel : public Component {
|
||||
public:
|
||||
struct Props {
|
||||
int min = 0;
|
||||
int max = 0;
|
||||
int step = 1;
|
||||
std::function<int()> getValue;
|
||||
std::function<void(int)> onChange;
|
||||
std::function<Rml::String(int)> formatValue;
|
||||
};
|
||||
|
||||
SteppedCarousel(Rml::Element* parent, Props props);
|
||||
|
||||
bool focus() override;
|
||||
void update() override;
|
||||
|
||||
private:
|
||||
bool handle_nav_command(NavCommand cmd);
|
||||
void apply(int value);
|
||||
|
||||
Props mProps;
|
||||
Rml::Element* mValueElem = nullptr;
|
||||
};
|
||||
|
||||
enum class GraphicsOption {
|
||||
InternalResolution,
|
||||
ShadowResolution,
|
||||
BloomMode,
|
||||
BloomMultiplier,
|
||||
};
|
||||
|
||||
Rml::String format_graphics_setting_value(GraphicsOption option, int value);
|
||||
|
||||
struct OverlayProps {
|
||||
GraphicsOption option;
|
||||
Rml::String title;
|
||||
Rml::String helpText;
|
||||
int valueMin = 0;
|
||||
int valueMax = 0;
|
||||
int defaultValue = 0;
|
||||
};
|
||||
|
||||
class Overlay : public Document {
|
||||
public:
|
||||
explicit Overlay(OverlayProps props);
|
||||
|
||||
void show() override;
|
||||
void hide(bool close) override;
|
||||
void update() override;
|
||||
bool focus() override;
|
||||
bool visible() const override;
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
private:
|
||||
template <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T> T& add_component(Args&&... args) {
|
||||
auto child = std::make_unique<T>(std::forward<Args>(args)...);
|
||||
T& ref = *child;
|
||||
mComponents.emplace_back(std::move(child));
|
||||
return ref;
|
||||
}
|
||||
|
||||
void reset_default();
|
||||
|
||||
GraphicsOption mOption;
|
||||
int mValueMin = 0;
|
||||
int mValueMax = 0;
|
||||
int mDefaultValue = 0;
|
||||
std::vector<std::unique_ptr<Component> > mComponents;
|
||||
Rml::Element* mRoot;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,151 @@
|
||||
#include "pane.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
Rml::Element* createRoot(Rml::Element* parent) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto elem = doc->CreateElement("pane");
|
||||
return parent->AppendChild(std::move(elem));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Pane::Pane(Rml::Element* parent, Type type) : FluentComponent(createRoot(parent)), mType(type) {
|
||||
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
|
||||
// If navigating to the next pane, select the focused item
|
||||
if (mType == Type::Controlled && cmd == NavCommand::Right) {
|
||||
auto* target = event.GetTargetElement();
|
||||
int focusedChild = -1;
|
||||
for (size_t i = 0; i < mChildren.size(); ++i) {
|
||||
if (mChildren[i]->contains(target)) {
|
||||
focusedChild = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (focusedChild == -1) {
|
||||
return;
|
||||
}
|
||||
set_selected_item(focusedChild);
|
||||
return;
|
||||
}
|
||||
|
||||
int direction = 0;
|
||||
if (cmd == NavCommand::Down) {
|
||||
direction = 1;
|
||||
} else if (cmd == NavCommand::Up) {
|
||||
direction = -1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
auto* target = event.GetTargetElement();
|
||||
int focusedChild = -1;
|
||||
for (size_t i = 0; i < mChildren.size(); ++i) {
|
||||
if (mChildren[i]->contains(target)) {
|
||||
focusedChild = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (focusedChild == -1) {
|
||||
return;
|
||||
}
|
||||
int i = focusedChild + direction;
|
||||
while (i >= 0 && i < mChildren.size()) {
|
||||
if (mChildren[i]->focus()) {
|
||||
event.StopPropagation();
|
||||
break;
|
||||
}
|
||||
i += direction;
|
||||
}
|
||||
});
|
||||
|
||||
if (type == Type::Controlled) {
|
||||
// For controlled panes, handle SelectButton Submit events for item selection
|
||||
listen(Rml::EventId::Submit, [this](Rml::Event& event) {
|
||||
int childIndex = -1;
|
||||
for (int i = 0; i < mChildren.size(); ++i) {
|
||||
if (event.GetTargetElement() == mChildren[i]->root()) {
|
||||
childIndex = i;
|
||||
}
|
||||
}
|
||||
set_selected_item(childIndex);
|
||||
// If the selection was handled locally, don't allow it to bubble up to window
|
||||
if (event.GetParameter("handled", false)) {
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Pane::update() {
|
||||
finalize();
|
||||
Component::update();
|
||||
}
|
||||
|
||||
void Pane::set_selected_item(int index) {
|
||||
if (mType == Type::Uncontrolled) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < mChildren.size(); ++i) {
|
||||
mChildren[i]->set_selected(i == index);
|
||||
}
|
||||
}
|
||||
|
||||
bool Pane::focus() {
|
||||
// Focus the first selected child
|
||||
for (const auto& child : mChildren) {
|
||||
if (child->selected() && child->focus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Otherwise, focus the first focusable child
|
||||
for (const auto& child : mChildren) {
|
||||
if (child->focus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Rml::Element* Pane::add_section(const Rml::String& text) {
|
||||
auto* elem = append(mRoot, "div");
|
||||
elem->SetClass("section-heading", true);
|
||||
elem->SetInnerRML(escape(text));
|
||||
return elem;
|
||||
}
|
||||
|
||||
Rml::Element* Pane::add_text(const Rml::String& text) {
|
||||
auto* elem = append(mRoot, "div");
|
||||
elem->SetInnerRML(escape(text));
|
||||
return elem;
|
||||
}
|
||||
|
||||
Rml::Element* Pane::add_rml(const Rml::String& rml) {
|
||||
auto* elem = append(mRoot, "div");
|
||||
elem->SetInnerRML(rml);
|
||||
return elem;
|
||||
}
|
||||
|
||||
void Pane::finalize() {
|
||||
if (finalized) {
|
||||
return;
|
||||
}
|
||||
finalized = true;
|
||||
|
||||
// Append spacer element to the bottom. RmlUi does not properly handle
|
||||
// padding-bottom or margin-bottom on a scrollable flex container, so
|
||||
// we need to create a fake spacer with an actual layout height to get
|
||||
// padding at the bottom of a scrollable container.
|
||||
append(mRoot, "spacer");
|
||||
}
|
||||
|
||||
void Pane::clear() {
|
||||
clear_children();
|
||||
finalized = false;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "component.hpp"
|
||||
#include "select_button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Pane : public FluentComponent<Pane> {
|
||||
public:
|
||||
enum class Type {
|
||||
Controlled,
|
||||
Uncontrolled,
|
||||
};
|
||||
|
||||
explicit Pane(Rml::Element* parent, Type type);
|
||||
|
||||
bool focus() override;
|
||||
void update() override;
|
||||
|
||||
void set_selected_item(int index);
|
||||
|
||||
Rml::Element* add_section(const Rml::String& text);
|
||||
ControlledButton& add_button(ControlledButton::Props props) {
|
||||
return add_child<ControlledButton>(std::move(props));
|
||||
}
|
||||
Button& add_button(Rml::String text) { return add_child<Button>(std::move(text)); }
|
||||
ControlledSelectButton& add_select_button(ControlledSelectButton::Props props) {
|
||||
return add_child<ControlledSelectButton>(std::move(props));
|
||||
}
|
||||
Rml::Element* add_text(const Rml::String& text);
|
||||
Rml::Element* add_rml(const Rml::String& rml);
|
||||
void finalize();
|
||||
void clear();
|
||||
|
||||
private:
|
||||
Type mType;
|
||||
bool finalized = false;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,127 @@
|
||||
#include "popup.hpp"
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
|
||||
#include "aurora/rmlui.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "editor.hpp"
|
||||
#include "imgui.h"
|
||||
#include "settings.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
<head>
|
||||
<link type="text/rcss" href="res/rml/tabbing.rcss" />
|
||||
<link type="text/rcss" href="res/rml/popup.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<popup id="popup"></div>
|
||||
</body>
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
}
|
||||
|
||||
Popup::Popup() : Document(kDocumentSource), mRoot(mDocument->GetElementById("popup")) {
|
||||
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{.autoSelect = false});
|
||||
mTabBar->add_tab("Settings", [] { push_document(std::make_unique<SettingsWindow>()); });
|
||||
mTabBar->add_tab("Warp", [] {
|
||||
// TODO
|
||||
});
|
||||
mTabBar->add_tab("Editor", [] { push_document(std::make_unique<EditorWindow>()); });
|
||||
mTabBar->add_tab("Reset", [this] {
|
||||
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
|
||||
mTabBar->set_active_tab(-1);
|
||||
hide(false);
|
||||
});
|
||||
mTabBar->add_tab("Exit", [] { IsRunning = false; });
|
||||
|
||||
// Hide document after transition completion
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") &&
|
||||
Document::visible())
|
||||
{
|
||||
Document::hide(mPendingClose);
|
||||
}
|
||||
});
|
||||
|
||||
// We start hidden, but want focus for an open nav event
|
||||
mDocument->Focus();
|
||||
}
|
||||
|
||||
void Popup::show() {
|
||||
Document::show();
|
||||
mRoot->SetAttribute("open", "");
|
||||
mTabBar->set_active_tab(-1);
|
||||
}
|
||||
|
||||
void Popup::hide(bool close) {
|
||||
mRoot->RemoveAttribute("open");
|
||||
if (close) {
|
||||
mPendingClose = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Popup::update() {
|
||||
update_safe_area();
|
||||
Document::update();
|
||||
}
|
||||
|
||||
void Popup::update_safe_area() noexcept {
|
||||
if (mDocument == nullptr || mTabBar == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid ImGui menu bar if shown
|
||||
if (const auto* viewport = ImGui::GetMainViewport();
|
||||
viewport != nullptr && mTopMargin != viewport->WorkPos.y)
|
||||
{
|
||||
mTopMargin = viewport->WorkPos.y;
|
||||
mRoot->SetProperty(Rml::PropertyId::MarginTop, Rml::Property(mTopMargin, Rml::Unit::DP));
|
||||
}
|
||||
|
||||
Rml::Context* context = mDocument->GetContext();
|
||||
Insets safeInsets = safe_area_insets(context);
|
||||
safeInsets = {
|
||||
0.0f,
|
||||
std::round(safeInsets.right),
|
||||
0.0f,
|
||||
std::round(safeInsets.left),
|
||||
};
|
||||
if (safeInsets == mTabBarPadding) {
|
||||
return;
|
||||
}
|
||||
|
||||
mTabBarPadding = safeInsets;
|
||||
auto* tabBar = mTabBar->root();
|
||||
tabBar->SetProperty(
|
||||
Rml::PropertyId::PaddingRight, Rml::Property(safeInsets.right, Rml::Unit::PX));
|
||||
tabBar->SetProperty(
|
||||
Rml::PropertyId::PaddingLeft, Rml::Property(safeInsets.left, Rml::Unit::PX));
|
||||
}
|
||||
|
||||
bool Popup::visible() const {
|
||||
return mRoot->HasAttribute("open");
|
||||
}
|
||||
|
||||
bool Popup::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Cancel) {
|
||||
hide(false);
|
||||
return true;
|
||||
}
|
||||
return Document::handle_nav_command(event, cmd);
|
||||
}
|
||||
|
||||
bool Popup::focus() {
|
||||
return mTabBar->focus();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "document.hpp"
|
||||
#include "tab_bar.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Popup : public Document {
|
||||
public:
|
||||
Popup();
|
||||
|
||||
Popup(const Popup&) = delete;
|
||||
Popup& operator=(const Popup&) = delete;
|
||||
|
||||
void show() override;
|
||||
void hide(bool close) override;
|
||||
void update() override;
|
||||
bool focus() override;
|
||||
bool visible() const override;
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
private:
|
||||
void update_safe_area() noexcept;
|
||||
|
||||
Rml::Element* mRoot;
|
||||
std::unique_ptr<TabBar> mTabBar;
|
||||
std::unique_ptr<Button> mCloseButton;
|
||||
Insets mTabBarPadding;
|
||||
float mTopMargin = 0.f;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,256 @@
|
||||
#include "prelaunch.hpp"
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/file_select.hpp"
|
||||
#include "dusk/iso_validate.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/ui/prelaunch_options.hpp"
|
||||
#include "version.h"
|
||||
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
#include <aurora/lib/window.hpp>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
<head>
|
||||
<link type="text/rcss" href="res/rml/prelaunch.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<content id="root" open>
|
||||
<menu>
|
||||
<hero class="intro-item delay-0">
|
||||
<div class="eyebrow"><span>Twilit Realm</span> presents</div>
|
||||
<img src="res/logo-mascot.png" />
|
||||
</hero>
|
||||
<div id="menu-list" />
|
||||
</menu>
|
||||
<disk-status class="intro-item delay-4">
|
||||
<span id="status" class="status" />
|
||||
<span id="detail" class="detail" />
|
||||
</disk-status>
|
||||
<version-info class="intro-item delay-5">
|
||||
<div class="version">Version <span id="version-text"></span></div>
|
||||
<div class="update"><span>Update available!</span> Download</div>
|
||||
</version-info>
|
||||
</content>
|
||||
</body>
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
constexpr std::array<SDL_DialogFileFilter, 2> kDiscFileFilters{{
|
||||
{"Game Disc Images", "iso;gcm;ciso;gcz;nfs;rvz;wbfs;wia;tgc"},
|
||||
{"All Files", "*"},
|
||||
}};
|
||||
|
||||
void file_dialog_callback(void*, const char* path, const char* error) {
|
||||
auto& state = prelaunch_state();
|
||||
if (error != nullptr) {
|
||||
return;
|
||||
}
|
||||
if (path == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.selectedIsoPath = path;
|
||||
state.errorString.clear();
|
||||
refresh_path_state();
|
||||
getSettings().backend.isoPath.setValue(state.selectedIsoPath);
|
||||
config::Save();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PrelaunchState sPrelaunchState;
|
||||
|
||||
PrelaunchState& prelaunch_state() noexcept {
|
||||
return sPrelaunchState;
|
||||
}
|
||||
|
||||
void refresh_path_state() noexcept {
|
||||
auto& state = prelaunch_state();
|
||||
state.isPal = !state.selectedIsoPath.empty() && iso::isPal(state.selectedIsoPath.c_str());
|
||||
}
|
||||
|
||||
void ensure_initialized() noexcept {
|
||||
auto& state = prelaunch_state();
|
||||
if (state.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.selectedIsoPath = getSettings().backend.isoPath;
|
||||
state.initialGraphicsBackend = getSettings().backend.graphicsBackend;
|
||||
state.errorString.clear();
|
||||
state.initialized = true;
|
||||
refresh_path_state();
|
||||
}
|
||||
|
||||
bool is_selected_path_valid() noexcept {
|
||||
return !prelaunch_state().selectedIsoPath.empty() &&
|
||||
SDL_GetPathInfo(prelaunch_state().selectedIsoPath.c_str(), nullptr);
|
||||
}
|
||||
|
||||
void open_iso_picker() noexcept {
|
||||
ensure_initialized();
|
||||
ShowFileSelect(&file_dialog_callback, nullptr, aurora::window::get_sdl_window(),
|
||||
kDiscFileFilters.data(), kDiscFileFilters.size(), nullptr, false);
|
||||
}
|
||||
|
||||
void apply_intro_animation(Rml::Element* element, const char* delay_class) {
|
||||
if (element == nullptr || delay_class == nullptr) {
|
||||
return;
|
||||
}
|
||||
element->SetClass("intro-item", true);
|
||||
element->SetClass(delay_class, true);
|
||||
}
|
||||
|
||||
Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementById("root")) {
|
||||
ensure_initialized();
|
||||
|
||||
if (auto* menuList = mDocument->GetElementById("menu-list")) {
|
||||
const bool hasValidPath = is_selected_path_valid();
|
||||
mMenuButtons.push_back(
|
||||
std::make_unique<Button>(menuList, hasValidPath ? "Start Game" : "Select Disk Image"));
|
||||
mMenuButtons.back()->on_pressed([this] {
|
||||
if (!is_selected_path_valid()) {
|
||||
open_iso_picker();
|
||||
return;
|
||||
}
|
||||
IsGameLaunched = true;
|
||||
hide(true);
|
||||
});
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
|
||||
|
||||
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Options"));
|
||||
mMenuButtons.back()->on_pressed(
|
||||
[] { push_document(std::make_unique<PrelaunchOptions>()); });
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-2");
|
||||
|
||||
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Quit To Desktop"));
|
||||
mMenuButtons.back()->on_pressed([] { IsRunning = false; });
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-3");
|
||||
}
|
||||
|
||||
mDiscStatus = mDocument->GetElementById("status");
|
||||
mDiscDetail = mDocument->GetElementById("detail");
|
||||
mVersion = mDocument->GetElementById("version-text");
|
||||
|
||||
listen(mDocument, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
auto* target = event.GetTargetElement();
|
||||
if (target == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (target == mDocument && !mDocument->HasAttribute("open")) {
|
||||
Document::hide(true);
|
||||
} else if (target->GetTagName() == "button" && !target->IsClassSet("anim-done")) {
|
||||
target->SetClass("anim-done", true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Prelaunch::show() {
|
||||
Document::show();
|
||||
mDocument->SetAttribute("open", "");
|
||||
mRoot->SetAttribute("open", "");
|
||||
}
|
||||
|
||||
void Prelaunch::hide(bool close) {
|
||||
if (close) {
|
||||
if (!mEntranceAnimationStarted) {
|
||||
// Close document immediately
|
||||
Document::hide(true);
|
||||
}
|
||||
mDocument->RemoveAttribute("open");
|
||||
} else {
|
||||
mRoot->RemoveAttribute("open");
|
||||
}
|
||||
}
|
||||
|
||||
void Prelaunch::update() {
|
||||
ensure_initialized();
|
||||
refresh_path_state();
|
||||
|
||||
auto& state = prelaunch_state();
|
||||
const bool hasValidPath = is_selected_path_valid();
|
||||
if (hasValidPath && getSettings().backend.skipPreLaunchUI) {
|
||||
hide(true);
|
||||
IsGameLaunched = true;
|
||||
}
|
||||
|
||||
if (!mEntranceAnimationStarted && mDocument != nullptr) {
|
||||
mDocument->SetClass("animate-in", true);
|
||||
mEntranceAnimationStarted = true;
|
||||
}
|
||||
|
||||
if (!mMenuButtons.empty()) {
|
||||
mMenuButtons[0]->set_text(hasValidPath ? "Start Game" : "Select Disk Image");
|
||||
}
|
||||
if (mDiscStatus != nullptr) {
|
||||
if (hasValidPath) {
|
||||
mDiscStatus->RemoveAttribute("bad");
|
||||
mDiscStatus->SetInnerRML("Disc Ready");
|
||||
} else {
|
||||
mDiscStatus->SetAttribute("bad", "");
|
||||
mDiscStatus->SetInnerRML("Disk Not Found");
|
||||
}
|
||||
}
|
||||
if (mDiscDetail != nullptr) {
|
||||
if (hasValidPath) {
|
||||
mDiscDetail->SetProperty(Rml::PropertyId::Display, Rml::Style::Display::Block);
|
||||
mDiscDetail->SetInnerRML(state.isPal ? "GameCube • PAL" : "GameCube • USA");
|
||||
} else {
|
||||
mDiscDetail->SetProperty(Rml::PropertyId::Display, Rml::Style::Display::None);
|
||||
}
|
||||
}
|
||||
if (mVersion != nullptr) {
|
||||
mVersion->SetInnerRML(escape(DUSK_WC_DESCRIBE));
|
||||
}
|
||||
|
||||
Document::update();
|
||||
}
|
||||
|
||||
bool Prelaunch::focus() {
|
||||
if (mMenuButtons.empty()) {
|
||||
return false;
|
||||
}
|
||||
return mMenuButtons[0]->focus();
|
||||
}
|
||||
|
||||
bool Prelaunch::visible() const {
|
||||
return mDocument->HasAttribute("open") && mRoot->HasAttribute("open");
|
||||
}
|
||||
|
||||
bool Prelaunch::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
int direction = 0;
|
||||
if (cmd == NavCommand::Down) {
|
||||
direction = 1;
|
||||
} else if (cmd == NavCommand::Up) {
|
||||
direction = -1;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
auto* target = event.GetTargetElement();
|
||||
int focusedButton = -1;
|
||||
for (int i = 0; i < mMenuButtons.size(); ++i) {
|
||||
if (mMenuButtons[i]->contains(target)) {
|
||||
focusedButton = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const auto buttonCount = static_cast<int>(mMenuButtons.size());
|
||||
int i = (focusedButton + direction) % buttonCount;
|
||||
if (i < 0) i += buttonCount;
|
||||
while (i >= 0 && i < mMenuButtons.size()) {
|
||||
if (mMenuButtons[i]->focus()) {
|
||||
event.StopPropagation();
|
||||
return true;
|
||||
}
|
||||
i += direction;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "document.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Prelaunch : public Document {
|
||||
public:
|
||||
Prelaunch();
|
||||
|
||||
void show() override;
|
||||
void hide(bool close) override;
|
||||
void update() override;
|
||||
bool focus() override;
|
||||
bool visible() const override;
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
private:
|
||||
bool mEntranceAnimationStarted = false;
|
||||
std::vector<std::unique_ptr<Button>> mMenuButtons;
|
||||
Rml::Element* mRoot = nullptr;
|
||||
Rml::Element* mDiscStatus = nullptr;
|
||||
Rml::Element* mDiscDetail = nullptr;
|
||||
Rml::Element* mVersion = nullptr;
|
||||
};
|
||||
|
||||
class PrelaunchOptions;
|
||||
|
||||
struct PrelaunchState {
|
||||
std::string selectedIsoPath;
|
||||
std::string errorString;
|
||||
std::string initialGraphicsBackend;
|
||||
bool isPal = false;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
PrelaunchState& prelaunch_state() noexcept;
|
||||
void ensure_initialized() noexcept;
|
||||
void refresh_path_state() noexcept;
|
||||
bool is_selected_path_valid() noexcept;
|
||||
void open_iso_picker() noexcept;
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,263 @@
|
||||
#include "prelaunch_options.hpp"
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/settings.h"
|
||||
#include "pane.hpp"
|
||||
#include "prelaunch.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
static constexpr std::array<const char*, 5> kLanguageNames = {
|
||||
"English", "German", "French", "Spanish", "Italian",
|
||||
};
|
||||
|
||||
// TODO: Copied from ImGui prelaunch. Needs a refactor?
|
||||
bool try_parse_backend(std::string_view backend, AuroraBackend& outBackend) {
|
||||
if (backend == "auto") {
|
||||
outBackend = BACKEND_AUTO;
|
||||
return true;
|
||||
}
|
||||
if (backend == "d3d11") {
|
||||
outBackend = BACKEND_D3D11;
|
||||
return true;
|
||||
}
|
||||
if (backend == "d3d12") {
|
||||
outBackend = BACKEND_D3D12;
|
||||
return true;
|
||||
}
|
||||
if (backend == "metal") {
|
||||
outBackend = BACKEND_METAL;
|
||||
return true;
|
||||
}
|
||||
if (backend == "vulkan") {
|
||||
outBackend = BACKEND_VULKAN;
|
||||
return true;
|
||||
}
|
||||
if (backend == "opengl") {
|
||||
outBackend = BACKEND_OPENGL;
|
||||
return true;
|
||||
}
|
||||
if (backend == "opengles") {
|
||||
outBackend = BACKEND_OPENGLES;
|
||||
return true;
|
||||
}
|
||||
if (backend == "webgpu") {
|
||||
outBackend = BACKEND_WEBGPU;
|
||||
return true;
|
||||
}
|
||||
if (backend == "null") {
|
||||
outBackend = BACKEND_NULL;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string_view backend_name(AuroraBackend backend) {
|
||||
switch (backend) {
|
||||
default:
|
||||
return "Auto";
|
||||
case BACKEND_D3D12:
|
||||
return "D3D12";
|
||||
case BACKEND_D3D11:
|
||||
return "D3D11";
|
||||
case BACKEND_METAL:
|
||||
return "Metal";
|
||||
case BACKEND_VULKAN:
|
||||
return "Vulkan";
|
||||
case BACKEND_OPENGL:
|
||||
return "OpenGL";
|
||||
case BACKEND_OPENGLES:
|
||||
return "OpenGL ES";
|
||||
case BACKEND_WEBGPU:
|
||||
return "WebGPU";
|
||||
case BACKEND_NULL:
|
||||
return "Null";
|
||||
}
|
||||
}
|
||||
|
||||
std::string_view backend_id(AuroraBackend backend) {
|
||||
switch (backend) {
|
||||
default:
|
||||
return "auto";
|
||||
case BACKEND_D3D12:
|
||||
return "d3d12";
|
||||
case BACKEND_D3D11:
|
||||
return "d3d11";
|
||||
case BACKEND_METAL:
|
||||
return "metal";
|
||||
case BACKEND_VULKAN:
|
||||
return "vulkan";
|
||||
case BACKEND_OPENGL:
|
||||
return "opengl";
|
||||
case BACKEND_OPENGLES:
|
||||
return "opengles";
|
||||
case BACKEND_WEBGPU:
|
||||
return "webgpu";
|
||||
case BACKEND_NULL:
|
||||
return "null";
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<AuroraBackend> available_backends() {
|
||||
std::vector<AuroraBackend> backends;
|
||||
backends.emplace_back(BACKEND_AUTO);
|
||||
size_t backendCount = 0;
|
||||
const AuroraBackend* raw = aurora_get_available_backends(&backendCount);
|
||||
for (size_t i = 0; i < backendCount; ++i) {
|
||||
// Do not expose NULL or D3D11
|
||||
if (raw[i] != BACKEND_NULL && raw[i] != BACKEND_D3D11) {
|
||||
backends.emplace_back(raw[i]);
|
||||
}
|
||||
}
|
||||
return backends;
|
||||
}
|
||||
|
||||
class LanguageSelect final : public SelectButton {
|
||||
public:
|
||||
explicit LanguageSelect(Rml::Element* parent) : SelectButton(parent, Props{.key = "Language"}) {}
|
||||
|
||||
void update() override {
|
||||
ensure_initialized();
|
||||
refresh_path_state();
|
||||
|
||||
const bool validPath = is_selected_path_valid();
|
||||
const bool ntscDiscLocked = validPath && !prelaunch_state().isPal;
|
||||
|
||||
if (ntscDiscLocked) {
|
||||
if (getSettings().game.language.getValue() != GameLanguage::English) {
|
||||
getSettings().game.language.setValue(GameLanguage::English);
|
||||
config::Save();
|
||||
}
|
||||
set_disabled(true);
|
||||
} else {
|
||||
set_disabled(false);
|
||||
}
|
||||
|
||||
const auto lang = getSettings().game.language.getValue();
|
||||
auto value = static_cast<u8>(lang);
|
||||
if (value >= kLanguageNames.size()) {
|
||||
getSettings().game.language.setValue(GameLanguage::English);
|
||||
config::Save();
|
||||
value = static_cast<u8>(getSettings().game.language.getValue());
|
||||
}
|
||||
set_value_label(kLanguageNames[value]);
|
||||
SelectButton::update();
|
||||
}
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(NavCommand cmd) override {
|
||||
if (disabled()) {
|
||||
return false;
|
||||
}
|
||||
if (cmd != NavCommand::Confirm && cmd != NavCommand::Left && cmd != NavCommand::Right) {
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr int n = static_cast<int>(kLanguageNames.size());
|
||||
int idx = static_cast<int>(getSettings().game.language.getValue());
|
||||
const int dir = (cmd == NavCommand::Left) ? -1 : 1;
|
||||
idx = ((idx + dir) % n + n) % n;
|
||||
getSettings().game.language.setValue(static_cast<GameLanguage>(idx));
|
||||
config::Save();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class BackendSelect final : public SelectButton {
|
||||
public:
|
||||
explicit BackendSelect(Rml::Element* parent) : SelectButton(parent, Props{.key = "Graphics Backend"}) {}
|
||||
|
||||
void update() override {
|
||||
AuroraBackend configuredBackend = BACKEND_AUTO;
|
||||
const auto configuredId = getSettings().backend.graphicsBackend.getValue();
|
||||
if (!try_parse_backend(configuredId, configuredBackend)) {
|
||||
configuredBackend = BACKEND_AUTO;
|
||||
}
|
||||
// Do not expose NULL or D3D11
|
||||
if (configuredBackend == BACKEND_NULL || configuredBackend == BACKEND_D3D11) {
|
||||
getSettings().backend.graphicsBackend.setValue("auto");
|
||||
config::Save();
|
||||
configuredBackend = BACKEND_AUTO;
|
||||
}
|
||||
|
||||
const auto backend = getSettings().backend.graphicsBackend.getValue();
|
||||
Rml::String value = backend_name(configuredBackend).data();
|
||||
if (backend != prelaunch_state().initialGraphicsBackend) {
|
||||
value += " (restart required)";
|
||||
}
|
||||
set_value_label(value);
|
||||
SelectButton::update();
|
||||
}
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(NavCommand cmd) override {
|
||||
if (cmd != NavCommand::Confirm && cmd != NavCommand::Left && cmd != NavCommand::Right) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto backends = available_backends();
|
||||
const int n = static_cast<int>(backends.size());
|
||||
if (n <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AuroraBackend configuredBackend = BACKEND_AUTO;
|
||||
const auto configuredId = getSettings().backend.graphicsBackend.getValue();
|
||||
if (!try_parse_backend(configuredId, configuredBackend)) {
|
||||
configuredBackend = BACKEND_AUTO;
|
||||
}
|
||||
|
||||
int idx = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (backends[static_cast<size_t>(i)] == configuredBackend) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const int dir = (cmd == NavCommand::Left) ? -1 : 1;
|
||||
idx = ((idx + dir) % n + n) % n;
|
||||
getSettings().backend.graphicsBackend.setValue(std::string(backend_id(backends[static_cast<size_t>(idx)])));
|
||||
config::Save();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class SaveTypeSelect final : public SelectButton {
|
||||
public:
|
||||
explicit SaveTypeSelect(Rml::Element* parent) : SelectButton(parent, Props{.key = "Save File Type"}) {}
|
||||
|
||||
void update() override {
|
||||
const CARDFileType cft = static_cast<CARDFileType>(getSettings().backend.cardFileType.getValue());
|
||||
set_value_label(cft == CARD_GCIFOLDER ? "GCI Folder" : "Card Image");
|
||||
SelectButton::update();
|
||||
}
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(NavCommand cmd) override {
|
||||
if (cmd != NavCommand::Confirm && cmd != NavCommand::Left && cmd != NavCommand::Right) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CARDFileType cft = static_cast<CARDFileType>(getSettings().backend.cardFileType.getValue());
|
||||
const CARDFileType newValue = cft == CARD_GCIFOLDER ? CARD_RAWIMAGE : CARD_GCIFOLDER;
|
||||
getSettings().backend.cardFileType.setValue(newValue);
|
||||
config::Save();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
PrelaunchOptions::PrelaunchOptions() {
|
||||
add_tab("Options", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
leftPane.add_child<LanguageSelect>();
|
||||
leftPane.add_child<BackendSelect>();
|
||||
leftPane.add_child<SaveTypeSelect>();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "window.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class PrelaunchOptions : public Window {
|
||||
public:
|
||||
PrelaunchOptions();
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,69 @@
|
||||
#include "select_button.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
Rml::Element* createRoot(Rml::Element* parent) {
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
auto elem = doc->CreateElement("select-button");
|
||||
return parent->AppendChild(std::move(elem));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SelectButton::SelectButton(Rml::Element* parent, Props props)
|
||||
: FluentComponent(createRoot(parent)) {
|
||||
mKeyElem = append(mRoot, "key");
|
||||
mValueElem = append(mRoot, "value");
|
||||
update_props(std::move(props));
|
||||
on_nav_command([this](Rml::Event&, NavCommand cmd) { return handle_nav_command(cmd); });
|
||||
}
|
||||
|
||||
void SelectButton::set_value_label(const Rml::String& value) {
|
||||
if (mProps.value != value) {
|
||||
mValueElem->SetInnerRML(escape(value));
|
||||
mProps.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
void SelectButton::update_props(Props props) {
|
||||
if (mProps.key != props.key) {
|
||||
mKeyElem->SetInnerRML(escape(props.key));
|
||||
}
|
||||
set_value_label(props.value);
|
||||
mProps = std::move(props);
|
||||
}
|
||||
|
||||
bool SelectButton::handle_nav_command(NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
mRoot->DispatchEvent(Rml::EventId::Submit, {});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BaseControlledSelectButton::update() {
|
||||
set_disabled(disabled());
|
||||
set_value_label(format_value());
|
||||
SelectButton::update();
|
||||
}
|
||||
|
||||
bool ControlledSelectButton::disabled() const {
|
||||
if (mIsDisabled) {
|
||||
return mIsDisabled();
|
||||
}
|
||||
return BaseControlledSelectButton::disabled();
|
||||
}
|
||||
|
||||
Rml::String ControlledSelectButton::format_value() {
|
||||
if (!mGetValue) {
|
||||
return "";
|
||||
}
|
||||
return mGetValue();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include "component.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class SelectButton : public FluentComponent<SelectButton> {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
Rml::String value;
|
||||
};
|
||||
|
||||
SelectButton(Rml::Element* parent, Props props);
|
||||
|
||||
void set_value_label(const Rml::String& value);
|
||||
|
||||
protected:
|
||||
void update_props(Props props);
|
||||
virtual bool handle_nav_command(NavCommand cmd);
|
||||
|
||||
Props mProps;
|
||||
Rml::Element* mKeyElem = nullptr;
|
||||
Rml::Element* mValueElem = nullptr;
|
||||
std::function<void()> mOnHover;
|
||||
};
|
||||
|
||||
class BaseControlledSelectButton : public SelectButton {
|
||||
public:
|
||||
BaseControlledSelectButton(Rml::Element* parent, Props props)
|
||||
: SelectButton(parent, std::move(props)) {}
|
||||
|
||||
void update() override;
|
||||
|
||||
protected:
|
||||
virtual Rml::String format_value() = 0;
|
||||
};
|
||||
|
||||
class ControlledSelectButton : public BaseControlledSelectButton {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
std::function<Rml::String()> getValue;
|
||||
std::function<bool()> isDisabled;
|
||||
};
|
||||
|
||||
ControlledSelectButton(Rml::Element* parent, Props props)
|
||||
: BaseControlledSelectButton(parent, {std::move(props.key)}),
|
||||
mGetValue(std::move(props.getValue)), mIsDisabled(std::move(props.isDisabled)) {}
|
||||
|
||||
bool disabled() const override;
|
||||
|
||||
protected:
|
||||
Rml::String format_value() override;
|
||||
|
||||
std::function<Rml::String()> mGetValue;
|
||||
std::function<bool()> mIsDisabled;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,637 @@
|
||||
#include "settings.hpp"
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "aurora/gfx.h"
|
||||
#include "bool_button.hpp"
|
||||
#include "dusk/audio/DuskAudioSystem.h"
|
||||
#include "dusk/audio/DuskDsp.hpp"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/imgui/ImGuiEngine.hpp"
|
||||
#include "dusk/livesplit.h"
|
||||
#include "m_Do/m_Do_main.h"
|
||||
#include "number_button.hpp"
|
||||
#include "overlay.hpp"
|
||||
#include "pane.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
void reset_for_speedrun_mode() {
|
||||
mDoMain::developmentMode = -1;
|
||||
|
||||
getSettings().game.damageMultiplier.setValue(1);
|
||||
getSettings().game.instantDeath.setValue(false);
|
||||
getSettings().game.noHeartDrops.setValue(false);
|
||||
|
||||
getSettings().game.infiniteHearts.setValue(false);
|
||||
getSettings().game.infiniteArrows.setValue(false);
|
||||
getSettings().game.infiniteBombs.setValue(false);
|
||||
getSettings().game.infiniteOil.setValue(false);
|
||||
getSettings().game.infiniteOxygen.setValue(false);
|
||||
getSettings().game.infiniteRupees.setValue(false);
|
||||
getSettings().game.enableIndefiniteItemDrops.setValue(false);
|
||||
|
||||
getSettings().game.moonJump.setValue(false);
|
||||
getSettings().game.superClawshot.setValue(false);
|
||||
getSettings().game.alwaysGreatspin.setValue(false);
|
||||
getSettings().game.enableFastIronBoots.setValue(false);
|
||||
getSettings().game.canTransformAnywhere.setValue(false);
|
||||
getSettings().game.fastSpinner.setValue(false);
|
||||
getSettings().game.freeMagicArmor.setValue(false);
|
||||
|
||||
getSettings().game.enableTurboKeybind.setValue(false);
|
||||
}
|
||||
|
||||
const Rml::String kInternalResolutionHelpText =
|
||||
"Configure the resolution used for rendering the game. Higher values are more demanding on "
|
||||
"your graphics hardware.";
|
||||
const Rml::String kShadowResolutionHelpText =
|
||||
"Configure the shadow-map resolution. Higher values improve shadow quality but increase GPU "
|
||||
"and memory usage.";
|
||||
const Rml::String kBloomHelpText =
|
||||
"Configure the post-processing bloom effect. Classic uses the original bloom pass; Dusk uses "
|
||||
"a higher-quality bloom pass.";
|
||||
const Rml::String kBloomBrightnessHelpText =
|
||||
"Configure bloom intensity. Higher values make bright areas glow more strongly.";
|
||||
|
||||
int bloom_multiplier_percent() {
|
||||
return std::clamp(
|
||||
static_cast<int>(getSettings().game.bloomMultiplier.getValue() * 100.0f + 0.5f), 0, 100);
|
||||
}
|
||||
|
||||
int float_setting_percent(ConfigVar<float>& var) {
|
||||
return static_cast<int>(var.getValue() * 100.0f + 0.5f);
|
||||
}
|
||||
|
||||
bool gyro_enabled() {
|
||||
return getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal;
|
||||
}
|
||||
|
||||
struct ConfigBoolProps {
|
||||
Rml::String key;
|
||||
Rml::String helpText;
|
||||
std::function<void(bool)> onChange;
|
||||
std::function<bool()> isDisabled;
|
||||
};
|
||||
|
||||
SelectButton& config_bool_select(
|
||||
Pane& leftPane, Pane& rightPane, ConfigVar<bool>& var, ConfigBoolProps props) {
|
||||
return leftPane
|
||||
.add_child<BoolButton>(BoolButton::Props{
|
||||
.key = std::move(props.key),
|
||||
.getValue = [&var] { return var.getValue(); },
|
||||
.setValue =
|
||||
[&var, callback = std::move(props.onChange)](bool value) {
|
||||
if (value == var.getValue()) {
|
||||
return;
|
||||
}
|
||||
var.setValue(value);
|
||||
config::Save();
|
||||
if (callback) {
|
||||
callback(value);
|
||||
}
|
||||
},
|
||||
.isDisabled = std::move(props.isDisabled),
|
||||
})
|
||||
.on_focus([&rightPane, helpText = std::move(props.helpText)](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_rml(helpText);
|
||||
});
|
||||
}
|
||||
|
||||
SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar<float>& var,
|
||||
Rml::String key, Rml::String helpText, int min, int max, int step = 5,
|
||||
std::function<bool()> isDisabled = {}) {
|
||||
return leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = std::move(key),
|
||||
.getValue = [&var] { return float_setting_percent(var); },
|
||||
.setValue =
|
||||
[&var, min, max](int value) {
|
||||
var.setValue(std::clamp(value, min, max) / 100.0f);
|
||||
config::Save();
|
||||
},
|
||||
.isDisabled = std::move(isDisabled),
|
||||
.min = min,
|
||||
.max = max,
|
||||
.step = step,
|
||||
.suffix = "%",
|
||||
})
|
||||
.on_focus([&rightPane, helpText = std::move(helpText)](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text(helpText);
|
||||
});
|
||||
}
|
||||
|
||||
class ControllerConfigWindow : public Window {
|
||||
public:
|
||||
ControllerConfigWindow() {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
add_tab(fmt::format("Port {}", i + 1), [this](Rml::Element* content) {
|
||||
auto& pane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
pane.add_section("Coming soon");
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
SettingsWindow::SettingsWindow() {
|
||||
add_tab("Audio", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
// TODO: Individual sliders for Main Music, Sub Music, Sound Effects, and Fanfare.
|
||||
leftPane.add_section("Volume");
|
||||
leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Master Volume",
|
||||
.getValue = [] { return getSettings().audio.masterVolume.getValue(); },
|
||||
.setValue =
|
||||
[](int value) {
|
||||
getSettings().audio.masterVolume.setValue(value);
|
||||
config::Save();
|
||||
audio::SetMasterVolume(value / 100.f);
|
||||
},
|
||||
.max = 100,
|
||||
.suffix = "%",
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text("Adjusts the volume of all sounds in the game.");
|
||||
});
|
||||
|
||||
leftPane.add_section("Effects");
|
||||
config_bool_select(leftPane, rightPane, getSettings().audio.enableReverb,
|
||||
{
|
||||
.key = "Enable Reverb",
|
||||
.helpText = "Enables the reverb effect in game audio.",
|
||||
.onChange = [](bool value) { audio::SetEnableReverb(value); },
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().audio.enableHrtf,
|
||||
{
|
||||
.key = "Enable Spatial Sound",
|
||||
.helpText = "Emulate surround sound via HRTF. Recommended only for use with headphones!",
|
||||
.onChange = [](bool value) { audio::EnableHrtf = value; },
|
||||
});
|
||||
|
||||
leftPane.add_section("Tweaks");
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.noLowHpSound,
|
||||
{
|
||||
.key = "No Low HP Sound",
|
||||
.helpText = "Disable the beeping sound when having low health.",
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.midnasLamentNonStop,
|
||||
{
|
||||
.key = "Non-Stop Midna's Lament",
|
||||
.helpText = "Prevents enemy music while Midna's Lament is playing.",
|
||||
});
|
||||
});
|
||||
|
||||
add_tab("Cheats", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
auto addCheat = [&](const Rml::String& key, ConfigVar<bool>& value,
|
||||
const Rml::String& helpText) {
|
||||
config_bool_select(leftPane, rightPane, value,
|
||||
{
|
||||
.key = key,
|
||||
.helpText = helpText,
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
});
|
||||
};
|
||||
|
||||
leftPane.add_section("Resources");
|
||||
addCheat("Infinite Hearts", getSettings().game.infiniteHearts,
|
||||
"Keeps your health full.");
|
||||
addCheat("Infinite Arrows", getSettings().game.infiniteArrows,
|
||||
"Keeps your arrow count full.");
|
||||
addCheat("Infinite Bombs", getSettings().game.infiniteBombs,
|
||||
"Keeps all bomb bags full.");
|
||||
addCheat("Infinite Oil", getSettings().game.infiniteOil,
|
||||
"Keeps your lantern oil full.");
|
||||
addCheat("Infinite Oxygen", getSettings().game.infiniteOxygen,
|
||||
"Keeps your underwater oxygen meter full.");
|
||||
addCheat("Infinite Rupees", getSettings().game.infiniteRupees,
|
||||
"Keeps your rupee count full.");
|
||||
addCheat("No Item Timer", getSettings().game.enableIndefiniteItemDrops,
|
||||
"Item drops such as rupees and hearts will never disappear after they drop.");
|
||||
|
||||
leftPane.add_section("Abilities");
|
||||
addCheat("Moon Jump (R+A)", getSettings().game.moonJump,
|
||||
"Hold R and A to rise into the air.");
|
||||
addCheat("Super Clawshot", getSettings().game.superClawshot,
|
||||
"Extends clawshot behavior beyond the normal game rules.");
|
||||
addCheat("Always Greatspin", getSettings().game.alwaysGreatspin,
|
||||
"Allows the Great Spin attack without requiring full health.");
|
||||
addCheat("Fast Iron Boots", getSettings().game.enableFastIronBoots,
|
||||
"Speeds up movement while wearing the Iron Boots.");
|
||||
addCheat("Can Transform Anywhere", getSettings().game.canTransformAnywhere,
|
||||
"Allows transforming even if NPCs are looking.");
|
||||
addCheat("Fast Spinner", getSettings().game.fastSpinner,
|
||||
"Speeds up Spinner movement while holding R.");
|
||||
addCheat("Free Magic Armor", getSettings().game.freeMagicArmor,
|
||||
"Lets the magic armor work without consuming rupees.");
|
||||
});
|
||||
|
||||
add_tab("Gameplay", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
auto addOption = [&](const Rml::String& key, ConfigVar<bool>& value,
|
||||
const Rml::String& helpText) {
|
||||
config_bool_select(leftPane, rightPane, value,
|
||||
{
|
||||
.key = key,
|
||||
.helpText = helpText,
|
||||
});
|
||||
};
|
||||
auto addSpeedrunDisabledOption = [&](const Rml::String& key, ConfigVar<bool>& value,
|
||||
const Rml::String& helpText) {
|
||||
config_bool_select(leftPane, rightPane, value,
|
||||
{
|
||||
.key = key,
|
||||
.helpText = helpText,
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
});
|
||||
};
|
||||
|
||||
leftPane.add_section("General");
|
||||
addOption("Mirror Mode", getSettings().game.enableMirrorMode,
|
||||
"Mirrors the world horizontally, matching the Wii version of the game.");
|
||||
addOption("Disable Main HUD", getSettings().game.disableMainHUD,
|
||||
"Disables the main HUD of the game.<br/>Useful for recording or a more immersive "
|
||||
"experience.");
|
||||
addOption("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches,
|
||||
"Restores patched glitches from Wii USA 1.0, the first released version.");
|
||||
addOption("Enable Rotating Link Doll", getSettings().game.enableLinkDollRotation,
|
||||
"Enables rotating Link in the collection menu with the C-Stick.");
|
||||
|
||||
leftPane.add_section("Difficulty");
|
||||
leftPane
|
||||
.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Damage Multiplier",
|
||||
.getValue = [] { return getSettings().game.damageMultiplier.getValue(); },
|
||||
.setValue =
|
||||
[](int value) {
|
||||
getSettings().game.damageMultiplier.setValue(value);
|
||||
config::Save();
|
||||
},
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
.min = 1,
|
||||
.max = 8,
|
||||
.prefix = "x",
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text("Multiplies incoming damage.");
|
||||
});
|
||||
addSpeedrunDisabledOption("Instant Death", getSettings().game.instantDeath,
|
||||
"Any hit will instantly kill you.");
|
||||
addSpeedrunDisabledOption("No Heart Drops", getSettings().game.noHeartDrops,
|
||||
"Hearts will never drop from enemies, pots, and various other places.");
|
||||
addOption("Hyper Enemies", getSettings().game.hyperEnemies,
|
||||
"Enemies and Bosses are twice as fast.");
|
||||
|
||||
leftPane.add_section("Quality of Life");
|
||||
addOption("Bigger Wallets", getSettings().game.biggerWallets,
|
||||
"Wallet sizes are like in the HD version. (500, 1000, 2000)");
|
||||
addOption("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes,
|
||||
"Rupees will not play cutscenes after you have collected them the first time.");
|
||||
addOption("Faster Climbing", getSettings().game.fastClimbing,
|
||||
"Quicker climbing on ladders and vines like the HD version.");
|
||||
addOption("Faster Tears of Light", getSettings().game.fastTears,
|
||||
"Tears of Light dropped by Shadow Insects pop out faster like the HD version.");
|
||||
addOption("Autosave", getSettings().game.autoSave,
|
||||
"Autosaves the game when going to a new area, opening a dungeon door, or getting "
|
||||
"a new item.<br/><br/>This feature is currently experimental, use at your own risk.");
|
||||
addOption("Instant Saves", getSettings().game.instantSaves,
|
||||
"Skips the delay when writing to the Memory Card.");
|
||||
addOption("Hold B for Instant Text", getSettings().game.instantText,
|
||||
"Makes text scroll immediately by holding B.");
|
||||
addOption("No Climbing Miss Animation", getSettings().game.noMissClimbing,
|
||||
"Prevents Link from playing a struggle animation when grabbing ledges or "
|
||||
"climbing on vines.");
|
||||
addOption("No Rupee Returns", getSettings().game.noReturnRupees,
|
||||
"Always collect Rupees even if your Wallet is too full.");
|
||||
addOption("No Sword Recoil", getSettings().game.noSwordRecoil,
|
||||
"Link will not recoil when his sword hits walls.");
|
||||
addOption("No 2nd Fish for Cat", getSettings().game.no2ndFishForCat,
|
||||
"Skip needing to catch a second fish for Sera's cat.");
|
||||
addOption("Skip TV Settings Screen", getSettings().game.hideTvSettingsScreen,
|
||||
"Skips the TV calibration screen shown when loading a save.");
|
||||
addOption("Skip Warning Screen", getSettings().game.skipWarningScreen,
|
||||
"Skips the warning screen shown when starting the game.");
|
||||
addOption("Sun's Song (R+X)", getSettings().game.sunsSong,
|
||||
"Allows Wolf Link to howl and change the time of day.");
|
||||
addOption("Quick Transform (R+Y)", getSettings().game.enableQuickTransform,
|
||||
"Transform instantly by pressing R and Y simultaneously.");
|
||||
|
||||
leftPane.add_section("Speedrunning");
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.speedrunMode,
|
||||
{
|
||||
.key = "Speedrun Mode",
|
||||
.helpText =
|
||||
"Enables speedrunning options while restricting certain gameplay modifiers.",
|
||||
.onChange = [](bool) { reset_for_speedrun_mode(); },
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.liveSplitEnabled,
|
||||
{
|
||||
.key = "LiveSplit Connection",
|
||||
.helpText = "Connect to LiveSplit server on localhost:16834.",
|
||||
.onChange =
|
||||
[](bool enabled) {
|
||||
if (enabled) {
|
||||
speedrun::connectLiveSplit();
|
||||
} else {
|
||||
speedrun::disconnectLiveSplit();
|
||||
}
|
||||
},
|
||||
.isDisabled = [] { return !getSettings().game.speedrunMode; },
|
||||
});
|
||||
});
|
||||
|
||||
add_tab("Input", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
auto addOption = [&](const Rml::String& key, ConfigVar<bool>& value,
|
||||
const Rml::String& helpText, std::function<bool()> isDisabled = {}) {
|
||||
config_bool_select(leftPane, rightPane, value,
|
||||
{
|
||||
.key = key,
|
||||
.helpText = helpText,
|
||||
.isDisabled = std::move(isDisabled),
|
||||
});
|
||||
};
|
||||
|
||||
leftPane.add_section("Controller");
|
||||
leftPane.add_button("Configure Controller")
|
||||
.on_pressed([] { push_document(std::make_unique<ControllerConfigWindow>()); })
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text("Open controller binding configuration.");
|
||||
});
|
||||
|
||||
leftPane.add_section("Camera");
|
||||
addOption("Free Camera", getSettings().game.freeCamera,
|
||||
"Enables twin-stick camera control, letting the C-Stick move the camera vertically as "
|
||||
"well as horizontally.");
|
||||
addOption("Invert Camera X Axis", getSettings().game.invertCameraXAxis,
|
||||
"Invert horizontal camera movement.");
|
||||
addOption("Invert Camera Y Axis", getSettings().game.invertCameraYAxis,
|
||||
"Invert vertical camera movement when Free Camera is enabled.",
|
||||
[] { return !getSettings().game.freeCamera; });
|
||||
config_percent_select(leftPane, rightPane, getSettings().game.freeCameraSensitivity,
|
||||
"Free Camera Sensitivity", "Adjusts twin-stick camera sensitivity.", 50, 200, 5,
|
||||
[] { return !getSettings().game.freeCamera; });
|
||||
|
||||
leftPane.add_section("Gyro");
|
||||
addOption("Gyro Aim", getSettings().game.enableGyroAim,
|
||||
"Enables gyro controls while in look mode, aiming a hawk, and aiming "
|
||||
"supported items.<br/><br/>Supported items include the Slingshot, Gale Boomerang, "
|
||||
"Hero's Bow, Clawshot(s), Ball and Chain, and Dominion Rod.");
|
||||
addOption("Gyro Rollgoal", getSettings().game.enableGyroRollgoal,
|
||||
"Enables gyro controls for Rollgoal in Hena's Cabin.");
|
||||
config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityY,
|
||||
"Gyro Pitch Sensitivity", "Controls vertical gyro aiming sensitivity.", 25, 400, 5,
|
||||
[] { return !gyro_enabled(); });
|
||||
config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityX,
|
||||
"Gyro Yaw Sensitivity", "Controls horizontal gyro aiming sensitivity.", 25, 400, 5,
|
||||
[] { return !gyro_enabled(); });
|
||||
config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityRollgoal,
|
||||
"Rollgoal Sensitivity", "Controls how strongly gyro input tilts the Rollgoal table.",
|
||||
25, 400, 5, [] { return !getSettings().game.enableGyroRollgoal; });
|
||||
config_percent_select(leftPane, rightPane, getSettings().game.gyroDeadband, "Gyro Deadband",
|
||||
"Ignores small gyro movement to reduce drift and jitter.", 0, 50, 1,
|
||||
[] { return !gyro_enabled(); });
|
||||
config_percent_select(leftPane, rightPane, getSettings().game.gyroSmoothing,
|
||||
"Gyro Smoothing", "Higher values smooth gyro input over time.", 0, 100, 1,
|
||||
[] { return !gyro_enabled(); });
|
||||
addOption("Invert Gyro Pitch", getSettings().game.gyroInvertPitch,
|
||||
"Invert vertical gyro aiming.", [] { return !gyro_enabled(); });
|
||||
addOption("Invert Gyro Yaw", getSettings().game.gyroInvertYaw,
|
||||
"Invert horizontal gyro aiming.", [] { return !gyro_enabled(); });
|
||||
|
||||
leftPane.add_section("Tools");
|
||||
addOption("Turbo Key", getSettings().game.enableTurboKeybind,
|
||||
"Hold Tab to increase game speed by up to 4x.",
|
||||
[] { return getSettings().game.speedrunMode; });
|
||||
});
|
||||
|
||||
add_tab("Graphics", [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("Display");
|
||||
|
||||
leftPane.add_button("Toggle Fullscreen").on_pressed([] {
|
||||
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
|
||||
VISetWindowFullscreen(getSettings().video.enableFullscreen);
|
||||
config::Save();
|
||||
});
|
||||
leftPane.add_button("Restore Default Window Size").on_pressed([] {
|
||||
getSettings().video.enableFullscreen.setValue(false);
|
||||
VISetWindowFullscreen(false);
|
||||
VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2);
|
||||
VICenterWindow();
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().video.enableVsync,
|
||||
{
|
||||
.key = "Enable VSync",
|
||||
.helpText = "Synchronizes the frame rate to your monitor's refresh rate.",
|
||||
.onChange = [](bool value) { aurora_enable_vsync(value); },
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().video.lockAspectRatio,
|
||||
{
|
||||
.key = "Lock 4:3 Aspect Ratio",
|
||||
.helpText = "Lock the game's aspect ratio to the original.",
|
||||
.onChange =
|
||||
[](bool value) {
|
||||
AuroraSetViewportPolicy(
|
||||
value ? AURORA_VIEWPORT_FIT : AURORA_VIEWPORT_STRETCH);
|
||||
},
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.pauseOnFocusLost,
|
||||
{
|
||||
.key = "Pause on Focus Lost",
|
||||
.isDisabled = [] { return IsMobile; },
|
||||
});
|
||||
|
||||
leftPane.add_section("Resolution");
|
||||
leftPane
|
||||
.add_select_button({
|
||||
.key = "Internal Resolution",
|
||||
.getValue =
|
||||
[] {
|
||||
return format_graphics_setting_value(GraphicsOption::InternalResolution,
|
||||
getSettings().game.internalResolutionScale.getValue());
|
||||
},
|
||||
})
|
||||
.on_nav_command([](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
|
||||
cmd == NavCommand::Right) {
|
||||
push_document(std::make_unique<Overlay>(OverlayProps{
|
||||
.option = GraphicsOption::InternalResolution,
|
||||
.title = "Internal Resolution",
|
||||
.helpText = kInternalResolutionHelpText,
|
||||
.valueMin = 0,
|
||||
.valueMax = 12,
|
||||
.defaultValue = 0,
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text(kInternalResolutionHelpText);
|
||||
});
|
||||
leftPane
|
||||
.add_select_button({
|
||||
.key = "Shadow Resolution",
|
||||
.getValue =
|
||||
[] {
|
||||
return format_graphics_setting_value(GraphicsOption::ShadowResolution,
|
||||
getSettings().game.shadowResolutionMultiplier.getValue());
|
||||
},
|
||||
})
|
||||
.on_nav_command([](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
|
||||
cmd == NavCommand::Right) {
|
||||
push_document(std::make_unique<Overlay>(OverlayProps{
|
||||
.option = GraphicsOption::ShadowResolution,
|
||||
.title = "Shadow Resolution",
|
||||
.helpText = kShadowResolutionHelpText,
|
||||
.valueMin = 1,
|
||||
.valueMax = 8,
|
||||
.defaultValue = 1,
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text(kShadowResolutionHelpText);
|
||||
});
|
||||
|
||||
leftPane.add_section("Post-Processing");
|
||||
leftPane
|
||||
.add_select_button({
|
||||
.key = "Bloom",
|
||||
.getValue =
|
||||
[] {
|
||||
return format_graphics_setting_value(GraphicsOption::BloomMode,
|
||||
static_cast<int>(getSettings().game.bloomMode.getValue()));
|
||||
},
|
||||
})
|
||||
.on_nav_command([](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
|
||||
cmd == NavCommand::Right) {
|
||||
push_document(std::make_unique<Overlay>(OverlayProps{
|
||||
.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),
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text(kBloomHelpText);
|
||||
});
|
||||
leftPane
|
||||
.add_select_button({
|
||||
.key = "Bloom Brightness",
|
||||
.getValue =
|
||||
[] {
|
||||
return format_graphics_setting_value(
|
||||
GraphicsOption::BloomMultiplier, bloom_multiplier_percent());
|
||||
},
|
||||
.isDisabled =
|
||||
[] { return getSettings().game.bloomMode.getValue() == BloomMode::Off; },
|
||||
})
|
||||
.on_nav_command([](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
|
||||
cmd == NavCommand::Right) {
|
||||
push_document(std::make_unique<Overlay>(OverlayProps{
|
||||
.option = GraphicsOption::BloomMultiplier,
|
||||
.title = "Bloom Brightness",
|
||||
.helpText = kBloomBrightnessHelpText,
|
||||
.valueMin = 0,
|
||||
.valueMax = 100,
|
||||
.defaultValue = 100,
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.on_focus([&rightPane](Rml::Event&) {
|
||||
rightPane.clear();
|
||||
rightPane.add_text(kBloomBrightnessHelpText);
|
||||
});
|
||||
|
||||
leftPane.add_section("Rendering");
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.enableFrameInterpolation,
|
||||
{
|
||||
.key = "Unlock Framerate",
|
||||
.helpText =
|
||||
"Uses inter-frame interpolation to enable higher frame rates.<br/><br/>Visual "
|
||||
"artifacts, animation glitches, or instability may occur.",
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.enableDepthOfField,
|
||||
{
|
||||
.key = "Enable Depth of Field",
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.enableMapBackground,
|
||||
{
|
||||
.key = "Enable Mini-Map Shadows",
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Reorganize all of this?
|
||||
add_tab("Interface", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.enableAchievementNotifications,
|
||||
{
|
||||
.key = "Enable Achievement Notifications",
|
||||
.helpText = "Display a toast when an achievement is unlocked.",
|
||||
});
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
config_bool_select(leftPane, rightPane, getSettings().backend.enableCrashReporting,
|
||||
{
|
||||
.key = "Enable Crash Reporting",
|
||||
.helpText = "Enable automatic reporting of crashes to the developers.<br/><br/>"
|
||||
"Submissions include logs which may contain sensitive information. Refrain from "
|
||||
"enabling reporting if you do not agree with the following inclusions:<br/><br/> "
|
||||
"- Operating System<br/>- CPU Architecture<br/>- GPU Model & Driver Version<br/>"
|
||||
"- Account Username"
|
||||
});
|
||||
#endif
|
||||
leftPane.add_section("Advanced");
|
||||
config_bool_select(leftPane, rightPane, getSettings().backend.skipPreLaunchUI,
|
||||
{
|
||||
.key = "Skip Pre-Launch UI",
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().backend.showPipelineCompilation,
|
||||
{
|
||||
.key = "Show Pipeline Compilation",
|
||||
.helpText = "Show an overlay when shaders are being compiled for your hardware."
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include "window.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class SettingsWindow : public Window {
|
||||
public:
|
||||
SettingsWindow();
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,137 @@
|
||||
#include "string_button.hpp"
|
||||
|
||||
#include <aurora/rmlui.hpp>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
BaseStringButton::BaseStringButton(Rml::Element* parent, Props props)
|
||||
: BaseControlledSelectButton(parent, {std::move(props.key)}), mType(std::move(props.type)),
|
||||
mMaxLength(props.maxLength) {
|
||||
mInputListeners.reserve(3);
|
||||
}
|
||||
|
||||
void BaseStringButton::update() {
|
||||
if (mPendingStopEditing) {
|
||||
stop_editing(mPendingCommit, mPendingRefocusRoot);
|
||||
}
|
||||
if (mPendingInputFocusFrames > 0) {
|
||||
--mPendingInputFocusFrames;
|
||||
if (mPendingInputFocusFrames == 0) {
|
||||
focus_input();
|
||||
}
|
||||
}
|
||||
BaseControlledSelectButton::update();
|
||||
}
|
||||
|
||||
void BaseStringButton::start_editing() {
|
||||
if (mInputElem != nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create input element
|
||||
auto* doc = mRoot->GetOwnerDocument();
|
||||
auto elemPtr = doc->CreateElement("input");
|
||||
mInputElem = rmlui_dynamic_cast<Rml::ElementFormControlInput*>(elemPtr.get());
|
||||
if (mInputElem == nullptr) {
|
||||
return;
|
||||
}
|
||||
mInputElem->SetAttribute("type", mType);
|
||||
mInputElem->SetAttribute("value", input_value());
|
||||
if (mMaxLength > -1) {
|
||||
mInputElem->SetAttribute("maxlength", mMaxLength);
|
||||
}
|
||||
mRoot->AppendChild(std::move(elemPtr));
|
||||
|
||||
// Hide value element
|
||||
mValueElem->SetProperty(Rml::PropertyId::Visibility, Rml::Style::Visibility::Hidden);
|
||||
|
||||
// RmlUi lays out the new input during render. Wait one full frame before focusing it so
|
||||
// mobile keyboard placement gets a valid caret rectangle.
|
||||
mPendingInputFocusFrames = 2;
|
||||
|
||||
// Dispatch a submit event so the pane can handle item selection
|
||||
// However, mark it as "handled" to ensure that we don't steal focus away
|
||||
mRoot->DispatchEvent(Rml::EventId::Submit, {{"handled", Rml::Variant{true}}});
|
||||
|
||||
// Register input listeners
|
||||
mInputListeners.emplace_back(std::make_unique<ScopedEventListener>(
|
||||
mInputElem, Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
request_stop_editing(true, true);
|
||||
event.StopImmediatePropagation();
|
||||
} else if (cmd == NavCommand::Cancel) {
|
||||
request_stop_editing(false, true);
|
||||
event.StopImmediatePropagation();
|
||||
}
|
||||
}));
|
||||
mInputListeners.emplace_back(std::make_unique<ScopedEventListener>(
|
||||
mInputElem, Rml::EventId::Click, [](Rml::Event& event) { event.StopPropagation(); }));
|
||||
mInputListeners.emplace_back(std::make_unique<ScopedEventListener>(mInputElem,
|
||||
Rml::EventId::Blur, [this](Rml::Event&) { request_stop_editing(true, false); }));
|
||||
}
|
||||
|
||||
void BaseStringButton::request_stop_editing(bool commit, bool refocusRoot) {
|
||||
mPendingStopEditing = true;
|
||||
mPendingCommit = commit;
|
||||
mPendingRefocusRoot = refocusRoot;
|
||||
}
|
||||
|
||||
bool BaseStringButton::handle_nav_command(NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
if (mInputElem == nullptr) {
|
||||
start_editing();
|
||||
} else {
|
||||
request_stop_editing(true, true);
|
||||
}
|
||||
return true;
|
||||
} else if (cmd == NavCommand::Cancel) {
|
||||
if (mInputElem != nullptr) {
|
||||
request_stop_editing(false, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BaseStringButton::focus_input() {
|
||||
if (mInputElem == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
aurora::rmlui::set_input_type(
|
||||
mType == "number" ? aurora::rmlui::InputType::Number : aurora::rmlui::InputType::Text);
|
||||
|
||||
if (mInputElem->Focus(true)) {
|
||||
const int end = static_cast<int>(Rml::StringUtilities::LengthUTF8(mInputElem->GetValue()));
|
||||
mInputElem->SetSelectionRange(0, end);
|
||||
}
|
||||
}
|
||||
|
||||
void BaseStringButton::stop_editing(bool commit, bool refocusRoot) {
|
||||
mPendingStopEditing = false;
|
||||
mPendingInputFocusFrames = 0;
|
||||
if (mInputElem == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (commit) {
|
||||
set_value(mInputElem->GetValue());
|
||||
}
|
||||
mInputListeners.clear();
|
||||
mRoot->RemoveChild(mInputElem);
|
||||
mInputElem = nullptr;
|
||||
|
||||
// Restore value element
|
||||
mValueElem->SetProperty(Rml::PropertyId::Visibility, Rml::Style::Visibility::Visible);
|
||||
|
||||
set_selected(false);
|
||||
if (refocusRoot) {
|
||||
mRoot->Focus(true);
|
||||
}
|
||||
}
|
||||
|
||||
StringButton::StringButton(Rml::Element* parent, Props props)
|
||||
: BaseStringButton(parent, {.key = std::move(props.key), .maxLength = props.maxLength}),
|
||||
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)) {}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include "select_button.hpp"
|
||||
|
||||
#include <RmlUi/Config/Config.h>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class BaseStringButton : public BaseControlledSelectButton {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
Rml::String type = "text";
|
||||
int maxLength = -1;
|
||||
};
|
||||
|
||||
BaseStringButton(Rml::Element* parent, Props props);
|
||||
void update() override;
|
||||
void start_editing();
|
||||
void request_stop_editing(bool commit, bool refocusRoot);
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(NavCommand cmd) override;
|
||||
virtual void set_value(Rml::String value) = 0;
|
||||
virtual Rml::String input_value() { return format_value(); }
|
||||
|
||||
private:
|
||||
void focus_input();
|
||||
void stop_editing(bool commit = true, bool refocusRoot = false);
|
||||
|
||||
Rml::ElementFormControlInput* mInputElem = nullptr;
|
||||
std::vector<std::unique_ptr<ScopedEventListener> > mInputListeners;
|
||||
Rml::String mType;
|
||||
int mMaxLength;
|
||||
int mPendingInputFocusFrames = 0;
|
||||
bool mPendingStopEditing = false;
|
||||
bool mPendingCommit = true;
|
||||
bool mPendingRefocusRoot = false;
|
||||
};
|
||||
|
||||
class StringButton : public BaseStringButton {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
std::function<Rml::String()> getValue;
|
||||
std::function<void(Rml::String)> setValue;
|
||||
int maxLength = -1;
|
||||
};
|
||||
|
||||
StringButton(Rml::Element* parent, Props props);
|
||||
|
||||
protected:
|
||||
Rml::String format_value() override { return mGetValue(); }
|
||||
void set_value(Rml::String value) override {
|
||||
if (mSetValue) {
|
||||
mSetValue(std::move(value));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<Rml::String()> mGetValue;
|
||||
std::function<void(Rml::String)> mSetValue;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user