diff --git a/CMakeLists.txt b/CMakeLists.txt index e8132a98fd..19fd5d673f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,6 +90,11 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) +# Folder-based instead of target-based organization +# in Visual Studio and Xcode generators +set_property(GLOBAL PROPERTY USE_FOLDERS ON) +set_property(GLOBAL PROPERTY PREDEFINED_TARGETS_FOLDER "_cmake") + if (CMAKE_SYSTEM_NAME STREQUAL Linux) set(DAWN_USE_WAYLAND ON CACHE BOOL "Enable support for Wayland surface" FORCE) endif () @@ -281,7 +286,7 @@ set(DUSK_TP_VERSION ${DUSK_GAME_NAME}${DUSK_GAME_VERSION}) message(STATUS "dusk: Game Version: ${DUSK_TP_VERSION}") -source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${JSYSTEM_FILES} ${JSYSTEM_DEBUG_FILES} ${REL_FILES}) +source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES}) source_group("dusk" FILES ${DUSK_FILES}) set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 @@ -374,47 +379,65 @@ endif () # game_debug is for game code files that we know work when compiled with DEBUG=1 # Of course, if building a release build, this distinction is irrelevant -add_library(game_debug OBJECT ${JSYSTEM_DEBUG_FILES} ${SSYSTEM_FILES} +set(GAME_DEBUG_FILES + ${SSYSTEM_FILES} src/dusk/audio/DuskAudioSystem.cpp src/dusk/audio/JASCriticalSection.cpp src/dusk/audio/DuskDsp.cpp src/dusk/audio/Adpcm.cpp src/dusk/audio/DspStub.cpp - src/dusk/imgui/ImGuiAudio.cpp) + src/dusk/imgui/ImGuiAudio.cpp +) +set_source_files_properties( + ${GAME_DEBUG_FILES} + PROPERTIES + COMPILE_DEFINITIONS "$<$:DEBUG=1>;$<$:PARTIAL_DEBUG=1>" +) # game_base is for all other game code files -add_library(game_base OBJECT ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${JSYSTEM_FILES} ${REL_FILES} ${DUSK_FILES} ${DOLPHIN_FILES}) +set(GAME_BASE_FILES + ${DOLZEL_FILES} + ${Z2AUDIOLIB_FILES} + ${REL_FILES} + ${DUSK_FILES} + ${DOLPHIN_FILES} +) +set_source_files_properties( + ${GAME_BASE_FILES} + PROPERTIES + COMPILE_DEFINITIONS "NDEBUG=1;NDEBUG_DEFINED=1;DEBUG_DEFINED=0;$<$:PARTIAL_DEBUG=1>" +) -target_compile_definitions(game_debug PRIVATE ${GAME_COMPILE_DEFS} $<$:DEBUG=1> $<$:PARTIAL_DEBUG=1>) -target_compile_definitions(game_base PRIVATE ${GAME_COMPILE_DEFS} NDEBUG=1 NDEBUG_DEFINED=1 DEBUG_DEFINED=0 $<$:PARTIAL_DEBUG=1>) +foreach(jsystem_lib IN LISTS JSYSTEM_LIBRARIES) + target_compile_definitions(${jsystem_lib} PRIVATE + ${GAME_COMPILE_DEFS} + $<$:DEBUG=1> + $<$:PARTIAL_DEBUG=1> + ) + target_include_directories(${jsystem_lib} PRIVATE ${GAME_INCLUDE_DIRS}) + target_link_libraries(${jsystem_lib} PRIVATE ${GAME_LIBS}) + set_target_properties(${jsystem_lib} PROPERTIES FOLDER "JSystem") +endforeach() -# only apply PCH to game_base since not all headers are necessarily validated with DEBUG=1 -target_precompile_headers(game_base PRIVATE "$<$:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>") - -target_include_directories(game_debug PRIVATE ${GAME_INCLUDE_DIRS}) -target_include_directories(game_base PRIVATE ${GAME_INCLUDE_DIRS}) - -# This implicitly pulls in the library include directories even though no -# linking actually takes place for object libraries -target_link_libraries(game_debug PRIVATE ${GAME_LIBS}) -target_link_libraries(game_base PRIVATE ${GAME_LIBS}) - -# Combined game library -add_library(game STATIC - $ - $) -target_link_libraries(game PUBLIC ${GAME_LIBS}) - -if(ANDROID) - add_library(dusk SHARED src/dusk/main.cpp) - set_target_properties(dusk PROPERTIES OUTPUT_NAME main) -else () - add_executable(dusk src/dusk/main.cpp) +set(JSYSTEM_LINK_LIBRARIES ${JSYSTEM_LIBRARIES}) +if (CMAKE_CXX_LINK_GROUP_USING_RESCAN_SUPPORTED OR CMAKE_LINK_GROUP_USING_RESCAN_SUPPORTED) + # GNU ld resolves static archives in a single left-to-right pass. The split + # JSystem libraries reference each other, so they need a RESCAN group there. + set(JSYSTEM_LINK_LIBRARIES "$") endif () -target_compile_definitions(dusk PRIVATE TARGET_PC AVOID_UB=1 VERSION=0) -target_include_directories(dusk PRIVATE include) -target_link_libraries(dusk PRIVATE game aurora::main) +set(DUSK_FILES src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES}) +if(ANDROID) + add_library(dusk SHARED ${DUSK_FILES}) + set_target_properties(dusk PROPERTIES OUTPUT_NAME main) +else () + add_executable(dusk ${DUSK_FILES}) +endif () + +target_compile_definitions(dusk PRIVATE ${GAME_COMPILE_DEFS}) +target_include_directories(dusk PRIVATE ${GAME_INCLUDE_DIRS}) +target_link_libraries(dusk PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES}) +target_precompile_headers(dusk PRIVATE "$<$:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>") if (TARGET crashpad_handler) add_dependencies(dusk crashpad_handler) endif () diff --git a/README.md b/README.md index 7322441b74..c2fe53576d 100644 --- a/README.md +++ b/README.md @@ -10,28 +10,19 @@ 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. | Version | sha1 hash | -| ------------ | ---------------------------------------- | +|--------------| ---------------------------------------- | | GameCube USA | 75edd3ddff41f125d1b4ce1a40378f1b565519e7 | +| GameCube PAL | 2601822a488eeb86fb89db16ca8f29c2c953e1ca | ### 2. Download [Dusk](https://github.com/TwilitRealm/dusk/releases) ### 3. Setup the game -#### Windows - Extract the zip folder -- Place your dump of the game into the same folder where you extracted to -- Launch `dusk.exe` +- Launch Dusk +- Select Options, then set the ISO Path to your supported game dump +- Press Start Game to play! -#### macOS -- TODO - -#### Linux -- TODO - -#### iOS -- TODO - -#### android -- TODO +![DuskOptions](assets/dusk_options.png) # Building If you'd like to build Dusk from source, please read the [build instructions](docs/building.md). diff --git a/assets/dusk_options.png b/assets/dusk_options.png new file mode 100644 index 0000000000..4ed4ad7563 Binary files /dev/null and b/assets/dusk_options.png differ diff --git a/extern/aurora b/extern/aurora index b1957f10cf..be1395c134 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit b1957f10cf9e7ea1e0e012c1968014bd11299297 +Subproject commit be1395c1343a5587cceea25754daa5cacce34f76 diff --git a/files.cmake b/files.cmake index e929ec85b0..e28d6797c9 100644 --- a/files.cmake +++ b/files.cmake @@ -15,7 +15,6 @@ set(DOLZEL_FILES src/m_Do/m_Do_DVDError.cpp src/m_Do/m_Do_MemCard.cpp src/m_Do/m_Do_MemCardRWmng.cpp - src/m_Do/m_Do_machine_exception.cpp src/m_Do/m_Do_hostIO.cpp src/c/c_damagereaction.cpp src/c/c_dylink.cpp @@ -315,7 +314,7 @@ set(SSYSTEM_FILES src/SSystem/SStandard/s_basic.cpp ) -set(JSYSTEM_DEBUG_FILES +add_library(JSystem_JParticle STATIC libs/JSystem/src/JParticle/JPAResourceManager.cpp libs/JSystem/src/JParticle/JPAResource.cpp libs/JSystem/src/JParticle/JPABaseShape.cpp @@ -331,10 +330,19 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JParticle/JPAEmitter.cpp libs/JSystem/src/JParticle/JPAParticle.cpp libs/JSystem/src/JParticle/JPAMath.cpp +) + +add_library(JSystem_JFramework STATIC libs/JSystem/src/JFramework/JFWSystem.cpp libs/JSystem/src/JFramework/JFWDisplay.cpp +) + +add_library(JSystem_J3DU STATIC libs/JSystem/src/J3DU/J3DUClipper.cpp libs/JSystem/src/J3DU/J3DUDL.cpp +) + +add_library(JSystem_JKernel STATIC libs/JSystem/src/JKernel/JKRHeap.cpp libs/JSystem/src/JKernel/JKRExpHeap.cpp libs/JSystem/src/JKernel/JKRSolidHeap.cpp @@ -360,14 +368,23 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JKernel/JKRDvdRipper.cpp libs/JSystem/src/JKernel/JKRDvdAramRipper.cpp libs/JSystem/src/JKernel/JKRDecomp.cpp +) + +add_library(JSystem_JMath STATIC libs/JSystem/src/JMath/JMath.cpp libs/JSystem/src/JMath/random.cpp libs/JSystem/src/JMath/JMATrigonometric.cpp +) + +add_library(JSystem_JSupport STATIC libs/JSystem/src/JSupport/JSUList.cpp libs/JSystem/src/JSupport/JSUInputStream.cpp libs/JSystem/src/JSupport/JSUOutputStream.cpp libs/JSystem/src/JSupport/JSUMemoryStream.cpp libs/JSystem/src/JSupport/JSUFileStream.cpp +) + +add_library(JSystem_JUtility STATIC libs/JSystem/src/JUtility/JUTCacheFont.cpp libs/JSystem/src/JUtility/JUTResource.cpp libs/JSystem/src/JUtility/JUTTexture.cpp @@ -388,6 +405,9 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JUtility/JUTConsole.cpp libs/JSystem/src/JUtility/JUTDirectFile.cpp libs/JSystem/src/JUtility/JUTFontData_Ascfont_fix12.cpp +) + +add_library(JSystem_JStage STATIC libs/JSystem/src/JStage/JSGActor.cpp libs/JSystem/src/JStage/JSGAmbientLight.cpp libs/JSystem/src/JStage/JSGCamera.cpp @@ -395,6 +415,9 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JStage/JSGLight.cpp libs/JSystem/src/JStage/JSGObject.cpp libs/JSystem/src/JStage/JSGSystem.cpp +) + +add_library(JSystem_J2DGraph STATIC libs/JSystem/src/J2DGraph/J2DGrafContext.cpp libs/JSystem/src/J2DGraph/J2DOrthoGraph.cpp libs/JSystem/src/J2DGraph/J2DTevs.cpp @@ -413,6 +436,9 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/J2DGraph/J2DAnmLoader.cpp libs/JSystem/src/J2DGraph/J2DAnimation.cpp libs/JSystem/src/J2DGraph/J2DManage.cpp +) + +add_library(JSystem_J3DGraphBase STATIC libs/JSystem/src/J3DGraphBase/J3DGD.cpp libs/JSystem/src/J3DGraphBase/J3DSys.cpp libs/JSystem/src/J3DGraphBase/J3DVertex.cpp @@ -427,6 +453,9 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/J3DGraphBase/J3DTevs.cpp libs/JSystem/src/J3DGraphBase/J3DDrawBuffer.cpp libs/JSystem/src/J3DGraphBase/J3DStruct.cpp +) + +add_library(JSystem_J3DGraphAnimator STATIC libs/JSystem/src/J3DGraphAnimator/J3DShapeTable.cpp libs/JSystem/src/J3DGraphAnimator/J3DJointTree.cpp libs/JSystem/src/J3DGraphAnimator/J3DModelData.cpp @@ -438,6 +467,9 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/J3DGraphAnimator/J3DCluster.cpp libs/JSystem/src/J3DGraphAnimator/J3DJoint.cpp libs/JSystem/src/J3DGraphAnimator/J3DMaterialAttach.cpp +) + +add_library(JSystem_J3DGraphLoader STATIC libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory.cpp libs/JSystem/src/J3DGraphLoader/J3DMaterialFactory_v21.cpp libs/JSystem/src/J3DGraphLoader/J3DClusterLoader.cpp @@ -446,6 +478,9 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/J3DGraphLoader/J3DJointFactory.cpp libs/JSystem/src/J3DGraphLoader/J3DShapeFactory.cpp libs/JSystem/src/J3DGraphLoader/J3DAnmLoader.cpp +) + +add_library(JSystem_JStudio STATIC libs/JSystem/src/JStudio/JStudio/ctb.cpp libs/JSystem/src/JStudio/JStudio/ctb-data.cpp libs/JSystem/src/JStudio/JStudio/functionvalue.cpp @@ -460,6 +495,9 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JStudio/JStudio/stb.cpp libs/JSystem/src/JStudio/JStudio/stb-data-parse.cpp libs/JSystem/src/JStudio/JStudio/stb-data.cpp +) + +add_library(JSystem_JStudio_JStage STATIC libs/JSystem/src/JStudio/JStudio_JStage/control.cpp libs/JSystem/src/JStudio/JStudio_JStage/object.cpp libs/JSystem/src/JStudio/JStudio_JStage/object-actor.cpp @@ -467,10 +505,19 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JStudio/JStudio_JStage/object-camera.cpp libs/JSystem/src/JStudio/JStudio_JStage/object-fog.cpp libs/JSystem/src/JStudio/JStudio_JStage/object-light.cpp +) + +add_library(JSystem_JStudio_JAudio2 STATIC libs/JSystem/src/JStudio/JStudio_JAudio2/control.cpp libs/JSystem/src/JStudio/JStudio_JAudio2/object-sound.cpp +) + +add_library(JSystem_JStudio_JParticle STATIC libs/JSystem/src/JStudio/JStudio_JParticle/control.cpp libs/JSystem/src/JStudio/JStudio_JParticle/object-particle.cpp +) + +add_library(JSystem_JAudio2 STATIC libs/JSystem/src/JAudio2/JASCalc.cpp libs/JSystem/src/JAudio2/JASTaskThread.cpp libs/JSystem/src/JAudio2/JASDvdThread.cpp @@ -535,22 +582,34 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JAudio2/JAUSoundAnimator.cpp libs/JSystem/src/JAudio2/JAUSoundTable.cpp libs/JSystem/src/JAudio2/JAUStreamFileTable.cpp +) + +add_library(JSystem_JMessage STATIC libs/JSystem/src/JMessage/control.cpp libs/JSystem/src/JMessage/data.cpp libs/JSystem/src/JMessage/processor.cpp libs/JSystem/src/JMessage/resource.cpp libs/JSystem/src/JMessage/locale.cpp +) + +add_library(JSystem_JGadget STATIC libs/JSystem/src/JGadget/binary.cpp libs/JSystem/src/JGadget/define.cpp libs/JSystem/src/JGadget/linklist.cpp libs/JSystem/src/JGadget/search.cpp libs/JSystem/src/JGadget/std-vector.cpp +) + +add_library(JSystem_JAHostIO STATIC libs/JSystem/src/JAHostIO/JAHFrameNode.cpp libs/JSystem/src/JAHostIO/JAHioMessage.cpp libs/JSystem/src/JAHostIO/JAHioMgr.cpp libs/JSystem/src/JAHostIO/JAHioNode.cpp libs/JSystem/src/JAHostIO/JAHioUtil.cpp libs/JSystem/src/JAHostIO/JAHVirtualNode.cpp +) + +add_library(JSystem_JHostIO STATIC libs/JSystem/src/JHostIO/JORFile.cpp libs/JSystem/src/JHostIO/JORHostInfo.cpp libs/JSystem/src/JHostIO/JORMessageBox.cpp @@ -560,7 +619,28 @@ set(JSYSTEM_DEBUG_FILES libs/JSystem/src/JHostIO/JHIMccBuf.cpp ) -set(JSYSTEM_FILES +set(JSYSTEM_LIBRARIES + JSystem_JParticle + JSystem_JFramework + JSystem_J3DU + JSystem_JKernel + JSystem_JMath + JSystem_JSupport + JSystem_JUtility + JSystem_JStage + JSystem_J2DGraph + JSystem_J3DGraphBase + JSystem_J3DGraphAnimator + JSystem_J3DGraphLoader + JSystem_JStudio + JSystem_JStudio_JStage + JSystem_JStudio_JAudio2 + JSystem_JStudio_JParticle + JSystem_JAudio2 + JSystem_JMessage + JSystem_JGadget + JSystem_JAHostIO + JSystem_JHostIO ) set(REL_FILES @@ -1342,13 +1422,14 @@ set(DUSK_FILES src/dusk/crash_reporting.cpp src/dusk/endian.cpp src/dusk/extras.c - src/dusk/extras.cpp src/dusk/file_select.cpp src/dusk/file_select.hpp src/dusk/frame_interpolation.cpp src/dusk/game_clock.cpp 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 @@ -1366,8 +1447,6 @@ set(DUSK_FILES src/dusk/imgui/ImGuiBloomWindow.hpp src/dusk/imgui/ImGuiMenuTools.cpp src/dusk/imgui/ImGuiMenuTools.hpp - src/dusk/imgui/ImGuiMenuEnhancements.cpp - src/dusk/imgui/ImGuiMenuEnhancements.hpp src/dusk/imgui/ImGuiPreLaunchWindow.cpp src/dusk/imgui/ImGuiPreLaunchWindow.hpp src/dusk/imgui/ImGuiFirstRunPreset.hpp diff --git a/include/d/actor/d_a_movie_player.h b/include/d/actor/d_a_movie_player.h index 0ddc675c24..0e666ae470 100644 --- a/include/d/actor/d_a_movie_player.h +++ b/include/d/actor/d_a_movie_player.h @@ -94,6 +94,12 @@ static void __THPAudioInitialize(THPAudioDecodeInfo* info, u8* ptr); #define THP_TEXTURE_SET_COUNT 3 #endif +#if TARGET_PC +namespace dusk { + void MoviePlayerShutdown(); +} +#endif + struct daMP_THPPlayer { /* 0x000 */ DVDFileInfo fileInfo; /* 0x03C */ THPHeader header; diff --git a/include/d/d_menu_dmap.h b/include/d/d_menu_dmap.h index 78c659e2cf..e50c533654 100644 --- a/include/d/d_menu_dmap.h +++ b/include/d/d_menu_dmap.h @@ -103,6 +103,10 @@ public: field_0xd98 = param_1; } +#if TARGET_PC + void resetScrollArrowMask() { field_0xdda = 0; } +#endif + /* 0xC98 */ JKRExpHeap* mpHeap; /* 0xC9C */ JKRExpHeap* mpTalkHeap; /* 0xCA0 */ STControl* mpStick; diff --git a/include/d/d_menu_fmap.h b/include/d/d_menu_fmap.h index 026e98e056..48db37fef3 100644 --- a/include/d/d_menu_fmap.h +++ b/include/d/d_menu_fmap.h @@ -75,7 +75,9 @@ public: /* 0x8 */ BE(u16) mAreaName; /* 0xA */ u8 mCount; #ifdef _MSVC_LANG - u8* __get_mRoomNos() const { return (u8*)(this + 1); } + // Room numbers start at offset 0xB (right after mCount), NOT at sizeof(data)=12. + // (u8*)(this+1) would give offset 12 because MSVC sizeof=12; use &mCount+1 instead. + u8* __get_mRoomNos() const { return (u8*)&mCount + 1; } __declspec(property(get = __get_mRoomNos)) u8* mRoomNos; #else /* 0xB */ u8 mRoomNos[0]; diff --git a/include/d/d_menu_ring.h b/include/d/d_menu_ring.h index 39d29a35e8..74624eac80 100644 --- a/include/d/d_menu_ring.h +++ b/include/d/d_menu_ring.h @@ -206,6 +206,15 @@ private: /* 0x6D3 */ u8 field_0x6d3; #if TARGET_PC f32 mSelectItemSlideElapsed[4]; + f32 mCursorInterpPrevX; + f32 mCursorInterpPrevY; + f32 mCursorInterpCurrX; + f32 mCursorInterpCurrY; + s16 mCursorInterpPrevAngle; + s16 mCursorInterpCurrAngle; + bool mCursorInterpPrevAngular; + bool mCursorInterpCurrAngular; + bool mCursorInterpInit; #endif }; diff --git a/include/d/d_select_cursor.h b/include/d/d_select_cursor.h index 1bd5991d16..1ca226611b 100644 --- a/include/d/d_select_cursor.h +++ b/include/d/d_select_cursor.h @@ -48,6 +48,9 @@ public: } #ifdef TARGET_PC + f32 getPositionX() const { return mPositionX; } + f32 getPositionY() const { return mPositionY; } + void refreshAspectScale(); #endif 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/dusk.h b/include/dusk/dusk.h index b751990d9c..911ddbb535 100644 --- a/include/dusk/dusk.h +++ b/include/dusk/dusk.h @@ -6,7 +6,6 @@ #include "aurora/gfx.h" extern AuroraInfo auroraInfo; -extern const char* configPath; namespace dusk { extern AuroraStats lastFrameAuroraStats; diff --git a/include/dusk/frame_interpolation.h b/include/dusk/frame_interpolation.h index bec9b600cf..8c19e7e59b 100644 --- a/include/dusk/frame_interpolation.h +++ b/include/dusk/frame_interpolation.h @@ -16,6 +16,7 @@ void ensure_initialized(); void begin_record(); void end_record(); +void begin_sim_tick(); void begin_frame(bool enabled, bool is_sim_frame, float step); void interpolate(); float get_interpolation_step(); diff --git a/include/dusk/game_clock.h b/include/dusk/game_clock.h index 4a394b5c2e..8bb277e070 100644 --- a/include/dusk/game_clock.h +++ b/include/dusk/game_clock.h @@ -1,13 +1,8 @@ -#ifndef DUSK_GAME_CLOCK_H -#define DUSK_GAME_CLOCK_H +#pragma once -#include - -namespace dusk { -namespace game_clock { +namespace dusk::game_clock { void ensure_initialized(); -void reset_accumulator(); void reset_frame_timer(); constexpr float sim_pace() { return 1.0f / 30.0f; } @@ -18,16 +13,14 @@ constexpr float ui_initial_dt() { return 1.0f / 60.0f; } struct MainLoopPacer { float presentation_dt_seconds; bool is_interpolating; - bool do_sim_tick; - float interpolation_step; + int sim_ticks_to_run; float sim_pace; }; MainLoopPacer advance_main_loop(); +void commit_sim_tick(); +float sample_interpolation_step(); float consume_interval(const void* consumer); -} // namespace game_clock -} // namespace dusk - -#endif // DUSK_GAME_CLOCK_H +} // namespace dusk::game_clock diff --git a/include/dusk/gamepad_color.h b/include/dusk/gamepad_color.h new file mode 100644 index 0000000000..c7cfc1e716 --- /dev/null +++ b/include/dusk/gamepad_color.h @@ -0,0 +1,8 @@ +#pragma once + +#ifndef GAMEPAD_COLOR_H +#define GAMEPAD_COLOR_H + +void handleGamepadColor(); + +#endif \ No newline at end of file diff --git a/include/dusk/logging.h b/include/dusk/logging.h index 0a9cbf238d..9b31b96bf2 100644 --- a/include/dusk/logging.h +++ b/include/dusk/logging.h @@ -4,10 +4,12 @@ #include #include +#include + void aurora_log_callback(AuroraLogLevel level, const char* module, const char* message, unsigned int len); namespace dusk { - void InitializeFileLogging(const char* configDir, AuroraLogLevel logLevel); + void InitializeFileLogging(const std::filesystem::path& configDir, AuroraLogLevel logLevel); void ShutdownFileLogging(); const char* GetLogFilePath(); void SendToStubLog(AuroraLogLevel level, const char* module, const char* message); diff --git a/include/dusk/main.h b/include/dusk/main.h index 2152a6d564..065f507d36 100644 --- a/include/dusk/main.h +++ b/include/dusk/main.h @@ -1,11 +1,14 @@ #ifndef DUSK_MAIN_H #define DUSK_MAIN_H +#include + namespace dusk { extern bool IsRunning; extern bool IsShuttingDown; extern bool IsGameLaunched; extern bool IsFocusPaused; + extern std::filesystem::path ConfigPath; } #endif // DUSK_MAIN_H diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 31058a7cdd..328825b8e7 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -70,6 +70,8 @@ struct UserSettings { ConfigVar invertCameraXAxis; ConfigVar disableMainHUD; ConfigVar pauseOnFocusLost; + ConfigVar enableLinkDollRotation; + // Graphics ConfigVar bloomMode; @@ -78,6 +80,7 @@ struct UserSettings { ConfigVar enableFrameInterpolation; ConfigVar internalResolutionScale; ConfigVar shadowResolutionMultiplier; + ConfigVar enableDepthOfField; // Audio ConfigVar noLowHpSound; @@ -101,6 +104,7 @@ struct UserSettings { ConfigVar infiniteOil; ConfigVar infiniteOxygen; ConfigVar infiniteRupees; + ConfigVar enableIndefiniteItemDrops; ConfigVar moonJump; ConfigVar superClawshot; ConfigVar alwaysGreatspin; @@ -124,6 +128,7 @@ struct UserSettings { ConfigVar wasPresetChosen; ConfigVar enableCrashReporting; ConfigVar duskMenuOpen; + ConfigVar cardFileType; } backend; }; @@ -148,6 +153,7 @@ struct TransientSettings { CollisionViewSettings collisionView; bool skipFrameRateLimit; bool moveLinkActive; + bool stateShareLoadActive; }; TransientSettings& getTransientSettings(); diff --git a/include/dusk/time.h b/include/dusk/time.h index 948a2fc171..c43437f639 100644 --- a/include/dusk/time.h +++ b/include/dusk/time.h @@ -1,9 +1,10 @@ #ifndef DUSK_TIME_H #define DUSK_TIME_H -#include -#include #include +#include + +#include "SDL3/SDL_timer.h" #ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN @@ -15,28 +16,26 @@ #include #include #include -#else -#include "SDL3/SDL_timer.h" #endif class Limiter { - using delta_clock = std::chrono::high_resolution_clock; - using duration_t = std::chrono::nanoseconds; - public: - void Reset() { m_oldTime = delta_clock::now(); } + using duration_t = Uint64; + + void Reset() { m_oldTime = SDL_GetTicksNS(); } void Sleep(duration_t targetFrameTime) { - if (targetFrameTime.count() == 0) { + if (targetFrameTime == 0) { return; } - auto start = delta_clock::now(); + const Uint64 start = SDL_GetTicksNS(); duration_t adjustedSleepTime = SleepTime(targetFrameTime); - if (adjustedSleepTime.count() > 0) { + if (adjustedSleepTime > 0) { NanoSleep(adjustedSleepTime); - duration_t overslept = TimeSince(start) - adjustedSleepTime; - if (overslept < duration_t{targetFrameTime}) { + const duration_t elapsed = TimeSince(start); + const duration_t overslept = elapsed > adjustedSleepTime ? elapsed - adjustedSleepTime : 0; + if (overslept < targetFrameTime) { m_overheadTimes[m_overheadTimeIdx] = overslept; m_overheadTimeIdx = (m_overheadTimeIdx + 1) % m_overheadTimes.size(); } @@ -45,23 +44,23 @@ public: } duration_t SleepTime(duration_t targetFrameTime) { - const auto sleepTime = duration_t{targetFrameTime} - TimeSince(m_oldTime); - m_overhead = std::accumulate(m_overheadTimes.begin(), m_overheadTimes.end(), duration_t{}) / m_overheadTimes.size(); + const duration_t elapsed = TimeSince(m_oldTime); + const duration_t sleepTime = elapsed < targetFrameTime ? targetFrameTime - elapsed : 0; + m_overhead = std::accumulate(m_overheadTimes.begin(), m_overheadTimes.end(), duration_t{0}) / + m_overheadTimes.size(); if (sleepTime > m_overhead) { return sleepTime - m_overhead; } - return duration_t{0}; + return 0; } private: - delta_clock::time_point m_oldTime; + Uint64 m_oldTime = 0; std::array m_overheadTimes{}; size_t m_overheadTimeIdx = 0; - duration_t m_overhead = duration_t{0}; + duration_t m_overhead = 0; - duration_t TimeSince(delta_clock::time_point start) { - return std::chrono::duration_cast(delta_clock::now() - start); - } + duration_t TimeSince(Uint64 start) const { return SDL_GetTicksNS() - start; } #if _WIN32 void NanoSleep(const duration_t duration) { @@ -85,9 +84,10 @@ private: LARGE_INTEGER start, current; QueryPerformanceCounter(&start); - LONGLONG ticksToWait = static_cast(duration.count() * countPerNs); - if (DWORD ms = std::chrono::duration_cast(duration).count(); ms > 1) { - ::Sleep(ms - 1); + const LONGLONG ticksToWait = static_cast(duration * countPerNs); + const Uint64 ms = duration / 1'000'000ULL; + if (ms > 1) { + ::Sleep(static_cast(ms - 1)); } do { QueryPerformanceCounter(¤t); @@ -99,7 +99,7 @@ private: } while (current.QuadPart - start.QuadPart < ticksToWait); } #else - void NanoSleep(const duration_t duration) { SDL_DelayPrecise(duration.count()); } + void NanoSleep(const duration_t duration) { SDL_DelayPrecise(duration); } #endif }; diff --git a/include/f_ap/f_ap_game.h b/include/f_ap/f_ap_game.h index 5d09d9bc28..fca2e70705 100644 --- a/include/f_ap/f_ap_game.h +++ b/include/f_ap/f_ap_game.h @@ -14,16 +14,6 @@ void fapGm_After(); void fapGm_Create(); void fapGm_Execute(); -#if TARGET_PC -void noAutoSave(); -void triggerAutoSave(); -void updateAutoSave(); -void enterAutoSave(); -void autoSaving(); -void waitingForWrite(); -void endAutoSave(); -#endif - class fapGm_HIO_c : public JORReflexible { public: fapGm_HIO_c(); diff --git a/include/global.h b/include/global.h index eda2a610ec..9301d12e11 100644 --- a/include/global.h +++ b/include/global.h @@ -73,6 +73,9 @@ #endif #ifndef __MWERKS__ +#ifdef __cplusplus +extern "C" { +#endif // Silence clangd errors about MWCC PPC intrinsics by declaring them here. extern int __cntlzw(unsigned int); extern int __rlwimi(int, int, int, int, int); @@ -80,7 +83,14 @@ extern void __dcbf(void*, int); extern void __dcbz(void*, int); extern void __sync(); extern int __abs(int); -void* __memcpy(void*, const void*, int); +#if defined(__has_builtin) && __has_builtin(__builtin_memcpy) +#define __memcpy __builtin_memcpy +#else +#define __memcpy memcpy +#endif +#ifdef __cplusplus +} +#endif #endif #ifndef M_PI diff --git a/libs/JSystem/include/JSystem/JUtility/JUTGamePad.h b/libs/JSystem/include/JSystem/JUtility/JUTGamePad.h index 30bf685700..45e7227fe3 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTGamePad.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTGamePad.h @@ -263,6 +263,9 @@ public: /* 0x9C */ u8 field_0x9c[4]; /* 0xA0 */ OSTime mResetHoldStartTime; /* 0xA8 */ u8 field_0xa8; +#if TARGET_PC + u32 mResetHoldFrameCount; +#endif }; /** diff --git a/libs/JSystem/src/JFramework/JFWDisplay.cpp b/libs/JSystem/src/JFramework/JFWDisplay.cpp index 64b0fedcf2..b8054e2130 100644 --- a/libs/JSystem/src/JFramework/JFWDisplay.cpp +++ b/libs/JSystem/src/JFramework/JFWDisplay.cpp @@ -18,6 +18,7 @@ #include "dusk/logging.h" #include "dusk/settings.h" #include "dusk/time.h" +#include "f_op/f_op_overlap_mng.h" #include "SDL3/SDL_timer.h" #include "tracy/Tracy.hpp" @@ -368,22 +369,30 @@ constexpr auto FRAME_PERIOD = std::chrono::duration_cast(1001.0 / 30000.0)); constexpr auto RETRACE_PERIOD = FRAME_PERIOD / 2; -static void waitPrecise(Limiter& limiter, Uint64 targetNs) { - const auto sleepTime = limiter.SleepTime(std::chrono::nanoseconds(targetNs)); +static void waitPrecise(Limiter& limiter, Limiter::duration_t targetNs) { + const auto sleepTime = limiter.SleepTime(targetNs); dusk::frameUsagePct = - 100.0f * (1.0f - static_cast(sleepTime.count()) / static_cast(targetNs)); - limiter.Sleep(std::chrono::nanoseconds(targetNs)); + 100.0f * (1.0f - static_cast(sleepTime) / static_cast(targetNs)); + limiter.Sleep(targetNs); } #endif static void waitForTick(u32 p1, u16 p2) { #if TARGET_PC if (dusk::getSettings().game.enableFrameInterpolation && !dusk::getTransientSettings().skipFrameRateLimit) { + dusk::frameUsagePct = 0.f; return; } if (dusk::getTransientSettings().skipFrameRateLimit) { p1 = OS_TIMER_CLOCK / 120; } + + #if TARGET_PC + if (fopOvlpM_IsPeek() && dusk::getTransientSettings().stateShareLoadActive) { + return; + } + #endif + ZoneScopedC(tracy::Color::DimGray); #endif diff --git a/libs/JSystem/src/JUtility/JUTGamePad.cpp b/libs/JSystem/src/JUtility/JUTGamePad.cpp index 091cc382d1..2ddf4397dc 100644 --- a/libs/JSystem/src/JUtility/JUTGamePad.cpp +++ b/libs/JSystem/src/JUtility/JUTGamePad.cpp @@ -64,6 +64,9 @@ BOOL JUTGamePad::init() { void JUTGamePad::clear() { mButtonReset.mReset = false; field_0xa8 = 1; +#if TARGET_PC + mResetHoldFrameCount = 0; +#endif } PADStatus JUTGamePad::mPadStatus[4]; @@ -219,11 +222,19 @@ void JUTGamePad::update() { mButtonReset.mReset = false; } else if (!JUTGamePad::C3ButtonReset::sResetOccurred) { if (mButtonReset.mReset == true) { +#if TARGET_PC + checkResetCallback(++mResetHoldFrameCount * (OS_TIMER_CLOCK / 30)); +#else OSTime hold_time = OSGetTime() - mResetHoldStartTime; checkResetCallback(hold_time); +#endif } else { mButtonReset.mReset = true; +#if TARGET_PC + mResetHoldFrameCount = 0; +#else mResetHoldStartTime = OSGetTime(); +#endif } } diff --git a/src/Z2AudioLib/Z2WolfHowlMgr.cpp b/src/Z2AudioLib/Z2WolfHowlMgr.cpp index cb4f387a21..f12d453fbe 100644 --- a/src/Z2AudioLib/Z2WolfHowlMgr.cpp +++ b/src/Z2AudioLib/Z2WolfHowlMgr.cpp @@ -117,8 +117,8 @@ static Z2WolfHowlLine sNewSong3[9] = { #if TARGET_PC static Z2WolfHowlLine sHowlTimeSong[6] = { - {HOWL_LINE_MID, 20}, {HOWL_LINE_LOW, 20}, {HOWL_LINE_HIGH, 40}, - {HOWL_LINE_MID, 20}, {HOWL_LINE_LOW, 20}, {HOWL_LINE_HIGH, 40}, + {HOWL_LINE_MID, 15}, {HOWL_LINE_LOW, 15}, {HOWL_LINE_HIGH, 30}, + {HOWL_LINE_MID, 15}, {HOWL_LINE_LOW, 15}, {HOWL_LINE_HIGH, 30}, }; #endif @@ -368,9 +368,9 @@ void Z2WolfHowlMgr::setCorrectData(s8 curveID, Z2WolfHowlData* data) { break; #if TARGET_PC case Z2WOLFHOWL_TIMESONG: - cPitchUp = 1.259906f; - cPitchCenter = 0.94387f; - cPitchDown = 0.840885f; + cPitchUp = 1.3348f; + cPitchCenter = 0.8909f; + cPitchDown = 0.7937f; break; #endif default: diff --git a/src/d/actor/d_a_alink_dusk.cpp b/src/d/actor/d_a_alink_dusk.cpp index 045a9655bd..d53476aa91 100644 --- a/src/d/actor/d_a_alink_dusk.cpp +++ b/src/d/actor/d_a_alink_dusk.cpp @@ -41,7 +41,7 @@ void daAlink_c::handleWolfHowl() { return; } - bool canTransform = false; + bool canHowl = false; if (mLinkAcch.ChkGroundHit() && !checkModeFlg(MODE_PLAYER_FLY) && !checkMagneBootsOn()) { if (!checkForestOldCentury()) { @@ -52,12 +52,17 @@ void daAlink_c::handleWolfHowl() { (checkEventRun() || getMidnaActor()->checkMetamorphoseEnable()) && (checkModeFlg(4) || dComIfGp_checkPlayerStatus0(0, 0x10)))) { - canTransform = true; + canHowl = true; } } } } + if (!canHowl) { + Z2GetAudioMgr()->seStart(Z2SE_SYS_ERROR, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); + return; + } + getWolfHowlMgrP()->setCorrectCurve(9); procWolfHowlDemoInit(); } @@ -154,6 +159,7 @@ bool daAlink_c::checkGyroAimContext() { case PROC_BOW_SUBJECT: case PROC_BOOMERANG_SUBJECT: case PROC_COPY_ROD_SUBJECT: + case PROC_HAWK_SUBJECT: case PROC_HOOKSHOT_SUBJECT: case PROC_SWIM_HOOKSHOT_SUBJECT: case PROC_HORSE_BOW_SUBJECT: diff --git a/src/d/actor/d_a_alink_wolf.inc b/src/d/actor/d_a_alink_wolf.inc index 0a6da841c5..b2e60aea01 100644 --- a/src/d/actor/d_a_alink_wolf.inc +++ b/src/d/actor/d_a_alink_wolf.inc @@ -149,6 +149,23 @@ void daAlink_c::changeWolf() { mpLinkModel = initModel(static_cast(dComIfG_getObjectRes(l_wArcName, 14)), 0x20200); +#ifdef TARGET_PC + // Update Wolf Link's eye maxLOD to prevent the eyes from disappearing + { + J3DTexture* tex = mpLinkModel->getModelData()->getTexture(); + JUTNameTab* nametable = mpLinkModel->getModelData()->getTextureName(); + if (tex != nullptr && nametable != nullptr) { + for (u16 i = 0; i < tex->getNum(); i++) { + const char* tex_name = nametable->getName(i); + if (tex_name != NULL && strcmp(tex_name, "wl_eyeball") == 0) { + ResTIMG* timg = tex->getResTIMG(i); + timg->maxLOD = 0; + } + } + } + } +#endif + J3DModelData* chainModelData = static_cast(dComIfG_getObjectRes(l_wArcName, 15)); for (u16 i = 0; i < 4; i++) { mpWlChainModels[i] = initModel(chainModelData, 0); @@ -162,6 +179,23 @@ void daAlink_c::changeWolf() { mpWlMidnaHairModel = initModelEnv(static_cast(dComIfG_getObjectRes(l_wArcName, 11)), 0x1000000); +#ifdef TARGET_PC + // Update Midna's eye maxLOD to prevent the eyes from disappearing + { + J3DTexture* tex = mpWlMidnaModel->getModelData()->getTexture(); + JUTNameTab* nametable = mpWlMidnaModel->getModelData()->getTextureName(); + if (tex != nullptr && nametable != nullptr) { + for (u16 i = 0; i < tex->getNum(); i++) { + const char* tex_name = nametable->getName(i); + if (tex_name != NULL && strcmp(tex_name, "midona_eyeball") == 0) { + ResTIMG* timg = tex->getResTIMG(i); + timg->maxLOD = 0; + } + } + } + } +#endif + mpDMidnaBrk = static_cast(dComIfG_getObjectRes(l_wArcName, 18)); mpDMidnaBrk->searchUpdateMaterialID(mpWlMidnaModel->getModelData()); mpWlMidnaModel->getModelData()->entryTevRegAnimator(mpDMidnaBrk); @@ -342,6 +376,26 @@ void daAlink_c::changeLink(int param_0) { initModel(static_cast(dComIfG_getObjectRes(mArcName, "zl_face.bmd")), 0x20200); } +#ifdef TARGET_PC + // Update Adult Link's eye maxLOD to prevent the eyes from disappearing + { + J3DTexture* tex = mpLinkFaceModel->getModelData()->getTexture(); + JUTNameTab* nametable = mpLinkFaceModel->getModelData()->getTextureName(); + if (tex != nullptr && nametable != nullptr) { + for (u16 i = 0; i < tex->getNum(); i++) { + const char* tex_name = nametable->getName(i); + if (tex_name != nullptr && + (strcmp(tex_name, "al_eyeball") == 0 || strcmp(tex_name, "highlight02") == 0 || + strcmp(tex_name, "eye_kage01") == 0)) + { + ResTIMG* timg = tex->getResTIMG(i); + timg->maxLOD = 0; + } + } + } + } +#endif + modelData = static_cast(dComIfG_getObjectRes(mArcName, "al_bootsH.bmd")); u16 i; for (i = 0; i < 2; i++) { diff --git a/src/d/actor/d_a_b_bq.cpp b/src/d/actor/d_a_b_bq.cpp index 70ab84705b..637e82360e 100644 --- a/src/d/actor/d_a_b_bq.cpp +++ b/src/d/actor/d_a_b_bq.cpp @@ -2059,7 +2059,15 @@ static void demo_camera(b_bq_class* i_this) { for (int i = 0; i < 5; i++) { static u16 g_e_i[] = {0x83EB, 0x83EC, 0x83ED, 0x83EE, 0x83EF}; - dComIfGp_particle_set(g_e_i[i], &pos, NULL, NULL); + #if TARGET_PC + if (i == 0) { + static const cXyz effWideScale = {mDoGph_gInf_c::getAspect(), 1.0f, 1.0f}; + dComIfGp_particle_set(g_e_i[i], &pos, NULL, &effWideScale); + } else + #endif + { + dComIfGp_particle_set(g_e_i[i], &pos, NULL, NULL); + } } i_this->mSound.startCreatureSound(Z2SE_EN_BOSS_CONVERGE, 0, 0); diff --git a/src/d/actor/d_a_b_ob.cpp b/src/d/actor/d_a_b_ob.cpp index 5622375fec..f7aa9b1f0b 100644 --- a/src/d/actor/d_a_b_ob.cpp +++ b/src/d/actor/d_a_b_ob.cpp @@ -2725,7 +2725,16 @@ static void demo_camera(b_ob_class* i_this) { for (int i = 0; i < 5; i++) { static u16 ex_eff[] = {dPa_RM(ID_ZI_S_OI_CONVERGE_FILTER), dPa_RM(ID_ZI_S_OI_CONVERGE_FILTEROUT), dPa_RM(ID_ZI_S_OI_CONVERGE_HIDE), dPa_RM(ID_ZI_S_OI_CONVERGE_POLYGON_A), dPa_RM(ID_ZI_S_OI_CONVERGE_POLYGON_B)}; - dComIfGp_particle_set(ex_eff[i], &room_pos, NULL, &sc); + + #if TARGET_PC + if (i == 0) { + static const cXyz effWideScale = {mDoGph_gInf_c::getAspect() * 10.0f, 10.0f, 10.0f}; + dComIfGp_particle_set(ex_eff[i], &room_pos, NULL, &effWideScale); + } else + #endif + { + dComIfGp_particle_set(ex_eff[i], &room_pos, NULL, &sc); + } } i_this->mDemoCamEye.set(-4820.0f, -18600.0f, -510.0f); diff --git a/src/d/actor/d_a_door_shutter.cpp b/src/d/actor/d_a_door_shutter.cpp index d416d0b2bc..ebd1c9a185 100644 --- a/src/d/actor/d_a_door_shutter.cpp +++ b/src/d/actor/d_a_door_shutter.cpp @@ -19,6 +19,7 @@ #if TARGET_PC #include +#include #endif char* daDoor20_c::getStopBmdName() { diff --git a/src/d/actor/d_a_e_fm.cpp b/src/d/actor/d_a_e_fm.cpp index ca01f73c29..81a1b404f0 100644 --- a/src/d/actor/d_a_e_fm.cpp +++ b/src/d/actor/d_a_e_fm.cpp @@ -1677,7 +1677,16 @@ static void demo_camera(e_fm_class* i_this) { cXyz spBC(0.0f, 0.0f, 0.0f); for (int i = 0; i < 4; i++) { static u16 g_e_i[] = {0x847B, 0x847C, 0x847D, 0x847E}; - dComIfGp_particle_set(g_e_i[i], &spBC, NULL, NULL); + + #if TARGET_PC + if (i == 0) { + static const cXyz effWideScale = {mDoGph_gInf_c::getAspect(), 1.0f, 1.0f}; + dComIfGp_particle_set(g_e_i[i], &spBC, NULL, &effWideScale); + } else + #endif + { + dComIfGp_particle_set(g_e_i[i], &spBC, NULL, NULL); + } } i_this->mDemoCamFovy = 55.0f + NREG_F(10); diff --git a/src/d/actor/d_a_mg_fshop.cpp b/src/d/actor/d_a_mg_fshop.cpp index 1c93648eac..99f39e410e 100644 --- a/src/d/actor/d_a_mg_fshop.cpp +++ b/src/d/actor/d_a_mg_fshop.cpp @@ -761,6 +761,11 @@ static void koro2_game(fshop_class* i_this) { sp5C.x = mDoCPd_c::getStickX3D(PAD_1); sp5C.y = 0.0f; sp5C.z = mDoCPd_c::getStickY(PAD_1); +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + sp5C.x = -sp5C.x; + } +#endif MtxPosition(&sp5C, &sp68); f32 reg_f31 = sp68.x; @@ -782,20 +787,15 @@ static void koro2_game(fshop_class* i_this) { reg_f30 = 0.0f; } + s16 gyro_ax = 0; + s16 gyro_az = 0; #if TARGET_PC if (dusk::getSettings().game.enableGyroRollgoal) { - s16 rg_add_x; - s16 rg_add_z; - dusk::gyro::rollgoalTableOffset(rg_add_x, rg_add_z); - s16 tgt_x = static_cast(reg_f30 * (-6000.0f + JREG_F(7))) + rg_add_x; - s16 tgt_z = static_cast(reg_f31 * (-6000.0f + JREG_F(8))) + rg_add_z; - cLib_addCalcAngleS2(&i_this->field_0x4020.x, tgt_x, 4, 0x200); - cLib_addCalcAngleS2(&i_this->field_0x4020.z, tgt_z, 4, 0x200); + dusk::gyro::rollgoalTableOffset(gyro_ax, gyro_az); } -#else - cLib_addCalcAngleS2(&i_this->field_0x4020.x, reg_f30 * (-6000.0f + JREG_F(7)), 4, 0x200); - cLib_addCalcAngleS2(&i_this->field_0x4020.z, reg_f31 * (-6000.0f + JREG_F(8)), 4, 0x200); #endif + cLib_addCalcAngleS2(&i_this->field_0x4020.x, reg_f30 * (-6000.0f + JREG_F(7)) + gyro_ax, 4, 0x200); + cLib_addCalcAngleS2(&i_this->field_0x4020.z, reg_f31 * (-6000.0f + JREG_F(8)) + gyro_az, 4, 0x200); } #if TARGET_PC if (i_this->field_0x4010 != 2) { diff --git a/src/d/actor/d_a_mirror.cpp b/src/d/actor/d_a_mirror.cpp index 4711dee681..fb4142c0bb 100644 --- a/src/d/actor/d_a_mirror.cpp +++ b/src/d/actor/d_a_mirror.cpp @@ -30,6 +30,10 @@ static char* l_arcName = "Mirror"; static char* l_arcName2 = "MR-Table"; dMirror_packet_c::dMirror_packet_c() { +#ifdef TARGET_PC + GXInitTexObj(&mTexObj, nullptr, 0, 0, static_cast(-1), GX_MAX_TEXWRAPMODE, + GX_MAX_TEXWRAPMODE, GX_FALSE); +#endif reset(); } diff --git a/src/d/actor/d_a_movie_player.cpp b/src/d/actor/d_a_movie_player.cpp index d972bddd4d..1f8bdeecd7 100644 --- a/src/d/actor/d_a_movie_player.cpp +++ b/src/d/actor/d_a_movie_player.cpp @@ -2855,7 +2855,7 @@ void* daMP_Reader(void*) { #endif } -static u8 daMP_ReadThreadStack[0x2000]; +static u8 daMP_ReadThreadStack[DUSK_IF_ELSE(8, 0x2000)]; #if TARGET_PC static BOOL VideoThreadCancelled; @@ -2880,7 +2880,7 @@ static BOOL daMP_CreateReadThread(s32 param_0) { static OSThread daMP_VideoDecodeThread; -static u8 daMP_VideoDecodeThreadStack[0x64000]; +static u8 daMP_VideoDecodeThreadStack[DUSK_IF_ELSE(8, 0x64000)]; static OSMessageQueue daMP_FreeTextureSetQueue; @@ -3132,7 +3132,7 @@ static BOOL AudioThreadCancelled; static OSThread daMP_AudioDecodeThread; -static u8 daMP_AudioDecodeThreadStack[0x64000]; +static u8 daMP_AudioDecodeThreadStack[DUSK_IF_ELSE(8, 0x64000)]; static OSMessageQueue daMP_FreeAudioBufferQueue; @@ -4580,3 +4580,12 @@ actor_process_profile_definition g_profile_MOVIE_PLAYER = { }; AUDIO_INSTANCES; + +#if TARGET_PC +void dusk::MoviePlayerShutdown() { + // We need to cleanly shut down the threads to avoid crashes on shutdown. + if (daMP_c::m_myObj) { + daMP_c::m_myObj->daMP_c_Finish(); + } +} +#endif \ No newline at end of file diff --git a/src/d/actor/d_a_obj_item.cpp b/src/d/actor/d_a_obj_item.cpp index 0db3ac3a55..e45de1933d 100644 --- a/src/d/actor/d_a_obj_item.cpp +++ b/src/d/actor/d_a_obj_item.cpp @@ -16,6 +16,10 @@ #include "f_op/f_op_camera_mng.h" #include "m_Do/m_Do_mtx.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + static f32 Reflect(cXyz* i_vec, cBgS_PolyInfo const& i_polyinfo, f32 i_scale) { cM3dGPla plane; @@ -31,6 +35,27 @@ static f32 Reflect(cXyz* i_vec, cBgS_PolyInfo const& i_polyinfo, f32 i_scale) { return 0.0f; } +#if TARGET_PC +static void d_a_obj_item_interp_callback(bool isSimFrame, void* pUserWork) { + daItem_c* item = static_cast(pUserWork); + if (item == NULL || item->mpModel == NULL || !item->chkDraw()) { + return; + } + item->setTevStr(); + if (item->mpBrkAnm != NULL) { + s8 tevFrm = item->getTevFrm(); + if (tevFrm != -1) { + item->mpBrkAnm->entry(item->mpModel->getModelData(), tevFrm); + } else { + item->mpBrkAnm->entry(item->mpModel->getModelData()); + } + } + if (item->chkFlag(4)) { + fopAcM_setEffectMtx(item, item->mpModel->getModelData()); + } +} +#endif + const daItemBase_data& daItemBase_c::getData() { return m_data; } @@ -353,6 +378,10 @@ int daItem_c::_daItem_draw() { return 1; } +#if TARGET_PC + dusk::frame_interp::add_interpolation_callback(&d_a_obj_item_interp_callback, this); +#endif + if (chkDraw()) { return DrawBase(); } @@ -390,6 +419,9 @@ void daItem_c::procMainNormal() { cLib_chaseF(&scale.z, mItemScale.z, step_z); } + #if TARGET_PC + if (!dusk::getSettings().game.enableIndefiniteItemDrops) { + #endif if (mWaitTimer == 0) { if (mDisappearTimer == 0) { deleteItem(); @@ -399,6 +431,9 @@ void daItem_c::procMainNormal() { changeDraw(); } } + #if TARGET_PC + } + #endif mCcCyl.SetC(current.pos); dComIfG_Ccsp()->Set(&mCcCyl); @@ -1058,9 +1093,16 @@ int daItem_c::CountTimer() { if (checkCountTimer()) { if (mWaitTimer > 0) { mWaitTimer--; - } else if (mDisappearTimer > 0) { + } + #if TARGET_PC + else if (!dusk::getSettings().game.enableIndefiniteItemDrops && mDisappearTimer > 0) { mDisappearTimer--; } + #else + else if (mDisappearTimer > 0) { + mDisappearTimer--; + } + #endif } cLib_calcTimer(&mBoomWindTgTimer); diff --git a/src/d/actor/d_flower.inc b/src/d/actor/d_flower.inc index 702337d9e5..58daa354a1 100644 --- a/src/d/actor/d_flower.inc +++ b/src/d/actor/d_flower.inc @@ -699,8 +699,8 @@ void dFlower_packet_c::draw() { if (!cLib_checkBit(sp44->m_state, 4) && !cLib_checkBit(sp44->m_state, 0x40)) { #ifdef TARGET_PC Mtx flower_mtx; - if (dusk::frame_interp::lookup_replacement(reinterpret_cast(&sp44->m_modelMtx), flower_mtx)) { - + if (dusk::frame_interp::lookup_replacement(&sp44->m_modelMtx, flower_mtx)) { + cMtx_concat(j3dSys.getViewMtx(), flower_mtx, flower_mtx); GXLoadPosMtxImm(flower_mtx, 0); } else #endif @@ -854,21 +854,18 @@ void dFlower_packet_c::draw() { if (!cLib_checkBit(sp34->m_state, 4) && cLib_checkBit(sp34->m_state, 0x40)) { #ifdef TARGET_PC Mtx flower_mtx; - if (dusk::frame_interp::lookup_replacement(reinterpret_cast(&sp34->m_modelMtx), flower_mtx)) { + if (dusk::frame_interp::lookup_replacement(&sp34->m_modelMtx, flower_mtx)) { cMtx_concat(j3dSys.getViewMtx(), flower_mtx, flower_mtx); GXLoadPosMtxImm(flower_mtx, 0); - } else { + } else #endif + { GXLoadPosMtxImm(sp34->m_modelMtx, 0); -#ifdef TARGET_PC } -#endif GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0); - #if TARGET_PC GXLoadTexObj(&mTexObj_l_J_Ohana01_64128_0419TEX, GX_TEXMAP0); #endif - if (!cLib_checkBit(sp34->m_state, 8)) { if (!cLib_checkBit(sp34->m_state, 0x10)) { GXCallDisplayList(mp_Jhana01DL, m_Jhana01DL_size); @@ -995,7 +992,7 @@ void dFlower_packet_c::update() { mDoMtx_stack_c::scaleM(temp_f31, temp_f31, temp_f31); cMtx_concat(j3dSys.getViewMtx(), temp_r28, data_p->m_modelMtx); #ifdef TARGET_PC - dusk::frame_interp::record_final_mtx(mDoMtx_stack_c::get(), data_p->m_modelMtx); + dusk::frame_interp::record_final_mtx(temp_r28, data_p->m_modelMtx); #endif } } diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index 23ceb96f00..17d5f2c4e4 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -70,11 +70,7 @@ dFs_HIO_c::dFs_HIO_c() { select_icon_appear_frames = 5; appear_display_wait_frames = 15; field_0x000d = 15; - #if TARGET_PC - card_wait_frames = 0; - #else card_wait_frames = 90; - #endif test_frame_counts[0] = 1.11f; test_frame_counts[1] = 1.11f; test_frame_counts[2] = 1.11f; @@ -2367,7 +2363,7 @@ void dFile_select_c::CommandExec() { break; } - mWaitTimer = g_fsHIO.card_wait_frames; + mWaitTimer = IF_DUSK(dusk::getSettings().game.instantSaves ? 0 :) g_fsHIO.card_wait_frames; } void dFile_select_c::DataEraseWait() { @@ -4759,7 +4755,7 @@ void dFile_select_c::MemCardFormatYesSel2Disp() { bool isErrorTxtChange = errorTxtChangeAnm(); bool isYnMenuMove = yesnoMenuMoveAnm(); if (isErrorTxtChange == true && isYnMenuMove == true) { - mWaitTimer = g_fsHIO.card_wait_frames; + mWaitTimer = IF_DUSK(dusk::getSettings().game.instantSaves ? 0 :) g_fsHIO.card_wait_frames; mDoMemCd_Format(); mCardCheckProc = MEMCARDCHECKPROC_FORMAT; } @@ -4830,7 +4826,7 @@ void dFile_select_c::MemCardMakeGameFileSelDisp() { if (isErrorTxtChange == true && isYnMenuMove == true && isKetteiTxtDisp == true) { if (field_0x0268 != 0) { - mWaitTimer = g_fsHIO.card_wait_frames; + mWaitTimer = IF_DUSK(dusk::getSettings().game.instantSaves ? 0 :) g_fsHIO.card_wait_frames; setInitSaveData(); dataSave(); mCardCheckProc = MEMCARDCHECKPROC_MAKE_GAMEFILE; diff --git a/src/d/d_map_path.cpp b/src/d/d_map_path.cpp index 916bd2c223..8288a2ef9f 100644 --- a/src/d/d_map_path.cpp +++ b/src/d/d_map_path.cpp @@ -237,6 +237,13 @@ void dDrawPath_c::rendering(dDrawPath_c::line_class const* p_line) { if (isDrawType(p_line->field_0x0)) { int width = getLineWidth(p_line->field_0x1); + #if TARGET_PC + f32 height = JUTVideo::getManager()->getRenderHeight() / 448.0f; + if (height > 1.0f) { + width /= 2; + } + #endif + if (width > 0 && p_line->mDataNum >= 2) { GXSetLineWidth(width, GX_TO_ZERO); GXSetTevColor(GX_TEVREG0, *getLineColor(p_line->field_0x0 & 0x3F, p_line->field_0x1)); diff --git a/src/d/d_menu_collect.cpp b/src/d/d_menu_collect.cpp index 46c9c1a9d9..612806d372 100644 --- a/src/d/d_menu_collect.cpp +++ b/src/d/d_menu_collect.cpp @@ -2399,6 +2399,13 @@ void dMenu_Collect3D_c::_move(u8 param_0, u8 param_1) { posZ = 550.0f; } toItem3Dpos(linkPos.x, posY, posZ, &itemPos); + +#if TARGET_PC + if (dusk::getSettings().game.enableLinkDollRotation) { + const f32 angle = mDoCPd_c::getSubStickX3D(PAD_1) * 2048; + ANGLE_ADD(mLinkAngle, angle); + } else +#endif if (param_0 == 0 && param_1 == 0) { f32 temp = 450.0f; ANGLE_ADD(mLinkAngle, temp); diff --git a/src/d/d_menu_dmap.cpp b/src/d/d_menu_dmap.cpp index 66533dd12c..bb557efdac 100644 --- a/src/d/d_menu_dmap.cpp +++ b/src/d/d_menu_dmap.cpp @@ -21,6 +21,7 @@ #include "d/d_msg_string.h" #include "d/d_meter_haihai.h" #include "d/d_menu_window.h" +#include "dusk/settings.h" #include "f_op/f_op_msg_mng.h" #include "m_Do/m_Do_graphic.h" #include @@ -945,9 +946,15 @@ void dMenu_DmapBg_c::draw() { mpMeterHaihai->drawHaihai(field_0xdda, x1 + (local_224.x + local_218.x) / 2, y1 + (local_224.y + local_218.y) / 2, - -35.0f + (local_224.x - local_218.x), + -35.0f + (local_224.x - local_218.x), -35.0f + (local_224.y - local_218.y)); +#if TARGET_PC + if (!dusk::getSettings().game.enableFrameInterpolation) { + field_0xdda = 0; + } +#else field_0xdda = 0; +#endif } dMenu_Dmap_c::myclass->drawFloorScreenTop(mFloorScreen, field_0xd94, field_0xd98, grafContext); @@ -984,7 +991,36 @@ void dMenu_DmapBg_c::update() { JUT_ASSERT(2323, mpBackTexture != NULL); void* spec = mpArchive->getResource("spec/spec.dat"); + #if TARGET_PC + struct dmap_spec { + /* 0x00 */ BE(f32) field_0x0; + /* 0x04 */ BE(f32) field_0x4; + /* 0x08 */ BE(f32) field_0x8; + /* 0x0C */ u8 field_0xc; + /* 0x0D */ u8 field_0xd; + /* 0x0E */ u8 field_0xe; + /* 0x0F */ u8 field_0xf; + /* 0x10 */ u8 field_0x10; + /* 0x11 */ u8 field_0x11; + /* 0x12 */ u8 field_0x12; + /* 0x13 */ u8 field_0x13; + }; + dmap_spec* dspec = (dmap_spec*)spec; + + field_0xd80 = dspec->field_0x0; + field_0xd84 = dspec->field_0x4; + field_0xd88 = dspec->field_0x8; + field_0xd8c = dspec->field_0xc; + field_0xd8d = dspec->field_0xd; + field_0xd8e = dspec->field_0xe; + field_0xd8f = dspec->field_0xf; + field_0xd90 = dspec->field_0x10; + field_0xd91 = dspec->field_0x11; + field_0xd92 = dspec->field_0x12; + field_0xd93 = dspec->field_0x13; + #else memcpy(&field_0xd80, spec, 20); + #endif } } @@ -2545,6 +2581,11 @@ void dMenu_Dmap_c::zoomIn_proc() { } void dMenu_Dmap_c::zoomOut_init_proc() { +#if TARGET_PC + if (dusk::getSettings().game.enableFrameInterpolation) { + mpDrawBg->resetScrollArrowMask(); + } +#endif Z2GetAudioMgr()->seStart(Z2SE_SY_MAP_ZOOMOUT, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); mMapCtrl->initZoomOut(10); mpDrawBg->iconScaleAnmInit(1.0f, 0.0f, 10); diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index a3afaa1641..8692156daf 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -1769,14 +1769,19 @@ void dMenu_Fmap2DBack_c::calcBlink() { t * (g_fmapHIO.mMapBlink[i + 1].mUnselectedRegion.mBlinkSpeed - g_fmapHIO.mMapBlink[i].mUnselectedRegion.mBlinkSpeed); - field_0x1218++; - if (field_0x1218 >= selected_blink_speed) { - field_0x1218 = 0; - } +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + field_0x1218++; + if (field_0x1218 >= selected_blink_speed) { + field_0x1218 = 0; + } - field_0x121a++; - if (field_0x121a >= unselected_blink_speed) { - field_0x121a = 0; + field_0x121a++; + if (field_0x121a >= unselected_blink_speed) { + field_0x121a = 0; + } } f32 t_selected = 0.0f; diff --git a/src/d/d_menu_ring.cpp b/src/d/d_menu_ring.cpp index 529597248a..ba2b86b760 100644 --- a/src/d/d_menu_ring.cpp +++ b/src/d/d_menu_ring.cpp @@ -185,6 +185,17 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i field_0x682 = 0xc000; break; } +#if TARGET_PC + mCursorInterpPrevX = 0.0f; + mCursorInterpPrevY = 0.0f; + mCursorInterpCurrX = 0.0f; + mCursorInterpCurrY = 0.0f; + mCursorInterpPrevAngle = 0; + mCursorInterpCurrAngle = 0; + mCursorInterpPrevAngular = false; + mCursorInterpCurrAngular = false; + mCursorInterpInit = false; +#endif for (int i = 0; i < 4; i++) { field_0x674[i] = 0; #if TARGET_PC @@ -631,7 +642,71 @@ void dMenu_Ring_c::_draw() { } else { drawSelectItem(); drawItem2(); +#if TARGET_PC + f32 simX = 0.0f; + f32 simY = 0.0f; + bool restoreSimPos = false; + if (dusk::frame_interp::is_enabled() && mAlphaRate >= 1.0f) { + simX = mpDrawCursor->getPositionX(); + simY = mpDrawCursor->getPositionY(); + + const bool isAngular = (mStatus == STATUS_MOVE) && !mDirectSelectActive; + + if (dusk::frame_interp::get_ui_tick_pending()) { + mCursorInterpPrevX = mCursorInterpCurrX; + mCursorInterpPrevY = mCursorInterpCurrY; + mCursorInterpPrevAngle = mCursorInterpCurrAngle; + mCursorInterpPrevAngular = mCursorInterpCurrAngular; + + mCursorInterpCurrX = simX; + mCursorInterpCurrY = simY; + mCursorInterpCurrAngle = field_0x66e; + mCursorInterpCurrAngular = isAngular; + + // reset prev = curr for first render pass or + // when angle modes prev/curr differ + // to prevent arrival jitter + if (!mCursorInterpInit || + mCursorInterpPrevAngular != mCursorInterpCurrAngular) { + mCursorInterpPrevX = mCursorInterpCurrX; + mCursorInterpPrevY = mCursorInterpCurrY; + mCursorInterpPrevAngle = mCursorInterpCurrAngle; + mCursorInterpPrevAngular = mCursorInterpCurrAngular; + mCursorInterpInit = true; + } + } + if (mCursorInterpInit) { + const f32 step = dusk::frame_interp::get_interpolation_step(); + if (mCursorInterpPrevAngular && mCursorInterpCurrAngular) { + const s16 delta = mCursorInterpCurrAngle - mCursorInterpPrevAngle; + const s16 lerpedAngle = mCursorInterpPrevAngle + (s16)(delta * step); + + // yoinked from stick_move_proc() + const f32 x = g_ringHIO.mItemRingPosX + FB_WIDTH_BASE / 2 + + mRingRadiusH * cM_ssin(lerpedAngle); + const f32 y = g_ringHIO.mItemRingPosY + FB_HEIGHT_BASE / 2 + + mRingRadiusV * cM_scos(lerpedAngle); + mpDrawCursor->setPos(x, y); + } else { + mpDrawCursor->setPos( + mCursorInterpPrevX + (mCursorInterpCurrX - mCursorInterpPrevX) * step, + mCursorInterpPrevY + (mCursorInterpCurrY - mCursorInterpPrevY) * step + ); + } + restoreSimPos = true; + } + } else { + mCursorInterpInit = false; + } +#endif mpDrawCursor->draw(); +#if TARGET_PC + // prevents offsetting at destination on the next frame + // since stick_wait_proc doesn't call setPos and we clobbered mPositionX/Y + if (restoreSimPos) { + mpDrawCursor->setPos(simX, simY); + } +#endif mpItemExplain->trans(mCenterPosX, mCenterPosY); mpItemExplain->draw((J2DOrthoGraph*)grafPort); drawFlag0(); diff --git a/src/d/d_menu_save.cpp b/src/d/d_menu_save.cpp index 8b1d2e87b3..1f9d11b63c 100644 --- a/src/d/d_menu_save.cpp +++ b/src/d/d_menu_save.cpp @@ -18,6 +18,7 @@ #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_graphic.h" #include "d/d_msg_scrn_explain.h" +#include "dusk/frame_interpolation.h" #include "dusk/settings.h" #include "JSystem/J2DGraph/J2DAnmLoader.h" #include "f_op/f_op_msg_mng.h" @@ -715,7 +716,9 @@ void dMenu_save_c::_move() { } (this->*MenuSaveProc[mMenuProc])(); +#if !TARGET_PC saveSelAnm(); +#endif if (mWarning != NULL) { mWarning->_move(); @@ -732,36 +735,46 @@ void dMenu_save_c::saveSelAnm() { } void dMenu_save_c::selFileWakuAnm() { - mFileWakuAnmFrame += 2; - if (mFileWakuAnmFrame >= mpFileWakuAnm->getFrameMax()) { - mFileWakuAnmFrame -= mpFileWakuAnm->getFrameMax(); +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + mFileWakuAnmFrame += 2; + if (mFileWakuAnmFrame >= mpFileWakuAnm->getFrameMax()) { + mFileWakuAnmFrame -= mpFileWakuAnm->getFrameMax(); + } + + mFileWakuRotAnmFrame += 2; + if (mFileWakuRotAnmFrame >= mpFileWakuRotAnm->getFrameMax()) { + mFileWakuRotAnmFrame -= mpFileWakuRotAnm->getFrameMax(); + } } mpFileWakuAnm->setFrame(mFileWakuAnmFrame); - - mFileWakuRotAnmFrame += 2; - if (mFileWakuRotAnmFrame >= mpFileWakuRotAnm->getFrameMax()) { - mFileWakuRotAnmFrame -= mpFileWakuRotAnm->getFrameMax(); - } mpFileWakuRotAnm->setFrame(mFileWakuRotAnmFrame); } void dMenu_save_c::bookIconAnm() { - field_0x154 += 2; - if (field_0x154 >= field_0x150->getFrameMax()) { - field_0x154 -= field_0x150->getFrameMax(); +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + field_0x154 += 2; + if (field_0x154 >= field_0x150->getFrameMax()) { + field_0x154 -= field_0x150->getFrameMax(); + } + + field_0x15c += 2; + if (field_0x15c >= field_0x158->getFrameMax()) { + field_0x15c -= field_0x158->getFrameMax(); + } + + field_0x164 += 2; + if (field_0x164 >= field_0x160->getFrameMax()) { + field_0x164 -= field_0x160->getFrameMax(); + } } field_0x150->setFrame(field_0x154); - - field_0x15c += 2; - if (field_0x15c >= field_0x158->getFrameMax()) { - field_0x15c -= field_0x158->getFrameMax(); - } field_0x158->setFrame(field_0x15c); - - field_0x164 += 2; - if (field_0x164 >= field_0x160->getFrameMax()) { - field_0x164 -= field_0x160->getFrameMax(); - } field_0x160->setFrame(field_0x164); } @@ -2809,6 +2822,9 @@ void dMenu_save_c::menuSaveWide() { void dMenu_save_c::_draw2() { if (field_0x21a1 == 0) { +#if TARGET_PC + saveSelAnm(); +#endif if (mpScrnExplain != NULL) { dComIfGd_set2DOpa(&mMenuSaveExplain); } diff --git a/src/d/d_msg_class.cpp b/src/d/d_msg_class.cpp index 37085a6bb9..7af18f0732 100644 --- a/src/d/d_msg_class.cpp +++ b/src/d/d_msg_class.cpp @@ -1987,6 +1987,13 @@ bool jmessage_tSequenceProcessor::do_isReady() { } #endif +#if TARGET_PC + if (dusk::getSettings().game.instantText && mDoCPd_c::getHoldB(0)) { + field_0xb2 = 1; + pReference->setSendTimer(0); + } +#endif + if (dComIfGp_checkMesgBgm()) { bool isItemMusicPlaying = true; if (mDoAud_checkPlayingSubBgmFlag() != Z2BGM_ITEM_GET && diff --git a/src/d/d_s_play.cpp b/src/d/d_s_play.cpp index 19c7d37c6d..b549bd073d 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 diff --git a/src/d/d_save.cpp b/src/d/d_save.cpp index 71f65b61e6..2b7358df87 100644 --- a/src/d/d_save.cpp +++ b/src/d/d_save.cpp @@ -28,6 +28,7 @@ #if TARGET_PC #include "dusk/settings.h" #include +#include #endif static u8 dSv_item_rename(u8 i_itemNo) { diff --git a/src/dusk/audio/DuskDsp.cpp b/src/dusk/audio/DuskDsp.cpp index bcad272a47..f576a5d0f1 100644 --- a/src/dusk/audio/DuskDsp.cpp +++ b/src/dusk/audio/DuskDsp.cpp @@ -5,14 +5,15 @@ #include #include +#include #include #include #include "Adpcm.hpp" #include "freeverb/revmodel.hpp" -#include "JSystem/JAudio2/JASDriverIF.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/endian.h" +#include "dusk/logging.h" #include "global.h" #include "tracy/Tracy.hpp" @@ -95,6 +96,13 @@ static void RenderChannel( ChannelAuxData& channelAux, OutputSubframe& subframe); +static void RenderOutputChannel( + const JASDsp::TChannel& sourceChannel, + ChannelAuxData& aux, + OutputChannel outputChannel, + const std::span inputSamples, + OutputSubframe& fullOutputSubframe); + /** * Converts a pitch value on a DSP channel to a sample rate. */ @@ -117,6 +125,8 @@ static void ResetChannel(JASDsp::TChannel& channel, ChannelAuxData& aux) { aux.resamplePos = 0.0; aux.resamplePrev = 0; + aux.oscPhase = 0; + aux.prev_lp_out = 0.0f; aux.prev_lp_in = 0.0f; @@ -141,6 +151,119 @@ static void MixSubframe(DspSubframe& dst, const DspSubframe& src) { } } +enum class OscType : u16 { + SQUARE_WAVE_PW_50 = 0, + SAW_WAVE = 1, + SQUARE_WAVE_PW_25 = 3, + TRIANGLE_WAVE = 4, + // idk what 5 and 6 are + SINE_WAVE = 7, + // idk what 8 and 9 are + SINE_WAVE_VAR_STEP = 10, + EVOLVING_HARMONIC = 11, + EVOLVING_RAMP = 12, +}; + +static s16 gEvolvingHarmonic[64]; + +static void GenerateEvolvingHarmonic() { + static bool initialized = false; + if (!initialized) { + gEvolvingHarmonic[62] = 8191; + gEvolvingHarmonic[63] = 16383; + initialized = true; + } + + u32 prev2 = (u32)gEvolvingHarmonic[62]; + u32 prev1 = (u32)gEvolvingHarmonic[63]; + + for (int i = 0; i < 64; i += 2) { + u32 cur = (u32)gEvolvingHarmonic[i]; + gEvolvingHarmonic[i] = (s16)((s32)(prev2 * prev1 - (cur << 16)) >> 16); + prev2 = prev1; + prev1 = cur; + + cur = (u32)gEvolvingHarmonic[i + 1]; + gEvolvingHarmonic[i + 1] = (s16)((s32)(2u * (prev2 * prev1 + (cur << 16))) >> 16); + prev2 = prev1; + prev1 = cur; + } +} + + +static void RenderOscChannel( + JASDsp::TChannel& channel, + ChannelAuxData& channelAux, + OutputSubframe& subframe) { + if (channel.mResetFlag) + ResetChannel(channel, channelAux); + + const u32 pitch = channel.mPitch; + DspSubframe buf = {}; + const auto oscType = static_cast(channel.mBytesPerBlock); + + switch (oscType) { + case OscType::SQUARE_WAVE_PW_50: { + std::generate(buf.begin(), buf.end(), [&] { + f32 s = channelAux.oscPhase < 0x8000u ? 0.5f : -0.5f; + channelAux.oscPhase += pitch >> 1; + return s; + }); + break; + } + case OscType::SQUARE_WAVE_PW_25: { + std::generate(buf.begin(), buf.end(), [&] { + f32 s = channelAux.oscPhase < 0x4000u ? 0.5f : -0.5f; + channelAux.oscPhase += pitch >> 1; + return s; + }); + break; + } + case OscType::SAW_WAVE: + case OscType::EVOLVING_RAMP: { + std::generate(buf.begin(), buf.end(), [&] { + f32 s = (f32)(s16)channelAux.oscPhase / 32768.0f; + channelAux.oscPhase += pitch >> 1; + return s; + }); + break; + } + case OscType::SINE_WAVE: + case OscType::SINE_WAVE_VAR_STEP: { + std::generate(buf.begin(), buf.end(), [&] { + f32 s = sinf((f32)channelAux.oscPhase * (2.0f * M_PI / 65536.0f)) * 0.5f; + channelAux.oscPhase += pitch >> 1; + return s; + }); + break; + } + case OscType::TRIANGLE_WAVE: { + std::generate(buf.begin(), buf.end(), [&] { + f32 s = 0.5f - fabsf((f32)(s16)channelAux.oscPhase / 32768.0f); + channelAux.oscPhase += pitch >> 1; + return s; + }); + break; + } + case OscType::EVOLVING_HARMONIC: { + std::generate(buf.begin(), buf.end(), [&] { + f32 s = gEvolvingHarmonic[channelAux.oscPhase >> 10] / 32768.0f; + channelAux.oscPhase += pitch >> 1; + return s; + }); + break; + } + default: + DuskLog.error("RenderOscChannel: unimplemented oscillator type {}", channel.mBytesPerBlock); + break; + } + + auto samples = std::span(buf).subspan(0, DSP_SUBFRAME_SIZE); + RenderOutputChannel(channel, channelAux, OutputChannel::LEFT, samples, subframe); + RenderOutputChannel(channel, channelAux, OutputChannel::RIGHT, samples, subframe); +} + + void dusk::audio::DspRender(OutputSubframe& subframe) { ZoneScoped; if (DumpAudio != sDumpWasActive) { @@ -152,6 +275,8 @@ void dusk::audio::DspRender(OutputSubframe& subframe) { } } + GenerateEvolvingHarmonic(); + std::span channels(JASDsp::CH_BUF, DSP_CHANNELS); DspSubframe reverbInputL = {}; @@ -174,17 +299,14 @@ void dusk::audio::DspRender(OutputSubframe& subframe) { channel.mIsFinished = true; continue; } - else if (channel.mWaveAramAddress == 0) { - // I think these are oscillator channels? Not backed by audio. - // No idea how to implement these yet, so skip them. - channel.mIsFinished = true; - continue; - } - - ValidateChannel(channel); OutputSubframe channelSubframe = {}; - RenderChannel(channel, channelAux, channelSubframe); + if (channel.mWaveAramAddress == 0) { + RenderOscChannel(channel, channelAux, channelSubframe); + } else { + ValidateChannel(channel); + RenderChannel(channel, channelAux, channelSubframe); + } if (EnableReverb) { // scale the input to the reverb rather than using wet/dry on the output. diff --git a/src/dusk/audio/DuskDsp.hpp b/src/dusk/audio/DuskDsp.hpp index 3ca90d6311..8000e627a1 100644 --- a/src/dusk/audio/DuskDsp.hpp +++ b/src/dusk/audio/DuskDsp.hpp @@ -53,6 +53,9 @@ namespace dusk::audio { // last consumed sample from decodeBuf s16 resamplePrev; + // phase of oscillator channels + u16 oscPhase; + // low pass previous state f32 prev_lp_out; // out[n-1] f32 prev_lp_in; // in[n-1] diff --git a/src/dusk/autosave.cpp b/src/dusk/autosave.cpp new file mode 100644 index 0000000000..1fd1248659 --- /dev/null +++ b/src/dusk/autosave.cpp @@ -0,0 +1,85 @@ +#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 && dComIfGp_getStartStageLayer() != 0) + { + mAutoSaveProc = 1; + } +} + +void updateAutoSave() { + (AutoSaveFuncsProc[mAutoSaveProc])(); +} + +void writeAutoSave() { + 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.ShowToast("Saving...", 2.0f); + mAutoSaveProc = 0; +} \ No newline at end of file diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index c377d1ba54..176967359d 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -10,7 +10,7 @@ #include #include -#include "dusk/dusk.h" +#include "dusk/main.h" using namespace dusk::config; @@ -24,7 +24,7 @@ static absl::flat_hash_map RegisteredConfigVar static bool RegistrationDone = false; static std::string GetConfigJsonPath() { - return fmt::format("{}{}", configPath, ConfigFileName); + return (dusk::ConfigPath / ConfigFileName).string(); } ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl) : name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) { diff --git a/src/dusk/crash_reporting.cpp b/src/dusk/crash_reporting.cpp index 73f432e418..0499f0d6a1 100644 --- a/src/dusk/crash_reporting.cpp +++ b/src/dusk/crash_reporting.cpp @@ -3,6 +3,7 @@ #include "dusk/app_info.hpp" #include "dusk/dusk.h" #include "dusk/logging.h" +#include "dusk/main.h" #include "dusk/settings.h" #include "version.h" @@ -66,7 +67,7 @@ std::string GetReleaseName() { } std::filesystem::path GetSentryDatabasePath() { - return std::filesystem::path(configPath) / "sentry"; + return dusk::ConfigPath / "sentry"; } std::filesystem::path GetLogAttachmentPath() { diff --git a/src/dusk/extras.c b/src/dusk/extras.c index e693985479..a1fb26e998 100644 --- a/src/dusk/extras.c +++ b/src/dusk/extras.c @@ -3,6 +3,27 @@ #include #include +#ifdef _MSC_VER +#include +#endif + +void __dcbz(void* addr, int offset) { + // Gekko cache lines are 32 bytes. + // dcbz usually requires addr to be 32-byte aligned. + memset((char*)addr + offset, 0, 32); +} + +int __cntlzw(unsigned int val) { + if (val == 0) return 32; // PowerPC returns 32 if the input is 0 +#ifdef _MSC_VER + unsigned long idx; + _BitScanReverse(&idx, val); + return 31 - (int)idx; +#else + return __builtin_clz(val); +#endif +} + #ifndef _MSC_VER int stricmp(const char* str1, const char* str2) { char a_var; @@ -48,11 +69,6 @@ int strnicmp(const char* str1, const char* str2, int n) { } #endif - -void *_memcpy(void* dest, void const* src, int n) { - return memcpy(dest, src, n); -} - void DCZeroRange(void* addr, uint32_t nBytes) { #if defined(_MSC_VER) || TARGET_ANDROID memset(addr, 0, nBytes); diff --git a/src/dusk/extras.cpp b/src/dusk/extras.cpp deleted file mode 100644 index de402f028b..0000000000 --- a/src/dusk/extras.cpp +++ /dev/null @@ -1,27 +0,0 @@ -// C++ Mangled version of extras.c -#include -#include -#ifdef _MSC_VER -#include -#endif - -void *__memcpy(void* dest, void const* src, int n) { - return memcpy(dest, src, n); -} - -void __dcbz(void* addr, int offset) { - // Gekko cache lines are 32 bytes. - // dcbz usually requires addr to be 32-byte aligned. - memset((char*)addr + offset, 0, 32); -} - -int __cntlzw(unsigned int val) { - if (val == 0) return 32; // PowerPC returns 32 if the input is 0 -#ifdef _MSC_VER - unsigned long idx; - _BitScanReverse(&idx, val); - return 31 - (int)idx; -#else - return __builtin_clz(val); -#endif -} diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index c72923da7d..f3f8a842e1 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -127,14 +127,20 @@ void ensure_initialized() { s_initialized = true; } +void begin_sim_tick() { + ensure_initialized(); + if (!g_enabled) { + return; + } + + s_interpolationCallBackWork.clear(); + s_cam_prev = std::move(s_cam_curr); +} + void begin_frame(bool enabled, bool is_sim_frame, float step) { g_enabled = enabled; g_is_sim_frame = is_sim_frame; g_step = std::clamp(step, 0.0f, 1.0f); - if (is_sim_frame) { - s_interpolationCallBackWork.clear(); - s_cam_prev = std::move(s_cam_curr); - } } bool is_enabled() { @@ -408,10 +414,8 @@ void begin_presentation_camera() { } mDoLib_clipper::setup(view->fovy, view->aspect, view->near_, far_); - -#if WIDESCREEN_SUPPORT - mDoGph_gInf_c::offWideZoom(); -#endif + + // FRAME INTERP NOTE: Removed the call to offWideZoom that was here, it causes problems with presentation during cutscenes. s_presentation_depth = 1; diff --git a/src/dusk/game_clock.cpp b/src/dusk/game_clock.cpp index a262d0283c..8b887f610c 100644 --- a/src/dusk/game_clock.cpp +++ b/src/dusk/game_clock.cpp @@ -5,62 +5,84 @@ #include #include -namespace dusk { -namespace game_clock { +namespace dusk::game_clock { using clock = std::chrono::steady_clock; bool s_initialized = false; clock::time_point s_previous_sample{}; -float s_sim_accumulator = 0.0f; +clock::time_point s_current_snapshot_time{}; std::unordered_map s_interval_last_sample; +constexpr clock::duration kSimPeriodDuration = + std::chrono::duration_cast(std::chrono::duration(sim_pace())); +constexpr clock::duration kAbnormalGapResetThreshold = std::chrono::milliseconds(250); +constexpr int kMaxSimTicksPerFrame = 2; + void ensure_initialized() { if (s_initialized) { return; } s_previous_sample = clock::now(); - s_sim_accumulator = sim_pace(); + s_current_snapshot_time = s_previous_sample; s_initialized = true; } -void reset_accumulator() { - ensure_initialized(); - s_sim_accumulator = fmodf(s_sim_accumulator, sim_pace()); -} - void reset_frame_timer() { s_previous_sample = clock::now(); - s_sim_accumulator = 0.0f; + s_current_snapshot_time = s_previous_sample - kSimPeriodDuration; } MainLoopPacer advance_main_loop() { ensure_initialized(); const clock::time_point now = clock::now(); - const float presentation_dt = std::chrono::duration(now - s_previous_sample).count(); + const clock::duration frame_gap = now - s_previous_sample; + const float presentation_dt = std::chrono::duration(frame_gap).count(); s_previous_sample = now; - s_sim_accumulator += presentation_dt; - MainLoopPacer out{}; out.presentation_dt_seconds = presentation_dt; - const bool should_interpolate = dusk::getSettings().game.enableFrameInterpolation && !dusk::getTransientSettings().skipFrameRateLimit; + const bool should_interpolate = dusk::getSettings().game.enableFrameInterpolation && + !dusk::getTransientSettings().skipFrameRateLimit; out.is_interpolating = should_interpolate; out.sim_pace = sim_pace(); if (!should_interpolate) { - s_sim_accumulator = 0.0f; - out.do_sim_tick = true; - out.interpolation_step = 0.0f; - return out; - } else { - out.do_sim_tick = s_sim_accumulator >= sim_pace(); - out.interpolation_step = out.do_sim_tick ? 0.0f : s_sim_accumulator / sim_pace(); + s_current_snapshot_time = now; + out.sim_ticks_to_run = 1; return out; } + + if (frame_gap > kAbnormalGapResetThreshold) { + s_current_snapshot_time = now - kSimPeriodDuration; + out.sim_ticks_to_run = 0; + return out; + } + + int sim_ticks_to_run = 0; + clock::time_point projected_snapshot_time = s_current_snapshot_time; + const clock::time_point render_time = now - kSimPeriodDuration; + while (sim_ticks_to_run < kMaxSimTicksPerFrame && projected_snapshot_time < render_time) { + projected_snapshot_time += kSimPeriodDuration; + sim_ticks_to_run++; + } + out.sim_ticks_to_run = sim_ticks_to_run; + return out; +} + +void commit_sim_tick() { + ensure_initialized(); + s_current_snapshot_time += kSimPeriodDuration; +} + +float sample_interpolation_step() { + ensure_initialized(); + const float step = + std::chrono::duration(clock::now() - s_current_snapshot_time).count() / sim_pace(); + return std::clamp(step, 0.0f, 1.0f); } float consume_interval(const void* consumer) { @@ -78,5 +100,4 @@ float consume_interval(const void* consumer) { return dt; } -} // namespace game_clock -} // namespace dusk +} // namespace dusk::game_clock diff --git a/src/dusk/gamepad_color.cpp b/src/dusk/gamepad_color.cpp new file mode 100644 index 0000000000..cba32da632 --- /dev/null +++ b/src/dusk/gamepad_color.cpp @@ -0,0 +1,106 @@ +#include +#include +#include +#include +#include +#include + +cXyz currentGamepadColor = {0, 0, 0}; +cXyz finalGamepadColor = {0, 0, 0}; +cXyz additionalGamepadColor = {0, 0, 0}; + +float lerpSpeed = 0.0f; + +const cXyz duskColor = {50, 50, -50}; +const cXyz noColor = {0, 0, 0}; + +cXyz LerpColor(cXyz a, cXyz b, float t) { + return {std::lerp(a.x, b.x, t), std::lerp(a.y, b.y, t), std::lerp(a.z, b.z, t)}; +} + +void FadeLED(cXyz newColor, float speed) { + finalGamepadColor = newColor; + lerpSpeed = speed / 30.0f; +} + +void SetLED(cXyz newColor) { + currentGamepadColor = newColor; + finalGamepadColor = newColor; +} + +void SetGamepadAdditionalColor(cXyz addColor) { + additionalGamepadColor.x = addColor.x; + additionalGamepadColor.y = addColor.y; + additionalGamepadColor.z = addColor.z; +} + +void handleGamepadColor() { + bool setColor = false; + + fopAc_ac_c* zhint = dComIfGp_att_getZHint(); + if (zhint != NULL) { + FadeLED({50, 50, 175}, 2.0f); + setColor = true; + } + + daPy_py_c* player = daPy_getPlayerActorClass(); + daAlink_c* link = daAlink_getAlinkActorClass(); + + if (link != nullptr && !setColor) { + if (link->checkWolf()) { + FadeLED({115, 115, 75}, 5.0f); + setColor = true; + } else { + switch (dComIfGs_getSelectEquipClothes()) { + case dItemNo_WEAR_KOKIRI_e: + FadeLED({0, 100, 0}, 5.0f); + setColor = true; + break; + case dItemNo_WEAR_ZORA_e: + FadeLED({0, 0, 100}, 5.0f); + setColor = true; + break; + case dItemNo_ARMOR_e: + if (link->checkMagicArmorHeavy()) { + FadeLED({5, 100, 100}, 5.0f); + } else { + FadeLED({100, 0, 5}, 5.0f); + } + setColor = true; + break; + case dItemNo_WEAR_CASUAL_e: + FadeLED({235, 230, 115}, 5.0f); + setColor = true; + break; + } + } + } + + if (dKy_darkworld_check()) { + SetGamepadAdditionalColor(duskColor); + } else { + SetGamepadAdditionalColor(noColor); + } + + f32 finalRed = finalGamepadColor.x + additionalGamepadColor.x; + f32 finalGreen = finalGamepadColor.y + additionalGamepadColor.y; + f32 finalBlue = finalGamepadColor.z + additionalGamepadColor.z; + + if (finalRed > 255) + finalRed = 255; + if (finalRed < 0) + finalRed = 0; + + if (finalGreen > 255) + finalGreen = 255; + if (finalGreen < 0) + finalGreen = 0; + + if (finalBlue > 255) + finalBlue = 255; + if (finalBlue < 0) + finalBlue = 0; + + currentGamepadColor = LerpColor(currentGamepadColor, cXyz{finalRed, finalGreen, finalBlue}, lerpSpeed); + PADSetColor(PAD_CHAN0, (u8)currentGamepadColor.x, (u8)currentGamepadColor.y, (u8)currentGamepadColor.z); +} \ No newline at end of file diff --git a/src/dusk/gyro.cpp b/src/dusk/gyro.cpp index d390c9c8b4..abe22909c1 100644 --- a/src/dusk/gyro.cpp +++ b/src/dusk/gyro.cpp @@ -1,16 +1,29 @@ #include "dusk/gyro.h" #include "d/actor/d_a_alink.h" +#include namespace dusk::gyro { namespace { -constexpr s32 kRollgoalTableMaxOffset = 12000; +constexpr s32 kRollgoalTableMaxOffset = 6500; constexpr float kGyroEmaAlphaMin = 0.05f; constexpr float kGyroEmaAlphaMax = 1.0f; +// Smooth gravity separately so the yaw/roll blend doesn't twitch with raw accel noise. +constexpr float kGravityEmaAlpha = 0.1f; +constexpr float kMinGravityProjection = 0.2f; +// Let roll contribute more strongly as the pad approaches an upright posture. +constexpr float kRollAimBoostMax = 2.0f; bool s_sensor_enabled = false; +bool s_accel_enabled = false; +bool s_was_aiming = false; +bool s_have_gravity_baseline = false; float s_smooth_gx = 0.0f; float s_smooth_gy = 0.0f; float s_smooth_gz = 0.0f; +float s_gravity_y = 0.0f; +float s_gravity_z = 0.0f; +float s_baseline_gravity_y = 0.0f; +float s_baseline_gravity_z = 0.0f; float s_yaw_rad = 0.0f; float s_pitch_rad = 0.0f; float s_roll_rad = 0.0f; @@ -19,6 +32,10 @@ s32 s_rollgoal_az = 0; void reset_filter_state() { s_smooth_gx = s_smooth_gy = s_smooth_gz = 0.0f; + s_gravity_y = s_gravity_z = 0.0f; + s_baseline_gravity_y = s_baseline_gravity_z = 0.0f; + s_was_aiming = false; + s_have_gravity_baseline = false; s_yaw_rad = s_pitch_rad = s_roll_rad = 0.0f; s_rollgoal_ax = s_rollgoal_az = 0; } @@ -49,15 +66,30 @@ bool queryGyroAimContext() { } void read(float dt) { - if (!s_sensor_keep_alive && !queryGyroAimContext()) { + const bool aim_active = queryGyroAimContext(); + const bool aim_just_started = aim_active && !s_was_aiming; + const bool aim_just_ended = !aim_active && s_was_aiming; + s_was_aiming = aim_active; + + if (!s_sensor_keep_alive && !aim_active) { if (s_sensor_enabled) { PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_GYRO, FALSE); s_sensor_enabled = false; } + if (s_accel_enabled) { + PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, FALSE); + s_accel_enabled = false; + } reset_filter_state(); return; } + if (aim_just_started || aim_just_ended) { + s_gravity_y = s_gravity_z = 0.0f; + s_baseline_gravity_y = s_baseline_gravity_z = 0.0f; + s_have_gravity_baseline = false; + } + if (!s_sensor_enabled) { if (!PADHasSensor(PAD_CHAN0, PAD_SENSOR_GYRO)) { return; @@ -68,6 +100,13 @@ void read(float dt) { s_sensor_enabled = true; } + if (!s_accel_enabled && PADHasSensor(PAD_CHAN0, PAD_SENSOR_ACCEL) && + PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, TRUE)) + { + // We only need accel for the gravity-aware yaw/roll mix. + s_accel_enabled = true; + } + f32 gyro[3]; if (!PADGetSensorData(PAD_CHAN0, PAD_SENSOR_GYRO, gyro, 3)) { return; @@ -80,9 +119,50 @@ void read(float dt) { s_smooth_gy += smooth_alpha * (gyro[1] - s_smooth_gy); s_smooth_gz += smooth_alpha * (gyro[2] - s_smooth_gz); - s_pitch_rad = -apply_deadband(s_smooth_gx, deadband) * dt * dusk::getSettings().game.gyroSensitivityX; - s_yaw_rad = apply_deadband(s_smooth_gy, deadband) * dt * dusk::getSettings().game.gyroSensitivityY; - s_roll_rad = apply_deadband(s_smooth_gz, deadband) * dt * dusk::getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X + const float pitch_rate = apply_deadband(s_smooth_gx, deadband); + const float yaw_rate = apply_deadband(s_smooth_gy, deadband); + const float roll_rate = apply_deadband(s_smooth_gz, deadband); + + s_pitch_rad = -pitch_rate * dt * dusk::getSettings().game.gyroSensitivityX; + s_roll_rad = roll_rate * dt * dusk::getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X + + float horizontal_rate = yaw_rate; + if (aim_active && s_accel_enabled) { + f32 accel[3]; + if (PADGetSensorData(PAD_CHAN0, PAD_SENSOR_ACCEL, accel, 3)) { + if (!s_have_gravity_baseline) { + s_gravity_y = accel[1]; + s_gravity_z = accel[2]; + } else { + s_gravity_y += kGravityEmaAlpha * (accel[1] - s_gravity_y); + s_gravity_z += kGravityEmaAlpha * (accel[2] - s_gravity_z); + } + + // Compare the current gravity projection against the gravity vector from + // aim start so the user's resting hold angle becomes the neutral baseline. + const float gravity_yz_len = std::sqrt((s_gravity_y * s_gravity_y) + (s_gravity_z * s_gravity_z)); + if (gravity_yz_len >= kMinGravityProjection) { + const float current_gravity_y = s_gravity_y / gravity_yz_len; + const float current_gravity_z = s_gravity_z / gravity_yz_len; + + if (!s_have_gravity_baseline) { + s_baseline_gravity_y = current_gravity_y; + s_baseline_gravity_z = current_gravity_z; + s_have_gravity_baseline = true; + } + + const float yaw_weight = + (s_baseline_gravity_y * current_gravity_y) + (s_baseline_gravity_z * current_gravity_z); + const float roll_weight = + (s_baseline_gravity_y * current_gravity_z) - (s_baseline_gravity_z * current_gravity_y); + const float roll_mix = std::fabs(roll_weight); + const float roll_boost = 1.0f + (roll_mix * (kRollAimBoostMax - 1.0f)); + horizontal_rate = (yaw_rate * yaw_weight) + (roll_rate * roll_weight * roll_boost); + } + } + } + + s_yaw_rad = horizontal_rate * dt * dusk::getSettings().game.gyroSensitivityY; s_pitch_rad = dusk::getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad; s_yaw_rad = dusk::getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad; diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 99a48e656e..4458cbfe86 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -305,6 +305,10 @@ namespace dusk { ImGuiMenuGame::ToggleFullscreen(); } + if (ImGui::IsKeyPressed(ImGuiKey_Escape) && getSettings().video.enableFullscreen) { + ImGuiMenuGame::ToggleFullscreen(); + } + if (!dusk::IsGameLaunched) { m_preLaunchWindow.draw(); } @@ -319,9 +323,11 @@ namespace dusk { } } + // The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg, + // so make the window bg fully transparent temporarily + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); if (showMenu && ImGui::BeginMainMenuBar()) { m_menuGame.draw(); - m_menuEnhancements.draw(); m_menuTools.draw(); const auto fpsLabel = @@ -336,6 +342,7 @@ namespace dusk { ImGui::EndMainMenuBar(); } + ImGui::PopStyleColor(); if (!getSettings().backend.wasPresetChosen) { m_firstRunPreset.draw(); diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index 2e8bd7497b..5250abc58d 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -9,7 +9,6 @@ #include #include "ImGuiFirstRunPreset.hpp" -#include "ImGuiMenuEnhancements.hpp" #include "ImGuiMenuGame.hpp" #include "ImGuiMenuTools.hpp" #include "ImGuiPreLaunchWindow.hpp" @@ -54,7 +53,6 @@ private: ImGuiFirstRunPreset m_firstRunPreset; ImGuiMenuGame m_menuGame; - ImGuiMenuEnhancements m_menuEnhancements; ImGuiPreLaunchWindow m_preLaunchWindow; // Keep always last diff --git a/src/dusk/imgui/ImGuiEngine.cpp b/src/dusk/imgui/ImGuiEngine.cpp index 46bfa8f3ab..4b6a7fb531 100644 --- a/src/dusk/imgui/ImGuiEngine.cpp +++ b/src/dusk/imgui/ImGuiEngine.cpp @@ -126,7 +126,7 @@ void ImGuiEngine_Initialize(float scale) { auto* colors = style.Colors; colors[ImGuiCol_Text] = ImVec4(0.95f, 0.96f, 0.98f, 1.00f); colors[ImGuiCol_TextDisabled] = ImVec4(0.36f, 0.42f, 0.47f, 1.00f); - colors[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.15f, 0.17f, 0.98f); colors[ImGuiCol_ChildBg] = ImVec4(0.15f, 0.18f, 0.22f, 1.00f); colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.94f); colors[ImGuiCol_Border] = ImVec4(0.08f, 0.10f, 0.12f, 1.00f); @@ -137,7 +137,7 @@ void ImGuiEngine_Initialize(float scale) { colors[ImGuiCol_TitleBg] = ImVec4(0.09f, 0.12f, 0.14f, 0.65f); colors[ImGuiCol_TitleBgActive] = ImVec4(0.08f, 0.10f, 0.12f, 1.00f); colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f); - colors[ImGuiCol_MenuBarBg] = ImVec4(0.15f, 0.18f, 0.22f, 1.00f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.15f, 0.18f, 0.22f, 0.80f); colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.39f); colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f); colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.18f, 0.22f, 0.25f, 1.00f); diff --git a/src/dusk/imgui/ImGuiMenuEnhancements.cpp b/src/dusk/imgui/ImGuiMenuEnhancements.cpp deleted file mode 100644 index e469a830df..0000000000 --- a/src/dusk/imgui/ImGuiMenuEnhancements.cpp +++ /dev/null @@ -1,260 +0,0 @@ -#include "imgui.h" - -#include "ImGuiMenuEnhancements.hpp" -#include "ImGuiConfig.hpp" -#include "dusk/settings.h" - -namespace dusk { - ImGuiMenuEnhancements::ImGuiMenuEnhancements() {} - - void ImGuiMenuEnhancements::draw() { - if (ImGui::BeginMenu("Enhancements")) { - if (ImGui::BeginMenu("Gameplay")) { - ImGui::SeparatorText("Preferences"); - - config::ImGuiCheckbox("Mirror Mode", getSettings().game.enableMirrorMode); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Mirrors the world horizontally, matching the Wii version of the game."); - } - - config::ImGuiCheckbox("Disable Main HUD", getSettings().game.disableMainHUD); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Disables the main HUD of the game.\n" - "Useful for recording or a more immersive experience!"); - } - - ImGui::SeparatorText("Difficulty"); - - config::ImGuiSliderInt("Damage Multiplier", getSettings().game.damageMultiplier, 1, 8, "x%d"); - - config::ImGuiCheckbox("Instant Death", getSettings().game.instantDeath); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Any hit will instantly kill you."); - } - - config::ImGuiCheckbox("No Heart Drops", getSettings().game.noHeartDrops); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Hearts will never drop from enemies,\n" - "pots and various other places."); - } - - ImGui::SeparatorText("Quality of Life"); - - config::ImGuiCheckbox("Bigger Wallets", getSettings().game.biggerWallets); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Wallet sizes are like in the HD version. (500, 1000, 2000)"); - } - - config::ImGuiCheckbox("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Rupees won't play cutscenes after you've collected them the first time."); - } - - config::ImGuiCheckbox("Faster Climbing", getSettings().game.fastClimbing); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Quicker climbing on ladders and vines like the HD version."); - } - - config::ImGuiCheckbox("Faster Tears of Light", getSettings().game.fastTears); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Tears of Light dropped by Shadow Insects pop out faster like the HD version."); - } - - config::ImGuiCheckbox("Instant Saves", getSettings().game.instantSaves); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Skip the delay when writing to the Memory Card."); - } - - config::ImGuiCheckbox("Autosave", getSettings().game.autoSave); - const bool autoSaveHovered = ImGui::IsItemHovered(); - ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.72f, 0.2f, 1.0f)); - ImGui::TextUnformatted("[EXPERIMENTAL]"); - ImGui::PopStyleColor(); - if (autoSaveHovered || ImGui::IsItemHovered()) { - ImGui::SetTooltip( - "Automatically saves the game when going to new areas,\n" - "getting an item, or opening a dungeon door."); - } - - config::ImGuiCheckbox("Hold B for Instant Text", getSettings().game.instantText); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Make text scroll immediately by holding B."); - } - - config::ImGuiCheckbox("No Climbing Miss Animation", getSettings().game.noMissClimbing); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Prevents Link from playing a struggle animation\n" - "when grabbing ledges or climbing on vines."); - } - - config::ImGuiCheckbox("No Rupee Returns", getSettings().game.noReturnRupees); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Always collect Rupees even if your Wallet is too full."); - } - - config::ImGuiCheckbox("No Sword Recoil", getSettings().game.noSwordRecoil); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Link won't recoil when his sword hits walls."); - } - - config::ImGuiCheckbox("Skip TV Settings Screen", getSettings().game.hideTvSettingsScreen); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Skip the TV calibration screen shown when loading a save."); - } - - config::ImGuiCheckbox("Skip Warning Screen", getSettings().game.skipWarningScreen); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Skip the warning screen shown when starting the game."); - } - - config::ImGuiCheckbox("Sun's Song (R+X)", getSettings().game.sunsSong); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Allows Wolf Link to howl and change the time of day."); - } - - config::ImGuiCheckbox("Quick Transform (R+Y)", getSettings().game.enableQuickTransform); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Transform instantly by pressing R and Y simultaneously."); - } - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Graphics")) { - config::ImGuiSliderInt("Shadow Resolution", getSettings().game.shadowResolutionMultiplier, 1, 8, "x%d"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Improves the shadow resolution, making them higher quality."); - } - - config::ImGuiCheckbox("Unlock Framerate", getSettings().game.enableFrameInterpolation); - const bool frameInterpolationHovered = ImGui::IsItemHovered(); - ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.72f, 0.2f, 1.0f)); - ImGui::TextUnformatted("[EXPERIMENTAL]"); - ImGui::PopStyleColor(); - if (frameInterpolationHovered || ImGui::IsItemHovered()) { - ImGui::SetTooltip("Uses inter-frame interpolation to enable higher frame rates.\nVisual artifacts, animation glitches, or instability may occur."); - } - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Audio")) { - config::ImGuiCheckbox("No Low HP Sound", getSettings().game.noLowHpSound); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Disable the beeping sound when having low health."); - } - - config::ImGuiCheckbox("Non-Stop Midna's Lament", getSettings().game.midnasLamentNonStop); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Prevents enemy music while Midna's Lament is playing."); - } - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Input")) { - config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis); - - ImGui::SeparatorText("Gyro"); - - config::ImGuiCheckbox("Gyro Aim", getSettings().game.enableGyroAim); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Enables the gyroscope on supported controllers\n" - "while in look mode (C-Up) and while aiming the\n" - "Slingshot, Gale Boomerang, Hero's Bow, Clawshot(s),\n" - "Ball and Chain, and Dominion Rod."); - } - - config::ImGuiCheckbox("Gyro Rollgoal", getSettings().game.enableGyroRollgoal); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Enables the gyroscope on supported controllers to\n" - "tilt the Rollgoal table in Hena's Cabin."); - } - - if (getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal) { - config::ImGuiSliderFloat("Gyro Pitch Sensitivity", getSettings().game.gyroSensitivityY, 0.25f, 4.0f, "%.2f"); - config::ImGuiSliderFloat("Gyro Yaw Sensitivity", getSettings().game.gyroSensitivityX, 0.25f, 4.0f, "%.2f"); - - if (getSettings().game.enableGyroRollgoal) { - config::ImGuiSliderFloat("Rollgoal Sensitivity", getSettings().game.gyroSensitivityRollgoal, 0.25f, 4.0f, "%.2f"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Additional multiplier for scaling how strongly\n" - "the gyroscope affects the Rollgoal table."); - } - } - - config::ImGuiSliderFloat("Gyro Deadband", getSettings().game.gyroDeadband, 0.0f, 0.5f, "%.3f"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Angular rates below this magnitude are treated as zero,\n" - "reducing drift and jitter when the controller is still."); - } - - config::ImGuiSliderFloat("Gyro Smoothing", getSettings().game.gyroSmoothing, 0.0f, 1.0f, "%.2f"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Low values track raw gyro input more closely,\n" - "while higher values smooth out input over time."); - } - - config::ImGuiCheckbox("Invert Gyro Pitch", getSettings().game.gyroInvertPitch); - config::ImGuiCheckbox("Invert Gyro Yaw", getSettings().game.gyroInvertYaw); - } - - ImGui::SeparatorText("Tools"); - - config::ImGuiCheckbox("Turbo Key", getSettings().game.enableTurboKeybind); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Hold TAB to increase game speed by up to 4x."); - } - - ImGui::EndMenu(); - } - - ImGui::Separator(); - - if (ImGui::BeginMenu("Cheats")) { - config::ImGuiCheckbox("Infinite Hearts", getSettings().game.infiniteHearts); - config::ImGuiCheckbox("Infinite Arrows", getSettings().game.infiniteArrows); - config::ImGuiCheckbox("Infinite Bombs", getSettings().game.infiniteBombs); - config::ImGuiCheckbox("Infinite Oil", getSettings().game.infiniteOil); - config::ImGuiCheckbox("Infinite Oxygen", getSettings().game.infiniteOxygen); - config::ImGuiCheckbox("Infinite Rupees", getSettings().game.infiniteRupees); - config::ImGuiCheckbox("Moon Jump (R+A)", getSettings().game.moonJump); - config::ImGuiCheckbox("Super Clawshot", getSettings().game.superClawshot); - config::ImGuiCheckbox("Always Greatspin", getSettings().game.alwaysGreatspin); - - config::ImGuiCheckbox("Fast Iron Boots", getSettings().game.enableFastIronBoots); - - config::ImGuiCheckbox("Can Transform Anywhere", getSettings().game.canTransformAnywhere); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Allows you to transform even if NPCs are looking."); - } - - config::ImGuiCheckbox("Fast Spinner", getSettings().game.fastSpinner); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Speeds up Spinner movement when holding R."); - } - - config::ImGuiCheckbox("Free Magic Armor", getSettings().game.freeMagicArmor); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Makes the magic armor work without rupees."); - } - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Technical")) { - config::ImGuiCheckbox("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Restores patched glitches from Wii USA 1.0,\n" - "the first released version."); - } - - ImGui::EndMenu(); - } - - ImGui::EndMenu(); - } - } -} diff --git a/src/dusk/imgui/ImGuiMenuEnhancements.hpp b/src/dusk/imgui/ImGuiMenuEnhancements.hpp deleted file mode 100644 index f40baaad65..0000000000 --- a/src/dusk/imgui/ImGuiMenuEnhancements.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef DUSK_IMGUI_MENUENHANCEMENTS_HPP -#define DUSK_IMGUI_MENUENHANCEMENTS_HPP - -#include -#include -#include - -#include "imgui.h" - -namespace dusk { - class ImGuiMenuEnhancements { - public: - ImGuiMenuEnhancements(); - void draw(); - }; -} - -#endif // DUSK_IMGUI_MENUENHANCEMENTS_HPP diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index c6676df774..38e701050f 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -5,45 +5,18 @@ #include "ImGuiConsole.hpp" #include "ImGuiMenuGame.hpp" #include "ImGuiConfig.hpp" -#include #include "JSystem/JUtility/JUTGamePad.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/audio/DuskDsp.hpp" -#include "dusk/dusk.h" +#include "dusk/main.h" #include "dusk/hotkeys.h" #include "dusk/settings.h" #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_graphic.h" #include -#include #include -#include - -#include "dusk/main.h" - -#if defined(__APPLE__) -#include -#endif - -#if defined(_WIN32) || (defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || (defined(__linux__) && !defined(__ANDROID__)) -#define DUSK_CAN_OPEN_DATA_FOLDER 1 - -namespace fs = std::filesystem; - -static void OpenDataFolder() { - const std::string path = fs::absolute(fs::path(aurora::g_config.configPath)).generic_string(); -#if defined(_WIN32) - const std::string url = std::string("file:///") + path; -#else - const std::string url = std::string("file://") + path; -#endif - (void)SDL_OpenURL(url.c_str()); -} -#else -#define DUSK_CAN_OPEN_DATA_FOLDER 0 -#endif namespace { constexpr int kInternalResolutionScaleMax = 12; @@ -63,149 +36,13 @@ namespace dusk { ImGuiMenuGame::ImGuiMenuGame() {} void ImGuiMenuGame::draw() { - if (ImGui::BeginMenu("Game")) { - if (ImGui::BeginMenu("Graphics")) { - if (!IsMobile) { - if (ImGui::MenuItem("Toggle Fullscreen", hotkeys::TOGGLE_FULLSCREEN)) { - ToggleFullscreen(); - } - - if (ImGui::MenuItem("Default Window Size")) { - getSettings().video.enableFullscreen.setValue(false); - VISetWindowFullscreen(false); - VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2); - VICenterWindow(); - } - } - - bool vsync = getSettings().video.enableVsync; - if (ImGui::Checkbox("Enable Vsync", &vsync)) { - getSettings().video.enableVsync.setValue(vsync); - aurora_enable_vsync(vsync); - config::Save(); - } - - bool lockAspect = getSettings().video.lockAspectRatio; - if (ImGui::Checkbox("Force 4:3 Aspect Ratio", &lockAspect)) { - getSettings().video.lockAspectRatio.setValue(lockAspect); - - if (lockAspect) { - AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT); - } else { - AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH); - } - - config::Save(); - } - - u32 internalResolutionWidth = 0; - u32 internalResolutionHeight = 0; - AuroraGetRenderSize(&internalResolutionWidth, &internalResolutionHeight); - ImGui::TextDisabled("Current internal resolution: %ux%u", internalResolutionWidth, - internalResolutionHeight); - - int scale = std::clamp(getSettings().game.internalResolutionScale.getValue(), 0, - kInternalResolutionScaleMax); - if (ImGui::SliderInt("Internal Resolution", &scale, 0, kInternalResolutionScaleMax, - scale == 0 ? "Auto" : "%dx")) - { - getSettings().game.internalResolutionScale.setValue(scale); - VISetFrameBufferScale(static_cast(scale)); - config::Save(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Auto renders at the native window resolution.\n" - "Higher values scale the game's internal framebuffer."); - } - - constexpr const char* bloomModeNames[] = {"Off", "Classic", "Dusk"}; - int bloomMode = static_cast(getSettings().game.bloomMode.getValue()); - if (ImGui::BeginCombo("Bloom", bloomModeNames[bloomMode])) { - for (int i = 0; i < IM_ARRAYSIZE(bloomModeNames); i++) { - const bool selected = bloomMode == i; - if (ImGui::Selectable(bloomModeNames[i], selected)) { - getSettings().game.bloomMode.setValue(static_cast(i)); - config::Save(); - } - if (selected) { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndCombo(); - } - - bool bloomOff = bloomMode == static_cast(BloomMode::Off); - if (bloomOff) ImGui::BeginDisabled(); - float mult = getSettings().game.bloomMultiplier.getValue(); - if (ImGui::SliderFloat("Bloom Brightness", &mult, 0.0f, 1.0f, "%.2f")) { - getSettings().game.bloomMultiplier.setValue(mult); - config::Save(); - } - if (bloomOff) ImGui::EndDisabled(); - - ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Audio")) { - ImGui::Text("Master Volume"); - if (config::ImGuiSliderInt("##masterVolume", getSettings().audio.masterVolume, 0, 100)) { - dusk::audio::SetMasterVolume(getSettings().audio.masterVolume / 100.0f); - } - - if (config::ImGuiCheckbox("Enable Reverb", getSettings().audio.enableReverb)) { - dusk::audio::SetEnableReverb(getSettings().audio.enableReverb); - } - /* - // TODO: Implement additional settings - ImGui::Text("Main Music Volume"); - ImGui::SliderFloat("##mainMusicVolume", &getSettings().audio.mainMusicVolume, 0, 100); - - ImGui::Text("Sub Music Volume"); - ImGui::SliderFloat("##subMusicVolume", &getSettings().audio.subMusicVolume, 0, 100); - - ImGui::Text("Sound Effects Volume"); - ImGui::SliderFloat("##soundEffectsVolume", &getSettings().audio.soundEffectsVolume, 0, 100); - - ImGui::Text("Fanfare Volume"); - ImGui::SliderFloat("##fanfareVolume", &getSettings().audio.fanfareVolume, 0, 100); - - Z2AudioMgr* audioMgr = Z2AudioMgr::getInterface(); - if (audioMgr != nullptr) { - } - */ - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Controller")) { - ImGui::MenuItem("Configure Controller", nullptr, &m_showControllerConfig); - ImGui::Checkbox("Show Input Viewer", &m_showInputViewer); - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Interface")) { - config::ImGuiCheckbox("Skip Pre-Launch UI", getSettings().backend.skipPreLaunchUI); - config::ImGuiCheckbox("Show Pipeline Compilation", getSettings().backend.showPipelineCompilation); -#if DUSK_ENABLE_SENTRY_NATIVE - config::ImGuiCheckbox("Enable Crash Reporting", getSettings().backend.enableCrashReporting); -#endif - if (!IsMobile) { - config::ImGuiCheckbox("Pause on Focus Lost", getSettings().game.pauseOnFocusLost); - } - - ImGui::EndMenu(); - } - - ImGui::Separator(); - -#if DUSK_CAN_OPEN_DATA_FOLDER - if (ImGui::MenuItem("Open Data Folder")) { - OpenDataFolder(); - } -#endif + if (ImGui::BeginMenu("Settings")) { + drawAudioMenu(); + drawCheatsMenu(); + drawGameplayMenu(); + drawGraphicsMenu(); + drawInputMenu(); + drawInterfaceMenu(); ImGui::Separator(); @@ -221,6 +58,416 @@ namespace dusk { } } + void ImGuiMenuGame::drawGraphicsMenu() { + if (ImGui::BeginMenu("Graphics")) { + ImGui::SeparatorText("Display"); + + if (!IsMobile) { + if (ImGui::MenuItem("Toggle Fullscreen", hotkeys::TOGGLE_FULLSCREEN)) { + ToggleFullscreen(); + } + + if (ImGui::Button("Restore Default Window Size")) { + getSettings().video.enableFullscreen.setValue(false); + VISetWindowFullscreen(false); + VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2); + VICenterWindow(); + } + } + + ImGui::Separator(); + + bool vsync = getSettings().video.enableVsync; + if (ImGui::Checkbox("Enable VSync", &vsync)) { + getSettings().video.enableVsync.setValue(vsync); + aurora_enable_vsync(vsync); + config::Save(); + } + + bool lockAspect = getSettings().video.lockAspectRatio; + if (ImGui::Checkbox("Force 4:3 Aspect Ratio", &lockAspect)) { + getSettings().video.lockAspectRatio.setValue(lockAspect); + + if (lockAspect) { + AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT); + } else { + AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH); + } + + config::Save(); + } + + ImGui::SeparatorText("Resolution"); + + u32 internalResolutionWidth = 0; + u32 internalResolutionHeight = 0; + AuroraGetRenderSize(&internalResolutionWidth, &internalResolutionHeight); + ImGui::TextDisabled("Current internal resolution: %ux%u", internalResolutionWidth, + internalResolutionHeight); + + int scale = std::clamp(getSettings().game.internalResolutionScale.getValue(), 0, + kInternalResolutionScaleMax); + if (ImGui::SliderInt("Internal Resolution", &scale, 0, kInternalResolutionScaleMax, + scale == 0 ? "Auto" : "%dx")) + { + getSettings().game.internalResolutionScale.setValue(scale); + VISetFrameBufferScale(static_cast(scale)); + config::Save(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Auto renders at the native window resolution.\n" + "Higher values scale the game's internal framebuffer."); + } + + config::ImGuiSliderInt("Shadow Resolution", getSettings().game.shadowResolutionMultiplier, 1, 8, "x%d"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Improves the shadow resolution, making them higher quality."); + } + + ImGui::SeparatorText("Post-Processing"); + + constexpr const char* bloomModeNames[] = {"Off", "Classic", "Dusk"}; + int bloomMode = static_cast(getSettings().game.bloomMode.getValue()); + if (ImGui::BeginCombo("Bloom", bloomModeNames[bloomMode])) { + for (int i = 0; i < IM_ARRAYSIZE(bloomModeNames); i++) { + const bool selected = bloomMode == i; + if (ImGui::Selectable(bloomModeNames[i], selected)) { + getSettings().game.bloomMode.setValue(static_cast(i)); + config::Save(); + } + if (selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + bool bloomOff = bloomMode == static_cast(BloomMode::Off); + if (bloomOff) ImGui::BeginDisabled(); + float mult = getSettings().game.bloomMultiplier.getValue(); + if (ImGui::SliderFloat("Bloom Brightness", &mult, 0.0f, 1.0f, "%.2f")) { + getSettings().game.bloomMultiplier.setValue(mult); + config::Save(); + } + if (bloomOff) ImGui::EndDisabled(); + + ImGui::SeparatorText("Rendering"); + + config::ImGuiCheckbox("Unlock Framerate", getSettings().game.enableFrameInterpolation); + const bool frameInterpolationHovered = ImGui::IsItemHovered(); + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.72f, 0.2f, 1.0f)); + ImGui::TextUnformatted("[EXPERIMENTAL]"); + ImGui::PopStyleColor(); + if (frameInterpolationHovered || ImGui::IsItemHovered()) { + ImGui::SetTooltip("Uses inter-frame interpolation to enable higher frame rates.\nVisual artifacts, animation glitches, or instability may occur."); + } + + ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); + + config::ImGuiCheckbox("Enable Depth of Field", getSettings().game.enableDepthOfField); + + ImGui::EndMenu(); + } + } + + void ImGuiMenuGame::drawGameplayMenu() { + if (ImGui::BeginMenu("Gameplay")) { + ImGui::SeparatorText("General"); + + config::ImGuiCheckbox("Mirror Mode", getSettings().game.enableMirrorMode); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Mirrors the world horizontally, matching the Wii version of the game."); + } + + config::ImGuiCheckbox("Disable Main HUD", getSettings().game.disableMainHUD); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Disables the main HUD of the game.\n" + "Useful for recording or a more immersive experience!"); + } + + config::ImGuiCheckbox("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Restores patched glitches from Wii USA 1.0,\n" + "the first released version."); + } + + config::ImGuiCheckbox("Enable Rotating Link Doll", getSettings().game.enableLinkDollRotation); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Enables rotating Link in the collection menu with the C-Stick"); + } + + ImGui::SeparatorText("Difficulty"); + + config::ImGuiSliderInt("Damage Multiplier", getSettings().game.damageMultiplier, 1, 8, "x%d"); + + config::ImGuiCheckbox("Instant Death", getSettings().game.instantDeath); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Any hit will instantly kill you."); + } + + config::ImGuiCheckbox("No Heart Drops", getSettings().game.noHeartDrops); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Hearts will never drop from enemies,\n" + "pots and various other places."); + } + + ImGui::SeparatorText("Quality of Life"); + + config::ImGuiCheckbox("Bigger Wallets", getSettings().game.biggerWallets); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Wallet sizes are like in the HD version. (500, 1000, 2000)"); + } + + config::ImGuiCheckbox("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Rupees won't play cutscenes after you've collected them the first time."); + } + + config::ImGuiCheckbox("Faster Climbing", getSettings().game.fastClimbing); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Quicker climbing on ladders and vines like the HD version."); + } + + config::ImGuiCheckbox("Faster Tears of Light", getSettings().game.fastTears); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Tears of Light dropped by Shadow Insects pop out faster like the HD version."); + } + + config::ImGuiCheckbox("Autosave", getSettings().game.autoSave); + const bool autoSaveHovered = ImGui::IsItemHovered(); + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.72f, 0.2f, 1.0f)); + ImGui::TextUnformatted("[EXPERIMENTAL]"); + ImGui::PopStyleColor(); + if (autoSaveHovered || ImGui::IsItemHovered()) { + ImGui::SetTooltip("Autosaves the game when going to a new area,\n" + "opening a dungeon door, or getting a new item."); + } + + config::ImGuiCheckbox("Instant Saves", getSettings().game.instantSaves); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Skip the delay when writing to the Memory Card."); + } + + config::ImGuiCheckbox("Hold B for Instant Text", getSettings().game.instantText); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Make text scroll immediately by holding B."); + } + + config::ImGuiCheckbox("No Climbing Miss Animation", getSettings().game.noMissClimbing); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Prevents Link from playing a struggle animation\n" + "when grabbing ledges or climbing on vines."); + } + + config::ImGuiCheckbox("No Rupee Returns", getSettings().game.noReturnRupees); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Always collect Rupees even if your Wallet is too full."); + } + + config::ImGuiCheckbox("No Sword Recoil", getSettings().game.noSwordRecoil); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Link won't recoil when his sword hits walls."); + } + + config::ImGuiCheckbox("Skip TV Settings Screen", getSettings().game.hideTvSettingsScreen); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Skip the TV calibration screen shown when loading a save."); + } + + config::ImGuiCheckbox("Skip Warning Screen", getSettings().game.skipWarningScreen); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Skip the warning screen shown when starting the game."); + } + + config::ImGuiCheckbox("Sun's Song (R+X)", getSettings().game.sunsSong); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Allows Wolf Link to howl and change the time of day."); + } + + config::ImGuiCheckbox("Quick Transform (R+Y)", getSettings().game.enableQuickTransform); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Transform instantly by pressing R and Y simultaneously."); + } + + ImGui::EndMenu(); + } + } + + void ImGuiMenuGame::drawCheatsMenu() { + if (ImGui::BeginMenu("Cheats")) { + ImGui::SeparatorText("Resources"); + config::ImGuiCheckbox("Infinite Hearts", getSettings().game.infiniteHearts); + config::ImGuiCheckbox("Infinite Arrows", getSettings().game.infiniteArrows); + config::ImGuiCheckbox("Infinite Bombs", getSettings().game.infiniteBombs); + config::ImGuiCheckbox("Infinite Oil", getSettings().game.infiniteOil); + config::ImGuiCheckbox("Infinite Oxygen", getSettings().game.infiniteOxygen); + config::ImGuiCheckbox("Infinite Rupees", getSettings().game.infiniteRupees); + config::ImGuiCheckbox("Items Don't Despawn", getSettings().game.enableIndefiniteItemDrops); + ImGui::SetItemTooltip("Items Don't Despawn Unless You Load A Different Room In Which Case They Do But Even Under Some Circumstances They Don't, It Is Quite Rare Though"); + + ImGui::SeparatorText("Abilities"); + config::ImGuiCheckbox("Moon Jump (R+A)", getSettings().game.moonJump); + config::ImGuiCheckbox("Super Clawshot", getSettings().game.superClawshot); + config::ImGuiCheckbox("Always Greatspin", getSettings().game.alwaysGreatspin); + config::ImGuiCheckbox("Fast Iron Boots", getSettings().game.enableFastIronBoots); + + config::ImGuiCheckbox("Can Transform Anywhere", getSettings().game.canTransformAnywhere); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Allows you to transform even if NPCs are looking."); + } + + config::ImGuiCheckbox("Fast Spinner", getSettings().game.fastSpinner); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Speeds up Spinner movement when holding R."); + } + + config::ImGuiCheckbox("Free Magic Armor", getSettings().game.freeMagicArmor); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Makes the magic armor work without rupees."); + } + + ImGui::EndMenu(); + } + } + + void ImGuiMenuGame::drawAudioMenu() { + if (ImGui::BeginMenu("Audio")) { + + ImGui::SeparatorText("Volume"); + + ImGui::Text("Master Volume"); + if (config::ImGuiSliderInt("##masterVolume", getSettings().audio.masterVolume, 0, 100)) { + dusk::audio::SetMasterVolume(getSettings().audio.masterVolume / 100.0f); + } + + /* + // TODO: Implement additional settings + ImGui::Text("Main Music Volume"); + ImGui::SliderFloat("##mainMusicVolume", &getSettings().audio.mainMusicVolume, 0, 100); + + ImGui::Text("Sub Music Volume"); + ImGui::SliderFloat("##subMusicVolume", &getSettings().audio.subMusicVolume, 0, 100); + + ImGui::Text("Sound Effects Volume"); + ImGui::SliderFloat("##soundEffectsVolume", &getSettings().audio.soundEffectsVolume, 0, 100); + + ImGui::Text("Fanfare Volume"); + ImGui::SliderFloat("##fanfareVolume", &getSettings().audio.fanfareVolume, 0, 100); + + Z2AudioMgr* audioMgr = Z2AudioMgr::getInterface(); + if (audioMgr != nullptr) { + } + */ + + ImGui::SeparatorText("Effects"); + + if (config::ImGuiCheckbox("Enable Reverb", getSettings().audio.enableReverb)) { + dusk::audio::SetEnableReverb(getSettings().audio.enableReverb); + } + + + ImGui::SeparatorText("Tweaks"); + + config::ImGuiCheckbox("No Low HP Sound", getSettings().game.noLowHpSound); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Disable the beeping sound when having low health."); + } + + config::ImGuiCheckbox("Non-Stop Midna's Lament", getSettings().game.midnasLamentNonStop); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Prevents enemy music while Midna's Lament is playing."); + } + + ImGui::EndMenu(); + } + } + + void ImGuiMenuGame::drawInputMenu() { + if (ImGui::BeginMenu("Input")) { + ImGui::SeparatorText("Controller"); + + if (ImGui::Button("Configure Controller")){ + m_showControllerConfig = !m_showControllerConfig; + } + + ImGui::SeparatorText("Camera"); + + config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis); + + ImGui::SeparatorText("Gyro"); + + config::ImGuiCheckbox("Gyro Aim", getSettings().game.enableGyroAim); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Enables the gyroscope on supported controllers\n" + "while in look mode (C-Up) and while aiming the\n" + "Slingshot, Gale Boomerang, Hero's Bow, Clawshot(s),\n" + "Ball and Chain, and Dominion Rod."); + } + + config::ImGuiCheckbox("Gyro Rollgoal", getSettings().game.enableGyroRollgoal); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Enables the gyroscope on supported controllers to\n" + "tilt the Rollgoal table in Hena's Cabin."); + } + + if (getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal) { + config::ImGuiSliderFloat("Gyro Pitch Sensitivity", getSettings().game.gyroSensitivityY, 0.25f, 4.0f, "%.2f"); + config::ImGuiSliderFloat("Gyro Yaw Sensitivity", getSettings().game.gyroSensitivityX, 0.25f, 4.0f, "%.2f"); + + if (getSettings().game.enableGyroRollgoal) { + config::ImGuiSliderFloat("Rollgoal Sensitivity", getSettings().game.gyroSensitivityRollgoal, 0.25f, 4.0f, "%.2f"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Additional multiplier for scaling how strongly\n" + "the gyroscope affects the Rollgoal table."); + } + } + + config::ImGuiSliderFloat("Gyro Deadband", getSettings().game.gyroDeadband, 0.0f, 0.5f, "%.3f"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Angular rates below this magnitude are treated as zero,\n" + "reducing drift and jitter when the controller is still."); + } + + config::ImGuiSliderFloat("Gyro Smoothing", getSettings().game.gyroSmoothing, 0.0f, 1.0f, "%.2f"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Low values track raw gyro input more closely,\n" + "while higher values smooth out input over time."); + } + + config::ImGuiCheckbox("Invert Gyro Pitch", getSettings().game.gyroInvertPitch); + config::ImGuiCheckbox("Invert Gyro Yaw", getSettings().game.gyroInvertYaw); + } + + ImGui::SeparatorText("Tools"); + + config::ImGuiCheckbox("Turbo Key", getSettings().game.enableTurboKeybind); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Hold TAB to increase game speed by up to 4x."); + } + + ImGui::Checkbox("Show Input Viewer", &m_showInputViewer); + + ImGui::EndMenu(); + } + } + + void ImGuiMenuGame::drawInterfaceMenu() { + if (ImGui::BeginMenu("Interface")) { + config::ImGuiCheckbox("Skip Pre-Launch UI", getSettings().backend.skipPreLaunchUI); + config::ImGuiCheckbox("Show Pipeline Compilation", getSettings().backend.showPipelineCompilation); +#if DUSK_ENABLE_SENTRY_NATIVE + config::ImGuiCheckbox("Enable Crash Reporting", getSettings().backend.enableCrashReporting); +#endif + if (!IsMobile) { + config::ImGuiCheckbox("Pause on Focus Lost", getSettings().game.pauseOnFocusLost); + } + + ImGui::EndMenu(); + } + } + static void drawVirtualStick(const char* id, const ImVec2& stick) { float scale = ImGuiScale(); ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x + 45 * scale, ImGui::GetCursorPos().y + 10)); diff --git a/src/dusk/imgui/ImGuiMenuGame.hpp b/src/dusk/imgui/ImGuiMenuGame.hpp index 4d51cbc865..e21374c8f4 100644 --- a/src/dusk/imgui/ImGuiMenuGame.hpp +++ b/src/dusk/imgui/ImGuiMenuGame.hpp @@ -19,6 +19,13 @@ namespace dusk { static void ToggleFullscreen(); private: + void drawAudioMenu(); + void drawInputMenu(); + void drawGraphicsMenu(); + void drawGameplayMenu(); + void drawCheatsMenu(); + void drawInterfaceMenu(); + struct { int m_selectedPort = 0; bool m_isReading = false; diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 1f9d42fbc9..00a03e635b 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -16,10 +16,58 @@ #include "dusk/main.h" #include "m_Do/m_Do_main.h" +#include +#include + +#if defined(__APPLE__) +#include +#endif + +#if defined(_WIN32) || (defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || (defined(__linux__) && !defined(__ANDROID__)) +#define DUSK_CAN_OPEN_DATA_FOLDER 1 + +namespace fs = std::filesystem; + +static void OpenDataFolder() { + const std::string path = fs::absolute(dusk::ConfigPath).generic_string(); +#if defined(_WIN32) + const std::string url = std::string("file:///") + path; +#else + const std::string url = std::string("file://") + path; +#endif + (void)SDL_OpenURL(url.c_str()); +} +#else +#define DUSK_CAN_OPEN_DATA_FOLDER 0 +#endif + namespace dusk { ImGuiMenuTools::ImGuiMenuTools() {} void ImGuiMenuTools::draw() { + if (ImGui::BeginMenu("Tools")) { + if (!dusk::IsGameLaunched) { + ImGui::BeginDisabled(); + } + + ImGui::MenuItem("Save Editor", hotkeys::SHOW_SAVE_EDITOR, &m_showSaveEditor); + ImGui::MenuItem("Map Loader", hotkeys::SHOW_MAP_LOADER, &m_showMapLoader); + ImGui::MenuItem("State Share", hotkeys::SHOW_STATE_SHARE, &m_showStateShare); + + if (!dusk::IsGameLaunched) { + ImGui::EndDisabled(); + } + +#if DUSK_CAN_OPEN_DATA_FOLDER + ImGui::Separator(); + if (ImGui::MenuItem("Open Data Folder")) { + OpenDataFolder(); + } +#endif + + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Debug")) { bool developmentMode = mDoMain::developmentMode == 1; if (ImGui::Checkbox("Development Mode", &developmentMode)) { @@ -59,9 +107,6 @@ namespace dusk { ImGui::MenuItem("Debug Overlay", hotkeys::SHOW_DEBUG_OVERLAY, &m_showDebugOverlay); ImGui::MenuItem("Heap Viewer", hotkeys::SHOW_HEAP_VIEWER, &m_showHeapOverlay); ImGui::MenuItem("Player Info", hotkeys::SHOW_PLAYER_INFO, &m_showPlayerInfo); - ImGui::MenuItem("Save Editor", hotkeys::SHOW_SAVE_EDITOR, &m_showSaveEditor); - ImGui::MenuItem("Map Loader", hotkeys::SHOW_MAP_LOADER, &m_showMapLoader); - ImGui::MenuItem("State Share", hotkeys::SHOW_STATE_SHARE, &m_showStateShare); ImGui::MenuItem("Debug Camera", hotkeys::SHOW_DEBUG_CAMERA, &m_showCameraOverlay); ImGui::MenuItem("Audio Debug", hotkeys::SHOW_AUDIO_DEBUG, &m_showAudioDebug); ImGui::MenuItem("Bloom", nullptr, &m_showBloomWindow); @@ -96,7 +141,9 @@ namespace dusk { ImGui::SetNextWindowBgAlpha(0.65f); if (ImGui::Begin("Debug Overlay", nullptr, windowFlags)) { ImGuiStringViewText(fmt::format(FMT_STRING("FPS: {:.2f}\n"), io.Framerate)); - ImGuiStringViewText(fmt::format(FMT_STRING("Frame usage: {:.1f}%\n"), frameUsagePct)); + if (frameUsagePct > 0.f) { + ImGuiStringViewText(fmt::format(FMT_STRING("Frame usage: {:.1f}%\n"), frameUsagePct)); + } ImGui::Separator(); @@ -163,7 +210,7 @@ namespace dusk { ImGui::Text("Link"); ImGuiStringViewText( player != nullptr - ? fmt::format("Position: {: .2f}, {: .2f}, {: .2f}\n", player->current.pos.x, player->current.pos.y, player->current.pos.z) + ? fmt::format("Position: {: .4f}, {: .4f}, {: .4f}\n", player->current.pos.x, player->current.pos.y, player->current.pos.z) : "Position: ?, ?, ?\n" ); @@ -175,7 +222,7 @@ namespace dusk { ImGuiStringViewText( player != nullptr - ? fmt::format("Speed: {0}\n", player->speedF) + ? fmt::format("Speed: {: .4f}\n", player->speedF) : "Speed: ?\n" ); @@ -183,7 +230,7 @@ namespace dusk { ImGui::Text("Epona"); ImGuiStringViewText( horse != nullptr - ? fmt::format("Position: {: .2f}, {: .2f}, {: .2f}\n", horse->current.pos.x, horse->current.pos.y, horse->current.pos.z) + ? fmt::format("Position: {: .4f}, {: .4f}, {: .4f}\n", horse->current.pos.x, horse->current.pos.y, horse->current.pos.z) : "Position: ?, ?, ?\n" ); @@ -195,7 +242,7 @@ namespace dusk { ImGuiStringViewText( horse != nullptr - ? fmt::format("Speed: {0}\n", horse->speedF) + ? fmt::format("Speed: {: .4f}\n", horse->speedF) : "Speed: ?\n" ); diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp index ecbd41e158..2727e78718 100644 --- a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp +++ b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp @@ -46,6 +46,17 @@ static std::string ShowIsoInvalidError(const iso::ValidationError code) { } } +static std::string_view card_type_name(CARDFileType type) { + switch (type) { + case CARD_GCIFOLDER: + return "GCI Folder"sv; + case CARD_RAWIMAGE: + return "Card Image"sv; + default: + return ""sv; + } +} + void fileDialogCallback(void* userdata, const char* path, const char* error) { auto* self = static_cast(userdata); if (error != nullptr) { @@ -216,6 +227,23 @@ void ImGuiPreLaunchWindow::drawOptions() { if (configuredBackendId != m_initialGraphicsBackend) { ImGui::TextDisabled("Restart Required"); } + auto curFileType = (CARDFileType)getSettings().backend.cardFileType.getValue(); + + if (ImGui::BeginCombo("Save File Type", card_type_name(curFileType).data())) { + + if (ImGui::Selectable("GCI Folder", curFileType == CARD_GCIFOLDER)) { + getSettings().backend.cardFileType.setValue(CARD_GCIFOLDER); + config::Save(); + } + + if (ImGui::Selectable("Card Image", curFileType == CARD_RAWIMAGE)) { + getSettings().backend.cardFileType.setValue(CARD_RAWIMAGE); + config::Save(); + } + + ImGui::EndCombo(); + } + ImGui::EndChild(); } diff --git a/src/dusk/imgui/ImGuiSaveEditor.cpp b/src/dusk/imgui/ImGuiSaveEditor.cpp index 5802c07b47..eced44c7f7 100644 --- a/src/dusk/imgui/ImGuiSaveEditor.cpp +++ b/src/dusk/imgui/ImGuiSaveEditor.cpp @@ -10,6 +10,7 @@ #include "d/d_item_data.h" #include "d/d_meter2_info.h" #include "d/d_save.h" +#include "d/actor/d_a_player.h" #include @@ -579,20 +580,21 @@ namespace dusk { if (ImGui::BeginCombo("Clothes", itemMap.find(statusA.mSelectEquip[0])->second.m_name.c_str())) { - if (ImGui::Selectable("None")) { - statusA.mSelectEquip[0] = dItemNo_NONE_e; - } if (ImGui::Selectable("Ordon Clothes")) { - statusA.mSelectEquip[0] = dItemNo_WEAR_CASUAL_e; + dMeter2Info_setCloth(dItemNo_WEAR_CASUAL_e, false); + daPy_getPlayerActorClass()->setClothesChange(0); } if (ImGui::Selectable("Hero's Clothes")) { - statusA.mSelectEquip[0] = dItemNo_WEAR_KOKIRI_e; + dMeter2Info_setCloth(dItemNo_WEAR_KOKIRI_e, false); + daPy_getPlayerActorClass()->setClothesChange(0); } if (ImGui::Selectable("Zora Armor")) { - statusA.mSelectEquip[0] = dItemNo_WEAR_ZORA_e; + dMeter2Info_setCloth(dItemNo_WEAR_ZORA_e, false); + daPy_getPlayerActorClass()->setClothesChange(0); } if (ImGui::Selectable("Magic Armor")) { - statusA.mSelectEquip[0] = dItemNo_ARMOR_e; + dMeter2Info_setCloth(dItemNo_ARMOR_e, false); + daPy_getPlayerActorClass()->setClothesChange(0); } ImGui::EndCombo(); } @@ -1489,11 +1491,11 @@ namespace dusk { } ImGui::TableNextColumn(); - ImGui::Text(e.flagName.c_str()); + ImGuiStringViewText(e.flagName); ImGui::TableNextColumn(); - ImGui::Text(e.location.c_str()); + ImGuiStringViewText(e.location); ImGui::TableNextColumn(); - ImGui::Text(e.description.c_str()); + ImGuiStringViewText(e.description); } ImGui::EndTable(); } diff --git a/src/dusk/imgui/ImGuiStateShare.cpp b/src/dusk/imgui/ImGuiStateShare.cpp index 172aad17a5..adbe5b6c67 100644 --- a/src/dusk/imgui/ImGuiStateShare.cpp +++ b/src/dusk/imgui/ImGuiStateShare.cpp @@ -5,14 +5,24 @@ #include "imgui.h" #include "fmt/format.h" #include "absl/strings/escaping.h" +#include "nlohmann/json.hpp" #include "d/d_com_inf_game.h" #include "dusk/main.h" +#include "dusk/io.hpp" +#include "dusk/logging.h" +#include "dusk/settings.h" +#include "f_op/f_op_overlap_mng.h" +#include "../file_select.hpp" +#include "aurora/lib/window.hpp" +#include #include namespace dusk { +using json = nlohmann::json; + #pragma pack(push, 1) struct StateSharePacket { char stageName[8]; @@ -23,9 +33,65 @@ struct StateSharePacket { }; #pragma pack(pop) -static constexpr size_t PACKET_TOTAL = sizeof(StateSharePacket) + sizeof(dSv_info_c); +static constexpr size_t PACKET_TOTAL = sizeof(StateSharePacket) + sizeof(dSv_info_c); +static constexpr size_t PACKET_SAVE_ONLY = sizeof(StateSharePacket) + sizeof(dSv_save_c); +static constexpr auto STATES_FILENAME = "states.json"; -void ImGuiStateShare::copyState() { +static bool ValidateEncodedState(const std::string&); + +void ImGuiStateShare::onMergeFileSelected(void* userdata, const char* path, const char* /*error*/) { + auto* self = static_cast(userdata); + if (path != nullptr) { + self->m_pendingMergePath = path; + } +} + + + +static std::string GetStatesFilePath() { + return (dusk::ConfigPath / STATES_FILENAME).string(); +} + +void ImGuiStateShare::loadStatesFile() { + m_loaded = true; + const std::filesystem::path filePath = dusk::ConfigPath / STATES_FILENAME; + if (!std::filesystem::exists(filePath)) { + return; + } + try { + const std::string pathStr = filePath.string(); + auto data = io::FileStream::ReadAllBytes(pathStr.c_str()); + auto j = json::parse(data); + if (!j.is_array()) { + return; + } + for (const auto& entry : j) { + if (!entry.contains("name") || !entry.contains("data")) { + continue; + } + SavedStateEntry s; + s.name = entry["name"].get(); + s.encoded = entry["data"].get(); + m_states.push_back(std::move(s)); + } + } catch (const std::exception& e) { + m_statusMsg = fmt::format("Failed to load states: {}", e.what()); + } +} + +void ImGuiStateShare::saveStatesFile() { + json j = json::array(); + for (const auto& s : m_states) { + j.push_back(json{{"name", s.name}, {"data", s.encoded}}); + } + try { + io::FileStream::WriteAllText(GetStatesFilePath().c_str(), j.dump(2)); + } catch (const std::exception& e) { + m_statusMsg = fmt::format("Failed to save states: {}", e.what()); + } +} + +std::string ImGuiStateShare::encodeCurrentState() { StateSharePacket pkt = {}; strncpy(pkt.stageName, dComIfGp_getStartStageName(), 7); pkt.roomNo = dComIfGp_getStartStageRoomNo(); @@ -40,26 +106,25 @@ void ImGuiStateShare::copyState() { std::string compressed(bound, '\0'); compressed.resize(ZSTD_compress(compressed.data(), bound, raw.data(), raw.size(), 1)); - std::string encoded = absl::Base64Escape(compressed); - ImGui::SetClipboardText(encoded.c_str()); - m_statusMsg = "Copied to clipboard."; + return absl::Base64Escape(compressed); } -bool ImGuiStateShare::pasteState() { - const char* clip = ImGui::GetClipboardText(); - if (!clip || clip[0] == '\0') { - m_statusMsg = "Clipboard is empty."; - return false; - } - +bool ImGuiStateShare::applyEncodedState(const std::string& encoded, const std::string& name) { std::string decoded; - if (!absl::Base64Unescape(clip, &decoded)) { + if (!absl::Base64Unescape(encoded, &decoded)) { m_statusMsg = "Invalid base64."; return false; } unsigned long long dSize = ZSTD_getFrameContentSize(decoded.data(), decoded.size()); - if (dSize == ZSTD_CONTENTSIZE_ERROR || dSize == ZSTD_CONTENTSIZE_UNKNOWN || dSize < PACKET_TOTAL) { + if (dSize == ZSTD_CONTENTSIZE_ERROR || dSize == ZSTD_CONTENTSIZE_UNKNOWN) { + m_statusMsg = "Not a valid state string."; + return false; + } + + const bool isFull = (dSize == PACKET_TOTAL); + const bool isPartial = (dSize == PACKET_SAVE_ONLY); + if (!isFull && !isPartial) { m_statusMsg = "Not a valid state string."; return false; } @@ -75,45 +140,272 @@ bool ImGuiStateShare::pasteState() { memcpy(&pkt, raw.data(), sizeof(pkt)); pkt.stageName[7] = '\0'; - memcpy(&g_dComIfG_gameInfo.info, raw.data() + sizeof(pkt), sizeof(dSv_info_c)); + if (isFull) { + memcpy(&g_dComIfG_gameInfo.info, raw.data() + sizeof(pkt), sizeof(dSv_info_c)); + m_pendingInfo = g_dComIfG_gameInfo.info; + m_pendingSavedata.reset(); + } else { + memcpy(&g_dComIfG_gameInfo.info.mSavedata, raw.data() + sizeof(pkt), sizeof(dSv_save_c)); + m_pendingSavedata = g_dComIfG_gameInfo.info.mSavedata; + m_pendingInfo.reset(); + } s16 spawnPoint = pkt.startPoint == -4 ? -1 : pkt.startPoint; - if (spawnPoint == -1) { dComIfGs_setRestartRoomParam(pkt.roomNo & 0x3F); } - dComIfGp_setNextStage(pkt.stageName, spawnPoint, pkt.roomNo, pkt.layer); - m_pendingInfo = g_dComIfG_gameInfo.info; + dusk::getTransientSettings().stateShareLoadActive = true; + m_stateSharePeekSeen = false; + dComIfGp_setNextStage(pkt.stageName, spawnPoint, pkt.roomNo, pkt.layer, 0.0f, 0, 1, 0, 0, 1, 3); - m_statusMsg = fmt::format("Warping to {} room {} layer {}.", pkt.stageName, (int)pkt.roomNo, (int)pkt.layer); + if (name.empty()) { + m_statusMsg = fmt::format("{} room {} layer {}.", pkt.stageName, (int)pkt.roomNo, (int)pkt.layer); + } else { + m_statusMsg = fmt::format("{}: {} room {} layer {}.", name, pkt.stageName, (int)pkt.roomNo, (int)pkt.layer); + } return true; } void ImGuiStateShare::tickPendingApply() { - if (!m_pendingInfo.has_value() || dComIfGp_isEnableNextStage()) + if (!m_pendingInfo.has_value() && !m_pendingSavedata.has_value()) { return; - g_dComIfG_gameInfo.info = *m_pendingInfo; - m_pendingInfo.reset(); + } + if (dComIfGp_isEnableNextStage()) { + return; + } + if (m_pendingInfo.has_value()) { + g_dComIfG_gameInfo.info = *m_pendingInfo; + m_pendingInfo.reset(); + } else { + g_dComIfG_gameInfo.info.mSavedata = *m_pendingSavedata; + m_pendingSavedata.reset(); + } + dComIfGp_offOxygenShowFlag(); + dComIfGp_setMaxOxygen(600); + dComIfGp_setOxygen(600); +} + +static bool ValidateEncodedState(const std::string& encoded) { + std::string decoded; + if (!absl::Base64Unescape(encoded, &decoded)) { + return false; + } + unsigned long long dSize = ZSTD_getFrameContentSize(decoded.data(), decoded.size()); + return dSize == PACKET_TOTAL || dSize == PACKET_SAVE_ONLY; +} + +void ImGuiStateShare::mergeFromFile(const std::string& path) { + try { + auto data = io::FileStream::ReadAllBytes(path.c_str()); + auto j = json::parse(data); + if (!j.is_array()) { + m_statusMsg = "File does not contain a JSON array."; + return; + } + + std::unordered_set existingNames; + for (const auto& s : m_states) { + existingNames.insert(s.name); + } + + int added = 0; + int skipped = 0; + for (const auto& entry : j) { + if (!entry.contains("name") || !entry.contains("data")) { + ++skipped; + continue; + } + const std::string name = entry["name"].get(); + const std::string encoded = entry["data"].get(); + if (!ValidateEncodedState(encoded)) { + ++skipped; + continue; + } + if (existingNames.count(name)) { + ++skipped; + continue; + } + SavedStateEntry s; + s.name = name; + s.encoded = encoded; + existingNames.insert(s.name); + m_states.push_back(std::move(s)); + ++added; + } + + if (added > 0) { + saveStatesFile(); + } + m_statusMsg = fmt::format("Merged: {} added, {} skipped.", added, skipped); + } catch (const std::exception& e) { + m_statusMsg = fmt::format("Failed to load file: {}", e.what()); + } } void ImGuiStateShare::draw(bool& open) { - if (dusk::IsGameLaunched) + if (dusk::IsGameLaunched) { tickPendingApply(); + if (dusk::getTransientSettings().stateShareLoadActive) { + if (fopOvlpM_IsPeek()) { + m_stateSharePeekSeen = true; + } else if (m_stateSharePeekSeen) { + dusk::getTransientSettings().stateShareLoadActive = false; + m_stateSharePeekSeen = false; + } + } + } - if (!open) + if (!m_loaded) { + loadStatesFile(); + } + + if (!m_pendingMergePath.empty()) { + mergeFromFile(m_pendingMergePath); + m_pendingMergePath.clear(); + } + + if (!open) { return; + } - if (!ImGui::Begin("State Share", &open, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) { + ImGui::SetNextWindowSizeConstraints(ImVec2(400, 0), ImVec2(FLT_MAX, FLT_MAX)); + if (!ImGui::Begin("State Manager", &open, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) { ImGui::End(); return; } - if (!dusk::IsGameLaunched) ImGui::BeginDisabled(); - if (ImGui::Button("Copy State")) copyState(); + const bool gameRunning = dusk::IsGameLaunched; + const bool loadInProgress = dusk::getTransientSettings().stateShareLoadActive; + + const float rowH = ImGui::GetTextLineHeightWithSpacing(); + const float listH = rowH * 8 + ImGui::GetStyle().FramePadding.y * 2; + ImGui::BeginChild("##states", ImVec2(0, listH), true); + + if (m_states.empty()) { + ImGui::TextDisabled("No saved states. Save or import one below."); + } + + int toDelete = -1; + for (int i = 0; i < (int)m_states.size(); ++i) { + ImGui::PushID(i); + + if (m_renamingIndex == i) { + ImGui::SetNextItemWidth(150); + bool done = ImGui::InputText("##rename", m_renameBuffer, sizeof(m_renameBuffer), + ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll); + if (done) { + if (m_renameBuffer[0] != '\0') { + m_states[i].name = m_renameBuffer; + } + m_renamingIndex = -1; + saveStatesFile(); + } else if (ImGui::IsItemDeactivated()) { + m_renamingIndex = -1; + } + } else { + ImGui::Selectable(m_states[i].name.c_str(), false, ImGuiSelectableFlags_None, ImVec2(150, 0)); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Double-click to rename"); + if (ImGui::IsMouseDoubleClicked(0)) { + m_renamingIndex = i; + strncpy(m_renameBuffer, m_states[i].name.c_str(), sizeof(m_renameBuffer) - 1); + m_renameBuffer[sizeof(m_renameBuffer) - 1] = '\0'; + ImGui::SetKeyboardFocusHere(-1); + } + } + } + + ImGui::SameLine(); + if (!gameRunning || loadInProgress) { ImGui::BeginDisabled(); } + if (ImGui::Button("Load")) { + applyEncodedState(m_states[i].encoded, m_states[i].name); + } + if (!gameRunning || loadInProgress) { ImGui::EndDisabled(); } + + ImGui::SameLine(); + if (ImGui::Button("Copy")) { + ImGui::SetClipboardText(m_states[i].encoded.c_str()); + m_statusMsg = fmt::format("'{}' copied to clipboard.", m_states[i].name); + } + + ImGui::SameLine(); + if (ImGui::Button("Del")) { + toDelete = i; + } + + ImGui::PopID(); + } + + if (toDelete >= 0) { + if (m_renamingIndex == toDelete) { m_renamingIndex = -1; } + m_states.erase(m_states.begin() + toDelete); + saveStatesFile(); + } + + ImGui::EndChild(); + + // Toolbar + if (!gameRunning) { ImGui::BeginDisabled(); } + if (ImGui::Button("Save")) { + SavedStateEntry entry; + entry.name = fmt::format("State {}", m_states.size() + 1); + entry.encoded = encodeCurrentState(); + m_states.push_back(std::move(entry)); + saveStatesFile(); + m_statusMsg = fmt::format("Saved as '{}'.", m_states.back().name); + } + if (!gameRunning) { ImGui::EndDisabled(); } + ImGui::SameLine(); - if (ImGui::Button("Import State")) pasteState(); - if (!dusk::IsGameLaunched) ImGui::EndDisabled(); + if (ImGui::Button("Import Clipboard")) { + const char* clip = ImGui::GetClipboardText(); + if (!clip || clip[0] == '\0') { + m_statusMsg = "Clipboard is empty."; + } else { + std::string clipStr = clip; + if (!ValidateEncodedState(clipStr)) { + m_statusMsg = "Clipboard does not contain a valid state."; + } else { + SavedStateEntry entry; + entry.name = fmt::format("Imported {}", m_states.size() + 1); + entry.encoded = std::move(clipStr); + m_states.push_back(std::move(entry)); + saveStatesFile(); + m_statusMsg = fmt::format("Imported as '{}'.", m_states.back().name); + } + } + } + + ImGui::SameLine(); + if (ImGui::Button("Load Pack")) { + static constexpr SDL_DialogFileFilter filter = {"State pack", "json"}; + ShowFileSelect(&onMergeFileSelected, this, aurora::window::get_sdl_window(), &filter, 1, nullptr, false); + } + + if (!m_states.empty()) { + ImGui::SameLine(); + if (ImGui::Button("Clear All")) { + ImGui::OpenPopup("##clearall"); + } + + if (ImGui::BeginPopup("##clearall")) { + ImGui::Text("Delete all saved states?"); + ImGui::Spacing(); + if (ImGui::Button("Yes, clear all")) { + m_states.clear(); + m_renamingIndex = -1; + saveStatesFile(); + m_statusMsg = "All states cleared."; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } if (!m_statusMsg.empty()) { ImGui::Spacing(); @@ -125,8 +417,9 @@ void ImGuiStateShare::draw(bool& open) { } void ImGuiMenuTools::ShowStateShare() { - if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F8, m_showStateShare)) + if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F8, m_showStateShare)) { return; + } m_stateShare.draw(m_showStateShare); } diff --git a/src/dusk/imgui/ImGuiStateShare.hpp b/src/dusk/imgui/ImGuiStateShare.hpp index a09cfd5963..a2dc681833 100644 --- a/src/dusk/imgui/ImGuiStateShare.hpp +++ b/src/dusk/imgui/ImGuiStateShare.hpp @@ -4,21 +4,39 @@ #include "d/d_save.h" #include #include +#include namespace dusk { - class ImGuiStateShare { - public: - void draw(bool& open); - private: - void copyState(); - bool pasteState(); - void tickPendingApply(); +struct SavedStateEntry { + std::string name; + std::string encoded; +}; + +class ImGuiStateShare { +public: + void draw(bool& open); + +private: + std::string encodeCurrentState(); + bool applyEncodedState(const std::string& encoded, const std::string& name = {}); + void tickPendingApply(); + void loadStatesFile(); + void saveStatesFile(); + void mergeFromFile(const std::string& path); + static void onMergeFileSelected(void* userdata, const char* path, const char* error); + + std::vector m_states; + std::string m_statusMsg; + std::optional m_pendingInfo; + std::optional m_pendingSavedata; + int m_renamingIndex = -1; + char m_renameBuffer[128] = {}; + bool m_loaded = false; + bool m_stateSharePeekSeen = false; + std::string m_pendingMergePath; +}; - std::string m_statusMsg; - std::optional m_pendingInfo; - }; } #endif - \ No newline at end of file diff --git a/src/dusk/logging.cpp b/src/dusk/logging.cpp index 3fdd6cdf45..172059aea4 100644 --- a/src/dusk/logging.cpp +++ b/src/dusk/logging.cpp @@ -168,14 +168,14 @@ void aurora_log_callback(AuroraLogLevel level, const char* module, const char* m aurora::Module DuskLog("dusk"); -void dusk::InitializeFileLogging(const char* configDir, AuroraLogLevel logLevel) { +void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraLogLevel logLevel) { std::lock_guard lock(g_logMutex); - if (g_logFile != nullptr || configDir == nullptr) { + if (g_logFile != nullptr || configDir.empty()) { return; } std::error_code ec; - const std::filesystem::path logsDir = std::filesystem::path(configDir) / "logs"; + const std::filesystem::path logsDir = configDir / "logs"; std::filesystem::create_directories(logsDir, ec); if (ec) { std::fprintf(stderr, "[WARNING | dusk] Failed to create log directory '%s': %s\n", diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 9e6ae0f5d5..2daa948ba3 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -44,6 +44,7 @@ UserSettings g_userSettings = { .invertCameraXAxis {"game.invertCameraXAxis", false}, .disableMainHUD {"game.disableMainHUD", false}, .pauseOnFocusLost {"game.pauseOnFocusLost", false}, + .enableLinkDollRotation = {"game.enableLinkDollRotation", false }, // Graphics .bloomMode {"game.bloomMode", BloomMode::Classic}, @@ -52,6 +53,7 @@ UserSettings g_userSettings = { .enableFrameInterpolation {"game.enableFrameInterpolation", false}, .internalResolutionScale {"game.internalResolutionScale", 0}, .shadowResolutionMultiplier {"game.shadowResolutionMultiplier", 1}, + .enableDepthOfField {"game.enableDepthOfField", true}, // Audio .noLowHpSound {"game.noLowHpSound", false}, @@ -70,14 +72,15 @@ UserSettings g_userSettings = { // Cheats .infiniteHearts {"game.infiniteHearts", false}, - .infiniteArrows {"game.infiniteArrows", false}, - .infiniteBombs {"game.infiniteBombs", false}, - .infiniteOil {"game.infiniteOil", false}, - .infiniteOxygen {"game.infiniteOxygen", false}, - .infiniteRupees {"game.infiniteRupees", false}, - .moonJump {"game.moonJump", false}, - .superClawshot {"game.superClawshot", false}, - .alwaysGreatspin {"game.alwaysGreatspin", false}, + .infiniteArrows{"game.infiniteArrows", false}, + .infiniteBombs{"game.infiniteBombs", false}, + .infiniteOil{"game.infiniteOil", false}, + .infiniteOxygen{"game.infiniteOxygen", false}, + .infiniteRupees{"game.infiniteRupees", false}, + .enableIndefiniteItemDrops {"game.enableIndefiniteItemDrops", false}, + .moonJump{"game.moonJump", false}, + .superClawshot{"game.superClawshot", false}, + .alwaysGreatspin{"game.alwaysGreatspin", false}, .enableFastIronBoots {"game.enableFastIronBoots", false}, .canTransformAnywhere {"game.canTransformAnywhere", false}, .fastSpinner {"game.fastSpinner", false}, @@ -97,7 +100,8 @@ UserSettings g_userSettings = { .showPipelineCompilation {"backend.showPipelineCompilation", false}, .wasPresetChosen {"backend.wasPresetChosen", false}, .enableCrashReporting {"backend.enableCrashReporting", true}, - .duskMenuOpen {"backend.duskMenuOpen", false} + .duskMenuOpen {"backend.duskMenuOpen", false}, + .cardFileType {"backend.cardFileType", static_cast(CARD_GCIFOLDER)} } }; @@ -145,10 +149,12 @@ void registerSettings() { Register(g_userSettings.game.disableWaterRefraction); Register(g_userSettings.game.internalResolutionScale); Register(g_userSettings.game.shadowResolutionMultiplier); + Register(g_userSettings.game.enableDepthOfField); Register(g_userSettings.game.enableFastIronBoots); Register(g_userSettings.game.canTransformAnywhere); Register(g_userSettings.game.freeMagicArmor); Register(g_userSettings.game.restoreWiiGlitches); + Register(g_userSettings.game.enableLinkDollRotation); Register(g_userSettings.game.noMissClimbing); Register(g_userSettings.game.noLowHpSound); Register(g_userSettings.game.midnasLamentNonStop); @@ -160,6 +166,7 @@ void registerSettings() { Register(g_userSettings.game.infiniteOil); Register(g_userSettings.game.infiniteOxygen); Register(g_userSettings.game.infiniteRupees); + Register(g_userSettings.game.enableIndefiniteItemDrops); Register(g_userSettings.game.moonJump); Register(g_userSettings.game.superClawshot); Register(g_userSettings.game.alwaysGreatspin); @@ -181,6 +188,7 @@ void registerSettings() { Register(g_userSettings.backend.wasPresetChosen); Register(g_userSettings.backend.enableCrashReporting); Register(g_userSettings.backend.duskMenuOpen); + Register(g_userSettings.backend.cardFileType); } // Transient settings diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp index 4a4c37fd7b..0968d5486e 100644 --- a/src/f_ap/f_ap_game.cpp +++ b/src/f_ap/f_ap_game.cpp @@ -25,9 +25,8 @@ #if TARGET_PC #include "tracy/Tracy.hpp" -#include -#include -#include +#include +#include #endif fapGm_HIO_c::fapGm_HIO_c() { @@ -739,93 +738,8 @@ static void fapGm_AfterRecord() { fapGm_After(); } -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 && dComIfGp_getStartStageLayer() != 0) - { - mAutoSaveProc = 1; - } -} - -void updateAutoSave() { - (AutoSaveFuncsProc[mAutoSaveProc])(); -} - -void writeAutoSave() { - 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.ShowToast("Saving...", 2.0f); - mAutoSaveProc = 0; -} - static void duskExecute() { + handleGamepadColor(); updateAutoSave(); if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigX(PAD_1)) { diff --git a/src/m_Do/m_Do_MemCard.cpp b/src/m_Do/m_Do_MemCard.cpp index a0c0302b09..5cb612f128 100644 --- a/src/m_Do/m_Do_MemCard.cpp +++ b/src/m_Do/m_Do_MemCard.cpp @@ -77,6 +77,8 @@ static OSThread MemCardThread; void mDoMemCd_Ctrl_c::ThdInit() { #if !PLATFORM_SHIELD + CARDSetLoadType((CARDFileType)dusk::getSettings().backend.cardFileType.getValue()); + CARDInit(DUSK_GAME_NAME, DUSK_GAME_VERSION); #endif diff --git a/src/m_Do/m_Do_ext.cpp b/src/m_Do/m_Do_ext.cpp index 84bf51ecf5..fc8952e91d 100644 --- a/src/m_Do/m_Do_ext.cpp +++ b/src/m_Do/m_Do_ext.cpp @@ -351,8 +351,13 @@ void mDoExt_modelUpdateDL(J3DModel* i_model) { void mDoExt_modelEntryDL(J3DModel* i_model) { #if TARGET_PC - if (!dusk::frame_interp::is_sim_frame()) + if (!dusk::frame_interp::is_sim_frame()) { + // FRAME INTERP NOTE: This fixes issue #355 where some lights would flicker. + // This is likely better solved by updating J3DMaterial::needsInterpCallBack, + // but it's unclear what exactly needs to be added. + i_model->diff(); return; + } #endif modelMtxErrorCheck(i_model); diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index d949d10ff0..425d1e25fe 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -1029,15 +1029,8 @@ static void drawDepth2(view_class* param_0, view_port_class* param_1, int param_ GX_FALSE, 0); } - #if TARGET_PC - // use full size for pc for higher quality background elements - u16 halfWidth = width; - u16 halfHeight = height; - #else u16 halfWidth = width >> 1; u16 halfHeight = height >> 1; - #endif - GXRenderModeObj* sp24 = JUTGetVideoManager()->getRenderMode(); GXSetCopyFilter(GX_FALSE, NULL, GX_TRUE, sp24->vfilter); GXSetTexCopySrc(x_orig, y_orig_pos, width, height); @@ -1155,6 +1148,9 @@ static void drawDepth2(view_class* param_0, view_port_class* param_1, int param_ GXSetProjection(ortho, GX_ORTHOGRAPHIC); GXSetCurrentMtx(0); +#ifdef TARGET_PC + if (dusk::getSettings().game.enableDepthOfField) +#endif if (l_tevColor0.a > -255 && sp8 == 1) { GXBegin(GX_QUADS, GX_VTXFMT0, 4); GXPosition3s16(x_orig, y_orig_pos, -5); diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index c597ad2804..579cbd8c8b 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -66,13 +66,15 @@ #include "SDL3/SDL_filesystem.h" #include "cxxopts.hpp" +#include "d/actor/d_a_movie_player.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/config.hpp" -#include "dusk/settings.h" #include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/settings.h" #include "dusk/discord_presence.hpp" #include "tracy/Tracy.hpp" #include "f_pc/f_pc_draw.h" +#include "tracy/Tracy.hpp" // --- GLOBALS --- s8 mDoMain::developmentMode = -1; @@ -96,6 +98,7 @@ bool dusk::IsRunning = true; bool dusk::IsShuttingDown = false; bool dusk::IsGameLaunched = false; bool dusk::IsFocusPaused = false; +std::filesystem::path dusk::ConfigPath; #endif s32 LOAD_COPYDATE(void*) { @@ -128,7 +131,6 @@ s32 LOAD_COPYDATE(void*) { AuroraInfo auroraInfo; AuroraStats dusk::lastFrameAuroraStats; float dusk::frameUsagePct = 0.0f; -const char* configPath; bool launchUILoop() { while (dusk::IsRunning && !dusk::IsGameLaunched) { @@ -240,8 +242,6 @@ void main01(void) { continue; } - const dusk::game_clock::MainLoopPacer pacing = dusk::game_clock::advance_main_loop(); - VIWaitForRetrace(); dusk::lastFrameAuroraStats = *aurora_get_stats(); @@ -252,28 +252,33 @@ void main01(void) { mDoGph_gInf_c::updateRenderSize(); - dusk::frame_interp::begin_frame(pacing.is_interpolating, pacing.do_sim_tick, pacing.interpolation_step); + const auto pacing = dusk::game_clock::advance_main_loop(); if (pacing.is_interpolating) { - if (pacing.do_sim_tick) { + if (pacing.sim_ticks_to_run > 0) { + dusk::frame_interp::begin_frame(true, true, 0.0f); dusk::frame_interp::set_ui_tick_pending(true); - mDoCPd_c::read(); - DuskDebugPad(); - dusk::gyro::read(pacing.sim_pace); - fapGm_Execute(); - mDoAud_Execute(); - dusk::game_clock::reset_accumulator(); + for (int sim_tick = 0; sim_tick < pacing.sim_ticks_to_run; ++sim_tick) { + dusk::frame_interp::begin_sim_tick(); + mDoCPd_c::read(); + DuskDebugPad(); + dusk::gyro::read(pacing.sim_pace); + fapGm_Execute(); + mDoAud_Execute(); + dusk::game_clock::commit_sim_tick(); + } } + + dusk::frame_interp::begin_frame(true, false, + dusk::game_clock::sample_interpolation_step()); dusk::frame_interp::interpolate(); dusk::frame_interp::begin_presentation_camera(); - if (!pacing.do_sim_tick) { - // run draw functions for anything specially marked to handle interp on non-sim - // ticks - fpcM_DrawIterater((fpcM_DrawIteraterFunc)fpcM_Draw); - } + // run draw functions for anything specially marked to handle interp + fpcM_DrawIterater((fpcM_DrawIteraterFunc)fpcM_Draw); cAPIGph_Painter(); dusk::frame_interp::end_presentation_camera(); dusk::frame_interp::set_ui_tick_pending(false); } else { + dusk::frame_interp::begin_frame(false, true, 0.0f); dusk::frame_interp::set_ui_tick_pending(true); // Game Inputs @@ -377,7 +382,7 @@ static void ApplyCVarOverrides(const cxxopts::OptionValue& option) { } } -static const char* CalculateConfigPath() { +static std::filesystem::path CalculateConfigPath() { const auto result = SDL_GetPrefPath(dusk::OrgName, dusk::AppName); if (!result) { DuskLog.fatal("Unable to get PrefPath: {}", SDL_GetError()); @@ -386,13 +391,12 @@ static const char* CalculateConfigPath() { return result; } -static void EnsureInitialPipelineCache(const char* configDir) { - if (configDir == nullptr) { +static void EnsureInitialPipelineCache(const std::filesystem::path& configDir) { + if (configDir.empty()) { return; } - const std::filesystem::path configPathFs(configDir); - const std::filesystem::path pipelineCachePath = configPathFs / "pipeline_cache.db"; + const std::filesystem::path pipelineCachePath = configDir / "pipeline_cache.db"; if (std::filesystem::exists(pipelineCachePath)) { return; } @@ -411,10 +415,10 @@ static void EnsureInitialPipelineCache(const char* configDir) { } std::error_code ec; - std::filesystem::create_directories(configPathFs, ec); + std::filesystem::create_directories(configDir, ec); if (ec) { DuskLog.warn("Failed to create config directory '{}' for pipeline cache: {}", - configPathFs.string(), ec.message()); + configDir.string(), ec.message()); return; } @@ -505,37 +509,38 @@ int game_main(int argc, char* argv[]) { exit(1); } - configPath = CalculateConfigPath(); + dusk::ConfigPath = CalculateConfigPath(); const auto startupLogLevel = static_cast(parsed_arg_options["log-level"].as()); - dusk::InitializeFileLogging(configPath, startupLogLevel); + dusk::InitializeFileLogging(dusk::ConfigPath, startupLogLevel); dusk::config::LoadFromUserPreferences(); ApplyCVarOverrides(parsed_arg_options["cvar"]); dusk::InitializeCrashReporting(); - EnsureInitialPipelineCache(configPath); - - AuroraConfig config{}; - config.appName = dusk::AppName; - config.configPath = configPath; - config.vsync = dusk::getSettings().video.enableVsync; - config.startFullscreen = dusk::getSettings().video.enableFullscreen; - config.windowPosX = -1; - config.windowPosY = -1; - config.windowWidth = defaultWindowWidth * 2; - config.windowHeight = defaultWindowHeight * 2; - config.desiredBackend = ResolveDesiredBackend(parsed_arg_options); - config.logCallback = &aurora_log_callback; - config.logLevel = startupLogLevel; - config.mem1Size = 256 * 1024 * 1024; - config.mem2Size = 24 * 1024 * 1024; - config.allowJoystickBackgroundEvents = true; - config.imGuiInitCallback = &aurora_imgui_init_callback; - config.allowTextureReplacements = true; - config.allowTextureDumps = false; - + EnsureInitialPipelineCache(dusk::ConfigPath); PADSetDefaultMapping(&defaultPadMapping); - auroraInfo = aurora_initialize(argc, argv, &config); + { + const auto configPathString = dusk::ConfigPath.string(); + AuroraConfig config{}; + config.appName = dusk::AppName; + config.configPath = configPathString.c_str(); + config.vsync = dusk::getSettings().video.enableVsync; + config.startFullscreen = dusk::getSettings().video.enableFullscreen; + config.windowPosX = -1; + config.windowPosY = -1; + config.windowWidth = defaultWindowWidth * 2; + config.windowHeight = defaultWindowHeight * 2; + config.desiredBackend = ResolveDesiredBackend(parsed_arg_options); + config.logCallback = &aurora_log_callback; + config.logLevel = startupLogLevel; + config.mem1Size = 256 * 1024 * 1024; + config.mem2Size = 24 * 1024 * 1024; + config.allowJoystickBackgroundEvents = true; + config.imGuiInitCallback = &aurora_imgui_init_callback; + config.allowTextureReplacements = true; + config.allowTextureDumps = false; + auroraInfo = aurora_initialize(argc, argv, &config); + } #ifdef DUSK_DISCORD_RPC dusk::discord::Initialize(); @@ -614,6 +619,8 @@ int game_main(int argc, char* argv[]) { main01(); + dusk::MoviePlayerShutdown(); + dusk::ShutdownCrashReporting(); dusk::ShutdownFileLogging(); fflush(stdout); diff --git a/tools/saves_to_states_json.py b/tools/saves_to_states_json.py new file mode 100644 index 0000000000..90032cf9d6 --- /dev/null +++ b/tools/saves_to_states_json.py @@ -0,0 +1,58 @@ +""" +Convert a folder of TPGZ saves to a states.json + +Usage: + python saves_to_states_json.py path/to/saves [prefix] + +Requirements: + pip install zstandard +""" + +import base64 +import json +import struct +import sys +import zstandard +from pathlib import Path + +SAVE_C_SIZE = 0x958 + +PACKET_FORMAT = "<8sbbh" + +RETURN_PLACE_OFF = 0x058 +NAME_OFF = RETURN_PLACE_OFF + 0x00 +ROOM_OFF = RETURN_PLACE_OFF + 0x09 +SPAWN_POINT_OFF = RETURN_PLACE_OFF + 0x08 + +folder = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent +out_path = folder / "states.json" + +if len(sys.argv) > 2: + prefix = sys.argv[2] +else: + prefix = None + +cctx = zstandard.ZstdCompressor(level=1) +states = [] + +for bin_path in sorted(folder.glob("*.bin")): + raw = bin_path.read_bytes() + save_c = raw[:SAVE_C_SIZE] + if len(save_c) < SAVE_C_SIZE: + print(f" skip {bin_path.name}: too small ({len(save_c)} bytes)") + continue + + stage_name = save_c[NAME_OFF:NAME_OFF + 8] + room_no = struct.unpack_from("b", save_c, ROOM_OFF)[0] + spawn_point = struct.unpack_from("B", save_c, SPAWN_POINT_OFF)[0] + + pkt = struct.pack(PACKET_FORMAT, stage_name, room_no, -1, spawn_point) + payload = pkt + save_c + encoded = base64.b64encode(cctx.compress(payload)).decode("ascii") + + stage_str = stage_name.rstrip(b"\x00").decode("ascii", errors="replace") + print(f" {bin_path.stem:30s} stage={stage_str!r} room={room_no} point={spawn_point}") + states.append({"name": f"({prefix}) {bin_path.stem}" if prefix else bin_path.stem, "data": encoded}) + +out_path.write_text(json.dumps(states, indent=2)) +print(f"\nWrote {len(states)} states to {out_path}")