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 b326931e79..6045ebc734 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)
@@ -296,8 +297,10 @@ set(GAME_INCLUDE_DIRS
extern
${CMAKE_BINARY_DIR})
+find_package(Threads REQUIRED)
set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd
- aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt)
+ aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
+ Threads::Threads)
list(APPEND GAME_LIBS libzstd_static)
@@ -306,6 +309,10 @@ if (DUSK_ENABLE_SENTRY_NATIVE)
list(APPEND GAME_COMPILE_DEFS DUSK_ENABLE_SENTRY_NATIVE=1 SENTRY_BUILD_STATIC=1)
endif ()
+if (WIN32)
+ list(APPEND GAME_LIBS Ws2_32)
+endif ()
+
if (DUSK_MOVIE_SUPPORT)
if (TARGET libjpeg-turbo::turbojpeg-static)
list(APPEND GAME_LIBS libjpeg-turbo::turbojpeg-static)
@@ -315,46 +322,13 @@ if (DUSK_MOVIE_SUPPORT)
list(APPEND GAME_COMPILE_DEFS MOVIE_SUPPORT=1)
endif ()
-option(DUSK_ENABLE_DISCORD_RPC "Enable Discord Rich Presence support" ON)
-if (DUSK_ENABLE_DISCORD_RPC AND NOT ANDROID AND NOT IOS AND NOT TVOS)
-
- FetchContent_Populate(discord_rpc
- URL https://github.com/discord/discord-rpc/archive/refs/tags/v3.4.0.tar.gz
- URL_HASH SHA256=e13427019027acd187352dacba6c65953af66fdf3c35fcf38fc40b454a9d7855
- DOWNLOAD_EXTRACT_TIMESTAMP TRUE
- )
- # RapidJSON is a git submodule absent from the discord-rpc tarball; fetch separately.
- FetchContent_Populate(rapidjson
- URL https://github.com/Tencent/rapidjson/archive/refs/tags/v1.1.0.tar.gz
- URL_HASH SHA256=bf7ced29704a1e696fbccf2a2b4ea068e7774fa37f6d7dd4039d0787f8bed98e
- DOWNLOAD_EXTRACT_TIMESTAMP TRUE
- )
-
- if (NOT TARGET discord-rpc)
- set(_drpc ${discord_rpc_SOURCE_DIR}/src)
- set(_drpc_src
- ${_drpc}/discord_rpc.cpp
- ${_drpc}/rpc_connection.cpp
- ${_drpc}/serialization.cpp
- )
- if (WIN32)
- list(APPEND _drpc_src ${_drpc}/connection_win.cpp ${_drpc}/discord_register_win.cpp)
- elseif (APPLE)
- list(APPEND _drpc_src ${_drpc}/connection_unix.cpp ${_drpc}/discord_register_osx.m)
- else ()
- list(APPEND _drpc_src ${_drpc}/connection_unix.cpp ${_drpc}/discord_register_linux.cpp)
- endif ()
- add_library(discord-rpc STATIC ${_drpc_src})
- target_include_directories(discord-rpc PUBLIC
- ${discord_rpc_SOURCE_DIR}/include
- ${rapidjson_SOURCE_DIR}/include
- )
- if (UNIX)
- target_link_libraries(discord-rpc PUBLIC pthread)
- endif ()
- endif ()
- list(APPEND GAME_LIBS discord-rpc)
- list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD_RPC=1)
+set(DUSK_ENABLE_DISCORD_DEFAULT ON)
+if (DEFINED DUSK_ENABLE_DISCORD_RPC AND NOT DEFINED DUSK_ENABLE_DISCORD)
+ set(DUSK_ENABLE_DISCORD_DEFAULT ${DUSK_ENABLE_DISCORD_RPC})
+endif ()
+option(DUSK_ENABLE_DISCORD "Enable Discord Rich Presence support" ${DUSK_ENABLE_DISCORD_DEFAULT})
+if (DUSK_ENABLE_DISCORD AND NOT ANDROID AND NOT IOS AND NOT TVOS)
+ list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD=1)
endif ()
# Edit & Continue
diff --git a/README.md b/README.md
index b6d6cec863..02b1ded875 100644
--- a/README.md
+++ b/README.md
@@ -1,31 +1,48 @@
-
+
+

-- ### **[Official Website](https://twilitrealm.dev)**
-- ### **[Discord](https://discord.gg/QACynxeyna)**
+
+ Official Website
+ •
+ Discord
+
+
+
+# Overview
+
+Dusk is a reverse-engineered reimplementation of Twilight Princess.
+
+It aims to be as accurate as possible to the original while also providing new options, enhancements, and tools to customize your experience.
# Setup
-**⚠️ Dusk does NOT provide any copyrighted assets. You must provide your own copy of the game.**
-### 1. Verify your ROM dump
-First make sure your dump of the game is clean and supported by Dusk. You can do this by checking the sha1 hash of your dump against this list of supported versions.
+> [!IMPORTANT]
+> Dusk does *not* provide any copyrighted assets. You must provide your own copy of the original game.
-| Version | sha1 hash |
-|--------------| ---------------------------------------- |
-| GameCube USA | 75edd3ddff41f125d1b4ce1a40378f1b565519e7 |
-| GameCube PAL | 2601822a488eeb86fb89db16ca8f29c2c953e1ca |
+### 1. Verify your dump
+
+First, make sure your dump of the game is clean and supported by Dusk. You can do this by checking the SHA-1 hash of your dump against this list of supported versions:
+
+| Version | SHA-1 hash |
+|--------------| ------------------------------------------ |
+| GameCube USA | `75edd3ddff41f125d1b4ce1a40378f1b565519e7` |
+| GameCube EUR | `2601822a488eeb86fb89db16ca8f29c2c953e1ca` |
### 2. Download [Dusk](https://github.com/TwilitRealm/dusk/releases)
### 3. Setup the game
-- Extract the zip folder
-- Launch Dusk
-- Select Options, then set the ISO Path to your supported game dump
-- Press Start Game to play!
-
+- Extract the .zip file
+- Launch Dusk
+- Press **Select Disc Image**, navigate to your game dump, and select the file
+- Press **Start Game** to play!
# Building
+
If you'd like to build Dusk from source, please read the [build instructions](docs/building.md).
+Pull requests are welcomed! Note that we do not accept contributions that are primarily AI-generated and will close your PR if we suspect as much.
+
# Credits
+
Special thanks to the [TP decompilation](https://github.com/zeldaret/tp) team, the GC/Wii decompilation community, the [Aurora](https://github.com/encounter/aurora) developers, the [TP speedrunning community](https://zsrtp.link), and all [contributors](https://github.com/TwilitRealm/dusk/graphs/contributors).
diff --git a/assets/dusk_options.png b/assets/dusk_options.png
deleted file mode 100644
index 4ed4ad7563..0000000000
Binary files a/assets/dusk_options.png and /dev/null differ
diff --git a/extern/aurora b/extern/aurora
index 8af9057689..518747aa86 160000
--- a/extern/aurora
+++ b/extern/aurora
@@ -1 +1 @@
-Subproject commit 8af9057689b9b85a55bbbd6218bc95304558fabd
+Subproject commit 518747aa86a50be62ecf92aeb309309b0d58a54a
diff --git a/files.cmake b/files.cmake
index f001a72f47..a463ce5129 100644
--- a/files.cmake
+++ b/files.cmake
@@ -1,7 +1,7 @@
set(DOLZEL_FILES
src/m_Do/m_Do_main.cpp
- src/m_Do/m_Do_printf.cpp
+ #src/m_Do/m_Do_printf.cpp
src/m_Do/m_Do_audio.cpp
src/m_Do/m_Do_controller_pad.cpp
#src/m_Do/m_Re_controller_pad.cpp
@@ -1429,6 +1429,7 @@ set(DUSK_FILES
src/dusk/globals.cpp
src/dusk/gyro.cpp
src/dusk/gamepad_color.cpp
+ src/dusk/autosave.cpp
src/dusk/io.cpp
src/dusk/layout.cpp
src/dusk/logging.cpp
@@ -1448,8 +1449,6 @@ set(DUSK_FILES
src/dusk/imgui/ImGuiMenuTools.hpp
src/dusk/imgui/ImGuiPreLaunchWindow.cpp
src/dusk/imgui/ImGuiPreLaunchWindow.hpp
- src/dusk/imgui/ImGuiFirstRunPreset.hpp
- src/dusk/imgui/ImGuiFirstRunPreset.cpp
src/dusk/imgui/ImGuiProcessOverlay.cpp
src/dusk/imgui/ImGuiCameraOverlay.cpp
src/dusk/imgui/ImGuiHeapOverlay.cpp
@@ -1460,14 +1459,61 @@ set(DUSK_FILES
src/dusk/imgui/ImGuiSaveEditor.cpp
src/dusk/imgui/ImGuiStateShare.hpp
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/controller_config.cpp
+ src/dusk/ui/controller_config.hpp
+ src/dusk/ui/document.cpp
+ src/dusk/ui/document.hpp
+ src/dusk/ui/achievements.cpp
+ src/dusk/ui/achievements.hpp
+ src/dusk/ui/preset.cpp
+ src/dusk/ui/preset.hpp
+ src/dusk/ui/editor.cpp
+ src/dusk/ui/editor.hpp
+ src/dusk/ui/event.cpp
+ src/dusk/ui/event.hpp
+ src/dusk/ui/input.cpp
+ src/dusk/ui/input.hpp
+ src/dusk/ui/nav_types.hpp
+ src/dusk/ui/number_button.cpp
+ src/dusk/ui/number_button.hpp
+ src/dusk/ui/overlay.cpp
+ src/dusk/ui/overlay.hpp
+ src/dusk/ui/pane.cpp
+ src/dusk/ui/pane.hpp
+ src/dusk/ui/popup.cpp
+ src/dusk/ui/popup.hpp
+ src/dusk/ui/prelaunch.cpp
+ src/dusk/ui/prelaunch.hpp
+ src/dusk/ui/prelaunch_options.cpp
+ src/dusk/ui/prelaunch_options.hpp
+ src/dusk/ui/select_button.cpp
+ src/dusk/ui/select_button.hpp
+ src/dusk/ui/settings.cpp
+ src/dusk/ui/settings.hpp
+ src/dusk/ui/string_button.cpp
+ src/dusk/ui/string_button.hpp
+ src/dusk/ui/tab_bar.cpp
+ src/dusk/ui/tab_bar.hpp
+ src/dusk/ui/ui.cpp
+ src/dusk/ui/ui.hpp
+ src/dusk/ui/window.cpp
+ src/dusk/ui/window.hpp
src/dusk/achievements.cpp
src/dusk/iso_validate.cpp
+ src/dusk/livesplit.cpp
src/dusk/offset_ptr.cpp
src/dusk/OSContext.cpp
+ src/dusk/OSReport.cpp
src/dusk/OSThread.cpp
src/dusk/OSMutex.cpp
+ src/dusk/discord.cpp
+ src/dusk/discord.hpp
src/dusk/discord_presence.cpp
src/dusk/version.cpp
)
diff --git a/include/d/actor/d_a_mirror.h b/include/d/actor/d_a_mirror.h
index 06c5603899..e2f9a51e30 100644
--- a/include/d/actor/d_a_mirror.h
+++ b/include/d/actor/d_a_mirror.h
@@ -27,6 +27,7 @@ public:
/* 0x17C */ cXyz mViewScale;
#if TARGET_PC
bool mbReset = false;
+ bool mbHadEntry = false;
#endif
};
diff --git a/include/d/actor/d_a_obj_klift00.h b/include/d/actor/d_a_obj_klift00.h
index 865fab8c47..ab5403d51b 100644
--- a/include/d/actor/d_a_obj_klift00.h
+++ b/include/d/actor/d_a_obj_klift00.h
@@ -25,6 +25,10 @@ public:
int Draw();
int Delete();
+#if TARGET_PC
+ void onInterpCallback();
+#endif
+
enum Param_e {
LOCK_e = (1 << 6), NO_BASE_DISP = (1 << 7)
};
@@ -50,6 +54,13 @@ private:
/* 0x1020 */ dCcD_Cyl mCylinderCollider;
/* 0x115C */ s32 mStopSwingingFrames;
+#if TARGET_PC
+ cXyz mChainInterpPrev[64];
+ cXyz mChainInterpCurr[64];
+ bool mChainInterpPrevValid;
+ bool mChainInterpCurrValid;
+#endif
+
// Number of chain models
u32 getArg0() {
return fopAcM_GetParamBit(this, 0, 6);
diff --git a/include/d/actor/d_a_obj_lv8Lift.h b/include/d/actor/d_a_obj_lv8Lift.h
index c5a8ae9f27..c327f4cf3c 100644
--- a/include/d/actor/d_a_obj_lv8Lift.h
+++ b/include/d/actor/d_a_obj_lv8Lift.h
@@ -58,6 +58,9 @@ public:
void setNextPoint();
int Draw();
int Delete();
+#if TARGET_PC
+ friend void daL8Lift_interp_callback(bool isSimFrame, void* pUserWork);
+#endif
u8 getPthID() { return fopAcM_GetParamBit(this, 0, 8); }
u8 getMoveSpeed() { return fopAcM_GetParamBit(this, 8, 4); }
diff --git a/include/d/d_camera.h b/include/d/d_camera.h
index 105c9cae9f..c481262a46 100644
--- a/include/d/d_camera.h
+++ b/include/d/d_camera.h
@@ -118,6 +118,18 @@ class camera_class;
class dCamera_c;
typedef bool (dCamera_c::*engine_fn)(s32);
+#if TARGET_PC
+struct DebugFlyCam {
+ bool initialized;
+ f32 pitch;
+ f32 yaw;
+ cXyz savedCenter;
+ cXyz savedEye;
+ f32 savedFovy;
+ cSAngle savedBank;
+};
+#endif
+
class dCamera_c {
public:
class dCamInfo_c {
@@ -1028,6 +1040,8 @@ public:
bool test2Camera(s32);
#if TARGET_PC
bool freeCamera();
+ bool executeDebugFlyCam();
+ void deactivateDebugFlyCam();
#endif
bool towerCamera(s32);
bool hookshotCamera(s32);
@@ -1376,6 +1390,10 @@ public:
/* 0x970 */ dCamSetup_c mCamSetup;
/* 0xAEC */ dCamParam_c mCamParam;
/* 0xB0C */ u8 field_0xb0c;
+
+#if TARGET_PC
+ DebugFlyCam mDebugFlyCam;
+#endif
}; // Size: 0xB10
dCamera_c* dCam_getBody();
diff --git a/include/d/d_menu_dmap.h b/include/d/d_menu_dmap.h
index e50c533654..81baea1446 100644
--- a/include/d/d_menu_dmap.h
+++ b/include/d/d_menu_dmap.h
@@ -91,6 +91,10 @@ public:
void calcCursor();
void drawCursor();
+ #if TARGET_PC
+ void dMapBgWide();
+ #endif
+
void setDPDFloorSelCurPos(s8 i_pos) { field_0xdd6 = i_pos; }
f32 getMapWidth() { return mMapWidth; }
diff --git a/include/d/d_menu_fmap2D.h b/include/d/d_menu_fmap2D.h
index 9b237f2771..d0152f0229 100644
--- a/include/d/d_menu_fmap2D.h
+++ b/include/d/d_menu_fmap2D.h
@@ -81,6 +81,10 @@ public:
void calcDrawPriority();
void setArrowPosAxis(f32, f32);
+ #if TARGET_PC
+ void fMapBackWide();
+ #endif
+
virtual void draw();
virtual ~dMenu_Fmap2DBack_c();
@@ -330,6 +334,10 @@ public:
void setHIO(bool);
bool isWarpAccept();
+ #if TARGET_PC
+ void fMapTopWide();
+ #endif
+
virtual void draw();
virtual ~dMenu_Fmap2DTop_c();
diff --git a/include/d/d_msg_object.h b/include/d/d_msg_object.h
index b55ea73904..d2ac4ecb2a 100644
--- a/include/d/d_msg_object.h
+++ b/include/d/d_msg_object.h
@@ -67,6 +67,9 @@ public:
bool isStaffMessage();
bool isSaveMessage();
bool isTalkMessage();
+#if TARGET_PC
+ bool isShopItemMessage();
+#endif
const char* getSmellName();
const char* getPortalName();
const char* getBombName();
diff --git a/include/dusk/achievements.h b/include/dusk/achievements.h
index cd4294b6f1..ca4676ab2d 100644
--- a/include/dusk/achievements.h
+++ b/include/dusk/achievements.h
@@ -4,6 +4,8 @@
#include
#include
#include
+#include
+#include
#include
#include "nlohmann/json.hpp"
@@ -14,6 +16,7 @@ enum class AchievementCategory : uint8_t {
Collection,
Challenge,
Minigame,
+ Misc,
Glitched
};
@@ -40,6 +43,11 @@ public:
void save();
void tick();
void clearAll();
+ void clearOne(const char* key);
+
+ // Signals are visible to all achievement checks within the same tick, then cleared.
+ void signal(const char* key);
+ bool hasSignal(const char* key) const;
std::vector getAchievements() const;
bool hasPendingUnlock() const { return !m_pendingUnlocks.empty(); }
@@ -57,6 +65,7 @@ private:
void processEntry(Entry& e);
std::vector m_entries;
+ std::unordered_set m_signals;
bool m_loaded = false;
bool m_dirty = false;
std::queue m_pendingUnlocks;
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/discord_presence.hpp b/include/dusk/discord_presence.hpp
index 899b2ad5f9..ebecc2d07b 100644
--- a/include/dusk/discord_presence.hpp
+++ b/include/dusk/discord_presence.hpp
@@ -1,18 +1,14 @@
#pragma once
-#ifdef DUSK_DISCORD_RPC
+#ifdef DUSK_DISCORD
-namespace dusk {
-namespace discord {
+namespace dusk::discord {
-void Initialize();
+void initialize();
+void run_callbacks();
+void update_presence();
+void shutdown();
-void RunCallbacks();
+} // namespace dusk::discord
-void UpdatePresence();
-
-void Shutdown();
-}
-}
-
-#endif // DUSK_DISCORD_RPC
+#endif // DUSK_DISCORD
diff --git a/include/dusk/livesplit.h b/include/dusk/livesplit.h
new file mode 100644
index 0000000000..b283a29af4
--- /dev/null
+++ b/include/dusk/livesplit.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include
+
+namespace dusk::speedrun {
+void onGameFrame();
+uint64_t getFrameCount();
+void start();
+void reset();
+void connectLiveSplit(const char* host = "127.0.0.1", int port = 16834);
+void disconnectLiveSplit();
+bool consumeConnectedEvent();
+bool consumeDisconnectedEvent();
+void updateLiveSplit();
+void shutdown();
+}
diff --git a/include/dusk/settings.h b/include/dusk/settings.h
index a7815569d0..7f7aa8cd69 100644
--- a/include/dusk/settings.h
+++ b/include/dusk/settings.h
@@ -55,6 +55,8 @@ struct UserSettings {
ConfigVar soundEffectsVolume;
ConfigVar fanfareVolume;
ConfigVar enableReverb;
+ ConfigVar enableHrtf;
+ ConfigVar menuSounds;
} audio;
// Game settings
@@ -76,9 +78,11 @@ struct UserSettings {
ConfigVar fastClimbing;
ConfigVar noMissClimbing;
ConfigVar fastTears;
+ ConfigVar no2ndFishForCat;
ConfigVar instantSaves;
ConfigVar instantText;
ConfigVar sunsSong;
+ ConfigVar autoSave;
// Preferences
ConfigVar enableMirrorMode;
@@ -96,6 +100,7 @@ struct UserSettings {
ConfigVar internalResolutionScale;
ConfigVar shadowResolutionMultiplier;
ConfigVar enableDepthOfField;
+ ConfigVar enableMapBackground;
// Audio
ConfigVar noLowHpSound;
@@ -115,6 +120,7 @@ struct UserSettings {
ConfigVar invertCameraXAxis;
ConfigVar invertCameraYAxis;
ConfigVar freeCameraSensitivity;
+ ConfigVar debugFlyCam;
// Cheats
ConfigVar infiniteHearts;
@@ -137,6 +143,10 @@ struct UserSettings {
// Controls
ConfigVar enableTurboKeybind;
+
+ // Tools
+ ConfigVar speedrunMode;
+ ConfigVar liveSplitEnabled;
} game;
struct {
@@ -146,7 +156,6 @@ struct UserSettings {
ConfigVar showPipelineCompilation;
ConfigVar wasPresetChosen;
ConfigVar enableCrashReporting;
- ConfigVar duskMenuOpen;
ConfigVar cardFileType;
} backend;
};
diff --git a/include/m_Do/m_Do_audio.h b/include/m_Do/m_Do_audio.h
index 1d58e8d8f2..4bc2f20c94 100644
--- a/include/m_Do/m_Do_audio.h
+++ b/include/m_Do/m_Do_audio.h
@@ -5,6 +5,7 @@
#include "Z2AudioLib/Z2EnvSeMgr.h"
#include "Z2AudioLib/Z2LinkMgr.h"
#include "dusk/audio.h"
+#include "dusk/settings.h"
class mDoAud_zelAudio_c : public Z2AudioMgr {
public:
@@ -132,6 +133,18 @@ inline void mDoAud_seStart(u32 i_sfxID, const Vec* i_sePos, u32 param_2, s8 i_re
-1.0f, -1.0f, 0);
}
+#if TARGET_PC
+inline void mDoAud_seStartMenu(u32 i_sfxID) {
+ if (!mDoAud_zelAudio_c::isInitFlag()) {
+ return;
+ }
+ if (!dusk::getSettings().audio.menuSounds.getValue()) {
+ return;
+ }
+ mDoAud_seStart(i_sfxID, nullptr, 0, 0);
+}
+#endif
+
inline void mDoAud_seStartLevel(u32 i_sfxID, const Vec* i_sePos, u32 param_2, s8 i_reverb) {
DUSK_AUDIO_SKIP()
Z2AudioMgr::getInterface()->seStartLevel(i_sfxID, i_sePos, param_2, i_reverb, 1.0f, 1.0f,
diff --git a/libs/JSystem/include/JSystem/JAudio2/JAISeqMgr.h b/libs/JSystem/include/JSystem/JAudio2/JAISeqMgr.h
index 87c85f316f..6f6d29ca98 100644
--- a/libs/JSystem/include/JSystem/JAudio2/JAISeqMgr.h
+++ b/libs/JSystem/include/JSystem/JAudio2/JAISeqMgr.h
@@ -59,6 +59,9 @@ public:
bool isActive() const { return mSeqList.getNumLinks() != 0; }
int getNumActiveSeqs() const { return mSeqList.getNumLinks(); }
void pause(bool paused) { mActivity.field_0x0.flags.flag2 = paused; }
+ #if TARGET_PC
+ JSUList* getSeqList() { return &mSeqList; }
+ #endif
private:
/* 0x08 */ JAIAudience* mAudience;
diff --git a/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h b/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h
index d348859ebe..795a5a2628 100644
--- a/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h
+++ b/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h
@@ -207,4 +207,11 @@ void JPARegistAlphaEnv(JPAEmitterWorkData*, JPABaseParticle*);
void JPARegistPrmAlpha(JPAEmitterWorkData*, JPABaseParticle*);
void JPARegistPrmAlphaEnv(JPAEmitterWorkData*, JPABaseParticle*);
+#if TARGET_PC
+void JPAInterpBillboard(JPAEmitterWorkData*, JPABaseParticle*);
+void JPAInterpRotBillboard(JPAEmitterWorkData*, JPABaseParticle*);
+void JPAInterpDirection(JPAEmitterWorkData*, JPABaseParticle*);
+void JPAInterpRotDirection(JPAEmitterWorkData*, JPABaseParticle*);
+#endif
+
#endif /* JPABASESHAPE_H */
diff --git a/libs/JSystem/include/JSystem/JParticle/JPAParticle.h b/libs/JSystem/include/JSystem/JParticle/JPAParticle.h
index 842f293286..c864a5cd4a 100644
--- a/libs/JSystem/include/JSystem/JParticle/JPAParticle.h
+++ b/libs/JSystem/include/JSystem/JParticle/JPAParticle.h
@@ -24,6 +24,9 @@ public:
void init_c(JPAEmitterWorkData*, JPABaseParticle*);
bool calc_p(JPAEmitterWorkData*);
bool calc_c(JPAEmitterWorkData*);
+#if TARGET_PC
+ void interp(JPAEmitterWorkData*, void const* drawFunc);
+#endif
bool canCreateChild(JPAEmitterWorkData*);
f32 getWidth(JPABaseEmitter const*) const;
f32 getHeight(JPABaseEmitter const*) const;
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/JAudio2/JASChannel.cpp b/libs/JSystem/src/JAudio2/JASChannel.cpp
index 61fe80a098..758167cc5c 100644
--- a/libs/JSystem/src/JAudio2/JASChannel.cpp
+++ b/libs/JSystem/src/JAudio2/JASChannel.cpp
@@ -1,6 +1,9 @@
#include "JSystem/JSystem.h" // IWYU pragma: keep
#include "JSystem/JAudio2/JASChannel.h"
+#if TARGET_PC
+#include "dusk/audio/DuskDsp.hpp"
+#endif
#include "JSystem/JAudio2/JASAiCtrl.h"
#include "JSystem/JAudio2/JASCalc.h"
#include "JSystem/JAudio2/JASDriverIF.h"
@@ -170,7 +173,12 @@ void JASChannel::updateEffectorParam(JASDsp::TChannel* i_channel, u16* i_mixerVo
f32 pan = 0.5f;
f32 dolby = 0.0f;
- switch (JASDriver::getOutputMode()) {
+#if TARGET_PC
+ u32 effectiveOutputMode = dusk::audio::EnableHrtf ? JAS_OUTPUT_SURROUND : JASDriver::getOutputMode();
+#else
+ u32 effectiveOutputMode = JASDriver::getOutputMode();
+#endif
+ switch (effectiveOutputMode) {
case JAS_OUTPUT_MONO:
break;
case JAS_OUTPUT_STEREO:
diff --git a/libs/JSystem/src/JAudio2/JASHeapCtrl.cpp b/libs/JSystem/src/JAudio2/JASHeapCtrl.cpp
index 45568cc29a..e3722e3726 100644
--- a/libs/JSystem/src/JAudio2/JASHeapCtrl.cpp
+++ b/libs/JSystem/src/JAudio2/JASHeapCtrl.cpp
@@ -302,7 +302,6 @@ void JASKernel::setupRootHeap(JKRSolidHeap* heap, u32 size) {
JKRHEAP_NAME(sSystemHeap, "JASKernel::sSystemHeap");
JUT_ASSERT(787, sSystemHeap);
sCommandHeap = JKR_NEW_ARGS (heap, 0) JASMemChunkPool<1024, JASThreadingModel::ObjectLevelLockable>;
- JKRHEAP_NAME(sSystemHeap, "JASKernel::sCommandHeap");
JUT_ASSERT(790, sCommandHeap);
JASDram = heap;
}
diff --git a/libs/JSystem/src/JAudio2/JAUSectionHeap.cpp b/libs/JSystem/src/JAudio2/JAUSectionHeap.cpp
index e1f45de1ae..cc94097712 100644
--- a/libs/JSystem/src/JAudio2/JAUSectionHeap.cpp
+++ b/libs/JSystem/src/JAudio2/JAUSectionHeap.cpp
@@ -442,6 +442,7 @@ static JAUSectionHeap* JAUNewSectionHeap(JKRSolidHeap* heap, bool param_1) {
JAUSectionHeap* JAUNewSectionHeap(bool param_0) {
s32 freeSize = JASDram->getFreeSize();
JKRSolidHeap* sectionHeap = JKRCreateSolidHeap(freeSize, JASDram, true);
+ JKRHEAP_NAME(sectionHeap, "sectionHeap");
JUT_ASSERT(821, sectionHeap);
return JAUNewSectionHeap(sectionHeap, param_0);
}
diff --git a/libs/JSystem/src/JKernel/JKRExpHeap.cpp b/libs/JSystem/src/JKernel/JKRExpHeap.cpp
index 4a6712cb65..dc88090a47 100644
--- a/libs/JSystem/src/JKernel/JKRExpHeap.cpp
+++ b/libs/JSystem/src/JKernel/JKRExpHeap.cpp
@@ -222,16 +222,11 @@ void* JKRExpHeap::do_alloc(u32 size, int alignment) {
OSReport_Error("Free block list as follows:\n");
OSReport_Error("Start | End | Size \n");
- int i = 0;
for (const CMemBlock* block = mHeadFreeList; block; block = block->mNext) {
if (block->mMagic) {
// Allocated, ignore.
continue;
}
- if (i++ > 10) {
- OSReport_Error("\n");
- break;
- }
auto blockStart = (uintptr_t)block - (uintptr_t)mStart;
auto blockEnd = (uintptr_t)block + block->size - (uintptr_t)mStart;
@@ -239,6 +234,14 @@ void* JKRExpHeap::do_alloc(u32 size, int alignment) {
OSReport_Error("%08X | %08X | %08X\n", (u32) blockStart, (u32) blockEnd, (u32) blockSize);
}
+ OSReport_Error("Child heaps as follows:\n");
+ OSReport_Error("Start | End | Name \n");
+
+ const JSUTree& tree = getHeapTree();
+ for (JSUTreeIterator iter(tree.getFirstChild()); iter != tree.getEndChild(); ++iter) {
+ OSReport_Error("%08X | %08X | %s\n", iter->getStartAddr(), iter->getEndAddr(), iter->getName());
+ }
+
CRASH("Aborting due to allocation failure!");
}
#else
diff --git a/libs/JSystem/src/JParticle/JPABaseShape.cpp b/libs/JSystem/src/JParticle/JPABaseShape.cpp
index 178606a687..ff346984cd 100644
--- a/libs/JSystem/src/JParticle/JPABaseShape.cpp
+++ b/libs/JSystem/src/JParticle/JPABaseShape.cpp
@@ -9,6 +9,9 @@
#include
#include
+#if TARGET_PC
+#include "dusk/frame_interpolation.h"
+#endif
#include "tracy/Tracy.hpp"
void JPASetPointSize(JPAEmitterWorkData* work) {
@@ -418,50 +421,95 @@ static projectionFunc p_prj[3] = {
loadPrjAnm,
};
-void JPADrawBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) {
- if (param_1->checkStatus(JPAPtclStts_Invisible)) {
+#if TARGET_PC
+void JPAInterpBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
+ Mtx ptclPosMtx;
+ MTXTrans(ptclPosMtx, ptcl->mPosition.x, ptcl->mPosition.y, ptcl->mPosition.z);
+ dusk::frame_interp::record_final_mtx(ptclPosMtx, ptcl);
+}
+
+void JPAInterpRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
+ Mtx ptclPosMtx;
+ f32 sinRot = JMASSin(ptcl->mRotateAngle);
+ f32 cosRot = JMASCos(ptcl->mRotateAngle);
+ MTXTrans(ptclPosMtx, ptcl->mPosition.x, ptcl->mPosition.y, ptcl->mPosition.z);
+ ptclPosMtx[0][0] = cosRot;
+ ptclPosMtx[0][1] = -sinRot;
+ ptclPosMtx[1][0] = sinRot;
+ ptclPosMtx[1][1] = cosRot;
+ dusk::frame_interp::record_final_mtx(ptclPosMtx, ptcl);
+}
+#endif
+
+void JPADrawBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
+ if (ptcl->checkStatus(JPAPtclStts_Invisible)) {
return;
}
- JGeometry::TVec3 local_48;
- MTXMultVec(work->mPosCamMtx, ¶m_1->mPosition, &local_48);
- Mtx local_38;
- local_38[0][0] = work->mGlobalPtclScl.x * param_1->mParticleScaleX;
- local_38[0][3] = local_48.x;
- local_38[1][1] = work->mGlobalPtclScl.y * param_1->mParticleScaleY;
- local_38[1][3] = local_48.y;
- local_38[2][2] = 1.0f;
- local_38[2][3] = local_48.z;
- local_38[0][1] = local_38[0][2] = local_38[1][0] = local_38[1][2] = local_38[2][0] = local_38[2][1] = 0.0f;
- GXLoadPosMtxImm(local_38, 0);
- p_prj[work->mPrjType](work, local_38);
+ JGeometry::TVec3 pos;
+#if TARGET_PC
+ Mtx ptclPosMtx;
+ if (dusk::frame_interp::lookup_replacement(ptcl, ptclPosMtx)) {
+ pos.set(ptclPosMtx[0][3], ptclPosMtx[1][3], ptclPosMtx[2][3]);
+ MTXMultVec(work->mPosCamMtx, &pos, &pos);
+ } else
+#endif
+ {
+ MTXMultVec(work->mPosCamMtx, &ptcl->mPosition, &pos);
+ }
+ Mtx posMtx;
+ posMtx[0][0] = work->mGlobalPtclScl.x * ptcl->mParticleScaleX;
+ posMtx[0][3] = pos.x;
+ posMtx[1][1] = work->mGlobalPtclScl.y * ptcl->mParticleScaleY;
+ posMtx[1][3] = pos.y;
+ posMtx[2][2] = 1.0f;
+ posMtx[2][3] = pos.z;
+ posMtx[0][1] = posMtx[0][2] = posMtx[1][0] = posMtx[1][2] = posMtx[2][0] = posMtx[2][1] = 0.0f;
+ GXLoadPosMtxImm(posMtx, GX_PNMTX0);
+ p_prj[work->mPrjType](work, posMtx);
GXCallDisplayList(jpa_dl, sizeof(jpa_dl));
}
-void JPADrawRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) {
- if (param_1->checkStatus(JPAPtclStts_Invisible)) {
+void JPADrawRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
+ if (ptcl->checkStatus(JPAPtclStts_Invisible)) {
return;
}
- JGeometry::TVec3 local_48;
- MTXMultVec(work->mPosCamMtx, ¶m_1->mPosition, &local_48);
- f32 sinRot = JMASSin(param_1->mRotateAngle);
- f32 cosRot = JMASCos(param_1->mRotateAngle);
- f32 particleX = work->mGlobalPtclScl.x * param_1->mParticleScaleX;
- f32 particleY = work->mGlobalPtclScl.y * param_1->mParticleScaleY;
+ if (work->mpRes->getUsrIdx() == 0x89d7) {
+ int a = 0;
+ }
- Mtx local_38;
- local_38[0][0] = cosRot * particleX;
- local_38[0][1] = -sinRot * particleY;
- local_38[0][3] = local_48.x;
- local_38[1][0] = sinRot * particleX;
- local_38[1][1] = cosRot * particleY;
- local_38[1][3] = local_48.y;
- local_38[2][2] = 1.0f;
- local_38[2][3] = local_48.z;
- local_38[0][2] = local_38[1][2] = local_38[2][0] = local_38[2][1] = 0.0f;
- GXLoadPosMtxImm(local_38, 0);
- p_prj[work->mPrjType](work, local_38);
+ JGeometry::TVec3 pos;
+ f32 sinRot, cosRot;
+#if TARGET_PC
+ Mtx ptclPosMtx;
+ MTXTrans(ptclPosMtx, ptcl->mPosition.x, ptcl->mPosition.y, ptcl->mPosition.z);
+ if (dusk::frame_interp::lookup_replacement(ptcl, ptclPosMtx)) {
+ pos.set(ptclPosMtx[0][3], ptclPosMtx[1][3], ptclPosMtx[2][3]);
+ sinRot = ptclPosMtx[1][0];
+ cosRot = ptclPosMtx[0][0];
+ MTXMultVec(work->mPosCamMtx, &pos, &pos);
+ } else
+#endif
+ {
+ MTXMultVec(work->mPosCamMtx, &ptcl->mPosition, &pos);
+ sinRot = JMASSin(ptcl->mRotateAngle);
+ cosRot = JMASCos(ptcl->mRotateAngle);
+ }
+ f32 particleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX;
+ f32 particleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY;
+ Mtx posMtx;
+ posMtx[0][0] = cosRot * particleX;
+ posMtx[0][1] = -sinRot * particleY;
+ posMtx[0][3] = pos.x;
+ posMtx[1][0] = sinRot * particleX;
+ posMtx[1][1] = cosRot * particleY;
+ posMtx[1][3] = pos.y;
+ posMtx[2][2] = 1.0f;
+ posMtx[2][3] = pos.z;
+ posMtx[0][2] = posMtx[1][2] = posMtx[2][0] = posMtx[2][1] = 0.0f;
+ GXLoadPosMtxImm(posMtx, GX_PNMTX0);
+ p_prj[work->mPrjType](work, posMtx);
GXCallDisplayList(jpa_dl, sizeof(jpa_dl));
}
@@ -484,7 +532,7 @@ void JPADrawYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) {
local_38[2][2] = work->mYBBCamMtx[2][2];
local_38[2][3] = local_48.z;
local_38[0][1] = local_38[0][2] = local_38[1][0] = local_38[2][0] = 0.0f;
- GXLoadPosMtxImm(local_38, 0);
+ GXLoadPosMtxImm(local_38, GX_PNMTX0);
p_prj[work->mPrjType](work, local_38);
GXCallDisplayList(jpa_dl, sizeof(jpa_dl));
}
@@ -517,7 +565,7 @@ void JPADrawRotYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) {
local_38[2][1] = local_94 * fVar1;
local_38[2][2] = local_90;
local_38[2][3] = local_48.z;
- GXLoadPosMtxImm(local_38, 0);
+ GXLoadPosMtxImm(local_38, GX_PNMTX0);
p_prj[work->mPrjType](work, local_38);
GXCallDisplayList(jpa_dl, sizeof(jpa_dl));
}
@@ -681,103 +729,197 @@ static u8* p_dl[2] = {
jpa_dl_x,
};
-void JPADrawDirection(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) {
- if (param_1->checkStatus(JPAPtclStts_Invisible)) {
+#if TARGET_PC
+void JPAInterpDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
+ JGeometry::TVec3 axisY;
+ JGeometry::TVec3 axisZ;
+ p_direction[work->mDirType](work, ptcl, &axisY);
+
+ if (axisY.isZero()) {
return;
}
- ZoneScoped;
+ axisY.normalize();
+ axisZ.cross(ptcl->mBaseAxis, axisY);
- JGeometry::TVec3 local_6c;
- JGeometry::TVec3 local_78;
- p_direction[param_0->mDirType](param_0, param_1, &local_6c);
-
- if (local_6c.isZero()) {
+ if (axisZ.isZero()) {
return;
}
- local_6c.normalize();
- local_78.cross(param_1->mBaseAxis, local_6c);
-
- if (local_78.isZero()) {
- return;
- }
-
- local_78.normalize();
- param_1->mBaseAxis.cross(local_6c, local_78);
- param_1->mBaseAxis.normalize();
- Mtx local_60;
- f32 fVar1 = param_0->mGlobalPtclScl.x * param_1->mParticleScaleX;
- f32 fVar2 = param_0->mGlobalPtclScl.y * param_1->mParticleScaleY;
- local_60[0][0] = param_1->mBaseAxis.x;
- local_60[0][1] = local_6c.x;
- local_60[0][2] = local_78.x;
- local_60[0][3] = param_1->mPosition.x;
- local_60[1][0] = param_1->mBaseAxis.y;
- local_60[1][1] = local_6c.y;
- local_60[1][2] = local_78.y;
- local_60[1][3] = param_1->mPosition.y;
- local_60[2][0] = param_1->mBaseAxis.z;
- local_60[2][1] = local_6c.z;
- local_60[2][2] = local_78.z;
- local_60[2][3] = param_1->mPosition.z;
- p_plane[param_0->mPlaneType](local_60, fVar1, fVar2);
- MTXConcat(param_0->mPosCamMtx, local_60, local_60);
- GXLoadPosMtxImm(local_60, 0);
- p_prj[param_0->mPrjType](param_0, local_60);
- GXCallDisplayList(p_dl[param_0->mDLType], sizeof(jpa_dl));
+ axisZ.normalize();
+ ptcl->mBaseAxis.cross(axisY, axisZ);
+ ptcl->mBaseAxis.normalize();
+ Mtx posMtx;
+ f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX;
+ f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY;
+ posMtx[0][0] = ptcl->mBaseAxis.x;
+ posMtx[0][1] = axisY.x;
+ posMtx[0][2] = axisZ.x;
+ posMtx[0][3] = ptcl->mPosition.x;
+ posMtx[1][0] = ptcl->mBaseAxis.y;
+ posMtx[1][1] = axisY.y;
+ posMtx[1][2] = axisZ.y;
+ posMtx[1][3] = ptcl->mPosition.y;
+ posMtx[2][0] = ptcl->mBaseAxis.z;
+ posMtx[2][1] = axisY.z;
+ posMtx[2][2] = axisZ.z;
+ posMtx[2][3] = ptcl->mPosition.z;
+ p_plane[work->mPlaneType](posMtx, scaleX, scaleY);
+ dusk::frame_interp::record_final_mtx(posMtx, ptcl);
}
-void JPADrawRotDirection(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) {
- if (param_1->checkStatus(JPAPtclStts_Invisible)) {
+void JPAInterpRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
+ f32 sinRot = JMASSin(ptcl->mRotateAngle);
+ f32 cosRot = JMASCos(ptcl->mRotateAngle);
+ JGeometry::TVec3 axisY;
+ JGeometry::TVec3 axisZ;
+ p_direction[work->mDirType](work, ptcl, &axisY);
+
+ if (axisY.isZero()) {
+ return;
+ }
+
+ axisY.normalize();
+ axisZ.cross(ptcl->mBaseAxis, axisY);
+
+ if (axisZ.isZero()) {
+ return;
+ }
+
+ axisZ.normalize();
+ ptcl->mBaseAxis.cross(axisY, axisZ);
+ ptcl->mBaseAxis.normalize();
+ f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX;
+ f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY;
+ Mtx mtx1;
+ Mtx mtx2;
+ p_rot[work->mRotType](sinRot, cosRot, mtx1);
+ p_plane[work->mPlaneType](mtx1, scaleX, scaleY);
+ mtx2[0][0] = ptcl->mBaseAxis.x;
+ mtx2[0][1] = axisY.x;
+ mtx2[0][2] = axisZ.x;
+ mtx2[0][3] = ptcl->mPosition.x;
+ mtx2[1][0] = ptcl->mBaseAxis.y;
+ mtx2[1][1] = axisY.y;
+ mtx2[1][2] = axisZ.y;
+ mtx2[1][3] = ptcl->mPosition.y;
+ mtx2[2][0] = ptcl->mBaseAxis.z;
+ mtx2[2][1] = axisY.z;
+ mtx2[2][2] = axisZ.z;
+ mtx2[2][3] = ptcl->mPosition.z;
+ MTXConcat(mtx2, mtx1, mtx1);
+ dusk::frame_interp::record_final_mtx(mtx1, ptcl);
+}
+#endif
+
+void JPADrawDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
+ if (ptcl->checkStatus(JPAPtclStts_Invisible)) {
return;
}
ZoneScoped;
- f32 sinRot = JMASSin(param_1->mRotateAngle);
- f32 cosRot = JMASCos(param_1->mRotateAngle);
- JGeometry::TVec3 local_6c;
- JGeometry::TVec3 local_78;
- p_direction[param_0->mDirType](param_0, param_1, &local_6c);
+ Mtx posMtx;
+#if TARGET_PC
+ if (!dusk::frame_interp::lookup_replacement(ptcl, posMtx))
+#endif
+ {
+ JGeometry::TVec3 axisY;
+ JGeometry::TVec3 axisZ;
+ p_direction[work->mDirType](work, ptcl, &axisY);
- if (local_6c.isZero()) {
+ if (axisY.isZero()) {
+ return;
+ }
+
+ axisY.normalize();
+ axisZ.cross(ptcl->mBaseAxis, axisY);
+
+ if (axisZ.isZero()) {
+ return;
+ }
+
+ axisZ.normalize();
+ ptcl->mBaseAxis.cross(axisY, axisZ);
+ ptcl->mBaseAxis.normalize();
+ f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX;
+ f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY;
+ posMtx[0][0] = ptcl->mBaseAxis.x;
+ posMtx[0][1] = axisY.x;
+ posMtx[0][2] = axisZ.x;
+ posMtx[0][3] = ptcl->mPosition.x;
+ posMtx[1][0] = ptcl->mBaseAxis.y;
+ posMtx[1][1] = axisY.y;
+ posMtx[1][2] = axisZ.y;
+ posMtx[1][3] = ptcl->mPosition.y;
+ posMtx[2][0] = ptcl->mBaseAxis.z;
+ posMtx[2][1] = axisY.z;
+ posMtx[2][2] = axisZ.z;
+ posMtx[2][3] = ptcl->mPosition.z;
+ p_plane[work->mPlaneType](posMtx, scaleX, scaleY);
+ }
+
+ MTXConcat(work->mPosCamMtx, posMtx, posMtx);
+ GXLoadPosMtxImm(posMtx, GX_PNMTX0);
+ p_prj[work->mPrjType](work, posMtx);
+ GXCallDisplayList(p_dl[work->mDLType], sizeof(jpa_dl));
+}
+
+void JPADrawRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) {
+ if (ptcl->checkStatus(JPAPtclStts_Invisible)) {
return;
}
- local_6c.normalize();
- local_78.cross(param_1->mBaseAxis, local_6c);
+ ZoneScoped;
- if (local_78.isZero()) {
- return;
+ Mtx mtx1;
+ Mtx mtx2;
+#if TARGET_PC
+ if (!dusk::frame_interp::lookup_replacement(ptcl, mtx1))
+#endif
+ {
+ f32 sinRot = JMASSin(ptcl->mRotateAngle);
+ f32 cosRot = JMASCos(ptcl->mRotateAngle);
+ JGeometry::TVec3 axisY;
+ JGeometry::TVec3 axisZ;
+ p_direction[work->mDirType](work, ptcl, &axisY);
+
+ if (axisY.isZero()) {
+ return;
+ }
+
+ axisY.normalize();
+ axisZ.cross(ptcl->mBaseAxis, axisY);
+
+ if (axisZ.isZero()) {
+ return;
+ }
+
+ axisZ.normalize();
+ ptcl->mBaseAxis.cross(axisY, axisZ);
+ ptcl->mBaseAxis.normalize();
+ f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX;
+ f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY;
+ p_rot[work->mRotType](sinRot, cosRot, mtx1);
+ p_plane[work->mPlaneType](mtx1, scaleX, scaleY);
+ mtx2[0][0] = ptcl->mBaseAxis.x;
+ mtx2[0][1] = axisY.x;
+ mtx2[0][2] = axisZ.x;
+ mtx2[0][3] = ptcl->mPosition.x;
+ mtx2[1][0] = ptcl->mBaseAxis.y;
+ mtx2[1][1] = axisY.y;
+ mtx2[1][2] = axisZ.y;
+ mtx2[1][3] = ptcl->mPosition.y;
+ mtx2[2][0] = ptcl->mBaseAxis.z;
+ mtx2[2][1] = axisY.z;
+ mtx2[2][2] = axisZ.z;
+ mtx2[2][3] = ptcl->mPosition.z;
+ MTXConcat(mtx2, mtx1, mtx1);
}
-
- local_78.normalize();
- param_1->mBaseAxis.cross(local_6c, local_78);
- param_1->mBaseAxis.normalize();
- f32 particleX = param_0->mGlobalPtclScl.x * param_1->mParticleScaleX;
- f32 particleY = param_0->mGlobalPtclScl.y * param_1->mParticleScaleY;
- Mtx auStack_80;
- Mtx local_60;
- p_rot[param_0->mRotType](sinRot, cosRot, auStack_80);
- p_plane[param_0->mPlaneType](auStack_80, particleX, particleY);
- local_60[0][0] = param_1->mBaseAxis.x;
- local_60[0][1] = local_6c.x;
- local_60[0][2] = local_78.x;
- local_60[0][3] = param_1->mPosition.x;
- local_60[1][0] = param_1->mBaseAxis.y;
- local_60[1][1] = local_6c.y;
- local_60[1][2] = local_78.y;
- local_60[1][3] = param_1->mPosition.y;
- local_60[2][0] = param_1->mBaseAxis.z;
- local_60[2][1] = local_6c.z;
- local_60[2][2] = local_78.z;
- local_60[2][3] = param_1->mPosition.z;
- MTXConcat(local_60, auStack_80, auStack_80);
- MTXConcat(param_0->mPosCamMtx, auStack_80, local_60);
- GXLoadPosMtxImm(local_60, 0);
- p_prj[param_0->mPrjType](param_0, local_60);
- GXCallDisplayList(p_dl[param_0->mDLType], sizeof(jpa_dl));
+ MTXConcat(work->mPosCamMtx, mtx1, mtx2);
+ GXLoadPosMtxImm(mtx2, GX_PNMTX0);
+ p_prj[work->mPrjType](work, mtx2);
+ GXCallDisplayList(p_dl[work->mDLType], sizeof(jpa_dl));
}
void JPADrawDBillboard(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) {
diff --git a/libs/JSystem/src/JParticle/JPAParticle.cpp b/libs/JSystem/src/JParticle/JPAParticle.cpp
index a9294e0b4e..d3490ab294 100644
--- a/libs/JSystem/src/JParticle/JPAParticle.cpp
+++ b/libs/JSystem/src/JParticle/JPAParticle.cpp
@@ -204,6 +204,28 @@ void JPABaseParticle::init_c(JPAEmitterWorkData* work, JPABaseParticle* parent)
mTexAnmIdx = 0;
}
+#if TARGET_PC
+void JPABaseParticle::interp(JPAEmitterWorkData* work, void const* drawFunc) {
+ static bool enable = false;
+ if (!enable)
+ return;
+
+ // don't interpolate the first frame
+ if (mAge == 0)
+ return;
+
+ if (drawFunc == JPADrawBillboard) {
+ JPAInterpBillboard(work, this);
+ } else if (drawFunc == JPADrawRotBillboard) {
+ JPAInterpRotBillboard(work, this);
+ } else if (drawFunc == JPADrawDirection) {
+ JPAInterpDirection(work, this);
+ } else if (drawFunc == JPADrawRotDirection) {
+ JPAInterpRotDirection(work, this);
+ }
+}
+#endif
+
bool JPABaseParticle::calc_p(JPAEmitterWorkData* work) {
if (++mAge >= mLifeTime) {
return true;
@@ -247,6 +269,17 @@ bool JPABaseParticle::calc_p(JPAEmitterWorkData* work) {
mOffsetPosition.y + mLocalPosition.y * work->mPublicScale.y,
mOffsetPosition.z + mLocalPosition.z * work->mPublicScale.z);
+#if TARGET_PC
+ JPABaseShape* pBsp = work->mpRes->getBsp();
+ work->mGlobalPtclScl.x = work->mpEmtr->mGlobalPScl.x * pBsp->getBaseSizeX();
+ work->mGlobalPtclScl.y = work->mpEmtr->mGlobalPScl.y * pBsp->getBaseSizeY();
+ work->mDirType = pBsp->getDirType();
+ work->mRotType = pBsp->getRotType();
+ work->mDLType = pBsp->getType() == 4 || pBsp->getType() == 8;
+ work->mPlaneType = work->mDLType ? 2 : pBsp->getBasePlaneType();
+ interp(work, (void const*)work->mpRes->mpDrawParticleFuncList[0]);
+#endif
+
return false;
}
@@ -289,6 +322,23 @@ bool JPABaseParticle::calc_c(JPAEmitterWorkData* work) {
mOffsetPosition.y + mLocalPosition.y * work->mPublicScale.y,
mOffsetPosition.z + mLocalPosition.z * work->mPublicScale.z);
+#if TARGET_PC
+ JPABaseShape* pBsp = work->mpRes->getBsp();
+ JPAChildShape* pCsp = work->mpRes->getCsp();
+ if (pCsp->isScaleInherited()) {
+ work->mGlobalPtclScl.x = work->mpEmtr->mGlobalPScl.x * pBsp->getBaseSizeX();
+ work->mGlobalPtclScl.y = work->mpEmtr->mGlobalPScl.y * pBsp->getBaseSizeY();
+ } else {
+ work->mGlobalPtclScl.x = work->mpEmtr->mGlobalPScl.x * pCsp->getScaleX();
+ work->mGlobalPtclScl.y = work->mpEmtr->mGlobalPScl.y * pCsp->getScaleY();
+ }
+ work->mDirType = pCsp->getDirType();
+ work->mRotType = pCsp->getRotType();
+ work->mDLType = pCsp->getType() == 4 || pCsp->getType() == 8;
+ work->mPlaneType = work->mDLType ? 2 : pCsp->getBasePlaneType();
+ interp(work, (void const*)work->mpRes->mpDrawParticleChildFuncList[0]);
+#endif
+
return false;
}
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-Bold.ttf b/res/FiraSans-Bold.ttf
new file mode 100644
index 0000000000..e3593fb0f3
Binary files /dev/null and b/res/FiraSans-Bold.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/MaterialSymbolsRounded-Regular.ttf b/res/MaterialSymbolsRounded-Regular.ttf
new file mode 100644
index 0000000000..782fc0a406
Binary files /dev/null and b/res/MaterialSymbolsRounded-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..e18bb436e5
--- /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);
+ 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: 48dp 64dp;
+}
+
+@media (max-height: 800dp) {
+ .overlay-root {
+ min-height: 38%;
+ }
+
+ .overlay {
+ gap: 16dp;
+ padding: 32dp 48dp;
+ }
+}
+
+.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;
+ font-family: "Material Symbols Rounded";
+ font-weight: normal;
+}
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..9556e74ef2
--- /dev/null
+++ b/res/rml/prelaunch.rcss
@@ -0,0 +1,212 @@
+*, *: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.2s linear-in-out;
+ z-index: -1;
+}
+
+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);
+}
+
+disc-info {
+ position: absolute;
+ left: 96dp;
+ bottom: 72dp;
+ display: flex;
+ flex-direction: column;
+ gap: 12dp;
+ font-size: 24dp;
+}
+
+version-info {
+ position: absolute;
+ right: 96dp;
+ bottom: 72dp;
+ display: flex;
+ flex-direction: column;
+ gap: 12dp;
+ text-align: right;
+ font-size: 24dp;
+}
+
+#disc-status {
+ display: flex;
+ align-items: center;
+ gap: 8dp;
+}
+
+#disc-status[status=good] {
+ color: #D8F999;
+}
+
+#disc-status[status=bad] {
+ color: #FFC9C9;
+}
+
+#disc-status icon {
+ display: block;
+ width: 24dp;
+ height: 24dp;
+ font-family: "Material Symbols Rounded";
+ font-weight: normal;
+ font-size: 24dp;
+}
+
+#disc-status[status=good] icon {
+ decorator: text("" center center);
+}
+
+#disc-status[status=bad] icon {
+ decorator: text("" center center);
+}
+
+#disc-version {
+ font-size: 20dp;
+}
+
+/* TODO: Hidden until an actual update checker is introduced */
+.update {
+ display: none;
+ font-size: 16dp;
+ font-weight: bold;
+ cursor: pointer;
+ color: #D8F999;
+}
+
+.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..194ca89f05
--- /dev/null
+++ b/res/rml/tabbing.rcss
@@ -0,0 +1,81 @@
+tab-bar {
+ display: flex;
+ position: relative;
+ min-width: 0;
+ overflow: auto hidden;
+ clip: always;
+ text-transform: uppercase;
+}
+
+tab-bar scrollbarhorizontal,
+tab-bar scrollbarhorizontal sliderarrowdec,
+tab-bar scrollbarhorizontal sliderarrowinc,
+tab-bar scrollbarhorizontal slidertrack,
+tab-bar scrollbarhorizontal sliderbar {
+ width: 0;
+ height: 0;
+}
+
+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);
+}
+
+tab-bar[closable] tab-end-spacer {
+ display: block;
+ flex: 0 0 64dp;
+ width: 64dp;
+ pointer-events: none;
+}
+
+tab-bar[closable] close {
+ display: block;
+ position: fixed;
+ top: 8dp;
+ right: 8dp;
+ z-index: 1;
+ width: 48dp;
+ height: 48dp;
+ font-family: "Material Symbols Rounded";
+ font-weight: normal;
+ font-size: 24dp;
+ color: rgba(224, 219, 200, 70%);
+ backdrop-filter: blur(2dp);
+ border-radius: 6dp;
+ decorator: text("" center center);
+ transition: color background-color 0.12s linear-in-out;
+ cursor: pointer;
+}
+
+tab-bar[closable] close:hover,
+tab-bar[closable] close:focus-visible {
+ color: #fff;
+ background-color: rgba(194, 164, 45, 24%);
+}
+
+tab-bar[closable] close:active {
+ color: #fff;
+ background-color: rgba(194, 164, 45, 40%);
+}
diff --git a/res/rml/window.rcss b/res/rml/window.rcss
new file mode 100644
index 0000000000..db3558778a
--- /dev/null
+++ b/res/rml/window.rcss
@@ -0,0 +1,401 @@
+*, *:before, *:after {
+ box-sizing: border-box;
+}
+
+body {
+ display: flex;
+ 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%;
+ width: 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.small {
+ height: auto;
+ width: auto;
+}
+
+window.preset {
+ min-width: 650dp;
+}
+
+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 auto;
+ min-width: 0;
+ min-height: 0;
+ overflow: hidden;
+}
+
+window content pane {
+ display: flex;
+ flex-flow: column;
+ flex: 1 1 0;
+ 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: 0 1 auto;
+}
+
+select-button value {
+ flex: 1 1 auto;
+ text-align: right;
+ font-size: 20dp;
+}
+
+select-button value.modified {
+ font-weight: bold;
+}
+
+select-button input {
+ text-align: right;
+ font-size: 20dp;
+}
+
+icon {
+ font-family: "Material Symbols Rounded";
+ font-weight: normal;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+icon.warning {
+ width: 1em;
+ height: 1em;
+ decorator: text("" center center);
+ color: #ffcc00;
+}
+
+.achievement-row {
+ display: flex;
+ align-items: flex-start;
+ gap: 10dp;
+ padding: 12dp 0;
+ border-bottom: 1dp rgba(146, 135, 91, 30%);
+}
+
+.achievement-info {
+ display: block;
+ flex: 1 1 0;
+ min-width: 0;
+}
+
+.achievement-header {
+ display: flex;
+ align-items: center;
+}
+
+.achievement-name {
+ flex: 1;
+ font-weight: bold;
+}
+
+.achievement-name.unlocked {
+ color: #ffa826;
+}
+
+.achievement-badge {
+ font-size: 14dp;
+ opacity: 0.7;
+}
+
+.achievement-badge.unlocked {
+ color: #44cc55;
+ opacity: 1;
+}
+
+.achievement-badge.locked {
+ color: #cc4444;
+ opacity: 1;
+}
+
+.achievement-desc {
+ display: block;
+ color: rgba(224, 219, 200, 55%);
+ font-size: 16dp;
+ margin: 4dp 0 0 0;
+}
+
+.achievement-progress {
+ display: block;
+ font-size: 13dp;
+ color: rgba(224, 219, 200, 45%);
+}
+
+progressbar {
+ display: block;
+ width: 100%;
+ height: 6dp;
+ border-radius: 3dp;
+ background-color: rgba(255, 255, 255, 10%);
+ margin: 6dp 0 2dp 0;
+}
+
+progressbar.progress-done fill {
+ background-color: #44aa22;
+ border-radius: 3dp;
+}
+
+progressbar.progress-ongoing fill {
+ background-color: #2255bb;
+ border-radius: 3dp;
+}
+
+button.achievement-clear {
+ flex: 0 0 auto;
+ align-self: center;
+ font-size: 14dp;
+ padding: 2dp 8dp;
+ 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;
+}
+
+.preset-col {
+ display: flex;
+ flex-flow: column;
+ gap: 12dp;
+ 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;
+}
diff --git a/src/SSystem/SComponent/c_lib.cpp b/src/SSystem/SComponent/c_lib.cpp
index 6441d6ceb8..d73dfb8627 100644
--- a/src/SSystem/SComponent/c_lib.cpp
+++ b/src/SSystem/SComponent/c_lib.cpp
@@ -468,9 +468,20 @@ s16 cLib_targetAngleX(cXyz const* lhs, cXyz const* rhs) {
void cLib_offsetPos(cXyz* pdest, cXyz const* psrc, s16 angle, cXyz const* vec) {
f32 cos = cM_scos(angle);
f32 sin = cM_ssin(angle);
+ // MWCC loads vec members into registers before writing to pdest; other compilers may not,
+ // which corrupts results when pdest and vec alias the same memory.
+#if !__MWERKS__
+ f32 vx = vec->x;
+ f32 vy = vec->y;
+ f32 vz = vec->z;
+ pdest->x = psrc->x + (vx * cos + vz * sin);
+ pdest->y = psrc->y + vy;
+ pdest->z = psrc->z + (vz * cos - vx * sin);
+#else
pdest->x = psrc->x + (vec->x * cos + vec->z * sin);
pdest->y = psrc->y + vec->y;
pdest->z = psrc->z + (vec->z * cos - vec->x * sin);
+#endif
}
/**
diff --git a/src/Z2AudioLib/Z2Audience.cpp b/src/Z2AudioLib/Z2Audience.cpp
index 5bb91eef31..dda1a4b2d0 100644
--- a/src/Z2AudioLib/Z2Audience.cpp
+++ b/src/Z2AudioLib/Z2Audience.cpp
@@ -1,5 +1,10 @@
#include "Z2AudioLib/Z2Audience.h"
#include "Z2AudioLib/Z2SoundInfo.h"
+#if TARGET_PC
+#include "dusk/audio/DuskDsp.hpp"
+#include "dusk/settings.h"
+#include
+#endif
#include "Z2AudioLib/Z2Calc.h"
#include "Z2AudioLib/Z2Param.h"
#include "JSystem/JAudio2/JAISound.h"
@@ -701,6 +706,11 @@ f32 Z2Audience::calcRelPosVolume(const Vec& param_0, f32 param_1, int camID) {
f32 Z2Audience::calcRelPosPan(const Vec& param_0, int camID) {
Vec local_54 = param_0;
local_54.y = 0.0f;
+#if TARGET_PC
+ if (dusk::getSettings().game.enableMirrorMode) {
+ local_54.x = -local_54.x;
+ }
+#endif
f32 dVar6 = VECMag(&local_54);
if (dVar6 < 0.1f) {
@@ -734,9 +744,22 @@ f32 Z2Audience::calcRelPosPan(const Vec& param_0, int camID) {
f32 Z2Audience::calcRelPosDolby(const Vec& param_0, int camID) {
f32 fVar1 = param_0.z + mAudioCamera[camID].getDolbyCenterZ();
+#if TARGET_PC
+ if (dusk::audio::EnableHrtf) {
+ // Normalize the direction so result is purely front/back orientation,
+ // independent of how far away the sound is
+ f32 lenSq = param_0.x * param_0.x + param_0.y * param_0.y + param_0.z * param_0.z;
+ if (lenSq < 0.0001f) {
+ return 0.5f;
+ }
+ f32 zNorm = param_0.z / sqrtf(lenSq);
+ f32 t = (zNorm + 1.0f) * 0.5f;
+ return 0.5f - 0.5f * cosf(t * static_cast(M_PI));
+ }
+#endif
if (fVar1 > mSetting.field_0x48) {
return 1.0f;
- }
+ }
if (fVar1 < mSetting.field_0x44) {
return 0.0f;
diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp
index d84e25abd3..31cd2a8062 100644
--- a/src/d/actor/d_a_alink.cpp
+++ b/src/d/actor/d_a_alink.cpp
@@ -4962,13 +4962,16 @@ int daAlink_c::create() {
setArcName(checkWolf());
setOriginalHeap(&mpArcHeap, 0xA2800);
+ JKRHEAP_NAME(mpArcHeap, "Alink ArcHeap");
if (dComIfG_resLoad(&mPhaseReq, mArcName, mpArcHeap) != cPhs_COMPLEATE_e) {
return cPhs_INIT_e;
}
setShieldArcName();
setOriginalHeap(&mpShieldArcHeap, 0x7000);
- if (dComIfG_resLoad(&mShieldPhaseReq, mShieldArcName, mpShieldArcHeap) != cPhs_COMPLEATE_e) {
+ JKRHEAP_NAME(mpShieldArcHeap, "Alink ShieldArcHeap");
+ if (dComIfG_resLoad(&mShieldPhaseReq, mShieldArcName, mpShieldArcHeap) != cPhs_COMPLEATE_e)
+ {
return cPhs_INIT_e;
}
diff --git a/src/d/actor/d_a_alink_damage.inc b/src/d/actor/d_a_alink_damage.inc
index ac5175e314..e095fc0393 100644
--- a/src/d/actor/d_a_alink_damage.inc
+++ b/src/d/actor/d_a_alink_damage.inc
@@ -8,6 +8,10 @@
#include "d/actor/d_a_horse.h"
#include "d/actor/d_a_crod.h"
#include "d/d_msg_object.h"
+#ifdef TARGET_PC
+#include "d/actor/d_a_obj_carry.h"
+#include "dusk/achievements.h"
+#endif
#if DEBUG
#include "d/d_s_menu.h"
@@ -677,6 +681,15 @@ BOOL daAlink_c::checkDamageAction() {
}
setDamagePoint(dmg, at_mtrl == dCcD_MTRL_FIRE || at_mtrl == dCcD_MTRL_ICE, TRUE, 0);
+
+#ifdef TARGET_PC
+ if (tghit_ac_name == fpcNm_Obj_Carry_e) {
+ auto* carry = static_cast(tghit_ac);
+ if (carry->prm_chk_type_ironball() && carry->checkCannon()) {
+ dusk::AchievementSystem::get().signal("iron_ball_hit_player");
+ }
+ }
+#endif
if (armor_no_dmg && at_mtrl != dCcD_MTRL_ELECTRIC && at_mtrl != dCcD_MTRL_ICE) {
setGuardSe(var_r29);
diff --git a/src/d/actor/d_a_alink_demo.inc b/src/d/actor/d_a_alink_demo.inc
index 5b2d36dce1..13ec1f2136 100644
--- a/src/d/actor/d_a_alink_demo.inc
+++ b/src/d/actor/d_a_alink_demo.inc
@@ -23,6 +23,7 @@
#include "d/actor/d_a_npc_tkc.h"
#include
+#include "dusk/imgui/ImGuiConsole.hpp"
#include "dusk/settings.h"
BOOL daAlink_c::checkEventRun() const {
@@ -4005,6 +4006,15 @@ int daAlink_c::procGanonFinishInit() {
field_0x37c8 = current.pos;
onEndResetFlg1(ERFLG1_SHIELD_BACKBONE);
+
+#if TARGET_PC
+ if (dusk::getSettings().game.speedrunMode) {
+ if (dusk::m_speedrunInfo.m_isRunStarted) {
+ dusk::m_speedrunInfo.stopRun();
+ }
+ }
+#endif
+
return 1;
}
diff --git a/src/d/actor/d_a_alink_hook.inc b/src/d/actor/d_a_alink_hook.inc
index d152190161..73049af08b 100644
--- a/src/d/actor/d_a_alink_hook.inc
+++ b/src/d/actor/d_a_alink_hook.inc
@@ -18,6 +18,10 @@ enum {
};
void daAlink_c::hsChainShape_c::draw() {
+ if (dusk::getSettings().game.superClawshot) {
+ return;
+ }
+
static const int dummy = 0;
daAlink_c* alink = (daAlink_c*)getUserArea();
diff --git a/src/d/actor/d_a_alink_horse.inc b/src/d/actor/d_a_alink_horse.inc
index ac7ce73e50..f0c585f1b8 100644
--- a/src/d/actor/d_a_alink_horse.inc
+++ b/src/d/actor/d_a_alink_horse.inc
@@ -2721,7 +2721,7 @@ int daAlink_c::procHorseRun() {
}
if (mProcVar2.field_0x300c == 0) {
- set3DStatus(BUTTON_STATUS_HOLD_ON, 4);
+ set3DStatus(BUTTON_STATUS_HOLD_ON, IF_DUSK(dusk::getSettings().game.enableMirrorMode ? 1 :) 4);
}
} else {
if (mProcVar3.field_0x300e != 0) {
@@ -2731,7 +2731,7 @@ int daAlink_c::procHorseRun() {
}
if (mProcVar2.field_0x300c == 0) {
- set3DStatus(BUTTON_STATUS_HOLD_ON, 1);
+ set3DStatus(BUTTON_STATUS_HOLD_ON, IF_DUSK(dusk::getSettings().game.enableMirrorMode ? 4 :) 1);
}
}
diff --git a/src/d/actor/d_a_alink_swindow.inc b/src/d/actor/d_a_alink_swindow.inc
index 81c7a1ca18..4f651e16e2 100644
--- a/src/d/actor/d_a_alink_swindow.inc
+++ b/src/d/actor/d_a_alink_swindow.inc
@@ -46,7 +46,6 @@ void daAlink_c::setOriginalHeap(JKRExpHeap** i_ppheap, u32 i_size) {
JKRHeap* parent = mDoExt_getGameHeap();
JKRExpHeap* heap = JKRExpHeap::create(size + (var_r29 + var_r28), parent, true);
- JKRHEAP_NAME(heap, "Alink original");
*i_ppheap = heap;
}
}
diff --git a/src/d/actor/d_a_balloon_2D.cpp b/src/d/actor/d_a_balloon_2D.cpp
index 6b5b5a2bd2..8627505371 100644
--- a/src/d/actor/d_a_balloon_2D.cpp
+++ b/src/d/actor/d_a_balloon_2D.cpp
@@ -6,6 +6,7 @@
#include "d/dolzel_rel.h" // IWYU pragma: keep
#include "d/actor/d_a_balloon_2D.h"
+#include "dusk/frame_interpolation.h"
#include "JSystem/J2DGraph/J2DGrafContext.h"
#include "JSystem/J2DGraph/J2DScreen.h"
#include "JSystem/J2DGraph/J2DTextBox.h"
@@ -438,7 +439,12 @@ void daBalloon2D_c::setComboAlpha() {
void daBalloon2D_c::drawAddScore() {
for (s32 i = 19; i >= 0; i--) {
if (field_0x5f8[i].field_0xe != 0) {
- field_0x5f8[i].field_0xe--;
+#ifdef TARGET_PC
+ if (dusk::frame_interp::get_ui_tick_pending())
+#endif
+ {
+ field_0x5f8[i].field_0xe--;
+ }
s32 score3;
s32 score2;
s32 score = field_0x5f8[i].field_0xc;
@@ -446,8 +452,13 @@ void daBalloon2D_c::drawAddScore() {
u8 local_88 = 0xff;
f32 dVar11 = 30.0f;
f32 dVar9 = 30.0f;
- field_0x5f8[i].field_0x0.x += cM_ssin(temp0) * 0.3f;
- field_0x5f8[i].field_0x0.y -= 1.0f;
+#ifdef TARGET_PC
+ if (dusk::frame_interp::get_ui_tick_pending())
+#endif
+ {
+ field_0x5f8[i].field_0x0.x += cM_ssin(temp0) * 0.3f;
+ field_0x5f8[i].field_0x0.y -= 1.0f;
+ }
if (field_0x5f8[i].field_0xe < 10) {
f32 fVar5 = field_0x5f8[i].field_0xe / 10.0f;
local_88 = fVar5 * 255.0f;
diff --git a/src/d/actor/d_a_door_shutter.cpp b/src/d/actor/d_a_door_shutter.cpp
index a481762132..ebd1c9a185 100644
--- a/src/d/actor/d_a_door_shutter.cpp
+++ b/src/d/actor/d_a_door_shutter.cpp
@@ -17,6 +17,11 @@
#include
#include
+#if TARGET_PC
+#include
+#include
+#endif
+
char* daDoor20_c::getStopBmdName() {
switch (door_param2_c::getKind(this)) {
case 3:
@@ -196,6 +201,7 @@ void daDoor20_c::setEventPrm() {
} else {
roomNo = FRoomNo;
}
+
if (dComIfGp_roomControl_checkStatusFlag(roomNo, 1)) {
if (door_param2_c::getKind(this) == 9) {
if (daPy_py_c::checkNowWolf()) {
@@ -564,6 +570,11 @@ int daDoor20_c::openEnd(int param_1) {
openEnd_1();
break;
}
+
+ #if TARGET_PC
+ triggerAutoSave();
+ #endif
+
return 1;
}
diff --git a/src/d/actor/d_a_e_oct_bg.cpp b/src/d/actor/d_a_e_oct_bg.cpp
index 14a421a3b6..94fa69596b 100644
--- a/src/d/actor/d_a_e_oct_bg.cpp
+++ b/src/d/actor/d_a_e_oct_bg.cpp
@@ -517,6 +517,12 @@ void daE_OctBg_c::core_fish_attack() {
field_0xbaf = cM_rndFX(80.0f) + 100.0f;
}
}
+ #if AVOID_UB
+ else {
+ in_f31 = cM_rndF(400.0f) + 80.0f;
+ field_0xbaf = cM_rndFX(80.0f) + 100.0f;
+ }
+ #endif
} else if (current.pos.abs(cStack_5c) < 400.0f) {
in_f31 = cM_rndF(50.0f) + 20.0f;
field_0xbaf = cM_rndFX(20.0f) + 40.0f;
diff --git a/src/d/actor/d_a_e_th.cpp b/src/d/actor/d_a_e_th.cpp
index f963350da4..43ad8ec19b 100644
--- a/src/d/actor/d_a_e_th.cpp
+++ b/src/d/actor/d_a_e_th.cpp
@@ -282,6 +282,11 @@ static void e_th_spin_B(e_th_class* i_this) {
i_this->current.pos += spC;
f32 speed_target;
+
+ #if AVOID_UB
+ speed_target = 0;
+ #endif
+
f32 anm_frame = i_this->mpModelMorf->getFrame();
switch (i_this->mMode) {
diff --git a/src/d/actor/d_a_kago.cpp b/src/d/actor/d_a_kago.cpp
index 147a031f7e..9444237f12 100644
--- a/src/d/actor/d_a_kago.cpp
+++ b/src/d/actor/d_a_kago.cpp
@@ -3519,7 +3519,15 @@ void daKago_c::action() {
checkSizeBg();
setFlyEffect();
+#if TARGET_PC
+ if (dusk::getSettings().game.enableMirrorMode) {
+ mStickX = -mDoCPd_c::getStickX3D(PAD_1);
+ } else {
+ mStickX = mDoCPd_c::getStickX3D(PAD_1);
+ }
+#else
mStickX = mDoCPd_c::getStickX3D(PAD_1);
+#endif
mStickY = mDoCPd_c::getStickY(PAD_1);
u8 prevIsWaterfall = mIsWaterfall;
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/actor/d_a_mirror.cpp b/src/d/actor/d_a_mirror.cpp
index 86ef4314f3..5c5218328e 100644
--- a/src/d/actor/d_a_mirror.cpp
+++ b/src/d/actor/d_a_mirror.cpp
@@ -40,6 +40,7 @@ dMirror_packet_c::dMirror_packet_c() {
void dMirror_packet_c::reset() {
#if TARGET_PC
mbReset = true;
+ mbHadEntry = false;
#else
mModelCount = 0;
#endif
@@ -84,11 +85,21 @@ void dMirror_packet_c::calcMinMax() {
}
int dMirror_packet_c::entryModel(J3DModel* i_model) {
+#if TARGET_PC
+ if (mbReset) {
+ mModelCount = 0;
+ mbReset = false;
+ }
+#endif
+
if (mModelCount >= 0x40) {
return 0;
}
mModels[mModelCount++] = i_model;
+#if TARGET_PC
+ mbHadEntry = true;
+#endif
return 1;
}
@@ -592,13 +603,6 @@ int daMirror_c::execute() {
return 1;
}
-#if TARGET_PC
- if (mPacket.mbReset) {
- mPacket.mModelCount = 0;
- mPacket.mbReset = false;
- }
-#endif
-
daPy_py_c* player = daPy_getLinkPlayerActorClass();
JUT_ASSERT(0, player != NULL);
@@ -624,6 +628,12 @@ int daMirror_c::draw() {
mDoExt_modelUpdateDL(mpModel);
}
+#if TARGET_PC
+ if (mPacket.mbReset && !mPacket.mbHadEntry) {
+ mPacket.mModelCount = 0;
+ }
+ mPacket.mbHadEntry = true;
+#endif
dComIfGd_getOpaListBG()->entryImm(&mPacket, 0);
return 1;
}
diff --git a/src/d/actor/d_a_npc_ne.cpp b/src/d/actor/d_a_npc_ne.cpp
index 1324c56cf5..90e33adea5 100644
--- a/src/d/actor/d_a_npc_ne.cpp
+++ b/src/d/actor/d_a_npc_ne.cpp
@@ -956,7 +956,7 @@ static void npc_ne_tame(npc_ne_class* i_this) {
i_this->mpMorf->setPlaySpeed(i_this->mAnmSpeed);
/* dSv_event_flag_c::F_0470 - Fishing Pond - Reserved for fishing */
- if (dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[470])) {
+ if (IF_DUSK(dusk::getSettings().game.no2ndFishForCat) || dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[470])) {
if (fpcEx_Search(s_fish_sub, _this) != NULL) {
i_this->mAction = npc_ne_class::ACT_HOME;
i_this->mMode = 10;
@@ -2948,8 +2948,7 @@ static int daNpc_Ne_Execute(npc_ne_class* i_this) {
if (i_this->mWantsFish && (i_this->mCounter & 0xf) == 0) {
/* dSv_event_flag_c::F_0470 - Fishing Pond - Reserved for fishing */
- if (dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[470])
- && i_this->mDistToTarget < 1500.0f) {
+ if ((IF_DUSK(dusk::getSettings().game.no2ndFishForCat) || dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[470])) && i_this->mDistToTarget < 1500.0f) {
if (fopAcM_SearchByName(fpcNm_MG_ROD_e) != NULL) {
i_this->mNoFollow = false;
} else {
diff --git a/src/d/actor/d_a_npc_toby.cpp b/src/d/actor/d_a_npc_toby.cpp
index 543d4b9abf..d175d199b7 100644
--- a/src/d/actor/d_a_npc_toby.cpp
+++ b/src/d/actor/d_a_npc_toby.cpp
@@ -14,6 +14,7 @@
#include "d/actor/d_a_obj_automata.h"
#include "d/d_msg_object.h"
#include "d/actor/d_a_obj_scannon.h"
+#include "dusk/frame_interpolation.h"
#include
const daNpc_Toby_HIOParam daNpc_Toby_Param_c::m = {
@@ -1398,6 +1399,7 @@ int daNpc_Toby_c::cutRepairSCannon(int arg0) {
old.pos = current.pos;
setAngle(cM_deg2s(5.0f * f32(mPath.getArg0())));
mEventTimer = mPath.getArg2();
+ dusk::frame_interp::request_presentation_sync();
}
} else if (!mHide) {
mHide = 1;
diff --git a/src/d/actor/d_a_obj_balloon.cpp b/src/d/actor/d_a_obj_balloon.cpp
index 41430f97f0..d857a569ba 100644
--- a/src/d/actor/d_a_obj_balloon.cpp
+++ b/src/d/actor/d_a_obj_balloon.cpp
@@ -62,6 +62,16 @@ void daObj_Balloon_c::saveBestScore() {
dComIfGp_setMessageCountNumber(m_balloon_score);
}
+#if TARGET_PC
+static void minigameReset() {
+ // !@bug d_a_obj_balloon.rel unload used to zero these file-statics; with static linking they dangle across scenes.
+ m_combo_type = 0xFFFFFFFF;
+ m_combo_count = 0;
+ m_combo_next_score = 0;
+ m_balloon_score = 0;
+}
+#endif
+
static u8 hio_set;
static daObj_Balloon_HIO_c l_HIO;
@@ -246,6 +256,7 @@ int daObj_Balloon_c::create() {
}
if (!hio_set) {
+ IF_DUSK(minigameReset());
mHIOInit = true;
hio_set = true;
l_HIO.field_0x04 = -1;
diff --git a/src/d/actor/d_a_obj_klift00.cpp b/src/d/actor/d_a_obj_klift00.cpp
index 99b204592e..e44cce7467 100644
--- a/src/d/actor/d_a_obj_klift00.cpp
+++ b/src/d/actor/d_a_obj_klift00.cpp
@@ -11,6 +11,8 @@
#include "d/d_bg_w.h"
#include "d/d_cc_uty.h"
#include "d/d_com_inf_game.h"
+#include "dusk/frame_interpolation.h"
+#include "dusk/settings.h"
struct daObjKLift00_HIO_c : public mDoHIO_entry_c {
daObjKLift00_HIO_c();
@@ -295,6 +297,11 @@ int daObjKLift00_c::Create() {
if(getLock())
mStopSwingingFrames = 5;
+#if TARGET_PC
+ mChainInterpPrevValid = false;
+ mChainInterpCurrValid = false;
+#endif
+
return 1;
}
@@ -436,6 +443,34 @@ int daObjKLift00_c::Execute(Mtx** i_mtx) {
return 1;
}
+#if TARGET_PC
+static void klift00_interp_callback(bool isSimFrame, void* pUserWork) {
+ static_cast(pUserWork)->onInterpCallback();
+}
+
+void daObjKLift00_c::onInterpCallback() {
+ if (!mChainInterpPrevValid || !mChainInterpCurrValid) {
+ return;
+ }
+
+ const f32 alpha = dusk::frame_interp::get_interpolation_step();
+ cXyz savedPositions[64];
+
+ for (int i = 0; i < mNumChains; i++) {
+ savedPositions[i] = mChainPositions[i].mCurrentPos;
+ const cXyz& p0 = mChainInterpPrev[i];
+ const cXyz& p1 = mChainInterpCurr[i];
+ mChainPositions[i].mCurrentPos = p0 + (p1 - p0) * alpha;
+ }
+
+ setMtx();
+
+ for (int i = 0; i < mNumChains; i++) {
+ mChainPositions[i].mCurrentPos = savedPositions[i];
+ }
+}
+#endif
+
int daObjKLift00_c::Draw() {
g_env_light.settingTevStruct(16, ¤t.pos, &tevStr);
g_env_light.setLightTevColorType_MAJI(mpLiftPlatform, &tevStr);
@@ -457,6 +492,22 @@ int daObjKLift00_c::Draw() {
dComIfGd_setList();
+#if TARGET_PC
+ if (dusk::getSettings().game.enableFrameInterpolation) {
+ if (mChainInterpCurrValid) {
+ memcpy(mChainInterpPrev, mChainInterpCurr, mNumChains * sizeof(cXyz));
+ mChainInterpPrevValid = true;
+ }
+
+ for (int i = 0; i < mNumChains; i++) {
+ mChainInterpCurr[i] = mChainPositions[i].mCurrentPos;
+ }
+
+ mChainInterpCurrValid = true;
+ dusk::frame_interp::add_interpolation_callback(&klift00_interp_callback, this);
+ }
+#endif
+
return 1;
}
diff --git a/src/d/actor/d_a_obj_lv8Lift.cpp b/src/d/actor/d_a_obj_lv8Lift.cpp
index 4f475a626c..8fa9422325 100644
--- a/src/d/actor/d_a_obj_lv8Lift.cpp
+++ b/src/d/actor/d_a_obj_lv8Lift.cpp
@@ -10,6 +10,10 @@
#include "d/d_path.h"
#include "d/d_bg_w.h"
+#if TARGET_PC
+#include "dusk/frame_interpolation.h"
+#endif
+
daL8Lift_HIO_c::daL8Lift_HIO_c() {
mStopDisappearTime = 30;
mStartMoveTime = 60;
@@ -380,7 +384,44 @@ void daL8Lift_c::setNextPoint() {
mCurrentPoint = next_point;
}
+#if TARGET_PC
+void daL8Lift_interp_callback(bool isSimFrame, void* pUserWork) {
+ daL8Lift_c* lift = static_cast(pUserWork);
+ if (lift == NULL || lift->mpModel == NULL) {
+ return;
+ }
+
+ g_env_light.settingTevStruct(0x10, &lift->current.pos, &lift->tevStr);
+ g_env_light.setLightTevColorType_MAJI(lift->mpModel, &lift->tevStr);
+
+ J3DModelData* modelData = lift->mpModel->getModelData();
+ J3DMaterial* materialp = modelData->getMaterialNodePointer(0);
+
+ if (materialp->getTexGenBlock()->getTexMtx(1) != NULL) {
+ J3DTexMtxInfo* mtx_info = &materialp->getTexGenBlock()->getTexMtx(1)->getTexMtxInfo();
+ if (mtx_info != NULL) {
+ Mtx m;
+ C_MTXLightOrtho(m, 100.0f, -100.0f, -100.0f, 100.0f, 1.0f, 1.0f, 0.0f, 0.0f);
+ mDoMtx_stack_c::XrotS(0x4000);
+ mDoMtx_stack_c::transM(-lift->current.pos.x, -lift->current.pos.y, -lift->current.pos.z);
+ cMtx_concat(m, mDoMtx_stack_c::get(), mtx_info->mEffectMtx);
+ }
+ }
+
+ lift->mBtk.entry(modelData);
+
+ J3DGXColor* color = materialp->getTevKColor(1);
+ color->r = l_HIO.mColorR;
+ color->g = l_HIO.mColorG;
+ color->b = l_HIO.mColorB;
+}
+#endif
+
int daL8Lift_c::Draw() {
+#if TARGET_PC
+ dusk::frame_interp::add_interpolation_callback(&daL8Lift_interp_callback, this);
+#endif
+
g_env_light.settingTevStruct(16, ¤t.pos, &tevStr);
g_env_light.setLightTevColorType_MAJI(mpModel, &tevStr);
J3DModelData* modelData = mpModel->getModelData();
diff --git a/src/d/actor/d_flower.inc b/src/d/actor/d_flower.inc
index 5f54ea4bda..4024d5ad6a 100644
--- a/src/d/actor/d_flower.inc
+++ b/src/d/actor/d_flower.inc
@@ -7,13 +7,8 @@
#include "dusk/frame_interpolation.h"
-#if TARGET_PC
-const u16 l_J_Ohana00_64TEX__width = 64;
-const u16 l_J_Ohana00_64TEX__height = 64;
-#else
const u16 l_J_Ohana00_64TEX__width = 63;
const u16 l_J_Ohana00_64TEX__height = 63;
-#endif
#if TARGET_PC
#include "dusk/dvd_asset.hpp"
@@ -136,13 +131,8 @@ l_matDL__d_a_grass(l_J_Ohana00_64TEX)
l_matLight4DL(l_J_Ohana00_64TEX)
#endif
-#if TARGET_PC
-const u16 l_J_Ohana01_64128_0419TEX__width = 64;
-const u16 l_J_Ohana01_64128_0419TEX__height = 128;
-#else
const u16 l_J_Ohana01_64128_0419TEX__width = 63;
const u16 l_J_Ohana01_64128_0419TEX__height = 127;
-#endif
#if TARGET_PC
using GameVersion = dusk::version::GameVersion;
@@ -592,11 +582,11 @@ dFlower_packet_c::dFlower_packet_c() {
#if TARGET_PC
GXInitTexObj(&mTexObj_l_J_Ohana00_64TEX, l_J_Ohana00_64TEX,
- l_J_Ohana00_64TEX__width, l_J_Ohana00_64TEX__height, GX_TF_CMPR, GX_MIRROR, GX_MIRROR, GX_FALSE
+ l_J_Ohana00_64TEX__width + 1, l_J_Ohana00_64TEX__height + 1, GX_TF_CMPR, GX_MIRROR, GX_MIRROR, GX_FALSE
);
GXInitTexObj(&mTexObj_l_J_Ohana01_64128_0419TEX, l_J_Ohana01_64128_0419TEX,
- l_J_Ohana01_64128_0419TEX__width, l_J_Ohana01_64128_0419TEX__height, GX_TF_CMPR, GX_MIRROR, GX_MIRROR, GX_FALSE
+ l_J_Ohana01_64128_0419TEX__width + 1, l_J_Ohana01_64128_0419TEX__height + 1, GX_TF_CMPR, GX_MIRROR, GX_MIRROR, GX_FALSE
);
#endif
diff --git a/src/d/actor/d_grass.inc b/src/d/actor/d_grass.inc
index 9bc8815753..3d67a57007 100644
--- a/src/d/actor/d_grass.inc
+++ b/src/d/actor/d_grass.inc
@@ -494,11 +494,11 @@ dGrass_packet_c::dGrass_packet_c() {
#if TARGET_PC
GXInitTexObj(&mTexObj_l_M_kusa05_RGBATEX, l_M_kusa05_RGBATEX,
- l_M_kusa05_RGBATEX__width, l_M_kusa05_RGBATEX__height, GX_TF_RGB5A3, GX_REPEAT, GX_CLAMP, GX_FALSE
+ l_M_kusa05_RGBATEX__width + 1, l_M_kusa05_RGBATEX__height + 1, GX_TF_RGB5A3, GX_REPEAT, GX_CLAMP, GX_FALSE
);
GXInitTexObj(&mTexObj_l_M_Hijiki00TEX, l_M_Hijiki00TEX,
- l_M_Hijiki00TEX__width, l_M_Hijiki00TEX__height, GX_TF_RGB5A3, GX_REPEAT, GX_CLAMP, GX_FALSE
+ l_M_Hijiki00TEX__width + 1, l_M_Hijiki00TEX__height + 1, GX_TF_RGB5A3, GX_REPEAT, GX_CLAMP, GX_FALSE
);
#endif
@@ -646,18 +646,14 @@ void dGrass_packet_c::draw() {
}
if (var_r29->field_0x05 <= 3 || var_r29->field_0x05 >= 10) {
-#if TARGET_PC
- GXLoadTexObj(&mTexObj_l_M_kusa05_RGBATEX, GX_TEXMAP0);
-#endif
+ IF_DUSK(GXLoadTexObj(&mTexObj_l_M_kusa05_RGBATEX, GX_TEXMAP0));
if (sp48 <= 3) {
GXCallDisplayList(mp_kusa9q_14_DL, m_kusa9q_DL_14_size);
} else {
GXCallDisplayList(mp_kusa9q_DL, m_kusa9q_DL_size);
}
} else {
-#if TARGET_PC
- GXLoadTexObj(&mTexObj_l_M_Hijiki00TEX, GX_TEXMAP0);
-#endif
+ IF_DUSK(GXLoadTexObj(&mTexObj_l_M_Hijiki00TEX, GX_TEXMAP0));
GXCallDisplayList(l_Tengusa_matDL, 0xA0);
}
@@ -683,12 +679,14 @@ void dGrass_packet_c::draw() {
while (var_r29 != NULL) {
if (var_r29->field_0x05 <= 3 || var_r29->field_0x05 >= 10) {
+ IF_DUSK(GXLoadTexObj(&mTexObj_l_M_kusa05_RGBATEX, GX_TEXMAP0));
if (sp48 <= 2) {
GXCallDisplayList(mp_kusa9q_14_DL, m_kusa9q_DL_14_size);
} else {
GXCallDisplayList(mp_kusa9q_DL, m_kusa9q_DL_size);
}
} else {
+ IF_DUSK(GXLoadTexObj(&mTexObj_l_M_Hijiki00TEX, GX_TEXMAP0));
GXCallDisplayList(l_Tengusa_matDL, 0xA0);
}
diff --git a/src/d/d_bright_check.cpp b/src/d/d_bright_check.cpp
index c3fcc806d5..3ee98be5c7 100644
--- a/src/d/d_bright_check.cpp
+++ b/src/d/d_bright_check.cpp
@@ -9,6 +9,8 @@
#include "JSystem/J2DGraph/J2DScreen.h"
#include "JSystem/J2DGraph/J2DTextBox.h"
#include "d/d_msg_string.h"
+#include "dusk/livesplit.h"
+#include "dusk/imgui/ImGuiConsole.hpp"
#include "m_Do/m_Do_controller_pad.h"
dBrightCheck_c::dBrightCheck_c(JKRArchive* i_archive) {
@@ -138,6 +140,17 @@ void dBrightCheck_c::modeWait() {}
void dBrightCheck_c::modeMove() {
if (mDoCPd_c::getTrigA(PAD_1) || mDoCPd_c::getTrigStart(PAD_1)) {
mDoAud_seStart(Z2SE_ENTER_GAME, NULL, 0, 0);
+#ifdef TARGET_PC
+ dusk::speedrun::start();
+
+ if (dusk::getSettings().game.speedrunMode && !dusk::getSettings().game.hideTvSettingsScreen) {
+ // start a new run if a run isn't already in progress
+ if (!dusk::m_speedrunInfo.m_isRunStarted) {
+ dusk::ImGuiMenuGame::resetForSpeedrunMode();
+ dusk::m_speedrunInfo.startRun();
+ }
+ }
+#endif
mCompleteCheck = true;
mMode = MODE_WAIT_e;
}
diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp
index 917b30709a..e559a56d95 100644
--- a/src/d/d_camera.cpp
+++ b/src/d/d_camera.cpp
@@ -794,16 +794,15 @@ void dCamera_c::updatePad() {
if (mTriggerLeftLast > mCamSetup.ManualEndVal()) {
if (mLockLActive == 0) {
+ #if TARGET_PC
+ mCamParam.mManualMode = 0;
+ #endif
mLockLJustActivated = 1;
} else {
mLockLJustActivated = 0;
}
mLockLActive = 1;
-
- #if TARGET_PC
- mCamParam.mManualMode = 0;
- #endif
} else {
mLockLJustActivated = 0;
mLockLActive = 0;
@@ -1041,6 +1040,11 @@ void dCamera_c::debugDrawInit() {
bool dCamera_c::Run() {
#if TARGET_PC
ResetView();
+ if (executeDebugFlyCam()) {
+ mFrameCounter++;
+ mTicks++;
+ return true;
+ }
#endif
daAlink_c* link = daAlink_getAlinkActorClass();
@@ -1176,14 +1180,14 @@ bool dCamera_c::Run() {
clrFlag(0x200000);
}
} else {
- sp0F = (this->*engine_tbl[mCamParam.Algorythmn(mCamStyle)])(mCamStyle);
-
#if TARGET_PC
if (mCamParam.Algorythmn(mCamStyle) != 1) {
mCamParam.mManualMode = 0;
}
#endif
+ sp0F = (this->*engine_tbl[mCamParam.Algorythmn(mCamStyle)])(mCamStyle);
+
field_0x170++;
field_0x160++;
mCurCamStyleTimer++;
@@ -1488,7 +1492,7 @@ void dCamera_c::CalcTrimSize() {
mTrimHeight += -mTrimHeight * 0.25f;
break;
case 2:
-#if WIDESCREEN_SUPPORT
+#if !TARGET_PC && WIDESCREEN_SUPPORT
if (mDoGph_gInf_c::isWide() && mDoGph_gInf_c::isWideZoom()) {
mTrimHeight += (16.0f - mTrimHeight) * 0.25f;
break;
@@ -3096,10 +3100,6 @@ bool dCamera_c::bumpCheck(u32 i_flags) {
field_0x968 *= mMonitor.field_0xc / 5.0f;
}
- #if TARGET_PC
- if (!dusk::getSettings().game.freeCamera || !mCamParam.mManualMode) {
- #endif
-
f32 tmp = field_0x96c * (mIsWolf == 1 ? 30.0f : 30.0f);
center += vec3.norm() * (tmp * globe.V().Sin());
cSGlobe globe2(vec2 - center);
@@ -3113,10 +3113,6 @@ bool dCamera_c::bumpCheck(u32 i_flags) {
vec = lin_chk1.GetCross();
}
- #if TARGET_PC
- }
- #endif
-
#if DEBUG
if (mCamSetup.CheckFlag(0x8000)) {
dDbVw_Report(20, 235, " U");
@@ -4208,6 +4204,11 @@ bool dCamera_c::chaseCamera(s32 param_0) {
chase->field_0x8 -= chase->field_0xc;
chase->field_0x8c = 0;
chase->field_0x90 = false;
+
+ #if TARGET_PC
+ freeCamera();
+ #endif
+
return true;
}
@@ -4631,10 +4632,6 @@ bool dCamera_c::chaseCamera(s32 param_0) {
sp110 = mViewCache.mDirection.R();
mViewCache.mDirection.R(mViewCache.mDirection.R() + (fVar55 - mViewCache.mDirection.R()) * chase->field_0x74);
- #if TARGET_PC
- freeCamera();
- #endif
-
chase->field_0x64 = mViewCache.mCenter + mViewCache.mDirection.Xyz();
mViewCache.mEye = chase->field_0x64;
@@ -4649,6 +4646,11 @@ bool dCamera_c::chaseCamera(s32 param_0) {
if (chase->field_0x1c != 0) {
chase->field_0x1c--;
}
+
+ #if TARGET_PC
+ freeCamera();
+ #endif
+
return true;
}
@@ -7096,10 +7098,12 @@ bool dCamera_c::subjectCamera(s32 param_0) {
cXyz sp1E0(val0, val2, val1);
#if TARGET_PC
- f32 aspect = mDoGph_gInf_c::getAspect();
- f32 baseAspect = FB_WIDTH / FB_HEIGHT;
- if (aspect > baseAspect) {
- sp1E0.z += (aspect - baseAspect) * 4;
+ if (sp13) {
+ f32 aspect = mDoGph_gInf_c::getAspect();
+ f32 baseAspect = FB_WIDTH / FB_HEIGHT;
+ if (aspect > baseAspect) {
+ sp1E0.z += (aspect - baseAspect) * 4;
+ }
}
#endif
@@ -7475,38 +7479,147 @@ bool dCamera_c::test2Camera(s32 param_0) {
return false;
}
+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;
+
#if TARGET_PC
+bool dCamera_c::executeDebugFlyCam() {
+ if (!dusk::getSettings().game.debugFlyCam) {
+ if (mDebugFlyCam.initialized) {
+ deactivateDebugFlyCam();
+ }
+ return false;
+ }
+
+ dEvt_control_c* event = dComIfGp_getEvent();
+ if (event == nullptr) {
+ return false;
+ }
+
+ if (!mDebugFlyCam.initialized && (event->mEventStatus != 0 || dComIfGp_isPauseFlag())) {
+ dusk::getSettings().game.debugFlyCam.setValue(false);
+ return false;
+ }
+
+ if (!mDebugFlyCam.initialized) {
+ mDebugFlyCam.savedCenter = mCenter;
+ mDebugFlyCam.savedEye = mEye;
+ mDebugFlyCam.savedFovy = mFovy;
+ mDebugFlyCam.savedBank = mBank;
+
+ f32 dx = mCenter.x - mEye.x;
+ f32 dy = mCenter.y - mEye.y;
+ f32 dz = mCenter.z - mEye.z;
+ mDebugFlyCam.yaw = atan2f(dz, dx);
+ f32 horizontal = sqrtf(dx * dx + dz * dz);
+ mDebugFlyCam.pitch = atan2f(dy, horizontal);
+
+ mDebugFlyCam.initialized = true;
+ }
+
+ event->mEventStatus = 1;
+ dComIfGp_getEventManager().setCameraPlay(1);
+
+ 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 verticalDisp = 0.0f;
+ if (trigR >= FLYCAM_TRIGGER_DEADZONE) {
+ verticalDisp += trigR;
+ }
+ if (trigL >= FLYCAM_TRIGGER_DEADZONE) {
+ verticalDisp -= trigL;
+ }
+
+ f32 moveDy = stickY * sinf(mDebugFlyCam.pitch) + verticalDisp;
+ 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;
+
+ mEye.x += speed * moveDx;
+ mEye.y += speed * moveDy;
+ mEye.z += speed * moveDz;
+
+ static constexpr f32 FLYCAM_TARGET_DIST = 100.0f;
+ mCenter.x = mEye.x + cosf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST;
+ mCenter.z = mEye.z + sinf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST;
+ mCenter.y = mEye.y + sinf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST;
+
+ Reset(mCenter, mEye);
+
+ f32 yawInput = dusk::getSettings().game.invertCameraXAxis ? cStickX : -cStickX;
+ mDebugFlyCam.yaw += yawInput * FLYCAM_ROTATION_SPEED;
+ mDebugFlyCam.yaw = fmodf(mDebugFlyCam.yaw + 2.0f * (f32)M_PI, 2.0f * (f32)M_PI);
+
+ f32 maxPitch = (f32)M_PI / 2.0f - 0.1f;
+ f32 minPitch = -(f32)M_PI / 2.0f + 0.1f;
+ mDebugFlyCam.pitch = std::clamp(mDebugFlyCam.pitch + cStickY * FLYCAM_ROTATION_SPEED, minPitch, maxPitch);
+
+ return true;
+}
+
+void dCamera_c::deactivateDebugFlyCam() {
+ Reset(mDebugFlyCam.savedCenter, mDebugFlyCam.savedEye, mDebugFlyCam.savedFovy, mDebugFlyCam.savedBank.Val());
+
+ dEvt_control_c* event = dComIfGp_getEvent();
+ if (event != nullptr) {
+ event->mEventStatus = 0;
+ }
+ dComIfGp_getEventManager().setCameraPlay(0);
+ mDebugFlyCam.initialized = false;
+}
+
bool dCamera_c::freeCamera() {
- if (!dusk::getSettings().game.freeCamera) {
+ if (dusk::getSettings().game.freeCamera && mGear == 1) {
+ mGear = 0;
+ }
+
+ if (!dusk::getSettings().game.freeCamera || mCamStyle == 70)
+ {
mCamParam.mManualMode = 0;
return false;
}
+ if (!mCamParam.mManualMode) {
+ mCamParam.freeXAngle = mViewCache.mDirection.mAzimuth.Degree();
+ mCamParam.freeYAngle = mViewCache.mDirection.mInclination.Degree();
+ }
+
cXyz camMovement = {mPadInfo.mCStick.mLastPosX, mPadInfo.mCStick.mLastPosY, 0.0f};
f32 magnitude = sqrt(mPadInfo.mCStick.mLastPosX * mPadInfo.mCStick.mLastPosX + mPadInfo.mCStick.mLastPosY * mPadInfo.mCStick.mLastPosY);
if (mPadInfo.mCStick.mLastPosX != 0 || mPadInfo.mCStick.mLastPosY != 0) {
- if (!mCamParam.mManualMode) {
- mCamParam.mManualMode = 1;
- mCamParam.freeXAngle = mViewCache.mDirection.mAzimuth.Degree();
- mCamParam.freeYAngle = mViewCache.mDirection.mInclination.Degree();
- }
-
+ mCamParam.mManualMode = 1;
camMovement = camMovement.normalize();
- camMovement.x *= (dusk::getSettings().game.invertCameraXAxis ? 1.0f : -1.0f) * dusk::getSettings().game.freeCameraSensitivity * 4.0f;
- camMovement.y *= (dusk::getSettings().game.invertCameraYAxis ? 1.0f : -1.0f) * dusk::getSettings().game.freeCameraSensitivity * 4.0f;
- mCamParam.freeXAngle += camMovement.x * magnitude * dusk::getSettings().game.freeCameraSensitivity;
- mCamParam.freeYAngle += camMovement.y * magnitude * dusk::getSettings().game.freeCameraSensitivity;
+ camMovement.y *= dusk::getSettings().game.invertCameraYAxis ? 1.0f : -1.0f;
+ mCamParam.freeXAngle += camMovement.x * magnitude * dusk::getSettings().game.freeCameraSensitivity * 5.0f;
+ mCamParam.freeYAngle += camMovement.y * magnitude * dusk::getSettings().game.freeCameraSensitivity * 5.0f;
}
- if (mCamParam.mManualMode) {
- mCamParam.freeYAngle = std::clamp(mCamParam.freeYAngle, -35.0f, 60.0f);
- mViewCache.mDirection.mAzimuth = cSAngle(mCamParam.freeXAngle);
- mViewCache.mDirection.mInclination = cSAngle(mCamParam.freeYAngle);
- mViewCache.mDirection.mRadius = std::clamp(mCamParam.freeYAngle * 15.0f, 300.0f, 10000.0f);
+ fopAc_ac_c* player = dComIfGp_getPlayer(0);
+ if (!mCamParam.mManualMode || player == nullptr) {
+ return false;
}
- return mCamParam.mManualMode;
+ f32 minYAngle = -30.0f;
+ f32 maxAngle = 50.0f;
+
+ mCamParam.freeYAngle = std::clamp(mCamParam.freeYAngle, minYAngle, maxAngle);
+ mViewCache.mDirection.mAzimuth = cSAngle(mCamParam.freeXAngle);
+ mViewCache.mDirection.mInclination = cSAngle(mCamParam.freeYAngle);
+
+ cXyz finalEye = mViewCache.mCenter + mViewCache.mDirection.Xyz();
+ mViewCache.mEye = finalEye;
+
+ return true;
}
#endif
@@ -11149,12 +11262,25 @@ static int camera_draw(camera_process_class* i_this) {
}
#endif
- int trim_height = body->TrimHeight();
-
#if TARGET_PC
+ auto trim_height = body->TrimHeight();
+
+ if (mDoGph_gInf_c::isWideZoom()) {
+ const auto target_ar = FB_WIDTH / (FB_HEIGHT - trim_height * 2.0f);
+ const auto current_ar = mDoGph_gInf_c::m_safeWidthF / mDoGph_gInf_c::m_safeHeightF;
+
+ if (current_ar < target_ar) {
+ trim_height = FB_HEIGHT / 2.0f * (1.0f - current_ar / target_ar);
+ } else {
+ trim_height = 0.0f;
+ }
+ }
+
trim_height *= viewport->height / FB_HEIGHT;
window->setScissor(0.0f, trim_height, viewport->width, viewport->height - trim_height * 2.0f);
#else
+ int trim_height = body->TrimHeight();
+
window->setScissor(0.0f, trim_height, FB_WIDTH, FB_HEIGHT - trim_height * 2.0f);
#endif
diff --git a/src/d/d_drawlist.cpp b/src/d/d_drawlist.cpp
index 373791f994..1f16b2f655 100644
--- a/src/d/d_drawlist.cpp
+++ b/src/d/d_drawlist.cpp
@@ -22,6 +22,10 @@
#include "dusk/frame_interpolation.h"
#include "dusk/gx_helper.h"
#include "dusk/logging.h"
+
+static const void* getInterpKey(const void* base, int idx) {
+ return reinterpret_cast(reinterpret_cast(base) ^ idx);
+}
#endif
class dDlst_2Dm_c {
@@ -1062,7 +1066,15 @@ void dDlst_shadowReal_c::reset() {
}
void dDlst_shadowReal_c::imageDraw(Mtx param_0) {
- GXSetProjection(mRenderProjMtx, GX_ORTHOGRAPHIC);
+#ifdef TARGET_PC
+ Mtx render_proj_mtx;
+ if (dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 2), render_proj_mtx)) {
+ GXSetProjection(render_proj_mtx, GX_ORTHOGRAPHIC);
+ } else
+#endif
+ {
+ GXSetProjection(mRenderProjMtx, GX_ORTHOGRAPHIC);
+ }
JUT_ASSERT(1916, mModelNum);
J3DModelData* model_data;
J3DModel** models = mpModels;
@@ -1075,7 +1087,15 @@ void dDlst_shadowReal_c::imageDraw(Mtx param_0) {
for (u16 j = 0; j < model_data->getShapeNum(); j++) {
if (!model_data->getShapeNodePointer(j)->checkFlag(1)) {
shape_pkt = (*models)->getShapePacket(j);
- shape_pkt->setBaseMtxPtr(&mViewMtx);
+#ifdef TARGET_PC
+ Mtx view_mtx;
+ if (dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 1), view_mtx)) {
+ shape_pkt->setBaseMtxPtr(&view_mtx);
+ } else
+#endif
+ {
+ shape_pkt->setBaseMtxPtr(&mViewMtx);
+ }
shape_pkt->drawFast();
shape_pkt->setBaseMtxPtr((Mtx*)param_0);
}
@@ -1096,7 +1116,18 @@ void dDlst_shadowReal_c::draw() {
GXSetVtxDesc(GX_VA_POS, GX_DIRECT);
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0);
GXSetCurrentMtx(GX_PNMTX0);
- GXLoadTexMtxImm(mReceiverProjMtx, GX_TEXMTX0, GX_MTX3x4);
+#ifdef TARGET_PC
+ Mtx view_mtx, recv_proj_mtx;
+ const auto have_view_mtx = dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 1), view_mtx);
+ const auto have_recv_proj_mtx = dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 3), recv_proj_mtx);
+ if (have_view_mtx && have_recv_proj_mtx) {
+ cMtx_concat(recv_proj_mtx, view_mtx, recv_proj_mtx);
+ GXLoadTexMtxImm(recv_proj_mtx, GX_TEXMTX0, GX_MTX3x4);
+ } else
+#endif
+ {
+ GXLoadTexMtxImm(mReceiverProjMtx, GX_TEXMTX0, GX_MTX3x4);
+ }
mShadowRealPoly.draw();
}
@@ -1253,6 +1284,13 @@ u8 dDlst_shadowReal_c::setShadowRealMtx(cXyz* param_0, cXyz* param_1, f32 param_
cMtx_lookAt(mViewMtx, &local_64, param_1, 0);
C_MTXOrtho(mRenderProjMtx, param_2, -param_2, -param_2, param_2, 1.0f, 10000.0f);
C_MTXLightOrtho(mReceiverProjMtx, param_2, -param_2, -param_2, param_2, 0.5f, -0.5f, 0.5f, 0.5f);
+
+#ifdef TARGET_PC
+ const auto keybase = mpModels[0];
+ dusk::frame_interp::record_final_mtx(mViewMtx, getInterpKey(keybase, 1));
+ dusk::frame_interp::record_final_mtx(mRenderProjMtx, getInterpKey(keybase, 2));
+ dusk::frame_interp::record_final_mtx(mReceiverProjMtx, getInterpKey(keybase, 3));
+#endif
cMtx_concat(mReceiverProjMtx, mViewMtx, mReceiverProjMtx);
return r29;
}
@@ -1277,6 +1315,10 @@ u32 dDlst_shadowReal_c::set(u32 i_key, J3DModel* i_model, cXyz* param_2, f32 par
}
}
+#ifdef TARGET_PC
+ // provide a stable key for interpolation
+ mpModels[0] = i_model;
+#endif
field_0x1 = setShadowRealMtx(&sp60, param_2, param_3, param_4, param_7, param_5);
if (!field_0x1) {
@@ -1370,12 +1412,6 @@ void dDlst_shadowSimple_c::draw() {
GXCallDisplayList(l_shadowVolumeDL, 0x40);
}
-#if TARGET_PC
-static const void* getInterpKey(const void* base, int idx) {
- return reinterpret_cast(reinterpret_cast(base) ^ idx);
-}
-#endif
-
void dDlst_shadowSimple_c::set(cXyz* param_0, f32 param_1, f32 param_2, cXyz* param_3,
s16 param_4, f32 param_5, TGXTexObj* param_6) {
if (param_5 < 0.0f) {
diff --git a/src/d/d_kankyo_rain.cpp b/src/d/d_kankyo_rain.cpp
index 822612768c..09e230c9fd 100644
--- a/src/d/d_kankyo_rain.cpp
+++ b/src/d/d_kankyo_rain.cpp
@@ -5962,6 +5962,8 @@ static void dKyr_evil_draw2(Mtx drawMtx, u8** tex) {
fopAc_ac_c* player = dComIfGp_getPlayer(0);
if (evil_packet != NULL) {
+ IF_DUSK(GXPushDebugGroup("dKyr_evil_draw2"));
+
j3dSys.reinitGX();
if (dComIfGd_getView() != NULL) {
MTXInverse(dComIfGd_getView()->viewMtxNoTrans, camMtx);
@@ -6162,6 +6164,8 @@ static void dKyr_evil_draw2(Mtx drawMtx, u8** tex) {
}
}
+ IF_DUSK(GXPopDebugGroup());
+
GXSetClipMode(GX_CLIP_ENABLE);
J3DShape::resetVcdVatCache();
}
@@ -6199,6 +6203,8 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) {
f32 sp60 = fabsf(cM_ssin(g_Counter.mCounter0 * 215));
if (evil_packet != NULL) {
+ IF_DUSK(GXPushDebugGroup("dKyr_evil_draw"));
+
j3dSys.reinitGX();
if (dComIfGd_getView() != NULL) {
MTXInverse(dComIfGd_getView()->viewMtxNoTrans, camMtx);
@@ -6231,8 +6237,8 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) {
GXLoadPosMtxImm(drawMtx, GX_PNMTX0);
GXSetCurrentMtx(GX_PNMTX0);
- GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_CLR_RGBA, GX_F32, 0);
- GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_CLR_RGBA, GX_RGBA4, 8);
+ GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0);
+ GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_RGBA4, 8);
GXClearVtxDesc();
GXSetVtxDesc(GX_VA_POS, GX_DIRECT);
GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT);
@@ -6255,6 +6261,19 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) {
GXSetClipMode(GX_CLIP_DISABLE);
GXSetNumIndStages(0);
+#if TARGET_PC
+ // move color_reg0 to vtx for perf
+ GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0);
+ GXSetVtxDesc(GX_VA_CLR0, GX_DIRECT);
+ GXSetNumChans(1);
+ GXSetChanCtrl(GX_COLOR0A0, GX_FALSE, GX_SRC_REG, GX_SRC_VTX, 0, GX_DF_NONE, GX_AF_NONE);
+ GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0);
+ GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_C1, GX_CC_RASC, GX_CC_TEXC, GX_CC_ZERO);
+ GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
+ GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_TEXA, GX_CA_RASA, GX_CA_ZERO);
+ GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
+#endif
+
dComIfG_Ccsp()->PrepareMass();
for (int i = 0; i < g_env_light.field_0x1054; i++) {
@@ -6373,7 +6392,7 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) {
color_reg0.b = (115.0f * sp28) + (15.0f * fabsf(sp2C - sp64));
}
- GXSetTevColor(GX_TEVREG0, color_reg0);
+ IF_NOT_DUSK(GXSetTevColor(GX_TEVREG0, color_reg0));
GXSetTevColor(GX_TEVREG1, color_reg1);
spC8 = spA4;
@@ -6412,12 +6431,16 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) {
GXBegin(GX_QUADS, GX_VTXFMT0, 4);
GXPosition3f32(pos[0].x, pos[0].y, pos[0].z);
+ IF_DUSK(GXColor4u8(color_reg0.r, color_reg0.g, color_reg0.b, color_reg0.a));
GXTexCoord2s16(0, 0);
GXPosition3f32(pos[1].x, pos[1].y, pos[1].z);
+ IF_DUSK(GXColor4u8(color_reg0.r, color_reg0.g, color_reg0.b, color_reg0.a));
GXTexCoord2s16(0xFF, 0);
GXPosition3f32(pos[2].x, pos[2].y, pos[2].z);
+ IF_DUSK(GXColor4u8(color_reg0.r, color_reg0.g, color_reg0.b, color_reg0.a));
GXTexCoord2s16(0xFF, 0xFF);
GXPosition3f32(pos[3].x, pos[3].y, pos[3].z);
+ IF_DUSK(GXColor4u8(color_reg0.r, color_reg0.g, color_reg0.b, color_reg0.a));
GXTexCoord2s16(0, 0xFF);
GXEnd();
}
@@ -6425,6 +6448,8 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) {
}
}
+ IF_DUSK(GXPopDebugGroup());
+
J3DShape::resetVcdVatCache();
GXSetClipMode(GX_CLIP_ENABLE);
diff --git a/src/d/d_map.cpp b/src/d/d_map.cpp
index 7dc5187a88..d4e5a98888 100644
--- a/src/d/d_map.cpp
+++ b/src/d/d_map.cpp
@@ -13,6 +13,9 @@
#include "SSystem/SComponent/c_math.h"
#include "d/actor/d_a_player.h"
#include "d/d_com_inf_game.h"
+#if TARGET_PC
+#include
+#endif
#include
#if DEBUG
@@ -539,17 +542,14 @@ void renderingAmap_c::rendering(dDrawPath_c::poly_class const* i_poly) {
}
}
-/* Enabling the following definition will modify the following function to
- * make the map look worse for extra speed in the emulator, especially in large
- * areas such as hyrule field.
- */
-#define HYRULE_FIELD_SPEEDHACK
bool renderingAmap_c::isDrawOutSideTrim() {
bool rt = false;
- #ifdef HYRULE_FIELD_SPEEDHACK
- return 0;
+ #if TARGET_PC
+ if (!dusk::getSettings().game.enableMapBackground) {
+ return 0;
+ }
#endif
if (getDispType() == 0 || getDispType() == 4 || getDispType() == 3 || getDispType() == 2 ||
@@ -1218,6 +1218,9 @@ void dMap_c::changeTextureSize(int param_1, int param_2, int param_3) {
void dMap_c::_remove() {
if (mImage_p != NULL) {
+#if TARGET_PC
+ GXDestroyCopyTex(mImage_p);
+#endif
JKR_DELETE_ARRAY(mImage_p);
mImage_p = NULL;
}
diff --git a/src/d/d_map_path.cpp b/src/d/d_map_path.cpp
index 8288a2ef9f..eca620ef98 100644
--- a/src/d/d_map_path.cpp
+++ b/src/d/d_map_path.cpp
@@ -16,6 +16,7 @@
#ifdef TARGET_PC
constexpr u16 kMapResolutionMultiplier = 4;
+constexpr u16 kMapCircleSize = 16 * kMapResolutionMultiplier;
#endif
void dMpath_n::dTexObjAggregate_c::create() {
@@ -32,6 +33,48 @@ void dMpath_n::dTexObjAggregate_c::create() {
JUT_ASSERT(74, image->magFilter == GX_NEAR);
mDoLib_setResTimgObj(image, mp_texObj[lp1], 0, NULL);
}
+
+#if TARGET_PC
+ auto hqCircle = JKR_NEW TGXTexObj();
+
+ static bool hqCircleDrawn = false;
+ static u8 hqCircleData[kMapCircleSize * kMapCircleSize];
+
+ if (!hqCircleDrawn) {
+ const auto center = kMapCircleSize / 2.0f;
+ const auto radiusSq = center * center;
+ const auto blocksAcross = kMapCircleSize >> 3;
+ const auto totalPixels = sizeof(hqCircleData);
+
+ for (size_t i = 0; i < totalPixels; 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;
+
+ const auto dx = (x + 0.5f) - center;
+ const auto dy = (y + 0.5f) - center;
+
+ // 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;
+ }
+
+ 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;
+#endif
}
void dMpath_n::dTexObjAggregate_c::remove() {
@@ -497,12 +540,6 @@ void dRenderingFDAmap_c::postRenderingMap() {
dMpath_n::dTexObjAggregate_c dMpath_n::m_texObjAgg;
-/* Enabling the following definition will modify the following function to
- * make the map look worse for extra speed in the emulator, especially in large
- * areas such as hyrule field.
- */
-#define HYRULE_FIELD_SPEEDHACK
-
void dRenderingFDAmap_c::renderingDecoration(dDrawPath_c::line_class const* p_line) {
s32 width = getDecorationLineWidth(p_line->field_0x1);
if (width <= 0) {
@@ -527,8 +564,32 @@ void dRenderingFDAmap_c::renderingDecoration(dDrawPath_c::line_class const* p_li
lineColor.r = lineColor.r - 4;
GXSetTevColor(GX_TEVREG1, lineColor);
+#if TARGET_PC
+ GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_C0);
+ GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
+ GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_KONST);
+ GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
+ GXBegin(GX_LINESTRIP, GX_VTXFMT0, 2 * (data_num - 1));
+ for (int i = 0; i < data_num - 1; i++) {
+ GXPosition1x16(data_p[i]);
+ GXTexCoord2f32(0, 0);
+ GXPosition1x16(data_p[i + 1]);
+ GXTexCoord2f32(0, 0);
+ }
+ GXEnd();
+
+ GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_KONST, GX_CC_TEXC, GX_CC_C1);
+ GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
+ GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_TEXA);
+ GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
+ GXBegin(GX_POINTS, GX_VTXFMT0, data_num);
+ for (int i = 0; i < data_num; i++) {
+ GXPosition1x16(data_p[i]);
+ GXTexCoord2f32(0, 0);
+ }
+ GXEnd();
+#else
for (int i = 0; i < data_num; i++) {
-#ifndef HYRULE_FIELD_SPEEDHACK
if (i < data_num - 1) {
GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_C0);
GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE,
@@ -547,7 +608,6 @@ void dRenderingFDAmap_c::renderingDecoration(dDrawPath_c::line_class const* p_li
GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_TEXA);
GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV);
-#endif
GXBegin(GX_POINTS, GX_VTXFMT0, 1);
GXPosition1x16(data_p[0]);
@@ -555,6 +615,7 @@ void dRenderingFDAmap_c::renderingDecoration(dDrawPath_c::line_class const* p_li
GXEnd();
data_p++;
}
+#endif
setTevSettingNonTextureDirectColor();
GXClearVtxDesc();
diff --git a/src/d/d_menu_dmap.cpp b/src/d/d_menu_dmap.cpp
index bb557efdac..8ef7fadd4b 100644
--- a/src/d/d_menu_dmap.cpp
+++ b/src/d/d_menu_dmap.cpp
@@ -856,7 +856,46 @@ void dMenu_DmapBg_c::decGoldFrameAlphaRate() {
setGoldFrameAlphaRate(rate);
}
+void dMenu_DmapBg_c::dMapBgWide() {
+ // Scale Base HUD
+ mBaseScreen->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f);
+ mBaseScreen->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f);
+
+ // Boss Key, Compass & Map icons
+ mBaseScreen->search(MULTI_CHAR('key_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
+ mBaseScreen->search(MULTI_CHAR('con_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
+ mBaseScreen->search(MULTI_CHAR('map_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
+
+ // Text Header
+ mBaseScreen->search(MULTI_CHAR('t_t00'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
+ mBaseScreen->search(MULTI_CHAR('f_t_00'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
+
+ // C Button
+ mBaseScreen->search(MULTI_CHAR('c_btn2'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
+
+ // Scale Buttons HUD
+ mButtonScreen->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f);
+ mButtonScreen->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f);
+
+ // Buttons
+ mButtonScreen->search(MULTI_CHAR('cont_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
+
+ // C Button
+ mButtonScreen->search(MULTI_CHAR('c_btn'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
+ mButtonScreen->search(MULTI_CHAR('c_text_s'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
+ mButtonScreen->search(MULTI_CHAR('c_text'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
+ mButtonScreen->search(MULTI_CHAR('f_text_s'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
+ mButtonScreen->search(MULTI_CHAR('f_text'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
+
+ // Decorations
+ mButtonScreen->search(MULTI_CHAR('kazari_n'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f);
+}
+
void dMenu_DmapBg_c::draw() {
+ #if TARGET_PC
+ dMapBgWide();
+ #endif
+
u32 scissor_left;
u32 scissor_top;
u32 scissor_width;
diff --git a/src/d/d_menu_dmap_map.cpp b/src/d/d_menu_dmap_map.cpp
index 8971b2630d..9100a7d63a 100644
--- a/src/d/d_menu_dmap_map.cpp
+++ b/src/d/d_menu_dmap_map.cpp
@@ -11,6 +11,9 @@
#include "d/d_menu_dmap_map.h"
#include "f_op/f_op_msg_mng.h"
#include "m_Do/m_Do_graphic.h"
+#if TARGET_PC
+#include
+#endif
struct dMdm_HIO_prm_res_dst_s {
static void* m_res;
@@ -291,6 +294,9 @@ void dMenu_DmapMap_c::_create(u16 param_0, u16 param_1, u16 param_2, u16 param_3
void dMenu_DmapMap_c::_delete() {
for (int i = 0; i < 2; i++) {
if (mMapImage_p[i] != NULL) {
+#if TARGET_PC
+ GXDestroyCopyTex(mMapImage_p[i]);
+#endif
JKR_DELETE_ARRAY(mMapImage_p[i]);
}
diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp
index 8692156daf..ea9912998b 100644
--- a/src/d/d_menu_fmap2D.cpp
+++ b/src/d/d_menu_fmap2D.cpp
@@ -20,6 +20,15 @@
#include "dusk/frame_interpolation.h"
#include
+#if TARGET_PC
+void dMenu_Fmap2DBack_c::fMapBackWide() {
+ mpBaseScreen->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f);
+ mpBaseScreen->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f);
+ mpBackScreen->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f);
+ mpBackScreen->translate(mDoGph_gInf_c::getSafeMinXF(), 0.0f);
+}
+#endif
+
dMenu_Fmap2DBack_c::dMenu_Fmap2DBack_c() {
dMeter2Info_setMapDrugFlag(0);
@@ -267,6 +276,10 @@ dMenu_Fmap2DBack_c::~dMenu_Fmap2DBack_c() {
}
void dMenu_Fmap2DBack_c::draw() {
+ #if TARGET_PC
+ fMapBackWide();
+ #endif
+
calcBlink();
J2DGrafContext* grafPort = dComIfGp_getCurrentGrafPort();
@@ -1199,7 +1212,7 @@ f32 dMenu_Fmap2DBack_c::getMapScissorAreaSizeX() {
}
f32 dMenu_Fmap2DBack_c::getMapScissorAreaSizeRealX() {
-#if PLATFORM_GCN && !TARGET_PC
+#if PLATFORM_GCN
return getMapScissorAreaSizeX();
#else
return getMapScissorAreaSizeX() * mDoGph_gInf_c::getScale();
@@ -1407,6 +1420,11 @@ void dMenu_Fmap2DBack_c::stageTextureDraw() {
mpSpotTexture->setAlpha(mAlphaRate * 255.0f * field_0xfa8 * mSpotTextureFadeAlpha);
}
+#if TARGET_PC
+ JUTPalette* pPalette = mpSpotTexture->getTexture(0)->getPalette();
+ pPalette->dataUploaded();
+#endif
+
mpSpotTexture->draw(mTransX + getMapScissorAreaLX(), mTransZ + getMapScissorAreaLY(),
getMapScissorAreaSizeRealX(), getMapScissorAreaSizeRealY(), false, false,
false);
@@ -2179,6 +2197,17 @@ void dMenu_Fmap2DBack_c::setArrowPosAxis(f32 i_posX, f32 i_posZ) {
control_ypos = 0.0f;
}
+#if TARGET_PC
+void dMenu_Fmap2DTop_c::fMapTopWide() {
+ mpTitleScreen->search(MULTI_CHAR('spot0_n'))->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f);
+ mpTitleScreen->search(MULTI_CHAR('spot2_n'))->scale(mDoGph_gInf_c::hudAspectScaleUp, 1.0f);
+ mpTitleScreen->search(MULTI_CHAR('name_n'))->translate(mDoGph_gInf_c::ScaleHUDXLeft(-243.0f), -169.0f);
+ mpTitleScreen->search(MULTI_CHAR('sub_n_n'))->translate(mDoGph_gInf_c::ScaleHUDXLeft(-80.0f), -154.0f);
+ mpTitleScreen->search(MULTI_CHAR('btn_i_n'))->translate(mDoGph_gInf_c::ScaleHUDXLeft(-241.0f), 177.0f);
+ mpTitleScreen->search(MULTI_CHAR('cont_n'))->translate(mDoGph_gInf_c::ScaleHUDXRight(515.0f), 83.0f);
+}
+#endif
+
dMenu_Fmap2DTop_c::dMenu_Fmap2DTop_c(JKRExpHeap* i_heap, STControl* i_stick) {
mpHeap = i_heap;
mTransX = 0.0f;
@@ -2572,6 +2601,10 @@ void dMenu_Fmap2DTop_c::setAllAlphaRate(f32 i_rate, bool i_init) {
}
void dMenu_Fmap2DTop_c::draw() {
+ #if TARGET_PC
+ fMapTopWide();
+ #endif
+
u32 scissor_left, scissor_top, scissor_width, scissor_height;
J2DOrthoGraph* ctx = static_cast(dComIfGp_getCurrentGrafPort());
ctx->setup2D();
diff --git a/src/d/d_menu_fmap_map.cpp b/src/d/d_menu_fmap_map.cpp
index 3d1a9e2523..c500aff0be 100644
--- a/src/d/d_menu_fmap_map.cpp
+++ b/src/d/d_menu_fmap_map.cpp
@@ -8,6 +8,9 @@
#include "d/d_debug_viewer.h"
#include "d/d_menu_fmap_map.h"
#include "m_Do/m_Do_graphic.h"
+#if TARGET_PC
+#include
+#endif
#include
static u8 twoValueLineInterpolation(u8 i_value1, u8 i_value2, f32 i_param) {
@@ -494,6 +497,9 @@ void dMenu_FmapMap_c::_delete() {
mResTIMG = NULL;
}
if (mMapImage_p != NULL) {
+#if TARGET_PC
+ GXDestroyCopyTex(mMapImage_p);
+#endif
JKR_DELETE_ARRAY(mMapImage_p);
mMapImage_p = NULL;
}
diff --git a/src/d/d_menu_letter.cpp b/src/d/d_menu_letter.cpp
index a0155ef6b4..589b2553c0 100644
--- a/src/d/d_menu_letter.cpp
+++ b/src/d/d_menu_letter.cpp
@@ -17,6 +17,10 @@
#include "d/d_msg_scrn_arrow.h"
#include "d/d_lib.h"
+#ifdef TARGET_PC
+#include "dusk/achievements.h"
+#endif
+
#if VERSION == VERSION_GCN_JPN
#define D_MENU_LETTER_LINE_MAX 9
#else
@@ -514,6 +518,10 @@ void dMenu_Letter_c::read_open_init() {
setAButtonString(0);
setBButtonString(0);
mpBlackTex->setAlpha(0);
+
+ #ifdef TARGET_PC
+ dusk::AchievementSystem::get().signal("open_letter");
+ #endif
}
void dMenu_Letter_c::read_open_move() {
diff --git a/src/d/d_meter2.cpp b/src/d/d_meter2.cpp
index 5bd321e7a8..fa6774d0a5 100644
--- a/src/d/d_meter2.cpp
+++ b/src/d/d_meter2.cpp
@@ -316,6 +316,12 @@ int dMeter2_c::_execute() {
}
int dMeter2_c::_draw() {
+ #if TARGET_PC
+ if (dusk::getSettings().game.disableMainHUD) {
+ return 1;
+ }
+ #endif
+
if (mpMap != NULL) {
mpMap->_draw();
}
@@ -424,12 +430,6 @@ void dMeter2_c::setLifeZero() {
void dMeter2_c::checkStatus() {
mStatus = 0;
- #if TARGET_PC
- if (dusk::getSettings().game.disableMainHUD) {
- mStatus |= 0xF0000000;
- }
- #endif
-
field_0x12c = field_0x128;
field_0x128 = daPy_py_c::checkNowWolf();
diff --git a/src/d/d_meter_HIO.cpp b/src/d/d_meter_HIO.cpp
index 2df28b2655..584a2852f6 100644
--- a/src/d/d_meter_HIO.cpp
+++ b/src/d/d_meter_HIO.cpp
@@ -2306,6 +2306,10 @@ void dMeter_drawHIO_c::updateOnWide() {
// River Canoe Minigame
g_drawHIO.mMiniGame.mCounterPosX[1] = mDoGph_gInf_c::ScaleHUDXRight(g_drawHIO.mMiniGame.mCounterPosX[1]);
g_drawHIO.mMiniGame.mIconPosX[1] = mDoGph_gInf_c::ScaleHUDXRight(g_drawHIO.mMiniGame.mIconPosX[1]);
+
+ // Bulblin Count in Hidden Village
+ g_drawHIO.mMiniGame.mCounterPosX[2] = mDoGph_gInf_c::ScaleHUDXRight(g_drawHIO.mMiniGame.mCounterPosX[2]);
+ g_drawHIO.mMiniGame.mIconPosX[2] = mDoGph_gInf_c::ScaleHUDXRight(g_drawHIO.mMiniGame.mIconPosX[2]);
#endif
}
diff --git a/src/d/d_msg_class.cpp b/src/d/d_msg_class.cpp
index 4040eb0d7f..f67d6054e6 100644
--- a/src/d/d_msg_class.cpp
+++ b/src/d/d_msg_class.cpp
@@ -2066,7 +2066,7 @@ bool jmessage_tSequenceProcessor::do_isReady() {
case 0:
case 5:
case 6:
- if (mDoCPd_c::getTrigA(PAD_1) || field_0xb2 != 0) {
+ if (mDoCPd_c::getTrigA(PAD_1) || field_0xb2 != 0 IF_DUSK(|| (dusk::getSettings().game.instantText && mDoCPd_c::getHoldB(0)))) {
field_0xa4 = 0;
pReference->onBatchFlag();
pReference->setCharCnt(D_MSG_CLASS_CHAR_CNT_MAX);
diff --git a/src/d/d_msg_object.cpp b/src/d/d_msg_object.cpp
index ae0e3d8427..7e3be2cb08 100644
--- a/src/d/d_msg_object.cpp
+++ b/src/d/d_msg_object.cpp
@@ -32,6 +32,9 @@
#if TARGET_PC
#include "dusk/settings.h"
+#include
+#include
+#include
#endif
static void dMsgObject_addFundRaising(s16 param_0);
@@ -424,6 +427,16 @@ static void dummyStrings() {
dMsgObject_HIO_c g_MsgObject_HIO_c;
int dMsgObject_c::_execute() {
+#if TARGET_PC
+ if (dusk::getSettings().game.enableMirrorMode) {
+ // enable wii message index override
+ g_MsgObject_HIO_c.mMessageDisplay = 1;
+ } else if (!dusk::getSettings().game.enableMirrorMode && g_MsgObject_HIO_c.mMessageDisplay == 1) {
+ g_MsgObject_HIO_c.mMessageDisplay = 0;
+ }
+#endif
+
+
field_0x4c7 = 0;
if (mpTalkHeap != NULL) {
field_0x148 = mDoExt_setCurrentHeap(mpTalkHeap);
@@ -1594,7 +1607,7 @@ u8 dMsgObject_c::isSend() {
return 2;
}
} else {
- if (IF_DUSK((dusk::getSettings().game.instantText && mDoCPd_c::getHoldB(0)) ||)
+ if (IF_DUSK((dusk::getSettings().game.instantText && mDoCPd_c::getHoldB(0) && !isShopItemMessage()) ||)
mDoCPd_c::getTrigA(0) != 0 || mDoCPd_c::getTrigB(0) != 0) {
return 2;
}
@@ -1866,6 +1879,39 @@ bool dMsgObject_c::isTalkMessage() {
return true;
}
+#if TARGET_PC
+bool dMsgObject_c::isShopItemMessage() {
+
+ // Probably a better way to do this than just listing every message id, but this works for now
+ // Note: Keep contents sorted so we can use binary search
+ const auto shopMsgIds = std::to_array>({
+ {},
+ // zel_01.bmg - Seras Shop
+ {7001, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010, 7013, 7014, 7022, 7023, 7028, 7029,
+ 7044, 7045, 7053},
+ // zel_02.bmg - Kakariko Shops
+ {5181, 5182, 5251, 5253, 5254, 5256, 5258, 5259, 5653, 5654, 5656, 5660, 5661, 5664, 5665,
+ 5697, 5698, 5699, 5803, 5804, 5806, 5810, 5811, 5812, 5814, 5821, 5823, 5824, 5987, 5988,
+ 5989, 5990, 5991, 5992, 5993, 5994, 5995, 5996, 5997, 5998, 5999},
+ // zel_03.bmg - Death Mountain Shop
+ {5303, 5304, 5306, 5310, 5311, 5314, 5315, 5322, 5323, 5324, 5496, 5497, 5498, 5499},
+ // zel_04.bmg - Castle Town Shops
+ {5407, 5408, 5409, 5410, 5411, 5412, 5413, 5414, 5415, 5416, 5417, 5418, 5419, 5420, 5431,
+ 5432, 5434, 5435, 5436, 5437, 5438, 5439, 5440, 5441, 5444, 5449, 5450, 5451, 5452, 5462},
+ // zel_05.bmg - Oocca Shop
+ {9428, 9429, 9430, 9431, 9432, 9437, 9443, 9448, 9449, 9451, 9459}
+ });
+
+ u16 id = mMessageID;
+ s16 group = dMsgObject_getGroupID();
+ if (group < shopMsgIds.size()) {
+ return std::ranges::binary_search(shopMsgIds[group], id);
+ }
+ return false;
+
+}
+#endif
+
const char* dMsgObject_c::getSmellName() {
JMSMesgInfo_c* info_header_p = (JMSMesgInfo_c*)((char*)mpMsgRes + 0x20);
char* data_ptr = (char*)info_header_p + info_header_p->header.size;
diff --git a/src/d/d_s_name.cpp b/src/d/d_s_name.cpp
index 735ce4d0ca..6baa07da30 100644
--- a/src/d/d_s_name.cpp
+++ b/src/d/d_s_name.cpp
@@ -5,19 +5,20 @@
#include "d/dolzel.h" // IWYU pragma: keep
-#include "d/d_s_name.h"
#include "JSystem/JKernel/JKRExpHeap.h"
#include "d/d_com_inf_game.h"
#include "d/d_meter2_info.h"
+#include "d/d_s_name.h"
+#include "dusk/imgui/ImGuiConsole.hpp"
+#include "dusk/memory.h"
+#include "dusk/settings.h"
+#include "f_op/f_op_overlap_mng.h"
#include "f_op/f_op_scene_mng.h"
#include "m_Do/m_Do_Reset.h"
#include "m_Do/m_Do_graphic.h"
#include "m_Do/m_Do_machine.h"
-#include "m_Do/m_Do_mtx.h"
#include "m_Do/m_Do_main.h"
-#include "f_op/f_op_overlap_mng.h"
-#include "dusk/memory.h"
-#include "dusk/settings.h"
+#include "m_Do/m_Do_mtx.h"
#if TARGET_PC
#define SHOW_TV_SETTINGS_SCREEN (this->mShowTvSettingsScreen)
@@ -412,6 +413,16 @@ void dScnName_c::changeGameScene() {
dKy_clear_game_init();
dComIfGs_resetDan();
dComIfGs_setRestartRoomParam(0);
+
+#if TARGET_PC
+ if (dusk::getSettings().game.speedrunMode && dusk::getSettings().game.hideTvSettingsScreen) {
+ // start a new run on file load if a run isn't already in progress
+ if (!dusk::m_speedrunInfo.m_isRunStarted) {
+ dusk::ImGuiMenuGame::resetForSpeedrunMode();
+ dusk::m_speedrunInfo.startRun();
+ }
+ }
+#endif
}
}
diff --git a/src/d/d_s_play.cpp b/src/d/d_s_play.cpp
index 38308d0668..f0e2330a15 100644
--- a/src/d/d_s_play.cpp
+++ b/src/d/d_s_play.cpp
@@ -41,6 +41,7 @@
#if TARGET_PC
#include "dusk/memory.h"
+#include
#endif
#if DEBUG
@@ -700,6 +701,10 @@ static u8 lbl_8074CAE4;
static u32 l_sceneChangeStartTick;
#endif
+#if TARGET_PC
+static BOOL autoSaved;
+#endif
+
static int dScnPly_Execute(dScnPly_c* i_this) {
#if DEBUG
fapGm_HIO_c::startCpuTimer();
@@ -742,6 +747,15 @@ static int dScnPly_Execute(dScnPly_c* i_this) {
}
}
+ #if TARGET_PC
+ if (!dComIfGp_event_runCheck() && !fopOvlpM_IsPeek() && !dComIfG_resetToOpening(i_this) &&
+ !dComIfGp_isEnableNextStage() && autoSaved == FALSE)
+ {
+ triggerAutoSave();
+ autoSaved = TRUE;
+ }
+ #endif
+
dKy_itudemo_se();
#if DEBUG
@@ -1593,6 +1607,11 @@ static int dScnPly_Create(scene_class* i_this) {
dScnPly_c* a_this = (dScnPly_c*)i_this;
int phase_state = dComLbG_PhaseHandler(&a_this->field_0x1c4, l_method, a_this);
+
+ #if TARGET_PC
+ autoSaved = FALSE;
+ #endif
+
return phase_state;
}
diff --git a/src/d/d_save.cpp b/src/d/d_save.cpp
index 15559e6307..0708c59716 100644
--- a/src/d/d_save.cpp
+++ b/src/d/d_save.cpp
@@ -27,7 +27,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) {
@@ -345,6 +349,10 @@ void dSv_player_item_c::setItem(int i_slotNo, u8 i_itemNo) {
dComIfGp_setSelectItem(i);
}
}
+
+ #if TARGET_PC
+ triggerAutoSave();
+ #endif
}
u8 dSv_player_item_c::getItem(int i_slotNo, bool i_checkCombo) const {
diff --git a/src/dusk/OSReport.cpp b/src/dusk/OSReport.cpp
new file mode 100644
index 0000000000..a54bcf0c78
--- /dev/null
+++ b/src/dusk/OSReport.cpp
@@ -0,0 +1,118 @@
+#include
+
+#include "aurora/lib/logging.hpp"
+#include "os_report.h"
+
+aurora::Module Log("dusk::osReport");
+
+bool dusk::OSReportReallyForceEnable = false;
+
+u8 __OSReport_disable;
+
+void OSReportDisable() {
+ __OSReport_disable = true;
+}
+
+void OSReportEnable() {
+ __OSReport_disable = false;
+}
+
+static bool checkEnabled() {
+ return !__OSReport_disable || dusk::OSReportReallyForceEnable;
+}
+
+static std::string FormatToString(const char* msg, va_list list) {
+ int ret = vsnprintf(nullptr, 0, msg, list);
+ if (ret <= 0) {
+ return {};
+ }
+ ++ret;
+ std::unique_ptr buf(new char[ret]);
+ vsnprintf(buf.get(), ret, msg, list);
+ buf[ret - 1] = '\0';
+ return {buf.get()};
+}
+
+void OSReport(const char* fmt, ...) {
+ if (!checkEnabled()) {
+ return;
+ }
+ va_list args;
+ va_start(args, fmt);
+ const auto str = FormatToString(fmt, args);
+ va_end(args);
+
+ Log.info("{}", str);
+}
+
+void OSPanic(const char* file, int line, const char* fmt, ...) {
+ va_list args;
+ va_start(args, fmt);
+ const auto str = FormatToString(fmt, args);
+ va_end(args);
+
+ Log.fatal("[{}:{}] {}", file, line, str);
+}
+
+void OSReport_Error(const char* fmt, ...) {
+ if (!checkEnabled()) {
+ return;
+ }
+
+ va_list args;
+ va_start(args, fmt);
+ const auto str = FormatToString(fmt, args);
+ va_end(args);
+
+ Log.error("{}", str);
+}
+
+void OSReport_FatalError(const char* fmt, ...) {
+ if (!checkEnabled()) {
+ return;
+ }
+
+ va_list args;
+ va_start(args, fmt);
+ const auto str = FormatToString(fmt, args);
+ va_end(args);
+
+ Log.fatal("{}", str);
+}
+
+void OSReport_Warning(const char* fmt, ...) {
+ if (!checkEnabled()) {
+ return;
+ }
+
+ va_list args;
+ va_start(args, fmt);
+ const auto str = FormatToString(fmt, args);
+ va_end(args);
+
+ Log.warn("{}", str);
+}
+
+void OSReport_System(const char* fmt, ...) {
+ va_list args;
+ va_start(args, fmt);
+ OSVAttention(fmt, args);
+ va_end(args);
+}
+
+void OSVAttention(const char* fmt, va_list args) {
+ if (!checkEnabled()) {
+ return;
+ }
+
+ const auto str = FormatToString(fmt, args);
+
+ Log.info("{}", str);
+}
+
+void OSAttention(const char* fmt, ...) {
+ va_list args;
+ va_start(args, fmt);
+ OSVAttention(fmt, args);
+ va_end(args);
+}
\ No newline at end of file
diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp
index 071332a7c2..26a302678e 100644
--- a/src/dusk/achievements.cpp
+++ b/src/dusk/achievements.cpp
@@ -8,6 +8,7 @@
#include "d/actor/d_a_player.h"
#include "d/d_demo.h"
#include "f_pc/f_pc_name.h"
+#include "f_op/f_op_actor_mng.h"
#include
#include
@@ -46,6 +47,21 @@ std::vector AchievementSystem::makeEntries() {
},
{}
},
+ {
+ {
+ "plumm_max",
+ "Thank You Berry Much",
+ "Score 61,454 points in the Plumm minigame.",
+ AchievementCategory::Minigame,
+ false, 0, 0, false
+ },
+ [](Achievement& a, json&) {
+ if (dComIfGs_getBalloonScore() >= 61454) {
+ a.progress = 1;
+ }
+ },
+ {}
+ },
{
{
"rollgoal_8",
@@ -258,6 +274,58 @@ std::vector AchievementSystem::makeEntries() {
},
{}
},
+ {
+ {
+ "friendly_fire",
+ "Friendly Fire",
+ "Get hit by your own cannonball.",
+ AchievementCategory::Misc,
+ false, 0, 0, false
+ },
+ [](Achievement& a, json&) {
+ if (AchievementSystem::get().hasSignal("iron_ball_hit_player")) {
+ a.progress = 1;
+ }
+ },
+ {}
+ },
+ {
+ {
+ "long_jump_attack",
+ "Long Jump Attack",
+ "Travel more than 20 meters in a single jump attack before landing.",
+ AchievementCategory::Misc,
+ false, 0, 0, false
+ },
+ [](Achievement& a, json&) {
+ static bool inJump = false;
+ static float startX = 0.0f, startZ = 0.0f;
+
+ const auto* link = static_cast(daPy_getPlayerActorClass());
+ if (link == nullptr) {
+ inJump = false;
+ return;
+ }
+
+ if (!inJump) {
+ if (link->mProcID == daAlink_c::PROC_CUT_JUMP) {
+ inJump = true;
+ startX = link->current.pos.x;
+ startZ = link->current.pos.z;
+ }
+ } else if (link->mProcID == daAlink_c::PROC_CUT_JUMP_LAND) {
+ inJump = false;
+ const float dx = link->current.pos.x - startX;
+ const float dz = link->current.pos.z - startZ;
+ if (dx * dx + dz * dz >= 2000.0f * 2000.0f) {
+ a.progress = 1;
+ }
+ } else if (link->mProcID != daAlink_c::PROC_CUT_JUMP) {
+ inJump = false;
+ }
+ },
+ {}
+ },
{
{
"back_in_time",
@@ -267,18 +335,13 @@ std::vector AchievementSystem::makeEntries() {
false, 0, 0, false
},
[](Achievement& a, json&) {
- static int titleNoDemoFrames = 0;
if (fopAcM_SearchByName(fpcNm_TITLE_e) == nullptr) {
- titleNoDemoFrames = 0;
return;
}
- const auto* link = static_cast(daPy_getPlayerActorClass());
- if (link != nullptr && dDemo_c::getMode() == 0) {
- if (++titleNoDemoFrames >= 60) {
+ const auto* player = static_cast(daPy_getPlayerActorClass());
+
+ if (player != nullptr && player->mDemo.getDemoMode() == 1) {
a.progress = 1;
- }
- } else {
- titleNoDemoFrames = 0;
}
},
{}
@@ -345,6 +408,41 @@ std::vector AchievementSystem::makeEntries() {
}
},
{}
+ },
+ {
+ {
+ "email_me",
+ "Email Me",
+ "Read a letter during the Dark Beast Ganon fight.",
+ AchievementCategory::Misc,
+ false, 0, 0, false
+ },
+ [](Achievement& a, json&) {
+ void* dbgExists = fopAcM_SearchByName(fpcNm_B_MGN_e);
+ if (dbgExists && AchievementSystem::get().hasSignal("open_letter")) {
+ a.progress = 1;
+ }
+ },
+ {}
+ },
+ {
+ {
+ "heavy-hitter",
+ "Heavy Hitter",
+ "Wear the Iron Boots during the end credits.",
+ AchievementCategory::Misc,
+ false, 0, 0, false
+ },
+ [](Achievement& a, json&) {
+ const auto* link = static_cast(daPy_getPlayerActorClass());
+ if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) {
+ return;
+ }
+ if (daPy_getPlayerActorClass()->checkEquipHeavyBoots()) {
+ a.progress = 1;
+ }
+ },
+ {}
}
};
}
@@ -426,6 +524,26 @@ void AchievementSystem::clearAll() {
save();
}
+void AchievementSystem::signal(const char* key) {
+ m_signals.insert(key);
+}
+
+bool AchievementSystem::hasSignal(const char* key) const {
+ return m_signals.count(key) > 0;
+}
+
+void AchievementSystem::clearOne(const char* key) {
+ for (auto& e : m_entries) {
+ if (std::string(e.achievement.key) == key) {
+ e.achievement.progress = 0;
+ e.achievement.unlocked = false;
+ e.extra = {};
+ break;
+ }
+ }
+ save();
+}
+
void AchievementSystem::processEntry(Entry& e) {
if (e.achievement.unlocked) {
return;
@@ -458,6 +576,7 @@ void AchievementSystem::tick() {
for (auto& e : m_entries) {
processEntry(e);
}
+ m_signals.clear();
if (m_dirty) {
save();
m_dirty = false;
diff --git a/src/dusk/audio/DuskDsp.cpp b/src/dusk/audio/DuskDsp.cpp
index f576a5d0f1..697371702f 100644
--- a/src/dusk/audio/DuskDsp.cpp
+++ b/src/dusk/audio/DuskDsp.cpp
@@ -48,6 +48,20 @@ f32 dusk::audio::MasterVolume = 1.0f;
f32 dusk::audio::PrevMasterVolume = 1.0f;
bool dusk::audio::EnableReverb = true;
bool dusk::audio::DumpAudio = false;
+bool dusk::audio::EnableHrtf = false;
+f32 dusk::audio::HrtfGain = 0.5f;
+
+
+// 3dB at 5kHz.
+static constexpr f32 HRTF_LP_K = 0.75f;
+static constexpr f32 HRTF_ALLPASS_G = 0.3f;
+// Front never drops below (1 - HRTF_EXTRACT_MAX).
+static constexpr f32 HRTF_EXTRACT_MAX = 0.6f;
+
+static f32 sHrtfLp1 = 0.0f;
+static f32 sHrtfLp2 = 0.0f;
+static f32 sHrtfApIn1 = 0.0f;
+static f32 sHrtfApOut1 = 0.0f;
/**
* Validate that a DSP channel's format is actually something we know how to play.
@@ -283,6 +297,9 @@ void dusk::audio::DspRender(OutputSubframe& subframe) {
DspSubframe reverbInputR = {};
bool anyReverbInput = false;
+ DspSubframe surroundBus = {};
+ bool anySurroundInput = false;
+
for (int i = 0; i < channels.size(); i++) {
auto& channel = channels[i];
auto& channelAux = ChannelAux[i];
@@ -324,6 +341,21 @@ void dusk::audio::DspRender(OutputSubframe& subframe) {
}
}
+ if (EnableHrtf && channel.mAutoMixerBeenSet) {
+ f32 dolby = (channel.mAutoMixerPanDolby & 0xFF) / 127.0f;
+ if (dolby > 0.0f) {
+ anySurroundInput = true;
+ f32 extract = dolby * HRTF_EXTRACT_MAX;
+ f32 frontScale = 1.0f - extract;
+ for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
+ f32 mono = (channelSubframe.channels[0][j] + channelSubframe.channels[1][j]) * 0.5f;
+ surroundBus[j] += mono * extract;
+ channelSubframe.channels[0][j] *= frontScale;
+ channelSubframe.channels[1][j] *= frontScale;
+ }
+ }
+ }
+
if (DumpAudio && sChannelDumpFiles[i]) {
f32 interleaved[DSP_SUBFRAME_SIZE * 2];
for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
@@ -349,6 +381,28 @@ void dusk::audio::DspRender(OutputSubframe& subframe) {
ReverbHasTail = wetEnergy >= REVERB_ENERGY_EPSILON;
}
+ if (EnableHrtf && anySurroundInput) {
+ // Two-pole LPF: -12 dB/oct above 3 kHz
+ for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
+ sHrtfLp1 = (1.0f - HRTF_LP_K) * sHrtfLp1 + HRTF_LP_K * surroundBus[j];
+ sHrtfLp2 = (1.0f - HRTF_LP_K) * sHrtfLp2 + HRTF_LP_K * sHrtfLp1;
+ surroundBus[j] = sHrtfLp2;
+ }
+
+ // Mix into L and R
+ // L gets the filtered signal directly; R gets it allpass for mild decorrelation
+ for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) {
+ f32 s = surroundBus[j];
+
+ subframe.channels[0][j] += s * HrtfGain;
+
+ f32 r = -HRTF_ALLPASS_G * s + sHrtfApIn1 + HRTF_ALLPASS_G * sHrtfApOut1;
+ sHrtfApIn1 = s;
+ sHrtfApOut1 = r;
+ subframe.channels[1][j] += r * HrtfGain;
+ }
+ }
+
for (auto& channel : subframe.channels) {
ApplyVolume(channel, channel, PrevMasterVolume, MasterVolume);
}
diff --git a/src/dusk/audio/DuskDsp.hpp b/src/dusk/audio/DuskDsp.hpp
index 8000e627a1..cfcbfe3f46 100644
--- a/src/dusk/audio/DuskDsp.hpp
+++ b/src/dusk/audio/DuskDsp.hpp
@@ -133,4 +133,6 @@ namespace dusk::audio {
extern f32 PrevMasterVolume;
extern bool EnableReverb;
extern bool DumpAudio;
+ extern bool EnableHrtf;
+ extern f32 HrtfGain;
}
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/discord.cpp b/src/dusk/discord.cpp
new file mode 100644
index 0000000000..f20541bfa4
--- /dev/null
+++ b/src/dusk/discord.cpp
@@ -0,0 +1,867 @@
+#ifdef DUSK_DISCORD
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+
+#include "discord.hpp"
+
+#include "dusk/logging.h"
+#include "nlohmann/json.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifdef _WIN32
+#define WIN32_LEAN_AND_MEAN
+#define NOMCX
+#define NOSERVICE
+#define NOIME
+#include
+#else
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#endif
+
+namespace dusk::discord::rpc {
+namespace {
+
+using json = nlohmann::json;
+
+constexpr uint32_t kRpcVersion = 1;
+constexpr size_t kFrameHeaderSize = sizeof(uint32_t) * 2;
+constexpr size_t kMaxFramePayloadSize = 64 * 1024;
+constexpr auto kIoWait = std::chrono::milliseconds(500);
+constexpr auto kShutdownClearTimeout = std::chrono::milliseconds(200);
+constexpr auto kInitialReconnectDelay = std::chrono::milliseconds(500);
+constexpr auto kMaxReconnectDelay = std::chrono::milliseconds(60 * 1000);
+
+enum class Opcode : uint32_t {
+ Handshake = 0,
+ Frame = 1,
+ Close = 2,
+ Ping = 3,
+ Pong = 4,
+};
+
+enum class ConnectionState {
+ Disconnected,
+ SentHandshake,
+ Connected,
+};
+
+enum class DisconnectCode : int {
+ PipeClosed = 1,
+ ReadCorrupt = 2,
+ BadFrame = 3,
+};
+
+struct Frame {
+ Opcode opcode = Opcode::Frame;
+ std::string payload;
+};
+
+struct QueuedEvent {
+ enum class Type {
+ Ready,
+ Disconnected,
+ Error,
+ };
+
+ Type type = Type::Ready;
+ User user;
+ int code = 0;
+ std::string message;
+};
+
+class Backoff {
+public:
+ std::chrono::milliseconds next_delay() {
+ const auto delay = currentDelay;
+ currentDelay = std::min(currentDelay * 2, kMaxReconnectDelay);
+ return delay;
+ }
+
+ void reset() { currentDelay = kInitialReconnectDelay; }
+
+private:
+ std::chrono::milliseconds currentDelay = kInitialReconnectDelay;
+};
+
+class IpcConnection {
+public:
+ IpcConnection() = default;
+ ~IpcConnection() { close(); }
+
+ IpcConnection(const IpcConnection&) = delete;
+ IpcConnection& operator=(const IpcConnection&) = delete;
+
+ bool open() {
+#ifdef _WIN32
+ wchar_t pipeName[] = L"\\\\?\\pipe\\discord-ipc-0";
+ constexpr size_t kPipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
+
+ for (wchar_t pipeNumber = L'0'; pipeNumber <= L'9'; ++pipeNumber) {
+ pipeName[kPipeDigit] = pipeNumber;
+ pipe = CreateFileW(
+ pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
+ if (pipe != INVALID_HANDLE_VALUE) {
+ return true;
+ }
+
+ if (GetLastError() == ERROR_PIPE_BUSY && WaitNamedPipeW(pipeName, 10000)) {
+ --pipeNumber;
+ }
+ }
+ return false;
+#else
+ const auto tempPaths = get_temp_paths();
+ for (const std::string& tempPath : tempPaths) {
+ for (int pipeNumber = 0; pipeNumber < 10; ++pipeNumber) {
+ socketFd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (socketFd == -1) {
+ return false;
+ }
+
+ sockaddr_un pipeAddress{};
+ pipeAddress.sun_family = AF_UNIX;
+ const std::string socketPath =
+ tempPath + "/discord-ipc-" + std::to_string(pipeNumber);
+ if (socketPath.size() >= sizeof(pipeAddress.sun_path)) {
+ close();
+ continue;
+ }
+
+ std::strncpy(
+ pipeAddress.sun_path, socketPath.c_str(), sizeof(pipeAddress.sun_path) - 1);
+ if (connect(socketFd, reinterpret_cast(&pipeAddress),
+ sizeof(pipeAddress)) == 0)
+ {
+ fcntl(socketFd, F_SETFL, O_NONBLOCK);
+#ifdef SO_NOSIGPIPE
+ int optval = 1;
+ setsockopt(socketFd, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval));
+#endif
+ return true;
+ }
+ close();
+ }
+ }
+ return false;
+#endif
+ }
+
+ void close() {
+#ifdef _WIN32
+ if (pipe != INVALID_HANDLE_VALUE) {
+ CloseHandle(pipe);
+ pipe = INVALID_HANDLE_VALUE;
+ }
+#else
+ if (socketFd != -1) {
+ ::close(socketFd);
+ socketFd = -1;
+ }
+#endif
+ pendingRead.clear();
+ }
+
+ bool is_open() const {
+#ifdef _WIN32
+ return pipe != INVALID_HANDLE_VALUE;
+#else
+ return socketFd != -1;
+#endif
+ }
+
+ bool write_frame(const Frame& frame) {
+ std::array header{};
+ write_u32(header.data(), static_cast(frame.opcode));
+ write_u32(header.data() + sizeof(uint32_t), static_cast(frame.payload.size()));
+
+ return write_all(header.data(), header.size()) &&
+ write_all(
+ reinterpret_cast(frame.payload.data()), frame.payload.size());
+ }
+
+ enum class ReadStatus {
+ None,
+ Frame,
+ Closed,
+ Corrupt,
+ };
+
+ ReadStatus read_frame(Frame& frame) {
+ if (!read_available()) {
+ return is_open() ? ReadStatus::None : ReadStatus::Closed;
+ }
+
+ if (pendingRead.size() < kFrameHeaderSize) {
+ return ReadStatus::None;
+ }
+
+ const uint32_t payloadLength = read_u32(pendingRead.data() + sizeof(uint32_t));
+ if (payloadLength > kMaxFramePayloadSize) {
+ return ReadStatus::Corrupt;
+ }
+
+ const size_t frameLength = kFrameHeaderSize + payloadLength;
+ if (pendingRead.size() < frameLength) {
+ return ReadStatus::None;
+ }
+
+ frame.opcode = static_cast(read_u32(pendingRead.data()));
+ frame.payload.assign(
+ reinterpret_cast(pendingRead.data() + kFrameHeaderSize), payloadLength);
+ pendingRead.erase(
+ pendingRead.begin(), pendingRead.begin() + static_cast(frameLength));
+ return ReadStatus::Frame;
+ }
+
+private:
+#ifndef _WIN32
+ static std::vector get_temp_paths() {
+ std::vector paths;
+ for (const char* name : {"XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"}) {
+ if (const char* value = std::getenv(name); value && value[0] != '\0') {
+ if (std::find(paths.begin(), paths.end(), value) == paths.end()) {
+ paths.emplace_back(value);
+ }
+ }
+ }
+ if (std::find(paths.begin(), paths.end(), "/tmp") == paths.end()) {
+ paths.emplace_back("/tmp");
+ }
+ return paths;
+ }
+#endif
+
+ static void write_u32(uint8_t* out, uint32_t value) {
+ out[0] = static_cast(value & 0xff);
+ out[1] = static_cast((value >> 8) & 0xff);
+ out[2] = static_cast((value >> 16) & 0xff);
+ out[3] = static_cast((value >> 24) & 0xff);
+ }
+
+ static uint32_t read_u32(const uint8_t* in) {
+ return static_cast(in[0]) | (static_cast(in[1]) << 8) |
+ (static_cast(in[2]) << 16) | (static_cast(in[3]) << 24);
+ }
+
+ bool write_all(const uint8_t* data, size_t length) {
+ size_t written = 0;
+ while (written < length) {
+#ifdef _WIN32
+ DWORD bytesWritten = 0;
+ if (WriteFile(pipe, data + written, static_cast(length - written), &bytesWritten,
+ nullptr) == FALSE ||
+ bytesWritten == 0)
+ {
+ close();
+ return false;
+ }
+ written += bytesWritten;
+#else
+#ifdef MSG_NOSIGNAL
+ constexpr int kMsgFlags = MSG_NOSIGNAL;
+#else
+ constexpr int kMsgFlags = 0;
+#endif
+ const ssize_t bytesWritten =
+ send(socketFd, data + written, length - written, kMsgFlags);
+ if (bytesWritten < 0) {
+ if (errno == EAGAIN || errno == EWOULDBLOCK) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+ continue;
+ }
+ close();
+ return false;
+ }
+ if (bytesWritten == 0) {
+ close();
+ return false;
+ }
+ written += static_cast(bytesWritten);
+#endif
+ }
+ return true;
+ }
+
+ bool read_available() {
+ std::array buffer{};
+ bool readAny = false;
+
+ for (;;) {
+#ifdef _WIN32
+ DWORD bytesAvailable = 0;
+ if (PeekNamedPipe(pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr) == FALSE) {
+ close();
+ return readAny;
+ }
+ if (bytesAvailable == 0) {
+ return readAny;
+ }
+
+ const DWORD bytesToRead =
+ std::min(bytesAvailable, static_cast(buffer.size()));
+ DWORD bytesRead = 0;
+ if (ReadFile(pipe, buffer.data(), bytesToRead, &bytesRead, nullptr) == FALSE) {
+ close();
+ return readAny;
+ }
+ if (bytesRead == 0) {
+ close();
+ return readAny;
+ }
+ pendingRead.insert(pendingRead.end(), buffer.begin(), buffer.begin() + bytesRead);
+ readAny = true;
+#else
+#ifdef MSG_NOSIGNAL
+ constexpr int kMsgFlags = MSG_NOSIGNAL;
+#else
+ constexpr int kMsgFlags = 0;
+#endif
+ const ssize_t bytesRead = recv(socketFd, buffer.data(), buffer.size(), kMsgFlags);
+ if (bytesRead < 0) {
+ if (errno == EAGAIN || errno == EWOULDBLOCK) {
+ return readAny;
+ }
+ close();
+ return readAny;
+ }
+ if (bytesRead == 0) {
+ close();
+ return readAny;
+ }
+ pendingRead.insert(pendingRead.end(), buffer.begin(), buffer.begin() + bytesRead);
+ readAny = true;
+#endif
+ }
+ }
+
+#ifdef _WIN32
+ HANDLE pipe = INVALID_HANDLE_VALUE;
+#else
+ int socketFd = -1;
+#endif
+ std::vector pendingRead;
+};
+
+int current_process_id() {
+#ifdef _WIN32
+ return static_cast(GetCurrentProcessId());
+#else
+ return static_cast(getpid());
+#endif
+}
+
+std::string next_nonce() {
+ static std::atomic_uint64_t sNonce{1};
+ return std::to_string(sNonce.fetch_add(1));
+}
+
+const json* find_member(const json& object, const char* key) {
+ if (!object.is_object()) {
+ return nullptr;
+ }
+
+ const auto member = object.find(key);
+ if (member == object.end()) {
+ return nullptr;
+ }
+ return &*member;
+}
+
+std::string json_string_member(const json& object, const char* key) {
+ const json* member = find_member(object, key);
+ if (!member || !member->is_string()) {
+ return {};
+ }
+ return member->get();
+}
+
+int json_int_member(const json& object, const char* key, int defaultValue) {
+ const json* member = find_member(object, key);
+ if (!member || !member->is_number_integer()) {
+ return defaultValue;
+ }
+
+ try {
+ return member->get();
+ } catch (const json::exception&) {
+ return defaultValue;
+ }
+}
+
+json make_presence_activity(const Presence& presence) {
+ json activity = json::object();
+
+ if (!presence.state.empty()) {
+ activity["state"] = presence.state;
+ }
+ if (!presence.details.empty()) {
+ activity["details"] = presence.details;
+ }
+ if (presence.startTimestamp != 0) {
+ activity["timestamps"] = {{"start", presence.startTimestamp}};
+ }
+ if (!presence.largeImageKey.empty() || !presence.largeImageText.empty()) {
+ json assets = json::object();
+ if (!presence.largeImageKey.empty()) {
+ assets["large_image"] = presence.largeImageKey;
+ }
+ if (!presence.largeImageText.empty()) {
+ assets["large_text"] = presence.largeImageText;
+ }
+ activity["assets"] = std::move(assets);
+ }
+
+ return activity;
+}
+
+Frame make_handshake_frame(std::string_view applicationId) {
+ return {
+ Opcode::Handshake,
+ json{
+ {"v", kRpcVersion},
+ {"client_id", std::string(applicationId)},
+ }
+ .dump(),
+ };
+}
+
+Frame make_set_activity_frame(std::string nonce, int pid, std::optional presence) {
+ json args = {{"pid", pid}};
+ if (presence) {
+ args["activity"] = make_presence_activity(*presence);
+ } else {
+ args["activity"] = nullptr;
+ }
+
+ return {
+ Opcode::Frame,
+ json{
+ {"cmd", "SET_ACTIVITY"},
+ {"nonce", std::move(nonce)},
+ {"args", std::move(args)},
+ }
+ .dump(),
+ };
+}
+
+class Client {
+public:
+ void initialize(std::string applicationId, EventHandlers handlers) {
+ shutdown();
+
+ {
+ std::lock_guard lock(mutex);
+ this->applicationId = std::move(applicationId);
+ this->handlers = std::move(handlers);
+ shouldRun = true;
+ queuedPresence.reset();
+ hasQueuedPresence = false;
+ clearRequested = false;
+ sentInitialConnectLog = false;
+ }
+
+ ioThread = std::thread([this] { io_loop(); });
+ }
+
+ void run_callbacks() {
+ std::deque events;
+ EventHandlers localHandlers;
+ {
+ std::lock_guard lock(mutex);
+ events.swap(queuedEvents);
+ localHandlers = handlers;
+ }
+
+ for (const QueuedEvent& event : events) {
+ switch (event.type) {
+ case QueuedEvent::Type::Ready:
+ if (localHandlers.ready) {
+ localHandlers.ready(event.user);
+ }
+ break;
+ case QueuedEvent::Type::Disconnected:
+ if (localHandlers.disconnected) {
+ localHandlers.disconnected(event.code, event.message);
+ }
+ break;
+ case QueuedEvent::Type::Error:
+ if (localHandlers.error) {
+ localHandlers.error(event.code, event.message);
+ }
+ break;
+ }
+ }
+ }
+
+ void update_presence(Presence presence) {
+ {
+ std::lock_guard lock(mutex);
+ if (!shouldRun) {
+ return;
+ }
+ queuedPresence = std::move(presence);
+ hasQueuedPresence = true;
+ }
+ cv.notify_all();
+ }
+
+ void clear_presence() {
+ {
+ std::lock_guard lock(mutex);
+ if (!shouldRun) {
+ return;
+ }
+ queuedPresence.reset();
+ hasQueuedPresence = false;
+ clearRequested = true;
+ }
+ cv.notify_all();
+ }
+
+ void shutdown() {
+ {
+ std::lock_guard lock(mutex);
+ if (!shouldRun && !ioThread.joinable()) {
+ return;
+ }
+ shouldRun = false;
+ clearRequested = true;
+ }
+ cv.notify_all();
+
+ if (ioThread.joinable()) {
+ ioThread.join();
+ }
+
+ std::lock_guard lock(mutex);
+ queuedPresence.reset();
+ hasQueuedPresence = false;
+ clearRequested = false;
+ queuedEvents.clear();
+ handlers = {};
+ applicationId.clear();
+ }
+
+private:
+ void io_loop() {
+ IpcConnection connection;
+ ConnectionState state = ConnectionState::Disconnected;
+ Backoff reconnectBackoff;
+ auto nextConnect = std::chrono::steady_clock::now();
+ const int pid = current_process_id();
+ std::string localApplicationId;
+
+ for (;;) {
+ {
+ std::unique_lock lock(mutex);
+ if (!shouldRun) {
+ break;
+ }
+ localApplicationId = applicationId;
+ }
+
+ const auto now = std::chrono::steady_clock::now();
+ if (state == ConnectionState::Disconnected && now >= nextConnect) {
+ if (connection.open()) {
+ if (connection.write_frame(make_handshake_frame(localApplicationId))) {
+ state = ConnectionState::SentHandshake;
+ } else {
+ connection.close();
+ }
+ }
+
+ if (state == ConnectionState::Disconnected) {
+ log_waiting_for_discord_once();
+ nextConnect = now + reconnectBackoff.next_delay();
+ }
+ }
+
+ if (state != ConnectionState::Disconnected) {
+ process_reads(connection, state, reconnectBackoff, nextConnect);
+ }
+
+ if (state == ConnectionState::Connected) {
+ flush_pending_presence(connection, pid);
+ }
+
+ std::unique_lock lock(mutex);
+ if (!shouldRun) {
+ break;
+ }
+ cv.wait_for(lock, kIoWait);
+ }
+
+ flush_shutdown_clear(connection, state, pid);
+ connection.close();
+ }
+
+ void process_reads(IpcConnection& connection, ConnectionState& state, Backoff& reconnectBackoff,
+ std::chrono::steady_clock::time_point& nextConnect) {
+ for (;;) {
+ Frame frame;
+ const auto status = connection.read_frame(frame);
+ if (status == IpcConnection::ReadStatus::None) {
+ return;
+ }
+ if (status == IpcConnection::ReadStatus::Closed) {
+ handle_disconnect(connection, state, reconnectBackoff, nextConnect,
+ static_cast(DisconnectCode::PipeClosed), "Pipe closed");
+ return;
+ }
+ if (status == IpcConnection::ReadStatus::Corrupt) {
+ handle_disconnect(connection, state, reconnectBackoff, nextConnect,
+ static_cast(DisconnectCode::ReadCorrupt), "Oversized Discord IPC frame");
+ return;
+ }
+
+ switch (frame.opcode) {
+ case Opcode::Frame:
+ process_json_frame(frame.payload, state, reconnectBackoff);
+ break;
+ case Opcode::Close:
+ process_close_frame(
+ frame.payload, connection, state, reconnectBackoff, nextConnect);
+ return;
+ case Opcode::Ping:
+ connection.write_frame({Opcode::Pong, frame.payload});
+ break;
+ case Opcode::Pong:
+ break;
+ case Opcode::Handshake:
+ default:
+ handle_disconnect(connection, state, reconnectBackoff, nextConnect,
+ static_cast(DisconnectCode::BadFrame), "Bad Discord IPC frame");
+ return;
+ }
+ }
+ }
+
+ void process_json_frame(
+ const std::string& payload, ConnectionState& state, Backoff& reconnectBackoff) {
+ json message;
+ try {
+ message = json::parse(payload);
+ } catch (const json::parse_error&) {
+ enqueue_error(
+ static_cast(DisconnectCode::ReadCorrupt), "Invalid Discord IPC JSON");
+ return;
+ }
+
+ const std::string cmd = json_string_member(message, "cmd");
+ const std::string evt = json_string_member(message, "evt");
+
+ if (state == ConnectionState::SentHandshake && cmd == "DISPATCH" && evt == "READY") {
+ state = ConnectionState::Connected;
+ reconnectBackoff.reset();
+ enqueue_ready(message);
+ return;
+ }
+
+ if (evt == "ERROR") {
+ const json* data = find_member(message, "data");
+ enqueue_error(data ? json_int_member(*data, "code", 0) : 0,
+ data ? json_string_member(*data, "message") : std::string{});
+ }
+ }
+
+ void process_close_frame(const std::string& payload, IpcConnection& connection,
+ ConnectionState& state, Backoff& reconnectBackoff,
+ std::chrono::steady_clock::time_point& nextConnect) {
+ int code = static_cast(DisconnectCode::PipeClosed);
+ std::string message = "Discord closed IPC connection";
+
+ try {
+ const json closePayload = json::parse(payload);
+ code = json_int_member(closePayload, "code", code);
+ const std::string closeMessage = json_string_member(closePayload, "message");
+ if (!closeMessage.empty()) {
+ message = closeMessage;
+ }
+ } catch (const json::exception&) {
+ }
+
+ handle_disconnect(connection, state, reconnectBackoff, nextConnect, code, message);
+ }
+
+ void handle_disconnect(IpcConnection& connection, ConnectionState& state,
+ Backoff& reconnectBackoff, std::chrono::steady_clock::time_point& nextConnect, int code,
+ std::string_view message) {
+ const bool wasConnected =
+ state == ConnectionState::Connected || state == ConnectionState::SentHandshake;
+ connection.close();
+ state = ConnectionState::Disconnected;
+ nextConnect = std::chrono::steady_clock::now() + reconnectBackoff.next_delay();
+ if (wasConnected) {
+ enqueue_disconnected(code, message);
+ }
+ }
+
+ void flush_pending_presence(IpcConnection& connection, int pid) {
+ std::optional presence;
+ bool shouldClear = false;
+ {
+ std::lock_guard lock(mutex);
+ if (hasQueuedPresence) {
+ presence = queuedPresence;
+ hasQueuedPresence = false;
+ } else if (clearRequested) {
+ shouldClear = true;
+ clearRequested = false;
+ }
+ }
+
+ if (presence) {
+ if (!connection.write_frame(
+ make_set_activity_frame(next_nonce(), pid, std::move(presence))))
+ {
+ requeue_presence();
+ }
+ } else if (shouldClear) {
+ connection.write_frame(make_set_activity_frame(next_nonce(), pid, std::nullopt));
+ }
+ }
+
+ void flush_shutdown_clear(IpcConnection& connection, ConnectionState state, int pid) {
+ if (state != ConnectionState::Connected || !connection.is_open()) {
+ return;
+ }
+
+ connection.write_frame(make_set_activity_frame(next_nonce(), pid, std::nullopt));
+ const auto deadline = std::chrono::steady_clock::now() + kShutdownClearTimeout;
+ while (std::chrono::steady_clock::now() < deadline) {
+ Frame frame;
+ const auto status = connection.read_frame(frame);
+ if (status == IpcConnection::ReadStatus::None) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(10));
+ continue;
+ }
+ if (status != IpcConnection::ReadStatus::Frame) {
+ break;
+ }
+ if (frame.opcode == Opcode::Ping) {
+ connection.write_frame({Opcode::Pong, frame.payload});
+ }
+ }
+ }
+
+ void requeue_presence() {
+ std::lock_guard lock(mutex);
+ if (queuedPresence) {
+ hasQueuedPresence = true;
+ }
+ }
+
+ void enqueue_ready(const json& readyMessage) {
+ User user;
+ const auto data = readyMessage.find("data");
+ if (data != readyMessage.end() && data->is_object()) {
+ const auto userIt = data->find("user");
+ if (userIt != data->end() && userIt->is_object()) {
+ user.id = json_string_member(*userIt, "id");
+ user.username = json_string_member(*userIt, "username");
+ user.discriminator = json_string_member(*userIt, "discriminator");
+ user.avatar = json_string_member(*userIt, "avatar");
+ }
+ }
+
+ std::lock_guard lock(mutex);
+ queuedEvents.push_back({QueuedEvent::Type::Ready, std::move(user)});
+ }
+
+ void enqueue_disconnected(int code, std::string_view message) {
+ std::lock_guard lock(mutex);
+ QueuedEvent event;
+ event.type = QueuedEvent::Type::Disconnected;
+ event.code = code;
+ event.message = message;
+ queuedEvents.push_back(std::move(event));
+ }
+
+ void enqueue_error(int code, std::string_view message) {
+ std::lock_guard lock(mutex);
+ QueuedEvent event;
+ event.type = QueuedEvent::Type::Error;
+ event.code = code;
+ event.message = message;
+ queuedEvents.push_back(std::move(event));
+ }
+
+ void log_waiting_for_discord_once() {
+ bool shouldLog = false;
+ {
+ std::lock_guard lock(mutex);
+ if (!sentInitialConnectLog) {
+ sentInitialConnectLog = true;
+ shouldLog = true;
+ }
+ }
+ if (shouldLog) {
+ DuskLog.info("Discord: Waiting for local Discord IPC");
+ }
+ }
+
+ std::mutex mutex;
+ std::condition_variable cv;
+ std::thread ioThread;
+ std::string applicationId;
+ EventHandlers handlers;
+ std::deque queuedEvents;
+ std::optional queuedPresence;
+ bool hasQueuedPresence = false;
+ bool clearRequested = false;
+ bool shouldRun = false;
+ bool sentInitialConnectLog = false;
+};
+
+Client& client() {
+ static Client sClient;
+ return sClient;
+}
+
+} // namespace
+
+void initialize(std::string applicationId, EventHandlers handlers) {
+ client().initialize(std::move(applicationId), std::move(handlers));
+}
+
+void run_callbacks() {
+ client().run_callbacks();
+}
+
+void update_presence(Presence presence) {
+ client().update_presence(std::move(presence));
+}
+
+void clear_presence() {
+ client().clear_presence();
+}
+
+void shutdown() {
+ client().shutdown();
+}
+
+} // namespace dusk::discord::rpc
+
+#endif // DUSK_DISCORD
diff --git a/src/dusk/discord.hpp b/src/dusk/discord.hpp
new file mode 100644
index 0000000000..67f54ff2f3
--- /dev/null
+++ b/src/dusk/discord.hpp
@@ -0,0 +1,41 @@
+#pragma once
+
+#ifdef DUSK_DISCORD
+
+#include
+#include
+#include
+#include
+
+namespace dusk::discord::rpc {
+
+struct User {
+ std::string id;
+ std::string username;
+ std::string discriminator;
+ std::string avatar;
+};
+
+struct Presence {
+ std::string state;
+ std::string details;
+ int64_t startTimestamp = 0;
+ std::string largeImageKey;
+ std::string largeImageText;
+};
+
+struct EventHandlers {
+ std::function ready;
+ std::function disconnected;
+ std::function error;
+};
+
+void initialize(std::string applicationId, EventHandlers handlers);
+void run_callbacks();
+void update_presence(Presence presence);
+void clear_presence();
+void shutdown();
+
+} // namespace dusk::discord::rpc
+
+#endif // DUSK_DISCORD
diff --git a/src/dusk/discord_presence.cpp b/src/dusk/discord_presence.cpp
index 3b12075c8b..6b2c7c912b 100644
--- a/src/dusk/discord_presence.cpp
+++ b/src/dusk/discord_presence.cpp
@@ -1,38 +1,39 @@
-#ifdef DUSK_DISCORD_RPC
+#ifdef DUSK_DISCORD
#include "dusk/discord_presence.hpp"
+#include "d/d_com_inf_game.h"
+#include "discord.hpp"
#include "dusk/logging.h"
#include "dusk/main.h"
#include "dusk/map_loader_definitions.h"
-#include "d/d_com_inf_game.h"
-#include "discord_rpc.h"
#include "fmt/format.h"
#include
-#include
#include
+#include
+#include
-namespace dusk {
-namespace discord {
+namespace dusk::discord {
static int64_t g_startTime = 0;
static bool g_initialized = false;
-static const char* APPLICATION_ID = "1495632471994405035";
+static constexpr const char* kApplicationId = "1495632471994405035";
-static void OnReady(const DiscordUser* user) {
- DuskLog.info("Discord: Connected as {}", user->username);
+static void on_ready(const rpc::User& user) {
+ DuskLog.info("Discord: Connected as {}", user.username);
}
-static void OnDisconnected(int errorCode, const char* message) {
+static void on_disconnected(int errorCode, std::string_view message) {
DuskLog.warn("Discord: Disconnected ({}: {})", errorCode, message);
}
-static void OnError(int errorCode, const char* message) {
+static void on_error(int errorCode, std::string_view message) {
DuskLog.warn("Discord: Error ({}: {})", errorCode, message);
}
-static const char* LookupMapName(const char* mapFile) {
- if (!mapFile || mapFile[0] == '\0') return nullptr;
+static const char* lookup_map_name(const char* mapFile) {
+ if (!mapFile || mapFile[0] == '\0')
+ return nullptr;
for (const auto& region : gameRegions) {
for (const auto& map : region.maps) {
if (map.mapFile && strcmp(mapFile, map.mapFile) == 0) {
@@ -43,80 +44,80 @@ static const char* LookupMapName(const char* mapFile) {
return nullptr;
}
-void Initialize() {
- g_startTime = static_cast(
- std::chrono::duration_cast(
- std::chrono::system_clock::now().time_since_epoch()
- ).count()
- );
+void initialize() {
+ g_startTime = std::chrono::duration_cast(
+ std::chrono::system_clock::now().time_since_epoch())
+ .count();
- DiscordEventHandlers handlers{};
- handlers.ready = OnReady;
- handlers.disconnected = OnDisconnected;
- handlers.errored = OnError;
- Discord_Initialize(APPLICATION_ID, &handlers, 0, nullptr);
+ rpc::EventHandlers handlers{};
+ handlers.ready = on_ready;
+ handlers.disconnected = on_disconnected;
+ handlers.error = on_error;
+ rpc::initialize(kApplicationId, std::move(handlers));
g_initialized = true;
DuskLog.info("Discord Rich Presence initialized");
}
-void RunCallbacks() {
- if (!g_initialized) return;
- Discord_RunCallbacks();
+void run_callbacks() {
+ if (!g_initialized)
+ return;
+ rpc::run_callbacks();
}
-void UpdatePresence() {
- if (!g_initialized) return;
+void update_presence() {
+ if (!g_initialized)
+ return;
- static auto lastUpdate = std::chrono::steady_clock::time_point{};
+ static auto sLastUpdate = std::chrono::steady_clock::time_point{};
const auto now = std::chrono::steady_clock::now();
- if (now - lastUpdate < std::chrono::seconds(15)) return;
- lastUpdate = now;
+ if (now - sLastUpdate < std::chrono::seconds(15))
+ return;
+ sLastUpdate = now;
- static std::string detailsBuf;
- static std::string stateBuf;
+ static std::string sDetailsBuf;
+ static std::string sStateBuf;
- DiscordRichPresence presence{};
+ rpc::Presence presence{};
presence.startTimestamp = g_startTime;
presence.largeImageKey = "icon";
presence.largeImageText = "Dusk";
- if (dusk::IsGameLaunched) {
+ if (IsGameLaunched) {
const char* stageName = dComIfGp_getLastPlayStageName();
// stageName is empty until a room is actually entered
if (stageName[0] != '\0') {
- const char* locationName = LookupMapName(stageName);
+ const char* locationName = lookup_map_name(stageName);
if (locationName) {
- detailsBuf = locationName;
- }
- else {
- detailsBuf = "Twilight Princess";
+ sDetailsBuf = locationName;
+ } else {
+ sDetailsBuf = "Twilight Princess";
}
- presence.details = detailsBuf.c_str();
+ presence.details = sDetailsBuf;
- stateBuf = fmt::format(FMT_STRING("{}/{} \u2665 | {} Rupees"),
+ sStateBuf = fmt::format(FMT_STRING("{}/{} \u2665 | {} Rupees"),
dComIfGs_getLife() / 4, dComIfGs_getMaxLife() / 5, dComIfGs_getRupee());
- presence.state = stateBuf.c_str();
+ presence.state = sStateBuf;
}
}
- Discord_UpdatePresence(&presence);
+ rpc::update_presence(std::move(presence));
DuskLog.debug("Discord Rich Presence sent");
}
-void Shutdown() {
- if (!g_initialized) return;
- Discord_ClearPresence();
- Discord_Shutdown();
+void shutdown() {
+ if (!g_initialized)
+ return;
+ rpc::clear_presence();
+ rpc::shutdown();
g_initialized = false;
DuskLog.info("Discord Rich Presence shut down");
}
-} // namespace discord
-} // namespace dusk
+} // namespace dusk::discord
-#endif // DUSK_DISCORD_RPC
+#endif // DUSK_DISCORD
diff --git a/src/dusk/imgui/ImGuiAchievements.cpp b/src/dusk/imgui/ImGuiAchievements.cpp
deleted file mode 100644
index 4b844b8386..0000000000
--- a/src/dusk/imgui/ImGuiAchievements.cpp
+++ /dev/null
@@ -1,228 +0,0 @@
-#include "ImGuiAchievements.hpp"
-#include "ImGuiConfig.hpp"
-#include "dusk/achievements.h"
-#include "dusk/settings.h"
-#include "fmt/format.h"
-#include "imgui.h"
-
-namespace dusk {
-
-void ImGuiAchievements::notify(std::string name) {
- if (m_notifyTimer <= 0.f) {
- m_notifyName = std::move(name);
- m_notifyTimer = NOTIFY_DURATION;
- } else {
- m_notifyQueue.push(std::move(name));
- }
-}
-
-void ImGuiAchievements::showNotification() {
- if (!getSettings().game.enableAchievementNotifications.getValue()) {
- return;
- }
- if (m_notifyTimer <= 0.f) {
- if (m_notifyQueue.empty()) {
- return;
- }
- m_notifyName = std::move(m_notifyQueue.front());
- m_notifyQueue.pop();
- m_notifyTimer = NOTIFY_DURATION;
- }
-
- m_notifyTimer -= ImGui::GetIO().DeltaTime;
-
- const float alpha = std::min({
- m_notifyTimer / NOTIFY_FADE_TIME,
- (NOTIFY_DURATION - m_notifyTimer) / NOTIFY_FADE_TIME,
- 1.0f
- });
-
- const ImGuiViewport* viewport = ImGui::GetMainViewport();
- const float padding = 12.0f;
- ImGui::SetNextWindowPos(
- ImVec2(viewport->WorkPos.x + viewport->WorkSize.x - padding, viewport->WorkPos.y + padding),
- ImGuiCond_Always, ImVec2(1.0f, 0.0f)
- );
-
- ImGui::SetNextWindowBgAlpha(alpha * 0.92f);
- ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.06f, 0.01f, alpha * 0.92f));
- ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 0.8f, 0.1f, alpha));
- ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, alpha));
- ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 2.0f);
- ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(14.0f, 10.0f));
-
- constexpr ImGuiWindowFlags flags =
- ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize |
- ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
- ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs;
-
- if (ImGui::Begin("##achievement_notify", nullptr, flags)) {
- ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.82f, 0.1f, alpha));
- ImGui::TextUnformatted("Achievement Unlocked!");
- ImGui::PopStyleColor();
- ImGui::Spacing();
- ImGui::TextUnformatted(m_notifyName.c_str());
- }
- ImGui::End();
-
- ImGui::PopStyleVar(2);
- ImGui::PopStyleColor(3);
-}
-
-void ImGuiAchievements::draw(bool& open) {
- showNotification();
-
- if (!open) {
- return;
- }
-
- ImGui::SetNextWindowSizeConstraints(ImVec2(640, 200), ImVec2(800, 900));
- ImGui::SetNextWindowSize(ImVec2(640, 480), ImGuiCond_FirstUseEver);
-
- if (!ImGui::Begin(
- "Achievements", &open,
- ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)
- )
- {
- ImGui::End();
- return;
- }
-
- const auto achievements = AchievementSystem::get().getAchievements();
-
- int unlocked = 0;
- for (const auto& a : achievements) {
- if (a.unlocked) {
- ++unlocked;
- }
- }
-
- ImGui::Text("%d / %d achievements unlocked", unlocked, (int)achievements.size());
- ImGui::SameLine();
- config::ImGuiCheckbox("Notifications", getSettings().game.enableAchievementNotifications);
- ImGui::Separator();
-
- static const struct {
- AchievementCategory cat;
- const char* label;
- ImVec4 color;
- } ACHIEVEMENT_CATEGORIES[] = {
- {AchievementCategory::Story, "Story", ImVec4(1.0f, 0.82f, 0.1f, 1.0f)},
- {AchievementCategory::Collection, "Collection", ImVec4(0.3f, 0.85f, 0.4f, 1.0f)},
- {AchievementCategory::Challenge, "Challenge", ImVec4(1.0f, 0.65f, 0.15f, 1.0f)},
- {AchievementCategory::Minigame, "Minigame", ImVec4(0.5f, 0.85f, 1.0f, 1.0f)},
- {AchievementCategory::Glitched, "Glitched", ImVec4(0.75f, 0.4f, 1.0f, 1.0f)},
- };
-
- const float footerHeight = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing();
-
- if (ImGui::BeginTabBar("##achievement_tabs", ImGuiTabBarFlags_FittingPolicyScroll)) {
- for (const auto& catInfo : ACHIEVEMENT_CATEGORIES) {
- int catTotal = 0, catUnlocked = 0;
- for (const auto& a : achievements) {
- if (a.category == catInfo.cat) {
- ++catTotal;
- if (a.unlocked) {
- ++catUnlocked;
- }
- }
- }
- if (catTotal == 0) {
- continue;
- }
-
- const std::string tabLabel = fmt::format("{} ({}/{})", catInfo.label, catUnlocked, catTotal);
-
- ImGui::PushStyleColor(ImGuiCol_Text, catInfo.color);
- const bool tabOpen = ImGui::BeginTabItem(tabLabel.c_str());
- ImGui::PopStyleColor();
-
- if (tabOpen) {
- ImGui::BeginChild(
- "##cat_list",
- ImVec2(0, -footerHeight),
- ImGuiChildFlags_None,
- ImGuiWindowFlags_NoBackground
- );
-
- ImGui::Spacing();
-
- for (const auto& a : achievements) {
- if (a.category != catInfo.cat) {
- continue;
- }
- ImGui::PushID(a.key);
-
- ImGui::PushStyleColor(
- ImGuiCol_Text,
- a.unlocked ?
- ImVec4(1.0f, 0.65f, 0.15f, 1.0f) :
- ImGui::GetStyleColorVec4(ImGuiCol_Text)
- );
-
- ImGui::TextUnformatted(a.name);
- ImGui::PopStyleColor();
-
- const char* statusLabel = a.unlocked ? "[Unlocked]" : "[Locked]";
- ImGui::SameLine(
- ImGui::GetContentRegionMax().x -
- ImGui::CalcTextSize(statusLabel).x
- );
-
- if (a.unlocked) {
- ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, 1.0f), "%s", statusLabel);
- } else {
- ImGui::TextColored(ImVec4(0.8f, 0.2f, 0.2f, 1.0f), "%s", statusLabel);
- }
-
- ImGui::TextDisabled("%s", a.description);
-
- if (a.isCounter) {
- const float fraction = a.goal > 0 ? (float)(a.progress) / (float)(a.goal) : 1.0f;
- const std::string overlay = fmt::format("{} / {}", a.progress, a.goal);
- ImGui::PushStyleColor(
- ImGuiCol_PlotHistogram,
- a.unlocked ?
- ImVec4(0.4f, 0.7f, 0.1f, 1.0f) :
- ImVec4(0.2f, 0.45f, 0.8f, 1.0f)
- );
- ImGui::ProgressBar(fraction, ImVec2(-1.0f, 0.0f), overlay.c_str());
- ImGui::PopStyleColor();
- }
-
- ImGui::Spacing();
- ImGui::PopID();
- }
-
- ImGui::EndChild();
- ImGui::EndTabItem();
- }
- }
- ImGui::EndTabBar();
- }
-
- ImGui::Separator();
- ImGui::Spacing();
-
- if (ImGui::Button("Clear All Achievements")) {
- ImGui::OpenPopup("##confirm_clear");
- }
-
- if (ImGui::BeginPopup("##confirm_clear")) {
- ImGui::Text("Reset all achievement progress?");
- ImGui::Spacing();
- if (ImGui::Button("Yes, reset all")) {
- AchievementSystem::get().clearAll();
- ImGui::CloseCurrentPopup();
- }
- ImGui::SameLine();
- if (ImGui::Button("Cancel")) {
- ImGui::CloseCurrentPopup();
- }
- ImGui::EndPopup();
- }
-
- ImGui::End();
-}
-
-} // namespace dusk
diff --git a/src/dusk/imgui/ImGuiAchievements.hpp b/src/dusk/imgui/ImGuiAchievements.hpp
deleted file mode 100644
index 5ee77373fc..0000000000
--- a/src/dusk/imgui/ImGuiAchievements.hpp
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma once
-
-#include
-#include
-
-namespace dusk {
-
-class ImGuiAchievements {
-public:
- void draw(bool& open);
- void notify(std::string name);
-
-private:
- void showNotification();
-
- std::string m_notifyName;
- float m_notifyTimer = 0.f;
- std::queue m_notifyQueue;
- static constexpr float NOTIFY_DURATION = 4.0f;
- static constexpr float NOTIFY_FADE_TIME = 0.5f;
-};
-
-} // namespace dusk
diff --git a/src/dusk/imgui/ImGuiAudio.cpp b/src/dusk/imgui/ImGuiAudio.cpp
index 9abe4c5261..6bddc6f07c 100644
--- a/src/dusk/imgui/ImGuiAudio.cpp
+++ b/src/dusk/imgui/ImGuiAudio.cpp
@@ -1,5 +1,7 @@
#include "ImGuiConsole.hpp"
#include "ImGuiMenuTools.hpp"
+#include
+#include "JSystem/JAudio2/JAISeq.h"
#include "JSystem/JAudio2/JAISeMgr.h"
#include "JSystem/JAudio2/JAISeqMgr.h"
#include "JSystem/JAudio2/JAIStreamMgr.h"
@@ -15,6 +17,24 @@ static std::array lastResetCounts = {};
static bool sortUpdateCount = true;
+static void DrawDirectionGauge(float pan, float dolby) {
+ constexpr float R = 20.0f;
+ constexpr float SIZE = R * 2.0f + 4.0f;
+
+ ImVec2 origin = ImGui::GetCursorScreenPos();
+ ImGui::Dummy(ImVec2(SIZE, SIZE));
+ ImDrawList* dl = ImGui::GetWindowDrawList();
+ ImVec2 c = ImVec2(origin.x + SIZE * 0.5f, origin.y + SIZE * 0.5f);
+
+ dl->AddCircle(c, R, IM_COL32(90, 90, 90, 255), 32);
+
+ float dx = (pan - 0.5f) * 2.0f;
+ float dy = dolby * 2.0f - 1.0f;
+ float len = sqrtf(dx * dx + dy * dy);
+ if (len > 1.0f) { dx /= len; dy /= len; }
+ dl->AddLine(c, ImVec2(c.x + dx * R, c.y + dy * R), IM_COL32(255, 200, 50, 255), 1.5f);
+}
+
static void DisplayDspChannel(int i) {
using namespace dusk::audio;
@@ -52,8 +72,10 @@ static void DisplayDspChannel(int i) {
auto fxMix = (channel.mAutoMixerFxMix >> 8) / 127.5f;
auto volume = VolumeFromU16(channel.mAutoMixerVolume);
auto pitch = channel.mPitch / 4096.0f;
+ DrawDirectionGauge(pan, dolby);
+ ImGui::SameLine();
ImGui::Text(
- "Auto mixer active (pan: %f, dolby: %f, fx: %f, volume: %f, pitch %f)",
+ "pan: %.2f dolby: %.2f\nfx: %.2f vol: %.2f pitch: %.2f",
pan, dolby, fxMix, volume, pitch);
} else {
ImGui::Text(
@@ -183,6 +205,10 @@ static void ShowAllJAISes() {
if (ImGui::Button("Pause All")) {
category->pause(true);
}
+ ImGui::SameLine();
+ if (ImGui::Button("Resume All")) {
+ category->pause(false);
+ }
for (auto seLink = category->getSeList()->getFirst(); seLink != nullptr; seLink = seLink->getNext()) {
const auto se = seLink->getObject();
@@ -196,6 +222,33 @@ static void ShowAllJAISes() {
}
+static void ShowSeqTracks(JAISeq& seq) {
+ JASTrack& root = seq.inner_.outputTrack;
+
+ for (int group = 0; group < 2; group++) {
+ JASTrack* groupTrack = root.getChild(group);
+ if (groupTrack == nullptr) {
+ continue;
+ }
+
+ for (int j = 0; j < JASTrack::MAX_CHILDREN; j++) {
+ JASTrack* track = groupTrack->getChild(j);
+ if (track == nullptr) {
+ continue;
+ }
+
+ int trackIdx = group * 16 + j;
+ char label[64];
+ snprintf(label, sizeof(label), "Track %d (bank %hu, prog %hu)##%p",
+ trackIdx, track->getBankNumber(), track->getProgNumber(), track);
+ bool muted = track->mFlags.mute;
+ if (ImGui::Checkbox(label, &muted)) {
+ track->mute(muted);
+ }
+ }
+ }
+}
+
static void ShowAllJAISeqs() {
auto& mgr = *JAISeqMgr::getInstance();
@@ -206,6 +259,26 @@ static void ShowAllJAISeqs() {
if (ImGui::Button("Unpause")) {
mgr.pause(false);
}
+
+ ImGui::Text("Active sequences: %d", mgr.getNumActiveSeqs());
+
+ auto* seqList = mgr.getSeqList();
+ for (auto* link = seqList->getFirst(); link != nullptr; link = link->getNext()) {
+ JAISeq* seq = link->getObject();
+ if (seq == nullptr) {
+ continue;
+ }
+
+ char buf[32];
+ snprintf(buf, sizeof(buf), "%p", seq);
+
+ if (ImGui::BeginChild(buf, ImVec2(), ImGuiChildFlags_Border | ImGuiChildFlags_AutoResizeY)) {
+ ImGui::Text("Seq [%p]", seq);
+ ShowSeqTracks(*seq);
+ }
+
+ ImGui::EndChild();
+ }
}
void dusk::ImGuiMenuTools::ShowAudioDebug() {
diff --git a/src/dusk/imgui/ImGuiCameraOverlay.cpp b/src/dusk/imgui/ImGuiCameraOverlay.cpp
index aa3d1ac093..2a39ef85eb 100644
--- a/src/dusk/imgui/ImGuiCameraOverlay.cpp
+++ b/src/dusk/imgui/ImGuiCameraOverlay.cpp
@@ -1,9 +1,12 @@
#include "f_op/f_op_camera_mng.h"
#include "SSystem/SComponent/c_xyz.h"
+#include "d/d_com_inf_game.h"
#include "imgui.h"
+#include "ImGuiConfig.hpp"
#include "ImGuiConsole.hpp"
#include "ImGuiMenuTools.hpp"
+#include "dusk/settings.h"
namespace dusk {
void ImGuiMenuTools::ShowCameraOverlay() {
@@ -46,70 +49,25 @@ namespace dusk {
ImGui::InputFloat("Camera FOV", &dCam->mFovy);
- ImGui::SeparatorText("Free-look Data");
+ ImGui::SeparatorText("Options");
- static float eyeYawDeg = 0.0f;
- static float moveSpeed = 5000.0f;
- static float rotSpeed = 5.0f;
- static cXyz freeLookPos = cXyz::Zero;
- static bool freeLookActive = false;
-
- bool changed = false;
-
- if (ImGui::IsKeyDown(ImGuiKey_LeftArrow)) {
- eyeYawDeg += rotSpeed;
- if (eyeYawDeg >= 360.0f)
- eyeYawDeg -= 360.0f;
-
- changed = true;
+ bool eventRunning = (dComIfGp_event_runCheck() || dComIfGp_isPauseFlag()) && !getSettings().game.debugFlyCam;
+ if (eventRunning) {
+ ImGui::BeginDisabled();
}
- else if (ImGui::IsKeyDown(ImGuiKey_RightArrow)) {
- eyeYawDeg -= rotSpeed;
- if (eyeYawDeg < 0.0f)
- eyeYawDeg += 360.0f;
-
- changed = true;
+ config::ImGuiCheckbox("Fly Mode", getSettings().game.debugFlyCam);
+ if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
+ if (eventRunning) {
+ 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");
+ }
}
- cSAngle yawAngle = cSAngle(eyeYawDeg);
- cXyz frontDir = cXyz(yawAngle.Sin(), 0.0f, yawAngle.Cos());
-
- if (ImGui::IsKeyDown(ImGuiKey_UpArrow)) {
- freeLookPos -= frontDir * moveSpeed * ImGui::GetIO().DeltaTime;
- changed = true;
+ if (eventRunning) {
+ ImGui::EndDisabled();
}
- else if (ImGui::IsKeyDown(ImGuiKey_DownArrow)) {
- freeLookPos += frontDir * moveSpeed * ImGui::GetIO().DeltaTime;
- changed = true;
- }
-
- if (ImGui::IsKeyDown(ImGuiKey_LeftShift)) {
- freeLookPos += cXyz::BaseY * moveSpeed * ImGui::GetIO().DeltaTime;
- changed = true;
- }
-
- if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl)) {
- freeLookPos -= cXyz::BaseY * moveSpeed * ImGui::GetIO().DeltaTime;
- changed = true;
- }
-
- if (!freeLookActive && changed) {
- freeLookPos += dCam->Center();
- freeLookActive = true;
- }
-
- if (ImGui::IsKeyDown(ImGuiKey_R)) {
- freeLookPos = cXyz::Zero;
- freeLookActive = false;
- }
-
- if (freeLookActive) {
- dCam->Reset(freeLookPos, freeLookPos + (frontDir * 100.0f));
- }
-
- ImGui::InputFloat("Free-look Yaw", &eyeYawDeg);
- ImGui::InputFloat3("Free-look Position", &freeLookPos.x);
- ImGui::InputFloat("Free-look Move Speed", &moveSpeed);
- ImGui::InputFloat("Free-look Rotation Speed", &rotSpeed);
ShowCornerContextMenu(m_cameraOverlayCorner, 0);
diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp
index c8a9fa86aa..355ef14450 100644
--- a/src/dusk/imgui/ImGuiConsole.cpp
+++ b/src/dusk/imgui/ImGuiConsole.cpp
@@ -10,17 +10,20 @@
#include "fmt/format.h"
#include "ImGuiConsole.hpp"
+#include "dusk/ui/preset.hpp"
+#include "dusk/ui/ui.hpp"
#include "JSystem/JUtility/JUTGamePad.h"
-#include "SDL3/SDL_events.h"
#include "SDL3/SDL_mouse.h"
-#include "aurora/lib/window.hpp"
#include "dusk/achievements.h"
#include "dusk/audio/DuskAudioSystem.h"
#include "dusk/config.hpp"
#include "dusk/dusk.h"
#include "dusk/frame_interpolation.h"
+#include "dusk/livesplit.h"
#include "dusk/main.h"
#include "dusk/settings.h"
+#include "f_pc/f_pc_manager.h"
+#include "f_pc/f_pc_name.h"
#include "m_Do/m_Do_controller_pad.h"
#include "m_Do/m_Do_main.h"
#include "tracy/Tracy.hpp"
@@ -34,14 +37,6 @@ using namespace std::string_literals;
using namespace std::string_view_literals;
namespace {
-ImVec2 TouchEventToScreenPos(const SDL_TouchFingerEvent& touch) {
- const AuroraWindowSize size = aurora::window::get_window_size();
- return ImVec2{
- touch.x * static_cast(size.width),
- touch.y * static_cast(size.height),
- };
-}
-
ImGuiWindow* FindDragScrollWindow(ImGuiWindow* window) {
while (window != nullptr) {
const bool canScrollX = window->ScrollMax.x > 0.0f;
@@ -65,6 +60,10 @@ namespace dusk {
ImGui::TextUnformatted(text.data(), text.data() + text.size());
}
+ void DuskToast(std::string_view message, float duration) {
+ g_imguiConsole.AddToast(message, duration);
+ }
+
void ImGuiTextCenter(std::string_view text) {
ImGui::NewLine();
float fontSize = ImGui::CalcTextSize(
@@ -236,48 +235,7 @@ namespace dusk {
ImGuiConsole::ImGuiConsole() {}
void ImGuiConsole::HandleSDLEvent(const SDL_Event& event) {
- if (!IsGameLaunched) {
- return;
- }
-
- switch (event.type) {
- case SDL_EVENT_FINGER_DOWN:
- if (!m_touchTapActive) {
- m_touchTapActive = true;
- m_touchTapMoved = false;
- m_touchTapFingerId = event.tfinger.fingerID;
- m_touchTapStartPos = TouchEventToScreenPos(event.tfinger);
- }
- break;
- case SDL_EVENT_FINGER_MOTION:
- if (m_touchTapActive && m_touchTapFingerId == event.tfinger.fingerID) {
- const auto currentPos = TouchEventToScreenPos(event.tfinger);
- const auto delta = currentPos - m_touchTapStartPos;
- if (ImLengthSqr(delta) > 144.0f) {
- m_touchTapMoved = true;
- }
- }
- break;
- case SDL_EVENT_FINGER_UP:
- if (m_touchTapActive && m_touchTapFingerId == event.tfinger.fingerID) {
- const bool shouldToggle =
- !m_touchTapMoved && (m_isHidden || !ImGui::GetIO().WantCaptureMouse);
- m_touchTapActive = false;
- m_touchTapMoved = false;
- if (shouldToggle) {
- m_isHidden = !m_isHidden;
- getSettings().backend.duskMenuOpen.setValue(!m_isHidden);
- Save();
- }
- }
- break;
- case SDL_EVENT_FINGER_CANCELED:
- m_touchTapActive = false;
- m_touchTapMoved = false;
- break;
- default:
- break;
- }
+ (void)event;
}
void ImGuiConsole::UpdateSettings() {
@@ -305,7 +263,8 @@ namespace dusk {
}
}
- if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) &&
+ if (!fpcM_SearchByName(fpcNm_LOGO_SCENE_e) &&
+ (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) &&
ImGui::IsKeyPressed(ImGuiKey_R))
{
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
@@ -315,23 +274,14 @@ namespace dusk {
ImGuiMenuGame::ToggleFullscreen();
}
- if (ImGui::IsKeyPressed(ImGuiKey_Escape) && getSettings().video.enableFullscreen) {
- 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;
// The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg,
// so make the window bg fully transparent temporarily
@@ -354,24 +304,32 @@ namespace dusk {
}
ImGui::PopStyleColor();
- if (!getSettings().backend.wasPresetChosen) {
- m_firstRunPreset.draw();
- return;
- }
-
if (dusk::IsGameLaunched && !m_isLaunchInitialized) {
- m_toasts.emplace_back(ImGui::GetIO().MouseSource == ImGuiMouseSource_TouchScreen ?
- "Tap to toggle menu"s :
+ AddToast(ImGui::GetIO().MouseSource == ImGuiMouseSource_TouchScreen ?
+ "3-finger 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();
+ }
}
UpdateDragScroll();
m_menuGame.windowControllerConfig();
m_menuGame.windowInputViewer();
- if (dusk::IsGameLaunched) {
+ m_menuGame.drawSpeedrunTimerOverlay();
+
+ if (getSettings().game.liveSplitEnabled) {
+ dusk::speedrun::updateLiveSplit();
+ if (dusk::speedrun::consumeConnectedEvent())
+ AddToast("LiveSplit connected");
+ else if (dusk::speedrun::consumeDisconnectedEvent())
+ AddToast("LiveSplit disconnected");
+ }
+
+ if (dusk::IsGameLaunched && !dusk::getSettings().game.speedrunMode) {
m_menuTools.ShowDebugOverlay();
m_menuTools.ShowCameraOverlay();
m_menuTools.ShowProcessManager();
@@ -382,9 +340,9 @@ namespace dusk {
m_menuTools.ShowPlayerInfo();
m_menuTools.ShowAudioDebug();
m_menuTools.ShowSaveEditor();
+ m_menuTools.ShowStateShare();
}
- m_menuTools.ShowStateShare();
- m_menuTools.ShowAchievements();
+ m_menuTools.showAchievementNotification();
DuskDebugPad(); // temporary, remove later
// Hide mouse cursor if the F1 menu is not open and the cursor is idle for 3 seconds.
@@ -556,6 +514,10 @@ namespace dusk {
return false;
}
+ void ImGuiConsole::AddToast(std::string_view message, float duration) {
+ m_toasts.emplace_back(std::string(message), duration);
+ }
+
void ImGuiConsole::ShowToasts() {
if (m_toasts.empty()) {
return;
diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp
index de4e66c7d6..1755c02856 100644
--- a/src/dusk/imgui/ImGuiConsole.hpp
+++ b/src/dusk/imgui/ImGuiConsole.hpp
@@ -6,9 +6,7 @@
#include
#include
-#include
-#include "ImGuiFirstRunPreset.hpp"
#include "ImGuiMenuGame.hpp"
#include "ImGuiMenuTools.hpp"
#include "ImGuiPreLaunchWindow.hpp"
@@ -27,6 +25,7 @@ public:
void PostDraw();
static bool CheckMenuViewToggle(ImGuiKey key, bool& active);
+ void AddToast(std::string_view message, float duration = 3.f);
private:
struct Toast {
@@ -41,15 +40,10 @@ private:
bool m_isHidden = true;
bool m_isLaunchInitialized = false;
- bool m_touchTapActive = false;
- bool m_touchTapMoved = false;
- SDL_FingerID m_touchTapFingerId = 0;
- ImVec2 m_touchTapStartPos = {};
ImGuiWindow* m_dragScrollWindow = nullptr;
ImVec2 m_dragScrollLastMousePos = {};
std::deque m_toasts;
- ImGuiFirstRunPreset m_firstRunPreset;
ImGuiMenuGame m_menuGame;
ImGuiPreLaunchWindow m_preLaunchWindow;
@@ -70,6 +64,7 @@ std::string BytesToString(size_t bytes);
void SetOverlayWindowLocation(int corner);
bool ShowCornerContextMenu(int& corner, int avoidCorner);
void ImGuiStringViewText(std::string_view text);
+void DuskToast(std::string_view message, float duration = 3.f);
void ImGuiBeginGroupPanel(const char* name, const ImVec2& size);
void ImGuiEndGroupPanel();
void ImGuiTextCenter(std::string_view text);
diff --git a/src/dusk/imgui/ImGuiEventFlags.hpp b/src/dusk/imgui/ImGuiEventFlags.hpp
index 694336da03..9928690b9b 100644
--- a/src/dusk/imgui/ImGuiEventFlags.hpp
+++ b/src/dusk/imgui/ImGuiEventFlags.hpp
@@ -3,6 +3,9 @@
#include
#include
+#include