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

This commit is contained in:
gymnast86
2026-05-07 23:23:16 -07:00
75 changed files with 5191 additions and 3613 deletions
+2 -1
View File
@@ -102,6 +102,7 @@ set(AURORA_ENABLE_DVD ON CACHE BOOL "Enable DVD API support" FORCE)
set(AURORA_ENABLE_CARD ON CACHE BOOL "Enable CARD API support" FORCE)
set(AURORA_ENABLE_RMLUI ON CACHE BOOL "Enable RmlUi UI support" FORCE)
add_subdirectory(extern/aurora EXCLUDE_FROM_ALL)
target_compile_definitions(aurora_mtx PRIVATE MTX_USE_PS=1)
add_subdirectory(libs/freeverb)
@@ -285,7 +286,7 @@ set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors")
source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES})
source_group("dusk" FILES ${DUSK_FILES})
set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0)
set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 MTX_USE_PS=1)
set(GAME_INCLUDE_DIRS
include
Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

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