mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-06-19 22:33:04 -04:00
Merge branch 'main' of https://github.com/TwilitRealm/dusk into randomizer
This commit is contained in:
+2
-1
@@ -102,6 +102,7 @@ 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)
|
||||
target_compile_definitions(aurora_mtx PRIVATE MTX_USE_PS=1)
|
||||
|
||||
add_subdirectory(libs/freeverb)
|
||||
|
||||
@@ -285,7 +286,7 @@ set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors")
|
||||
source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES})
|
||||
source_group("dusk" FILES ${DUSK_FILES})
|
||||
|
||||
set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0)
|
||||
set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 MTX_USE_PS=1)
|
||||
|
||||
set(GAME_INCLUDE_DIRS
|
||||
include
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 85 KiB |
Vendored
+1
-1
Submodule extern/aurora updated: 4cd8d2f009...18dc51234f
@@ -1452,7 +1452,6 @@ set(DUSK_FILES
|
||||
src/dusk/imgui/ImGuiProcessOverlay.cpp
|
||||
src/dusk/imgui/ImGuiCameraOverlay.cpp
|
||||
src/dusk/imgui/ImGuiHeapOverlay.cpp
|
||||
src/dusk/imgui/ImGuiDebugPad.cpp
|
||||
src/dusk/imgui/ImGuiControllerOverlay.cpp
|
||||
src/dusk/imgui/ImGuiStubLog.cpp
|
||||
src/dusk/imgui/ImGuiMapLoader.cpp
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
|
||||
struct RoomEntry {
|
||||
u8 roomNo;
|
||||
std::vector<s16> roomPoints = {};
|
||||
|
||||
+1
-137
@@ -2,9 +2,6 @@
|
||||
#define _SRC_DUSK_MATH_H_
|
||||
|
||||
#include <cmath>
|
||||
#include <array>
|
||||
#include <limits>
|
||||
#include <bit>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846f
|
||||
@@ -19,139 +16,6 @@ inline float i_cosf(float x) { return cos(x); }
|
||||
inline float i_tanf(float x) { return tan(x); }
|
||||
inline float i_acosf(float x) { return acos(x); }
|
||||
|
||||
|
||||
// frsqrte matching courtesy of Geotale, with reference to https://achurch.org/cpu-tests/ppc750cl.s
|
||||
|
||||
struct BaseAndDec32 {
|
||||
uint32_t base;
|
||||
int32_t dec;
|
||||
};
|
||||
|
||||
struct BaseAndDec64 {
|
||||
uint64_t base;
|
||||
int64_t dec;
|
||||
};
|
||||
|
||||
union c32 {
|
||||
constexpr c32(const float p) {
|
||||
f = p;
|
||||
}
|
||||
|
||||
constexpr c32(const uint32_t p) {
|
||||
u = p;
|
||||
}
|
||||
|
||||
uint32_t u;
|
||||
float f;
|
||||
};
|
||||
|
||||
union c64 {
|
||||
constexpr c64(const double p) {
|
||||
f = p;
|
||||
}
|
||||
|
||||
constexpr c64(const uint64_t p) {
|
||||
u = p;
|
||||
}
|
||||
|
||||
uint64_t u;
|
||||
double f;
|
||||
};
|
||||
|
||||
static constexpr uint64_t EXPONENT_SHIFT_F64 = 52;
|
||||
static constexpr uint64_t MANTISSA_MASK_F64 = 0x000fffffffffffffULL;
|
||||
static constexpr uint64_t EXPONENT_MASK_F64 = 0x7ff0000000000000ULL;
|
||||
static constexpr uint64_t SIGN_MASK_F64 = 0x8000000000000000ULL;
|
||||
|
||||
static constexpr std::array<BaseAndDec64, 32> RSQRTE_TABLE = {{
|
||||
{0x69fa000000000ULL, -0x15a0000000LL},
|
||||
{0x5f2e000000000ULL, -0x13cc000000LL},
|
||||
{0x554a000000000ULL, -0x1234000000LL},
|
||||
{0x4c30000000000ULL, -0x10d4000000LL},
|
||||
{0x43c8000000000ULL, -0x0f9c000000LL},
|
||||
{0x3bfc000000000ULL, -0x0e88000000LL},
|
||||
{0x34b8000000000ULL, -0x0d94000000LL},
|
||||
{0x2df0000000000ULL, -0x0cb8000000LL},
|
||||
{0x2794000000000ULL, -0x0bf0000000LL},
|
||||
{0x219c000000000ULL, -0x0b40000000LL},
|
||||
{0x1bfc000000000ULL, -0x0aa0000000LL},
|
||||
{0x16ae000000000ULL, -0x0a0c000000LL},
|
||||
{0x11a8000000000ULL, -0x0984000000LL},
|
||||
{0x0ce6000000000ULL, -0x090c000000LL},
|
||||
{0x0862000000000ULL, -0x0898000000LL},
|
||||
{0x0416000000000ULL, -0x082c000000LL},
|
||||
{0xffe8000000000ULL, -0x1e90000000LL},
|
||||
{0xf0a4000000000ULL, -0x1c00000000LL},
|
||||
{0xe2a8000000000ULL, -0x19c0000000LL},
|
||||
{0xd5c8000000000ULL, -0x17c8000000LL},
|
||||
{0xc9e4000000000ULL, -0x1610000000LL},
|
||||
{0xbedc000000000ULL, -0x1490000000LL},
|
||||
{0xb498000000000ULL, -0x1330000000LL},
|
||||
{0xab00000000000ULL, -0x11f8000000LL},
|
||||
{0xa204000000000ULL, -0x10e8000000LL},
|
||||
{0x9994000000000ULL, -0x0fe8000000LL},
|
||||
{0x91a0000000000ULL, -0x0f08000000LL},
|
||||
{0x8a1c000000000ULL, -0x0e38000000LL},
|
||||
{0x8304000000000ULL, -0x0d78000000LL},
|
||||
{0x7c48000000000ULL, -0x0cc8000000LL},
|
||||
{0x75e4000000000ULL, -0x0c28000000LL},
|
||||
{0x6fd0000000000ULL, -0x0b98000000LL},
|
||||
}};
|
||||
|
||||
[[nodiscard]] static inline double frsqrte(const double val) {
|
||||
c64 bits(val);
|
||||
|
||||
uint64_t mantissa = bits.u & MANTISSA_MASK_F64;
|
||||
int64_t exponent = bits.u & EXPONENT_MASK_F64;
|
||||
bool sign = (bits.u & SIGN_MASK_F64) != 0;
|
||||
|
||||
// Handle 0 case
|
||||
if (mantissa == 0 && exponent == 0) {
|
||||
return std::copysign(std::numeric_limits<double>::infinity(), bits.f);
|
||||
}
|
||||
|
||||
// Handle NaN-like
|
||||
if (exponent == EXPONENT_MASK_F64) {
|
||||
if (mantissa == 0) {
|
||||
return sign ? std::numeric_limits<double>::quiet_NaN() : 0.0;
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
// Handle negative inputs
|
||||
if (sign) {
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
|
||||
if (exponent == 0) {
|
||||
// Shift so one bit goes to where the exponent would be,
|
||||
// then clear that bit to mimic a not-subnormal number!
|
||||
// Aka, if there are 12 leading zeroes, shift left once
|
||||
uint32_t shift = std::countl_zero(mantissa) - static_cast<uint32_t>(63 - EXPONENT_SHIFT_F64);
|
||||
|
||||
mantissa <<= shift;
|
||||
mantissa &= MANTISSA_MASK_F64;
|
||||
// The shift is subtracted by 1 because denormals by default
|
||||
// are offset by 1 (exponent 0 doesn't have implied 1 bit)
|
||||
exponent -= static_cast<int64_t>(shift - 1) << EXPONENT_SHIFT_F64;
|
||||
}
|
||||
|
||||
// In reality this doesn't get the full exponent -- Only the least significant bit
|
||||
// Only that's needed because square roots of higher exponent bits simply multiply the
|
||||
// result by 2!!
|
||||
uint32_t key = static_cast<uint32_t>((static_cast<uint64_t>(exponent) | mantissa) >> 37);
|
||||
uint64_t new_exp =
|
||||
(static_cast<uint64_t>((0xbfcLL << EXPONENT_SHIFT_F64) - exponent) >> 1) & EXPONENT_MASK_F64;
|
||||
|
||||
// Remove the bits relating to anything higher than the LSB of the exponent
|
||||
const auto &entry = RSQRTE_TABLE[0x1f & (key >> 11)];
|
||||
|
||||
// The result is given by an estimate then an adjustment based on the original
|
||||
// key that was computed
|
||||
uint64_t new_mantissa = static_cast<uint64_t>(entry.base + entry.dec * static_cast<int64_t>(key & 0x7ff));
|
||||
|
||||
return c64(new_exp | new_mantissa).f;
|
||||
}
|
||||
#include <dolphin/ppc_math.h>
|
||||
|
||||
#endif // _SRC_DUSK_MATH_H_
|
||||
|
||||
+19
-1
@@ -21,6 +21,12 @@ enum class GameLanguage : u8 {
|
||||
Italian = OS_LANGUAGE_ITALIAN,
|
||||
};
|
||||
|
||||
enum class DiscVerificationState : u8 {
|
||||
Unknown = 0,
|
||||
Success,
|
||||
HashMismatch,
|
||||
};
|
||||
|
||||
namespace config {
|
||||
template <>
|
||||
struct ConfigEnumRange<BloomMode> {
|
||||
@@ -33,6 +39,12 @@ struct ConfigEnumRange<GameLanguage> {
|
||||
static constexpr auto min = GameLanguage::English;
|
||||
static constexpr auto max = GameLanguage::Italian;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct ConfigEnumRange<DiscVerificationState> {
|
||||
static constexpr auto min = DiscVerificationState::Unknown;
|
||||
static constexpr auto max = DiscVerificationState::HashMismatch;
|
||||
};
|
||||
}
|
||||
|
||||
// Persistent user settings
|
||||
@@ -45,6 +57,8 @@ struct UserSettings {
|
||||
ConfigVar<bool> enableFullscreen;
|
||||
ConfigVar<bool> enableVsync;
|
||||
ConfigVar<bool> lockAspectRatio;
|
||||
ConfigVar<bool> enableFpsOverlay;
|
||||
ConfigVar<int> fpsOverlayCorner;
|
||||
} video;
|
||||
|
||||
struct {
|
||||
@@ -85,7 +99,7 @@ struct UserSettings {
|
||||
|
||||
// Preferences
|
||||
ConfigVar<bool> enableMirrorMode;
|
||||
ConfigVar<bool> disableMainHUD;
|
||||
ConfigVar<bool> minimalHUD;
|
||||
ConfigVar<bool> pauseOnFocusLost;
|
||||
ConfigVar<bool> enableLinkDollRotation;
|
||||
ConfigVar<bool> enableAchievementNotifications;
|
||||
@@ -120,6 +134,7 @@ struct UserSettings {
|
||||
ConfigVar<bool> invertCameraYAxis;
|
||||
ConfigVar<float> freeCameraSensitivity;
|
||||
ConfigVar<bool> debugFlyCam;
|
||||
ConfigVar<bool> debugFlyCamLockEvents;
|
||||
|
||||
// Cheats
|
||||
ConfigVar<bool> infiniteHearts;
|
||||
@@ -146,16 +161,19 @@ struct UserSettings {
|
||||
// Tools
|
||||
ConfigVar<bool> speedrunMode;
|
||||
ConfigVar<bool> liveSplitEnabled;
|
||||
ConfigVar<bool> recordingMode;
|
||||
} game;
|
||||
|
||||
struct {
|
||||
ConfigVar<std::string> isoPath;
|
||||
ConfigVar<DiscVerificationState> isoVerification;
|
||||
ConfigVar<std::string> graphicsBackend;
|
||||
ConfigVar<bool> skipPreLaunchUI;
|
||||
ConfigVar<bool> showPipelineCompilation;
|
||||
ConfigVar<bool> wasPresetChosen;
|
||||
ConfigVar<bool> enableCrashReporting;
|
||||
ConfigVar<int> cardFileType;
|
||||
ConfigVar<bool> enableAdvancedSettings;
|
||||
} backend;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#ifndef J3DSTRUCT_H
|
||||
#define J3DSTRUCT_H
|
||||
|
||||
#include <cstring>
|
||||
#include <gx.h>
|
||||
#include <mtx.h>
|
||||
#include <mtx.h>
|
||||
#include "global.h"
|
||||
#include "JSystem/JMath/JMath.h"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
/**
|
||||
* @ingroup jsystem-j3d
|
||||
*
|
||||
*
|
||||
*/
|
||||
struct J3DLightInfo {
|
||||
bool operator==(J3DLightInfo& other) const;
|
||||
@@ -28,7 +28,7 @@ struct J3DLightInfo {
|
||||
|
||||
/**
|
||||
* @ingroup jsystem-j3d
|
||||
*
|
||||
*
|
||||
*/
|
||||
struct J3DTextureSRTInfo {
|
||||
// NOTE: Big endian when loaded from file!
|
||||
@@ -79,7 +79,7 @@ enum J3DTexMtxMode {
|
||||
|
||||
/**
|
||||
* @ingroup jsystem-j3d
|
||||
*
|
||||
*
|
||||
*/
|
||||
struct J3DTexMtxInfo {
|
||||
bool operator==(J3DTexMtxInfo& other) const;
|
||||
@@ -97,7 +97,7 @@ struct J3DTexMtxInfo {
|
||||
|
||||
/**
|
||||
* @ingroup jsystem-j3d
|
||||
*
|
||||
*
|
||||
*/
|
||||
struct J3DIndTexMtxInfo {
|
||||
J3DIndTexMtxInfo& operator=(J3DIndTexMtxInfo const&);
|
||||
@@ -107,7 +107,7 @@ struct J3DIndTexMtxInfo {
|
||||
|
||||
/**
|
||||
* @ingroup jsystem-j3d
|
||||
*
|
||||
*
|
||||
*/
|
||||
struct J3DFogInfo {
|
||||
bool operator==(J3DFogInfo&) const;
|
||||
@@ -126,7 +126,7 @@ struct J3DFogInfo {
|
||||
|
||||
/**
|
||||
* @ingroup jsystem-j3d
|
||||
*
|
||||
*
|
||||
*/
|
||||
struct J3DNBTScaleInfo {
|
||||
bool operator==(const J3DNBTScaleInfo& other) const;
|
||||
@@ -153,7 +153,7 @@ struct J3DIndTexOrderInfo {
|
||||
|
||||
/**
|
||||
* @ingroup jsystem-j3d
|
||||
*
|
||||
*
|
||||
*/
|
||||
struct J3DTevSwapModeInfo {
|
||||
/* 0x0 */ u8 mRasSel;
|
||||
@@ -164,7 +164,7 @@ struct J3DTevSwapModeInfo {
|
||||
|
||||
/**
|
||||
* @ingroup jsystem-j3d
|
||||
*
|
||||
*
|
||||
*/
|
||||
struct J3DTevSwapModeTableInfo {
|
||||
/* 0x0 */ u8 field_0x0;
|
||||
@@ -175,7 +175,7 @@ struct J3DTevSwapModeTableInfo {
|
||||
|
||||
/**
|
||||
* @ingroup jsystem-j3d
|
||||
*
|
||||
*
|
||||
*/
|
||||
struct J3DTevStageInfo {
|
||||
/* 0x0 */ u8 field_0x0;
|
||||
@@ -202,7 +202,7 @@ struct J3DTevStageInfo {
|
||||
|
||||
/**
|
||||
* @ingroup jsystem-j3d
|
||||
*
|
||||
*
|
||||
*/
|
||||
struct J3DIndTevStageInfo {
|
||||
/* 0x0 */ u8 mIndStage;
|
||||
@@ -219,7 +219,7 @@ struct J3DIndTevStageInfo {
|
||||
|
||||
/**
|
||||
* @ingroup jsystem-j3d
|
||||
*
|
||||
*
|
||||
*/
|
||||
struct J3DTexCoordInfo {
|
||||
/* 0x0 */ u8 mTexGenType;
|
||||
@@ -265,7 +265,7 @@ struct J3DBlendInfo {
|
||||
|
||||
/**
|
||||
* @ingroup jsystem-j3d
|
||||
*
|
||||
*
|
||||
*/
|
||||
struct J3DTevOrderInfo {
|
||||
void operator=(const J3DTevOrderInfo& other) {
|
||||
|
||||
+117
-5
@@ -20,18 +20,22 @@ body {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
fps,
|
||||
toast {
|
||||
position: absolute;
|
||||
border: 1dp #92875B;
|
||||
background-color: rgba(21, 22, 16, 80%);
|
||||
}
|
||||
|
||||
toast {
|
||||
top: 40dp;
|
||||
right: 40dp;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
border-radius: 14dp;
|
||||
overflow: hidden;
|
||||
border: 1dp #92875B;
|
||||
backdrop-filter: blur(5dp);
|
||||
box-shadow: 0 0 15dp 3dp;
|
||||
background-color: rgba(21, 22, 16, 80%);
|
||||
filter: opacity(0);
|
||||
transform: scale(0.9);
|
||||
transform-origin: center;
|
||||
@@ -65,13 +69,31 @@ toast heading {
|
||||
color: #92875B;
|
||||
}
|
||||
|
||||
toast message {
|
||||
toast heading > span {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
toast heading > row {
|
||||
flex: 1 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
gap: 4dp;
|
||||
}
|
||||
|
||||
toast message {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: 8dp;
|
||||
}
|
||||
|
||||
toast message row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
toast message row.muted {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
toast progress {
|
||||
height: 4dp;
|
||||
position: absolute;
|
||||
@@ -92,6 +114,47 @@ toast.achievement heading {
|
||||
color: #C2A42D;
|
||||
}
|
||||
|
||||
toast.controller-warning {
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: 40dp;
|
||||
left: 50%;
|
||||
width: 440dp;
|
||||
max-width: 90%;
|
||||
transform: translateX(-50%) scale(0.9);
|
||||
}
|
||||
|
||||
toast.controller-warning[open] {
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
|
||||
toast.controller-warning heading {
|
||||
color: #C2A42D;
|
||||
}
|
||||
|
||||
toast.menu-notification {
|
||||
top: 40dp;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
left: 50%;
|
||||
max-width: 90%;
|
||||
transform: translateX(-50%) scale(0.9);
|
||||
}
|
||||
|
||||
toast.menu-notification[open] {
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
|
||||
toast.menu-notification message {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
toast.menu-notification message row {
|
||||
align-items: center;
|
||||
gap: 6dp;
|
||||
}
|
||||
|
||||
icon {
|
||||
font-family: "Material Symbols Rounded";
|
||||
font-weight: normal;
|
||||
@@ -113,6 +176,55 @@ icon.trophy {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
icon.controller {
|
||||
width: 24dp;
|
||||
height: 24dp;
|
||||
font-size: 24dp;
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
icon.warning {
|
||||
width: 24dp;
|
||||
height: 24dp;
|
||||
font-size: 24dp;
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
fps {
|
||||
display: none;
|
||||
z-index: 99;
|
||||
font-size: 18dp;
|
||||
font-weight: bold;
|
||||
padding: 9dp 12dp;
|
||||
border-radius: 7dp;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
fps[open] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
fps[corner=tl] {
|
||||
top: 12dp;
|
||||
left: 12dp;
|
||||
}
|
||||
|
||||
fps[corner=tr] {
|
||||
top: 12dp;
|
||||
right: 12dp;
|
||||
}
|
||||
|
||||
fps[corner=bl] {
|
||||
bottom: 12dp;
|
||||
left: 12dp;
|
||||
}
|
||||
|
||||
fps[corner=br] {
|
||||
bottom: 12dp;
|
||||
right: 12dp;
|
||||
}
|
||||
|
||||
logo {
|
||||
position: absolute;
|
||||
width: 100dp;
|
||||
@@ -161,4 +273,4 @@ logo img.outer {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+125
-2
@@ -134,8 +134,14 @@ hero img {
|
||||
decorator: horizontal-gradient(#00000000 #00000000);
|
||||
}
|
||||
|
||||
#menu-list button:disabled {
|
||||
opacity: 0.75;
|
||||
cursor: default;
|
||||
decorator: horizontal-gradient(#00000000 #00000000);
|
||||
}
|
||||
|
||||
#menu-list button.anim-done {
|
||||
transition: decorator color 0.1s linear-in-out;
|
||||
transition: decorator color opacity 0.1s linear-in-out;
|
||||
}
|
||||
|
||||
#menu-list button:hover,
|
||||
@@ -210,8 +216,24 @@ body.mirrored version-info {
|
||||
color: #FFC9C9;
|
||||
}
|
||||
|
||||
#disc-status[status=verifying] {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
#disc-status[status=mismatch] {
|
||||
color: #FFD6A7;
|
||||
}
|
||||
|
||||
#disc-status[status=unknown] {
|
||||
color: rgba(224, 219, 200, 65%);
|
||||
}
|
||||
|
||||
#disc-status[status=pending] {
|
||||
color: #FEE685;
|
||||
}
|
||||
|
||||
#disc-status icon {
|
||||
display: block;
|
||||
display: none;
|
||||
width: 24dp;
|
||||
height: 24dp;
|
||||
font-family: "Material Symbols Rounded";
|
||||
@@ -219,6 +241,10 @@ body.mirrored version-info {
|
||||
font-size: 24dp;
|
||||
}
|
||||
|
||||
#disc-status[status] icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#disc-status[status=good] icon {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
@@ -227,6 +253,22 @@ body.mirrored version-info {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
#disc-status[status=verifying] icon {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
#disc-status[status=mismatch] icon {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
#disc-status[status=unknown] icon {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
#disc-status[status=pending] icon {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
#disc-version {
|
||||
font-size: 20dp;
|
||||
}
|
||||
@@ -280,3 +322,84 @@ body.animate-in .intro-item {
|
||||
.delay-5 {
|
||||
transition: opacity transform 0.3s 0.6s cubic-in-out;
|
||||
}
|
||||
|
||||
/* Mobile layout */
|
||||
@media (max-height: 640dp) {
|
||||
.gradient {
|
||||
decorator: horizontal-gradient(#00000000 #000000FF);
|
||||
}
|
||||
|
||||
body.mirrored .gradient {
|
||||
decorator: horizontal-gradient(#000000FF #00000000);
|
||||
}
|
||||
|
||||
menu {
|
||||
left: 20dp;
|
||||
right: 20dp;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16dp;
|
||||
}
|
||||
|
||||
body.mirrored menu {
|
||||
left: 20dp;
|
||||
right: 20dp;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
hero {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
max-width: 48%;
|
||||
|
||||
}
|
||||
|
||||
body.mirrored hero {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
hero img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#menu-list {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
max-width: 52%;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
#menu-list button {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#menu-list button:hover,
|
||||
#menu-list button:focus-visible {
|
||||
decorator: horizontal-gradient(#FEE68500 #FEE685FF);
|
||||
}
|
||||
|
||||
body.mirrored #menu-list {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
body.mirrored #menu-list button {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
body.mirrored #menu-list button:hover,
|
||||
body.mirrored #menu-list button:focus-visible {
|
||||
decorator: horizontal-gradient(#FEE685FF #FEE68500);
|
||||
}
|
||||
|
||||
.eyebrow,
|
||||
disc-info,
|
||||
version-info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
+104
-51
@@ -39,12 +39,8 @@ window.small {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
window.preset {
|
||||
min-width: 650dp;
|
||||
}
|
||||
|
||||
window.modal {
|
||||
max-width: 816dp;
|
||||
max-width: 640dp;
|
||||
}
|
||||
|
||||
window[open] {
|
||||
@@ -116,12 +112,6 @@ window content pane > spacer {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
window modal {
|
||||
padding: 32dp;
|
||||
gap: 20dp;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
scrollbarvertical {
|
||||
width: 8dp;
|
||||
margin: 4dp 4dp 4dp 0;
|
||||
@@ -209,9 +199,8 @@ button:not(:disabled):active {
|
||||
}
|
||||
|
||||
button.modal-btn {
|
||||
font-size: 20dp;
|
||||
padding: 16dp 10dp;
|
||||
flex: 1 1 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
select-button {
|
||||
@@ -274,6 +263,8 @@ select-button input {
|
||||
}
|
||||
|
||||
icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-family: "Material Symbols Rounded";
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
@@ -281,10 +272,23 @@ icon {
|
||||
}
|
||||
|
||||
icon.warning {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
decorator: text("" center center);
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
icon.error {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
icon.verifying {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
icon.celebration {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
icon.question-mark {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
.achievement-row {
|
||||
@@ -343,7 +347,7 @@ icon.warning {
|
||||
color: rgba(224, 219, 200, 45%);
|
||||
}
|
||||
|
||||
progressbar {
|
||||
progress {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 6dp;
|
||||
@@ -352,12 +356,12 @@ progressbar {
|
||||
margin: 6dp 0 2dp 0;
|
||||
}
|
||||
|
||||
progressbar.progress-done fill {
|
||||
progress.progress-done fill {
|
||||
background-color: #44aa22;
|
||||
border-radius: 3dp;
|
||||
}
|
||||
|
||||
progressbar.progress-ongoing fill {
|
||||
progress.progress-ongoing fill {
|
||||
background-color: #2255bb;
|
||||
border-radius: 3dp;
|
||||
}
|
||||
@@ -370,35 +374,13 @@ button.achievement-clear {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.preset-dialog {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
padding: 32dp;
|
||||
gap: 20dp;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.preset-title {
|
||||
display: block;
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
font-size: 30dp;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preset-intro {
|
||||
display: block;
|
||||
font-size: 18dp;
|
||||
text-align: center;
|
||||
color: rgba(224, 219, 200, 65%);
|
||||
}
|
||||
|
||||
.preset-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20dp;
|
||||
flex: 0 1 auto;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.preset-col {
|
||||
@@ -408,33 +390,104 @@ button.achievement-clear {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
button.preset-btn {
|
||||
font-size: 22dp;
|
||||
padding: 20dp 16dp;
|
||||
}
|
||||
|
||||
.preset-desc {
|
||||
display: block;
|
||||
font-size: 16dp;
|
||||
color: rgba(224, 219, 200, 65%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
padding: 16dp;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 24dp;
|
||||
gap: 20dp;
|
||||
flex: 0 1 auto;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
window.modal.danger {
|
||||
border: 2dp #852221;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
gap: 16dp;
|
||||
}
|
||||
|
||||
.modal-header icon {
|
||||
font-size: 24dp;
|
||||
color: #92875B;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
display: block;
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-size: 18dp;
|
||||
color: #92875B;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
window.modal.danger .modal-title,
|
||||
window.modal.danger .modal-header icon {
|
||||
color: #B3261E;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: block;
|
||||
width: 100%;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
font-size: 20dp;
|
||||
color: #FFFFFF;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.modal-body span.tip {
|
||||
font-size: 14dp;
|
||||
color: #92875B;
|
||||
}
|
||||
|
||||
.verification-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10dp;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.verification-file {
|
||||
display: block;
|
||||
font-size: 17dp;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
progress.verification-progress-bar {
|
||||
height: 8dp;
|
||||
margin: 2dp 0 0 0;
|
||||
}
|
||||
|
||||
.verification-detail {
|
||||
display: block;
|
||||
font-size: 14dp;
|
||||
color: rgba(224, 219, 200, 65%);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: stretch;
|
||||
align-items: stretch;
|
||||
gap: 12dp;
|
||||
padding-top: 12dp;
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
padding-top: 4dp;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ void daAlink_c::setOriginalHeap(JKRExpHeap** i_ppheap, u32 i_size) {
|
||||
u32 var_r28 = 0x10;
|
||||
u32 size = ROUND(i_size, 16);
|
||||
#if TARGET_PC
|
||||
size *= 2;
|
||||
size *= 20; // Increase Link's heap size to prevent mods from crashing with higher-quality models.
|
||||
#endif
|
||||
JKRHeap* parent = mDoExt_getGameHeap();
|
||||
|
||||
|
||||
@@ -254,7 +254,11 @@ BOOL daBdoor_c::checkArea() {
|
||||
if (fabsf(vec.z) > 100.0f) {
|
||||
return false;
|
||||
}
|
||||
#ifdef TARGET_PC
|
||||
return (s16)((s32)fabs(current.angle.y - 0x7fff - player->current.angle.y) & 0xffff) <= 0x4000 ? 1 : 0;
|
||||
#else
|
||||
return (s16)fabs((f64)(current.angle.y - 0x7fff - player->current.angle.y)) <= 0x4000 ? 1 : 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
BOOL daBdoor_c::checkFront() {
|
||||
|
||||
@@ -825,7 +825,11 @@ int daBdoorL1_c::checkArea() {
|
||||
if (fabsf(local_48.z) > 100.0f) {
|
||||
return 0;
|
||||
}
|
||||
#ifdef TARGET_PC
|
||||
if ((s16)((s32)fabs(current.angle.y - 0x7fff - player->current.angle.y) & 0xffff) <= 0x4000) {
|
||||
#else
|
||||
if ((s16)fabs((f64)(current.angle.y - 0x7fff - player->current.angle.y)) <= 0x4000) {
|
||||
#endif
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
|
||||
@@ -348,7 +348,11 @@ int daBdoorL5_c::checkArea() {
|
||||
if (fabsf(local_48.z) > 100.0f) {
|
||||
return 0;
|
||||
}
|
||||
#ifdef TARGET_PC
|
||||
if ((s16)((s32)fabs(current.angle.y - 0x7fff - player->current.angle.y) & 0xffff) <= 0x4000) {
|
||||
#else
|
||||
if ((s16)fabs((f64)(current.angle.y - 0x7fff - player->current.angle.y)) <= 0x4000) {
|
||||
#endif
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
|
||||
@@ -1317,8 +1317,12 @@ int daMBdoorL1_c::checkArea() {
|
||||
if (fabsf(local_48.z) > 110.0f) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
#ifdef TARGET_PC
|
||||
if ((s16)((s32)fabs(angle - 0x7fff - player->current.angle.y) & 0xffff) > 0x4000) {
|
||||
#else
|
||||
if ((s16)fabs((f64)(angle - 0x7fff - player->current.angle.y)) > 0x4000) {
|
||||
#endif
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
#include "SSystem/SComponent/c_counter.h"
|
||||
#include <cstring>
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/settings.h"
|
||||
#endif
|
||||
|
||||
#define DRAW_TYPE_YELLOW 0
|
||||
#define DRAW_TYPE_RED 1
|
||||
|
||||
@@ -1422,6 +1426,11 @@ int dAttention_c::Run() {
|
||||
}
|
||||
|
||||
void dAttention_c::Draw() {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.recordingMode) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (mAttParam.CheckFlag(dAttParam_c::EFlag_ARROW_OFF)) {
|
||||
draw[0].field_0x173 = 3;
|
||||
draw[1].field_0x173 = 3;
|
||||
|
||||
+63
-11
@@ -31,6 +31,7 @@
|
||||
#if TARGET_PC
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/logging.h"
|
||||
#include "imgui.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
@@ -7483,6 +7484,8 @@ static constexpr f32 FLYCAM_SPEED = 0.5f;
|
||||
static constexpr f32 FLYCAM_FAST_SPEED = 4.0f;
|
||||
static constexpr f32 FLYCAM_ROTATION_SPEED = 0.002f;
|
||||
static constexpr f32 FLYCAM_TRIGGER_DEADZONE = 20.0f;
|
||||
static constexpr s16 FLYCAM_ROLL_SPEED = 256;
|
||||
static ImVec2 sFlyCamLastMousePos = {-1.f, -1.f};
|
||||
|
||||
#if TARGET_PC
|
||||
bool dCamera_c::executeDebugFlyCam() {
|
||||
@@ -7490,6 +7493,7 @@ bool dCamera_c::executeDebugFlyCam() {
|
||||
if (mDebugFlyCam.initialized) {
|
||||
deactivateDebugFlyCam();
|
||||
}
|
||||
sFlyCamLastMousePos = {-1.f, -1.f};
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7519,16 +7523,63 @@ bool dCamera_c::executeDebugFlyCam() {
|
||||
mDebugFlyCam.initialized = true;
|
||||
}
|
||||
|
||||
event->mEventStatus = 1;
|
||||
dComIfGp_getEventManager().setCameraPlay(1);
|
||||
if (dusk::getSettings().game.debugFlyCamLockEvents) {
|
||||
event->mEventStatus = 1;
|
||||
dComIfGp_getEventManager().setCameraPlay(1);
|
||||
} else {
|
||||
if (event->mEventStatus != 0) {
|
||||
event->mEventStatus = 0;
|
||||
}
|
||||
dComIfGp_getEventManager().setCameraPlay(0);
|
||||
}
|
||||
|
||||
interface_of_controller_pad& pad = mDoCPd_c::getCpadInfo(0);
|
||||
f32 stickY = pad.mMainStickPosY * 72.0f;
|
||||
f32 stickX = pad.mMainStickPosX * 72.0f;
|
||||
f32 cStickY = pad.mCStickPosY * 59.0f;
|
||||
f32 cStickX = pad.mCStickPosX * 59.0f;
|
||||
f32 trigL = pad.mTriggerLeft * 150.0f;
|
||||
f32 trigR = pad.mTriggerRight * 150.0f;
|
||||
f32 stickY = 0.f;
|
||||
f32 stickX = 0.f;
|
||||
f32 cStickY = 0.f;
|
||||
f32 cStickX = 0.f;
|
||||
f32 trigL = 0.f;
|
||||
f32 trigR = 0.f;
|
||||
f32 rollInput = 0.f;
|
||||
bool fast = false;
|
||||
|
||||
if (dusk::getSettings().game.debugFlyCamLockEvents) {
|
||||
interface_of_controller_pad& pad = mDoCPd_c::getCpadInfo(0);
|
||||
stickY = pad.mMainStickPosY * 72.0f;
|
||||
stickX = pad.mMainStickPosX * 72.0f;
|
||||
cStickY = pad.mCStickPosY * 59.0f;
|
||||
cStickX = pad.mCStickPosX * 59.0f;
|
||||
trigL = pad.mTriggerLeft * 150.0f;
|
||||
trigR = pad.mTriggerRight * 150.0f;
|
||||
fast = mDoCPd_c::getHoldZ(PAD_1);
|
||||
if (mDoCPd_c::getHoldY(PAD_1)) rollInput -= 1.f;
|
||||
if (mDoCPd_c::getHoldX(PAD_1)) rollInput += 1.f;
|
||||
}
|
||||
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
if (!io.WantCaptureKeyboard) {
|
||||
f32 kbX = 0.0f, kbY = 0.0f;
|
||||
if (ImGui::IsKeyDown(ImGuiKey_W) || ImGui::IsKeyDown(ImGuiKey_UpArrow)) kbY += 1.f;
|
||||
if (ImGui::IsKeyDown(ImGuiKey_S) || ImGui::IsKeyDown(ImGuiKey_DownArrow)) kbY -= 1.f;
|
||||
if (ImGui::IsKeyDown(ImGuiKey_D) || ImGui::IsKeyDown(ImGuiKey_RightArrow)) kbX += 1.f;
|
||||
if (ImGui::IsKeyDown(ImGuiKey_A) || ImGui::IsKeyDown(ImGuiKey_LeftArrow)) kbX -= 1.f;
|
||||
f32 len = sqrtf(kbX * kbX + kbY * kbY);
|
||||
if (len > 1.f) { kbX /= len; kbY /= len; }
|
||||
stickX += kbX * 72.0f;
|
||||
stickY += kbY * 72.0f;
|
||||
if (ImGui::IsKeyDown(ImGuiKey_Space)) trigR += 150.0f;
|
||||
if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl)) trigL += 150.0f;
|
||||
if (ImGui::IsKeyDown(ImGuiKey_LeftShift)) fast = true;
|
||||
if (ImGui::IsKeyDown(ImGuiKey_Q)) rollInput -= 1.0f;
|
||||
if (ImGui::IsKeyDown(ImGuiKey_E)) rollInput += 1.0f;
|
||||
}
|
||||
bool mouseValid = !io.WantCaptureMouse && io.MousePos.x >= 0.0f && io.MousePos.y >= 0.0f;
|
||||
if (mouseValid && sFlyCamLastMousePos.x >= 0.0f) {
|
||||
cStickX -= (io.MousePos.x - sFlyCamLastMousePos.x) * 2.0f;
|
||||
cStickY -= (io.MousePos.y - sFlyCamLastMousePos.y) * 2.0f;
|
||||
}
|
||||
sFlyCamLastMousePos = mouseValid ? io.MousePos : ImVec2{-1.0f, -1.0f};
|
||||
}
|
||||
|
||||
f32 verticalDisp = 0.0f;
|
||||
if (trigR >= FLYCAM_TRIGGER_DEADZONE) {
|
||||
@@ -7542,7 +7593,7 @@ bool dCamera_c::executeDebugFlyCam() {
|
||||
f32 moveDx = stickY * cosf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) - stickX * sinf(mDebugFlyCam.yaw);
|
||||
f32 moveDz = stickY * sinf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) + stickX * cosf(mDebugFlyCam.yaw);
|
||||
|
||||
f32 speed = mDoCPd_c::getHoldZ(PAD_1) ? FLYCAM_FAST_SPEED : FLYCAM_SPEED;
|
||||
f32 speed = fast ? FLYCAM_FAST_SPEED : FLYCAM_SPEED;
|
||||
|
||||
mEye.x += speed * moveDx;
|
||||
mEye.y += speed * moveDy;
|
||||
@@ -7553,6 +7604,7 @@ bool dCamera_c::executeDebugFlyCam() {
|
||||
mCenter.z = mEye.z + sinf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST;
|
||||
mCenter.y = mEye.y + sinf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST;
|
||||
|
||||
mBank = mBank + static_cast<s16>(rollInput * FLYCAM_ROLL_SPEED * (fast ? FLYCAM_FAST_SPEED / FLYCAM_SPEED : 1.f));
|
||||
Reset(mCenter, mEye);
|
||||
|
||||
f32 yawInput = dusk::getSettings().game.invertCameraXAxis ? cStickX : -cStickX;
|
||||
@@ -7570,7 +7622,7 @@ void dCamera_c::deactivateDebugFlyCam() {
|
||||
Reset(mDebugFlyCam.savedCenter, mDebugFlyCam.savedEye, mDebugFlyCam.savedFovy, mDebugFlyCam.savedBank.Val());
|
||||
|
||||
dEvt_control_c* event = dComIfGp_getEvent();
|
||||
if (event != nullptr) {
|
||||
if (event != nullptr && event->mEventStatus != 0) {
|
||||
event->mEventStatus = 0;
|
||||
}
|
||||
dComIfGp_getEventManager().setCameraPlay(0);
|
||||
|
||||
+121
-31
@@ -15,13 +15,47 @@
|
||||
#include <cstring>
|
||||
|
||||
#ifdef TARGET_PC
|
||||
#include <span>
|
||||
#include <numbers>
|
||||
#include <array>
|
||||
|
||||
constexpr u16 kMapResolutionMultiplier = 4;
|
||||
constexpr u16 kMapCircleSize = 16 * kMapResolutionMultiplier;
|
||||
constexpr u16 kMapImageSide = 16 * kMapResolutionMultiplier;
|
||||
constexpr u32 kMapImageTotalPixels = kMapImageSide * kMapImageSide;
|
||||
|
||||
typedef std::function<u8(size_t, size_t)> PaintI8Fn;
|
||||
|
||||
void paint_i8(std::span<u8> dst, size_t width, PaintI8Fn paint) {
|
||||
const auto blocksAcross = width >> 3;
|
||||
|
||||
for (size_t i = 0; i < dst.size(); i++) {
|
||||
// 8x4 block swizzling for I8
|
||||
const auto blockIdx = i >> 5;
|
||||
const auto localIdx = i & 31;
|
||||
|
||||
const auto blockY = blockIdx / blocksAcross;
|
||||
const auto blockX = blockIdx % blocksAcross;
|
||||
|
||||
const auto localY = localIdx >> 3;
|
||||
const auto localX = localIdx & 7;
|
||||
|
||||
const auto x = (blockX << 3) + localX;
|
||||
const auto y = (blockY << 2) + localY;
|
||||
|
||||
dst[i] = paint(x, y);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void dMpath_n::dTexObjAggregate_c::create() {
|
||||
static int const data[7] = {
|
||||
79, 80, 77, 78, 76, 81, 82,
|
||||
79, // 0: im_map_icon_square_4i.bti
|
||||
80, // 1: im_map_icon_tresurebox_4i.bti
|
||||
77, // 2: im_map_icon_enter_4i.bti
|
||||
78, // 3: im_map_icon_nijumaru_4i.bti
|
||||
76, // 4: im_map_icon_circle_4i.bti
|
||||
81, // 5: im_map_icon_try_force_4i.bti
|
||||
82, // 6: map_icon_circle16x16_4i.bti
|
||||
};
|
||||
|
||||
for (int lp1 = 0; lp1 < 7; lp1++) {
|
||||
@@ -35,45 +69,101 @@ void dMpath_n::dTexObjAggregate_c::create() {
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
auto hqCircle = JKR_NEW TGXTexObj();
|
||||
static bool hqTexsDrawn = false;
|
||||
|
||||
static bool hqCircleDrawn = false;
|
||||
static u8 hqCircleData[kMapCircleSize * kMapCircleSize];
|
||||
static u8 hqCircleData[kMapImageTotalPixels];
|
||||
static u8 hqCircleAltData[kMapImageTotalPixels];
|
||||
static u8 hqNijumaruData[kMapImageTotalPixels];
|
||||
static u8 hqEnterData[kMapImageTotalPixels];
|
||||
static u8 hqTryForceData[kMapImageTotalPixels];
|
||||
|
||||
if (!hqCircleDrawn) {
|
||||
const auto center = kMapCircleSize / 2.0f;
|
||||
const auto radiusSq = center * center;
|
||||
const auto blocksAcross = kMapCircleSize >> 3;
|
||||
const auto totalPixels = sizeof(hqCircleData);
|
||||
if (!hqTexsDrawn) {
|
||||
constexpr auto center = kMapImageSide / 2.0f;
|
||||
constexpr auto radiusSq = center * center;
|
||||
|
||||
for (size_t i = 0; i < totalPixels; i++) {
|
||||
// 8x4 block swizzling for I8
|
||||
const auto blockIdx = i >> 5;
|
||||
const auto localIdx = i & 31;
|
||||
// 6: map_icon_circle16x16_4i.bti - simple circle
|
||||
paint_i8(std::span{hqCircleData}, kMapImageSide, [=](auto x, auto y) {
|
||||
const auto dx = (x + 0.5f) - center;
|
||||
const auto dy = (y + 0.5f) - center;
|
||||
return (dx * dx + dy * dy < radiusSq) ? 0x11 : 0;
|
||||
});
|
||||
|
||||
const auto blockY = blockIdx / blocksAcross;
|
||||
const auto blockX = blockIdx % blocksAcross;
|
||||
|
||||
const auto localY = localIdx >> 3;
|
||||
const auto localX = localIdx & 7;
|
||||
|
||||
const auto x = (blockX << 3) + localX;
|
||||
const auto y = (blockY << 2) + localY;
|
||||
// 4: im_map_icon_circle_4i.bti - outlined circle
|
||||
paint_i8(std::span{hqCircleAltData}, kMapImageSide, [=](auto x, auto y) {
|
||||
constexpr auto innerRadius = kMapImageSide * 3.0f / 8.0f;
|
||||
constexpr auto innerRadiusSq = innerRadius * innerRadius;
|
||||
|
||||
const auto dx = (x + 0.5f) - center;
|
||||
const auto dy = (y + 0.5f) - center;
|
||||
const auto dSq = dx * dx + dy * dy;
|
||||
|
||||
// the original texture is in I4 format and uses 1 to indicate if inside the circle
|
||||
// so we scale to I8 range: 255 / 15 = 17
|
||||
hqCircleData[i] = (dx * dx + dy * dy < radiusSq) ? 17 : 0;
|
||||
}
|
||||
hqCircleDrawn = true;
|
||||
return dSq < radiusSq ? (dSq < innerRadiusSq ? 0x22 : 0x11) : 0;
|
||||
});
|
||||
|
||||
// 3: im_map_icon_nijumaru_4i.bti - concentric rings
|
||||
paint_i8(std::span{hqNijumaruData}, kMapImageSide, [=](auto x, auto y) {
|
||||
constexpr u8 nijumaruRings[] = {0x11, 0x22, 0x11, 0x11, 0x22, 0x22};
|
||||
|
||||
const auto dx = (x + 0.5f) - center;
|
||||
const auto dy = (y + 0.5f) - center;
|
||||
const auto dSq = dx * dx + dy * dy;
|
||||
|
||||
if (dSq < radiusSq) {
|
||||
const auto ringIndex =
|
||||
static_cast<size_t>(std::trunc(std::sqrt(dSq) / kMapImageSide * 12));
|
||||
return nijumaruRings[ringIndex];
|
||||
}
|
||||
return u8{0};
|
||||
});
|
||||
|
||||
// 2: im_map_icon_enter_4i.bti - outlined octagram
|
||||
paint_i8(std::span{hqEnterData}, kMapImageSide, [=](auto x, auto y) {
|
||||
constexpr auto outlineWidth = kMapImageSide / 6.0f;
|
||||
|
||||
const auto adx = std::abs((x + 0.5f) - center);
|
||||
const auto ady = std::abs((y + 0.5f) - center);
|
||||
const auto dist =
|
||||
std::min(adx + ady, std::max(adx, ady) * std::numbers::sqrt2_v<float>) -
|
||||
kMapImageSide / 2.0f;
|
||||
|
||||
return dist > 0.0f ? 0 : (dist > -outlineWidth ? 0x22 : 0x33);
|
||||
});
|
||||
|
||||
// 5: im_map_icon_try_force_4i.bti - outlined circle with triangle
|
||||
paint_i8(std::span{hqTryForceData}, kMapImageSide, [=](auto x, auto y) {
|
||||
constexpr auto innerRadiusNorm = 5.0f / 12.0f;
|
||||
constexpr auto innerRadius = kMapImageSide * innerRadiusNorm;
|
||||
constexpr auto innerRadiusSq = innerRadius * innerRadius;
|
||||
constexpr auto triRadius = kMapImageSide * innerRadiusNorm / 2.0f;
|
||||
|
||||
const auto dx = (x + 0.5f) - center;
|
||||
const auto dy = (y + 0.5f) - center;
|
||||
const auto dSq = dx * dx + dy * dy;
|
||||
const auto triSideDist = (std::numbers::sqrt3_v<float> * std::abs(dx) - dy) * 0.5f;
|
||||
const auto insideTri = std::max(dy, triSideDist) < triRadius;
|
||||
|
||||
return insideTri ? 0x22 : (dSq < radiusSq ? (dSq < innerRadiusSq ? 0x33 : 0x22) : 0);
|
||||
});
|
||||
|
||||
hqTexsDrawn = true;
|
||||
}
|
||||
|
||||
GXInitTexObj(hqCircle, hqCircleData, kMapCircleSize, kMapCircleSize, GX_TF_I8, GX_CLAMP,
|
||||
GX_CLAMP, GX_FALSE);
|
||||
GXInitTexObjLOD(hqCircle, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1);
|
||||
mp_texObj[6] = hqCircle;
|
||||
constexpr auto replacements = std::to_array<std::pair<size_t, const u8*> >({
|
||||
{2, hqEnterData},
|
||||
{3, hqNijumaruData},
|
||||
{4, hqCircleAltData},
|
||||
{5, hqTryForceData},
|
||||
{6, hqCircleData},
|
||||
});
|
||||
|
||||
for (const auto& [idx, data] : replacements) {
|
||||
JKR_DELETE(mp_texObj[idx]);
|
||||
const auto texobj = JKR_NEW TGXTexObj();
|
||||
GXInitTexObj(
|
||||
texobj, data, kMapImageSide, kMapImageSide, GX_TF_I8, GX_CLAMP, GX_CLAMP, GX_FALSE);
|
||||
GXInitTexObjLOD(texobj, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1);
|
||||
mp_texObj[idx] = texobj;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -1403,7 +1403,7 @@ void dMenu_Fmap2DBack_c::regionTextureDraw() {
|
||||
if (uVar10 != uVar9) {
|
||||
bool b = 0;
|
||||
f32 v = mTransX + (dVar14 + (mRegionMinMapX[uVar10] + field_0xf0c[uVar10]));
|
||||
|
||||
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
b = true;
|
||||
|
||||
+6
-3
@@ -24,9 +24,10 @@
|
||||
#include "d/actor/d_a_horse.h"
|
||||
#include <cstring>
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/memory.h"
|
||||
|
||||
#include "dusk/memory.h"
|
||||
#include "dusk/settings.h"
|
||||
#endif
|
||||
|
||||
int dMeter2_c::_create() {
|
||||
stage_stag_info_class* stag_info = dComIfGp_getStageStagInfo();
|
||||
@@ -317,7 +318,9 @@ int dMeter2_c::_execute() {
|
||||
|
||||
int dMeter2_c::_draw() {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.disableMainHUD) {
|
||||
if (dusk::getSettings().game.recordingMode || dusk::getSettings().game.minimalHUD ||
|
||||
dusk::getSettings().game.debugFlyCam)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include "d/d_pane_class.h"
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/settings.h"
|
||||
#endif
|
||||
|
||||
dMsgScrnArrow_c::dMsgScrnArrow_c() {
|
||||
mpScreen = JKR_NEW J2DScreen();
|
||||
JUT_ASSERT(0, mpScreen != NULL);
|
||||
@@ -65,6 +69,11 @@ dMsgScrnArrow_c::~dMsgScrnArrow_c() {
|
||||
}
|
||||
|
||||
void dMsgScrnArrow_c::draw() {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.recordingMode) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
J2DGrafContext* graf_ctx = dComIfGp_getCurrentGrafPort();
|
||||
mpScreen->draw(0.0f, 0.0f, graf_ctx);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
#include "d/d_pane_class.h"
|
||||
#include <cstring>
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/settings.h"
|
||||
#endif
|
||||
|
||||
dMsgScrnBase_c::dMsgScrnBase_c() {
|
||||
init();
|
||||
}
|
||||
@@ -57,12 +61,22 @@ void dMsgScrnBase_c::init() {
|
||||
}
|
||||
|
||||
void dMsgScrnBase_c::multiDraw() {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.recordingMode) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (field_0x48 != NULL) {
|
||||
dComIfGd_set2DOpa(field_0x48);
|
||||
}
|
||||
}
|
||||
|
||||
void dMsgScrnBase_c::draw() {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.recordingMode) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
J2DGrafContext* ctx = dComIfGp_getCurrentGrafPort();
|
||||
|
||||
ctx->setup2D();
|
||||
@@ -72,10 +86,20 @@ void dMsgScrnBase_c::draw() {
|
||||
}
|
||||
|
||||
void dMsgScrnBase_c::drawSelf() {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.recordingMode) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
drawOutFont(0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
void dMsgScrnBase_c::drawOutFont(f32 param_0, f32 param_1, f32 param_2) {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.recordingMode) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
mpOutFont->draw(NULL, param_0, param_1, param_2);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
#include "d/d_msg_object.h"
|
||||
#include "d/d_pane_class.h"
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/settings.h"
|
||||
#endif
|
||||
|
||||
dMsgScrnBoss_c::dMsgScrnBoss_c() {
|
||||
static u64 t_tag[7] = {
|
||||
MULTI_CHAR('sfontb0'), MULTI_CHAR('sfontb1'), MULTI_CHAR('sfontb2'), MULTI_CHAR('sfontl0'), MULTI_CHAR('sfontl1'), MULTI_CHAR('sfontl2'), MULTI_CHAR('sfont00'),
|
||||
@@ -91,6 +95,11 @@ void dMsgScrnBoss_c::exec() {
|
||||
}
|
||||
|
||||
void dMsgScrnBoss_c::drawSelf() {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.recordingMode) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
J2DGrafContext* ctx = dComIfGp_getCurrentGrafPort();
|
||||
ctx->setup2D();
|
||||
drawOutFont(0.0f, 0.0f, 1.0f);
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
#include "d/d_msg_out_font.h"
|
||||
#include "d/d_pane_class.h"
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/settings.h"
|
||||
#endif
|
||||
|
||||
dMsgScrnKanban_c::dMsgScrnKanban_c(JKRExpHeap* param_0) {
|
||||
if (param_0 != NULL) {
|
||||
field_0xd4 = param_0;
|
||||
@@ -176,6 +180,11 @@ void dMsgScrnKanban_c::exec() {
|
||||
}
|
||||
|
||||
void dMsgScrnKanban_c::draw() {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.recordingMode) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort();
|
||||
grafContext->setup2D();
|
||||
mpScreen->draw(0.0f, 0.0f, grafContext);
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
#include "d/d_msg_object.h"
|
||||
#include "d/d_pane_class.h"
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/settings.h"
|
||||
#endif
|
||||
|
||||
dMsgScrnPlace_c::dMsgScrnPlace_c() {
|
||||
static u64 t_tag[7] = {
|
||||
MULTI_CHAR('sfontb0'), MULTI_CHAR('sfontb1'), MULTI_CHAR('sfontb2'), MULTI_CHAR('sfontl0'), MULTI_CHAR('sfontl1'), MULTI_CHAR('sfontl2'), MULTI_CHAR('sfont00'),
|
||||
@@ -127,6 +131,11 @@ void dMsgScrnPlace_c::exec() {
|
||||
}
|
||||
|
||||
void dMsgScrnPlace_c::drawSelf() {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.recordingMode) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort();
|
||||
grafContext->setup2D();
|
||||
drawOutFont(0.0f, 0.0f, 1.0f);
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
#include "JSystem/J2DGraph/J2DScreen.h"
|
||||
#include <cstring>
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/settings.h"
|
||||
#endif
|
||||
|
||||
dMsgScrnTalk_c::dMsgScrnTalk_c(u8 param_1, u8 param_2, JKRExpHeap* param_3) {
|
||||
if (param_3 != NULL) {
|
||||
field_0xe4 = param_3;
|
||||
@@ -303,6 +307,11 @@ void dMsgScrnTalk_c::exec() {
|
||||
}
|
||||
|
||||
void dMsgScrnTalk_c::drawSelf() {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.recordingMode) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
J2DGrafContext* grafContext[1];
|
||||
grafContext[0] = dComIfGp_getCurrentGrafPort();
|
||||
grafContext[0]->setup2D();
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
#include "d/d_msg_out_font.h"
|
||||
#include "d/d_pane_class.h"
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/settings.h"
|
||||
#endif
|
||||
|
||||
dMsgScrnTree_c::dMsgScrnTree_c(JUTFont* param_0, JKRExpHeap* param_1) {
|
||||
if (param_1 != NULL) {
|
||||
field_0xd8 = param_1;
|
||||
@@ -187,6 +191,11 @@ void dMsgScrnTree_c::exec() {
|
||||
}
|
||||
|
||||
void dMsgScrnTree_c::draw() {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.recordingMode) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort();
|
||||
grafContext->setup2D();
|
||||
mpScreen->draw(0.0f, 0.0f, grafContext);
|
||||
|
||||
+31
-8
@@ -21,16 +21,39 @@ static bool checkEnabled() {
|
||||
return !__OSReport_disable || dusk::OSReportReallyForceEnable;
|
||||
}
|
||||
|
||||
#ifndef va_copy
|
||||
#define va_copy(d, s) ((d) = (s))
|
||||
#endif
|
||||
|
||||
static std::string FormatToString(const char* msg, va_list list) {
|
||||
int ret = vsnprintf(nullptr, 0, msg, list);
|
||||
if (ret <= 0) {
|
||||
return {};
|
||||
size_t size = (strlen(msg) * 2) + 50;
|
||||
std::string str;
|
||||
va_list ap;
|
||||
int attempts = 0;
|
||||
while (true) {
|
||||
str.resize(size);
|
||||
va_copy(ap, list);
|
||||
int n = vsnprintf(str.data(), size, msg, ap);
|
||||
va_end(ap);
|
||||
if (n > -1 && n < size) {
|
||||
str.resize(n);
|
||||
break;
|
||||
}
|
||||
|
||||
++attempts;
|
||||
if (attempts >= 3) {
|
||||
if (n == -1) {
|
||||
str.clear();
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (n > -1) {
|
||||
size = n + 1;
|
||||
} else {
|
||||
size *= 2;
|
||||
}
|
||||
}
|
||||
++ret;
|
||||
std::unique_ptr<char[]> buf(new char[ret]);
|
||||
vsnprintf(buf.get(), ret, msg, list);
|
||||
buf[ret - 1] = '\0';
|
||||
return {buf.get()};
|
||||
return str;
|
||||
}
|
||||
|
||||
void OSReport(const char* fmt, ...) {
|
||||
|
||||
@@ -154,6 +154,7 @@ namespace dusk::config {
|
||||
template class ConfigImpl<f64>;
|
||||
template class ConfigImpl<std::string>;
|
||||
template class ConfigImpl<dusk::BloomMode>;
|
||||
template class ConfigImpl<dusk::DiscVerificationState>;
|
||||
template class ConfigImpl<dusk::GameLanguage>;
|
||||
}
|
||||
|
||||
|
||||
@@ -282,7 +282,9 @@ static void ShowAllJAISeqs() {
|
||||
}
|
||||
|
||||
void dusk::ImGuiMenuTools::ShowAudioDebug() {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F10, m_showAudioDebug)) {
|
||||
if (!getSettings().backend.enableAdvancedSettings ||
|
||||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F10, m_showAudioDebug))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -328,7 +330,9 @@ void dusk::ImGuiMenuTools::ShowAudioDebug() {
|
||||
}
|
||||
|
||||
void dusk::ImGuiMenuTools::ShowSaveEditor() {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F6, m_showSaveEditor)) {
|
||||
if (!getSettings().backend.enableAdvancedSettings ||
|
||||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F6, m_showSaveEditor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_saveEditor.draw(m_showSaveEditor);
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
|
||||
namespace dusk {
|
||||
void ImGuiMenuTools::ShowCameraOverlay() {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F9, m_showCameraOverlay)) {
|
||||
if (!getSettings().backend.enableAdvancedSettings ||
|
||||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F9, m_showCameraOverlay))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -61,16 +63,32 @@ namespace dusk {
|
||||
ImGui::SetTooltip("Cannot enable while paused or during an active event.");
|
||||
} else {
|
||||
ImGui::SetTooltip("Detach camera and fly freely.\n"
|
||||
"Left stick: move, C-stick: look\n"
|
||||
"L/R triggers: up/down, Z: fast");
|
||||
"WASD/Arrows/Left stick: move, Mouse/C-stick: look\n"
|
||||
"Ctrl/L: down, Space/R: up, Shift/Z: fast\n"
|
||||
"Q Key/Y: roll left, R Key/X: roll right");
|
||||
}
|
||||
}
|
||||
if (eventRunning) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
if (!getSettings().game.debugFlyCam) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
config::ImGuiCheckbox("Lock Events", getSettings().game.debugFlyCamLockEvents);
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
|
||||
if (!getSettings().game.debugFlyCam) {
|
||||
ImGui::SetTooltip("Enable Fly Mode first.");
|
||||
} else {
|
||||
ImGui::SetTooltip("Freeze game events while flying.");
|
||||
}
|
||||
}
|
||||
if (!getSettings().game.debugFlyCam) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
ShowCornerContextMenu(m_cameraOverlayCorner, 0);
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
#include "fmt/format.h"
|
||||
#include "ImGuiConsole.hpp"
|
||||
#include "dusk/ui/ui.hpp"
|
||||
#include "ImGuiEngine.hpp"
|
||||
#include "JSystem/JUtility/JUTGamePad.h"
|
||||
#include "SDL3/SDL_mouse.h"
|
||||
#include "dusk/audio/DuskAudioSystem.h"
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "dusk/livesplit.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/ui/ui.hpp"
|
||||
#include "f_pc/f_pc_manager.h"
|
||||
#include "f_pc/f_pc_name.h"
|
||||
#include "m_Do/m_Do_controller_pad.h"
|
||||
@@ -260,12 +261,19 @@ namespace dusk {
|
||||
}
|
||||
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F11)) {
|
||||
ImGuiMenuGame::ToggleFullscreen();
|
||||
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
|
||||
VISetWindowFullscreen(getSettings().video.enableFullscreen);
|
||||
config::Save();
|
||||
}
|
||||
|
||||
if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) {
|
||||
m_isHidden = !m_isHidden;
|
||||
if (getSettings().backend.enableAdvancedSettings) {
|
||||
m_isHidden = !m_isHidden;
|
||||
} else {
|
||||
m_isHidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool showMenu = !m_isHidden;
|
||||
|
||||
// The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg,
|
||||
@@ -276,25 +284,11 @@ namespace dusk {
|
||||
m_menuRandomizer.draw();
|
||||
m_menuTools.draw();
|
||||
|
||||
const auto fpsLabel =
|
||||
fmt::format(FMT_STRING("FPS: {:.2f}\n"), ImGui::GetIO().Framerate);
|
||||
const auto fpsSize =
|
||||
ImGui::CalcTextSize(fpsLabel.data(), fpsLabel.data() + fpsLabel.size());
|
||||
ImGui::SetCursorPosX(
|
||||
ImMax(ImGui::GetCursorPosX(), ImGui::GetWindowWidth() -
|
||||
ImGui::GetStyle().DisplaySafeAreaPadding.x -
|
||||
fpsSize.x - ImGui::GetStyle().ItemSpacing.x));
|
||||
ImGuiStringViewText(fpsLabel);
|
||||
|
||||
ImGui::EndMainMenuBar();
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
if (dusk::IsGameLaunched && !m_isLaunchInitialized) {
|
||||
AddToast(ImGui::GetIO().MouseSource == ImGuiMouseSource_TouchScreen ?
|
||||
"3-finger tap to toggle menu"s :
|
||||
"Press F1 to toggle menu"s,
|
||||
4.f);
|
||||
m_isLaunchInitialized = true;
|
||||
if (getSettings().game.liveSplitEnabled) {
|
||||
dusk::speedrun::connectLiveSplit();
|
||||
@@ -303,8 +297,68 @@ namespace dusk {
|
||||
|
||||
UpdateDragScroll();
|
||||
|
||||
m_menuGame.windowControllerConfig();
|
||||
m_menuGame.windowInputViewer();
|
||||
// Show message when Aurora backend is Null
|
||||
if (aurora_get_backend() == BACKEND_NULL) {
|
||||
auto& io = ImGui::GetIO();
|
||||
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y));
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
||||
ImGui::SetNextWindowBgAlpha(0.65f);
|
||||
ImGui::Begin("Pre Launch Window", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImGui::NewLine();
|
||||
if (ImGuiEngine::duskLogo) {
|
||||
const auto& windowSize = ImGui::GetWindowSize();
|
||||
ImGui::NewLine();
|
||||
float iconSize = 150.f;
|
||||
float width = iconSize * 2.5f;
|
||||
ImGui::SameLine(windowSize.x / 2 - width + (width / 2));
|
||||
ImGui::Image(ImGuiEngine::duskLogo, ImVec2{width, iconSize});
|
||||
} else {
|
||||
ImGui::PushFont(ImGuiEngine::fontExtraLarge);
|
||||
ImGuiTextCenter("Dusk");
|
||||
ImGui::PopFont();
|
||||
}
|
||||
ImGui::PushFont(ImGuiEngine::fontLarge);
|
||||
ImGuiTextCenter("Failed to initialize any graphics backend");
|
||||
const auto& style = ImGui::GetStyle();
|
||||
const auto retrySize = ImGui::CalcTextSize("Retry (Auto backend)");
|
||||
const auto quitSize = ImGui::CalcTextSize("Quit");
|
||||
float buttonsWidth = quitSize.x + style.FramePadding.x * 2.0f;
|
||||
if constexpr (SupportsProcessRestart) {
|
||||
buttonsWidth += retrySize.x + style.FramePadding.x * 2.0f + style.ItemSpacing.x;
|
||||
}
|
||||
#if DUSK_CAN_OPEN_DATA_FOLDER
|
||||
const auto openSize = ImGui::CalcTextSize("Open Data Folder");
|
||||
buttonsWidth += openSize.x + style.FramePadding.x * 2.0f + style.ItemSpacing.x;
|
||||
#endif
|
||||
ImGui::NewLine();
|
||||
ImGui::SetCursorPosX(
|
||||
ImMax(style.WindowPadding.x, (ImGui::GetWindowSize().x - buttonsWidth) * 0.5f));
|
||||
if constexpr (SupportsProcessRestart) {
|
||||
if (ImGui::Button("Retry (Auto backend)")) {
|
||||
getSettings().backend.graphicsBackend.setValue("auto");
|
||||
config::Save();
|
||||
RestartRequested = true;
|
||||
IsRunning = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
}
|
||||
#if DUSK_CAN_OPEN_DATA_FOLDER
|
||||
if (ImGui::Button("Open Data Folder")) {
|
||||
OpenDataFolder();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
#endif
|
||||
if (ImGui::Button("Quit")) {
|
||||
IsRunning = false;
|
||||
}
|
||||
ImGui::PopFont();
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
m_menuTools.ShowInputViewer();
|
||||
m_menuGame.drawSpeedrunTimerOverlay();
|
||||
|
||||
if (getSettings().game.liveSplitEnabled) {
|
||||
@@ -330,7 +384,6 @@ namespace dusk {
|
||||
}
|
||||
m_menuRandomizer.windowRandoStats();
|
||||
m_menuRandomizer.windowRandoGeneration();
|
||||
DuskDebugPad(); // temporary, remove later
|
||||
|
||||
// Hide mouse cursor if the F1 menu is not open and the cursor is idle for 3 seconds.
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
#define DUSK_IMGUI_HPP
|
||||
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <SDL3/SDL_misc.h>
|
||||
#include <aurora/aurora.h>
|
||||
|
||||
#include "ImGuiMenuGame.hpp"
|
||||
#include "ImGuiMenuTools.hpp"
|
||||
#include "ImGuiMenuRandomizer.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "imgui.h"
|
||||
|
||||
union SDL_Event;
|
||||
@@ -24,7 +27,7 @@ public:
|
||||
void PreDraw();
|
||||
void PostDraw();
|
||||
|
||||
static bool CheckMenuViewToggle(ImGuiKey key, bool& active);
|
||||
static bool CheckMenuViewToggle(ImGuiKey key, bool& active);
|
||||
void AddToast(std::string_view message, float duration = 3.f);
|
||||
|
||||
private:
|
||||
@@ -72,6 +75,24 @@ bool ImGuiButtonCenter(std::string_view text);
|
||||
float ImGuiScale();
|
||||
} // namespace dusk
|
||||
|
||||
void DuskDebugPad();
|
||||
#if defined(_WIN32) || \
|
||||
(defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_TV && !TARGET_OS_MACCATALYST) || \
|
||||
(defined(__linux__) && !defined(__ANDROID__))
|
||||
#define DUSK_CAN_OPEN_DATA_FOLDER 1
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static void OpenDataFolder() {
|
||||
const std::string path = fs::absolute(dusk::ConfigPath).generic_string();
|
||||
#if defined(_WIN32)
|
||||
const std::string url = std::string("file:///") + path;
|
||||
#else
|
||||
const std::string url = std::string("file://") + path;
|
||||
#endif
|
||||
(void)SDL_OpenURL(url.c_str());
|
||||
}
|
||||
#else
|
||||
#define DUSK_CAN_OPEN_DATA_FOLDER 0
|
||||
#endif
|
||||
|
||||
#endif // DUSK_IMGUI_HPP
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
#include "imgui.h"
|
||||
#include <imgui_internal.h>
|
||||
#include "ImGuiConsole.hpp"
|
||||
#include "ImGuiMenuGame.hpp"
|
||||
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
namespace dusk {
|
||||
void ImGuiMenuGame::windowInputViewer() {
|
||||
void ImGuiMenuTools::ShowInputViewer() {
|
||||
if (!m_showInputViewer) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
#include "m_Do/m_Do_controller_pad.h"
|
||||
|
||||
#include "imgui.h"
|
||||
#include "ImGuiConsole.hpp"
|
||||
|
||||
void DuskDebugPad() {
|
||||
auto& pad = mDoCPd_c::getCpadInfo(PAD_1);
|
||||
auto KeyFlag = [&](ImGuiKey key, u32 padFlag) {
|
||||
if (ImGui::IsKeyDown(key))
|
||||
pad.mButtonFlags |= padFlag;
|
||||
if (ImGui::IsKeyPressed(key))
|
||||
pad.mPressedButtonFlags |= padFlag;
|
||||
|
||||
};
|
||||
|
||||
KeyFlag(ImGuiKey_K, PAD_BUTTON_A);
|
||||
KeyFlag(ImGuiKey_J, PAD_BUTTON_B);
|
||||
KeyFlag(ImGuiKey_L, PAD_BUTTON_X);
|
||||
KeyFlag(ImGuiKey_I, PAD_BUTTON_Y);
|
||||
KeyFlag(ImGuiKey_H, PAD_BUTTON_START);
|
||||
KeyFlag(ImGuiKey_O, PAD_TRIGGER_Z);
|
||||
KeyFlag(ImGuiKey_Keypad8, PAD_BUTTON_UP);
|
||||
KeyFlag(ImGuiKey_Keypad2, PAD_BUTTON_DOWN);
|
||||
KeyFlag(ImGuiKey_Keypad6, PAD_BUTTON_RIGHT);
|
||||
KeyFlag(ImGuiKey_Keypad4, PAD_BUTTON_LEFT);
|
||||
|
||||
if (ImGui::IsKeyDown(ImGuiKey_W)) {
|
||||
pad.mMainStickPosY = 1.0f;
|
||||
pad.mMainStickValue = 1.0f;
|
||||
pad.mMainStickAngle = 0x8000;
|
||||
}
|
||||
|
||||
if (ImGui::IsKeyDown(ImGuiKey_S)) {
|
||||
pad.mMainStickPosY = -1.0f;
|
||||
pad.mMainStickValue = 1.0f;
|
||||
pad.mMainStickAngle = 0;
|
||||
}
|
||||
|
||||
if (ImGui::IsKeyDown(ImGuiKey_D)) {
|
||||
pad.mMainStickPosX = 1.0f;
|
||||
pad.mMainStickValue = 1.0f;
|
||||
pad.mMainStickAngle = 0x4000;
|
||||
}
|
||||
|
||||
if (ImGui::IsKeyDown(ImGuiKey_A)) {
|
||||
pad.mMainStickPosX = -1.0f;
|
||||
pad.mMainStickValue = 1.0f;
|
||||
pad.mMainStickAngle = -0x4000;
|
||||
}
|
||||
|
||||
if (ImGui::IsKeyDown(ImGuiKey_Q)) {
|
||||
pad.mTriggerLeft = 1.0;
|
||||
pad.mTrigLockL = 1;
|
||||
pad.mHoldLockL = 1;
|
||||
}
|
||||
if (ImGui::IsKeyDown(ImGuiKey_E)) {
|
||||
pad.mTriggerRight = 1.0;
|
||||
pad.mTrigLockR = 1;
|
||||
pad.mHoldLockR = 1;
|
||||
}
|
||||
}
|
||||
@@ -219,7 +219,7 @@ void ImGuiEngine_AddTextures() {
|
||||
ImGuiEngine::orgIcon = AddTexture("org-icon.png");
|
||||
}
|
||||
if (ImGuiEngine::duskLogo == 0) {
|
||||
ImGuiEngine::duskLogo = AddTexture("logo.png");
|
||||
ImGuiEngine::duskLogo = AddTexture("logo-mascot.png");
|
||||
}
|
||||
}
|
||||
} // namespace dusk
|
||||
|
||||
+2317
-2190
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,9 @@ namespace dusk {
|
||||
void ShowHeapDetailed(JKRHeap* heap, OpenHeapData& data, bool& open);
|
||||
|
||||
void ImGuiMenuTools::ShowHeapOverlay() {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F4, m_showHeapOverlay)) {
|
||||
if (!getSettings().backend.enableAdvancedSettings ||
|
||||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F4, m_showHeapOverlay))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
|
||||
namespace dusk {
|
||||
void ImGuiMenuTools::ShowMapLoader() {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F7, m_showMapLoader)) {
|
||||
if (!getSettings().backend.enableAdvancedSettings ||
|
||||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F7, m_showMapLoader))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,573 +1,16 @@
|
||||
#include "fmt/format.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include "ImGuiEngine.hpp"
|
||||
#include "ImGuiConsole.hpp"
|
||||
#include "ImGuiConfig.hpp"
|
||||
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/hotkeys.h"
|
||||
#include "m_Do/m_Do_main.h"
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
|
||||
namespace dusk {
|
||||
void ImGuiMenuGame::ToggleFullscreen() {
|
||||
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
|
||||
VISetWindowFullscreen(getSettings().video.enableFullscreen);
|
||||
config::Save();
|
||||
}
|
||||
|
||||
ImGuiMenuGame::ImGuiMenuGame() {}
|
||||
|
||||
void ImGuiMenuGame::draw() {
|
||||
if (ImGui::BeginMenu("Settings")) {
|
||||
// TODO: Remove this once Controller Config exists in RmlUi
|
||||
if (ImGui::Button("Configure Controller")){
|
||||
m_showControllerConfig = !m_showControllerConfig;
|
||||
}
|
||||
|
||||
ImGui::Checkbox("Show Input Viewer", &m_showInputViewer);
|
||||
|
||||
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));
|
||||
|
||||
ImGui::BeginChild(id, ImVec2(80 * scale, 80 * scale), 0, ImGuiWindowFlags_NoBackground);
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
|
||||
float radius = 30.0f * scale;
|
||||
ImVec2 pos = ImVec2(p.x + radius, p.y + radius);
|
||||
|
||||
constexpr ImU32 stickGray = IM_COL32(150, 150, 150, 255);
|
||||
constexpr ImU32 white = IM_COL32(255, 255, 255, 255);
|
||||
constexpr ImU32 red = IM_COL32(230, 0, 0, 255);
|
||||
|
||||
dl->AddCircleFilled(pos, radius, stickGray, 8);
|
||||
dl->AddCircleFilled(ImVec2(pos.x + stick.x * (radius), pos.y + -stick.y * (radius)), 3 * scale, red);
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
struct SpecificButtonName {
|
||||
SDL_GamepadType Type;
|
||||
const char* Name;
|
||||
};
|
||||
|
||||
struct ButtonNames {
|
||||
SDL_GamepadButton Button;
|
||||
std::vector<SpecificButtonName> Names;
|
||||
};
|
||||
|
||||
// clang-format off
|
||||
static const std::vector<ButtonNames> GamepadButtonNames = {
|
||||
{ SDL_GAMEPAD_BUTTON_LEFT_STICK, {
|
||||
{SDL_GAMEPAD_TYPE_PS3, "L3"},
|
||||
{SDL_GAMEPAD_TYPE_PS4, "L3"},
|
||||
{SDL_GAMEPAD_TYPE_PS5, "L3"},
|
||||
{SDL_GAMEPAD_TYPE_XBOX360, "Left Stick"},
|
||||
{SDL_GAMEPAD_TYPE_XBOXONE, "Left Stick"},
|
||||
{SDL_GAMEPAD_TYPE_GAMECUBE, "Control Stick"},
|
||||
}},
|
||||
{ SDL_GAMEPAD_BUTTON_RIGHT_STICK, {
|
||||
{SDL_GAMEPAD_TYPE_PS3, "R3"},
|
||||
{SDL_GAMEPAD_TYPE_PS4, "R3"},
|
||||
{SDL_GAMEPAD_TYPE_PS5, "R3"},
|
||||
{SDL_GAMEPAD_TYPE_XBOX360, "Right Stick"},
|
||||
{SDL_GAMEPAD_TYPE_XBOXONE, "Right Stick"},
|
||||
{SDL_GAMEPAD_TYPE_GAMECUBE, "C Stick"},
|
||||
}},
|
||||
{ SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, {
|
||||
{SDL_GAMEPAD_TYPE_PS3, "L1"},
|
||||
{SDL_GAMEPAD_TYPE_PS4, "L1"},
|
||||
{SDL_GAMEPAD_TYPE_PS5, "L1"},
|
||||
{SDL_GAMEPAD_TYPE_XBOX360, "LB"},
|
||||
{SDL_GAMEPAD_TYPE_XBOXONE, "LB"},
|
||||
}},
|
||||
{ SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, {
|
||||
{SDL_GAMEPAD_TYPE_PS3, "R1"},
|
||||
{SDL_GAMEPAD_TYPE_PS4, "R1"},
|
||||
{SDL_GAMEPAD_TYPE_PS5, "R1"},
|
||||
{SDL_GAMEPAD_TYPE_XBOX360, "RB"},
|
||||
{SDL_GAMEPAD_TYPE_XBOXONE, "RB"},
|
||||
{SDL_GAMEPAD_TYPE_GAMECUBE, "Z"},
|
||||
}},
|
||||
{ SDL_GAMEPAD_BUTTON_BACK, {
|
||||
{SDL_GAMEPAD_TYPE_PS3, "Select"},
|
||||
{SDL_GAMEPAD_TYPE_PS4, "Share"},
|
||||
{SDL_GAMEPAD_TYPE_PS5, "Create"},
|
||||
{SDL_GAMEPAD_TYPE_XBOX360, "Back"},
|
||||
{SDL_GAMEPAD_TYPE_XBOXONE, "View"},
|
||||
}},
|
||||
{ SDL_GAMEPAD_BUTTON_START, {
|
||||
{SDL_GAMEPAD_TYPE_PS3, "Start"},
|
||||
{SDL_GAMEPAD_TYPE_PS4, "Options"},
|
||||
{SDL_GAMEPAD_TYPE_PS5, "Options"},
|
||||
{SDL_GAMEPAD_TYPE_XBOX360, "Start"},
|
||||
{SDL_GAMEPAD_TYPE_XBOXONE, "Menu"},
|
||||
{SDL_GAMEPAD_TYPE_GAMECUBE, "Start/Pause"},
|
||||
}},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
static const char* GetNameForGamepadButton(SDL_Gamepad* gamepad, u32 buttonUntyped) {
|
||||
if (buttonUntyped == PAD_NATIVE_BUTTON_INVALID) {
|
||||
return "Not bound";
|
||||
}
|
||||
|
||||
auto button = static_cast<SDL_GamepadButton>(buttonUntyped);
|
||||
auto label = SDL_GetGamepadButtonLabel(gamepad, button);
|
||||
|
||||
switch (label) {
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_A:
|
||||
return "A";
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_B:
|
||||
return "B";
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_X:
|
||||
return "X";
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_Y:
|
||||
return "Y";
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_CROSS:
|
||||
return "Cross";
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE:
|
||||
return "Circle";
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE:
|
||||
return "Triangle";
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_SQUARE:
|
||||
return "Square";
|
||||
default:; // Fall through
|
||||
}
|
||||
|
||||
auto padType = SDL_GetGamepadType(gamepad);
|
||||
for (const auto& buttonNames : GamepadButtonNames) {
|
||||
if (buttonNames.Button != button) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const auto& name : buttonNames.Names) {
|
||||
if (name.Type == padType) {
|
||||
return name.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (button) {
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
|
||||
return "D-pad left";
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
|
||||
return "D-pad right";
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_UP:
|
||||
return "D-pad up";
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
|
||||
return "D-pad down";
|
||||
default:
|
||||
return PADGetNativeButtonName(buttonUntyped);
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiMenuGame::windowControllerConfig() {
|
||||
if (!m_showControllerConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if pending for a button mapping, check to set new input
|
||||
if (m_controllerConfig.m_pendingButtonMapping != nullptr) {
|
||||
s32 nativeButton = PADGetNativeButtonPressed(m_controllerConfig.m_pendingPort);
|
||||
if (nativeButton != -1) {
|
||||
m_controllerConfig.m_pendingButtonMapping->nativeButton = nativeButton;
|
||||
m_controllerConfig.m_pendingButtonMapping = nullptr;
|
||||
m_controllerConfig.m_pendingPort = -1;
|
||||
PADBlockInput(false);
|
||||
PADSerializeMappings();
|
||||
}
|
||||
}
|
||||
|
||||
// if pending for an axis mapping, check to set new input
|
||||
if (m_controllerConfig.m_pendingAxisMapping != nullptr) {
|
||||
auto nativeAxis = PADGetNativeAxisPulled(m_controllerConfig.m_pendingPort);
|
||||
if (nativeAxis.nativeAxis != -1) {
|
||||
m_controllerConfig.m_pendingAxisMapping->nativeAxis = nativeAxis;
|
||||
m_controllerConfig.m_pendingAxisMapping->nativeButton = -1;
|
||||
m_controllerConfig.m_pendingAxisMapping = nullptr;
|
||||
m_controllerConfig.m_pendingPort = -1;
|
||||
PADBlockInput(false);
|
||||
PADSerializeMappings();
|
||||
} else {
|
||||
auto nativeButton = PADGetNativeButtonPressed(m_controllerConfig.m_pendingPort);
|
||||
if (nativeButton != -1) {
|
||||
m_controllerConfig.m_pendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE};
|
||||
m_controllerConfig.m_pendingAxisMapping->nativeButton = nativeButton;
|
||||
m_controllerConfig.m_pendingAxisMapping = nullptr;
|
||||
m_controllerConfig.m_pendingPort = -1;
|
||||
PADBlockInput(false);
|
||||
PADSerializeMappings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float scale = ImGuiScale();
|
||||
ImGuiWindowFlags windowFlags =
|
||||
ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_AlwaysAutoResize;
|
||||
|
||||
// ImGui::SetNextWindowBgAlpha(0.65f);
|
||||
|
||||
if (!ImGui::Begin("Controller Config", &m_showControllerConfig, windowFlags)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// port tabs
|
||||
ImGui::BeginTabBar("##ControllerTabs");
|
||||
for (int i = PAD_1; i <= PAD_4; i++) {
|
||||
if (ImGui::BeginTabItem(fmt::format("Port {}", i + 1).c_str())) {
|
||||
m_controllerConfig.m_selectedPort = i;
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
|
||||
// if tab is changed while waiting for input, cancel pending
|
||||
if ((m_controllerConfig.m_pendingButtonMapping != nullptr ||
|
||||
m_controllerConfig.m_pendingAxisMapping != nullptr) &&
|
||||
m_controllerConfig.m_pendingPort != m_controllerConfig.m_selectedPort)
|
||||
{
|
||||
m_controllerConfig.m_pendingButtonMapping = nullptr;
|
||||
m_controllerConfig.m_pendingAxisMapping = nullptr;
|
||||
m_controllerConfig.m_pendingPort = -1;
|
||||
PADBlockInput(false);
|
||||
}
|
||||
|
||||
// get a list of all available controller's names
|
||||
std::vector<std::string> controllerList;
|
||||
controllerList.push_back("None");
|
||||
for (int i = 0; i < PADCount(); i++) {
|
||||
// attach index to name for unique name
|
||||
controllerList.push_back(fmt::format("{}-{}", PADGetNameForControllerIndex(i), i));
|
||||
}
|
||||
|
||||
// get current controller name
|
||||
const char* tmpName = PADGetName(m_controllerConfig.m_selectedPort);
|
||||
std::string currentName = "None";
|
||||
if (tmpName != nullptr) {
|
||||
currentName = fmt::format("{}-{}", tmpName, PADGetIndexForPort(m_controllerConfig.m_selectedPort));
|
||||
}
|
||||
|
||||
// controller selection combo box
|
||||
bool changedController = false;
|
||||
int changedControllerIndex = 0;
|
||||
ImGui::SetNextItemWidth(400.0f * scale);
|
||||
if (ImGui::BeginCombo("##ControllerDeviceList", currentName.c_str())) {
|
||||
for (int i = 0; const auto& name : controllerList) {
|
||||
if (ImGui::Selectable(name.c_str(), currentName == name)) {
|
||||
changedControllerIndex = i;
|
||||
changedController = true;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
// handle controller change
|
||||
if (changedController) {
|
||||
if (changedControllerIndex > 0) {
|
||||
PADSetPortForIndex(changedControllerIndex - 1, m_controllerConfig.m_selectedPort);
|
||||
}
|
||||
else if (changedControllerIndex == 0) {
|
||||
// if "None" selected
|
||||
PADClearPort(m_controllerConfig.m_selectedPort);
|
||||
}
|
||||
PADSerializeMappings();
|
||||
}
|
||||
|
||||
// restore defaults button
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Default")) {
|
||||
PADRestoreDefaultMapping(m_controllerConfig.m_selectedPort);
|
||||
PADSerializeMappings();
|
||||
}
|
||||
|
||||
// buttons panel
|
||||
const float uiButtonSize = 40 * scale;
|
||||
ImVec2 btnSize(110.0f * scale, 30.0f * scale);
|
||||
|
||||
ImGuiBeginGroupPanel("Buttons", ImVec2(150 * scale, 20 * scale));
|
||||
|
||||
SDL_Gamepad* gamepad = PADGetSDLGamepadForIndex(PADGetIndexForPort(m_controllerConfig.m_selectedPort));
|
||||
u32 buttonCount;
|
||||
PADButtonMapping* btnMappingList = PADGetButtonMappings(m_controllerConfig.m_selectedPort, &buttonCount);
|
||||
if (btnMappingList != nullptr) {
|
||||
for (int i = 0; i < buttonCount; i++) {
|
||||
const char* btnName = PADGetButtonName(btnMappingList[i].padButton);
|
||||
ImVec2 len = ImGui::CalcTextSize(btnName);
|
||||
ImVec2 pos = ImGui::GetCursorPos();
|
||||
|
||||
ImGui::SetCursorPosY(pos.y + len.y / 4);
|
||||
ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize));
|
||||
ImGui::Text("%s", btnName);
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::SetCursorPosY(pos.y);
|
||||
|
||||
std::string dispName;
|
||||
if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingButtonMapping == &btnMappingList[i]) {
|
||||
dispName = fmt::format("Press a Key...##{}", btnName);
|
||||
} else {
|
||||
const char* nativeName = GetNameForGamepadButton(gamepad, btnMappingList[i].nativeButton);
|
||||
if (nativeName == nullptr) {
|
||||
nativeName = "[unbound]";
|
||||
}
|
||||
dispName = fmt::format("{0}##-{1}", nativeName, i);
|
||||
}
|
||||
bool pressed = ImGui::Button(dispName.c_str(),
|
||||
btnSize);
|
||||
|
||||
if (pressed) {
|
||||
m_controllerConfig.m_isReading = true;
|
||||
m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort;
|
||||
m_controllerConfig.m_pendingButtonMapping = &btnMappingList[i];
|
||||
PADBlockInput(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGuiEndGroupPanel();
|
||||
ImGui::SameLine();
|
||||
|
||||
uint32_t axisCount;
|
||||
PADAxisMapping* axisMappingList = PADGetAxisMappings(m_controllerConfig.m_selectedPort, &axisCount);
|
||||
|
||||
ImGuiBeginGroupPanel("Analog Triggers", ImVec2(150 * scale, 20 * scale));
|
||||
|
||||
PADAxis triggers[] = {PAD_AXIS_TRIGGER_L, PAD_AXIS_TRIGGER_R};
|
||||
if (axisMappingList != nullptr) {
|
||||
for (PADAxis trigger : triggers) {
|
||||
const char* axisName = PADGetAxisName(axisMappingList[trigger].padAxis);
|
||||
ImVec2 len = ImGui::CalcTextSize(axisName);
|
||||
ImVec2 pos = ImGui::GetCursorPos();
|
||||
|
||||
ImGui::SetCursorPosY(pos.y + len.y / 4);
|
||||
ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize));
|
||||
ImGui::Text("%s", axisName);
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::SetCursorPosY(pos.y);
|
||||
|
||||
std::string dispName;
|
||||
if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[trigger]) {
|
||||
dispName = fmt::format("Press a Key...##{}", axisName);
|
||||
} else {
|
||||
dispName = fmt::format("{0}##-{1}", PADGetNativeAxisName(axisMappingList[trigger].nativeAxis), trigger);
|
||||
}
|
||||
bool pressed = ImGui::Button(dispName.c_str(),
|
||||
btnSize);
|
||||
|
||||
if (pressed) {
|
||||
m_controllerConfig.m_isReading = true;
|
||||
m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort;
|
||||
m_controllerConfig.m_pendingAxisMapping = &axisMappingList[trigger];
|
||||
PADBlockInput(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int port = m_controllerConfig.m_selectedPort;
|
||||
PADDeadZones* deadZones = PADGetDeadZones(port);
|
||||
|
||||
if (deadZones != nullptr) {
|
||||
ImGui::Text("L Threshold");
|
||||
ImGui::SameLine();
|
||||
{
|
||||
float tmp = static_cast<float>(deadZones->leftTriggerActivationZone * 100.f) / 32767.f;
|
||||
if (ImGui::DragFloat("##LThreshold", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) {
|
||||
deadZones->leftTriggerActivationZone = static_cast<u16>((tmp / 100.f) * 32767);
|
||||
PADSerializeMappings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deadZones != nullptr) {
|
||||
ImGui::Text("R Threshold");
|
||||
ImGui::SameLine();
|
||||
{
|
||||
float tmp = static_cast<float>(deadZones->rightTriggerActivationZone * 100.f) / 32767.f;
|
||||
if (ImGui::DragFloat("##RThreshold", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) {
|
||||
deadZones->rightTriggerActivationZone = static_cast<u16>((tmp / 100.f) * 32767);
|
||||
PADSerializeMappings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGuiEndGroupPanel();
|
||||
ImGui::SameLine();
|
||||
|
||||
// main stick panel
|
||||
ImGuiBeginGroupPanel("Control Stick", ImVec2(150 * scale, 20 * scale));
|
||||
|
||||
drawVirtualStick("##mainStick", ImVec2{ mDoCPd_c::getStickX(port), mDoCPd_c::getStickY(port) });
|
||||
|
||||
if (axisMappingList != nullptr) {
|
||||
const PADAxis lStickAxes[] = {PAD_AXIS_LEFT_Y_POS, PAD_AXIS_LEFT_Y_NEG, PAD_AXIS_LEFT_X_NEG, PAD_AXIS_LEFT_X_POS};
|
||||
for (auto axis : lStickAxes) {
|
||||
const char* label = PADGetAxisDirectionLabel(axis);
|
||||
ImVec2 len = ImGui::CalcTextSize(label);
|
||||
ImVec2 pos = ImGui::GetCursorPos();
|
||||
|
||||
ImGui::SetCursorPosY(pos.y + len.y / 4);
|
||||
ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize));
|
||||
ImGui::Text("%s", label);
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::SetCursorPosY(pos.y);
|
||||
|
||||
std::string dispName;
|
||||
if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[axis]) {
|
||||
dispName = fmt::format("Press a Key...##{}", label);
|
||||
} else {
|
||||
if (axisMappingList[axis].nativeAxis.nativeAxis != -1) {
|
||||
const char* signStr;
|
||||
if (axis == PAD_AXIS_TRIGGER_L || axis == PAD_AXIS_TRIGGER_R) {
|
||||
signStr = "";
|
||||
} else if (axisMappingList[axis].nativeAxis.sign == AXIS_SIGN_POSITIVE) {
|
||||
signStr = "+";
|
||||
} else {
|
||||
signStr = "-";
|
||||
}
|
||||
dispName = fmt::format("{0}{1}##-{2}", PADGetNativeAxisName(axisMappingList[axis].nativeAxis), signStr, axis);
|
||||
} else {
|
||||
assert(axisMappingList[axis].nativeButton != -1);
|
||||
dispName = fmt::format("{0}##-{1}", PADGetNativeButtonName(axisMappingList[axis].nativeButton), axis);
|
||||
}
|
||||
}
|
||||
bool pressed = ImGui::Button(dispName.c_str(), btnSize);
|
||||
|
||||
if (pressed) {
|
||||
m_controllerConfig.m_isReading = true;
|
||||
m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort;
|
||||
m_controllerConfig.m_pendingAxisMapping = &axisMappingList[axis];
|
||||
PADBlockInput(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deadZones != nullptr) {
|
||||
ImGui::Text("Dead Zone");
|
||||
{
|
||||
float tmp = static_cast<float>(deadZones->stickDeadZone * 100.f) / 32767.f;
|
||||
if (ImGui::DragFloat("##mainDeadZone", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) {
|
||||
deadZones->stickDeadZone = static_cast<u16>((tmp / 100.f) * 32767);
|
||||
PADSerializeMappings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGuiEndGroupPanel();
|
||||
ImGui::SameLine();
|
||||
|
||||
// sub stick panel
|
||||
ImGuiBeginGroupPanel("C Stick", ImVec2(150 * scale, 20 * scale));
|
||||
|
||||
drawVirtualStick("##subStick", ImVec2{ mDoCPd_c::getSubStickX(port), mDoCPd_c::getSubStickY(port) });
|
||||
|
||||
if (axisMappingList != nullptr) {
|
||||
const PADAxis rStickAxes[] = {PAD_AXIS_RIGHT_Y_POS, PAD_AXIS_RIGHT_Y_NEG, PAD_AXIS_RIGHT_X_NEG, PAD_AXIS_RIGHT_X_POS};
|
||||
for (auto axis : rStickAxes) {
|
||||
const char* label = PADGetAxisDirectionLabel(axisMappingList[axis].padAxis);
|
||||
ImVec2 len = ImGui::CalcTextSize(label);
|
||||
ImVec2 pos = ImGui::GetCursorPos();
|
||||
|
||||
ImGui::SetCursorPosY(pos.y + len.y / 4);
|
||||
ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize));
|
||||
ImGui::Text("%s", label);
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::SetCursorPosY(pos.y);
|
||||
|
||||
std::string dispName;
|
||||
if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[axis]) {
|
||||
dispName = fmt::format("Press a Key...##sub{}", label);
|
||||
} else {
|
||||
if (axisMappingList[axis].nativeAxis.nativeAxis != -1) {
|
||||
const char* signStr;
|
||||
if (axis == PAD_AXIS_TRIGGER_L || axis == PAD_AXIS_TRIGGER_R) {
|
||||
signStr = "";
|
||||
} else if (axisMappingList[axis].nativeAxis.sign == AXIS_SIGN_POSITIVE) {
|
||||
signStr = "+";
|
||||
} else {
|
||||
signStr = "-";
|
||||
}
|
||||
dispName = fmt::format("{0}{1}##-{2}", PADGetNativeAxisName(axisMappingList[axis].nativeAxis), signStr, axis);
|
||||
} else {
|
||||
assert(axisMappingList[axis].nativeButton != -1);
|
||||
dispName = fmt::format("{0}##-{1}", PADGetNativeButtonName(axisMappingList[axis].nativeButton), axis);
|
||||
}
|
||||
}
|
||||
bool pressed = ImGui::Button(fmt::format("{0}##sub{1}", dispName, label).c_str(), btnSize);
|
||||
|
||||
if (pressed) {
|
||||
m_controllerConfig.m_isReading = true;
|
||||
m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort;
|
||||
m_controllerConfig.m_pendingAxisMapping = &axisMappingList[axis];
|
||||
PADBlockInput(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deadZones != nullptr) {
|
||||
ImGui::Text("Dead Zone");
|
||||
{
|
||||
float tmp = static_cast<float>(deadZones->substickDeadZone * 100.f) / 32767.f;
|
||||
if (ImGui::DragFloat("##subDeadZone", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) {
|
||||
deadZones->substickDeadZone = static_cast<u16>((tmp / 100.f) * 32767);
|
||||
PADSerializeMappings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGuiEndGroupPanel();
|
||||
ImGui::SameLine();
|
||||
|
||||
// Options panel
|
||||
ImGuiBeginGroupPanel("Options", ImVec2(150 * scale, -1));
|
||||
|
||||
if (deadZones != nullptr) {
|
||||
if (ImGui::Checkbox("Enable Dead Zones", &deadZones->useDeadzones)) {
|
||||
PADSerializeMappings();
|
||||
}
|
||||
if (ImGui::Checkbox("Emulate Triggers", &deadZones->emulateTriggers)) {
|
||||
PADSerializeMappings();
|
||||
}
|
||||
}
|
||||
|
||||
if (PADSupportsRumbleIntensity(m_controllerConfig.m_selectedPort)) {
|
||||
ImGuiBeginGroupPanel("Rumble Intensity", ImVec2(150 * scale, -1));
|
||||
u16 low;
|
||||
u16 high;
|
||||
(void)PADGetRumbleIntensity(m_controllerConfig.m_selectedPort, &low, &high);
|
||||
float fLow = (static_cast<float>(low) / 32767.f) * 100.f;
|
||||
bool changed = ImGui::SliderFloat("Low", &fLow, 0.f, 100.f, "%.0f%%");
|
||||
float fHigh = (static_cast<float>(high) / 32767.f) * 100.f;
|
||||
changed |= ImGui::SliderFloat("High", &fHigh, 0.f, 100.f, "%.0f%%");
|
||||
if (changed) {
|
||||
PADSetRumbleIntensity(m_controllerConfig.m_selectedPort,
|
||||
static_cast<u16>((fLow / 100) * 32767),
|
||||
static_cast<u16>((fHigh / 100) * 32767));
|
||||
PADSerializeMappings();
|
||||
}
|
||||
if (ImGui::Button(fmt::format("{0}...##rumbleTest", m_controllerConfig.m_isRumbling ? "Stop": "Test").c_str(), {-1, 0})) {
|
||||
PADControlMotor(m_controllerConfig.m_selectedPort, !m_controllerConfig.m_isRumbling ? PAD_MOTOR_RUMBLE : PAD_MOTOR_STOP_HARD);
|
||||
m_controllerConfig.m_isRumbling ^= 1;
|
||||
}
|
||||
ImGuiEndGroupPanel();
|
||||
}
|
||||
ImGuiEndGroupPanel();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
void ImGuiMenuGame::draw() {}
|
||||
|
||||
static std::string GetFormattedTime(OSTime ticks) {
|
||||
OSCalendarTime time;
|
||||
|
||||
@@ -46,31 +46,11 @@ namespace dusk {
|
||||
ImGuiMenuGame();
|
||||
void draw();
|
||||
|
||||
void windowInputViewer();
|
||||
void windowControllerConfig();
|
||||
void drawSpeedrunTimerOverlay();
|
||||
|
||||
static void ToggleFullscreen();
|
||||
|
||||
static void resetForSpeedrunMode();
|
||||
|
||||
private:
|
||||
struct {
|
||||
int m_selectedPort = 0;
|
||||
bool m_isReading = false;
|
||||
PADButtonMapping* m_pendingButtonMapping = nullptr;
|
||||
PADAxisMapping* m_pendingAxisMapping = nullptr;
|
||||
int m_pendingPort = -1;
|
||||
bool m_isRumbling = false;
|
||||
} m_controllerConfig;
|
||||
|
||||
bool m_showControllerConfig = false;
|
||||
|
||||
bool m_showInputViewer = false;
|
||||
bool m_showInputViewerGyro = false;
|
||||
int m_inputOverlayCorner = 3;
|
||||
std::string m_controllerName;
|
||||
|
||||
bool m_showTimerWindow = false;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,24 +23,6 @@
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32) || (defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || (defined(__linux__) && !defined(__ANDROID__))
|
||||
#define DUSK_CAN_OPEN_DATA_FOLDER 1
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static void OpenDataFolder() {
|
||||
const std::string path = fs::absolute(dusk::ConfigPath).generic_string();
|
||||
#if defined(_WIN32)
|
||||
const std::string url = std::string("file:///") + path;
|
||||
#else
|
||||
const std::string url = std::string("file://") + path;
|
||||
#endif
|
||||
(void)SDL_OpenURL(url.c_str());
|
||||
}
|
||||
#else
|
||||
#define DUSK_CAN_OPEN_DATA_FOLDER 0
|
||||
#endif
|
||||
|
||||
namespace aurora::gx {
|
||||
extern bool enableLodBias;
|
||||
}
|
||||
@@ -66,6 +48,8 @@ namespace dusk {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Checkbox("Show Input Viewer", &m_showInputViewer);
|
||||
|
||||
#if DUSK_CAN_OPEN_DATA_FOLDER
|
||||
ImGui::Separator();
|
||||
@@ -137,7 +121,9 @@ namespace dusk {
|
||||
}
|
||||
|
||||
void ImGuiMenuTools::ShowDebugOverlay() {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F3, m_showDebugOverlay)) {
|
||||
if (!getSettings().backend.enableAdvancedSettings ||
|
||||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F3, m_showDebugOverlay))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -201,7 +187,9 @@ namespace dusk {
|
||||
}
|
||||
|
||||
void ImGuiMenuTools::ShowPlayerInfo() {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F5, m_showPlayerInfo)) {
|
||||
if (!getSettings().backend.enableAdvancedSettings ||
|
||||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F5, m_showPlayerInfo))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace dusk {
|
||||
void ShowAudioDebug();
|
||||
void ShowSaveEditor();
|
||||
void ShowStateShare();
|
||||
void ShowInputViewer();
|
||||
|
||||
private:
|
||||
bool m_showDebugOverlay = false;
|
||||
@@ -66,6 +67,11 @@ namespace dusk {
|
||||
|
||||
bool m_showStateShare = false;
|
||||
ImGuiStateShare m_stateShare;
|
||||
|
||||
bool m_showInputViewer = false;
|
||||
bool m_showInputViewerGyro = false;
|
||||
int m_inputOverlayCorner = 3;
|
||||
std::string m_controllerName;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,9 @@ namespace dusk {
|
||||
}
|
||||
|
||||
void ImGuiMenuTools::ShowProcessManager() {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F2, m_showProcessManagement)) {
|
||||
if (!getSettings().backend.enableAdvancedSettings ||
|
||||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F2, m_showProcessManagement))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "d/actor/d_a_player.h"
|
||||
|
||||
#include <map>
|
||||
#include <bit>
|
||||
|
||||
namespace dusk {
|
||||
enum ItemType {
|
||||
@@ -1354,38 +1353,30 @@ namespace dusk {
|
||||
// genCommonAreaFlags(membit);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
concept FlagIter = requires(T t) {
|
||||
++t;
|
||||
--t;
|
||||
t + 1;
|
||||
t < t;
|
||||
{ t->flagID } -> std::convertible_to<u16>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept FlagTester = requires(T t, u16 flagID) {
|
||||
{ t(flagID) } -> std::convertible_to<bool>;
|
||||
};
|
||||
|
||||
static void sortByFlags(FlagIter auto begin, FlagIter auto end, FlagTester auto&& flagTester) {
|
||||
template <typename FlagIter, typename FlagTester>
|
||||
requires requires(FlagIter a, FlagTester tester) {
|
||||
--a; ++a; a < a; *a;
|
||||
a + 1;
|
||||
{ tester(*a) } -> std::convertible_to<bool>;
|
||||
}
|
||||
static void sortByFlags(FlagIter begin, FlagIter end, FlagTester&& flagTester) {
|
||||
if (begin == end) return;
|
||||
|
||||
FlagIter auto fullEnd = end;
|
||||
auto fullEnd = end;
|
||||
|
||||
// We want to find the location of where we can swap our `On` flags to.
|
||||
// We're gonna put the `Off` bits first, and the `On` bits last. 0 < 1
|
||||
// We can achieve this by skipping all the `On` bits at the end.
|
||||
|
||||
// backtrack until we find a bit that is off
|
||||
while (begin < --end && flagTester(end->flagID)) {
|
||||
while (begin < --end && flagTester(*end)) {
|
||||
// move the end pointer back while we find on bits
|
||||
}
|
||||
|
||||
// end should now be pointing to a bit that is off
|
||||
while (begin < end) {
|
||||
// if there's a flag that's on
|
||||
if (flagTester(begin->flagID)) {
|
||||
if (flagTester(*begin)) {
|
||||
// move it to the end
|
||||
std::rotate(begin, begin + 1, fullEnd);
|
||||
// move back the end of where we're checking
|
||||
@@ -1401,122 +1392,82 @@ namespace dusk {
|
||||
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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));
|
||||
const auto LoadFlag = [&](const EventAreaFlags& flag) -> bool {
|
||||
switch (flag.flag.type) {
|
||||
case AreaFlagType::Item: {
|
||||
return membit.isItem(flag.flag.flagID);
|
||||
} break;
|
||||
case AreaFlagType::Switch: {
|
||||
return membit.isSwitch(flag.flag.flagID);
|
||||
} break;
|
||||
case AreaFlagType::Tbox: {
|
||||
return membit.isTbox(flag.flag.flagID);
|
||||
} break;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const auto SetFlag = [&](uint16_t flag, bool set) -> void {
|
||||
const auto byteIndex = getByteIndexFromFlag(flag);
|
||||
const auto SetFlag = [&](const AreaFlagInd& flag, bool set) -> void {
|
||||
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));
|
||||
switch (flag.type) {
|
||||
case AreaFlagType::Item: {
|
||||
membit.onItem(flag.flagID);
|
||||
} break;
|
||||
case AreaFlagType::Switch: {
|
||||
membit.onSwitch(flag.flagID);
|
||||
} break;
|
||||
case AreaFlagType::Tbox: {
|
||||
membit.onTbox(flag.flagID);
|
||||
} break;
|
||||
}
|
||||
} 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));
|
||||
switch (flag.type) {
|
||||
case AreaFlagType::Item: {
|
||||
membit.offItem(flag.flagID);
|
||||
} break;
|
||||
case AreaFlagType::Switch: {
|
||||
membit.offSwitch(flag.flagID);
|
||||
} break;
|
||||
case AreaFlagType::Tbox: {
|
||||
membit.offTbox(flag.flagID);
|
||||
} break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
const auto LoadMultiByteFlag = [&](const AreaFlagMultibit& flag) -> uint8_t {
|
||||
BE(u32)* areaFlags = nullptr;
|
||||
switch (flag.type) {
|
||||
case AreaFlagType::Item: {
|
||||
areaFlags = membit.mItem;
|
||||
} break;
|
||||
case AreaFlagType::Switch: {
|
||||
areaFlags = membit.mSwitch;
|
||||
} break;
|
||||
case AreaFlagType::Tbox: {
|
||||
areaFlags = membit.mTbox;
|
||||
} break;
|
||||
}
|
||||
return val;
|
||||
assert(areaFlags != nullptr);
|
||||
return (areaFlags[flag.index] & flag.mask) >> flag.shift;
|
||||
};
|
||||
|
||||
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 SetMultiByteFlag = [&](const AreaFlagMultibit& flag, uint8_t val) -> void {
|
||||
BE(u32)* areaFlags = nullptr;
|
||||
switch (flag.type) {
|
||||
case AreaFlagType::Item: {
|
||||
areaFlags = membit.mItem;
|
||||
} break;
|
||||
case AreaFlagType::Switch: {
|
||||
areaFlags = membit.mSwitch;
|
||||
} break;
|
||||
case AreaFlagType::Tbox: {
|
||||
areaFlags = membit.mTbox;
|
||||
} break;
|
||||
}
|
||||
};
|
||||
|
||||
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));
|
||||
areaFlags[flag.index] &= ~flag.mask;
|
||||
areaFlags[flag.index] |= (val << flag.shift) & flag.mask;
|
||||
};
|
||||
|
||||
auto iter = imguiAreaFlagLookup.find(areaIndex);
|
||||
@@ -1568,7 +1519,7 @@ namespace dusk {
|
||||
case COLUMN_DESC:
|
||||
return l.description < r.description;
|
||||
case COLUMN_BIT:
|
||||
return l.flagID < r.flagID;
|
||||
return l.GetFlagID() < r.GetFlagID();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -1597,9 +1548,9 @@ namespace dusk {
|
||||
|
||||
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);
|
||||
bool flag = LoadFlag(e);
|
||||
if (ImGui::Checkbox(fmt::format("##_unused_area_flag_{}", e.flag.flagID).c_str(), &flag)) {
|
||||
SetFlag(e.flag, flag);
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
@@ -1611,7 +1562,7 @@ namespace dusk {
|
||||
}
|
||||
|
||||
for (const auto& multiByteFlag : areaFlags.multibyteFlags) {
|
||||
auto flagValue = LoadSpreadMultiByte(multiByteFlag.highOrderflag, multiByteFlag.lowOrderflag);
|
||||
auto flagValue = LoadMultiByteFlag(multiByteFlag.flag);
|
||||
|
||||
const char* currentVal = "UNKNOWN";
|
||||
|
||||
@@ -1623,7 +1574,7 @@ namespace dusk {
|
||||
if (ImGui::BeginCombo(multiByteFlag.name, currentVal)) {
|
||||
for (const auto& [val, name] : multiByteFlag.enumValues) {
|
||||
if (ImGui::Selectable(name)) {
|
||||
SetSpreadMultiByte(multiByteFlag.highOrderflag, multiByteFlag.lowOrderflag, val);
|
||||
SetMultiByteFlag(multiByteFlag.flag, val);
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
@@ -1756,7 +1707,9 @@ namespace dusk {
|
||||
// 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) {
|
||||
const auto testEventFunc = [&event](u16 flag) -> bool { return event.isEventBit(flag); };
|
||||
const auto testEventFunc = [&event](const duskImguiEventFlagEntry& flag) -> bool {
|
||||
return event.isEventBit(flag.flagID);
|
||||
};
|
||||
|
||||
if (direction == ImGuiSortDirection_Ascending) {
|
||||
sortByFlags(std::begin(duskImguiEventFlags),
|
||||
|
||||
@@ -417,7 +417,9 @@ void ImGuiStateShare::draw(bool& open) {
|
||||
}
|
||||
|
||||
void ImGuiMenuTools::ShowStateShare() {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F8, m_showStateShare)) {
|
||||
if (!getSettings().backend.enableAdvancedSettings ||
|
||||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F8, m_showStateShare))
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_stateShare.draw(m_showStateShare);
|
||||
|
||||
+172
-83
@@ -1,49 +1,99 @@
|
||||
#include "iso_validate.hpp"
|
||||
|
||||
#include <SDL3/SDL_iostream.h>
|
||||
#include <nod.h>
|
||||
#include <span>
|
||||
#include <xxhash.h>
|
||||
|
||||
#include "SDL3/SDL_iostream.h"
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t hex_nibble_to_u8(char c) {
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0';
|
||||
if (c >= 'a' && c <= 'f')
|
||||
return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F')
|
||||
return c - 'A' + 10;
|
||||
throw std::invalid_argument("invalid hex character");
|
||||
}
|
||||
|
||||
constexpr uint64_t parse_u64_hex(std::string_view s) {
|
||||
if (s.size() != 16)
|
||||
throw std::invalid_argument("expected 16 hex chars for uint64");
|
||||
|
||||
uint64_t value = 0;
|
||||
for (char c : s) {
|
||||
value = (value << 4) | hex_nibble_to_u8(c);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
constexpr XXH128_hash_t parse_xxh128(std::string_view hex) {
|
||||
if (hex.size() != 32)
|
||||
throw std::invalid_argument("expected 32 hex chars for XXH128");
|
||||
|
||||
return XXH128_hash_t{
|
||||
.low64 = parse_u64_hex(hex.substr(16, 16)),
|
||||
.high64 = parse_u64_hex(hex.substr(0, 16)),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace dusk::iso {
|
||||
|
||||
constexpr const char* TP_GAME_IDS[] = {
|
||||
"GZ2E01", // GCN USA
|
||||
"GZ2P01", // GCN PAL
|
||||
"GZ2J01", // GCN JPN
|
||||
"RZDE01", // Wii USA
|
||||
"RZDP01", // Wii PAL
|
||||
"RZDJ01", // Wii JPN
|
||||
"RZDK01", // Wii KOR
|
||||
enum class Platform : u8 {
|
||||
GameCube,
|
||||
Wii,
|
||||
};
|
||||
|
||||
constexpr const char* PAL_GAME_IDS[] = {
|
||||
"GZ2P01", // GCN PAL
|
||||
"RZDP01", // Wii PAL
|
||||
enum class Region : u8 {
|
||||
NorthAmerica,
|
||||
Europe,
|
||||
Japan,
|
||||
Korea,
|
||||
};
|
||||
|
||||
constexpr const char* SUPPORTED_TP_GAME_IDS[] = {
|
||||
"GZ2E01", // GCN USA
|
||||
"GZ2P01", // GCN PAL
|
||||
struct KnownDisc {
|
||||
std::string_view id;
|
||||
Platform platform;
|
||||
Region region;
|
||||
bool supported = false;
|
||||
XXH128_hash_t hash{};
|
||||
|
||||
constexpr KnownDisc(std::string_view id, Platform platform, Region region)
|
||||
: id(id), platform(platform), region(region) {}
|
||||
constexpr KnownDisc(
|
||||
std::string_view id, Platform platform, Region region, const std::string_view hash)
|
||||
: id(id), platform(platform), region(region), supported(true), hash(parse_xxh128(hash)) {}
|
||||
};
|
||||
|
||||
template <size_t N>
|
||||
constexpr bool matches(const char (&id)[6], const char* const (&valid)[N]) {
|
||||
for (auto elem : valid) {
|
||||
if (strncmp(id, elem, 6) == 0) {
|
||||
return true;
|
||||
}
|
||||
constexpr auto KNOWN_DISCS = std::to_array<KnownDisc>({
|
||||
{"GZ2E01", Platform::GameCube, Region::NorthAmerica, "14e886f08e548a000afde98a3195e788"},
|
||||
{"GZ2J01", Platform::GameCube, Region::Japan},
|
||||
{"GZ2P01", Platform::GameCube, Region::Europe, "9ef597588b0035ca9e91b333fa9a8a7e"},
|
||||
{"RZDE01", Platform::Wii, Region::NorthAmerica},
|
||||
{"RZDJ01", Platform::Wii, Region::Japan},
|
||||
{"RZDK01", Platform::Wii, Region::Korea},
|
||||
{"RZDP01", Platform::Wii, Region::Europe},
|
||||
});
|
||||
|
||||
constexpr const KnownDisc* find_disc(std::string_view id) {
|
||||
for (const auto& disc : KNOWN_DISCS) {
|
||||
if (disc.id == id)
|
||||
return &disc;
|
||||
}
|
||||
|
||||
return false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
struct NodHandleWrapper {
|
||||
NodHandle* handle;
|
||||
|
||||
NodHandleWrapper() : handle(nullptr) {
|
||||
}
|
||||
|
||||
NodHandleWrapper() : handle(nullptr) {}
|
||||
~NodHandleWrapper() {
|
||||
if (handle != nullptr) {
|
||||
nod_free(handle);
|
||||
@@ -52,7 +102,7 @@ struct NodHandleWrapper {
|
||||
}
|
||||
};
|
||||
|
||||
static ValidationError convertNodError(NodResult result) {
|
||||
static ValidationError convert_nod_error(NodResult result) {
|
||||
switch (result) {
|
||||
case NOD_RESULT_ERR_IO:
|
||||
return ValidationError::IOError;
|
||||
@@ -67,96 +117,135 @@ s64 StreamReadAt(void* user_data, u64 offset, void* out, size_t len) {
|
||||
if (len == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto io = static_cast<SDL_IOStream*>(user_data);
|
||||
|
||||
auto ret = SDL_SeekIO(io, static_cast<s64>(offset), SDL_IO_SEEK_SET);
|
||||
auto* io = static_cast<SDL_IOStream*>(user_data);
|
||||
const auto ret = SDL_SeekIO(io, static_cast<s64>(offset), SDL_IO_SEEK_SET);
|
||||
if (ret < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto read = SDL_ReadIO(io, out, len);
|
||||
const auto read = SDL_ReadIO(io, out, len);
|
||||
if (read == 0) {
|
||||
if (SDL_GetIOStatus(io) == SDL_IO_STATUS_EOF) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
return static_cast<s64>(read);
|
||||
}
|
||||
|
||||
s64 StreamLength(void* user_data) {
|
||||
auto io = static_cast<SDL_IOStream*>(user_data);
|
||||
return SDL_GetIOSize(io);
|
||||
return SDL_GetIOSize(static_cast<SDL_IOStream*>(user_data));
|
||||
}
|
||||
|
||||
void StreamClose(void* user_data) {
|
||||
auto io = static_cast<SDL_IOStream*>(user_data);
|
||||
SDL_CloseIO(io);
|
||||
SDL_CloseIO(static_cast<SDL_IOStream*>(user_data));
|
||||
}
|
||||
|
||||
ValidationError validate(const char* path) {
|
||||
NodHandleWrapper disc;
|
||||
ValidationError verify_disc(NodHandle* disc, VerificationStatus& status) {
|
||||
std::unique_ptr<XXH3_state_t, decltype(&XXH3_freeState)> hashState(
|
||||
XXH3_createState(), XXH3_freeState);
|
||||
if (!hashState) {
|
||||
return ValidationError::Unknown;
|
||||
}
|
||||
XXH3_128bits_reset(hashState.get());
|
||||
|
||||
while (true) {
|
||||
if (status.shouldCancel.load(std::memory_order_relaxed)) {
|
||||
return ValidationError::Canceled;
|
||||
}
|
||||
|
||||
size_t bytesAvail;
|
||||
const auto buf = nod_buf_read(disc, &bytesAvail);
|
||||
if (!bytesAvail)
|
||||
break;
|
||||
|
||||
XXH3_128bits_update(hashState.get(), buf, bytesAvail);
|
||||
|
||||
status.bytesRead.fetch_add(bytesAvail, std::memory_order_relaxed);
|
||||
nod_buf_consume(disc, bytesAvail);
|
||||
}
|
||||
|
||||
const auto hash = XXH3_128bits_digest(hashState.get());
|
||||
if (!XXH128_isEqual(hash, status.knownDisc->hash)) {
|
||||
return ValidationError::HashMismatch;
|
||||
}
|
||||
return ValidationError::Success;
|
||||
}
|
||||
|
||||
ValidationError validate(const char* path, VerificationStatus& status, DiscInfo& info) {
|
||||
const auto sdlStream = SDL_IOFromFile(path, "rb");
|
||||
if (sdlStream == nullptr) {
|
||||
return ValidationError::IOError;
|
||||
}
|
||||
|
||||
const NodDiscStream nod_stream {
|
||||
.user_data = sdlStream,
|
||||
.read_at = StreamReadAt,
|
||||
.stream_len = StreamLength,
|
||||
.close = StreamClose,
|
||||
};
|
||||
|
||||
auto result = nod_disc_open_stream(&nod_stream, nullptr, &disc.handle);
|
||||
if (disc.handle == nullptr || result != NOD_RESULT_OK) {
|
||||
return convertNodError(result);
|
||||
}
|
||||
|
||||
NodDiscHeader header{};
|
||||
result = nod_disc_header(disc.handle, &header);
|
||||
if (result != NOD_RESULT_OK) {
|
||||
return convertNodError(result);
|
||||
}
|
||||
|
||||
if (!matches(header.game_id, TP_GAME_IDS)) {
|
||||
return ValidationError::WrongGame;
|
||||
}
|
||||
|
||||
if (!matches(header.game_id, SUPPORTED_TP_GAME_IDS)) {
|
||||
return ValidationError::WrongVersion;
|
||||
}
|
||||
|
||||
return ValidationError::Success;
|
||||
}
|
||||
bool isPal(const char* path) {
|
||||
NodHandleWrapper disc;
|
||||
|
||||
const auto sdlStream = SDL_IOFromFile(path, "rb");
|
||||
if (sdlStream == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const NodDiscStream nod_stream{
|
||||
.user_data = sdlStream,
|
||||
.read_at = StreamReadAt,
|
||||
.stream_len = StreamLength,
|
||||
.close = StreamClose,
|
||||
};
|
||||
auto result = nod_disc_open_stream(&nod_stream, nullptr, &disc.handle);
|
||||
if (disc.handle == nullptr || result != NOD_RESULT_OK) {
|
||||
return convert_nod_error(result);
|
||||
}
|
||||
|
||||
if (nod_disc_open_stream(&nod_stream, nullptr, &disc.handle) != NOD_RESULT_OK || disc.handle == nullptr) {
|
||||
return false;
|
||||
status.bytesTotal.store(nod_disc_size(disc.handle), std::memory_order_relaxed);
|
||||
|
||||
NodDiscHeader header{};
|
||||
result = nod_disc_header(disc.handle, &header);
|
||||
if (result != NOD_RESULT_OK) {
|
||||
return convert_nod_error(result);
|
||||
}
|
||||
|
||||
const auto knownDisc = find_disc(std::string_view(header.game_id, 6));
|
||||
if (!knownDisc) {
|
||||
return ValidationError::WrongGame;
|
||||
}
|
||||
status.knownDisc = knownDisc;
|
||||
info.isPal = knownDisc->region == Region::Europe;
|
||||
if (!knownDisc->supported) {
|
||||
return ValidationError::WrongVersion;
|
||||
}
|
||||
return verify_disc(disc.handle, status);
|
||||
}
|
||||
|
||||
ValidationError inspect(const char* path, DiscInfo& info) {
|
||||
const auto sdlStream = SDL_IOFromFile(path, "rb");
|
||||
if (sdlStream == nullptr) {
|
||||
return ValidationError::IOError;
|
||||
}
|
||||
|
||||
NodHandleWrapper disc;
|
||||
const NodDiscStream nod_stream{
|
||||
.user_data = sdlStream,
|
||||
.read_at = StreamReadAt,
|
||||
.stream_len = StreamLength,
|
||||
.close = StreamClose,
|
||||
};
|
||||
auto result = nod_disc_open_stream(&nod_stream, nullptr, &disc.handle);
|
||||
if (disc.handle == nullptr || result != NOD_RESULT_OK) {
|
||||
return convert_nod_error(result);
|
||||
}
|
||||
|
||||
NodDiscHeader header{};
|
||||
if (nod_disc_header(disc.handle, &header) != NOD_RESULT_OK) {
|
||||
return false;
|
||||
result = nod_disc_header(disc.handle, &header);
|
||||
if (result != NOD_RESULT_OK) {
|
||||
return convert_nod_error(result);
|
||||
}
|
||||
|
||||
return matches(header.game_id, PAL_GAME_IDS);
|
||||
const auto knownDisc = find_disc(std::string_view(header.game_id, 6));
|
||||
if (!knownDisc) {
|
||||
return ValidationError::WrongGame;
|
||||
}
|
||||
info.isPal = knownDisc->region == Region::Europe;
|
||||
if (!knownDisc->supported) {
|
||||
return ValidationError::WrongVersion;
|
||||
}
|
||||
return ValidationError::Success;
|
||||
}
|
||||
} // namespace dusk::iso
|
||||
|
||||
bool isPal(const char* path) {
|
||||
DiscInfo info{};
|
||||
return inspect(path, info) == ValidationError::Success && info.isPal;
|
||||
}
|
||||
} // namespace dusk::iso
|
||||
|
||||
+31
-13
@@ -1,19 +1,37 @@
|
||||
#ifndef DUSK_ISO_VALIDATE_HPP
|
||||
#define DUSK_ISO_VALIDATE_HPP
|
||||
|
||||
namespace dusk::iso {
|
||||
enum class ValidationError : u8 {
|
||||
Success = 0,
|
||||
IOError,
|
||||
InvalidImage,
|
||||
WrongGame,
|
||||
WrongVersion,
|
||||
ExecutableMismatch,
|
||||
Unknown
|
||||
};
|
||||
#include <atomic>
|
||||
|
||||
ValidationError validate(const char* path);
|
||||
bool isPal(const char* path);
|
||||
}
|
||||
namespace dusk::iso {
|
||||
struct KnownDisc;
|
||||
|
||||
enum class ValidationError : u8 {
|
||||
Unknown = 0,
|
||||
IOError,
|
||||
InvalidImage,
|
||||
WrongGame,
|
||||
WrongVersion,
|
||||
Canceled,
|
||||
HashMismatch,
|
||||
Success
|
||||
};
|
||||
|
||||
struct VerificationStatus {
|
||||
std::atomic_size_t bytesRead = 0;
|
||||
std::atomic_size_t bytesTotal = 0;
|
||||
const KnownDisc* knownDisc = nullptr;
|
||||
std::atomic_bool shouldCancel = false;
|
||||
};
|
||||
|
||||
struct DiscInfo {
|
||||
bool isPal = false;
|
||||
};
|
||||
|
||||
ValidationError inspect(const char* path, DiscInfo& info);
|
||||
ValidationError validate(const char* path, VerificationStatus& status, DiscInfo& info);
|
||||
bool isPal(const char* path);
|
||||
|
||||
} // namespace dusk::iso
|
||||
|
||||
#endif // DUSK_ISO_VALIDATE_HPP
|
||||
|
||||
+25
-13
@@ -8,6 +8,8 @@ UserSettings g_userSettings = {
|
||||
.enableFullscreen {"video.enableFullscreen", false},
|
||||
.enableVsync {"video.enableVsync", true},
|
||||
.lockAspectRatio {"video.lockAspectRatio", false},
|
||||
.enableFpsOverlay {"game.enableFpsOverlay", false},
|
||||
.fpsOverlayCorner {"game.fpsOverlayCorner", 0},
|
||||
},
|
||||
|
||||
.audio = {
|
||||
@@ -45,9 +47,9 @@ UserSettings g_userSettings = {
|
||||
|
||||
// Preferences
|
||||
.enableMirrorMode {"game.enableMirrorMode", false},
|
||||
.disableMainHUD {"game.disableMainHUD", false},
|
||||
.minimalHUD {"game.minimalHUD", false},
|
||||
.pauseOnFocusLost {"game.pauseOnFocusLost", false},
|
||||
.enableLinkDollRotation = {"game.enableLinkDollRotation", false},
|
||||
.enableLinkDollRotation {"game.enableLinkDollRotation", false},
|
||||
.enableAchievementNotifications {"game.enableAchievementNotifications", true},
|
||||
|
||||
// Graphics
|
||||
@@ -79,18 +81,19 @@ UserSettings g_userSettings = {
|
||||
.invertCameraYAxis {"game.invertCameraYAxis", false},
|
||||
.freeCameraSensitivity {"game.freeCameraSensitivity", 1.0f},
|
||||
.debugFlyCam {"game.debugFlyCam", false},
|
||||
.debugFlyCamLockEvents {"game.debugFlyCamLockEvents", true},
|
||||
|
||||
// Cheats
|
||||
.infiniteHearts {"game.infiniteHearts", false},
|
||||
.infiniteArrows{"game.infiniteArrows", false},
|
||||
.infiniteBombs{"game.infiniteBombs", false},
|
||||
.infiniteOil{"game.infiniteOil", false},
|
||||
.infiniteOxygen{"game.infiniteOxygen", false},
|
||||
.infiniteRupees{"game.infiniteRupees", false},
|
||||
.infiniteArrows {"game.infiniteArrows", false},
|
||||
.infiniteBombs {"game.infiniteBombs", false},
|
||||
.infiniteOil {"game.infiniteOil", false},
|
||||
.infiniteOxygen {"game.infiniteOxygen", false},
|
||||
.infiniteRupees {"game.infiniteRupees", false},
|
||||
.enableIndefiniteItemDrops {"game.enableIndefiniteItemDrops", false},
|
||||
.moonJump{"game.moonJump", false},
|
||||
.superClawshot{"game.superClawshot", false},
|
||||
.alwaysGreatspin{"game.alwaysGreatspin", false},
|
||||
.moonJump {"game.moonJump", false},
|
||||
.superClawshot {"game.superClawshot", false},
|
||||
.alwaysGreatspin {"game.alwaysGreatspin", false},
|
||||
.enableFastIronBoots {"game.enableFastIronBoots", false},
|
||||
.canTransformAnywhere {"game.canTransformAnywhere", false},
|
||||
.fastSpinner {"game.fastSpinner", false},
|
||||
@@ -104,17 +107,20 @@ UserSettings g_userSettings = {
|
||||
|
||||
// Tools
|
||||
.speedrunMode {"game.speedrunMode", false},
|
||||
.liveSplitEnabled {"game.liveSplitEnabled", false}
|
||||
.liveSplitEnabled {"game.liveSplitEnabled", false},
|
||||
.recordingMode {"game.recordingMode", false}
|
||||
},
|
||||
|
||||
.backend = {
|
||||
.isoPath {"backend.isoPath", ""},
|
||||
.isoVerification {"backend.isoVerification", DiscVerificationState::Unknown},
|
||||
.graphicsBackend {"backend.graphicsBackend", "auto"},
|
||||
.skipPreLaunchUI {"backend.skipPreLaunchUI", false},
|
||||
.showPipelineCompilation {"backend.showPipelineCompilation", false},
|
||||
.wasPresetChosen {"backend.wasPresetChosen", false},
|
||||
.enableCrashReporting {"backend.enableCrashReporting", true},
|
||||
.cardFileType {"backend.cardFileType", static_cast<int>(CARD_GCIFOLDER)}
|
||||
.cardFileType {"backend.cardFileType", static_cast<int>(CARD_GCIFOLDER)},
|
||||
.enableAdvancedSettings {"backend.enableAdvancedSettings", false},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -127,6 +133,8 @@ void registerSettings() {
|
||||
Register(g_userSettings.video.enableFullscreen);
|
||||
Register(g_userSettings.video.enableVsync);
|
||||
Register(g_userSettings.video.lockAspectRatio);
|
||||
Register(g_userSettings.video.enableFpsOverlay);
|
||||
Register(g_userSettings.video.fpsOverlayCorner);
|
||||
|
||||
// Audio
|
||||
Register(g_userSettings.audio.masterVolume);
|
||||
@@ -160,7 +168,7 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.invertCameraXAxis);
|
||||
Register(g_userSettings.game.invertCameraYAxis);
|
||||
Register(g_userSettings.game.freeCameraSensitivity);
|
||||
Register(g_userSettings.game.disableMainHUD);
|
||||
Register(g_userSettings.game.minimalHUD);
|
||||
Register(g_userSettings.game.pauseOnFocusLost);
|
||||
Register(g_userSettings.game.bloomMode);
|
||||
Register(g_userSettings.game.bloomMultiplier);
|
||||
@@ -181,6 +189,7 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.enableTurboKeybind);
|
||||
Register(g_userSettings.game.speedrunMode);
|
||||
Register(g_userSettings.game.liveSplitEnabled);
|
||||
Register(g_userSettings.game.recordingMode);
|
||||
Register(g_userSettings.game.fastSpinner);
|
||||
Register(g_userSettings.game.infiniteHearts);
|
||||
Register(g_userSettings.game.infiniteArrows);
|
||||
@@ -204,14 +213,17 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.gyroInvertYaw);
|
||||
Register(g_userSettings.game.freeCamera);
|
||||
Register(g_userSettings.game.debugFlyCam);
|
||||
Register(g_userSettings.game.debugFlyCamLockEvents);
|
||||
|
||||
Register(g_userSettings.backend.isoPath);
|
||||
Register(g_userSettings.backend.isoVerification);
|
||||
Register(g_userSettings.backend.graphicsBackend);
|
||||
Register(g_userSettings.backend.skipPreLaunchUI);
|
||||
Register(g_userSettings.backend.showPipelineCompilation);
|
||||
Register(g_userSettings.backend.wasPresetChosen);
|
||||
Register(g_userSettings.backend.enableCrashReporting);
|
||||
Register(g_userSettings.backend.cardFileType);
|
||||
Register(g_userSettings.backend.enableAdvancedSettings);
|
||||
}
|
||||
|
||||
// Transient settings
|
||||
|
||||
@@ -41,7 +41,7 @@ Rml::String build_achievement_info_rml(const Achievement& a) {
|
||||
if (a.isCounter) {
|
||||
float fraction = a.goal > 0 ? float(a.progress) / float(a.goal) : 1.0f;
|
||||
s += fmt::format(
|
||||
R"(<progressbar value="{:.3f}" class="{}"/>)"
|
||||
R"(<progress value="{:.3f}" class="{}"/>)"
|
||||
R"(<span class="achievement-progress">{} / {}</span>)",
|
||||
fraction,
|
||||
a.unlocked ? "progress-done" : "progress-ongoing",
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
#include "bool_button.hpp"
|
||||
#include "button.hpp"
|
||||
#include "pane.hpp"
|
||||
#include "select_button.hpp"
|
||||
#include "number_button.hpp"
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <SDL3/SDL_keyboard.h>
|
||||
#include <SDL3/SDL_mouse.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <array>
|
||||
@@ -17,9 +18,17 @@
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
bool keyboard_active(int port) {
|
||||
u32 count = 0;
|
||||
return PADGetKeyButtonBindings(static_cast<u32>(port), &count) != nullptr;
|
||||
}
|
||||
|
||||
Rml::String current_controller_name(int port) {
|
||||
const char* name = PADGetName(port);
|
||||
return name == nullptr ? "None" : name;
|
||||
if (name != nullptr) {
|
||||
return name;
|
||||
}
|
||||
return keyboard_active(port) ? "Keyboard" : "None";
|
||||
}
|
||||
|
||||
Rml::String controller_index_name(u32 index) {
|
||||
@@ -202,6 +211,86 @@ bool keyboard_escape_pressed() {
|
||||
return keys != nullptr && SDL_SCANCODE_ESCAPE < keyCount && keys[SDL_SCANCODE_ESCAPE];
|
||||
}
|
||||
|
||||
Rml::String keyboard_key_name(s32 scancode) {
|
||||
if (scancode == PAD_KEY_INVALID) {
|
||||
return "Not bound";
|
||||
}
|
||||
switch (scancode) {
|
||||
case PAD_KEY_MOUSE_LEFT:
|
||||
return "Mouse Left";
|
||||
case PAD_KEY_MOUSE_MIDDLE:
|
||||
return "Mouse Middle";
|
||||
case PAD_KEY_MOUSE_RIGHT:
|
||||
return "Mouse Right";
|
||||
case PAD_KEY_MOUSE_X1:
|
||||
return "Mouse X1";
|
||||
case PAD_KEY_MOUSE_X2:
|
||||
return "Mouse X2";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (scancode < 0) {
|
||||
return "Unknown";
|
||||
}
|
||||
const char* name = SDL_GetScancodeName(static_cast<SDL_Scancode>(scancode));
|
||||
if (name == nullptr || name[0] == '\0') {
|
||||
return "Unknown";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
bool keyboard_neutral() {
|
||||
int keyCount = 0;
|
||||
const bool* keys = SDL_GetKeyboardState(&keyCount);
|
||||
if (keys != nullptr) {
|
||||
for (int i = 0; i < keyCount; ++i) {
|
||||
if (keys[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
float x, y;
|
||||
if (SDL_GetMouseState(&x, &y) != 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
s32 keyboard_key_pressed() {
|
||||
int keyCount = 0;
|
||||
const bool* keys = SDL_GetKeyboardState(&keyCount);
|
||||
if (keys != nullptr) {
|
||||
for (int i = 1; i < keyCount; ++i) {
|
||||
if (i == SDL_SCANCODE_ESCAPE) {
|
||||
continue;
|
||||
}
|
||||
if (keys[i]) {
|
||||
return static_cast<s32>(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
float x, y;
|
||||
const auto mouseButtons = SDL_GetMouseState(&x, &y);
|
||||
for (int btn = 1; btn <= 5; ++btn) {
|
||||
if (mouseButtons & (1u << (btn - 1))) {
|
||||
return -(btn + 1); // maps to PAD_KEY_MOUSE_LEFT (-2), etc.
|
||||
}
|
||||
}
|
||||
return PAD_KEY_INVALID;
|
||||
}
|
||||
|
||||
u16 percent_to_raw(int percent) {
|
||||
return static_cast<u16>((static_cast<float>(percent) / 100.f) * 32767.f);
|
||||
}
|
||||
|
||||
int deadzone_raw_to_percent(u16 raw) {
|
||||
return static_cast<int>((static_cast<float>(raw) * 100.f) / 32767.f + 0.5f);
|
||||
}
|
||||
|
||||
int rumble_raw_to_percent(u16 raw) {
|
||||
return static_cast<int>((static_cast<float>(raw) / 32767.f) * 100.f + 0.5f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ControllerConfigWindow::ControllerConfigWindow() {
|
||||
@@ -231,6 +320,7 @@ ControllerConfigWindow::ControllerConfigWindow() {
|
||||
}
|
||||
|
||||
void ControllerConfigWindow::hide(bool close) {
|
||||
stop_rumble_test();
|
||||
cancel_pending_binding();
|
||||
Window::hide(close);
|
||||
}
|
||||
@@ -241,16 +331,18 @@ void ControllerConfigWindow::update() {
|
||||
}
|
||||
|
||||
void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) {
|
||||
stop_rumble_test();
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
mRightPane = &rightPane;
|
||||
mActivePort = port;
|
||||
|
||||
auto addPageButton = [this, &leftPane, &rightPane, port](
|
||||
Page page, Rml::String key, auto getValue) {
|
||||
Page page, Rml::String key, auto getValue, auto isDisabled) {
|
||||
leftPane.register_control(leftPane.add_select_button({
|
||||
.key = std::move(key),
|
||||
.getValue = std::move(getValue),
|
||||
.isDisabled = std::move(isDisabled),
|
||||
}),
|
||||
rightPane, [this, port, page](Pane& pane) {
|
||||
mPage = page;
|
||||
@@ -258,10 +350,11 @@ void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) {
|
||||
});
|
||||
};
|
||||
|
||||
addPageButton(Page::Controller, "Controller", [port] { return current_controller_name(port); });
|
||||
addPageButton(Page::Buttons, "Buttons", [] { return Rml::String(">"); });
|
||||
addPageButton(Page::Triggers, "Triggers", [] { return Rml::String(">"); });
|
||||
addPageButton(Page::Sticks, "Sticks", [] { return Rml::String(">"); });
|
||||
addPageButton(Page::Controller, "Controller", [port] { return current_controller_name(port); }, [] { return false; });
|
||||
addPageButton(Page::Buttons, "Buttons", [] { return Rml::String(">"); }, [] { return false; });
|
||||
addPageButton(Page::Triggers, "Triggers", [] { return Rml::String(">"); }, [] { return false; });
|
||||
addPageButton(Page::Sticks, "Sticks", [] { return Rml::String(">"); }, [] { return false; });
|
||||
addPageButton(Page::Rumble, "Rumble", [] { return Rml::String(">"); }, [port] { return !PADSupportsRumbleIntensity(static_cast<u32>(port)); });
|
||||
|
||||
leftPane.add_section("Options");
|
||||
leftPane.register_control(leftPane.add_child<BoolButton>(BoolButton::Props{
|
||||
@@ -311,23 +404,38 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
|
||||
|
||||
switch (page) {
|
||||
case Page::Controller: {
|
||||
const u32 controllerCount = PADCount();
|
||||
if (controllerCount == 0) {
|
||||
pane.add_text("No controllers detected");
|
||||
break;
|
||||
}
|
||||
pane.add_button(
|
||||
{
|
||||
.text = "None",
|
||||
.isSelected =
|
||||
[port] { return PADGetIndexForPort(port) < 0 && !keyboard_active(port); },
|
||||
})
|
||||
.on_pressed([this, port] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
cancel_pending_binding();
|
||||
PADClearPort(port);
|
||||
PADSetKeyboardActive(static_cast<u32>(port), FALSE);
|
||||
PADSerializeMappings();
|
||||
});
|
||||
|
||||
pane.add_button({
|
||||
.text = "None",
|
||||
.isSelected = [port] { return PADGetIndexForPort(port) < 0; },
|
||||
.text = "Keyboard",
|
||||
.isSelected = [port] { return keyboard_active(port); },
|
||||
})
|
||||
.on_pressed([this, port] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
cancel_pending_binding();
|
||||
PADClearPort(port);
|
||||
PADSetKeyboardActive(static_cast<u32>(port), TRUE);
|
||||
PADSerializeMappings();
|
||||
});
|
||||
|
||||
const u32 controllerCount = PADCount();
|
||||
if (controllerCount == 0) {
|
||||
pane.add_text("No controllers detected");
|
||||
break;
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < controllerCount; ++i) {
|
||||
pane.add_button(
|
||||
{
|
||||
@@ -338,6 +446,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
|
||||
.on_pressed([this, port, i] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
cancel_pending_binding();
|
||||
PADSetKeyboardActive(static_cast<u32>(port), FALSE);
|
||||
PADSetPortForIndex(i, port);
|
||||
PADSerializeMappings();
|
||||
});
|
||||
@@ -345,6 +454,54 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
|
||||
break;
|
||||
}
|
||||
case Page::Buttons: {
|
||||
if (keyboard_active(port)) {
|
||||
auto addKeyButton = [&](PADButton button) {
|
||||
pane.add_select_button(
|
||||
{
|
||||
.key = PADGetButtonName(button),
|
||||
.getValue =
|
||||
[this, port, button] {
|
||||
if (mPendingKeyButton == static_cast<int>(button)) {
|
||||
return pending_key_label();
|
||||
}
|
||||
u32 count = 0;
|
||||
PADKeyButtonBinding* bindings =
|
||||
PADGetKeyButtonBindings(static_cast<u32>(port), &count);
|
||||
if (bindings == nullptr) {
|
||||
return Rml::String("Not bound");
|
||||
}
|
||||
for (u32 i = 0; i < PAD_BUTTON_COUNT; ++i) {
|
||||
if (bindings[i].padButton == button) {
|
||||
return keyboard_key_name(bindings[i].scancode);
|
||||
}
|
||||
}
|
||||
return Rml::String("Not bound");
|
||||
},
|
||||
})
|
||||
.on_pressed([this, port, button] {
|
||||
cancel_pending_binding();
|
||||
mPendingPort = port;
|
||||
mPendingBindingArmed = false;
|
||||
mPendingKeyButton = static_cast<int>(button);
|
||||
});
|
||||
};
|
||||
|
||||
pane.add_section("Buttons");
|
||||
addKeyButton(PAD_BUTTON_A);
|
||||
addKeyButton(PAD_BUTTON_B);
|
||||
addKeyButton(PAD_BUTTON_X);
|
||||
addKeyButton(PAD_BUTTON_Y);
|
||||
addKeyButton(PAD_BUTTON_START);
|
||||
addKeyButton(PAD_TRIGGER_Z);
|
||||
|
||||
pane.add_section("D-Pad");
|
||||
addKeyButton(PAD_BUTTON_UP);
|
||||
addKeyButton(PAD_BUTTON_DOWN);
|
||||
addKeyButton(PAD_BUTTON_LEFT);
|
||||
addKeyButton(PAD_BUTTON_RIGHT);
|
||||
break;
|
||||
}
|
||||
|
||||
u32 buttonCount = 0;
|
||||
PADButtonMapping* mappings = PADGetButtonMappings(port, &buttonCount);
|
||||
if (mappings == nullptr) {
|
||||
@@ -407,6 +564,79 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
|
||||
break;
|
||||
}
|
||||
case Page::Triggers: {
|
||||
if (keyboard_active(port)) {
|
||||
auto addKeyButton = [&](PADButton button) {
|
||||
pane.add_select_button(
|
||||
{
|
||||
.key = PADGetButtonName(button),
|
||||
.getValue =
|
||||
[this, port, button] {
|
||||
if (mPendingKeyButton == static_cast<int>(button)) {
|
||||
return pending_key_label();
|
||||
}
|
||||
u32 count = 0;
|
||||
PADKeyButtonBinding* bindings =
|
||||
PADGetKeyButtonBindings(static_cast<u32>(port), &count);
|
||||
if (bindings == nullptr) {
|
||||
return Rml::String("Not bound");
|
||||
}
|
||||
for (u32 i = 0; i < PAD_BUTTON_COUNT; ++i) {
|
||||
if (bindings[i].padButton == button) {
|
||||
return keyboard_key_name(bindings[i].scancode);
|
||||
}
|
||||
}
|
||||
return Rml::String("Not bound");
|
||||
},
|
||||
})
|
||||
.on_pressed([this, port, button] {
|
||||
cancel_pending_binding();
|
||||
mPendingPort = port;
|
||||
mPendingBindingArmed = false;
|
||||
mPendingKeyButton = static_cast<int>(button);
|
||||
});
|
||||
};
|
||||
|
||||
auto addKeyAxis = [&](PADAxis axis) {
|
||||
pane.add_select_button(
|
||||
{
|
||||
.key = PADGetAxisName(axis),
|
||||
.getValue =
|
||||
[this, port, axis] {
|
||||
if (mPendingKeyAxis == static_cast<int>(axis)) {
|
||||
return pending_key_label();
|
||||
}
|
||||
u32 count = 0;
|
||||
PADKeyAxisBinding* bindings =
|
||||
PADGetKeyAxisBindings(static_cast<u32>(port), &count);
|
||||
if (bindings == nullptr) {
|
||||
return Rml::String("Not bound");
|
||||
}
|
||||
for (u32 i = 0; i < PAD_AXIS_COUNT; ++i) {
|
||||
if (bindings[i].padAxis == axis) {
|
||||
return keyboard_key_name(bindings[i].scancode);
|
||||
}
|
||||
}
|
||||
return Rml::String("Not bound");
|
||||
},
|
||||
})
|
||||
.on_pressed([this, port, axis] {
|
||||
cancel_pending_binding();
|
||||
mPendingPort = port;
|
||||
mPendingBindingArmed = false;
|
||||
mPendingKeyAxis = static_cast<int>(axis);
|
||||
});
|
||||
};
|
||||
|
||||
pane.add_section("Analog");
|
||||
addKeyAxis(PAD_AXIS_TRIGGER_L);
|
||||
addKeyAxis(PAD_AXIS_TRIGGER_R);
|
||||
|
||||
pane.add_section("Digital");
|
||||
addKeyButton(PAD_TRIGGER_L);
|
||||
addKeyButton(PAD_TRIGGER_R);
|
||||
break;
|
||||
}
|
||||
|
||||
u32 axisCount = 0;
|
||||
PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount);
|
||||
u32 buttonCount = 0;
|
||||
@@ -470,9 +700,87 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (PADDeadZones* deadZones = PADGetDeadZones(port)) {
|
||||
pane.add_section("Emulated Trigger Thresholds");
|
||||
pane.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "L Threshold",
|
||||
.getValue = [deadZones] { return deadzone_raw_to_percent(deadZones->leftTriggerActivationZone); },
|
||||
.setValue =
|
||||
[deadZones](int value) {
|
||||
deadZones->leftTriggerActivationZone = percent_to_raw(value);
|
||||
PADSerializeMappings();
|
||||
},
|
||||
.isDisabled = [deadZones] { return !deadZones->emulateTriggers; },
|
||||
.min = 0,
|
||||
.max = 100,
|
||||
.step = 1,
|
||||
.suffix = "%",
|
||||
});
|
||||
pane.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "R Threshold",
|
||||
.getValue = [deadZones] { return deadzone_raw_to_percent(deadZones->rightTriggerActivationZone); },
|
||||
.setValue =
|
||||
[deadZones](int value) {
|
||||
deadZones->rightTriggerActivationZone = percent_to_raw(value);
|
||||
PADSerializeMappings();
|
||||
},
|
||||
.isDisabled = [deadZones] { return !deadZones->emulateTriggers; },
|
||||
.min = 0,
|
||||
.max = 100,
|
||||
.step = 1,
|
||||
.suffix = "%",
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Page::Sticks: {
|
||||
if (keyboard_active(port)) {
|
||||
auto addKeyAxis = [&](PADAxis axis) {
|
||||
pane.add_select_button(
|
||||
{
|
||||
.key = PADGetAxisDirectionLabel(axis),
|
||||
.getValue =
|
||||
[this, port, axis] {
|
||||
if (mPendingKeyAxis == static_cast<int>(axis)) {
|
||||
return pending_key_label();
|
||||
}
|
||||
u32 count = 0;
|
||||
PADKeyAxisBinding* bindings =
|
||||
PADGetKeyAxisBindings(static_cast<u32>(port), &count);
|
||||
if (bindings == nullptr) {
|
||||
return Rml::String("Not bound");
|
||||
}
|
||||
for (u32 i = 0; i < PAD_AXIS_COUNT; ++i) {
|
||||
if (bindings[i].padAxis == axis) {
|
||||
return keyboard_key_name(bindings[i].scancode);
|
||||
}
|
||||
}
|
||||
return Rml::String("Not bound");
|
||||
},
|
||||
})
|
||||
.on_pressed([this, port, axis] {
|
||||
cancel_pending_binding();
|
||||
mPendingPort = port;
|
||||
mPendingBindingArmed = false;
|
||||
mPendingKeyAxis = static_cast<int>(axis);
|
||||
});
|
||||
};
|
||||
|
||||
pane.add_section("Control Stick");
|
||||
addKeyAxis(PAD_AXIS_LEFT_Y_POS);
|
||||
addKeyAxis(PAD_AXIS_LEFT_Y_NEG);
|
||||
addKeyAxis(PAD_AXIS_LEFT_X_NEG);
|
||||
addKeyAxis(PAD_AXIS_LEFT_X_POS);
|
||||
|
||||
pane.add_section("C Stick");
|
||||
addKeyAxis(PAD_AXIS_RIGHT_Y_POS);
|
||||
addKeyAxis(PAD_AXIS_RIGHT_Y_NEG);
|
||||
addKeyAxis(PAD_AXIS_RIGHT_X_NEG);
|
||||
addKeyAxis(PAD_AXIS_RIGHT_X_POS);
|
||||
break;
|
||||
}
|
||||
|
||||
u32 axisCount = 0;
|
||||
PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount);
|
||||
if (axes == nullptr) {
|
||||
@@ -509,12 +817,121 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
|
||||
addAxis(PAD_AXIS_LEFT_Y_NEG);
|
||||
addAxis(PAD_AXIS_LEFT_X_NEG);
|
||||
addAxis(PAD_AXIS_LEFT_X_POS);
|
||||
if (PADDeadZones* deadZones = PADGetDeadZones(port)) {
|
||||
pane.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Deadzone",
|
||||
.getValue = [deadZones] { return deadzone_raw_to_percent(deadZones->stickDeadZone); },
|
||||
.setValue =
|
||||
[deadZones](int value) {
|
||||
deadZones->stickDeadZone = percent_to_raw(value);
|
||||
PADSerializeMappings();
|
||||
},
|
||||
.isDisabled = [deadZones] { return !deadZones->useDeadzones; },
|
||||
.min = 0,
|
||||
.max = 100,
|
||||
.step = 1,
|
||||
.suffix = "%",
|
||||
});
|
||||
}
|
||||
|
||||
pane.add_section("C Stick");
|
||||
addAxis(PAD_AXIS_RIGHT_Y_POS);
|
||||
addAxis(PAD_AXIS_RIGHT_Y_NEG);
|
||||
addAxis(PAD_AXIS_RIGHT_X_NEG);
|
||||
addAxis(PAD_AXIS_RIGHT_X_POS);
|
||||
if (PADDeadZones* deadZones = PADGetDeadZones(port)) {
|
||||
pane.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Deadzone",
|
||||
.getValue = [deadZones] { return deadzone_raw_to_percent(deadZones->substickDeadZone); },
|
||||
.setValue =
|
||||
[deadZones](int value) {
|
||||
deadZones->substickDeadZone = percent_to_raw(value);
|
||||
PADSerializeMappings();
|
||||
},
|
||||
.isDisabled = [deadZones] { return !deadZones->useDeadzones; },
|
||||
.min = 0,
|
||||
.max = 100,
|
||||
.step = 1,
|
||||
.suffix = "%",
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case Page::Rumble: {
|
||||
auto& rumbleTest = pane.add_select_button({
|
||||
.key = "Test Rumble",
|
||||
.getValue =
|
||||
[this, port] {
|
||||
return (mRumbleTestActive && mRumbleTestPort == port) ? Rml::String("Stop")
|
||||
: Rml::String("Start");
|
||||
},
|
||||
});
|
||||
rumbleTest.on_pressed([this, port] {
|
||||
if (!PADSupportsRumbleIntensity(static_cast<u32>(port))) {
|
||||
return;
|
||||
}
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
if (mRumbleTestActive && mRumbleTestPort == port) {
|
||||
PADControlMotor(port, PAD_MOTOR_STOP_HARD);
|
||||
mRumbleTestActive = false;
|
||||
mRumbleTestPort = -1;
|
||||
} else {
|
||||
if (mRumbleTestActive) {
|
||||
PADControlMotor(mRumbleTestPort, PAD_MOTOR_STOP_HARD);
|
||||
}
|
||||
PADControlMotor(port, PAD_MOTOR_RUMBLE);
|
||||
mRumbleTestActive = true;
|
||||
mRumbleTestPort = port;
|
||||
}
|
||||
});
|
||||
pane.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "Low Rumble Frequency",
|
||||
.getValue =
|
||||
[port] {
|
||||
u16 low = 0;
|
||||
u16 high = 0;
|
||||
PADGetRumbleIntensity(static_cast<u32>(port), &low, &high);
|
||||
return rumble_raw_to_percent(low);
|
||||
},
|
||||
.setValue =
|
||||
[port](int value) {
|
||||
u16 low = 0;
|
||||
u16 high = 0;
|
||||
PADGetRumbleIntensity(static_cast<u32>(port), &low, &high);
|
||||
PADSetRumbleIntensity(static_cast<u32>(port), percent_to_raw(value), high);
|
||||
PADSerializeMappings();
|
||||
},
|
||||
.isDisabled = [this] { return mRumbleTestActive; },
|
||||
.min = 0,
|
||||
.max = 100,
|
||||
.step = 1,
|
||||
.suffix = "%",
|
||||
});
|
||||
pane.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = "High Rumble Frequency",
|
||||
.getValue =
|
||||
[port] {
|
||||
u16 low = 0;
|
||||
u16 high = 0;
|
||||
PADGetRumbleIntensity(static_cast<u32>(port), &low, &high);
|
||||
return rumble_raw_to_percent(high);
|
||||
},
|
||||
.setValue =
|
||||
[port](int value) {
|
||||
u16 low = 0;
|
||||
u16 high = 0;
|
||||
PADGetRumbleIntensity(static_cast<u32>(port), &low, &high);
|
||||
PADSetRumbleIntensity(static_cast<u32>(port), low, percent_to_raw(value));
|
||||
PADSerializeMappings();
|
||||
},
|
||||
.isDisabled = [this] { return mRumbleTestActive; },
|
||||
.min = 0,
|
||||
.max = 100,
|
||||
.step = 1,
|
||||
.suffix = "%",
|
||||
});
|
||||
pane.add_text("Configure your desired rumble intensities, then run a test to check how they feel.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -549,6 +966,21 @@ void ControllerConfigWindow::poll_pending_binding() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mPendingKeyButton >= 0 || mPendingKeyAxis >= 0) {
|
||||
const s32 scancode = keyboard_key_pressed();
|
||||
if (scancode != PAD_KEY_INVALID) {
|
||||
if (mPendingKeyButton >= 0) {
|
||||
PADSetKeyButtonBinding(static_cast<u32>(mPendingPort),
|
||||
{scancode, static_cast<PADButton>(mPendingKeyButton)});
|
||||
} else {
|
||||
PADSetKeyAxisBinding(static_cast<u32>(mPendingPort),
|
||||
{scancode, static_cast<PADAxis>(mPendingKeyAxis), 0});
|
||||
}
|
||||
finish_pending_key_binding();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mPendingButtonMapping != nullptr) {
|
||||
const s32 nativeButton = PADGetNativeButtonPressed(mPendingPort);
|
||||
if (nativeButton != -1) {
|
||||
@@ -590,26 +1022,40 @@ void ControllerConfigWindow::finish_pending_binding(int completedPort) {
|
||||
}
|
||||
|
||||
void ControllerConfigWindow::unmap_pending_binding() {
|
||||
if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr) {
|
||||
if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr &&
|
||||
mPendingKeyButton < 0 && mPendingKeyAxis < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int completedPort = mPendingPort;
|
||||
if (mPendingButtonMapping != nullptr) {
|
||||
mPendingButtonMapping->nativeButton = PAD_NATIVE_BUTTON_INVALID;
|
||||
}
|
||||
if (mPendingAxisMapping != nullptr) {
|
||||
finish_pending_binding(completedPort);
|
||||
} else if (mPendingAxisMapping != nullptr) {
|
||||
mPendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE};
|
||||
mPendingAxisMapping->nativeButton = -1;
|
||||
finish_pending_binding(completedPort);
|
||||
} else if (mPendingKeyButton >= 0) {
|
||||
PADSetKeyButtonBinding(static_cast<u32>(completedPort),
|
||||
{PAD_KEY_INVALID, static_cast<PADButton>(mPendingKeyButton)});
|
||||
finish_pending_key_binding();
|
||||
} else if (mPendingKeyAxis >= 0) {
|
||||
PADSetKeyAxisBinding(static_cast<u32>(completedPort),
|
||||
{PAD_KEY_INVALID, static_cast<PADAxis>(mPendingKeyAxis), 0});
|
||||
finish_pending_key_binding();
|
||||
}
|
||||
finish_pending_binding(completedPort);
|
||||
}
|
||||
|
||||
bool ControllerConfigWindow::capture_active() const {
|
||||
return mPendingButtonMapping != nullptr || mPendingAxisMapping != nullptr;
|
||||
return mPendingButtonMapping != nullptr || mPendingAxisMapping != nullptr ||
|
||||
mPendingKeyButton >= 0 || mPendingKeyAxis >= 0;
|
||||
}
|
||||
|
||||
bool ControllerConfigWindow::pending_input_neutral() const {
|
||||
if (mPendingKeyButton >= 0 || mPendingKeyAxis >= 0) {
|
||||
return keyboard_neutral();
|
||||
}
|
||||
return input_neutral(mPendingPort);
|
||||
}
|
||||
|
||||
@@ -623,16 +1069,41 @@ Rml::String ControllerConfigWindow::pending_axis_label() const {
|
||||
|
||||
void ControllerConfigWindow::cancel_pending_binding() {
|
||||
if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr &&
|
||||
!mSuppressNavigationUntilNeutral)
|
||||
!mSuppressNavigationUntilNeutral && mPendingKeyButton < 0 && mPendingKeyAxis < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
mPendingButtonMapping = nullptr;
|
||||
mPendingAxisMapping = nullptr;
|
||||
mPendingKeyButton = -1;
|
||||
mPendingKeyAxis = -1;
|
||||
mPendingPort = -1;
|
||||
mPendingBindingArmed = false;
|
||||
mSuppressNavigationUntilNeutral = false;
|
||||
mSuppressNavigationPort = -1;
|
||||
}
|
||||
|
||||
void ControllerConfigWindow::finish_pending_key_binding() {
|
||||
mPendingKeyButton = -1;
|
||||
mPendingKeyAxis = -1;
|
||||
mPendingPort = -1;
|
||||
mPendingBindingArmed = false;
|
||||
PADSerializeMappings();
|
||||
}
|
||||
|
||||
Rml::String ControllerConfigWindow::pending_key_label() const {
|
||||
return mPendingBindingArmed ? "Press a key or mouse button..." : "Waiting...";
|
||||
}
|
||||
|
||||
void ControllerConfigWindow::stop_rumble_test() {
|
||||
if (!mRumbleTestActive) {
|
||||
return;
|
||||
}
|
||||
if (mRumbleTestPort >= PAD_CHAN0 && mRumbleTestPort < PAD_CHANMAX) {
|
||||
PADControlMotor(mRumbleTestPort, PAD_MOTOR_STOP_HARD);
|
||||
}
|
||||
mRumbleTestActive = false;
|
||||
mRumbleTestPort = -1;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -19,6 +19,7 @@ private:
|
||||
Buttons,
|
||||
Triggers,
|
||||
Sticks,
|
||||
Rumble,
|
||||
};
|
||||
|
||||
void build_port_tab(Rml::Element* content, int port);
|
||||
@@ -32,6 +33,9 @@ private:
|
||||
Rml::String pending_button_label() const;
|
||||
Rml::String pending_axis_label() const;
|
||||
void cancel_pending_binding();
|
||||
void finish_pending_key_binding();
|
||||
Rml::String pending_key_label() const;
|
||||
void stop_rumble_test();
|
||||
|
||||
Page mPage = Page::Controller;
|
||||
Pane* mRightPane = nullptr;
|
||||
@@ -42,6 +46,10 @@ private:
|
||||
int mSuppressNavigationPort = -1;
|
||||
PADButtonMapping* mPendingButtonMapping = nullptr;
|
||||
PADAxisMapping* mPendingAxisMapping = nullptr;
|
||||
int mPendingKeyButton = -1;
|
||||
int mPendingKeyAxis = -1;
|
||||
bool mRumbleTestActive = false;
|
||||
int mRumbleTestPort = -1;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -268,8 +268,8 @@ std::map<int, itemInfo> itemMap = {
|
||||
{dItemNo_DUNGEON_BACK_e, {"Ooccoo Jr.", ITEMTYPE_EQUIP_e}},
|
||||
{dItemNo_SWORD_e, {"Ordon Sword"}},
|
||||
{dItemNo_MASTER_SWORD_e, {"Master Sword"}},
|
||||
{dItemNo_WOOD_SHIELD_e, {"Wooden Shield"}},
|
||||
{dItemNo_SHIELD_e, {"Ordon Shield"}},
|
||||
{dItemNo_WOOD_SHIELD_e, {"Ordon Shield"}},
|
||||
{dItemNo_SHIELD_e, {"Wooden Shield"}},
|
||||
{dItemNo_HYLIA_SHIELD_e, {"Hylian Shield"}},
|
||||
{dItemNo_TKS_LETTER_e, {"Ooccoo's Note", ITEMTYPE_EQUIP_e}},
|
||||
{dItemNo_WEAR_CASUAL_e, {"Ordon Clothes"}},
|
||||
|
||||
@@ -91,18 +91,23 @@ Rml::Element* create_stepped_carousel_arrow(
|
||||
return parent->AppendChild(std::move(button));
|
||||
}
|
||||
|
||||
void update_carousel_arrow_color(Rml::Element* arrow, bool dim) {
|
||||
const Rml::Colourb& color = Rml::Colourb(255, 255, 255, dim ? 128 : 255);
|
||||
arrow->SetProperty(Rml::PropertyId::Color, Rml::Property(color, Rml::Unit::COLOUR));
|
||||
}
|
||||
|
||||
} // 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", "");
|
||||
mPrevElem = 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", "");
|
||||
mNextElem = create_stepped_carousel_arrow(mRoot, "next", "");
|
||||
|
||||
listen(prevElem, Rml::EventId::Click,
|
||||
listen(mPrevElem, Rml::EventId::Click,
|
||||
[this](Rml::Event&) { handle_nav_command(NavCommand::Left); });
|
||||
listen(nextElem, Rml::EventId::Click,
|
||||
listen(mNextElem, 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);
|
||||
@@ -126,6 +131,9 @@ void SteppedCarousel::update() {
|
||||
} else {
|
||||
mValueElem->SetInnerRML(std::to_string(value));
|
||||
}
|
||||
|
||||
update_carousel_arrow_color(mPrevElem, value == mProps.min);
|
||||
update_carousel_arrow_color(mNextElem, value == mProps.max);
|
||||
}
|
||||
|
||||
bool SteppedCarousel::handle_nav_command(NavCommand cmd) {
|
||||
@@ -280,4 +288,4 @@ void GraphicsTuner::reset_default() {
|
||||
set_value(mOption, mDefaultValue);
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -34,6 +34,8 @@ private:
|
||||
void apply(int value);
|
||||
|
||||
Props mProps;
|
||||
Rml::Element* mPrevElem = nullptr;
|
||||
Rml::Element* mNextElem = nullptr;
|
||||
Rml::Element* mValueElem = nullptr;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace dusk::ui::input {
|
||||
namespace {
|
||||
|
||||
constexpr double kGamepadRepeatInitialDelay = 0.32;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk::ui {
|
||||
union SDL_Event;
|
||||
|
||||
namespace dusk::ui::input {
|
||||
|
||||
void handle_event(const SDL_Event& event) noexcept;
|
||||
void update_input() noexcept;
|
||||
void reset_input_state() noexcept;
|
||||
void sync_input_block() noexcept;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "f_pc/f_pc_manager.h"
|
||||
#include "f_pc/f_pc_name.h"
|
||||
#include "imgui.h"
|
||||
#include "modal.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "window.hpp"
|
||||
@@ -50,17 +51,68 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById(
|
||||
// mTabBar->add_tab("Warp", [] {
|
||||
// // TODO
|
||||
// });
|
||||
mTabBar->add_tab("Editor", [this] { push(std::make_unique<EditorWindow>()); });
|
||||
|
||||
if (getSettings().backend.enableAdvancedSettings) {
|
||||
mTabBar->add_tab("Editor", [this] { push(std::make_unique<EditorWindow>()); });
|
||||
}
|
||||
|
||||
mTabBar->add_tab("Achievements", [this] { push(std::make_unique<AchievementsWindow>()); });
|
||||
mTabBar->add_tab("Reset", [this] {
|
||||
mTabBar->set_active_tab(-1);
|
||||
if (fpcM_SearchByName(fpcNm_LOGO_SCENE_e)) {
|
||||
return;
|
||||
}
|
||||
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
|
||||
hide(false);
|
||||
const auto dismiss = [](Modal& modal) { modal.pop(); };
|
||||
push(std::make_unique<Modal>(Modal::Props{
|
||||
.title = "Reset Game",
|
||||
.bodyRml = "Unsaved progress will be lost.<br/>"
|
||||
"<span class=\"tip\">Tip: You can also reset by holding Start+X+B</span>",
|
||||
.actions =
|
||||
{
|
||||
ModalAction{
|
||||
.label = "Cancel",
|
||||
.onPressed = dismiss,
|
||||
},
|
||||
ModalAction{
|
||||
.label = "Reset",
|
||||
.onPressed =
|
||||
[this, dismiss](Modal& modal) {
|
||||
if (fpcM_SearchByName(fpcNm_LOGO_SCENE_e)) {
|
||||
dismiss(modal);
|
||||
return;
|
||||
}
|
||||
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
|
||||
dismiss(modal);
|
||||
hide(false);
|
||||
},
|
||||
},
|
||||
},
|
||||
.onDismiss = dismiss,
|
||||
.icon = "question-mark",
|
||||
}));
|
||||
});
|
||||
mTabBar->add_tab("Quit", [this] {
|
||||
mTabBar->set_active_tab(-1);
|
||||
const auto dismiss = [](Modal& modal) { modal.pop(); };
|
||||
push(std::make_unique<Modal>(Modal::Props{
|
||||
.title = "Quit Dusk",
|
||||
.bodyRml = "Unsaved progress will be lost.",
|
||||
.actions =
|
||||
{
|
||||
ModalAction{
|
||||
.label = "Cancel",
|
||||
.onPressed = dismiss,
|
||||
},
|
||||
ModalAction{
|
||||
.label = "Quit",
|
||||
.onPressed =
|
||||
[dismiss](Modal& modal) {
|
||||
dismiss(modal);
|
||||
IsRunning = false;
|
||||
},
|
||||
},
|
||||
},
|
||||
.onDismiss = dismiss,
|
||||
.icon = "question-mark",
|
||||
}));
|
||||
});
|
||||
mTabBar->add_tab("Quit", [] { IsRunning = false; });
|
||||
|
||||
// Hide document after transition completion
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
|
||||
+16
-5
@@ -2,14 +2,25 @@
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
Modal::Modal(Props props)
|
||||
: WindowSmall("modal", "modal-dialog"), mProps(std::move(props)) {
|
||||
auto* title = append(mDialog, "div");
|
||||
title->SetClass("preset-title", true);
|
||||
Modal::Modal(Props props) : WindowSmall("modal", "modal-dialog"), mProps(std::move(props)) {
|
||||
if (!mProps.variant.empty()) {
|
||||
mRoot->SetClass(mProps.variant, true);
|
||||
}
|
||||
|
||||
auto* header = append(mDialog, "div");
|
||||
header->SetClass("modal-header", true);
|
||||
|
||||
auto* title = append(header, "div");
|
||||
title->SetClass("modal-title", true);
|
||||
title->SetInnerRML(mProps.title);
|
||||
|
||||
if (!mProps.icon.empty()) {
|
||||
auto* icon = append(header, "icon");
|
||||
icon->SetClass(mProps.icon, true);
|
||||
}
|
||||
|
||||
auto* body = append(mDialog, "div");
|
||||
body->SetClass("preset-intro", true);
|
||||
body->SetClass("modal-body", true);
|
||||
body->SetInnerRML(mProps.bodyRml);
|
||||
|
||||
auto* actions = append(mDialog, "div");
|
||||
|
||||
@@ -18,6 +18,8 @@ public:
|
||||
Rml::String bodyRml;
|
||||
std::vector<ModalAction> actions;
|
||||
std::function<void(Modal&)> onDismiss;
|
||||
Rml::String variant;
|
||||
Rml::String icon = "";
|
||||
};
|
||||
|
||||
explicit Modal(Props props);
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "string_button.hpp"
|
||||
|
||||
#include <climits>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class NumberButton : public BaseStringButton {
|
||||
|
||||
+247
-32
@@ -1,11 +1,14 @@
|
||||
#include "overlay.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "magic_enum.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "dusk/achievements.h"
|
||||
#include "magic_enum.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
#include <SDL3/SDL_timer.h>
|
||||
#include <algorithm>
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
@@ -17,51 +20,183 @@ const Rml::String kDocumentSource = R"RML(
|
||||
<link type="text/rcss" href="res/rml/overlay.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<fps id="fps" />
|
||||
</body>
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
constexpr std::array<std::pair<const char*, const char*>, 3> kAutoSaveLayers{{
|
||||
{"inner", "res/org-icon-inner.png"},
|
||||
{"outer", "res/org-icon-outer.png"},
|
||||
{"center", "res/org-icon-center.png"},
|
||||
}};
|
||||
|
||||
constexpr auto kMenuNotificationDuration = std::chrono::milliseconds(2500);
|
||||
|
||||
constexpr std::array<const char*, 4> kFpsCorners = { "tl", "tr", "bl", "br" };
|
||||
|
||||
Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) {
|
||||
if (toast.type == "autosave") {
|
||||
Rml::Factory::InstanceElementText(parent, R"RML(
|
||||
<logo>
|
||||
<img class="inner" src="res/org-icon-inner.png" />
|
||||
<img class="outer" src="res/org-icon-outer.png" />
|
||||
<img class="center" src="res/org-icon-center.png" />
|
||||
</logo>
|
||||
)RML");
|
||||
return parent->GetFirstChild();
|
||||
} else {
|
||||
auto* elem = append(parent, "toast");
|
||||
if (!toast.type.empty()) {
|
||||
elem->SetClass(toast.type, true);
|
||||
auto* logo = append(parent, "logo");
|
||||
for (const auto [cls, src] : kAutoSaveLayers) {
|
||||
auto* img = append(logo, "img");
|
||||
img->SetClass(cls, true);
|
||||
img->SetAttribute("src", src);
|
||||
}
|
||||
{
|
||||
auto* heading = append(elem, "heading");
|
||||
return logo;
|
||||
}
|
||||
|
||||
auto* elem = append(parent, "toast");
|
||||
if (!toast.type.empty()) {
|
||||
elem->SetClass(toast.type, true);
|
||||
}
|
||||
{
|
||||
auto* heading = append(elem, "heading");
|
||||
if (toast.title.starts_with("<")) {
|
||||
heading->SetInnerRML(toast.title);
|
||||
} else {
|
||||
auto* span = append(heading, "span");
|
||||
span->SetInnerRML(toast.title);
|
||||
if (toast.type == "achievement") {
|
||||
auto* icon = append(heading, "icon");
|
||||
icon->SetClass("trophy", true);
|
||||
mDoAud_seStartMenu(kSoundAchievementUnlock);
|
||||
}
|
||||
}
|
||||
{
|
||||
auto* message = append(elem, "message");
|
||||
if (toast.type == "achievement") {
|
||||
auto* icon = append(heading, "icon");
|
||||
icon->SetClass("trophy", true);
|
||||
mDoAud_seStartMenu(kSoundAchievementUnlock);
|
||||
} else if (toast.type == "controller") {
|
||||
auto* icon = append(heading, "icon");
|
||||
icon->SetClass("controller", true);
|
||||
}
|
||||
}
|
||||
{
|
||||
auto* message = append(elem, "message");
|
||||
if (toast.content.starts_with("<")) {
|
||||
message->SetInnerRML(toast.content);
|
||||
} else {
|
||||
auto* span = append(message, "span");
|
||||
span->SetInnerRML(toast.content);
|
||||
}
|
||||
{
|
||||
auto* progress = append(elem, "progress");
|
||||
progress->SetAttribute("value", 1.f);
|
||||
}
|
||||
return elem;
|
||||
}
|
||||
{
|
||||
auto* progress = append(elem, "progress");
|
||||
progress->SetAttribute("value", 1.f);
|
||||
}
|
||||
return elem;
|
||||
}
|
||||
|
||||
Rml::Element* create_controller_warning(Rml::Element* parent) {
|
||||
auto* elem = append(parent, "toast");
|
||||
elem->SetClass("controller-warning", true);
|
||||
|
||||
auto* heading = append(elem, "heading");
|
||||
auto* title = append(heading, "span");
|
||||
title->SetInnerRML("No controller assigned");
|
||||
auto* icon = append(heading, "icon");
|
||||
icon->SetClass("warning", true);
|
||||
|
||||
auto* message = append(elem, "message");
|
||||
auto* content = append(message, "span");
|
||||
content->SetInnerRML("Configure controller port 1 in Settings.");
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
SDL_Gamepad* gamepad_for_port(u32 port) noexcept {
|
||||
const s32 index = PADGetIndexForPort(port);
|
||||
if (index < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
return PADGetSDLGamepadForIndex(static_cast<u32>(index));
|
||||
}
|
||||
|
||||
Rml::String back_button_name() {
|
||||
if (auto* gamepad = gamepad_for_port(PAD_CHAN0)) {
|
||||
switch (SDL_GetGamepadType(gamepad)) {
|
||||
case SDL_GAMEPAD_TYPE_PS3:
|
||||
return "Select";
|
||||
case SDL_GAMEPAD_TYPE_PS4:
|
||||
return "Share";
|
||||
case SDL_GAMEPAD_TYPE_PS5:
|
||||
return "Create";
|
||||
case SDL_GAMEPAD_TYPE_XBOX360:
|
||||
return "Back";
|
||||
case SDL_GAMEPAD_TYPE_XBOXONE:
|
||||
return "View";
|
||||
case SDL_GAMEPAD_TYPE_GAMECUBE:
|
||||
return "R + Start";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return "Back";
|
||||
}
|
||||
|
||||
Rml::Element* create_menu_notification(Rml::Element* parent) {
|
||||
auto* elem = append(parent, "toast");
|
||||
elem->SetClass("menu-notification", true);
|
||||
|
||||
auto* message = append(elem, "message");
|
||||
auto* row = append(message, "row");
|
||||
append(row, "span")->SetInnerRML("Press F1 or");
|
||||
auto* icon = append(row, "icon");
|
||||
icon->SetClass("controller", true);
|
||||
append(row, "span")->SetInnerRML(escape(back_button_name()));
|
||||
append(row, "span")->SetInnerRML("to open menu");
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
void remove_element(Rml::Element*& elem) noexcept {
|
||||
if (elem == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (auto* parent = elem->GetParentNode()) {
|
||||
parent->RemoveChild(elem);
|
||||
}
|
||||
elem = nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// https://vplesko.com/posts/how_to_implement_an_fps_counter.html
|
||||
void Overlay::advance_fps_counter(float& outFps, Uint64 perfFreq) {
|
||||
if (perfFreq == 0) {
|
||||
outFps = 0.f;
|
||||
return;
|
||||
}
|
||||
|
||||
const Uint64 curr = SDL_GetPerformanceCounter();
|
||||
if (!mFpsHavePrevCounter) {
|
||||
mFpsPrevCounter = curr;
|
||||
mFpsHavePrevCounter = true;
|
||||
outFps = 0.f;
|
||||
return;
|
||||
}
|
||||
|
||||
const Uint64 processingTicks = curr - mFpsPrevCounter;
|
||||
mFpsPrevCounter = curr;
|
||||
|
||||
mFpsFrameEvents.push_back({curr, processingTicks});
|
||||
mFpsSumTicks += processingTicks;
|
||||
|
||||
while (!mFpsFrameEvents.empty() && mFpsFrameEvents.front().endCounter + perfFreq < curr) {
|
||||
mFpsSumTicks -= mFpsFrameEvents.front().processingTicks;
|
||||
mFpsFrameEvents.pop_front();
|
||||
}
|
||||
|
||||
const auto n = mFpsFrameEvents.size();
|
||||
if (n == 0 || mFpsSumTicks == 0) {
|
||||
outFps = 0.f;
|
||||
return;
|
||||
}
|
||||
|
||||
const double avgSeconds =
|
||||
static_cast<double>(mFpsSumTicks) / static_cast<double>(n) / static_cast<double>(perfFreq);
|
||||
outFps = static_cast<float>(1.0 / avgSeconds);
|
||||
}
|
||||
|
||||
Overlay::Overlay() : Document(kDocumentSource) {
|
||||
mFpsCounter = mDocument->GetElementById("fps");
|
||||
|
||||
listen(mDocument, Rml::EventId::Focus, [](Rml::Event&) { Log.warn("Overlay received focus"); });
|
||||
listen(mDocument, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mCurrentToast) {
|
||||
@@ -70,6 +205,15 @@ Overlay::Overlay() : Document(kDocumentSource) {
|
||||
{
|
||||
mCurrentToast->SetPseudoClass("done", true);
|
||||
}
|
||||
} else if (mControllerWarning != nullptr &&
|
||||
event.GetTargetElement() == mControllerWarning &&
|
||||
!mControllerWarning->HasAttribute("open"))
|
||||
{
|
||||
mControllerWarning->SetPseudoClass("done", true);
|
||||
} else if (mMenuNotification != nullptr && event.GetTargetElement() == mMenuNotification &&
|
||||
!mMenuNotification->HasAttribute("open"))
|
||||
{
|
||||
mMenuNotification->SetPseudoClass("done", true);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -82,6 +226,78 @@ void Overlay::show() {
|
||||
|
||||
void Overlay::update() {
|
||||
Document::update();
|
||||
if (mDocument == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mFpsCounter != nullptr) {
|
||||
if (getSettings().video.enableFpsOverlay.getValue()) {
|
||||
const int idx = getSettings().video.fpsOverlayCorner.getValue();
|
||||
mFpsCounter->SetAttribute("open", "");
|
||||
mFpsCounter->SetAttribute("corner", kFpsCorners[idx]);
|
||||
|
||||
const Uint64 perfFreq = SDL_GetPerformanceFrequency();
|
||||
float fps = 0.f;
|
||||
advance_fps_counter(fps, perfFreq);
|
||||
|
||||
const Uint64 now = SDL_GetPerformanceCounter();
|
||||
// Limit updates to twice per second
|
||||
const bool refreshLabel = perfFreq == 0 || mFpsLastUpdate == 0 ||
|
||||
static_cast<double>(now - mFpsLastUpdate) >= 0.5 * static_cast<double>(perfFreq);
|
||||
if (refreshLabel) {
|
||||
mFpsLastUpdate = now;
|
||||
mFpsCounter->SetInnerRML(escape(fmt::format("{:.0f} FPS", fps)));
|
||||
}
|
||||
} else {
|
||||
mFpsCounter->RemoveAttribute("open");
|
||||
mFpsFrameEvents.clear();
|
||||
mFpsSumTicks = 0;
|
||||
mFpsHavePrevCounter = false;
|
||||
mFpsLastUpdate = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const bool showControllerWarning = PADGetIndexForPort(PAD_CHAN0) < 0 &&
|
||||
PADGetKeyButtonBindings(PAD_CHAN0, nullptr) == nullptr &&
|
||||
dynamic_cast<Window*>(top_document()) == nullptr &&
|
||||
dynamic_cast<WindowSmall*>(top_document()) == nullptr;
|
||||
if (showControllerWarning && mControllerWarning == nullptr) {
|
||||
mControllerWarning = create_controller_warning(mDocument);
|
||||
} else if (showControllerWarning && mControllerWarning != nullptr) {
|
||||
mControllerWarning->SetAttribute("open", "");
|
||||
mControllerWarning->SetPseudoClass("opened", true);
|
||||
mControllerWarning->SetPseudoClass("done", false);
|
||||
} else if (!showControllerWarning && mControllerWarning != nullptr) {
|
||||
if (mControllerWarning->IsPseudoClassSet("done") ||
|
||||
!mControllerWarning->IsPseudoClassSet("opened"))
|
||||
{
|
||||
remove_element(mControllerWarning);
|
||||
} else {
|
||||
mControllerWarning->RemoveAttribute("open");
|
||||
}
|
||||
}
|
||||
|
||||
if (mMenuNotification != nullptr) {
|
||||
if (clock::now() >= mMenuNotificationStartTime + kMenuNotificationDuration) {
|
||||
if (mMenuNotification->IsPseudoClassSet("done") ||
|
||||
!mMenuNotification->IsPseudoClassSet("opened"))
|
||||
{
|
||||
remove_element(mMenuNotification);
|
||||
} else {
|
||||
mMenuNotification->RemoveAttribute("open");
|
||||
}
|
||||
} else {
|
||||
mMenuNotification->SetAttribute("open", "");
|
||||
mMenuNotification->SetPseudoClass("opened", true);
|
||||
mMenuNotification->SetPseudoClass("done", false);
|
||||
}
|
||||
}
|
||||
if (consume_menu_notification_request()) {
|
||||
if (mMenuNotification == nullptr) {
|
||||
mMenuNotification = create_menu_notification(mDocument);
|
||||
}
|
||||
mMenuNotificationStartTime = clock::now();
|
||||
}
|
||||
|
||||
auto& toasts = get_toasts();
|
||||
if (mCurrentToast == nullptr) {
|
||||
@@ -107,8 +323,7 @@ void Overlay::update() {
|
||||
// Fallback for large gaps in time where we never actually opened it
|
||||
!mCurrentToast->IsPseudoClassSet("opened"))
|
||||
{
|
||||
mCurrentToast->GetParentNode()->RemoveChild(mCurrentToast);
|
||||
mCurrentToast = nullptr;
|
||||
remove_element(mCurrentToast);
|
||||
toasts.pop_front();
|
||||
} else {
|
||||
mCurrentToast->RemoveAttribute("open");
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "document.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
@@ -16,8 +17,25 @@ public:
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
Rml::Element* mFpsCounter = nullptr;
|
||||
Rml::Element* mCurrentToast = nullptr;
|
||||
Rml::Element* mControllerWarning = nullptr;
|
||||
Rml::Element* mMenuNotification = nullptr;
|
||||
clock::time_point mCurrentToastStartTime;
|
||||
clock::time_point mMenuNotificationStartTime;
|
||||
|
||||
struct FpsFrameEvent {
|
||||
Uint64 endCounter;
|
||||
Uint64 processingTicks;
|
||||
};
|
||||
|
||||
std::deque<FpsFrameEvent> mFpsFrameEvents;
|
||||
Uint64 mFpsSumTicks = 0;
|
||||
bool mFpsHavePrevCounter = false;
|
||||
Uint64 mFpsPrevCounter = 0;
|
||||
Uint64 mFpsLastUpdate = 0;
|
||||
|
||||
void advance_fps_counter(float& outFps, Uint64 perfFreq);
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
+10
-1
@@ -126,11 +126,20 @@ Component& Pane::register_control(
|
||||
}
|
||||
});
|
||||
component.listen(component.root(), Rml::EventId::Focus,
|
||||
[&component, &nextPane, callback = std::move(callback)](Rml::Event&) {
|
||||
[this, &component, &nextPane, callback = std::move(callback)](Rml::Event&) {
|
||||
if (component.disabled()) {
|
||||
return;
|
||||
}
|
||||
nextPane.clear();
|
||||
|
||||
// If an item is already selected, deselect
|
||||
for (const auto& child : mChildren) {
|
||||
if (child->selected()) {
|
||||
set_selected_item(-1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(nextPane);
|
||||
}
|
||||
|
||||
+489
-72
@@ -11,11 +11,23 @@
|
||||
#include "version.h"
|
||||
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
#include <aurora/lib/logging.hpp>
|
||||
#include <aurora/lib/window.hpp>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
#include "m_Do/m_Do_MemCard.h"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
aurora::Module PrelaunchLog{"dusk::ui::prelaunch"};
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
@@ -54,8 +66,129 @@ constexpr std::array<SDL_DialogFileFilter, 2> kDiscFileFilters{{
|
||||
{"All Files", "*"},
|
||||
}};
|
||||
|
||||
static std::string get_error_msg(iso::ValidationError error) {
|
||||
struct DiscVerificationResult {
|
||||
std::string path;
|
||||
iso::DiscInfo info;
|
||||
iso::ValidationError validation = iso::ValidationError::Unknown;
|
||||
};
|
||||
|
||||
struct DiscVerificationTask {
|
||||
explicit DiscVerificationTask(std::string discPath) : path(std::move(discPath)) {
|
||||
worker = std::thread([this] {
|
||||
try {
|
||||
validation = iso::validate(path.c_str(), status, info);
|
||||
} catch (const std::exception& e) {
|
||||
PrelaunchLog.error(
|
||||
"Disc verification failed with exception for '{}': {}", path, e.what());
|
||||
validation = iso::ValidationError::Unknown;
|
||||
} catch (...) {
|
||||
PrelaunchLog.error(
|
||||
"Disc verification failed with unknown exception for '{}'", path);
|
||||
validation = iso::ValidationError::Unknown;
|
||||
}
|
||||
done.store(true, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
~DiscVerificationTask() {
|
||||
status.shouldCancel.store(true, std::memory_order_relaxed);
|
||||
join();
|
||||
}
|
||||
|
||||
void join() {
|
||||
if (worker.joinable()) {
|
||||
worker.join();
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool finished() const { return done.load(std::memory_order_acquire); }
|
||||
|
||||
std::string path;
|
||||
iso::DiscInfo info;
|
||||
iso::VerificationStatus status;
|
||||
iso::ValidationError validation = iso::ValidationError::Unknown;
|
||||
std::atomic_bool done = false;
|
||||
std::thread worker;
|
||||
};
|
||||
|
||||
std::unique_ptr<DiscVerificationTask> sDiscVerificationTask;
|
||||
bool sDiscVerificationModalPushed = false;
|
||||
|
||||
bool verification_state_allows_launch(iso::ValidationError validation) noexcept {
|
||||
return validation == iso::ValidationError::Unknown ||
|
||||
validation == iso::ValidationError::Success ||
|
||||
validation == iso::ValidationError::HashMismatch;
|
||||
}
|
||||
|
||||
iso::ValidationError verification_from_config(DiscVerificationState value) noexcept {
|
||||
switch (value) {
|
||||
case DiscVerificationState::Success:
|
||||
return iso::ValidationError::Success;
|
||||
case DiscVerificationState::HashMismatch:
|
||||
return iso::ValidationError::HashMismatch;
|
||||
default:
|
||||
return iso::ValidationError::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
DiscVerificationState verification_to_config(iso::ValidationError validation) {
|
||||
switch (validation) {
|
||||
case iso::ValidationError::Success:
|
||||
return DiscVerificationState::Success;
|
||||
case iso::ValidationError::HashMismatch:
|
||||
return DiscVerificationState::HashMismatch;
|
||||
default:
|
||||
return DiscVerificationState::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
std::string format_bytes(std::size_t bytes) {
|
||||
constexpr double KiB = 1024.0;
|
||||
constexpr double MiB = KiB * 1024.0;
|
||||
constexpr double GiB = MiB * 1024.0;
|
||||
if (bytes >= static_cast<std::size_t>(GiB)) {
|
||||
return fmt::format("{:.2f} GiB", static_cast<double>(bytes) / GiB);
|
||||
}
|
||||
if (bytes >= static_cast<std::size_t>(MiB)) {
|
||||
return fmt::format("{:.0f} MiB", static_cast<double>(bytes) / MiB);
|
||||
}
|
||||
if (bytes >= static_cast<std::size_t>(KiB)) {
|
||||
return fmt::format("{:.0f} KiB", static_cast<double>(bytes) / KiB);
|
||||
}
|
||||
return fmt::format("{} B", bytes);
|
||||
}
|
||||
|
||||
void begin_disc_verification(std::string path) noexcept {
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
if (sDiscVerificationTask != nullptr) {
|
||||
sDiscVerificationTask->status.shouldCancel.store(true, std::memory_order_relaxed);
|
||||
sDiscVerificationTask.reset();
|
||||
}
|
||||
sDiscVerificationTask = std::make_unique<DiscVerificationTask>(std::move(path));
|
||||
sDiscVerificationModalPushed = false;
|
||||
}
|
||||
|
||||
std::optional<DiscVerificationResult> take_finished_disc_verification() {
|
||||
if (sDiscVerificationTask == nullptr || !sDiscVerificationTask->finished()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
DiscVerificationResult result{
|
||||
.path = sDiscVerificationTask->path,
|
||||
.info = sDiscVerificationTask->info,
|
||||
.validation = sDiscVerificationTask->validation,
|
||||
};
|
||||
sDiscVerificationTask->join();
|
||||
sDiscVerificationTask.reset();
|
||||
sDiscVerificationModalPushed = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string get_error_msg(iso::ValidationError error) {
|
||||
switch (error) {
|
||||
default:
|
||||
return "The selected disc image could not be validated.";
|
||||
case iso::ValidationError::IOError:
|
||||
return "Unable to read the selected file.";
|
||||
case iso::ValidationError::InvalidImage:
|
||||
@@ -64,50 +197,334 @@ static std::string get_error_msg(iso::ValidationError error) {
|
||||
return "The selected game is not supported by Dusk.";
|
||||
case iso::ValidationError::WrongVersion:
|
||||
return "Dusk currently supports GameCube USA and PAL disc images only.";
|
||||
case iso::ValidationError::Canceled:
|
||||
return "Disc verification was canceled. Dusk cannot guarantee the selected disc image "
|
||||
"is compatible.";
|
||||
case iso::ValidationError::HashMismatch:
|
||||
return "The selected disc image did not pass hash verification. It may be corrupt or "
|
||||
"modified.";
|
||||
case iso::ValidationError::Success:
|
||||
return "The selected disc image is valid.";
|
||||
default:
|
||||
return "The selected disc image could not be validated.";
|
||||
}
|
||||
}
|
||||
|
||||
void file_dialog_callback(void*, const char* path, const char* error) {
|
||||
auto& state = prelaunch_state();
|
||||
if (error != nullptr) {
|
||||
return;
|
||||
}
|
||||
if (path == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto validation = iso::validate(path);
|
||||
if (validation != iso::ValidationError::Success) {
|
||||
state.errorString = escape(get_error_msg(validation));
|
||||
return;
|
||||
}
|
||||
|
||||
state.selectedDiscPath = path;
|
||||
state.errorString.clear();
|
||||
getSettings().backend.isoPath.setValue(state.selectedDiscPath);
|
||||
void persist_disc_choice(const std::string& path, iso::ValidationError validation) {
|
||||
getSettings().backend.isoPath.setValue(path);
|
||||
getSettings().backend.isoVerification.setValue(verification_to_config(validation));
|
||||
config::Save();
|
||||
refresh_state();
|
||||
}
|
||||
|
||||
void apply_valid_disc_result(
|
||||
const std::string& path, const iso::DiscInfo& info, iso::ValidationError validation) {
|
||||
auto& state = prelaunch_state();
|
||||
state.configuredDiscPath = path;
|
||||
state.configuredDiscCanLaunch = true;
|
||||
state.configuredDiscInfo = info;
|
||||
state.configuredDiscValidation = validation;
|
||||
if (state.activeDiscPath.empty() || path == state.activeDiscPath) {
|
||||
state.activeDiscPath = path;
|
||||
state.activeDiscInfo = info;
|
||||
}
|
||||
persist_disc_choice(path, validation);
|
||||
}
|
||||
|
||||
void apply_disc_verification_result(const DiscVerificationResult& result) {
|
||||
auto& state = prelaunch_state();
|
||||
|
||||
if (result.validation == iso::ValidationError::HashMismatch ||
|
||||
result.validation == iso::ValidationError::Canceled)
|
||||
{
|
||||
state.pendingDiscPath = result.path;
|
||||
state.pendingDiscInfo = result.info;
|
||||
state.pendingDiscValidation = result.validation;
|
||||
state.errorString = escape(get_error_msg(result.validation));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.validation == iso::ValidationError::Success) {
|
||||
apply_valid_disc_result(result.path, result.info, result.validation);
|
||||
state.errorString.clear();
|
||||
state.pendingDiscPath.clear();
|
||||
state.pendingDiscInfo = {};
|
||||
state.pendingDiscValidation = iso::ValidationError::Unknown;
|
||||
return;
|
||||
}
|
||||
|
||||
state.pendingDiscPath.clear();
|
||||
state.pendingDiscInfo = {};
|
||||
state.pendingDiscValidation = iso::ValidationError::Unknown;
|
||||
state.errorString = escape(get_error_msg(result.validation));
|
||||
}
|
||||
|
||||
class DiscVerificationModal : public WindowSmall {
|
||||
public:
|
||||
DiscVerificationModal() : WindowSmall("modal", "modal-dialog") {
|
||||
auto* header = append(mDialog, "div");
|
||||
header->SetClass("modal-header", true);
|
||||
|
||||
auto* title = append(header, "div");
|
||||
title->SetClass("modal-title", true);
|
||||
title->SetInnerRML("Verifying disc image");
|
||||
|
||||
auto* icon = append(header, "icon");
|
||||
icon->SetClass("verifying", true);
|
||||
|
||||
auto* body = append(mDialog, "div");
|
||||
body->SetClass("modal-body", true);
|
||||
|
||||
auto* content = append(body, "div");
|
||||
content->SetClass("verification-progress", true);
|
||||
|
||||
mFileName = append(content, "div");
|
||||
mFileName->SetClass("verification-file", true);
|
||||
|
||||
mProgress = append(content, "progress");
|
||||
mProgress->SetClass("progress-ongoing", true);
|
||||
mProgress->SetClass("verification-progress-bar", true);
|
||||
mProgress->SetAttribute("value", 0.f);
|
||||
|
||||
mDetail = append(content, "div");
|
||||
mDetail->SetClass("verification-detail", true);
|
||||
|
||||
auto* actions = append(mDialog, "div");
|
||||
actions->SetClass("modal-actions", true);
|
||||
mCancelButton = std::make_unique<Button>(actions, "Cancel");
|
||||
mCancelButton->root()->SetClass("modal-btn", true);
|
||||
mCancelButton->on_pressed([this] { request_cancel(); });
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void update() override {
|
||||
if (mFinished) {
|
||||
return;
|
||||
}
|
||||
if (auto result = take_finished_disc_verification()) {
|
||||
mFinished = true;
|
||||
apply_disc_verification_result(*result);
|
||||
pop();
|
||||
return;
|
||||
}
|
||||
if (sDiscVerificationTask == nullptr) {
|
||||
mFinished = true;
|
||||
pop();
|
||||
return;
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
bool focus() override { return mCancelButton != nullptr && mCancelButton->focus(); }
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override {
|
||||
if (cmd == NavCommand::Cancel || cmd == NavCommand::Menu) {
|
||||
request_cancel();
|
||||
event.StopPropagation();
|
||||
return true;
|
||||
}
|
||||
if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
void request_cancel() {
|
||||
if (sDiscVerificationTask == nullptr || mCancelRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
mCancelRequested = true;
|
||||
sDiscVerificationTask->status.shouldCancel.store(true, std::memory_order_relaxed);
|
||||
if (mCancelButton != nullptr) {
|
||||
mCancelButton->set_text("Cancelling...");
|
||||
mCancelButton->set_disabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
if (sDiscVerificationTask == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mCancelRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mFileName != nullptr) {
|
||||
std::string fileName =
|
||||
std::filesystem::path(sDiscVerificationTask->path).filename().string();
|
||||
if (fileName.empty()) {
|
||||
fileName = sDiscVerificationTask->path;
|
||||
}
|
||||
mFileName->SetInnerRML(escape(fileName));
|
||||
}
|
||||
|
||||
const std::size_t bytesRead =
|
||||
sDiscVerificationTask->status.bytesRead.load(std::memory_order_relaxed);
|
||||
const std::size_t bytesTotal =
|
||||
sDiscVerificationTask->status.bytesTotal.load(std::memory_order_relaxed);
|
||||
|
||||
if (bytesTotal == 0) {
|
||||
if (mProgress != nullptr) {
|
||||
mProgress->SetAttribute("value", 0.f);
|
||||
}
|
||||
if (mDetail != nullptr) {
|
||||
mDetail->SetInnerRML("Opening disc image...");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const float fraction =
|
||||
std::clamp(static_cast<float>(bytesRead) / static_cast<float>(bytesTotal), 0.0f, 1.0f);
|
||||
if (mProgress != nullptr) {
|
||||
mProgress->SetAttribute("value", fraction);
|
||||
}
|
||||
if (mDetail != nullptr) {
|
||||
mDetail->SetInnerRML(escape(fmt::format("{} / {} ({:.0f}%)", format_bytes(bytesRead),
|
||||
format_bytes(bytesTotal), fraction * 100.0f)));
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Element* mFileName = nullptr;
|
||||
Rml::Element* mProgress = nullptr;
|
||||
Rml::Element* mDetail = nullptr;
|
||||
std::unique_ptr<Button> mCancelButton;
|
||||
bool mCancelRequested = false;
|
||||
bool mFinished = false;
|
||||
};
|
||||
|
||||
void file_dialog_callback(void*, const char* path, const char* error) {
|
||||
if (path == nullptr || error != nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
begin_disc_verification(path);
|
||||
}
|
||||
|
||||
PrelaunchState sPrelaunchState;
|
||||
|
||||
} // namespace
|
||||
|
||||
PrelaunchState& prelaunch_state() noexcept {
|
||||
return sPrelaunchState;
|
||||
}
|
||||
|
||||
void refresh_state() noexcept {
|
||||
void refresh_configured_disc_state() noexcept {
|
||||
auto& state = prelaunch_state();
|
||||
const auto validation = iso::validate(state.selectedDiscPath.c_str());
|
||||
if (state.selectedDiscPath.empty() || validation != iso::ValidationError::Success) {
|
||||
state.selectedDiscIsValid = false;
|
||||
if (state.configuredDiscPath.empty()) {
|
||||
state.configuredDiscCanLaunch = false;
|
||||
state.configuredDiscInfo = {};
|
||||
state.configuredDiscValidation = iso::ValidationError::Unknown;
|
||||
return;
|
||||
}
|
||||
state.selectedDiscIsValid = true;
|
||||
state.selectedDiscIsPal = iso::isPal(state.selectedDiscPath.c_str());
|
||||
|
||||
iso::DiscInfo info{};
|
||||
const auto metadataValidation = iso::inspect(state.configuredDiscPath.c_str(), info);
|
||||
if (metadataValidation != iso::ValidationError::Success) {
|
||||
state.configuredDiscCanLaunch = false;
|
||||
state.configuredDiscInfo = {};
|
||||
state.configuredDiscValidation = metadataValidation;
|
||||
if (state.configuredDiscPath == state.activeDiscPath) {
|
||||
state.activeDiscInfo = {};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto verification = iso::ValidationError::Unknown;
|
||||
if (state.configuredDiscPath == getSettings().backend.isoPath.getValue()) {
|
||||
verification = verification_from_config(getSettings().backend.isoVerification.getValue());
|
||||
}
|
||||
|
||||
if (verification_state_allows_launch(verification)) {
|
||||
state.configuredDiscCanLaunch = true;
|
||||
state.configuredDiscInfo = info;
|
||||
state.configuredDiscValidation = verification;
|
||||
if (state.configuredDiscPath == state.activeDiscPath) {
|
||||
state.activeDiscInfo = info;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
state.configuredDiscCanLaunch = false;
|
||||
state.configuredDiscInfo = {};
|
||||
state.configuredDiscValidation = iso::ValidationError::Unknown;
|
||||
if (state.configuredDiscPath == state.activeDiscPath) {
|
||||
state.activeDiscInfo = {};
|
||||
}
|
||||
}
|
||||
|
||||
void try_push_verification_modal(Document& host) {
|
||||
auto& state = prelaunch_state();
|
||||
if (sDiscVerificationTask != nullptr && !sDiscVerificationModalPushed) {
|
||||
sDiscVerificationModalPushed = true;
|
||||
host.push(std::make_unique<DiscVerificationModal>());
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.errorString.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto dismiss = [](Modal& modal) {
|
||||
auto& state = prelaunch_state();
|
||||
state.errorString.clear();
|
||||
state.pendingDiscPath.clear();
|
||||
state.pendingDiscInfo = {};
|
||||
state.pendingDiscValidation = iso::ValidationError::Unknown;
|
||||
modal.pop();
|
||||
};
|
||||
|
||||
if (!state.pendingDiscPath.empty()) {
|
||||
const Rml::String bodyRml =
|
||||
state.errorString + "<br/><br/>You may proceed at your own risk.";
|
||||
auto acceptHashMismatch = [](Modal& modal) {
|
||||
auto& st = prelaunch_state();
|
||||
std::string path = std::move(st.pendingDiscPath);
|
||||
const auto info = st.pendingDiscInfo;
|
||||
const auto validation = st.pendingDiscValidation;
|
||||
st.pendingDiscPath.clear();
|
||||
st.pendingDiscInfo = {};
|
||||
st.pendingDiscValidation = iso::ValidationError::Unknown;
|
||||
st.errorString.clear();
|
||||
apply_valid_disc_result(path, info, validation);
|
||||
refresh_configured_disc_state();
|
||||
modal.pop();
|
||||
};
|
||||
host.push(std::make_unique<Modal>(Modal::Props{
|
||||
.title = "Disc verification warning",
|
||||
.bodyRml = bodyRml,
|
||||
.actions =
|
||||
{
|
||||
ModalAction{
|
||||
.label = "Cancel",
|
||||
.onPressed = dismiss,
|
||||
},
|
||||
ModalAction{
|
||||
.label = "Continue anyway",
|
||||
.onPressed = acceptHashMismatch,
|
||||
},
|
||||
},
|
||||
.onDismiss = dismiss,
|
||||
.variant = "danger",
|
||||
.icon = "warning",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
host.push(std::make_unique<Modal>(Modal::Props{
|
||||
.title = "Disc verification error",
|
||||
.bodyRml = state.errorString,
|
||||
.actions =
|
||||
{
|
||||
ModalAction{
|
||||
.label = "OK",
|
||||
.onPressed = dismiss,
|
||||
},
|
||||
},
|
||||
.onDismiss = dismiss,
|
||||
.icon = "error",
|
||||
}));
|
||||
}
|
||||
|
||||
void ensure_initialized() noexcept {
|
||||
@@ -116,17 +533,16 @@ void ensure_initialized() noexcept {
|
||||
return;
|
||||
}
|
||||
|
||||
state.selectedDiscPath = getSettings().backend.isoPath;
|
||||
state.initialDiscPath = state.selectedDiscPath;
|
||||
if (iso::validate(state.initialDiscPath.c_str()) == iso::ValidationError::Success) {
|
||||
state.initialDiscIsPal = iso::isPal(state.initialDiscPath.c_str());
|
||||
}
|
||||
state.configuredDiscPath = getSettings().backend.isoPath;
|
||||
state.activeDiscPath = state.configuredDiscPath;
|
||||
state.configuredDiscValidation =
|
||||
verification_from_config(getSettings().backend.isoVerification.getValue());
|
||||
state.initialLanguage = getSettings().game.language;
|
||||
state.initialGraphicsBackend = getSettings().backend.graphicsBackend;
|
||||
state.initialCardFileType = getSettings().backend.cardFileType;
|
||||
state.errorString.clear();
|
||||
state.initialized = true;
|
||||
refresh_state();
|
||||
refresh_configured_disc_state();
|
||||
}
|
||||
|
||||
void open_iso_picker() noexcept {
|
||||
@@ -137,7 +553,7 @@ void open_iso_picker() noexcept {
|
||||
|
||||
bool is_restart_pending() noexcept {
|
||||
const auto& state = prelaunch_state();
|
||||
if (!state.initialDiscPath.empty() && state.selectedDiscPath != state.initialDiscPath) {
|
||||
if (!state.activeDiscPath.empty() && state.configuredDiscPath != state.activeDiscPath) {
|
||||
return true;
|
||||
}
|
||||
if (getSettings().backend.graphicsBackend.getValue() != state.initialGraphicsBackend) {
|
||||
@@ -169,15 +585,17 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
|
||||
|
||||
if (auto* menuList = mDocument->GetElementById("menu-list")) {
|
||||
auto& state = prelaunch_state();
|
||||
mMenuButtons.push_back(std::make_unique<Button>(
|
||||
menuList, state.selectedDiscIsValid ? "Play" : "Select Disc Image"));
|
||||
const bool activeDiscLoaded = !state.activeDiscPath.empty();
|
||||
mMenuButtons.push_back(
|
||||
std::make_unique<Button>(menuList, activeDiscLoaded ? "Play" : "Select Disc Image"));
|
||||
mMenuButtons.back()->on_pressed([this] {
|
||||
if (!prelaunch_state().selectedDiscIsValid) {
|
||||
if (prelaunch_state().activeDiscPath.empty()) {
|
||||
open_iso_picker();
|
||||
return;
|
||||
}
|
||||
|
||||
mDoAud_seStartMenu(kSoundPlay);
|
||||
show_menu_notification();
|
||||
|
||||
if (getSettings().audio.menuSounds) {
|
||||
JAISoundHandle* handle = g_mEnvSeMgr.field_0x144.getHandle();
|
||||
@@ -192,21 +610,18 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
|
||||
}
|
||||
|
||||
IsGameLaunched = true;
|
||||
if (!getSettings().backend.wasPresetChosen) {
|
||||
push_document(std::make_unique<dusk::ui::PresetWindow>());
|
||||
}
|
||||
hide(true);
|
||||
});
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
|
||||
|
||||
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Options"));
|
||||
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Settings"));
|
||||
mMenuButtons.back()->on_pressed([this] {
|
||||
mRestartSuppressed = false;
|
||||
push(std::make_unique<SettingsWindow>(true));
|
||||
});
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-2");
|
||||
|
||||
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Quit To Desktop"));
|
||||
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Quit"));
|
||||
mMenuButtons.back()->on_pressed([] { IsRunning = false; });
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-3");
|
||||
}
|
||||
@@ -288,32 +703,18 @@ void Prelaunch::update() {
|
||||
ensure_initialized();
|
||||
try_apply_mirrored_layout(mDocument);
|
||||
|
||||
auto& state = prelaunch_state();
|
||||
if (!state.errorString.empty() && top_document() == this) {
|
||||
auto dismiss = [](Modal& modal) {
|
||||
prelaunch_state().errorString.clear();
|
||||
modal.pop();
|
||||
};
|
||||
push(std::make_unique<Modal>(Modal::Props{
|
||||
.title = "Invalid disc image",
|
||||
.bodyRml = state.errorString,
|
||||
.actions =
|
||||
{
|
||||
ModalAction{
|
||||
.label = "OK",
|
||||
.onPressed = dismiss,
|
||||
},
|
||||
},
|
||||
.onDismiss = dismiss,
|
||||
}));
|
||||
if (top_document() == this) {
|
||||
try_push_verification_modal(*this);
|
||||
}
|
||||
|
||||
const bool hasValidPath = prelaunch_state().selectedDiscIsValid;
|
||||
mDocument->SetClass("disc-ready", hasValidPath);
|
||||
if (hasValidPath) {
|
||||
if (getSettings().backend.skipPreLaunchUI) {
|
||||
hide(true);
|
||||
}
|
||||
const auto& state = prelaunch_state();
|
||||
|
||||
const bool canLaunchConfiguredDisc = state.configuredDiscCanLaunch;
|
||||
const bool activeDiscLoaded = !state.activeDiscPath.empty();
|
||||
const bool discRestartPending =
|
||||
activeDiscLoaded && state.configuredDiscPath != state.activeDiscPath;
|
||||
mDocument->SetClass("disc-ready", IsGameLaunched);
|
||||
if (canLaunchConfiguredDisc) {
|
||||
IsGameLaunched = true;
|
||||
}
|
||||
|
||||
@@ -323,22 +724,38 @@ void Prelaunch::update() {
|
||||
}
|
||||
|
||||
if (!mMenuButtons.empty()) {
|
||||
mMenuButtons[0]->set_text(hasValidPath ? "Play" : "Select Disc Image");
|
||||
mMenuButtons[0]->set_text(activeDiscLoaded ? "Play" : "Select Disc Image");
|
||||
}
|
||||
|
||||
const auto discStatusLabel = mDiscStatus->GetElementById("disc-status-label");
|
||||
|
||||
if (mDiscStatus != nullptr && discStatusLabel != nullptr) {
|
||||
if (hasValidPath) {
|
||||
if (!activeDiscLoaded) {
|
||||
mDiscStatus->RemoveAttribute("status");
|
||||
discStatusLabel->SetInnerRML("No disc image found.");
|
||||
} else if (discRestartPending) {
|
||||
mDiscStatus->SetAttribute("status", "pending");
|
||||
discStatusLabel->SetInnerRML("Pending restart.");
|
||||
} else if (state.configuredDiscValidation == iso::ValidationError::Success) {
|
||||
mDiscStatus->SetAttribute("status", "good");
|
||||
discStatusLabel->SetInnerRML("Disc ready.");
|
||||
} else if (state.configuredDiscValidation == iso::ValidationError::HashMismatch) {
|
||||
mDiscStatus->SetAttribute("status", "mismatch");
|
||||
discStatusLabel->SetInnerRML("Disc hash mismatch.");
|
||||
} else if (canLaunchConfiguredDisc) {
|
||||
mDiscStatus->SetAttribute("status", "unknown");
|
||||
discStatusLabel->SetInnerRML("Disc not verified.");
|
||||
} else {
|
||||
mDiscStatus->SetAttribute("status", "bad");
|
||||
discStatusLabel->SetInnerRML("Disc unavailable.");
|
||||
}
|
||||
}
|
||||
if (mDiscDetail != nullptr) {
|
||||
if (hasValidPath) {
|
||||
if (activeDiscLoaded) {
|
||||
mDiscDetail->SetProperty(Rml::PropertyId::Display, Rml::Style::Display::Block);
|
||||
mDiscDetail->SetInnerRML(
|
||||
prelaunch_state().initialDiscIsPal ? "GameCube • EUR" : "GameCube • USA");
|
||||
Rml::String innerRML = "GameCube • ";
|
||||
innerRML += state.activeDiscInfo.isPal ? "EUR" : "USA";
|
||||
mDiscDetail->SetInnerRML(innerRML);
|
||||
} else {
|
||||
mDiscDetail->SetProperty(Rml::PropertyId::Display, Rml::Style::Display::None);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "button.hpp"
|
||||
#include "document.hpp"
|
||||
#include "dusk/iso_validate.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -25,7 +26,7 @@ protected:
|
||||
private:
|
||||
bool mEntranceAnimationStarted = false;
|
||||
bool mRestartSuppressed = false;
|
||||
std::vector<std::unique_ptr<Button>> mMenuButtons;
|
||||
std::vector<std::unique_ptr<Button> > mMenuButtons;
|
||||
Rml::Element* mRoot = nullptr;
|
||||
Rml::Element* mDiscStatus = nullptr;
|
||||
Rml::Element* mDiscDetail = nullptr;
|
||||
@@ -36,21 +37,26 @@ class PrelaunchOptions;
|
||||
|
||||
struct PrelaunchState {
|
||||
bool initialized = false;
|
||||
std::string selectedDiscPath;
|
||||
bool selectedDiscIsValid = false;
|
||||
bool selectedDiscIsPal = false;
|
||||
std::string errorString;
|
||||
bool initialDiscIsPal = false;
|
||||
std::string initialDiscPath;
|
||||
std::string configuredDiscPath;
|
||||
bool configuredDiscCanLaunch = false;
|
||||
iso::DiscInfo configuredDiscInfo{};
|
||||
iso::ValidationError configuredDiscValidation = iso::ValidationError::Unknown;
|
||||
std::string activeDiscPath;
|
||||
iso::DiscInfo activeDiscInfo{};
|
||||
GameLanguage initialLanguage = GameLanguage::English;
|
||||
std::string initialGraphicsBackend;
|
||||
int initialCardFileType = 0;
|
||||
std::string errorString;
|
||||
std::string pendingDiscPath;
|
||||
iso::DiscInfo pendingDiscInfo{};
|
||||
iso::ValidationError pendingDiscValidation = iso::ValidationError::Unknown;
|
||||
};
|
||||
|
||||
PrelaunchState& prelaunch_state() noexcept;
|
||||
void ensure_initialized() noexcept;
|
||||
void refresh_state() noexcept;
|
||||
void refresh_configured_disc_state() noexcept;
|
||||
void open_iso_picker() noexcept;
|
||||
bool is_restart_pending() noexcept;
|
||||
void try_push_verification_modal(Document& host);
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
+15
-8
@@ -43,20 +43,28 @@ void applyPresetDusk() {
|
||||
s.game.internalResolutionScale.setValue(0);
|
||||
s.game.shadowResolutionMultiplier.setValue(4);
|
||||
s.game.enableGyroAim.setValue(true);
|
||||
s.game.autoSave.setValue(true);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PresetWindow::PresetWindow() : WindowSmall("preset", "preset-dialog") {
|
||||
auto* title = append(mDialog, "div");
|
||||
title->SetClass("preset-title", true);
|
||||
title->SetInnerRML("Welcome to Dusk!");
|
||||
PresetWindow::PresetWindow() : WindowSmall("modal", "modal-dialog") {
|
||||
mDialog->SetClass("modal-dialog", true);
|
||||
|
||||
auto* header = append(mDialog, "div");
|
||||
header->SetClass("modal-header", true);
|
||||
|
||||
auto* title = append(header, "div");
|
||||
title->SetClass("modal-title", true);
|
||||
title->SetInnerRML("Welcome to Dusk");
|
||||
|
||||
auto* headIcon = append(header, "icon");
|
||||
headIcon->SetClass("celebration", true);
|
||||
|
||||
auto* intro = append(mDialog, "div");
|
||||
intro->SetClass("preset-intro", true);
|
||||
intro->SetClass("modal-body", true);
|
||||
intro->SetInnerRML(
|
||||
"Choose a preset to get started.<br/>"
|
||||
"You can change any setting later from the Settings menu.");
|
||||
"Choose a preset to get started. You can change any setting later from the Settings menu.");
|
||||
|
||||
auto* grid = append(mDialog, "div");
|
||||
grid->SetClass("preset-grid", true);
|
||||
@@ -83,7 +91,6 @@ PresetWindow::PresetWindow() : WindowSmall("preset", "preset-dialog") {
|
||||
col->SetClass("preset-col", true);
|
||||
|
||||
auto btn = std::make_unique<Button>(col, Rml::String(preset.name));
|
||||
btn->root()->SetClass("preset-btn", true);
|
||||
btn->on_nav_command([this, apply = preset.apply](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
apply();
|
||||
|
||||
+103
-42
@@ -10,6 +10,7 @@
|
||||
#include "dusk/livesplit.h"
|
||||
#include "graphics_tuner.hpp"
|
||||
#include "m_Do/m_Do_main.h"
|
||||
#include "menu_bar.hpp"
|
||||
#include "number_button.hpp"
|
||||
#include "pane.hpp"
|
||||
#include "prelaunch.hpp"
|
||||
@@ -17,8 +18,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "modal.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
@@ -35,6 +34,13 @@ constexpr std::array kCardFileTypes = {
|
||||
"GCI Folder",
|
||||
};
|
||||
|
||||
constexpr std::array kFpsOverlayCornerNames = {
|
||||
"Top Left",
|
||||
"Top Right",
|
||||
"Bottom Left",
|
||||
"Bottom Right",
|
||||
};
|
||||
|
||||
bool try_parse_backend(std::string_view backend, AuroraBackend& outBackend) {
|
||||
if (backend == "auto") {
|
||||
outBackend = BACKEND_AUTO;
|
||||
@@ -304,7 +310,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.key = "Disc Image",
|
||||
.getValue =
|
||||
[] {
|
||||
const auto& path = prelaunch_state().selectedDiscPath;
|
||||
const auto& path = prelaunch_state().configuredDiscPath;
|
||||
std::string display;
|
||||
if (path.empty()) {
|
||||
display = "(none)";
|
||||
@@ -319,8 +325,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.isModified =
|
||||
[] {
|
||||
const auto& state = prelaunch_state();
|
||||
const auto& initial = state.initialDiscPath;
|
||||
return !initial.empty() && state.selectedDiscPath != initial;
|
||||
const auto& active = state.activeDiscPath;
|
||||
return !active.empty() && state.configuredDiscPath != active;
|
||||
},
|
||||
})
|
||||
.on_pressed([] { open_iso_picker(); }),
|
||||
@@ -334,7 +340,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.getValue =
|
||||
[] {
|
||||
const auto& state = prelaunch_state();
|
||||
if (!state.selectedDiscIsValid || !state.selectedDiscIsPal) {
|
||||
if (!state.configuredDiscCanLaunch || !state.configuredDiscInfo.isPal) {
|
||||
return kLanguageNames[0];
|
||||
}
|
||||
const u8 idx = static_cast<u8>(getSettings().game.language.getValue());
|
||||
@@ -343,7 +349,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.isDisabled =
|
||||
[] {
|
||||
const auto& state = prelaunch_state();
|
||||
return !state.selectedDiscIsValid || !state.selectedDiscIsPal;
|
||||
return !state.configuredDiscCanLaunch ||
|
||||
!state.configuredDiscInfo.isPal;
|
||||
},
|
||||
.isModified =
|
||||
[] {
|
||||
@@ -429,7 +436,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
});
|
||||
}
|
||||
|
||||
add_tab("Graphics", [this](Rml::Element* content) {
|
||||
add_tab("Video", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
|
||||
@@ -471,6 +478,57 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.key = "Pause on Focus Lost",
|
||||
.isDisabled = [] { return IsMobile; },
|
||||
});
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
.key = "Show FPS Counter",
|
||||
.getValue =
|
||||
[] {
|
||||
if (!getSettings().video.enableFpsOverlay.getValue()) {
|
||||
return Rml::String{"Off"};
|
||||
}
|
||||
const int idx = getSettings().video.fpsOverlayCorner.getValue();
|
||||
return Rml::String{kFpsOverlayCornerNames[idx]};
|
||||
},
|
||||
.isModified =
|
||||
[] {
|
||||
const auto& enable = getSettings().video.enableFpsOverlay;
|
||||
const auto& corner = getSettings().video.fpsOverlayCorner;
|
||||
return enable.getValue() != enable.getDefaultValue() ||
|
||||
(enable.getValue() && corner.getValue() != corner.getDefaultValue());
|
||||
},
|
||||
}),
|
||||
rightPane, [](Pane& pane) {
|
||||
pane.add_button(
|
||||
{
|
||||
.text = "Off",
|
||||
.isSelected =
|
||||
[] { return !getSettings().video.enableFpsOverlay.getValue(); },
|
||||
})
|
||||
.on_pressed([] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().video.enableFpsOverlay.setValue(false);
|
||||
config::Save();
|
||||
});
|
||||
for (int i = 0; i < static_cast<int>(kFpsOverlayCornerNames.size()); ++i) {
|
||||
pane.add_button(
|
||||
{
|
||||
.text = kFpsOverlayCornerNames[i],
|
||||
.isSelected =
|
||||
[i] {
|
||||
return getSettings().video.enableFpsOverlay.getValue() &&
|
||||
getSettings().video.fpsOverlayCorner.getValue() == i;
|
||||
},
|
||||
})
|
||||
.on_pressed([i] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().video.enableFpsOverlay.setValue(true);
|
||||
getSettings().video.fpsOverlayCorner.setValue(i);
|
||||
config::Save();
|
||||
});
|
||||
}
|
||||
pane.add_rml(
|
||||
"<br/>Display the current framerate in a corner of the screen while playing.");
|
||||
});
|
||||
|
||||
leftPane.add_section("Resolution");
|
||||
graphics_tuner_control(*this, leftPane, rightPane,
|
||||
@@ -686,8 +744,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
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 "
|
||||
addOption("Minimal HUD", getSettings().game.minimalHUD,
|
||||
"Disables the elements of the main HUD of the game.<br/>Useful for 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.");
|
||||
@@ -735,11 +793,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.autoSave,
|
||||
{
|
||||
.key = "Autosave",
|
||||
.icon = "warning",
|
||||
.helpText =
|
||||
"Autosaves the game when going to a new area, opening a dungeon door, "
|
||||
"or getting a new item.<br/><br/><icon class=\"warning\"/> Experimental "
|
||||
"feature: Use at your own risk.",
|
||||
.helpText = "Autosaves the game when going to a new area, opening a dungeon door, "
|
||||
"or getting a new item.",
|
||||
});
|
||||
addOption("Instant Saves", getSettings().game.instantSaves,
|
||||
"Skips the delay when writing to the Memory Card.");
|
||||
@@ -827,11 +882,11 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
"Lets the magic armor work without consuming rupees.");
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
leftPane.add_section("Dusk");
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.enableAchievementNotifications,
|
||||
{
|
||||
.key = "Achievement Notifications",
|
||||
@@ -852,44 +907,50 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
config_bool_select(leftPane, rightPane, getSettings().backend.skipPreLaunchUI,
|
||||
{
|
||||
.key = "Skip Dusk Main Menu",
|
||||
.helpText = "When starting Dusk, skips the main menu and boots straight into the "
|
||||
.helpText = "When starting Dusk, skip the main menu and boot straight into the "
|
||||
"game if a disc image is available.",
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.hideTvSettingsScreen,
|
||||
{
|
||||
.key = "Skip TV Settings Screen",
|
||||
.helpText = "Skips the TV calibration screen shown when loading a save.",
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().backend.showPipelineCompilation,
|
||||
{
|
||||
.key = "Show Pipeline Compilation",
|
||||
.helpText = "Show an overlay when shaders are being compiled for your hardware.",
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().backend.enableAdvancedSettings,
|
||||
{
|
||||
.key = "Enable Advanced Settings",
|
||||
.icon = "warning",
|
||||
.helpText = "Show advanced settings and debugging tools with "
|
||||
"Shift+F1.<br/><br/><icon class=\"warning\"/> WARNING: Debugging tools "
|
||||
"can easily break your game. Do not use on a regular save!",
|
||||
.onChange =
|
||||
[](bool) {
|
||||
for (auto& doc : get_document_stack()) {
|
||||
if (dynamic_cast<MenuBar*>(doc.get())) {
|
||||
doc = std::make_unique<MenuBar>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
leftPane.add_section("Game");
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.hideTvSettingsScreen,
|
||||
{
|
||||
.key = "Skip TV Settings Screen",
|
||||
.helpText = "Skips the TV calibration screen shown when loading a save.",
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.recordingMode,
|
||||
{
|
||||
.key = "Recording Mode",
|
||||
.helpText = "Disables the game HUD and all background music.<br/><br/>Useful for "
|
||||
"recording footage.",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void SettingsWindow::update() {
|
||||
// Show disc validation error message if present
|
||||
if (mPrelaunch && top_document() == this) {
|
||||
auto& state = prelaunch_state();
|
||||
if (!state.errorString.empty()) {
|
||||
auto dismissInvalidDisc = [](Modal& modal) {
|
||||
prelaunch_state().errorString.clear();
|
||||
modal.pop();
|
||||
};
|
||||
push_document(std::make_unique<Modal>(Modal::Props{
|
||||
.title = "Invalid disc image",
|
||||
.bodyRml = state.errorString,
|
||||
.actions =
|
||||
{
|
||||
ModalAction{
|
||||
.label = "OK",
|
||||
.onPressed = dismissInvalidDisc,
|
||||
},
|
||||
},
|
||||
.onDismiss = dismissInvalidDisc,
|
||||
}));
|
||||
}
|
||||
try_push_verification_modal(*this);
|
||||
}
|
||||
|
||||
Window::update();
|
||||
|
||||
+11
-1
@@ -212,6 +212,15 @@ bool TabBar::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
if (activeTab != -1) {
|
||||
currentComponent = activeTab;
|
||||
}
|
||||
} else {
|
||||
int activeTab = tab_containing(event.GetTargetElement());
|
||||
if (activeTab != -1) {
|
||||
currentComponent = activeTab;
|
||||
} else if (mLastFocusedTabIndex >= 0 &&
|
||||
mLastFocusedTabIndex < static_cast<int>(mTabs.size()))
|
||||
{
|
||||
currentComponent = mLastFocusedTabIndex;
|
||||
}
|
||||
}
|
||||
int direction = isNext ? 1 : -1;
|
||||
if (currentComponent == -1) {
|
||||
@@ -221,8 +230,9 @@ bool TabBar::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
return false;
|
||||
}
|
||||
currentComponent = -1;
|
||||
} else if (cmd == NavCommand::Next) {
|
||||
currentComponent = -1;
|
||||
} else {
|
||||
// Next/Previous require a currently selected tab to navigate from
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+146
-12
@@ -2,7 +2,9 @@
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
#include <absl/container/flat_hash_set.h>
|
||||
#include <aurora/rmlui.hpp>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
@@ -25,6 +27,13 @@ std::vector<std::unique_ptr<Document> > sDocumentStack;
|
||||
// Documents that don't participate in the focus stack
|
||||
std::vector<std::unique_ptr<Document> > sPassiveDocuments;
|
||||
std::deque<Toast> sToasts;
|
||||
bool sMenuNotificationRequested = false;
|
||||
|
||||
// Sometimes gamepads can connect and disconnect quickly, especially during
|
||||
// connection negotiation. In this case, we'll receive an _ADDED event for a
|
||||
// disconnected gamepad. Storing IDs here lets use only show disconnected
|
||||
// notifications for gamepads that we sent a connected notification for.
|
||||
absl::flat_hash_set<SDL_JoystickID> sConnectedGamepads;
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -51,22 +60,124 @@ bool initialize() noexcept {
|
||||
void shutdown() noexcept {
|
||||
sDocumentStack.clear();
|
||||
sPassiveDocuments.clear();
|
||||
reset_input_state();
|
||||
release_input_block();
|
||||
sConnectedGamepads.clear();
|
||||
input::reset_input_state();
|
||||
input::release_input_block();
|
||||
sInitialized = false;
|
||||
}
|
||||
|
||||
const char* battery_icon(SDL_PowerState state, int level) noexcept {
|
||||
if (state == SDL_POWERSTATE_UNKNOWN || state == SDL_POWERSTATE_NO_BATTERY) {
|
||||
return "e1a6"; // Battery Unknown
|
||||
}
|
||||
if (state == SDL_POWERSTATE_ERROR) {
|
||||
return "f7ea"; // Battery Error
|
||||
}
|
||||
if (state == SDL_POWERSTATE_CHARGED || level == 100) {
|
||||
return "e1a4"; // Battery Full
|
||||
}
|
||||
if (state == SDL_POWERSTATE_CHARGING) {
|
||||
if (level >= 90)
|
||||
return "f0a7"; // Battery Charging 90
|
||||
if (level >= 80)
|
||||
return "f0a6"; // Battery Charging 80
|
||||
if (level >= 60)
|
||||
return "f0a5"; // Battery Charging 60
|
||||
if (level >= 50)
|
||||
return "f0a4"; // Battery Charging 50
|
||||
if (level >= 30)
|
||||
return "f0a3"; // Battery Charging 30
|
||||
if (level >= 20)
|
||||
return "f0a2"; // Battery Charging 20
|
||||
return "e1a3"; // Battery Charging Full (we use it as empty)
|
||||
}
|
||||
if (level >= 90)
|
||||
return "ebd2"; // Battery 6 Bar
|
||||
if (level >= 80)
|
||||
return "ebd4"; // Battery 5 Bar
|
||||
if (level >= 60)
|
||||
return "ebe2"; // Battery 4 Bar
|
||||
if (level >= 50)
|
||||
return "ebdd"; // Battery 3 Bar
|
||||
if (level >= 30)
|
||||
return "ebe0"; // Battery 2 Bar
|
||||
if (level >= 20)
|
||||
return "ebd9"; // Battery 1 Bar
|
||||
return "e19c"; // Battery Alert
|
||||
}
|
||||
|
||||
const char* connection_state_icon(SDL_JoystickConnectionState state) noexcept {
|
||||
switch (state) {
|
||||
case SDL_JOYSTICK_CONNECTION_WIRELESS:
|
||||
return "e1a7";
|
||||
case SDL_JOYSTICK_CONNECTION_WIRED:
|
||||
return "e1e0";
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void handle_event(const SDL_Event& event) noexcept {
|
||||
if (!aurora::rmlui::is_initialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED) {
|
||||
auto* gamepad = SDL_GetGamepadFromID(event.gdevice.which);
|
||||
if (SDL_GamepadConnected(gamepad)) {
|
||||
const char* name = SDL_GetGamepadName(gamepad);
|
||||
Rml::String content = fmt::format("<span>{}</span>", name ? name : "[Unknown]");
|
||||
Rml::String title = "Controller connected";
|
||||
if (const char* icon = connection_state_icon(SDL_GetGamepadConnectionState(gamepad))) {
|
||||
title = fmt::format(
|
||||
"<row><span>{}</span> <icon class=\"connection\">&#x{};</icon></row>", title,
|
||||
icon);
|
||||
}
|
||||
int batteryLevel = -1;
|
||||
const auto powerState = SDL_GetGamepadPowerInfo(gamepad, &batteryLevel);
|
||||
if (powerState != SDL_POWERSTATE_UNKNOWN) {
|
||||
content = fmt::format(
|
||||
"<row>{}</row><row class=\"muted\"><icon class=\"battery\">&#x{};</icon>",
|
||||
content, battery_icon(powerState, batteryLevel));
|
||||
if (batteryLevel > -1) {
|
||||
content = fmt::format("{} <span>{}%</span>", content, batteryLevel);
|
||||
}
|
||||
content += "</row>";
|
||||
}
|
||||
push_toast({
|
||||
.type = "controller",
|
||||
.title = title,
|
||||
.content = content,
|
||||
.duration = std::chrono::seconds(4),
|
||||
});
|
||||
sConnectedGamepads.insert(event.gdevice.which);
|
||||
}
|
||||
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED &&
|
||||
sConnectedGamepads.contains(event.gdevice.which))
|
||||
{
|
||||
const char* name = SDL_GetGamepadNameForID(event.gdevice.which);
|
||||
push_toast({
|
||||
.type = "controller",
|
||||
.title = "Controller disconnected",
|
||||
.content = name ? name : "[Unknown]",
|
||||
.duration = std::chrono::seconds(4),
|
||||
});
|
||||
sConnectedGamepads.erase(event.gdevice.which);
|
||||
}
|
||||
input::handle_event(event);
|
||||
}
|
||||
|
||||
Document& push_document(std::unique_ptr<Document> doc, bool show, bool passive) noexcept {
|
||||
Document& ret = *doc;
|
||||
if (passive) {
|
||||
sPassiveDocuments.push_back(std::move(doc));
|
||||
} else {
|
||||
sDocumentStack.push_back({std::move(doc)});
|
||||
sDocumentStack.push_back(std::move(doc));
|
||||
}
|
||||
if (show) {
|
||||
ret.show();
|
||||
}
|
||||
sync_input_block();
|
||||
input::sync_input_block();
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -74,7 +185,7 @@ void show_top_document() noexcept {
|
||||
if (auto* doc = top_document()) {
|
||||
doc->show();
|
||||
}
|
||||
sync_input_block();
|
||||
input::sync_input_block();
|
||||
}
|
||||
|
||||
bool any_document_visible() noexcept {
|
||||
@@ -99,14 +210,23 @@ Document* top_document() noexcept {
|
||||
}
|
||||
|
||||
void update() noexcept {
|
||||
update_input();
|
||||
for (const auto& doc : sDocumentStack) {
|
||||
doc->update();
|
||||
}
|
||||
for (const auto& doc : sPassiveDocuments) {
|
||||
doc->update();
|
||||
if (!aurora::rmlui::is_initialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
input::update_input();
|
||||
const auto update_documents = [](auto& documents) {
|
||||
const std::size_t count = documents.size();
|
||||
for (std::size_t i = 0; i < count && i < documents.size(); ++i) {
|
||||
Document* doc = documents[i].get();
|
||||
if (doc != nullptr && !doc->closed()) {
|
||||
doc->update();
|
||||
}
|
||||
}
|
||||
};
|
||||
update_documents(sDocumentStack);
|
||||
update_documents(sPassiveDocuments);
|
||||
|
||||
// Remove closed documents
|
||||
{
|
||||
const auto [first, last] =
|
||||
@@ -131,7 +251,7 @@ void update() noexcept {
|
||||
}
|
||||
}
|
||||
|
||||
sync_input_block();
|
||||
input::sync_input_block();
|
||||
}
|
||||
|
||||
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept {
|
||||
@@ -243,8 +363,22 @@ void push_toast(Toast toast) noexcept {
|
||||
sToasts.push_back(std::move(toast));
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<Document> >& get_document_stack() noexcept {
|
||||
return sDocumentStack;
|
||||
}
|
||||
|
||||
std::deque<Toast>& get_toasts() noexcept {
|
||||
return sToasts;
|
||||
}
|
||||
|
||||
void show_menu_notification() noexcept {
|
||||
sMenuNotificationRequested = true;
|
||||
}
|
||||
|
||||
bool consume_menu_notification_request() noexcept {
|
||||
const bool requested = sMenuNotificationRequested;
|
||||
sMenuNotificationRequested = false;
|
||||
return requested;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -82,7 +82,14 @@ Rml::Element* append(Rml::Element* parent, const Rml::String& tag) noexcept;
|
||||
NavCommand map_nav_event(const Rml::Event& event) noexcept;
|
||||
Insets safe_area_insets(Rml::Context* context) noexcept;
|
||||
|
||||
std::vector<std::unique_ptr<Document> >& get_document_stack() noexcept;
|
||||
|
||||
void push_toast(Toast toast) noexcept;
|
||||
std::deque<Toast>& get_toasts() noexcept;
|
||||
void show_menu_notification() noexcept;
|
||||
bool consume_menu_notification_request() noexcept;
|
||||
|
||||
const char* battery_icon(SDL_PowerState state, int level) noexcept;
|
||||
const char* connection_state_icon(SDL_JoystickConnectionState state) noexcept;
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -743,6 +743,10 @@ static void duskExecute() {
|
||||
handleGamepadColor();
|
||||
updateAutoSave();
|
||||
|
||||
if (dusk::getSettings().game.recordingMode) {
|
||||
Z2GetSeqMgr()->bgmAllMute(0, 0);
|
||||
}
|
||||
|
||||
if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigX(PAD_1)) {
|
||||
if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) {
|
||||
dynamic_cast<daAlink_c*>(link)->handleWolfHowl();
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
#include "dusk/gx_helper.h"
|
||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/settings.h"
|
||||
#endif
|
||||
|
||||
class mDoGph_HIO_c : public JORReflexible {
|
||||
@@ -1172,6 +1173,11 @@ static void drawDepth2(view_class* param_0, view_port_class* param_1, int param_
|
||||
}
|
||||
|
||||
static void trimming(view_class* param_0, view_port_class* param_1) {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.recordingMode) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
ZoneScoped;
|
||||
UNUSED(param_0);
|
||||
|
||||
|
||||
+31
-8
@@ -285,7 +285,6 @@ void main01(void) {
|
||||
for (int sim_tick = 0; sim_tick < pacing.sim_ticks_to_run; ++sim_tick) {
|
||||
dusk::frame_interp::begin_sim_tick();
|
||||
mDoCPd_c::read();
|
||||
DuskDebugPad();
|
||||
dusk::gyro::read(pacing.sim_pace);
|
||||
fapGm_Execute();
|
||||
mDoAud_Execute();
|
||||
@@ -616,32 +615,54 @@ int game_main(int argc, char* argv[]) {
|
||||
dusk::audio::SetEnableReverb(dusk::getSettings().audio.enableReverb);
|
||||
dusk::audio::EnableHrtf = dusk::getSettings().audio.enableHrtf;
|
||||
|
||||
// Run ImGui UI loop if Aurora couldn't initialize a backend
|
||||
if (auroraInfo.backend == BACKEND_NULL) {
|
||||
launchUILoop();
|
||||
dusk::ShutdownCrashReporting();
|
||||
dusk::ShutdownFileLogging();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
#ifdef DUSK_DISCORD
|
||||
dusk::discord::shutdown();
|
||||
#endif
|
||||
dusk::ui::shutdown();
|
||||
aurora_shutdown();
|
||||
return 0;
|
||||
}
|
||||
|
||||
dusk::ui::initialize();
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::Overlay>(), true, true);
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::MenuBar>(), false);
|
||||
|
||||
// Invalidate a bad saved isoPath so that Dusk can't get blocked from starting up
|
||||
// Invalidate a bad saved isoPath so that Dusk can't get blocked from starting up.
|
||||
// This is only a metadata check; full hash verification is handled by the prelaunch UI.
|
||||
const std::string p = dusk::getSettings().backend.isoPath;
|
||||
if (!p.empty() && dusk::iso::validate(p.c_str()) != dusk::iso::ValidationError::Success) {
|
||||
dusk::iso::DiscInfo discInfo{};
|
||||
if (!p.empty() &&
|
||||
dusk::iso::inspect(p.c_str(), discInfo) != dusk::iso::ValidationError::Success)
|
||||
{
|
||||
dusk::getSettings().backend.isoPath.setValue("");
|
||||
dusk::getSettings().backend.isoVerification.setValue(dusk::DiscVerificationState::Unknown);
|
||||
}
|
||||
|
||||
std::string dvd_path;
|
||||
bool dvd_opened = false;
|
||||
if (parsed_arg_options.count("dvd")) {
|
||||
dvd_path = parsed_arg_options["dvd"].as<std::string>();
|
||||
if (dusk::iso::validate(dvd_path.c_str()) == dusk::iso::ValidationError::Success) {
|
||||
if (dusk::iso::inspect(dvd_path.c_str(), discInfo) == dusk::iso::ValidationError::Success) {
|
||||
DuskLog.info("Loading DVD image from command line: {}", dvd_path);
|
||||
dvd_opened = aurora_dvd_open(dvd_path.c_str());
|
||||
if (!dvd_opened) {
|
||||
DuskLog.warn("Failed to open DVD image from command line: {}, opening prelaunch UI", dvd_path);
|
||||
} else {
|
||||
dusk::getSettings().backend.isoPath.setValue(dvd_path);
|
||||
dusk::getSettings().backend.isoVerification.setValue(
|
||||
dusk::DiscVerificationState::Unknown);
|
||||
dusk::config::Save();
|
||||
dusk::IsGameLaunched = true;
|
||||
}
|
||||
} else {
|
||||
DuskLog.warn("DVD image from command line failed verification: {}, opening prelaunch UI", dvd_path);
|
||||
DuskLog.warn("DVD image from command line failed validation: {}, opening prelaunch UI", dvd_path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -669,8 +690,10 @@ int game_main(int argc, char* argv[]) {
|
||||
if (dvd_path.empty()) {
|
||||
DuskLog.fatal("No DVD image specified, unable to boot!");
|
||||
}
|
||||
if (dusk::iso::validate(dvd_path.c_str()) != dusk::iso::ValidationError::Success) {
|
||||
DuskLog.fatal("DVD image failed verification: {}", dvd_path);
|
||||
if (!dusk::IsGameLaunched &&
|
||||
dusk::iso::inspect(dvd_path.c_str(), discInfo) != dusk::iso::ValidationError::Success)
|
||||
{
|
||||
DuskLog.fatal("DVD image failed validation: {}", dvd_path);
|
||||
}
|
||||
DuskLog.info("Loading DVD image: {}", dvd_path);
|
||||
if (!aurora_dvd_open(dvd_path.c_str())) {
|
||||
@@ -701,7 +724,7 @@ int game_main(int argc, char* argv[]) {
|
||||
dComIfG_ct();
|
||||
|
||||
// Development Mode
|
||||
mDoMain::developmentMode = 1; // Force Dev Mode for Debugging
|
||||
// mDoMain::developmentMode = 1; // Force Dev Mode for Debugging
|
||||
mDoDvdThd::SyncWidthSound = false;
|
||||
|
||||
OSReport("Starting main01 (Game Loop)...\n");
|
||||
|
||||
Reference in New Issue
Block a user