diff --git a/.clang-format b/.clang-format index 1428d3c71e..8ffd4ebe96 100644 --- a/.clang-format +++ b/.clang-format @@ -2,7 +2,7 @@ Language: Cpp Standard: C++03 AccessModifierOffset: -4 -AlignAfterOpenBracket: Align +AlignAfterOpenBracket: DontAlign AlignConsecutiveAssignments: false AlignConsecutiveDeclarations: false AlignOperands: true diff --git a/CMakeLists.txt b/CMakeLists.txt index f63b5c93f2..fa4dc5db0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL Linux) endif () set(AURORA_ENABLE_DVD ON CACHE BOOL "Enable DVD API support" FORCE) set(AURORA_ENABLE_CARD ON CACHE BOOL "Enable CARD API support" FORCE) +set(AURORA_ENABLE_RMLUI ON CACHE BOOL "Enable RmlUi UI support" FORCE) add_subdirectory(extern/aurora EXCLUDE_FROM_ALL) add_subdirectory(libs/freeverb) diff --git a/README.md b/README.md index a66a423c5e..85c899aeb0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![DuskLogo](res/logo-mascot.webp) +![DuskLogo](res/logo-mascot.png) - ### **[Official Website](https://twilitrealm.dev)** - ### **[Discord](https://discord.gg/QACynxeyna)** diff --git a/extern/aurora b/extern/aurora index 1bd972429e..41d5c9c5a2 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 1bd972429ebba4e486012b1acd08d4d7580eb917 +Subproject commit 41d5c9c5a2462f57105356ae578d9246e37f6ef2 diff --git a/files.cmake b/files.cmake index aa36d584a2..26f810861b 100644 --- a/files.cmake +++ b/files.cmake @@ -1429,6 +1429,7 @@ set(DUSK_FILES src/dusk/globals.cpp src/dusk/gyro.cpp src/dusk/gamepad_color.cpp + src/dusk/autosave.cpp src/dusk/io.cpp src/dusk/layout.cpp src/dusk/logging.cpp @@ -1464,6 +1465,45 @@ set(DUSK_FILES src/dusk/imgui/ImGuiStateShare.cpp src/dusk/imgui/ImGuiAchievements.hpp src/dusk/imgui/ImGuiAchievements.cpp + src/dusk/ui/bool_button.cpp + src/dusk/ui/bool_button.hpp + src/dusk/ui/button.cpp + src/dusk/ui/button.hpp + src/dusk/ui/component.cpp + src/dusk/ui/component.hpp + src/dusk/ui/document.cpp + src/dusk/ui/document.hpp + src/dusk/ui/editor.cpp + src/dusk/ui/editor.hpp + src/dusk/ui/event.cpp + src/dusk/ui/event.hpp + src/dusk/ui/input.cpp + src/dusk/ui/input.hpp + src/dusk/ui/nav_types.hpp + src/dusk/ui/number_button.cpp + src/dusk/ui/number_button.hpp + src/dusk/ui/overlay.cpp + src/dusk/ui/overlay.hpp + src/dusk/ui/pane.cpp + src/dusk/ui/pane.hpp + src/dusk/ui/popup.cpp + src/dusk/ui/popup.hpp + src/dusk/ui/prelaunch.cpp + src/dusk/ui/prelaunch.hpp + src/dusk/ui/prelaunch_options.cpp + src/dusk/ui/prelaunch_options.hpp + src/dusk/ui/select_button.cpp + src/dusk/ui/select_button.hpp + src/dusk/ui/settings.cpp + src/dusk/ui/settings.hpp + src/dusk/ui/string_button.cpp + src/dusk/ui/string_button.hpp + src/dusk/ui/tab_bar.cpp + src/dusk/ui/tab_bar.hpp + src/dusk/ui/ui.cpp + src/dusk/ui/ui.hpp + src/dusk/ui/window.cpp + src/dusk/ui/window.hpp src/dusk/achievements.cpp src/dusk/iso_validate.cpp src/dusk/livesplit.cpp diff --git a/include/dusk/autosave.h b/include/dusk/autosave.h new file mode 100644 index 0000000000..248924fcb8 --- /dev/null +++ b/include/dusk/autosave.h @@ -0,0 +1,17 @@ +#pragma once + +#ifndef AUTOSAVE_H +#define AUTOSAVE_H + +#include +#include + +void noAutoSave(); +void triggerAutoSave(); +void updateAutoSave(); +void enterAutoSave(); +void autoSaving(); +void waitingForWrite(); +void endAutoSave(); + +#endif \ No newline at end of file diff --git a/include/dusk/settings.h b/include/dusk/settings.h index e6fb0bd32b..e11647e8e4 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -81,6 +81,7 @@ struct UserSettings { ConfigVar instantSaves; ConfigVar instantText; ConfigVar sunsSong; + ConfigVar autoSave; // Preferences ConfigVar enableMirrorMode; diff --git a/libs/JSystem/include/JSystem/JUtility/JUTPalette.h b/libs/JSystem/include/JSystem/JUtility/JUTPalette.h index 017d511d50..5d29005a92 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTPalette.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTPalette.h @@ -40,6 +40,9 @@ public: JUTTransparency getTransparency() const { return JUTTransparency(mTransparency); } u16 getNumColors() const { return mNumColors; } ResTLUT* getColorTable() const { return mColorTable; } +#if TARGET_PC + void dataUploaded(); +#endif private: /* 0x00 */ GXTlutObj mTlutObj; diff --git a/libs/JSystem/include/JSystem/JUtility/JUTTexture.h b/libs/JSystem/include/JSystem/JUtility/JUTTexture.h index e0f1e15ff0..6a7a8f9a0e 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTTexture.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTTexture.h @@ -75,6 +75,7 @@ public: s32 getTransparency() const { return mTexInfo->alphaEnabled; } s32 getWidth() const { return mTexInfo->width; } s32 getHeight() const { return mTexInfo->height; } + JUTPalette* getPalette() const { return mPalette; } void setCaptureFlag(bool flag) { mFlags &= 2 | flag; } bool getCaptureFlag() const { return mFlags & 1; } bool getEmbPaletteDelFlag() const { return mFlags & 2; } @@ -82,7 +83,7 @@ public: int getTlutName() const { return mTlutName; } bool operator==(const JUTTexture& other) { return mTexInfo == other.mTexInfo - && field_0x2c == other.field_0x2c + && mPalette == other.mPalette && mWrapS == other.mWrapS && mWrapT == other.mWrapT && mMinFilter == other.mMinFilter @@ -100,7 +101,7 @@ private: /* 0x20 */ const ResTIMG* mTexInfo; /* 0x24 */ void* mTexData; /* 0x28 */ JUTPalette* mEmbPalette; - /* 0x2C */ JUTPalette* field_0x2c; + /* 0x2C */ JUTPalette* mPalette; /* 0x30 */ u8 mWrapS; /* 0x31 */ u8 mWrapT; /* 0x32 */ u8 mMinFilter; diff --git a/libs/JSystem/src/JParticle/JPAParticle.cpp b/libs/JSystem/src/JParticle/JPAParticle.cpp index e75cb260d0..d3490ab294 100644 --- a/libs/JSystem/src/JParticle/JPAParticle.cpp +++ b/libs/JSystem/src/JParticle/JPAParticle.cpp @@ -206,6 +206,10 @@ void JPABaseParticle::init_c(JPAEmitterWorkData* work, JPABaseParticle* parent) #if TARGET_PC void JPABaseParticle::interp(JPAEmitterWorkData* work, void const* drawFunc) { + static bool enable = false; + if (!enable) + return; + // don't interpolate the first frame if (mAge == 0) return; diff --git a/libs/JSystem/src/JUtility/JUTPalette.cpp b/libs/JSystem/src/JUtility/JUTPalette.cpp index d98bf98c2e..9c8a51432c 100644 --- a/libs/JSystem/src/JUtility/JUTPalette.cpp +++ b/libs/JSystem/src/JUtility/JUTPalette.cpp @@ -38,3 +38,9 @@ bool JUTPalette::load() { return check; } + +#if TARGET_PC +void JUTPalette::dataUploaded() { + GXInitTlutObjData(&mTlutObj, (void*)mColorTable); +} +#endif diff --git a/libs/JSystem/src/JUtility/JUTTexture.cpp b/libs/JSystem/src/JUtility/JUTTexture.cpp index 060c581e80..1d799c2820 100644 --- a/libs/JSystem/src/JUtility/JUTTexture.cpp +++ b/libs/JSystem/src/JUtility/JUTTexture.cpp @@ -27,7 +27,7 @@ void JUTTexture::storeTIMG(ResTIMG const* param_0, u8 param_1) { mTexData = (void*)((intptr_t)mTexInfo + 0x20); } - field_0x2c = NULL; + mPalette = NULL; mTlutName = 0; mWrapS = mTexInfo->wrapS; mWrapT = mTexInfo->wrapT; @@ -95,7 +95,7 @@ void JUTTexture::storeTIMG(ResTIMG const* param_0, JUTPalette* param_1, GXTlut p } mEmbPalette = param_1; setEmbPaletteDelFlag(false); - field_0x2c = NULL; + mPalette = NULL; if (param_1 != NULL) { mTlutName = param_2; if (param_2 != param_1->getTlutName()) { @@ -120,11 +120,11 @@ void JUTTexture::storeTIMG(ResTIMG const* param_0, JUTPalette* param_1, GXTlut p void JUTTexture::attachPalette(JUTPalette* param_0) { if (mTexInfo->indexTexture) { if (param_0 == NULL && mEmbPalette != NULL) { - field_0x2c = mEmbPalette; + mPalette = mEmbPalette; } else { - field_0x2c = param_0; + mPalette = param_0; } - initTexObj(field_0x2c->getTlutName()); + initTexObj(mPalette->getTlutName()); } } @@ -133,9 +133,9 @@ void JUTTexture::init() { initTexObj(); } else { if (mEmbPalette != NULL) { - field_0x2c = mEmbPalette; + mPalette = mEmbPalette; - initTexObj(field_0x2c->getTlutName()); + initTexObj(mPalette->getTlutName()); } else { OS_REPORT("This texture is CI-Format, but EmbPalette is NULL.\n"); } @@ -179,8 +179,8 @@ void JUTTexture::initTexObj(GXTlut param_0) { } void JUTTexture::load(GXTexMapID param_0) { - if (field_0x2c) { - field_0x2c->load(); + if (mPalette) { + mPalette->load(); } GXLoadTexObj(&mTexObj, param_0); } diff --git a/res/AlegreyaSC-Bold.ttf b/res/AlegreyaSC-Bold.ttf new file mode 100644 index 0000000000..dc73beb916 Binary files /dev/null and b/res/AlegreyaSC-Bold.ttf differ diff --git a/res/AlegreyaSC-Regular.ttf b/res/AlegreyaSC-Regular.ttf new file mode 100644 index 0000000000..31cae6c8ad Binary files /dev/null and b/res/AlegreyaSC-Regular.ttf differ diff --git a/res/FiraSans-Regular.ttf b/res/FiraSans-Regular.ttf new file mode 100644 index 0000000000..6f80647494 Binary files /dev/null and b/res/FiraSans-Regular.ttf differ diff --git a/res/FiraSansCondensed-Bold.ttf b/res/FiraSansCondensed-Bold.ttf new file mode 100644 index 0000000000..ec7e841549 Binary files /dev/null and b/res/FiraSansCondensed-Bold.ttf differ diff --git a/res/FiraSansCondensed-Regular.ttf b/res/FiraSansCondensed-Regular.ttf new file mode 100644 index 0000000000..6e1a192127 Binary files /dev/null and b/res/FiraSansCondensed-Regular.ttf differ diff --git a/res/logo-mascot.png b/res/logo-mascot.png new file mode 100644 index 0000000000..9f9a5d1ace Binary files /dev/null and b/res/logo-mascot.png differ diff --git a/res/logo-mascot.webp b/res/logo-mascot.webp deleted file mode 100644 index c22f3bc90f..0000000000 Binary files a/res/logo-mascot.webp and /dev/null differ diff --git a/res/prelaunch-bg.png b/res/prelaunch-bg.png new file mode 100644 index 0000000000..045dcbe655 Binary files /dev/null and b/res/prelaunch-bg.png differ diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss new file mode 100644 index 0000000000..a8323bb7b2 --- /dev/null +++ b/res/rml/overlay.rcss @@ -0,0 +1,147 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + overflow: visible; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + font-family: "Fira Sans Condensed"; + font-size: 24dp; + color: #FFFFFF; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: stretch; +} + +.overlay-root { + width: 100%; + min-height: 45%; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: stretch; + decorator: vertical-gradient(#00000000 #151610F2); + padding: 48dp 0 40dp 0; + filter: opacity(0); + transition: filter 0.2s linear-in-out; +} + +.overlay-root[open] { + filter: opacity(1); +} + +.overlay { + width: 100%; + max-width: 1216dp; + margin-left: auto; + margin-right: auto; + display: flex; + flex-direction: column; + gap: 24dp; + padding: 0 32dp; +} + +@media (max-height: 800dp) { + .overlay-root { + min-height: 38%; + padding: 32dp 0 28dp 0; + } + + .overlay { + gap: 16dp; + padding: 0 24dp; + } +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24dp; +} + +.carousel-container { + flex: 1 1 auto; + display: flex; + justify-content: flex-end; + min-width: 0; +} + +.description { + font-size: 18dp; + line-height: 22dp; + color: rgba(255, 255, 255, 50%); +} + +.divider { + margin: 1dp 0; + border-top: 1dp rgba(217, 217, 217, 50%); +} + +.footer { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24dp; +} + +footer-button { + display: block; + width: 100%; + max-width: 220dp; + border: 0; + padding: 0; + background-color: transparent; + font-family: "Fira Sans Condensed"; + font-weight: bold; + font-size: 20dp; + line-height: 24dp; + text-transform: uppercase; + color: #FFFFFF; + opacity: 1; + cursor: pointer; +} + +footer-button.return { + text-align: left; +} + +footer-button.reset { + text-align: right; +} + +.stepped-carousel { + display: flex; + align-items: center; + justify-content: center; + gap: 16dp; + width: auto; + min-width: 246dp; + padding: 0; + background-color: transparent; + font-family: "Fira Sans Condensed"; + font-weight: bold; +} + +.stepped-carousel-value { + line-height: 29dp; + min-width: 166dp; + text-align: center; + white-space: nowrap; + opacity: 0.9; +} + +.stepped-carousel-arrow { + width: 24dp; + height: 24dp; + min-width: 24dp; + padding: 0; + border: 0; + background-color: transparent; + opacity: 1; + cursor: pointer; +} diff --git a/res/rml/popup.rcss b/res/rml/popup.rcss new file mode 100644 index 0000000000..effc80344d --- /dev/null +++ b/res/rml/popup.rcss @@ -0,0 +1,45 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + overflow: visible; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + font-family: "Fira Sans Condensed"; + font-weight: bold; + font-size: 18dp; + color: #E0DBC8; +} + +button { + cursor: pointer; + focus: auto; +} + +popup { + width: 100%; + display: flex; + align-items: stretch; + height: 64dp; + background-color: rgba(21, 22, 16, 80%); + border-bottom: 2dp #92875B; + backdrop-filter: blur(5dp); + transform: translateY(-64dp); + transition: transform 0.2s cubic-in-out; +} + +popup[open] { + transform: translateY(0); +} + +popup tab-bar { + flex: 1 1 0; +} + +popup tab-bar tab { + opacity: 0.35; + color: #E0DBC8; +} diff --git a/res/rml/prelaunch.rcss b/res/rml/prelaunch.rcss new file mode 100644 index 0000000000..6c52f4cf82 --- /dev/null +++ b/res/rml/prelaunch.rcss @@ -0,0 +1,187 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + width: 100%; + height: 100%; + font-family: "Fira Sans"; + font-weight: normal; + font-size: 20dp; + color: #FFFFFF; + background-color: #000000; + decorator: image(../prelaunch-bg.png cover left center); + filter: opacity(0); + transition: filter 1s 0.1s linear-in-out; +} + +body[open] { + filter: opacity(1); +} + +content { + display: block; + width: 100%; + height: 100%; + filter: opacity(0); + transition: filter 0.2s linear-in-out; +} + +content[open] { + filter: opacity(1); +} + +menu { + position: absolute; + left: 96dp; + top: 50%; + transform: translateY(-50%); + /* Scale based on a reference screen width, 428/1216 */ + width: 35.230264vw; + min-width: 428dp; + max-width: 856dp; + height: auto; + display: flex; + flex-direction: column; + gap: 48dp; +} + +hero { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + gap: 8dp; +} + +hero img { + width: 100%; +} + +.eyebrow { + font-family: "Alegreya SC"; + font-size: 32dp; +} + +@media (min-width: 1216dp) { + .eyebrow { + /* Same logic as .menu, 32/1216 */ + font-size: 2.631579vw; + } +} + +.eyebrow span { + font-weight: bold; +} + +#menu-list { + display: flex; + flex-direction: column; + gap: 12dp; +} + +#menu-list button { + width: 428dp; + height: 54dp; + padding: 8dp 16dp; + border-radius: 8dp; + text-transform: uppercase; + font-family: "Fira Sans Condensed"; + font-size: 32dp; + font-weight: normal; + cursor: pointer; + /* Define a fully transparent gradient as the default state, otherwise a white flash occurs */ + decorator: horizontal-gradient(#00000000 #00000000); +} + +#menu-list button.anim-done { + transition: decorator color 0.1s linear-in-out; +} + +#menu-list button:hover, +#menu-list button:focus-visible { + color: black; + decorator: horizontal-gradient(#FEE685FF #FEE68500); +} + +disk-status { + position: absolute; + left: 96dp; + bottom: 72dp; + display: flex; + flex-direction: column; + gap: 8dp; +} + +version-info { + position: absolute; + right: 96dp; + bottom: 72dp; + display: flex; + flex-direction: column; + gap: 8dp; + text-align: right; +} + +.status, +.version { + font-size: 24dp; +} + +.status, +.update { + color: #D8F999; +} + +.status[bad] { + color: #FFC9C9; +} + +/* TODO: Hidden until an actual update checker is introduced */ +.update { + display: none; + font-size: 16dp; + font-weight: bold; + cursor: pointer; +} + +.detail, +.update span { + color: #A6A09B; +} + +/* Startup animation */ +.intro-item { + opacity: 0; + transform: translateY(10dp); + transition: opacity transform 0.3s 0.1s cubic-in-out; +} + +body.animate-in .intro-item { + opacity: 1; + transform: translateY(0dp); +} + +.delay-0 { + transition: opacity transform 0.3s 0.1s cubic-in-out; +} + +.delay-1 { + transition: opacity transform 0.3s 0.2s cubic-in-out; +} + +.delay-2 { + transition: opacity transform 0.3s 0.3s cubic-in-out; +} + +.delay-3 { + transition: opacity transform 0.3s 0.4s cubic-in-out; +} + +.delay-4 { + transition: opacity transform 0.3s 0.5s cubic-in-out; +} + +.delay-5 { + transition: opacity transform 0.3s 0.6s cubic-in-out; +} diff --git a/res/rml/tabbing.rcss b/res/rml/tabbing.rcss new file mode 100644 index 0000000000..e223a4cfad --- /dev/null +++ b/res/rml/tabbing.rcss @@ -0,0 +1,33 @@ +tab-bar { + display: flex; + min-width: 0; + overflow: auto hidden; + text-transform: uppercase; +} + +tab-bar tab { + flex: 0 0 auto; + padding: 0 24dp; + line-height: 64dp; + white-space: nowrap; + decorator: vertical-gradient(#c2a42d00 #c2a42d00); + transition: decorator 0.1s linear-in-out, opacity 0.1s linear-in-out; + cursor: pointer; +} + +tab-bar tab:selected { + opacity: 1; + border-bottom: 4dp #C2A42D; + font-effect: glow(0dp 4dp 0dp 4dp black); +} + +tab-bar tab:focus-visible, +tab-bar tab:hover { + opacity: 1; + font-effect: glow(0dp 4dp 0dp 4dp black); + decorator: vertical-gradient(#c2a42d00 #c2a42d26); +} + +tab-bar tab:active { + decorator: vertical-gradient(#c2a42d10 #c2a42d40); +} diff --git a/res/rml/window.rcss b/res/rml/window.rcss new file mode 100644 index 0000000000..197711f2a0 --- /dev/null +++ b/res/rml/window.rcss @@ -0,0 +1,239 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + width: 100%; + height: 100%; + padding: 64dp; + font-family: "Fira Sans"; + font-weight: normal; + font-style: normal; + font-size: 15dp; + color: #E0DBC8; +} + +window { + display: flex; + flex-flow: column; + height: 100%; + max-width: 1088dp; + max-height: 768dp; + margin: auto; + border-radius: 14dp; + overflow: hidden; + border: 2dp #92875B; + backdrop-filter: blur(5dp); + box-shadow: 0 0 25dp 5dp; + background-color: rgba(21, 22, 16, 90%); + filter: opacity(0); + transform: scale(0.9); + transform-origin: center; + transition: filter transform 0.2s cubic-in-out; +} + +window[open] { + filter: opacity(1); + transform: scale(1); +} + +@media (max-height: 640dp) { + body { + padding: 16dp; + } + window { + box-shadow: none; + } +} + +window tab-bar { + flex: 0 0 64dp; + height: 64dp; + background-color: rgba(217, 217, 217, 10%); + font-family: "Fira Sans Condensed"; + font-weight: bold; + font-size: 18dp; + border-bottom: 2dp #92875B; +} + +window tab-bar tab { + opacity: 0.25; +} + +window content { + display: flex; + flex: 1 1 0; + min-width: 0; + min-height: 0; + overflow: hidden; +} + +window content pane { + display: flex; + flex-flow: column; + flex: 1 1 0; + height: 100%; + min-width: 0; + min-height: 0; + padding: 24dp; + padding-bottom: 0dp; + gap: 8dp; + overflow: hidden auto; + font-size: 20dp; +} + +window content pane:not(:last-of-type) { + border-right: 1dp #92875B; +} + +window content pane > * { + flex: 0 0 auto; +} + +window content pane > spacer { + display: block; + /* Completes the 24dp bottom inset after the pane's 8dp gap. */ + flex: 0 0 16dp; + height: 16dp; + pointer-events: none; +} + +scrollbarvertical { + width: 8dp; + margin: 4dp 4dp 4dp 0; +} + +scrollbarvertical sliderarrowdec, +scrollbarvertical sliderarrowinc { + width: 0; + height: 0; +} + +scrollbarvertical slidertrack { + width: 8dp; +} + +scrollbarvertical sliderbar { + width: 8dp; + min-height: 24dp; + background-color: rgba(224, 219, 200, 45%); + border-radius: 2dp; + transition: background-color 0.2s cubic-in-out; +} + +scrollbarvertical sliderbar:hover, +scrollbarvertical sliderbar:active { + background-color: rgba(194, 164, 45, 80%); +} + +scrollbarhorizontal { + height: 0; +} + +scrollbarhorizontal sliderarrowdec, +scrollbarhorizontal sliderarrowinc { + width: 0; + height: 0; +} + +scrollbarhorizontal slidertrack, +scrollbarhorizontal sliderbar { + width: 0; + height: 0; +} + +.section-heading { + font-family: "Fira Sans Condensed"; + font-weight: bold; + text-transform: uppercase; + font-size: 22dp; + opacity: 0.25; +} + +.section-heading:not(:first-of-type) { + padding-top: 12dp; +} + +button { + text-align: center; + background-color: rgba(17, 16, 10, 20%); + opacity: 0.9; + padding: 8dp 16dp; + border-radius: 14dp; + box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp; + font-size: 20dp; + transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out; + cursor: pointer; + focus: auto; +} + +button:not(:disabled):hover, +button:not(:disabled):focus-visible { + background-color: rgba(204, 184, 119, 20%); + box-shadow: #C2A42D 0 0 0 2dp; +} + +button:not(:disabled):selected { + opacity: 1; + background-color: rgba(204, 184, 119, 40%); +} + +button:not(:disabled):active { + opacity: 1; + background-color: rgba(204, 184, 119, 40%); + box-shadow: #C2A42D 0 0 0 2dp; +} + +select-button { + display: flex; + align-items: center; + gap: 8dp; + background-color: rgba(17, 16, 10, 20%); + opacity: 0.9; + padding: 8dp 16dp; + border-radius: 14dp; + box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp; + transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out; + cursor: pointer; + focus: auto; +} + +select-button:not(:disabled):hover, +select-button:not(:disabled):focus-visible { + background-color: rgba(204, 184, 119, 20%); + box-shadow: #C2A42D 0 0 0 2dp; +} + +select-button:not(:disabled):selected { + opacity: 1; + background-color: rgba(204, 184, 119, 40%); +} + +select-button:not(:disabled):active { + opacity: 1; + background-color: rgba(204, 184, 119, 40%); + box-shadow: #C2A42D 0 0 0 2dp; +} + +select-button:disabled { + opacity: 0.35; + cursor: default; +} + +select-button key { + font-family: "Fira Sans Condensed"; + font-weight: bold; + font-size: 18dp; + text-transform: uppercase; + flex: 1 0 auto; +} + +select-button value { + margin-left: auto; + font-size: 20dp; +} + +select-button input { + text-align: right; + font-size: 20dp; +} diff --git a/src/d/actor/d_a_door_shutter.cpp b/src/d/actor/d_a_door_shutter.cpp index adbc96d528..b48ff7fec6 100644 --- a/src/d/actor/d_a_door_shutter.cpp +++ b/src/d/actor/d_a_door_shutter.cpp @@ -20,6 +20,11 @@ #include #include +#if TARGET_PC +#include +#include +#endif + char* daDoor20_c::getStopBmdName() { switch (door_param2_c::getKind(this)) { case 3: @@ -199,6 +204,7 @@ void daDoor20_c::setEventPrm() { } else { roomNo = FRoomNo; } + if (dComIfGp_roomControl_checkStatusFlag(roomNo, 1)) { if (door_param2_c::getKind(this) == 9) { if (daPy_py_c::checkNowWolf()) { @@ -577,6 +583,11 @@ int daDoor20_c::openEnd(int param_1) { openEnd_1(); break; } + + #if TARGET_PC + triggerAutoSave(); + #endif + return 1; } diff --git a/src/d/actor/d_a_midna.cpp b/src/d/actor/d_a_midna.cpp index 7bb844932e..2356b693be 100644 --- a/src/d/actor/d_a_midna.cpp +++ b/src/d/actor/d_a_midna.cpp @@ -463,6 +463,23 @@ int daMidna_c::createHeap() { JKRReadIdxResource(mBckHeap[0].getBuffer(), mBckHeap[0].getBufferSize(), 0x1DC, dComIfGp_getAnmArchive()); J3DAnmTransform* md_anm = (J3DAnmTransform*)J3DAnmLoaderDataBase::load(mBckHeap[0].getBuffer()); modelData = (J3DModelData*)dComIfG_getObjectRes(l_arcName, 14); + +#if TARGET_PC + J3DTexture* tex = modelData->getTexture(); + JUTNameTab* nametable = modelData->getTextureName(); + if (tex != NULL && nametable != NULL) { + for (u16 i = 0; i < tex->getNum(); i++) { + const char* name = nametable->getName(i); + if (name != NULL && strcmp(name, "midona_eye") == 0) { + ResTIMG* timg = tex->getResTIMG(i); + timg->mipmapEnabled = false; + tex->loadGXTexObj(i); + break; + } + } + } +#endif + JUT_ASSERT(852, modelData != NULL); mpMorf = JKR_NEW mDoExt_McaMorfSO(modelData, &mMorfCB, NULL, md_anm, J3DFrameCtrl::EMode_LOOP, 1.0f, 0, -1, NULL, 0, 0x11000284); if (mpMorf == NULL || mpMorf->getModel() == NULL) { diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index f01ef2a10d..ea9912998b 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -1420,6 +1420,11 @@ void dMenu_Fmap2DBack_c::stageTextureDraw() { mpSpotTexture->setAlpha(mAlphaRate * 255.0f * field_0xfa8 * mSpotTextureFadeAlpha); } +#if TARGET_PC + JUTPalette* pPalette = mpSpotTexture->getTexture(0)->getPalette(); + pPalette->dataUploaded(); +#endif + mpSpotTexture->draw(mTransX + getMapScissorAreaLX(), mTransZ + getMapScissorAreaLY(), getMapScissorAreaSizeRealX(), getMapScissorAreaSizeRealY(), false, false, false); diff --git a/src/d/d_s_play.cpp b/src/d/d_s_play.cpp index c162d5c9f0..c22e14bd38 100644 --- a/src/d/d_s_play.cpp +++ b/src/d/d_s_play.cpp @@ -42,6 +42,7 @@ #if TARGET_PC #include "dusk/memory.h" #include "dusk/randomizer/game/tools.h" +#include #endif #if DEBUG @@ -701,6 +702,10 @@ static u8 lbl_8074CAE4; static u32 l_sceneChangeStartTick; #endif +#if TARGET_PC +static BOOL autoSaved; +#endif + static int dScnPly_Execute(dScnPly_c* i_this) { #if DEBUG fapGm_HIO_c::startCpuTimer(); @@ -743,6 +748,15 @@ static int dScnPly_Execute(dScnPly_c* i_this) { } } + #if TARGET_PC + if (!dComIfGp_event_runCheck() && !fopOvlpM_IsPeek() && !dComIfG_resetToOpening(i_this) && + !dComIfGp_isEnableNextStage() && autoSaved == FALSE) + { + triggerAutoSave(); + autoSaved = TRUE; + } + #endif + dKy_itudemo_se(); #if DEBUG @@ -1594,6 +1608,11 @@ static int dScnPly_Create(scene_class* i_this) { dScnPly_c* a_this = (dScnPly_c*)i_this; int phase_state = dComLbG_PhaseHandler(&a_this->field_0x1c4, l_method, a_this); + + #if TARGET_PC + autoSaved = FALSE; + #endif + return phase_state; } diff --git a/src/d/d_save.cpp b/src/d/d_save.cpp index a0bcd68e1a..c2bf4c2ab5 100644 --- a/src/d/d_save.cpp +++ b/src/d/d_save.cpp @@ -36,7 +36,11 @@ #include "lingcod/lingcod.h" #endif +#if TARGET_PC #include "dusk/settings.h" +#include +#include +#endif static u8 dSv_item_rename(u8 i_itemNo) { switch (i_itemNo) { @@ -354,6 +358,10 @@ void dSv_player_item_c::setItem(int i_slotNo, u8 i_itemNo) { dComIfGp_setSelectItem(i); } } + + #if TARGET_PC + triggerAutoSave(); + #endif } u8 dSv_player_item_c::getItem(int i_slotNo, bool i_checkCombo) const { diff --git a/src/dusk/autosave.cpp b/src/dusk/autosave.cpp new file mode 100644 index 0000000000..f9b76f4c67 --- /dev/null +++ b/src/dusk/autosave.cpp @@ -0,0 +1,88 @@ +#include "dusk/autosave.h" +#include "imgui/ImGuiConsole.hpp" + +u8 mSaveBuffer[QUEST_LOG_SIZE * 3]; +u8 mAutoSaveProc = 0; +int autoSaveWriteState = 0; + +typedef void (*AutoSaveFuncs)(); +static AutoSaveFuncs AutoSaveFuncsProc[] = { + noAutoSave, enterAutoSave, autoSaving, waitingForWrite, endAutoSave, +}; + +void noAutoSave() {} + +void triggerAutoSave() { + if (dusk::getSettings().game.autoSave && mAutoSaveProc == 0 && + strcmp(dComIfGp_getStartStageName(), "F_SP102") != 0) + { + mAutoSaveProc = 1; + } +} + +void updateAutoSave() { + (AutoSaveFuncsProc[mAutoSaveProc])(); +} + +void writeAutoSave() { + int stageNo = dStage_stagInfo_GetSaveTbl(dComIfGp_getStageStagInfo()); + + dComIfGs_putSave(stageNo); + dComIfGs_setMemoryToCard(mSaveBuffer, dComIfGs_getDataNum()); + mDoMemCdRWm_SetCheckSumGameData(mSaveBuffer, dComIfGs_getDataNum()); + + u8* save = mSaveBuffer; + for (int i = 0; i < 3; i++) { + mDoMemCdRWm_TestCheckSumGameData(save); + save += QUEST_LOG_SIZE; + } + + g_mDoMemCd_control.save(mSaveBuffer, sizeof(mSaveBuffer), 0); +} + +void autoSaving() { + int cardState = g_mDoMemCd_control.LoadSync(mSaveBuffer, sizeof(mSaveBuffer), 0); + if (cardState != 0) { + if (cardState == 2) { + mAutoSaveProc = 1; + } else if (cardState == 1) { + writeAutoSave(); + mAutoSaveProc = 3; + } + } +} + +void enterAutoSave() { + u32 cardStatus = g_mDoMemCd_control.getStatus(0); + + if (cardStatus != 14) { + switch (cardStatus) { + case 2: + g_mDoMemCd_control.load(); + mAutoSaveProc = 2; + break; + case 3: + case 4: + case 5: + break; + default: + mAutoSaveProc = 0; + break; + } + } +} + +void waitingForWrite() { + autoSaveWriteState = g_mDoMemCd_control.SaveSync(); + + if (autoSaveWriteState == 2) { + mAutoSaveProc = 0; + } else if (autoSaveWriteState == 1) { + mAutoSaveProc = 4; + } +} + +void endAutoSave() { + dusk::g_imguiConsole.AddToast("Saving...", 2.0f); + mAutoSaveProc = 0; +} \ No newline at end of file diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 2c42f58722..543fdd754b 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -324,18 +324,18 @@ namespace dusk { ImGuiMenuGame::ToggleFullscreen(); } - if (!dusk::IsGameLaunched) { - m_preLaunchWindow.draw(); - } + // if (!dusk::IsGameLaunched) { + // m_preLaunchWindow.draw(); + // } m_isHidden = !getSettings().backend.duskMenuOpen; - bool showMenu = !dusk::IsGameLaunched || !CheckMenuViewToggle(ImGuiKey_F1, m_isHidden); - if (dusk::IsGameLaunched) { - const bool menuOpen = !m_isHidden; - if (getSettings().backend.duskMenuOpen != menuOpen) { - getSettings().backend.duskMenuOpen.setValue(menuOpen); - Save(); - } + if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) { + m_isHidden = !m_isHidden; + } + bool showMenu = !m_isHidden; + if (getSettings().backend.duskMenuOpen != showMenu) { + getSettings().backend.duskMenuOpen.setValue(showMenu); + Save(); } // The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg, @@ -366,10 +366,10 @@ namespace dusk { } if (dusk::IsGameLaunched && !m_isLaunchInitialized) { - m_toasts.emplace_back(ImGui::GetIO().MouseSource == ImGuiMouseSource_TouchScreen ? + AddToast(ImGui::GetIO().MouseSource == ImGuiMouseSource_TouchScreen ? "Tap to toggle menu"s : "Press F1 to toggle menu"s, - 2.5f); + 4.f); m_isLaunchInitialized = true; if (getSettings().game.liveSplitEnabled) { dusk::speedrun::connectLiveSplit(); diff --git a/src/dusk/imgui/ImGuiFirstRunPreset.cpp b/src/dusk/imgui/ImGuiFirstRunPreset.cpp index b728b8cd24..6b561cd877 100644 --- a/src/dusk/imgui/ImGuiFirstRunPreset.cpp +++ b/src/dusk/imgui/ImGuiFirstRunPreset.cpp @@ -44,6 +44,7 @@ static void ApplyPresetDusk() { s.game.enableFrameInterpolation.setValue(true); s.game.sunsSong.setValue(true); s.game.bloomMode.setValue(BloomMode::Dusk); + s.game.autoSave.setValue(true); } // ========================================================================= diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index cf72e17a9a..c70c55c2fb 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -3,31 +3,13 @@ #include "ImGuiEngine.hpp" #include "ImGuiConsole.hpp" -#include "ImGuiMenuGame.hpp" #include "ImGuiConfig.hpp" -#include "JSystem/JUtility/JUTGamePad.h" -#include "dusk/audio/DuskAudioSystem.h" -#include "dusk/audio/DuskDsp.hpp" #include "dusk/main.h" #include "dusk/hotkeys.h" -#include "dusk/settings.h" -#include "dusk/livesplit.h" -#include "m_Do/m_Do_controller_pad.h" -#include "m_Do/m_Do_graphic.h" - -#include -#include - #include "m_Do/m_Do_main.h" -namespace { -constexpr int kInternalResolutionScaleMax = 12; -} // namespace - -namespace aurora::gx { -extern bool enableLodBias; -} +#include namespace dusk { void ImGuiMenuGame::ToggleFullscreen() { @@ -40,481 +22,17 @@ namespace dusk { void ImGuiMenuGame::draw() { if (ImGui::BeginMenu("Settings")) { - drawAudioMenu(); - drawCheatsMenu(); - drawGameplayMenu(); - drawGraphicsMenu(); - drawInputMenu(); - drawInterfaceMenu(); - - ImGui::Separator(); - - if (ImGui::MenuItem("Reset", hotkeys::DO_RESET)) { - JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; - } - - if (!IsMobile && ImGui::MenuItem("Exit")) { - dusk::IsRunning = false; - } - - ImGui::EndMenu(); - } - } - - void ImGuiMenuGame::drawGraphicsMenu() { - if (ImGui::BeginMenu("Graphics")) { - ImGui::SeparatorText("Display"); - - if (!IsMobile) { - if (ImGui::MenuItem("Toggle Fullscreen", hotkeys::TOGGLE_FULLSCREEN)) { - ToggleFullscreen(); - } - - if (ImGui::Button("Restore Default Window Size")) { - getSettings().video.enableFullscreen.setValue(false); - VISetWindowFullscreen(false); - VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2); - VICenterWindow(); - } - } - - ImGui::Separator(); - - bool vsync = getSettings().video.enableVsync; - if (ImGui::Checkbox("Enable VSync", &vsync)) { - getSettings().video.enableVsync.setValue(vsync); - aurora_enable_vsync(vsync); - config::Save(); - } - - bool lockAspect = getSettings().video.lockAspectRatio; - if (ImGui::Checkbox("Force 4:3 Aspect Ratio", &lockAspect)) { - getSettings().video.lockAspectRatio.setValue(lockAspect); - - if (lockAspect) { - AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT); - } else { - AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH); - } - - config::Save(); - } - - ImGui::SeparatorText("Resolution"); - - u32 internalResolutionWidth = 0; - u32 internalResolutionHeight = 0; - AuroraGetRenderSize(&internalResolutionWidth, &internalResolutionHeight); - ImGui::TextDisabled("Current internal resolution: %ux%u", internalResolutionWidth, - internalResolutionHeight); - - int scale = std::clamp(getSettings().game.internalResolutionScale.getValue(), 0, - kInternalResolutionScaleMax); - if (ImGui::SliderInt("Internal Resolution", &scale, 0, kInternalResolutionScaleMax, - scale == 0 ? "Auto" : "%dx")) - { - getSettings().game.internalResolutionScale.setValue(scale); - VISetFrameBufferScale(static_cast(scale)); - config::Save(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Auto renders at the native window resolution.\n" - "Higher values scale the game's internal framebuffer."); - } - - config::ImGuiSliderInt("Shadow Resolution", getSettings().game.shadowResolutionMultiplier, 1, 8, "x%d"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Improves the shadow resolution, making them higher quality."); - } - - ImGui::SeparatorText("Post-Processing"); - - constexpr const char* bloomModeNames[] = {"Off", "Classic", "Dusk"}; - int bloomMode = static_cast(getSettings().game.bloomMode.getValue()); - if (ImGui::BeginCombo("Bloom", bloomModeNames[bloomMode])) { - for (int i = 0; i < IM_ARRAYSIZE(bloomModeNames); i++) { - const bool selected = bloomMode == i; - if (ImGui::Selectable(bloomModeNames[i], selected)) { - getSettings().game.bloomMode.setValue(static_cast(i)); - config::Save(); - } - if (selected) { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndCombo(); - } - - bool bloomOff = bloomMode == static_cast(BloomMode::Off); - if (bloomOff) ImGui::BeginDisabled(); - float mult = getSettings().game.bloomMultiplier.getValue(); - if (ImGui::SliderFloat("Bloom Brightness", &mult, 0.0f, 1.0f, "%.2f")) { - getSettings().game.bloomMultiplier.setValue(mult); - config::Save(); - } - if (bloomOff) ImGui::EndDisabled(); - - ImGui::SeparatorText("Rendering"); - - config::ImGuiCheckbox("Unlock Framerate", getSettings().game.enableFrameInterpolation); - const bool frameInterpolationHovered = ImGui::IsItemHovered(); - ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.72f, 0.2f, 1.0f)); - ImGui::TextUnformatted("[EXPERIMENTAL]"); - ImGui::PopStyleColor(); - if (frameInterpolationHovered || ImGui::IsItemHovered()) { - ImGui::SetTooltip("Uses inter-frame interpolation to enable higher frame rates.\nVisual artifacts, animation glitches, or instability may occur."); - } - - ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); - - config::ImGuiCheckbox("Enable Depth of Field", getSettings().game.enableDepthOfField); - - config::ImGuiCheckbox("Enable Mini-Map Shadows", getSettings().game.enableMapBackground); - - ImGui::EndMenu(); - } - } - - void ImGuiMenuGame::drawGameplayMenu() { - if (ImGui::BeginMenu("Gameplay")) { - ImGui::SeparatorText("General"); - - config::ImGuiCheckbox("Mirror Mode", getSettings().game.enableMirrorMode); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Mirrors the world horizontally, matching the Wii version of the game."); - } - - config::ImGuiCheckbox("Disable Main HUD", getSettings().game.disableMainHUD); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Disables the main HUD of the game.\n" - "Useful for recording or a more immersive experience!"); - } - - config::ImGuiCheckbox("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Restores patched glitches from Wii USA 1.0,\n" - "the first released version."); - } - - config::ImGuiCheckbox("Enable Rotating Link Doll", getSettings().game.enableLinkDollRotation); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Enables rotating Link in the collection menu with the C-Stick"); - } - - ImGui::SeparatorText("Difficulty"); - - ImGui::BeginDisabled(getSettings().game.speedrunMode); - config::ImGuiSliderInt("Damage Multiplier", getSettings().game.damageMultiplier, 1, 8, "x%d"); - - config::ImGuiCheckbox("Instant Death", getSettings().game.instantDeath); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Any hit will instantly kill you."); - } - - config::ImGuiCheckbox("No Heart Drops", getSettings().game.noHeartDrops); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Hearts will never drop from enemies,\n" - "pots and various other places."); - } - ImGui::EndDisabled(); - - ImGui::SeparatorText("Quality of Life"); - - config::ImGuiCheckbox("Bigger Wallets", getSettings().game.biggerWallets); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Wallet sizes are like in the HD version. (500, 1000, 2000)"); - } - - config::ImGuiCheckbox("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Rupees won't play cutscenes after you've collected them the first time."); - } - - config::ImGuiCheckbox("Faster Climbing", getSettings().game.fastClimbing); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Quicker climbing on ladders and vines like the HD version."); - } - - config::ImGuiCheckbox("Faster Tears of Light", getSettings().game.fastTears); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Tears of Light dropped by Shadow Insects pop out faster like the HD version."); - } - - config::ImGuiCheckbox("Instant Saves", getSettings().game.instantSaves); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Skip the delay when writing to the Memory Card."); - } - - config::ImGuiCheckbox("Hold B for Instant Text", getSettings().game.instantText); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Make text scroll immediately by holding B."); - } - - config::ImGuiCheckbox("No Climbing Miss Animation", getSettings().game.noMissClimbing); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Prevents Link from playing a struggle animation\n" - "when grabbing ledges or climbing on vines."); - } - - config::ImGuiCheckbox("No Rupee Returns", getSettings().game.noReturnRupees); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Always collect Rupees even if your Wallet is too full."); - } - - config::ImGuiCheckbox("No Sword Recoil", getSettings().game.noSwordRecoil); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Link won't recoil when his sword hits walls."); - } - - config::ImGuiCheckbox("No 2nd Fish for Cat", getSettings().game.no2ndFishForCat); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Only need to fish once for Sera's cat to return."); - } - - config::ImGuiCheckbox("Skip TV Settings Screen", getSettings().game.hideTvSettingsScreen); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Skip the TV calibration screen shown when loading a save."); - } - - config::ImGuiCheckbox("Skip Warning Screen", getSettings().game.skipWarningScreen); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Skip the warning screen shown when starting the game."); - } - - config::ImGuiCheckbox("Sun's Song (R+X)", getSettings().game.sunsSong); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Allows Wolf Link to howl and change the time of day."); - } - - config::ImGuiCheckbox("Quick Transform (R+Y)", getSettings().game.enableQuickTransform); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Transform instantly by pressing R and Y simultaneously."); - } - - ImGui::SeparatorText("Speedrunning"); - if (config::ImGuiCheckbox("Speedrun Mode", getSettings().game.speedrunMode)) { - resetForSpeedrunMode(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Enables Speedrunning options, while restricting certain gameplay modifiers."); - } - - ImGui::BeginDisabled(!getSettings().game.speedrunMode); - bool prevLiveSplit = getSettings().game.liveSplitEnabled; - config::ImGuiCheckbox("LiveSplit Connection", getSettings().game.liveSplitEnabled); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Connect to LiveSplit server on localhost:16834."); - } - ImGui::EndDisabled(); - - if ((bool)getSettings().game.liveSplitEnabled != prevLiveSplit) { - if (getSettings().game.liveSplitEnabled) { - dusk::speedrun::connectLiveSplit(); - } else { - dusk::speedrun::disconnectLiveSplit(); - DuskToast("LiveSplit disconnected", 3.f); - } - } - - ImGui::EndMenu(); - } - } - - void ImGuiMenuGame::drawCheatsMenu() { - if (ImGui::BeginMenu("Cheats")) { - ImGui::BeginDisabled(getSettings().game.speedrunMode); - - ImGui::SeparatorText("Resources"); - config::ImGuiCheckbox("Infinite Hearts", getSettings().game.infiniteHearts); - config::ImGuiCheckbox("Infinite Arrows", getSettings().game.infiniteArrows); - config::ImGuiCheckbox("Infinite Bombs", getSettings().game.infiniteBombs); - config::ImGuiCheckbox("Infinite Oil", getSettings().game.infiniteOil); - config::ImGuiCheckbox("Infinite Oxygen", getSettings().game.infiniteOxygen); - config::ImGuiCheckbox("Infinite Rupees", getSettings().game.infiniteRupees); - config::ImGuiCheckbox("No Item Timer", getSettings().game.enableIndefiniteItemDrops); - ImGui::SetItemTooltip("Item drops such as Rupees, Hearts, etc. will never disappear after they drop."); - - ImGui::SeparatorText("Abilities"); - config::ImGuiCheckbox("Moon Jump (R+A)", getSettings().game.moonJump); - config::ImGuiCheckbox("Super Clawshot", getSettings().game.superClawshot); - config::ImGuiCheckbox("Always Greatspin", getSettings().game.alwaysGreatspin); - config::ImGuiCheckbox("Fast Iron Boots", getSettings().game.enableFastIronBoots); - - config::ImGuiCheckbox("Can Transform Anywhere", getSettings().game.canTransformAnywhere); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Allows you to transform even if NPCs are looking."); - } - - config::ImGuiCheckbox("Fast Spinner", getSettings().game.fastSpinner); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Speeds up Spinner movement when holding R."); - } - - config::ImGuiCheckbox("Free Magic Armor", getSettings().game.freeMagicArmor); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Makes the magic armor work without rupees."); - } - - ImGui::EndDisabled(); - - ImGui::EndMenu(); - } - } - - void ImGuiMenuGame::drawAudioMenu() { - if (ImGui::BeginMenu("Audio")) { - - ImGui::SeparatorText("Volume"); - - ImGui::Text("Master Volume"); - if (config::ImGuiSliderInt("##masterVolume", getSettings().audio.masterVolume, 0, 100)) { - dusk::audio::SetMasterVolume(getSettings().audio.masterVolume / 100.0f); - } - - /* - // TODO: Implement additional settings - ImGui::Text("Main Music Volume"); - ImGui::SliderFloat("##mainMusicVolume", &getSettings().audio.mainMusicVolume, 0, 100); - - ImGui::Text("Sub Music Volume"); - ImGui::SliderFloat("##subMusicVolume", &getSettings().audio.subMusicVolume, 0, 100); - - ImGui::Text("Sound Effects Volume"); - ImGui::SliderFloat("##soundEffectsVolume", &getSettings().audio.soundEffectsVolume, 0, 100); - - ImGui::Text("Fanfare Volume"); - ImGui::SliderFloat("##fanfareVolume", &getSettings().audio.fanfareVolume, 0, 100); - - Z2AudioMgr* audioMgr = Z2AudioMgr::getInterface(); - if (audioMgr != nullptr) { - } - */ - - ImGui::SeparatorText("Effects"); - - if (config::ImGuiCheckbox("Enable Reverb", getSettings().audio.enableReverb)) { - dusk::audio::SetEnableReverb(getSettings().audio.enableReverb); - } - - if (config::ImGuiCheckbox("Spatial Sound", getSettings().audio.enableHrtf)) { - dusk::audio::EnableHrtf = getSettings().audio.enableHrtf; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Emulate surround sound via HRTF (for headphone use only)!"); - } - - ImGui::SeparatorText("Tweaks"); - - config::ImGuiCheckbox("No Low HP Sound", getSettings().game.noLowHpSound); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Disable the beeping sound when having low health."); - } - - config::ImGuiCheckbox("Non-Stop Midna's Lament", getSettings().game.midnasLamentNonStop); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Prevents enemy music while Midna's Lament is playing."); - } - - ImGui::EndMenu(); - } - } - - void ImGuiMenuGame::drawInputMenu() { - if (ImGui::BeginMenu("Input")) { - ImGui::SeparatorText("Controller"); - + // TODO: Remove this once Controller Config exists in RmlUi if (ImGui::Button("Configure Controller")){ m_showControllerConfig = !m_showControllerConfig; } - ImGui::SeparatorText("Camera"); - - config::ImGuiCheckbox("Free Camera", getSettings().game.freeCamera); - - if (getSettings().game.freeCamera) { - config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis); - config::ImGuiCheckbox("Invert Camera Y Axis", getSettings().game.invertCameraYAxis); - config::ImGuiSliderFloat("Free Camera Sensitivity", getSettings().game.freeCameraSensitivity, 0.5f, 2.0f, "%.1f"); - } else { - config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis); - } - - ImGui::SeparatorText("Gyro"); - - config::ImGuiCheckbox("Gyro Aim", getSettings().game.enableGyroAim); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Enables the gyroscope on supported controllers\n" - "while in look mode (C-Up) and while aiming the\n" - "Slingshot, Gale Boomerang, Hero's Bow, Clawshot(s),\n" - "Ball and Chain, and Dominion Rod."); - } - - config::ImGuiCheckbox("Gyro Rollgoal", getSettings().game.enableGyroRollgoal); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Enables the gyroscope on supported controllers to\n" - "tilt the Rollgoal table in Hena's Cabin."); - } - - if (getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal) { - config::ImGuiSliderFloat("Gyro Pitch Sensitivity", getSettings().game.gyroSensitivityY, 0.25f, 4.0f, "%.2f"); - config::ImGuiSliderFloat("Gyro Yaw Sensitivity", getSettings().game.gyroSensitivityX, 0.25f, 4.0f, "%.2f"); - - if (getSettings().game.enableGyroRollgoal) { - config::ImGuiSliderFloat("Rollgoal Sensitivity", getSettings().game.gyroSensitivityRollgoal, 0.25f, 4.0f, "%.2f"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Additional multiplier for scaling how strongly\n" - "the gyroscope affects the Rollgoal table."); - } - } - - config::ImGuiSliderFloat("Gyro Deadband", getSettings().game.gyroDeadband, 0.0f, 0.5f, "%.3f"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Angular rates below this magnitude are treated as zero,\n" - "reducing drift and jitter when the controller is still."); - } - - config::ImGuiSliderFloat("Gyro Smoothing", getSettings().game.gyroSmoothing, 0.0f, 1.0f, "%.2f"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Low values track raw gyro input more closely,\n" - "while higher values smooth out input over time."); - } - - config::ImGuiCheckbox("Invert Gyro Pitch", getSettings().game.gyroInvertPitch); - config::ImGuiCheckbox("Invert Gyro Yaw", getSettings().game.gyroInvertYaw); - } - - ImGui::SeparatorText("Tools"); - - ImGui::BeginDisabled(getSettings().game.speedrunMode); - config::ImGuiCheckbox("Turbo Key", getSettings().game.enableTurboKeybind); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Hold TAB to increase game speed by up to 4x."); - } - ImGui::EndDisabled(); - ImGui::Checkbox("Show Input Viewer", &m_showInputViewer); ImGui::EndMenu(); } } - void ImGuiMenuGame::drawInterfaceMenu() { - if (ImGui::BeginMenu("Interface")) { - config::ImGuiCheckbox("Achievement Notifications", getSettings().game.enableAchievementNotifications); - config::ImGuiCheckbox("Skip Pre-Launch UI", getSettings().backend.skipPreLaunchUI); - config::ImGuiCheckbox("Show Pipeline Compilation", getSettings().backend.showPipelineCompilation); -#if DUSK_ENABLE_SENTRY_NATIVE - config::ImGuiCheckbox("Enable Crash Reporting", getSettings().backend.enableCrashReporting); -#endif - if (!IsMobile) { - config::ImGuiCheckbox("Pause on Focus Lost", getSettings().game.pauseOnFocusLost); - } - - ImGui::EndMenu(); - } - } - static void drawVirtualStick(const char* id, const ImVec2& stick) { float scale = ImGuiScale(); ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x + 45 * scale, ImGui::GetCursorPos().y + 10)); diff --git a/src/dusk/imgui/ImGuiMenuGame.hpp b/src/dusk/imgui/ImGuiMenuGame.hpp index e54541a010..b5ecfa37c0 100644 --- a/src/dusk/imgui/ImGuiMenuGame.hpp +++ b/src/dusk/imgui/ImGuiMenuGame.hpp @@ -55,13 +55,6 @@ namespace dusk { static void resetForSpeedrunMode(); private: - void drawAudioMenu(); - void drawInputMenu(); - void drawGraphicsMenu(); - void drawGameplayMenu(); - void drawCheatsMenu(); - void drawInterfaceMenu(); - struct { int m_selectedPort = 0; bool m_isReading = false; diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 2e3738aab9..da217022f8 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -41,6 +41,10 @@ static void OpenDataFolder() { #define DUSK_CAN_OPEN_DATA_FOLDER 0 #endif +namespace aurora::gx { +extern bool enableLodBias; +} + namespace dusk { ImGuiMenuTools::ImGuiMenuTools() {} @@ -91,6 +95,7 @@ namespace dusk { getSettings().game.disableWaterRefraction.setValue(disableWaterRefraction); config::Save(); } + ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); ImGui::EndMenu(); } diff --git a/src/dusk/iso_validate.cpp b/src/dusk/iso_validate.cpp index e83b3fd34e..a4e64c5b70 100644 --- a/src/dusk/iso_validate.cpp +++ b/src/dusk/iso_validate.cpp @@ -101,6 +101,10 @@ ValidationError validate(const char* path) { NodHandleWrapper disc; const auto sdlStream = SDL_IOFromFile(path, "rb"); + if (sdlStream == nullptr) { + return ValidationError::IOError; + } + const NodDiscStream nod_stream { .user_data = sdlStream, .read_at = StreamReadAt, diff --git a/src/dusk/logging.cpp b/src/dusk/logging.cpp index 172059aea4..8b96fcffd5 100644 --- a/src/dusk/logging.cpp +++ b/src/dusk/logging.cpp @@ -1,5 +1,6 @@ #include "dusk/logging.h" #include +#include #include #include #include @@ -32,9 +33,33 @@ static constexpr std::string_view StubFragments[] = { }; namespace { -std::mutex g_logMutex; -FILE* g_logFile = nullptr; -std::string g_logFilePath; +// On macOS, std::mutex becomes poisoned when its dtor is run. +// We use this to check if the LogState is destroyed before attempting to acquire it. +std::atomic g_logStateAlive(true); + +struct LogState { + std::mutex mutex; + FILE* file = nullptr; + std::string filePath; + + ~LogState() { + CloseFile(); + g_logStateAlive.store(false, std::memory_order_release); + } + + void CloseFile() { + if (!g_logStateAlive.load(std::memory_order_acquire)) { + return; + } + std::lock_guard lock(mutex); + if (file != nullptr) { + std::fflush(file); + std::fclose(file); + file = nullptr; + } + } +}; +LogState g_logState; const char* LogLevelString(AuroraLogLevel level) { switch (level) { @@ -152,10 +177,10 @@ void aurora_log_callback(AuroraLogLevel level, const char* module, const char* m FILE* out = LogStreamForLevel(level); WriteLogLine(out, levelStr, module, message, len); - { - std::lock_guard lock(g_logMutex); - if (g_logFile != nullptr) { - WriteLogLine(g_logFile, levelStr, module, message, len); + if (g_logStateAlive.load(std::memory_order_acquire)) { + std::lock_guard lock(g_logState.mutex); + if (g_logState.file != nullptr) { + WriteLogLine(g_logState.file, levelStr, module, message, len); } } @@ -169,8 +194,11 @@ void aurora_log_callback(AuroraLogLevel level, const char* module, const char* m aurora::Module DuskLog("dusk"); void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraLogLevel logLevel) { - std::lock_guard lock(g_logMutex); - if (g_logFile != nullptr || configDir.empty()) { + if (!g_logStateAlive.load(std::memory_order_acquire)) { + return; + } + std::lock_guard lock(g_logState.mutex); + if (g_logState.file != nullptr || configDir.empty()) { return; } @@ -184,31 +212,30 @@ void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraL } const std::filesystem::path logPath = logsDir / MakeTimestampedLogName(); - g_logFile = std::fopen(logPath.string().c_str(), "wb"); - if (g_logFile == nullptr) { + g_logState.file = std::fopen(logPath.string().c_str(), "wb"); + if (g_logState.file == nullptr) { std::fprintf(stderr, "[WARNING | dusk] Failed to open log file '%s'\n", logPath.string().c_str()); return; } - g_logFilePath = logPath.string(); + g_logState.filePath = logPath.string(); aurora::g_config.logCallback = &aurora_log_callback; aurora::g_config.logLevel = logLevel; - WriteLogLine(g_logFile, "INFO", "dusk", "File logging initialized", 24); + WriteLogLine(g_logState.file, "INFO", "dusk", "File logging initialized", 24); } void dusk::ShutdownFileLogging() { - std::lock_guard lock(g_logMutex); - if (g_logFile == nullptr) { + if (!g_logStateAlive.load(std::memory_order_acquire)) { return; } - - std::fflush(g_logFile); - std::fclose(g_logFile); - g_logFile = nullptr; + g_logState.CloseFile(); } const char* dusk::GetLogFilePath() { - std::lock_guard lock(g_logMutex); - return g_logFilePath.empty() ? nullptr : g_logFilePath.c_str(); + if (!g_logStateAlive.load(std::memory_order_acquire)) { + return nullptr; + } + std::lock_guard lock(g_logState.mutex); + return g_logState.filePath.empty() ? nullptr : g_logState.filePath.c_str(); } diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 6198e789d1..49df869dd4 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -32,7 +32,7 @@ UserSettings g_userSettings = { .disableRupeeCutscenes {"game.disableRupeeCutscenes", false}, .noSwordRecoil {"game.noSwordRecoil", false}, .damageMultiplier {"game.damageMultiplier", 1}, - .noHeartDrops{"game.noHeartDrops", false}, + .noHeartDrops {"game.noHeartDrops", false}, .instantDeath {"game.instantDeath", false}, .fastClimbing {"game.fastClimbing", false}, .noMissClimbing {"game.noMissClimbing", false}, @@ -41,6 +41,7 @@ UserSettings g_userSettings = { .instantSaves {"game.instantSaves", false}, .instantText {"game.instantText", false}, .sunsSong {"game.sunsSong", false}, + .autoSave {"game.autoSave", false}, // Preferences .enableMirrorMode {"game.enableMirrorMode", false}, @@ -53,7 +54,7 @@ UserSettings g_userSettings = { .bloomMode {"game.bloomMode", BloomMode::Classic}, .bloomMultiplier {"game.bloomMultiplier", 1.0f}, .disableWaterRefraction {"game.disableWaterRefraction", false}, - .enableFrameInterpolation = {"game.enableFrameInterpolation", false}, + .enableFrameInterpolation {"game.enableFrameInterpolation", false}, .internalResolutionScale {"game.internalResolutionScale", 0}, .shadowResolutionMultiplier {"game.shadowResolutionMultiplier", 1}, .enableDepthOfField {"game.enableDepthOfField", true}, @@ -154,6 +155,7 @@ void registerSettings() { Register(g_userSettings.game.instantSaves); Register(g_userSettings.game.instantText); Register(g_userSettings.game.sunsSong); + Register(g_userSettings.game.autoSave); Register(g_userSettings.game.enableMirrorMode); Register(g_userSettings.game.invertCameraXAxis); Register(g_userSettings.game.invertCameraYAxis); diff --git a/src/dusk/ui/bool_button.cpp b/src/dusk/ui/bool_button.cpp new file mode 100644 index 0000000000..cc43bb8b65 --- /dev/null +++ b/src/dusk/ui/bool_button.cpp @@ -0,0 +1,29 @@ +#include "bool_button.hpp" + +namespace dusk::ui { + +BoolButton::BoolButton(Rml::Element* parent, Props props) + : BaseControlledSelectButton(parent, {std::move(props.key)}), + mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)), + mIsDisabled(std::move(props.isDisabled)) {} + +bool BoolButton::disabled() const { + if (mIsDisabled) { + return mIsDisabled(); + } + return BaseControlledSelectButton::disabled(); +} + +Rml::String BoolButton::format_value() { + return mGetValue() ? "On" : "Off"; +} + +bool BoolButton::handle_nav_command(NavCommand cmd) { + if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || cmd == NavCommand::Right) { + mSetValue(!mGetValue()); + return true; + } + return false; +} + +} // namespace dusk::ui \ No newline at end of file diff --git a/src/dusk/ui/bool_button.hpp b/src/dusk/ui/bool_button.hpp new file mode 100644 index 0000000000..750c7e5cdb --- /dev/null +++ b/src/dusk/ui/bool_button.hpp @@ -0,0 +1,29 @@ +#pragma once +#include "select_button.hpp" + +namespace dusk::ui { + +class BoolButton : public BaseControlledSelectButton { +public: + struct Props { + Rml::String key; + std::function getValue; + std::function setValue; + std::function isDisabled; + }; + + BoolButton(Rml::Element* parent, Props props); + + bool disabled() const override; + +protected: + Rml::String format_value() override; + bool handle_nav_command(NavCommand cmd) override; + +private: + std::function mGetValue; + std::function mSetValue; + std::function mIsDisabled; +}; + +} // namespace dusk::ui diff --git a/src/dusk/ui/button.cpp b/src/dusk/ui/button.cpp new file mode 100644 index 0000000000..a51918004e --- /dev/null +++ b/src/dusk/ui/button.cpp @@ -0,0 +1,64 @@ +#include "button.hpp" + +#include "ui.hpp" + +#include + +namespace dusk::ui { +namespace { + +Rml::Element* createRoot(Rml::Element* parent, const Rml::String& tagName) { + auto* doc = parent->GetOwnerDocument(); + auto elem = doc->CreateElement(tagName); + return parent->AppendChild(std::move(elem)); +} + +} // namespace + +Button::Button(Rml::Element* parent, Props props, const Rml::String& tagName) + : FluentComponent(createRoot(parent, tagName)) { + update_props(std::move(props)); +} + +void Button::set_text(const Rml::String& text) { + if (mProps.text != text) { + mRoot->SetInnerRML(escape(text)); + mProps.text = text; + } +} + +Button& Button::on_pressed(ButtonCallback callback) { + if (!callback) { + return *this; + } + // TODO: convert this to a FluentComponent method? + on_nav_command([callback = std::move(callback)](Rml::Event&, NavCommand cmd) { + if (cmd == NavCommand::Confirm) { + callback(); + return true; + } + return false; + }); + return *this; +} + +void Button::update_props(Props props) { + set_text(props.text); + mProps = std::move(props); +} + +void ControlledButton::update() { + if (mIsSelected) { + set_selected(mIsSelected()); + } + Button::update(); +} + +bool ControlledButton::selected() const { + if (mIsSelected) { + return mIsSelected(); + } + return Button::selected(); +} + +} // namespace dusk::ui \ No newline at end of file diff --git a/src/dusk/ui/button.hpp b/src/dusk/ui/button.hpp new file mode 100644 index 0000000000..43e349e3fa --- /dev/null +++ b/src/dusk/ui/button.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include "component.hpp" + +namespace dusk::ui { + +using ButtonCallback = std::function; + +class Button : public FluentComponent