mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-04 03:12:48 -04:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3968b67c5d | |||
| 119830c979 | |||
| 647394c875 | |||
| c50d777309 | |||
| 4954685cca | |||
| 39d43a8d8f | |||
| b4761cc54e | |||
| 9aee2e9cfe | |||
| 7c7c8b228e | |||
| ca7714dafd | |||
| 1ad5ab8632 | |||
| c4e7838089 | |||
| 7313712263 | |||
| aa48d95f24 | |||
| c5baabbe9c | |||
| 1d2716139f | |||
| 742ad2c150 | |||
| 18eb0692f0 | |||
| b5f98f69db | |||
| 47593d0eb4 | |||
| 4404fce369 | |||
| 72c20f4dd0 | |||
| 3240885bfd | |||
| 7f0955f022 | |||
| c21bce0093 | |||
| 4e23472ed5 | |||
| cfe1f2304b | |||
| de6568d750 | |||
| 3aafe7fa16 | |||
| c1e65d19e7 |
Binary file not shown.
|
Before Width: | Height: | Size: 85 KiB |
Vendored
+1
-1
Submodule extern/aurora updated: 4cd8d2f009...69fcfbffc2
@@ -1450,7 +1450,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_
|
||||
|
||||
@@ -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
|
||||
@@ -150,6 +162,7 @@ struct UserSettings {
|
||||
|
||||
struct {
|
||||
ConfigVar<std::string> isoPath;
|
||||
ConfigVar<DiscVerificationState> isoVerification;
|
||||
ConfigVar<std::string> graphicsBackend;
|
||||
ConfigVar<bool> skipPreLaunchUI;
|
||||
ConfigVar<bool> showPipelineCompilation;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 13 KiB |
+89
-3
@@ -65,13 +65,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 +110,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 +172,20 @@ 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);
|
||||
}
|
||||
|
||||
logo {
|
||||
position: absolute;
|
||||
width: 100dp;
|
||||
@@ -134,13 +207,26 @@ logo img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: drop-shadow(#0008 0 0 14dp);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
logo img.inner {
|
||||
animation: 24s linear-in-out infinite logo-inner-spin;
|
||||
}
|
||||
|
||||
logo img.outer {
|
||||
transform-origin: center;
|
||||
animation: 8s linear-in-out infinite logo-outer-spin;
|
||||
}
|
||||
|
||||
@keyframes logo-inner-spin {
|
||||
from {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes logo-outer-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
|
||||
+44
-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;
|
||||
}
|
||||
|
||||
+96
-48
@@ -39,12 +39,8 @@ window.small {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
window.preset {
|
||||
min-width: 650dp;
|
||||
}
|
||||
|
||||
window.modal {
|
||||
max-width: 816dp;
|
||||
max-width: 640dp;
|
||||
}
|
||||
|
||||
window[open] {
|
||||
@@ -104,6 +100,10 @@ window content pane > * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
window content pane:last-of-type > div {
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
window content pane > spacer {
|
||||
display: block;
|
||||
/* Completes the 24dp bottom inset after the pane's 8dp gap. */
|
||||
@@ -112,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;
|
||||
@@ -205,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 {
|
||||
@@ -270,6 +263,8 @@ select-button input {
|
||||
}
|
||||
|
||||
icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-family: "Material Symbols Rounded";
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
@@ -277,10 +272,19 @@ 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);
|
||||
}
|
||||
|
||||
.achievement-row {
|
||||
@@ -366,35 +370,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 {
|
||||
@@ -404,33 +386,99 @@ 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;
|
||||
}
|
||||
|
||||
.verification-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10dp;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.verification-file {
|
||||
display: block;
|
||||
font-size: 17dp;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
progressbar.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;
|
||||
}
|
||||
|
||||
@@ -44,16 +44,14 @@ void daAlink_c::handleWolfHowl() {
|
||||
bool canHowl = false;
|
||||
|
||||
if (mLinkAcch.ChkGroundHit() && !checkModeFlg(MODE_PLAYER_FLY) && !checkMagneBootsOn()) {
|
||||
if (!checkForestOldCentury()) {
|
||||
if (checkMidnaRide()) {
|
||||
if ((checkWolf() &&
|
||||
(checkModeFlg(MODE_UNK_1000) || dComIfGp_checkPlayerStatus0(0, 0x10))) ||
|
||||
(!checkWolf() &&
|
||||
(checkEventRun() || getMidnaActor()->checkMetamorphoseEnable()) &&
|
||||
(checkModeFlg(4) || dComIfGp_checkPlayerStatus0(0, 0x10))))
|
||||
{
|
||||
canHowl = true;
|
||||
}
|
||||
if (checkMidnaRide()) {
|
||||
if ((checkWolf() &&
|
||||
(checkModeFlg(MODE_UNK_1000) || dComIfGp_checkPlayerStatus0(0, 0x10))) ||
|
||||
(!checkWolf() &&
|
||||
(checkEventRun() || getMidnaActor()->checkMetamorphoseEnable()) &&
|
||||
(checkModeFlg(4) || dComIfGp_checkPlayerStatus0(0, 0x10))))
|
||||
{
|
||||
canHowl = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,16 +122,14 @@ void daAlink_c::handleQuickTransform() {
|
||||
bool canTransform = false;
|
||||
|
||||
if (mLinkAcch.ChkGroundHit() && !checkModeFlg(MODE_PLAYER_FLY) && !checkMagneBootsOn()) {
|
||||
if (!checkForestOldCentury()) {
|
||||
if (checkMidnaRide()) {
|
||||
if ((checkWolf() &&
|
||||
(checkModeFlg(MODE_UNK_1000) || dComIfGp_checkPlayerStatus0(0, 0x10))) ||
|
||||
(!checkWolf() &&
|
||||
(checkEventRun() || getMidnaActor()->checkMetamorphoseEnable()) &&
|
||||
(checkModeFlg(4) || dComIfGp_checkPlayerStatus0(0, 0x10))))
|
||||
{
|
||||
canTransform = true;
|
||||
}
|
||||
if (checkMidnaRide()) {
|
||||
if ((checkWolf() &&
|
||||
(checkModeFlg(MODE_UNK_1000) || dComIfGp_checkPlayerStatus0(0, 0x10))) ||
|
||||
(!checkWolf() &&
|
||||
(checkEventRun() || getMidnaActor()->checkMetamorphoseEnable()) &&
|
||||
(checkModeFlg(4) || dComIfGp_checkPlayerStatus0(0, 0x10))))
|
||||
{
|
||||
canTransform = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+4
-4
@@ -1595,7 +1595,7 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
|
||||
calcMapCmPerTexel(field_0x80, &field_0x58);
|
||||
getPack(field_0x80, &mPackX, &mPackZ);
|
||||
|
||||
mCenterX += mPackX;
|
||||
mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -mPackX :) mPackX;
|
||||
mCenterZ -= mPackZ;
|
||||
mCenterX += field_0x64;
|
||||
mCenterZ += mPackPlusZ;
|
||||
@@ -1657,7 +1657,7 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
|
||||
calcMapCmPerTexel(field_0x80, &field_0x58);
|
||||
getPack(field_0x80, &mPackX, &mPackZ);
|
||||
|
||||
mCenterX += mPackX;
|
||||
mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -mPackX :) mPackX;
|
||||
mCenterZ -= mPackZ;
|
||||
}
|
||||
break;
|
||||
@@ -1737,7 +1737,7 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
|
||||
calcMapCmPerTexel(field_0x80, &field_0x58);
|
||||
getPack(field_0x80, &mPackX, &mPackZ);
|
||||
|
||||
mCenterX += mPackX;
|
||||
mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -mPackX :) mPackX;
|
||||
mCenterZ -= mPackZ;
|
||||
field_0x8f = 4;
|
||||
#if DEBUG
|
||||
@@ -1829,7 +1829,7 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
|
||||
|
||||
sp14 += temp_f31_2 * (spC - sp14);
|
||||
sp10 += temp_f31_2 * (sp8 - sp10);
|
||||
mCenterX += sp14;
|
||||
mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -sp14 :) sp14;
|
||||
mCenterZ -= sp10;
|
||||
break;
|
||||
}
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
|
||||
@@ -942,7 +942,10 @@ void dMenu_DmapBg_c::draw() {
|
||||
f32 local_28c = mpBackTexture->getBounds().i.x;
|
||||
mpBackTexture->setBlackWhite(color_black, color_white);
|
||||
mpBackTexture->draw(local_28c, field_0xd94 + mpBackTexture->getBounds().i.y, mpBackTexture->getWidth(),
|
||||
mpBackTexture->getHeight(), false, false, false);
|
||||
mpBackTexture->getHeight(),
|
||||
IF_DUSK(dusk::getSettings().game.enableMirrorMode ? true :) false,
|
||||
false,
|
||||
false);
|
||||
|
||||
grafContext->scissor(field_0xd94 + mDoGph_gInf_c::getMinXF(),
|
||||
scissor_top, mDoGph_gInf_c::getWidthF(),
|
||||
|
||||
@@ -368,7 +368,7 @@ void dMenu_StageMapCtrl_c::initGetTreasureList(u8 param_0, s8 param_1) {
|
||||
}
|
||||
|
||||
inline static s16 rightModeCnvRot(s16 param_0) {
|
||||
return param_0;
|
||||
return IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -param_0 :) param_0;
|
||||
}
|
||||
|
||||
bool dMenu_StageMapCtrl_c::getTreasureList(f32* o_posX, f32* o_posY, s8* param_2, u8* o_swbit,
|
||||
@@ -405,7 +405,7 @@ bool dMenu_StageMapCtrl_c::getTreasureList(f32* o_posX, f32* o_posY, s8* param_2
|
||||
}
|
||||
|
||||
inline static f32 rightModeCnvPos(f32 param_0) {
|
||||
return param_0;
|
||||
return IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -param_0 :) param_0;
|
||||
}
|
||||
|
||||
void dMenu_StageMapCtrl_c::cnvPosTo2Dpos(f32 param_0, f32 param_1, f32* param_2,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -290,10 +291,6 @@ namespace dusk {
|
||||
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();
|
||||
@@ -302,6 +299,67 @@ namespace dusk {
|
||||
|
||||
UpdateDragScroll();
|
||||
|
||||
// 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_menuGame.windowControllerConfig();
|
||||
m_menuGame.windowInputViewer();
|
||||
m_menuGame.drawSpeedrunTimerOverlay();
|
||||
@@ -327,7 +385,6 @@ namespace dusk {
|
||||
m_menuTools.ShowSaveEditor();
|
||||
m_menuTools.ShowStateShare();
|
||||
}
|
||||
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,13 +2,16 @@
|
||||
#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 "dusk/main.h"
|
||||
#include "imgui.h"
|
||||
|
||||
union SDL_Event;
|
||||
@@ -23,7 +26,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:
|
||||
@@ -70,6 +73,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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
+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
|
||||
|
||||
@@ -109,6 +109,7 @@ UserSettings g_userSettings = {
|
||||
|
||||
.backend = {
|
||||
.isoPath {"backend.isoPath", ""},
|
||||
.isoVerification {"backend.isoVerification", DiscVerificationState::Unknown},
|
||||
.graphicsBackend {"backend.graphicsBackend", "auto"},
|
||||
.skipPreLaunchUI {"backend.skipPreLaunchUI", false},
|
||||
.showPipelineCompilation {"backend.showPipelineCompilation", false},
|
||||
@@ -206,6 +207,7 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.debugFlyCam);
|
||||
|
||||
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);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#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,74 @@ 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;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ControllerConfigWindow::ControllerConfigWindow() {
|
||||
@@ -311,23 +388,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 +430,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 +438,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 +548,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;
|
||||
@@ -473,6 +687,52 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
|
||||
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) {
|
||||
@@ -549,6 +809,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 +865,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 +912,30 @@ 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...";
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -32,6 +32,8 @@ 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;
|
||||
|
||||
Page mPage = Page::Controller;
|
||||
Pane* mRightPane = nullptr;
|
||||
@@ -42,6 +44,8 @@ private:
|
||||
int mSuppressNavigationPort = -1;
|
||||
PADButtonMapping* mPendingButtonMapping = nullptr;
|
||||
PADAxisMapping* mPendingAxisMapping = nullptr;
|
||||
int mPendingKeyButton = -1;
|
||||
int mPendingKeyAxis = -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"}},
|
||||
|
||||
@@ -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;
|
||||
|
||||
+19
-5
@@ -2,14 +2,28 @@
|
||||
|
||||
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.isWarning) {
|
||||
auto* icon = append(header, "icon");
|
||||
icon->SetClass("warning", true);
|
||||
} else if (mProps.isError) {
|
||||
auto* icon = append(header, "icon");
|
||||
icon->SetClass("error", 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,9 @@ public:
|
||||
Rml::String bodyRml;
|
||||
std::vector<ModalAction> actions;
|
||||
std::function<void(Modal&)> onDismiss;
|
||||
Rml::String variant;
|
||||
bool isWarning = false;
|
||||
bool isError = false;
|
||||
};
|
||||
|
||||
explicit Modal(Props props);
|
||||
|
||||
+177
-31
@@ -1,11 +1,13 @@
|
||||
#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 <algorithm>
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
@@ -21,41 +23,132 @@ const Rml::String kDocumentSource = R"RML(
|
||||
</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);
|
||||
|
||||
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" />
|
||||
</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
|
||||
@@ -69,6 +162,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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -81,6 +183,51 @@ void Overlay::show() {
|
||||
|
||||
void Overlay::update() {
|
||||
Document::update();
|
||||
if (mDocument == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -106,8 +253,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");
|
||||
|
||||
@@ -17,7 +17,10 @@ protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
Rml::Element* mCurrentToast = nullptr;
|
||||
Rml::Element* mControllerWarning = nullptr;
|
||||
Rml::Element* mMenuNotification = nullptr;
|
||||
clock::time_point mCurrentToastStartTime;
|
||||
clock::time_point mMenuNotificationStartTime;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
+488
-65
@@ -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, "progressbar");
|
||||
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",
|
||||
.isWarning = true,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
host.push(std::make_unique<Modal>(Modal::Props{
|
||||
.title = "Disc verification error",
|
||||
.bodyRml = state.errorString,
|
||||
.actions =
|
||||
{
|
||||
ModalAction{
|
||||
.label = "OK",
|
||||
.onPressed = dismiss,
|
||||
},
|
||||
},
|
||||
.onDismiss = dismiss,
|
||||
.isError = true,
|
||||
}));
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -199,7 +617,7 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
|
||||
});
|
||||
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));
|
||||
@@ -288,29 +706,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) {
|
||||
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) {
|
||||
if (getSettings().backend.skipPreLaunchUI) {
|
||||
hide(true);
|
||||
}
|
||||
@@ -323,22 +730,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
|
||||
|
||||
+14
-8
@@ -47,16 +47,23 @@ void applyPresetDusk() {
|
||||
|
||||
} // 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 +90,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();
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "modal.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
@@ -304,7 +302,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 +317,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 +332,9 @@ 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 +343,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 =
|
||||
[] {
|
||||
@@ -869,27 +870,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
+142
-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 {
|
||||
@@ -247,4 +367,14 @@ 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
|
||||
|
||||
@@ -84,5 +84,10 @@ Insets safe_area_insets(Rml::Context* context) 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
|
||||
|
||||
+30
-7
@@ -280,7 +280,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();
|
||||
@@ -600,32 +599,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,8 +674,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())) {
|
||||
|
||||
Reference in New Issue
Block a user