diff --git a/CMakeLists.txt b/CMakeLists.txt index ee495ecad4..c0ab5aa26e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -182,8 +182,15 @@ FetchContent_Declare(miniz DOWNLOAD_EXTRACT_TIMESTAMP TRUE EXCLUDE_FROM_ALL ) +message(STATUS "dusklight: Fetching PicoSHA2") +FetchContent_Declare(picosha2 + URL https://github.com/okdshin/PicoSHA2/archive/refs/tags/v1.0.1.tar.gz + URL_HASH SHA256=9983136544234e573fe07cc1a22fdf978ad7979043e7be2e9082f6d4991ff8a8 + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + EXCLUDE_FROM_ALL +) -set(_fetch_content_deps miniz) +set(_fetch_content_deps miniz picosha2) if (DUSK_ENABLE_OPUS) message(STATUS "dusklight: Fetching opusfile") @@ -237,6 +244,14 @@ if (DUSK_HAS_FUNCHOOK) endif () FetchContent_MakeAvailable(${_fetch_content_deps}) +if (DUSK_HAS_FUNCHOOK AND APPLE) + target_sources(funchook-static PRIVATE src/dusk/mods/loader/code_patch_macos.cpp) + target_include_directories(funchook-static PRIVATE src/dusk/mods/loader) + set_source_files_properties(src/dusk/mods/loader/code_patch_macos.cpp + TARGET_DIRECTORY funchook-static PROPERTIES + COMPILE_OPTIONS "-O2;-fno-sanitize=all;-fno-stack-protector") +endif () + # Use signed char on ARM to match the original game (and x86) string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _arch) if(_arch MATCHES "^(arm|aarch64)" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU") @@ -262,8 +277,8 @@ include(cmake/GameABIConfig.cmake) find_package(Threads REQUIRED) set(GAME_COMPILE_DEFS DUSK_BUILDING_GAME=1) set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd aurora::thp - aurora::card borealis::cli borealis::crash borealis::data borealis::disc borealis::discord borealis::file_select borealis::io borealis::log borealis::net borealis::presentation borealis::sentry borealis::update borealis::ws freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt - Threads::Threads zstd::libzstd dusklight_game_headers) + aurora::card borealis::http borealis::cli borealis::crash borealis::data borealis::disc borealis::discord borealis::file_select borealis::io borealis::log borealis::net borealis::presentation borealis::sentry borealis::update borealis::ws freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt + Threads::Threads zstd::libzstd dusklight_game_headers picosha2 PNG::PNG) if (DUSK_HAS_FUNCHOOK) list(APPEND GAME_LIBS funchook-static) endif () diff --git a/README.md b/README.md index 473efd8b68..7445e43ea9 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ Dusklight is a reverse-engineered reimplementation of Twilight Princess. It aims to be as accurate as possible to the original while also providing new options, enhancements, and tools to customize your experience. +> [!IMPORTANT] +> Dusklight's official website is https://twilitrealm.dev/, any other website is not affiliated and may be promoting AI-generated misinformation. + # Setup > [!IMPORTANT] diff --git a/cmake/PatchFunchook.cmake b/cmake/PatchFunchook.cmake index 6d5a6e36b4..cbd91fc38e 100644 --- a/cmake/PatchFunchook.cmake +++ b/cmake/PatchFunchook.cmake @@ -2,59 +2,55 @@ file(READ "${SOURCE_DIR}/cmake/capstone.cmake.in" _content) # Insert PATCH_COMMAND before CONFIGURE_COMMAND in the ExternalProject_Add. # Bracket args prevent cmake from substituting ${...} while writing this file. -string(REPLACE - " CONFIGURE_COMMAND \"\"" - [=[ PATCH_COMMAND "${CMAKE_COMMAND}" -DDIR=${CMAKE_CURRENT_BINARY_DIR}/capstone-src -P "${CAPSTONE_FIX_SCRIPT}" +if (NOT _content MATCHES "CAPSTONE_FIX_SCRIPT") + string(REPLACE + " CONFIGURE_COMMAND \"\"" + [=[ PATCH_COMMAND "${CMAKE_COMMAND}" -DDIR=${CMAKE_CURRENT_BINARY_DIR}/capstone-src -P "${CAPSTONE_FIX_SCRIPT}" CONFIGURE_COMMAND ""]=] - _content "${_content}") - -file(WRITE "${SOURCE_DIR}/cmake/capstone.cmake.in" "${_content}") - -file(READ "${SOURCE_DIR}/src/funchook_unix.c" _unix_content) - -# macOS rejects the POSIX mprotect RWX/RW transition for executable image pages on arm64. -# Use Mach VM_PROT_COPY for the short patch window, then restore RX permissions. -if (NOT _unix_content MATCHES "VM_PROT_READ \\| VM_PROT_WRITE \\| VM_PROT_COPY") - string(REPLACE - [=[ rv = mprotect(mstate->addr, mstate->size, prot);]=] - [=[#ifdef __APPLE__ - kern_return_t kr = vm_protect(mach_task_self(), (vm_address_t)mstate->addr, - (vm_size_t)mstate->size, FALSE, - VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY); - if (kr == KERN_SUCCESS) { - funchook_log(funchook, " unprotect memory %p (size=%"PRIuPTR", prot=read,write,copy) <- %p (size=%"PRIuPTR")\n", - mstate->addr, mstate->size, start, len); - return 0; - } - funchook_set_error_message(funchook, "Failed to unprotect memory %p (size=%"PRIuPTR", prot=read,write,copy) <- %p (size=%"PRIuPTR", error=%s)", - mstate->addr, mstate->size, start, len, - mach_error_string(kr)); - return FUNCHOOK_ERROR_MEMORY_FUNCTION; -#endif - rv = mprotect(mstate->addr, mstate->size, prot);]=] - _unix_content "${_unix_content}") - - string(REPLACE - [=[ char errbuf[128]; - int rv = mprotect(mstate->addr, mstate->size, PROT_READ | PROT_EXEC);]=] - [=[ char errbuf[128]; -#ifdef __APPLE__ - kern_return_t kr = vm_protect(mach_task_self(), (vm_address_t)mstate->addr, - (vm_size_t)mstate->size, FALSE, - VM_PROT_READ | VM_PROT_EXECUTE); - - if (kr == KERN_SUCCESS) { - funchook_log(funchook, " protect memory %p (size=%"PRIuPTR", prot=read,exec)\n", - mstate->addr, mstate->size); - return 0; - } - funchook_set_error_message(funchook, "Failed to protect memory %p (size=%"PRIuPTR", prot=read,exec, error=%s)", - mstate->addr, mstate->size, - mach_error_string(kr)); - return FUNCHOOK_ERROR_MEMORY_FUNCTION; -#endif - int rv = mprotect(mstate->addr, mstate->size, PROT_READ | PROT_EXEC);]=] - _unix_content "${_unix_content}") + _content "${_content}") + file(WRITE "${SOURCE_DIR}/cmake/capstone.cmake.in" "${_content}") endif () -file(WRITE "${SOURCE_DIR}/src/funchook_unix.c" "${_unix_content}") +file(READ "${SOURCE_DIR}/src/funchook.c" _content) +if (NOT _content MATCHES "commit_code_patch") + string(REPLACE "#include \"funchook_internal.h\"" + "#include \"funchook_internal.h\"\n#ifdef __APPLE__\n#include \"code_patch_macos.hpp\"\n#endif" + _content "${_content}") + + foreach(_operation install uninstall) + if (_operation STREQUAL "install") + set(_expected old_code) + set(_replacement new_code) + else () + set(_expected new_code) + set(_replacement old_code) + endif () + set(_original " mem_state_t mstate; + int rv = funchook_unprotect_begin(funchook, &mstate, entry->target_func, JUMP32_BYTE_SIZE); + + if (rv != 0) { + return rv; + } + memcpy(entry->target_func, entry->${_replacement}, JUMP32_BYTE_SIZE); + rv = funchook_unprotect_end(funchook, &mstate); + if (rv != 0) { + return rv; + } + flush_instruction_cache(entry->target_func, JUMP32_BYTE_SIZE);") + string(FIND "${_content}" "${_original}" _position) + if (_position EQUAL -1) + message(FATAL_ERROR "Funchook ${_operation} patch site changed") + endif () + string(REPLACE "${_original}" "#ifdef __APPLE__ + int rv = commit_code_patch(entry->target_func, entry->${_expected}, + entry->${_replacement}, JUMP32_BYTE_SIZE); + if (rv != 0) { + funchook_set_error_message(funchook, \"Code patch commit failed (Mach error %d)\", rv); + return FUNCHOOK_ERROR_MEMORY_FUNCTION; + } +#else +${_original} +#endif" _content "${_content}") + endforeach () + file(WRITE "${SOURCE_DIR}/src/funchook.c" "${_content}") +endif () diff --git a/extern/borealis b/extern/borealis index f55910bd79..0bdba6c50a 160000 --- a/extern/borealis +++ b/extern/borealis @@ -1 +1 @@ -Subproject commit f55910bd79248db250ebd9debb6624d78538c21c +Subproject commit 0bdba6c50a46409c4862474c72b4a3a631fbe0ec diff --git a/files.cmake b/files.cmake index cdfbdad688..c7d451a39f 100644 --- a/files.cmake +++ b/files.cmake @@ -1422,6 +1422,8 @@ set(DUSK_FILES src/dusk/achievements.cpp src/dusk/action_bindings.cpp src/dusk/action_bindings.h + src/dusk/archive.cpp + src/dusk/archive.hpp src/dusk/asserts.cpp src/dusk/autosave.cpp src/dusk/config.cpp @@ -1432,10 +1434,10 @@ set(DUSK_FILES src/dusk/dvd_asset.cpp src/dusk/dvd_asset.hpp src/dusk/extras.c - src/dusk/frame_interpolation.cpp src/dusk/commands.cpp src/dusk/commands.hpp src/dusk/game_clock.cpp + src/dusk/hash.hpp src/dusk/game_mode.cpp src/dusk/gamepad_color.cpp src/dusk/globals.cpp @@ -1462,6 +1464,10 @@ set(DUSK_FILES src/dusk/imgui/ImGuiStateShare.cpp src/dusk/imgui/ImGuiStateShare.hpp src/dusk/imgui/ImGuiStubLog.cpp + src/dusk/interp/camera.cpp + src/dusk/interp/dual_buffer.cpp + src/dusk/interp/frame_interpolation.cpp + src/dusk/interp/line.cpp src/dusk/io.cpp src/dusk/iso_validate.cpp src/dusk/language.cpp @@ -1478,10 +1484,20 @@ set(DUSK_FILES src/dusk/mods/loader/depgraph.hpp src/dusk/mods/loader/loader.cpp src/dusk/mods/loader/loader.hpp + src/dusk/mods/loader/manifest.cpp + src/dusk/mods/loader/manifest.hpp + src/dusk/mods/loader/natives.cpp + src/dusk/mods/loader/natives.hpp + src/dusk/mods/loader/packages.cpp + src/dusk/mods/loader/packages.hpp src/dusk/mods/loader/native_module.cpp src/dusk/mods/loader/native_module.hpp src/dusk/mods/loader/prepatch.cpp src/dusk/mods/loader/prepatch.hpp + src/dusk/mods/catalog.cpp + src/dusk/mods/catalog.hpp + src/dusk/mods/queue.cpp + src/dusk/mods/queue.hpp src/dusk/mods/item.hpp src/dusk/mods/item_actor.cpp src/dusk/mods/item_checks.cpp @@ -1558,6 +1574,8 @@ set(DUSK_FILES src/dusk/ui/controls.hpp src/dusk/ui/document.cpp src/dusk/ui/document.hpp + src/dusk/ui/drop_install_modal.cpp + src/dusk/ui/drop_install_modal.hpp src/dusk/ui/editor.cpp src/dusk/ui/editor.hpp src/dusk/ui/event.cpp @@ -1578,8 +1596,18 @@ set(DUSK_FILES src/dusk/ui/list.hpp src/dusk/ui/menu_bar.cpp src/dusk/ui/menu_bar.hpp + src/dusk/ui/mod_browser.cpp + src/dusk/ui/mod_browser.hpp + src/dusk/ui/queue_window.cpp + src/dusk/ui/queue_window.hpp + src/dusk/ui/package_row.cpp + src/dusk/ui/package_row.hpp src/dusk/ui/mod_texture_provider.cpp src/dusk/ui/mod_texture_provider.hpp + src/dusk/ui/remote_texture_provider.cpp + src/dusk/ui/remote_texture_provider.hpp + src/dusk/ui/runtime_image.cpp + src/dusk/ui/runtime_image.hpp src/dusk/ui/mod_window.cpp src/dusk/ui/mod_window.hpp src/dusk/ui/modal.cpp @@ -1589,6 +1617,12 @@ set(DUSK_FILES src/dusk/ui/nav_types.hpp src/dusk/ui/nav_group.cpp src/dusk/ui/nav_group.hpp + src/dusk/ui/context_menu.cpp + src/dusk/ui/context_menu.hpp + src/dusk/ui/icon_button.cpp + src/dusk/ui/icon_button.hpp + src/dusk/ui/tooltip.cpp + src/dusk/ui/tooltip.hpp src/dusk/ui/number_button.cpp src/dusk/ui/number_button.hpp src/dusk/ui/overlay.cpp diff --git a/include/d/actor/d_a_alink.h b/include/d/actor/d_a_alink.h index 75ac1aac2b..a0b70747bc 100644 --- a/include/d/actor/d_a_alink.h +++ b/include/d/actor/d_a_alink.h @@ -4556,29 +4556,6 @@ public: bool checkAimContext(); bool checkAimInputContext(); - void onIronBallChainInterpCallback(); - - static const int IRON_BALL_CHAIN_COUNT = 102; - cXyz mIBChainInterpPrevPos[IRON_BALL_CHAIN_COUNT]; - cXyz mIBChainInterpCurrPos[IRON_BALL_CHAIN_COUNT]; - csXyz mIBChainInterpPrevAngle[IRON_BALL_CHAIN_COUNT]; - csXyz mIBChainInterpCurrAngle[IRON_BALL_CHAIN_COUNT]; - cXyz mIBChainInterpPrevHandRoot; - cXyz mIBChainInterpCurrHandRoot; - bool mIBChainInterpPrevValid; - bool mIBChainInterpCurrValid; - - cXyz mHsChainInterpPrevTop; - cXyz mHsChainInterpCurrTop; - cXyz mHsChainInterpPrevRoot; - cXyz mHsChainInterpCurrRoot; - cXyz mHsChainInterpPrevSubRoot; - cXyz mHsChainInterpCurrSubRoot; - cXyz mHsChainInterpPrevSubTop; - cXyz mHsChainInterpCurrSubTop; - bool mHsChainInterpPrevValid; - bool mHsChainInterpCurrValid; - bool mIsRollstab = false; void* mAnmBuffers[3] = {}; #endif diff --git a/include/d/actor/d_a_b_gnd.h b/include/d/actor/d_a_b_gnd.h index 0827102eec..dcf6c94905 100644 --- a/include/d/actor/d_a_b_gnd.h +++ b/include/d/actor/d_a_b_gnd.h @@ -189,12 +189,6 @@ public: /* 0x2740 */ u8 field_0x2740; /* 0x2744 */ dMsgFlow_c mMsgFlow; #if TARGET_PC - cXyz mReinsInterpPrev[2][16]; - cXyz mReinsInterpCurr[2][16]; - cXyz mReinsTexInterpPrev[2]; - cXyz mReinsTexInterpCurr[2]; - bool mReinsInterpPrevValid; - bool mReinsInterpCurrValid; s8 mDemoCamSyncTicks; #endif }; diff --git a/include/d/actor/d_a_e_db.h b/include/d/actor/d_a_e_db.h index 4f10715443..a95722a6d1 100644 --- a/include/d/actor/d_a_e_db.h +++ b/include/d/actor/d_a_e_db.h @@ -80,12 +80,6 @@ public: /* 0x125C */ u32 field_0x125c; /* 0x1260 */ u8 field_0x1260[0x126C - 0x1260]; /* 0x126C */ u8 HIOInit; -#if TARGET_PC - cXyz mStalkLineInterpPrev[12]; - cXyz mStalkLineInterpCurr[12]; - bool mStalkLineInterpPrevValid; - bool mStalkLineInterpCurrValid; -#endif }; STATIC_ASSERT(sizeof(e_db_class) == 0x1270); diff --git a/include/d/actor/d_a_e_hb.h b/include/d/actor/d_a_e_hb.h index 3069bcd325..a7e0241007 100644 --- a/include/d/actor/d_a_e_hb.h +++ b/include/d/actor/d_a_e_hb.h @@ -73,12 +73,6 @@ public: /* 0x124C */ f32 field_0x124c; /* 0x1250 */ u8 field_0x1250[0x1264 - 0x1250]; /* 0x1264 */ u8 HIOInit; -#if TARGET_PC - cXyz mStalkLineInterpPrev[12]; - cXyz mStalkLineInterpCurr[12]; - bool mStalkLineInterpPrevValid; - bool mStalkLineInterpCurrValid; -#endif }; STATIC_ASSERT(sizeof(e_hb_class) == 0x1268); diff --git a/include/d/actor/d_a_e_mb.h b/include/d/actor/d_a_e_mb.h index 527cf30eac..f36c744879 100644 --- a/include/d/actor/d_a_e_mb.h +++ b/include/d/actor/d_a_e_mb.h @@ -44,12 +44,6 @@ public: /* 0x88C */ u8 field_0x88C[0x8C8 - 0x88C]; /* 0x8C8 */ s8 field_0x8c8; /* 0x8C9 */ u8 mInitHIO; -#if TARGET_PC - cXyz mRopeInterpPrev[16]; - cXyz mRopeInterpCurr[16]; - bool mRopeInterpPrevValid; - bool mRopeInterpCurrValid; -#endif }; STATIC_ASSERT(sizeof(e_mb_class) == 0x8cc); diff --git a/include/d/actor/d_a_e_s1.h b/include/d/actor/d_a_e_s1.h index 5cbae84696..a631ba47ac 100644 --- a/include/d/actor/d_a_e_s1.h +++ b/include/d/actor/d_a_e_s1.h @@ -81,15 +81,6 @@ public: /* 0x306D */ u8 field_0x306D[0x307C - 0x306D]; /* 0x307C */ u32 mBodyEffEmtrID; /* 0x3080 */ u8 mInitHIO; - -#if TARGET_PC - static const int HAIR_STRAND_COUNT = 22; - static const int HAIR_SEGMENT_COUNT = 16; - cXyz mHairInterpPrev[HAIR_STRAND_COUNT * HAIR_SEGMENT_COUNT]; - cXyz mHairInterpCurr[HAIR_STRAND_COUNT * HAIR_SEGMENT_COUNT]; - bool mHairInterpPrevValid; - bool mHairInterpCurrValid; -#endif }; STATIC_ASSERT(sizeof(e_s1_class) == 0x3084); diff --git a/include/d/actor/d_a_e_wb.h b/include/d/actor/d_a_e_wb.h index aecbac96a1..770e537f2e 100644 --- a/include/d/actor/d_a_e_wb.h +++ b/include/d/actor/d_a_e_wb.h @@ -221,12 +221,6 @@ public: /* 0x17E4 */ u8 field_0x17e4[0x17e8 - 0x17e4]; /* 0x17E8 */ f32 ride_speed_max; ///< @brief Speed rate for riding calculations. #if TARGET_PC - cXyz himo_mat_interp_prev[2][16]; - cXyz himo_mat_interp_curr[2][16]; - cXyz himo_tex_interp_prev[2]; - cXyz himo_tex_interp_curr[2]; - bool himo_interp_prev_valid; - bool himo_interp_curr_valid; s8 demo_cam_sync_ticks; #endif }; diff --git a/include/d/actor/d_a_e_yd.h b/include/d/actor/d_a_e_yd.h index 44d6036600..188e435ba5 100644 --- a/include/d/actor/d_a_e_yd.h +++ b/include/d/actor/d_a_e_yd.h @@ -74,12 +74,6 @@ public: /* 0x1250 */ f32 field_0x1250; /* 0x1254 */ u8 field_0x1254[0x1268 - 0x1254]; /* 0x1268 */ u8 field_0x1268; -#if TARGET_PC - cXyz mLineMatInterpPrev[12]; - cXyz mLineMatInterpCurr[12]; - bool mLineMatInterpPrevValid; - bool mLineMatInterpCurrValid; -#endif }; STATIC_ASSERT(sizeof(e_yd_class) == 0x126c); diff --git a/include/d/actor/d_a_e_yg.h b/include/d/actor/d_a_e_yg.h index 6be19933cb..69a85430a8 100644 --- a/include/d/actor/d_a_e_yg.h +++ b/include/d/actor/d_a_e_yg.h @@ -63,15 +63,6 @@ public: /* 0x0BB4 */ yg_ke_s mYgKes[13]; /* 0x1880 */ mDoExt_3DlineMat0_c mLineMat; /* 0x189C */ u8 mIsFirstSpawn; - -#if TARGET_PC - static const int TENTACLE_STRAND_COUNT = 13; - static const int TENTACLE_SEGMENT_COUNT = 10; - cXyz mTentacleInterpPrev[TENTACLE_STRAND_COUNT * TENTACLE_SEGMENT_COUNT]; - cXyz mTentacleInterpCurr[TENTACLE_STRAND_COUNT * TENTACLE_SEGMENT_COUNT]; - bool mTentacleInterpPrevValid; - bool mTentacleInterpCurrValid; -#endif }; STATIC_ASSERT(sizeof(e_yg_class) == 0x18a0); diff --git a/include/d/actor/d_a_e_yh.h b/include/d/actor/d_a_e_yh.h index 63e8ac1882..519e5e2779 100644 --- a/include/d/actor/d_a_e_yh.h +++ b/include/d/actor/d_a_e_yh.h @@ -77,12 +77,6 @@ public: /* 0x1260 */ u32 field_0x1260; /* 0x1260 */ u8 field_0x1264[0x1270 - 0x1264]; /* 0x1270 */ bool mIsHIOOwner; -#if TARGET_PC - cXyz mLineInterpPrev[12]; - cXyz mLineInterpCurr[12]; - bool mLineInterpPrevValid; - bool mLineInterpCurrValid; -#endif }; STATIC_ASSERT(sizeof(e_yh_class) == 0x1274); diff --git a/include/d/actor/d_a_horse.h b/include/d/actor/d_a_horse.h index bb7ce7ccb1..2074570517 100644 --- a/include/d/actor/d_a_horse.h +++ b/include/d/actor/d_a_horse.h @@ -196,9 +196,6 @@ public: void copyReinPos(); void setReinPosHandSubstance(int); void setReinPosNormalSubstance(); -#if TARGET_PC - void lerpControlPoints(f32 alpha); -#endif void bgCheck(); bool checkSpecialWallHitSubstance(cXyz const&) const; void setServiceWaitTimer(); diff --git a/include/d/actor/d_a_mg_rod.h b/include/d/actor/d_a_mg_rod.h index 97a0bb1c72..76863f33d5 100644 --- a/include/d/actor/d_a_mg_rod.h +++ b/include/d/actor/d_a_mg_rod.h @@ -299,13 +299,6 @@ public: /* 0x168C */ u8 field_0x168c; /* 0x168D */ u8 field_0x168d; /* 0x168E */ u8 HIOInit; - -#if TARGET_PC - cXyz mLineInterpPrev[MG_ROD_LURE_LINE_LEN]; - cXyz mLineInterpCurr[MG_ROD_LURE_LINE_LEN]; - bool mLineInterpPrevValid; - bool mLineInterpCurrValid; -#endif }; #endif /* D_A_MG_ROD_H */ diff --git a/include/d/actor/d_a_npc_zra.h b/include/d/actor/d_a_npc_zra.h index ee786522f9..825076e192 100644 --- a/include/d/actor/d_a_npc_zra.h +++ b/include/d/actor/d_a_npc_zra.h @@ -370,6 +370,9 @@ public: u32 getAngleNoFromParam() { return (u8)(fopAcM_GetParam(this) >> 8); } void setBlastFlag(u8 i_flag) { mBlastFlag = i_flag; } MtxP getHeadMtx() { return mAnm_p->getModel()->getAnmMtx(4); } +#if TARGET_PC + friend void daNpc_zrA_interp_callback(void* pUserWork); +#endif /* 0x0B48 */ Z2Creature mCreatureSound; /* 0x0BD8 */ J3DModel* mpObjectModel[3]; diff --git a/include/d/actor/d_a_obj_fchain.h b/include/d/actor/d_a_obj_fchain.h index 80e96f097b..e9250fbe24 100644 --- a/include/d/actor/d_a_obj_fchain.h +++ b/include/d/actor/d_a_obj_fchain.h @@ -31,10 +31,6 @@ public: csXyz* getAngle() { return field_0x8a4; } J3DModelData* getModelData() { return mModelData; } -#if TARGET_PC - void onInterpCallback(); -#endif - /* 0x568 */ request_of_phase_process_class mPhase; /* 0x570 */ J3DModelData* mModelData; /* 0x574 */ daObjFchain_shape_c mShape; @@ -45,14 +41,6 @@ public: /* 0x694 */ cXyz field_0x694[22]; /* 0x79C */ cXyz field_0x79c[22]; /* 0x8A4 */ csXyz field_0x8a4[22]; - -#if TARGET_PC - static const int CHAIN_COUNT = 22; - cXyz mChainInterpPrev[CHAIN_COUNT]; - cXyz mChainInterpCurr[CHAIN_COUNT]; - bool mChainInterpPrevValid; - bool mChainInterpCurrValid; -#endif }; STATIC_ASSERT(sizeof(daObjFchain_c) == 0x928); diff --git a/include/d/actor/d_a_obj_keyhole.h b/include/d/actor/d_a_obj_keyhole.h index b8a813776d..fd39641ad8 100644 --- a/include/d/actor/d_a_obj_keyhole.h +++ b/include/d/actor/d_a_obj_keyhole.h @@ -66,18 +66,9 @@ public: /* 0x2CA7 */ s8 hide_lock; /* 0x2CA8 */ cXyz field_0x2ca8; /* 0x2CB4 */ u8 field_0x2cb4; - -#if TARGET_PC - Mtx mChainInterpPrev[6][16]; - Mtx mChainInterpCurr[6][16]; - bool mChainInterpPrevValid; - bool mChainInterpCurrValid; -#endif }; -#if !TARGET_PC STATIC_ASSERT(sizeof(obj_keyhole_class) == 0x2CB8); -#endif class daObj_Keyhole_HIO_c : public JORReflexible { public: diff --git a/include/d/actor/d_a_obj_klift00.h b/include/d/actor/d_a_obj_klift00.h index d87c2d4459..a41c5c7b5c 100644 --- a/include/d/actor/d_a_obj_klift00.h +++ b/include/d/actor/d_a_obj_klift00.h @@ -26,7 +26,7 @@ public: int Delete(); #if TARGET_PC - void onInterpCallback(); + void onInterpPresentation(); #endif enum Param_e { @@ -53,13 +53,6 @@ public: /* 0x1020 */ dCcD_Cyl mCylinderCollider; /* 0x115C */ s32 mStopSwingingFrames; -#if TARGET_PC - cXyz mChainInterpPrev[64]; - cXyz mChainInterpCurr[64]; - bool mChainInterpPrevValid; - bool mChainInterpCurrValid; -#endif - // Number of chain models u32 getArg0() { return fopAcM_GetParamBit(this, 0, 6); diff --git a/include/d/actor/d_a_obj_lv8Lift.h b/include/d/actor/d_a_obj_lv8Lift.h index 5c2ab58219..4f243d0906 100644 --- a/include/d/actor/d_a_obj_lv8Lift.h +++ b/include/d/actor/d_a_obj_lv8Lift.h @@ -59,7 +59,7 @@ public: int Draw(); int Delete(); #if TARGET_PC - friend void daL8Lift_interp_callback(bool isSimFrame, void* pUserWork); + friend void daL8Lift_interp_callback(void* pUserWork); #endif u8 getPthID() { return fopAcM_GetParamBit(this, 0, 8); } diff --git a/include/d/d_drawlist.h b/include/d/d_drawlist.h index f792f544cc..4baf6ed324 100644 --- a/include/d/d_drawlist.h +++ b/include/d/d_drawlist.h @@ -441,7 +441,7 @@ public: } #if TARGET_PC - void refresh3DlineMats(const cXyz& eye); + void refresh3DlineMats(); #endif void peekZdata() { mPeekZ.peekData(); } diff --git a/include/m_Do/m_Do_ext.h b/include/m_Do/m_Do_ext.h index bd1baac2df..4c1ceeb997 100644 --- a/include/m_Do/m_Do_ext.h +++ b/include/m_Do/m_Do_ext.h @@ -542,10 +542,18 @@ public: virtual void setMaterial() = 0; virtual void draw() = 0; #if TARGET_PC - virtual void refreshGeometryForPresentationEye(const cXyz& eye) {} + virtual void captureInterpPoints() {} + virtual void refreshGeometryForPresentation() {} #endif /* 0x4 */ mDoExt_3DlineMat_c* field_0x4; + +#if TARGET_PC +protected: + u8 mInterpKind; + f32 mInterpWidth; + u16 mInterpTaper; +#endif }; class mDoExt_3DlineMat0_c : public mDoExt_3DlineMat_c { @@ -567,6 +575,10 @@ public: virtual int getMaterialID() { return 0; } virtual void setMaterial(); virtual void draw(); +#if TARGET_PC + void captureInterpPoints() override; + void refreshGeometryForPresentation() override; +#endif cXyz* getPos(int param_0) { return field_0x18[param_0].field_0x0; } f32* getSize(int param_0) { return field_0x18[param_0].field_0x4; } @@ -585,18 +597,14 @@ class dKy_tevstr_c; class mDoExt_3DlineMat1_c : public mDoExt_3DlineMat_c { public: int init(u16, u16, ResTIMG*, int); -#if TARGET_PC - void update(int, GXColor&, dKy_tevstr_c*, const cXyz* presentationEye = nullptr); - void update(int, f32, GXColor&, u16, dKy_tevstr_c*, const cXyz* presentationEye = nullptr); -#else void update(int, GXColor&, dKy_tevstr_c*); void update(int, f32, GXColor&, u16, dKy_tevstr_c*); -#endif int getMaterialID() { return 1; } void setMaterial(); void draw(); #if TARGET_PC - void refreshGeometryForPresentationEye(const cXyz& eye) override; + void captureInterpPoints() override; + void refreshGeometryForPresentation() override; #endif cXyz* getPos(int i_idx) { return mpLines[i_idx].field_0x0; } @@ -611,11 +619,6 @@ private: /* 0x34 */ u16 field_0x34; /* 0x36 */ u8 mIsDrawn; /* 0x38 */ mDoExt_3Dline_c* mpLines; -#if TARGET_PC - u8 mInterpLineKind; - f32 mInterpLineF; - u16 mInterpLineU16; -#endif }; class mDoExt_3DlineMat2_c : public mDoExt_3DlineMat1_c { diff --git a/libs/JSystem/include/JSystem/J2DGraph/J2DManage.h b/libs/JSystem/include/JSystem/J2DGraph/J2DManage.h index 27218d0d0d..3eb1967e2c 100644 --- a/libs/JSystem/include/JSystem/J2DGraph/J2DManage.h +++ b/libs/JSystem/include/JSystem/J2DGraph/J2DManage.h @@ -3,6 +3,10 @@ #include +#if TARGET_PC + #include "helpers/endian.h" +#endif + class JSUInputStream; /** diff --git a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h index d55bb82465..a7edb19b70 100644 --- a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h +++ b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h @@ -79,7 +79,9 @@ public: virtual ~J3DModel() {} #if TARGET_PC - static void interp_callback(bool isSimFrame, void* pUserWork); + static void interp_callback(void* pUserWork); + void calc_presentation_base_mtx(); + void prepare_presentation_view(); #endif J3DModelData* getModelData() { return mModelData; } @@ -133,6 +135,9 @@ public: /* 0xD0 */ J3DVtxColorCalc* mVtxColorCalc; /* 0xD4 */ J3DUnkCalc1* mUnkCalc1; /* 0xD8 */ J3DUnkCalc2* mUnkCalc2; +#if TARGET_PC + Mtx mPresentationBase; +#endif }; #endif /* J3DMODEL_H */ diff --git a/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp b/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp index 51f3951e38..dfb9050cbd 100644 --- a/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp +++ b/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp @@ -10,7 +10,7 @@ #include "JSystem/JKernel/JKRHeap.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif #define J3D_ASSERTMSG(LINE, COND, MSG) JUT_ASSERT_MSG(LINE, (COND) != 0, MSG) @@ -29,6 +29,7 @@ void J3DModel::initialize() { MTXIdentity(mBaseTransformMtx); MTXIdentity(mInternalView); + IF_DUSK(MTXIdentity(mPresentationBase)); mMtxBuffer = NULL; mMatPacket = NULL; @@ -101,17 +102,32 @@ s32 J3DModel::entryModelData(J3DModelData* pModelData, u32 mdlFlags, u32 mtxNum) } #if TARGET_PC -void J3DModel::interp_callback(bool isSimFrame, void* pUserWork) { +void J3DModel::interp_callback(void* pUserWork) { J3DModel* i_this = static_cast(pUserWork); - if (!isSimFrame) { - i_this->calcMaterial(); - i_this->diff(); - } + i_this->calcMaterial(); + i_this->diff(); } void J3DModel::setAnmMtx(int jointNo, Mtx m) { mMtxBuffer->setAnmMtx(jointNo, m); - dusk::frame_interp::record_final_mtx(mMtxBuffer->getAnmMtx(jointNo)); + dusk::interp::record_final_mtx(mMtxBuffer->getAnmMtx(jointNo)); +} + +void J3DModel::calc_presentation_base_mtx() { + Mtx identity; + MTXIdentity(identity); + J3DCalcViewBaseMtx(identity, mBaseScale, mBaseTransformMtx, mPresentationBase); + dusk::interp::record_final_mtx(mPresentationBase); + prepare_presentation_view(); +} + +void J3DModel::prepare_presentation_view() { + Mtx replacement; + MtxP presentationBase = mPresentationBase; + if (dusk::interp::lookup_replacement(mPresentationBase, replacement)) { + presentationBase = replacement; + } + MTXConcat(j3dSys.getViewMtx(), presentationBase, mInternalView); } #endif @@ -470,11 +486,11 @@ void J3DModel::calc() { #ifdef TARGET_PC for (u16 i = 0; i < mModelData->getJointNum(); ++i) { - dusk::frame_interp::record_final_mtx(getAnmMtx(i)); + dusk::interp::record_final_mtx(getAnmMtx(i)); } for (u16 i = 0; i < mModelData->getWEvlpMtxNum(); ++i) { - dusk::frame_interp::record_final_mtx(getWeightAnmMtx(i)); + dusk::interp::record_final_mtx(getWeightAnmMtx(i)); } #endif } @@ -505,8 +521,9 @@ void J3DModel::entry() { } #if TARGET_PC - if (mModelData->needsInterpCallBack()) - dusk::frame_interp::add_interpolation_callback(&J3DModel::interp_callback, this); + if (mModelData->needsInterpCallBack()) { + dusk::interp::add_interpolation_callback(&J3DModel::interp_callback, this); + } #endif } @@ -516,18 +533,20 @@ void J3DModel::viewCalc() { if (getModelData()->checkFlag(0x10)) { if (getMtxCalcMode() == 2) { +#if TARGET_PC + calc_presentation_base_mtx(); +#else J3DCalcViewBaseMtx(j3dSys.getViewMtx(), mBaseScale, mBaseTransformMtx, (MtxP)&mInternalView); -#ifdef TARGET_PC - dusk::frame_interp::record_final_mtx(mInternalView); #endif } } else if (isCpuSkinningOn()) { if (getMtxCalcMode() == 2) { +#if TARGET_PC + calc_presentation_base_mtx(); +#else J3DCalcViewBaseMtx(j3dSys.getViewMtx(), mBaseScale, mBaseTransformMtx, (MtxP)&mInternalView); -#ifdef TARGET_PC - dusk::frame_interp::record_final_mtx(mInternalView); #endif } } else if (checkFlag(J3DMdlFlag_SkinPosCpu)) { @@ -549,15 +568,6 @@ void J3DModel::viewCalc() { DCStoreRange(getNrmMtxPtr(), mModelData->getDrawMtxNum() * sizeof(Mtx33)); } -#ifdef TARGET_PC - Mtx* drawMtx = getDrawMtxPtr(); - if (drawMtx != J3DMtxBuffer::sNoUseDrawMtxPtr) { - for (u16 i = 0; i < mModelData->getDrawMtxNum(); ++i) { - dusk::frame_interp::record_final_mtx(drawMtx[i]); - } - } -#endif - prepareShapePackets(); } diff --git a/libs/JSystem/src/J3DGraphBase/J3DPacket.cpp b/libs/JSystem/src/J3DGraphBase/J3DPacket.cpp index 38cfddd260..fb82b8244d 100644 --- a/libs/JSystem/src/J3DGraphBase/J3DPacket.cpp +++ b/libs/JSystem/src/J3DGraphBase/J3DPacket.cpp @@ -342,6 +342,11 @@ int J3DShapePacket::newDifferedDisplayList(u32 diffFlags) { void J3DShapePacket::prepareDraw() const { mpModel->getVertexBuffer()->setArray(); j3dSys.setModel(mpModel); +#if TARGET_PC + if (mpModel->getMtxCalcMode() == 2) { + mpModel->prepare_presentation_view(); + } +#endif j3dSys.setShapePacket((J3DShapePacket*)this); J3DShapeMtx::setLODFlag(mpModel->checkFlag(J3DMdlFlag_EnableLOD) != 0); diff --git a/libs/JSystem/src/J3DGraphBase/J3DShapeMtx.cpp b/libs/JSystem/src/J3DGraphBase/J3DShapeMtx.cpp index cedc451cb4..ae9021f76c 100644 --- a/libs/JSystem/src/J3DGraphBase/J3DShapeMtx.cpp +++ b/libs/JSystem/src/J3DGraphBase/J3DShapeMtx.cpp @@ -8,14 +8,14 @@ #include "JSystem/J3DGraphBase/J3DTexture.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif DUSK_GAME_DATA u16 J3DShapeMtx::sMtxLoadCache[10]; #if TARGET_PC static void J3DFrameInterpConcat(MtxP lhs, MtxP rhs, Mtx out) { - if (!dusk::frame_interp::lookup_concat_replacement(lhs, rhs, out)) { + if (!dusk::interp::lookup_concat_replacement(lhs, rhs, out)) { MTXConcat(lhs, rhs, out); } } diff --git a/libs/JSystem/src/J3DGraphBase/J3DSys.cpp b/libs/JSystem/src/J3DGraphBase/J3DSys.cpp index f1810ca158..0f5beb9db5 100644 --- a/libs/JSystem/src/J3DGraphBase/J3DSys.cpp +++ b/libs/JSystem/src/J3DGraphBase/J3DSys.cpp @@ -7,7 +7,7 @@ #include "global.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include "helpers/gx_helper.h" #include @@ -379,7 +379,7 @@ void J3DSys::reinitPixelProc() { #if TARGET_PC void J3DSys::setViewMtx(const Mtx m) { Mtx patched; - if (dusk::frame_interp::lookup_replacement(m, patched)) { + if (dusk::interp::lookup_replacement(m, patched)) { m = patched; } MTXCopy(m, mViewMtx); diff --git a/libs/JSystem/src/JAudio2/JASChannel.cpp b/libs/JSystem/src/JAudio2/JASChannel.cpp index 3799832119..5b4a377321 100644 --- a/libs/JSystem/src/JAudio2/JASChannel.cpp +++ b/libs/JSystem/src/JAudio2/JASChannel.cpp @@ -176,11 +176,7 @@ void JASChannel::updateEffectorParam(JASDsp::TChannel* i_channel, u16* i_mixerVo f32 pan = 0.5f; f32 dolby = 0.0f; -#if TARGET_PC - u32 effectiveOutputMode = dusk::audio::EnableHrtf ? JAS_OUTPUT_SURROUND : JASDriver::getOutputMode(); -#else u32 effectiveOutputMode = JASDriver::getOutputMode(); -#endif switch (effectiveOutputMode) { case JAS_OUTPUT_MONO: break; diff --git a/libs/JSystem/src/JAudio2/JASDriverIF.cpp b/libs/JSystem/src/JAudio2/JASDriverIF.cpp index 87a88adca0..a08e3339ea 100644 --- a/libs/JSystem/src/JAudio2/JASDriverIF.cpp +++ b/libs/JSystem/src/JAudio2/JASDriverIF.cpp @@ -3,6 +3,7 @@ #include "JSystem/JAudio2/JASDriverIF.h" #include "JSystem/JAudio2/JASAiCtrl.h" #include "JSystem/JAudio2/JASDSPInterface.h" +#include "dusk/settings.h" #include void JASDriver::setDSPLevel(f32 param_0) { @@ -30,7 +31,18 @@ void JASDriver::setOutputMode(u32 mode) { } u32 JASDriver::getOutputMode() { +#ifdef TARGET_PC + switch (dusk::getSettings().audio.outputMode) { + case dusk::AudioOutputMode::StereoSpeakers: + return JAS_OUTPUT_STEREO; + case dusk::AudioOutputMode::StereoHeadphones: + case dusk::AudioOutputMode::Surround6ch: + case dusk::AudioOutputMode::Surround8ch: + return JAS_OUTPUT_SURROUND; + } +#else return JASDriver::JAS_SYSTEM_OUTPUT_MODE; +#endif } void JASDriver::waitSubFrame() { diff --git a/libs/JSystem/src/JFramework/JFWDisplay.cpp b/libs/JSystem/src/JFramework/JFWDisplay.cpp index 8d6f4eb868..0524cb86f7 100644 --- a/libs/JSystem/src/JFramework/JFWDisplay.cpp +++ b/libs/JSystem/src/JFramework/JFWDisplay.cpp @@ -14,7 +14,7 @@ #if TARGET_PC #include "dusk/dusk.h" -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/logging.h" #include "dusk/settings.h" #include "dusk/time.h" @@ -218,8 +218,8 @@ void JFWDisplay::endGX() { if (mFader != NULL) { ortho.setPort(); -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) { +#if TARGET_PC + if (dusk::interp::get_ui_tick_pending()) { mFader->advance(); } if (mFader->getStatus() != JUTFader::Wait) { @@ -382,7 +382,7 @@ static void waitForTick(u32 p1, u16 p2) { #if TARGET_PC static Limiter limiter; - if (dusk::frame_interp::is_enabled() || dusk::getTransientSettings().turboMode) { + if (dusk::interp::is_enabled() || dusk::getTransientSettings().turboMode) { limiter.Reset(); dusk::frameUsagePct = 0.f; return; diff --git a/libs/JSystem/src/JParticle/JPABaseShape.cpp b/libs/JSystem/src/JParticle/JPABaseShape.cpp index bab7e89646..bebf947fec 100644 --- a/libs/JSystem/src/JParticle/JPABaseShape.cpp +++ b/libs/JSystem/src/JParticle/JPABaseShape.cpp @@ -10,7 +10,7 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include #endif @@ -552,7 +552,7 @@ static void submit_particle_quad( void JPAInterpBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { Mtx ptclPosMtx; MTXTrans(ptclPosMtx, ptcl->mPosition.x, ptcl->mPosition.y, ptcl->mPosition.z); - dusk::frame_interp::record_final_mtx(ptclPosMtx, ptcl); + dusk::interp::record_final_mtx(ptclPosMtx, ptcl); } void JPAInterpRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { @@ -564,7 +564,7 @@ void JPAInterpRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { ptclPosMtx[0][1] = -sinRot; ptclPosMtx[1][0] = sinRot; ptclPosMtx[1][1] = cosRot; - dusk::frame_interp::record_final_mtx(ptclPosMtx, ptcl); + dusk::interp::record_final_mtx(ptclPosMtx, ptcl); } #endif @@ -577,7 +577,7 @@ void JPADrawBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_C JGeometry::TVec3 pos; #if TARGET_PC Mtx ptclPosMtx; - if (dusk::frame_interp::lookup_replacement(ptcl, ptclPosMtx)) { + if (dusk::interp::lookup_replacement(ptcl, ptclPosMtx)) { pos.set(ptclPosMtx[0][3], ptclPosMtx[1][3], ptclPosMtx[2][3]); MTXMultVec(work->mPosCamMtx, &pos, &pos); } else @@ -617,7 +617,7 @@ void JPADrawRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRA #if TARGET_PC Mtx ptclPosMtx; MTXTrans(ptclPosMtx, ptcl->mPosition.x, ptcl->mPosition.y, ptcl->mPosition.z); - if (dusk::frame_interp::lookup_replacement(ptcl, ptclPosMtx)) { + if (dusk::interp::lookup_replacement(ptcl, ptclPosMtx)) { pos.set(ptclPosMtx[0][3], ptclPosMtx[1][3], ptclPosMtx[2][3]); sinRot = ptclPosMtx[1][0]; cosRot = ptclPosMtx[0][0]; @@ -994,7 +994,7 @@ void JPAInterpDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { posMtx[2][2] = axisZ.z; posMtx[2][3] = ptcl->mPosition.z; p_plane[work->mPlaneType](posMtx, scaleX, scaleY); - dusk::frame_interp::record_final_mtx(posMtx, ptcl); + dusk::interp::record_final_mtx(posMtx, ptcl); } void JPAInterpRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { @@ -1037,7 +1037,7 @@ void JPAInterpRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { mtx2[2][2] = axisZ.z; mtx2[2][3] = ptcl->mPosition.z; MTXConcat(mtx2, mtx1, mtx1); - dusk::frame_interp::record_final_mtx(mtx1, ptcl); + dusk::interp::record_final_mtx(mtx1, ptcl); } #endif @@ -1050,7 +1050,7 @@ void JPADrawDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_C Mtx posMtx; #if TARGET_PC - if (!dusk::frame_interp::lookup_replacement(ptcl, posMtx) && + if (!dusk::interp::lookup_replacement(ptcl, posMtx) && !make_direction_mtx(work, ptcl, posMtx)) { return; @@ -1113,7 +1113,7 @@ void JPADrawRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRA Mtx mtx1; Mtx mtx2; #if TARGET_PC - if (!dusk::frame_interp::lookup_replacement(ptcl, mtx1) && + if (!dusk::interp::lookup_replacement(ptcl, mtx1) && !make_rot_direction_mtx(work, ptcl, mtx1)) { return; diff --git a/libs/JSystem/src/JParticle/JPAParticle.cpp b/libs/JSystem/src/JParticle/JPAParticle.cpp index 99ba95453c..0bb03d607a 100644 --- a/libs/JSystem/src/JParticle/JPAParticle.cpp +++ b/libs/JSystem/src/JParticle/JPAParticle.cpp @@ -8,7 +8,7 @@ #include "JSystem/JParticle/JPAExtraShape.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif JPAParticleCallBack::~JPAParticleCallBack() { @@ -210,7 +210,7 @@ void JPABaseParticle::init_c(JPAEmitterWorkData* work, JPABaseParticle* parent) #if TARGET_PC void JPABaseParticle::interp(JPAEmitterWorkData* work, void const* drawFunc) { - if (!dusk::frame_interp::is_enabled()) + if (!dusk::interp::is_enabled()) return; // don't interpolate the first frame diff --git a/libs/JSystem/src/JParticle/JPAResource.cpp b/libs/JSystem/src/JParticle/JPAResource.cpp index 1eef1b08a5..8a4d931e16 100644 --- a/libs/JSystem/src/JParticle/JPAResource.cpp +++ b/libs/JSystem/src/JParticle/JPAResource.cpp @@ -15,7 +15,7 @@ #include "global.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include @@ -839,7 +839,7 @@ bool JPAResource::calc(JPAEmitterWorkData* work, JPABaseEmitter* emtr) { #ifdef TARGET_PC if (((pBsp && pBsp->getDirType() == 3) || (pCsp && pCsp->getDirType() == 3)) && - dusk::frame_interp::is_enabled()) + dusk::interp::is_enabled()) { // ensure mGlobalEmtrDir is valid calcWorkData_d(work); diff --git a/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp b/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp index 2f408d6184..b6f41098e5 100644 --- a/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp +++ b/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp @@ -4,7 +4,7 @@ #if TARGET_PC #include "dusk/audio.h" -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/settings.h" #endif @@ -655,10 +655,10 @@ value_or_fun: value: #if TARGET_PC - if (dusk::frame_interp::is_enabled() && u <= 5 && + if (dusk::interp::is_enabled() && u <= 5 && (operation == data::UNK_0x2 || operation == data::UNK_0x3 || operation == data::UNK_0x12)) { - dusk::frame_interp::request_presentation_sync(); + dusk::interp::request_presentation_sync(); } #endif adaptor->adaptor_setVariableValue(control, u, operation, param_2, param_3); @@ -666,11 +666,11 @@ value: value_n: #if TARGET_PC - if (dusk::frame_interp::is_enabled() && + if (dusk::interp::is_enabled() && (pN == TAdaptor_camera::sauVariableValue_3_POSITION_XYZ || pN == TAdaptor_camera::sauVariableValue_3_TARGET_POSITION_XYZ) && (operation == data::UNK_0x2 || operation == data::UNK_0x3 || operation == data::UNK_0x12)) { - dusk::frame_interp::request_presentation_sync(); + dusk::interp::request_presentation_sync(); } #endif adaptor->adaptor_setVariableValue_n(control, pN, u, operation, param_2, param_3); diff --git a/libs/JSystem/src/JUtility/JUTFader.cpp b/libs/JSystem/src/JUtility/JUTFader.cpp index a8d9fe6028..2e937b176e 100644 --- a/libs/JSystem/src/JUtility/JUTFader.cpp +++ b/libs/JSystem/src/JUtility/JUTFader.cpp @@ -9,8 +9,9 @@ #include "JSystem/J2DGraph/J2DOrthoGraph.h" #ifdef TARGET_PC +#include "dusk/interp/frame_interpolation.h" + #include -#include "dusk/frame_interpolation.h" #endif JUTFader::JUTFader(int x, int y, int width, int height, JUtility::TColor pColor) @@ -74,8 +75,8 @@ void JUTFader::control() { void JUTFader::draw() { if (mColor.a != 0) { #ifdef TARGET_PC - if (dusk::frame_interp::is_enabled() && mDuration != 0) { - const auto step = dusk::frame_interp::get_interpolation_step(); + if (dusk::interp::is_enabled() && mDuration != 0) { + const auto step = dusk::interp::get_interpolation_step(); const auto progress = static_cast(mTimer) / static_cast(mDuration); const auto timer = mTimer - 1 + step + progress; auto alpha = timer / mDuration; diff --git a/mods/luau_runtime/src/runtime.cpp b/mods/luau_runtime/src/runtime.cpp index 2583d33a10..664bb1d9b1 100644 --- a/mods/luau_runtime/src/runtime.cpp +++ b/mods/luau_runtime/src/runtime.cpp @@ -161,6 +161,7 @@ std::optional normalize_module_path( } int module_require(lua_State* state); +int print(lua_State* state); void install_module_require(lua_State* state, Vm& vm, std::string_view currentPath) { lua_pushlightuserdata(state, &vm); @@ -169,6 +170,11 @@ void install_module_require(lua_State* state, Vm& vm, std::string_view currentPa lua_setglobal(state, "require"); } +void install_globals(Vm& vm) { + push_vm_closure(vm.state, vm, print, "print"); + lua_setglobal(vm.state, "print"); +} + bool load_source_module( lua_State* caller, Vm& vm, const std::string& path, bool keepResult, std::string& outError) { if (svc_resource == nullptr) { @@ -281,6 +287,29 @@ int module_require(lua_State* state) { return 1; } +int print(lua_State* state) { + Vm& vm = vm_from_upvalue(state); + if (svc_log == nullptr) { + service_unavailable(state, "LogService"); + } + + std::string value; + int const numArgs = lua_gettop(state); + for (int i = 0; i < numArgs; i += 1) { + if (i != 0) { + value.push_back('\t'); + } + + size_t length; + char const* str = luaL_tolstring(state, i + 1, &length); + value.append(str, length); + lua_pop(state, 1); + } + + svc_log->info(vm.subject, value.data()); + return 0; +} + ModResult runtime_activate(ModContext*, ModContext* subject, ModError* outError) { if (subject == nullptr) { return set_error(outError, MOD_INVALID_ARGUMENT, "Delegated mod context is null"); @@ -298,6 +327,7 @@ ModResult runtime_activate(ModContext*, ModContext* subject, ModError* outError) lua_pushlightuserdata(vm->state, vm.get()); lua_rawsetp(vm->state, LUA_REGISTRYINDEX, &vm_registry_index); lua_callbacks(vm->state)->userdata = vm.get(); +#if NDEBUG // Annoying for debuggers lua_callbacks(vm->state)->interrupt = [](lua_State* state, int gc) { auto* current = static_cast(lua_callbacks(state)->userdata); if (gc < 0 && current != nullptr && current->deadlineActive && @@ -306,7 +336,9 @@ ModResult runtime_activate(ModContext*, ModContext* subject, ModError* outError) luaL_error(state, "script execution exceeded its time budget"); } }; +#endif luaL_openlibs(vm->state); + install_globals(*vm); luaL_sandbox(vm->state); std::string error; diff --git a/res/rml/command_console.rcss b/res/rml/command_console.rcss index b5378b5d7a..d19246692a 100644 --- a/res/rml/command_console.rcss +++ b/res/rml/command_console.rcss @@ -1,8 +1,6 @@ -*, *:before, *:after { - box-sizing: border-box; -} - body { + --console-command-color: #FFD966; + display: block; width: 100%; height: 100%; @@ -19,11 +17,11 @@ console { width: 50%; display: flex; flex-direction: column; - background-color: rgba(0, 0, 0, 60%); + background-color: rgba(var(--color-black-rgb), 60%); pointer-events: auto; - font-family: "Noto Mono"; - font-size: 14dp; - color: #FFFFFF; + font-family: var(--font-family-monospace); + font-size: var(--font-size-sm); + color: var(--color-white); transition: background-color 0.8s linear-in-out; } @@ -31,7 +29,7 @@ output { display: block; overflow: hidden; max-height: 480dp; - padding: 4dp 8dp; + padding: var(--space-xs) var(--space-sm); line-height: 1.4em; } @@ -46,11 +44,11 @@ console:not([open]) { } console:not([open])[fading] { - background-color: rgba(0, 0, 0, 0%); + background-color: rgba(var(--color-black-rgb), 0%); } console[open] { - background-color: rgba(0, 0, 0, 60%); + background-color: rgba(var(--color-black-rgb), 60%); transition: none; } @@ -75,19 +73,19 @@ output[open] line { } line.cmd { - color: #FFD966; + color: var(--console-command-color); } console input { display: none; width: 100%; - background-color: rgba(0, 0, 0, 40%); + background-color: rgba(var(--color-black-rgb), 40%); border: 0dp; - border-top: 1dp rgba(255, 255, 255, 20%); - color: #FFFFFF; - font-family: "Noto Mono"; - font-size: 14dp; - padding: 4dp 8dp; + border-top: 1dp rgba(var(--color-white-rgb), 20%); + color: var(--color-white); + font-family: var(--font-family-monospace); + font-size: var(--font-size-sm); + padding: var(--space-xs) var(--space-sm); } console[open] input { diff --git a/res/rml/logs.rcss b/res/rml/logs.rcss index 126684f9d2..fd1933ed3a 100644 --- a/res/rml/logs.rcss +++ b/res/rml/logs.rcss @@ -2,19 +2,20 @@ window.logs content { flex-flow: column; } -window.logs .log-toolbar { +window.logs log-toolbar { display: flex; flex-flow: row; - flex: 0 0 64dp; - height: 64dp; + flex: 0 0 var(--toolbar-height); + height: var(--toolbar-height); align-items: center; - gap: 8dp; + gap: var(--space-sm); padding-right: 72dp; - background-color: rgba(217, 217, 217, 10%); - border-bottom: 2dp #92875B; - font-family: "Fira Sans Condensed"; + background-color: rgba(var(--color-neutral-rgb), 10%); + border-bottom-width: 2dp; + border-bottom-color: var(--color-border); + font-family: var(--font-family-heading); font-weight: bold; - font-size: 18dp; + font-size: var(--font-size-xl); } window.logs > close { @@ -22,76 +23,77 @@ window.logs > close { right: 8dp; } -window.logs .log-title { +window.logs log-title { align-self: stretch; flex: 0 0 auto; - padding: 0 24dp; - line-height: 64dp; + padding: 0 var(--space-xl); + line-height: var(--toolbar-height); text-transform: uppercase; - border-bottom: 4dp #C2A42D; + border-bottom-width: 4dp; + border-bottom-color: var(--color-accent); font-effect: glow(0dp 4dp 0dp 4dp black); } -window.logs .log-title-mod { +window.logs log-title-mod { flex: 0 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; - font-family: "Fira Sans"; + font-family: var(--font-family-body); font-weight: normal; - font-size: 15dp; - color: rgba(224, 219, 200, 55%); + font-size: var(--font-size-base); + color: rgba(var(--color-text-rgb), 55%); } -.log-toolbar-spacer { +log-toolbar-spacer { flex: 1 1 0; } -.log-toolbar button { +log-toolbar button { flex: 0 0 auto; - font-family: "Fira Sans"; + font-family: var(--font-family-body); font-weight: normal; - font-size: 15dp; - padding: 5dp 12dp; + font-size: var(--font-size-base); + padding: 5dp var(--space-md); } window.logs content pane.log-view { flex: 1 1 0; - padding: 12dp 16dp; + padding: var(--space-md) var(--space-lg); padding-bottom: 0dp; gap: 0dp; } -.log-lines { +log-lines { display: block; } -.log-line { +log-line { display: block; - font-family: "Noto Mono"; - font-size: 13dp; + font-family: var(--font-family-monospace); + font-size: var(--font-size-xs); line-height: 1.5; word-break: break-word; white-space: pre-wrap; } -.log-line .log-time { - color: rgba(224, 219, 200, 45%); +log-line log-time { + color: rgba(var(--color-text-rgb), 45%); } -.log-line .log-mod { - color: rgba(194, 164, 45, 80%); +log-line log-mod { + color: rgba(var(--color-accent-rgb), 80%); } -.log-line.lvl-trace, -.log-line.lvl-debug { +log-line.lvl-trace, +log-line.lvl-debug { opacity: 0.55; } -.log-line.lvl-warn .log-msg { - color: #ffa826; +log-line.lvl-warn log-msg { + color: var(--color-warning); } -.log-line.lvl-error .log-msg { - color: #cc4444; +log-line.lvl-error log-msg { + color: var(--color-error); } diff --git a/res/rml/mod_browser.rcss b/res/rml/mod_browser.rcss new file mode 100644 index 0000000000..e88285b019 --- /dev/null +++ b/res/rml/mod_browser.rcss @@ -0,0 +1,922 @@ +window.mod-browser, +window.mod-browser-detail, +window.screenshot-viewer { + background-color: rgba(var(--color-surface-rgb), 96%); +} + +window.mod-browser > content { + flex-flow: row; +} + +catalog-filters { + display: flex; + flex-flow: column; + flex: 0 0 264dp; + min-width: 0; + padding: var(--space-xl) 18dp; + gap: var(--space-sm); + border-right-width: 1dp; + border-right-color: var(--color-border); + background-color: rgba(var(--color-control-rgb), 45%); +} + +catalog-filters h1, +catalog-results header h1 { + margin: 0; + font-family: var(--font-family-heading); + font-size: var(--font-size-5xl); + font-weight: bold; +} + +catalog-filters h1 { + padding: 0 var(--space-sm) var(--space-sm) var(--space-sm); +} + +catalog-filters h2 { + margin: var(--space-md) var(--space-sm) 0 var(--space-sm); + font-family: var(--font-family-heading); + font-size: var(--font-size-xs); + font-weight: bold; + text-transform: uppercase; + opacity: 0.42; +} + +catalog-filters select-button { + padding: var(--space-sm) 10dp; + border-radius: var(--radius-panel); +} + +catalog-filters select-button key { + font-size: var(--font-size-xs); +} + +catalog-filters select-button value, +catalog-filters select-button input { + font-size: var(--font-size-sm); +} + +.catalog-library-link { + padding: var(--space-sm) 10dp; + border-radius: var(--radius-panel); + text-align: left; + font-size: var(--font-size-sm); +} + +catalog-results { + display: flex; + flex-flow: column; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + padding: var(--space-xl); + gap: var(--space-lg); +} + +catalog-results header { + display: block; + flex: 0 0 auto; + padding-right: var(--space-2xl); +} + +catalog-results > header small { + display: block; + margin-top: var(--space-2xs); + font-size: var(--font-size-xs); + opacity: 0.5; +} + +catalog-viewport { + display: block; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + overflow: hidden auto; +} + +catalog-grid { + display: flex; + flex-flow: row wrap; + min-width: 0; + gap: 14dp; + padding: var(--space-2xs); +} + +.catalog-card { + display: flex; + flex-flow: column; + flex: 0 0 48%; + min-width: 260dp; + height: 310dp; + padding: 0; + overflow: hidden; + text-align: left; + border-radius: 12dp; +} + +catalog-card-art { + display: block; + position: relative; + flex: 0 0 118dp; + min-height: 118dp; +} + +catalog-card-art-image { + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; + mask-image: linear-gradient(180deg, #fff 45%, transparent); +} + +catalog-card-body { + display: flex; + position: relative; + flex-flow: column; + flex: 1 1 auto; + min-height: 0; + padding: var(--space-xl) var(--space-lg) var(--space-lg); + gap: 7dp; +} + +catalog-official-badge { + padding: var(--space-2xs) 5dp; + border-radius: var(--radius-small); + background-color: rgba(var(--color-accent-rgb), 28%); + color: var(--color-info); + font-style: normal; + font-size: var(--font-size-3xs); +} + +catalog-card-body > section > b { + display: block; + font-family: var(--font-family-heading); + font-size: var(--font-size-sm); + font-weight: bold; + text-transform: uppercase; + color: var(--color-accent); +} + +catalog-card-body > small { + position: absolute; + top: var(--space-md); + right: var(--space-lg); + margin: 0; + padding: 0; + font-size: var(--font-size-sm); + font-weight: normal; + text-transform: none; + opacity: 0.45; +} + +.catalog-card mod-icon { + display: block; + position: absolute; + left: 14dp; + bottom: -18dp; + z-index: 1; + width: 56dp; + height: 56dp; + border-radius: var(--radius-panel); + overflow: hidden; + box-shadow: rgba(var(--color-black-rgb), 60%) 0 6dp 16dp; +} + +mod-icon-image { + display: block; + width: 100%; + height: 100%; + border-radius: var(--radius-panel); +} + +catalog-card-body > section { + display: block; + margin: 0; + padding: 0; +} + +catalog-card-body > section h2 { + display: block; + margin: 0; + font-family: var(--font-family-body); + font-size: var(--font-size-3xl); + font-weight: bold; + color: var(--color-neutral); + line-height: 1.5; +} + +catalog-card-body > section small { + display: block; + font-size: var(--font-size-3xs); + opacity: 0.55; +} + +catalog-card-body > p { + flex: 1 1 auto; + min-height: 0; + margin: 0; + overflow: hidden; + font-size: var(--font-size-base); + line-height: 1.35; + color: rgba(var(--color-text-rgb), 68%); +} + +catalog-card-body > footer { + display: flex; + align-items: center; + gap: 10dp; + font-size: var(--font-size-3xs); + opacity: 0.52; +} + +catalog-card-body > footer stat { + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 3dp; + white-space: nowrap; +} + +catalog-card-body > footer icon { + flex: 0 0 13dp; + font-size: var(--font-size-sm); + line-height: 1; +} + +catalog-card-body > footer .size { + margin-left: auto; + font-size: var(--font-size-sm); +} + +catalog-card-body > footer .size.installed { + color: var(--color-success); +} + +catalog-pagination { + display: flex; + flex-flow: row; + align-items: center; + flex: 0 0 auto; + gap: var(--space-sm); +} + +catalog-pagination-label { + flex: 1 1 auto; + text-align: center; + font-size: var(--font-size-xs); + opacity: 0.5; +} + +catalog-pagination button { + font-size: var(--font-size-sm); + padding: 6dp var(--space-md); +} + +catalog-results-status, +catalog-detail-status { + display: flex; + flex-flow: column; + align-items: center; + justify-content: center; + height: 100%; + gap: var(--space-sm); + text-align: center; +} + +catalog-results-status h2, +catalog-detail-status h2 { + display: block; + margin: 0; + font-family: var(--font-family-heading); + font-size: var(--font-size-4xl); + font-weight: bold; +} + +catalog-results-status p, +catalog-detail-status p { + display: block; + margin: 0; + opacity: 0.58; +} + +catalog-results-status button, +catalog-detail-status button { + margin-top: var(--space-sm); + font-size: var(--font-size-base); +} + +window.mod-browser-detail > content { + display: block; +} + +detail-scroll { + display: flex; + flex-flow: column; + width: 100%; + height: 100%; + min-width: 0; + overflow: hidden auto; + padding-bottom: var(--space-2xl); +} + +catalog-detail-hero { + display: flex; + position: relative; + flex-flow: column; + justify-content: space-between; + flex: 0 0 220dp; + min-height: 220dp; + padding: 18dp var(--space-xl) 20dp var(--space-xl); +} + +catalog-detail-hero-image { + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; + mask-image: linear-gradient(180deg, #fff 40%, transparent); +} + +catalog-detail-actions { + display: flex; + position: relative; + z-index: 1; + flex-flow: row; + gap: var(--space-sm); +} + +catalog-detail-actions button, +catalog-source-actions button { + font-size: var(--font-size-sm); + padding: 7dp var(--space-md); + --button-background: rgba(var(--color-control-rgb), 75%); + --button-background-hover: rgba(var(--color-control-rgb), 90%); + --button-background-selected: rgba(var(--color-control-rgb), 90%); + --button-background-active: rgba(var(--color-surface-rgb), 90%); +} + +.catalog-icon-action { + display: flex; + align-items: center; + gap: var(--space-sm); +} + +.catalog-icon-action icon { + flex: 0 0 18dp; + font-size: var(--font-size-xl); + line-height: 1; +} + +.catalog-install-action { + display: flex; + position: relative; + align-items: center; + justify-content: center; + gap: 10dp; + min-width: 132dp; + padding: 8dp 22dp; + border-radius: var(--radius-window); + overflow: hidden; + font-size: var(--font-size-2xl); + opacity: 1; + --button-background: rgba(var(--color-control-rgb), 20%); + --button-background-hover: rgba(var(--color-interactive-rgb), 20%); + --button-background-selected: rgba(var(--color-interactive-rgb), 20%); + --button-background-active: rgba(var(--color-interactive-rgb), 20%); + box-shadow: var(--color-accent) 0 0 0 2dp; +} + +.catalog-install-action icon { + flex: 0 0 22dp; + font-size: var(--font-size-3xl); + line-height: 1; +} + +.catalog-install-action > span { + white-space: nowrap; +} + +.catalog-install-action.idle { + --button-background: rgba(var(--color-interactive-rgb), 18%); + --button-background-hover: rgba(var(--color-interactive-rgb), 42%); + --button-background-selected: rgba(var(--color-interactive-rgb), 42%); + --button-background-active: rgba(var(--color-interactive-rgb), 65%); + box-shadow: rgba(var(--color-accent-rgb), 65%) 0 0 0 1dp; +} + +.catalog-install-action.paused { + box-shadow: rgba(var(--color-border-rgb), 25%) 0 0 0 1dp; +} + +.catalog-install-action.paused:not(:disabled):hover, +.catalog-install-action.paused:not(:disabled):focus-visible { + box-shadow: var(--color-accent) 0 0 0 2dp; +} + +.catalog-install-action progress { + position: absolute; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 4dp; + margin: 0; + border-radius: 0; + overflow: hidden; + background-color: rgba(var(--color-white-rgb), 10%); +} + +.catalog-install-action progress fill { + border-radius: 0; + background-color: rgba(var(--color-accent-rgb), 80%); +} + +.catalog-install-action.paused progress fill { + background-color: rgba(var(--color-text-rgb), 35%); +} + +.catalog-install-action.retrying { + color: var(--color-warning); + box-shadow: rgba(var(--color-warning-rgb), 60%) 0 0 0 2dp; +} + +.catalog-install-action.retrying progress fill { + background-color: rgba(var(--color-warning-rgb), 60%); +} + +.catalog-install-action.failed { + color: var(--color-white); + --button-background: rgba(var(--color-error-rgb), 20%); + --button-background-hover: rgba(var(--color-error-rgb), 35%); + --button-background-selected: rgba(var(--color-error-rgb), 35%); + --button-background-active: rgba(var(--color-error-rgb), 35%); + box-shadow: var(--color-error) 0 0 0 2dp; +} + +.catalog-install-action.failed progress fill { + background-color: rgba(var(--color-error-rgb), 70%); +} + +.catalog-install-action.installed { + color: var(--color-success); + box-shadow: rgba(var(--color-success-rgb), 50%) 0 0 0 2dp; +} + +.catalog-install-action.installing progress fill { + background-color: rgba(var(--color-info-rgb), 80%); +} + +catalog-install-control { + display: flex; + flex-flow: column; + align-items: flex-end; + flex: 0 0 auto; + gap: var(--space-xs); +} + +catalog-install-caption { + display: block; + max-width: 240dp; + overflow: hidden; + font-size: var(--font-size-xs); + color: rgba(var(--color-text-rgb), 50%); + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; +} + +catalog-install-caption.failed { + color: var(--color-error); +} + +catalog-detail-identity { + display: flex; + position: relative; + z-index: 1; + align-items: flex-end; + gap: var(--space-lg); +} + +catalog-detail-identity mod-icon { + display: block; + flex: 0 0 70dp; + width: 70dp; + height: 70dp; + border-radius: var(--radius-panel); + overflow: hidden; + box-shadow: rgba(var(--color-black-rgb), 65%) 0 8dp 20dp; +} + +catalog-detail-identity > header { + display: block; + flex: 1 1 auto; + min-width: 0; +} + +catalog-detail-identity > header > b { + display: block; + font-family: var(--font-family-heading); + font-size: var(--font-size-sm); + font-weight: bold; + text-transform: uppercase; + color: var(--color-accent); +} + +catalog-detail-identity h1 { + display: block; + margin: 1dp 0; + font-family: var(--font-family-body); + font-size: var(--font-size-5xl); + font-weight: bold; + color: var(--color-neutral); +} + +catalog-detail-identity h1 small { + margin-left: 9dp; + font-family: var(--font-family-body); + font-size: var(--font-size-xs); + font-weight: normal; + opacity: 0.55; +} + +catalog-detail-identity p { + display: block; + margin: 0; + font-size: var(--font-size-xs); + opacity: 0.72; +} + +catalog-detail-stats { + display: flex; + flex-flow: row; + align-items: center; + gap: 28dp; + margin: 0 var(--space-xl); + font-size: var(--font-size-2xs); + color: rgba(var(--color-text-rgb), 58%); +} + +catalog-detail-stats > stat { + display: flex; + align-items: center; + gap: 5dp; +} + +catalog-detail-stats icon { + flex: 0 0 17dp; + font-size: var(--font-size-lg); + line-height: 1; +} + +catalog-detail-body { + display: flex; + flex-flow: row; + align-items: flex-start; + gap: 28dp; + padding: var(--space-xl); +} + +catalog-detail-body main { + display: flex; + flex-flow: column; + flex: 1 1 auto; + min-width: 0; + gap: 28dp; +} + +catalog-detail-body section { + display: block; +} + +catalog-detail-body section.catalog-scroll-anchor { + focus: auto; +} + +catalog-detail-body section.catalog-scroll-anchor:focus-visible { + border-radius: var(--radius-control); + box-shadow: rgba(var(--color-accent-rgb), 45%) 0 0 0 1dp; +} + +catalog-detail-body h2, +catalog-detail-body h3 { + display: block; + margin: 0 0 10dp 0; + font-family: var(--font-family-heading); + font-size: var(--font-size-2xl); + font-weight: bold; +} + +catalog-detail-body h2 small { + margin-left: 7dp; + font-family: var(--font-family-body); + font-size: var(--font-size-3xs); + font-weight: normal; + opacity: 0.5; +} + +catalog-fragment { + display: block; + font-size: var(--font-size-md); + line-height: 1.55; + color: var(--color-neutral); +} + +catalog-fragment p, +catalog-fragment ul, +catalog-fragment ol { + display: block; + margin: 0 0 10dp 0; +} + +catalog-fragment ul, +catalog-fragment ol { + padding-left: var(--space-xl); +} + +catalog-fragment li { + display: block; + position: relative; + margin: 3dp 0; +} + +catalog-list-marker { + display: block; + position: absolute; + right: 100%; + width: var(--space-xl); + padding-right: var(--space-sm); + text-align: right; +} + +catalog-fragment h1, +catalog-fragment h2, +catalog-fragment h3, +catalog-fragment h4, +catalog-fragment h5, +catalog-fragment h6 { + display: block; + margin: 15dp 0 5dp 0; + font-family: var(--font-family-heading); + font-weight: bold; + font-size: var(--font-size-xl); + color: var(--color-neutral); +} + +catalog-fragment h1 { + font-size: var(--font-size-5xl); +} + +catalog-fragment h2 { + font-size: var(--font-size-4xl); +} + +catalog-fragment h3 { + font-size: var(--font-size-2xl); +} + +window.screenshot-viewer > close { + display: none; +} + +catalog-gallery { + display: flex; + flex-flow: row; + height: 210dp; + gap: var(--space-sm); +} + +.catalog-screenshot { + position: relative; + overflow: hidden; + flex: 1 1 0; + height: 100%; + min-width: 0; + padding: 0; + border-radius: var(--radius-panel); + --button-background: rgba(var(--color-border-rgb), 14%); + font-size: var(--font-size-5xl); +} + +.catalog-screenshot.primary { + flex: 2 1 0; +} + +catalog-screenshot-image, +catalog-screenshot-more { + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; +} + +catalog-screenshot-more { + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(var(--color-black-rgb), 60%); + color: var(--color-white); +} + +catalog-dependencies { + display: flex; + flex-flow: column; + gap: 7dp; + font-size: var(--font-size-xs); +} + +catalog-dependency { + display: flex; + flex-flow: column; + padding: 9dp 11dp; + border-radius: var(--radius-control); + background-color: rgba(var(--color-border-rgb), 9%); +} + +catalog-dependency-status { + display: block; + font-size: var(--font-size-3xs); + opacity: 0.56; +} + +catalog-dependency.missing { + color: var(--color-warning); +} + +catalog-detail-body aside { + display: flex; + flex-flow: column; + flex: 0 0 264dp; + min-width: 0; + padding: var(--space-lg); + gap: var(--space-md); + border-radius: 9dp; + background-color: rgba(var(--color-control-rgb), 42%); + box-shadow: rgba(var(--color-border-rgb), 24%) 0 0 0 1dp; +} + +catalog-detail-body dl { + display: flex; + flex-flow: row wrap; + margin: 0; + font-size: var(--font-size-sm); +} + +catalog-detail-body dt { + flex: 0 0 42%; + padding: 5dp 0; + font-weight: bold; + opacity: 0.8; +} + +catalog-detail-body dd { + flex: 1 1 52%; + margin: 0; + padding: 5dp 0; + text-align: right; +} + +window.screenshot-viewer > content { + flex-flow: column; + padding: 18dp; + gap: var(--space-md); +} + +catalog-screenshot-full { + display: block; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + background-color: rgba(var(--color-black-rgb), 45%); +} + +catalog-screenshot-actions { + display: flex; + flex-flow: row; + justify-content: center; + flex: 0 0 auto; + gap: var(--space-sm); +} + +catalog-screenshot-actions button { + font-size: var(--font-size-sm); + padding: 7dp 13dp; +} + +@media (max-height: 640dp) { + catalog-filters { + flex-basis: 232dp; + padding: 15dp var(--space-md); + gap: 5dp; + } + + catalog-filters h1, + catalog-results header h1 { + font-size: var(--font-size-4xl); + } + + catalog-filters h2 { + margin-top: 6dp; + } + + catalog-results { + padding: 15dp; + gap: 10dp; + } + + .catalog-card { + min-width: 220dp; + height: 254dp; + } + + catalog-card-art { + flex-basis: 78dp; + min-height: 78dp; + } + + catalog-card-body { + padding: 18dp 10dp var(--space-sm) 10dp; + } + + .catalog-card mod-icon { + left: 10dp; + bottom: -14dp; + width: 40dp; + height: 40dp; + } + + catalog-card-body > section > b, + catalog-card-body > small { + font-size: var(--font-size-3xs); + } + + catalog-card-body > section h2 { + font-size: var(--font-size-md); + } + + catalog-card-body > p { + font-size: var(--font-size-3xs); + } + + catalog-detail-hero { + flex-basis: 160dp; + min-height: 160dp; + padding: var(--space-md) 18dp; + } + + catalog-detail-identity mod-icon { + flex-basis: 56dp; + width: 56dp; + height: 56dp; + } + + catalog-detail-identity { + gap: var(--space-md); + } + + catalog-detail-identity > header > b { + font-size: var(--font-size-2xs); + } + + .catalog-install-action { + gap: var(--space-sm); + min-width: 112dp; + padding: 7dp 18dp; + border-radius: 12dp; + font-size: var(--font-size-md); + } + + .catalog-install-action icon { + flex-basis: 18dp; + font-size: var(--font-size-xl); + } + + catalog-install-control { + gap: 3dp; + } + + catalog-install-caption { + max-width: 190dp; + font-size: var(--font-size-3xs); + } + + catalog-detail-identity h1 { + font-size: var(--font-size-3xl); + } + + catalog-detail-body { + padding: 18dp; + gap: 18dp; + } + + catalog-gallery { + height: 160dp; + } +} diff --git a/res/rml/mods.rcss b/res/rml/mods.rcss index 34e86f485b..acd6d923a3 100644 --- a/res/rml/mods.rcss +++ b/res/rml/mods.rcss @@ -1,8 +1,8 @@ window.mods content pane.mod-list { flex: 0 0 360dp; - padding: 16dp; + padding: var(--space-lg); padding-bottom: 0dp; - gap: 4dp; + gap: var(--space-xs); } @media (max-height: 640dp) { @@ -12,29 +12,66 @@ window.mods content pane.mod-list { } window.mods content pane.mod-detail { - gap: 12dp; + gap: var(--space-md); } -.mod-info-row { +mod-entry.browser, +mod-entry.installs { + min-height: 76dp; +} + +mod-entry.browser mod-icon { + color: var(--color-accent); + decorator: text("" center center); +} + +mod-entry.installs mod-icon { + color: var(--color-accent); + decorator: text("" center center); +} + +mod-entry.installs { + background-color: rgba(var(--color-control-rgb), 20%); + box-shadow: rgba(var(--color-border-rgb), 25%) 0 0 0 1dp; +} + +mod-entry.installs:hover, +mod-entry.installs:focus-visible { + background-color: rgba(var(--color-interactive-rgb), 12%); + box-shadow: var(--color-accent) 0 0 0 2dp; +} + +mod-entry.installs progress { + height: 6dp; + margin: var(--space-xs) 0 0 0; +} + +mod-list-separator { + display: block; + height: 1dp; + margin: var(--space-sm) 10dp; + background-color: rgba(var(--color-border-rgb), 30%); +} + +mod-info-row { display: flex; align-items: center; - gap: 12dp; - padding: 4dp 0; + gap: var(--space-md); + padding: var(--space-xs) 0; } -.mod-info-label { - font-family: "Fira Sans Condensed"; - font-weight: bold; +mod-info-row > b { + font-family: var(--font-family-heading); opacity: 0.55; flex: 0 0 auto; } -.mod-info-value { +mod-info-row > span { flex: 1 1 0; } .mod-path { - font-size: 14dp; + font-size: var(--font-size-sm); word-break: break-all; opacity: 0.7; } @@ -42,106 +79,113 @@ window.mods content pane.mod-detail { mod-entry { display: flex; flex-flow: row; - gap: 12dp; + gap: var(--space-md); padding: 10dp; border-radius: 10dp; - decorator: vertical-gradient(#c2a42d00 #c2a42d00); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 0%) rgba(var(--color-accent-rgb), 0%)); transition: decorator 0.1s linear-in-out; cursor: pointer; focus: auto; } mod-entry.current { - box-shadow: rgba(146, 135, 91, 40%) 0 0 0 1dp; + box-shadow: rgba(var(--color-border-rgb), 40%) 0 0 0 1dp; } mod-entry:hover, mod-entry:focus-visible { - decorator: vertical-gradient(#c2a42d00 #c2a42d26); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 0) rgba(var(--color-accent-rgb), 38)); } mod-entry:selected { - decorator: vertical-gradient(#c2a42d10 #c2a42d40); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 16) rgba(var(--color-accent-rgb), 64)); } -mod-entry .mod-icon { +mod-icon { + display: block; flex: 0 0 auto; width: 56dp; height: 56dp; - border-radius: 8dp; -} - -mod-entry icon.mod-icon { + border-radius: var(--radius-panel); + font-family: var(--font-family-icons); font-size: 36dp; - background-color: rgba(17, 16, 10, 20%); - color: rgba(224, 219, 200, 45%); + background-color: rgba(var(--color-control-rgb), 20%); + color: rgba(var(--color-text-rgb), 45%); decorator: text("" center center); + overflow: hidden; } -mod-entry .mod-entry-info { +mod-icon img { + display: block; + width: 100%; + height: 100%; + border-radius: var(--radius-panel); +} + +mod-info { display: flex; flex-flow: column; flex: 1 1 0; min-width: 0; - gap: 2dp; + gap: var(--space-2xs); } -mod-entry .mod-entry-name { +mod-info header { display: flex; flex-flow: row; align-items: baseline; gap: 6dp; } -mod-entry .mod-entry-name-text { +mod-info header b { flex: 0 1 auto; min-width: 0; - font-weight: bold; white-space: nowrap; overflow: hidden; } -mod-entry .mod-entry-version { +mod-info header small { flex: 0 0 auto; - font-size: 13dp; - color: rgba(224, 219, 200, 50%); + font-size: var(--font-size-xs); + color: rgba(var(--color-text-rgb), 50%); } -mod-entry .mod-entry-status.active { - color: #44cc55; +mod-status.active { + color: var(--color-success); } -mod-entry .mod-entry-status.failed { - color: #cc4444; +mod-status.failed { + color: var(--color-error); } -mod-entry .mod-entry-network { +mod-network { margin-left: 6dp; padding: 1dp 5dp; border-radius: 5dp; - background-color: rgba(67, 151, 219, 20%); - color: #6fb7ef; + background-color: rgba(var(--color-info-rgb), 20%); + color: var(--color-info); } -mod-entry .mod-entry-desc { - font-size: 14dp; +mod-info > p { + margin: 0; + font-size: var(--font-size-sm); line-height: 1.3; - color: rgba(224, 219, 200, 65%); + color: rgba(var(--color-text-rgb), 65%); max-height: 2.6em; overflow: hidden; white-space: pre-wrap; } -mod-entry .mod-entry-sub { - font-size: 13dp; - color: rgba(224, 219, 200, 50%); +mod-info > small { + font-size: var(--font-size-xs); + color: rgba(var(--color-text-rgb), 50%); } -mod-entry.inactive .mod-icon { +mod-entry.inactive mod-icon { filter: grayscale(1); } -mod-entry.inactive .mod-entry-info { +mod-entry.inactive mod-info { opacity: 0.5; } @@ -155,20 +199,37 @@ mod-header.has-banner { margin: -24dp -24dp 0dp -24dp; } -mod-header .mod-actions { +mod-header-image { + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; + mask-image: linear-gradient(180deg, #fff 40%, transparent); + filter: grayscale(0); +} + +mod-header.inactive mod-header-image { + filter: grayscale(1); +} + +mod-actions { position: absolute; top: 24dp; left: 24dp; display: flex; flex-flow: row; - gap: 8dp; + gap: var(--space-sm); } -mod-header .mod-actions button { - font-size: 16dp; - padding: 6dp 14dp; - background-color: rgba(21, 22, 16, 80%); - box-shadow: rgba(146, 135, 91, 60%) 0 0 0 1dp; +mod-actions button { + --button-background: rgba(var(--color-surface-rgb), 80%); + --button-background-hover: rgba(var(--color-control-rgb), 90%); + --button-background-selected: rgba(var(--color-control-rgb), 90%); + --button-background-active: rgba(var(--color-surface-rgb), 90%); + box-shadow: rgba(var(--color-border-rgb), 60%) 0 0 0 1dp; } mod-header.no-banner { @@ -177,56 +238,55 @@ mod-header.no-banner { align-items: center; } -mod-header.no-banner .mod-actions { +mod-header.no-banner mod-actions { position: static; } -window.mods .mod-title { +mod-title { display: block; - font-size: 28dp; + font-size: var(--font-size-5xl); font-weight: bold; } -window.mods .mod-title .mod-title-version { +mod-title small { font-weight: normal; - font-size: 16dp; - color: rgba(224, 219, 200, 55%); + font-size: var(--font-size-md); + color: rgba(var(--color-text-rgb), 55%); } -window.mods .mod-author { +mod-author { display: block; - font-size: 15dp; - color: rgba(224, 219, 200, 55%); + font-size: var(--font-size-base); + color: rgba(var(--color-text-rgb), 55%); } -window.mods .mod-restart-note { - font-size: 15dp; - color: #ffa826; +mod-restart-note { + font-size: var(--font-size-base); + color: var(--color-warning); opacity: 0.85; } -window.mods .mod-description { +mod-description { line-height: 1.5; } -.status-badge { - font-size: 14dp; +status-badge { + font-size: var(--font-size-sm); opacity: 0.7; } -.status-badge.active, -.mod-info-label.active { - color: #44cc55; +status-badge.active, +mod-info-row > b.active { + color: var(--color-success); opacity: 1; } -.status-badge.failed, -.mod-info-label.failed { - color: #cc4444; +status-badge.failed, +mod-info-row > b.failed { + color: var(--color-error); opacity: 1; } - -.status-badge.network { - color: #6fb7ef; +status-badge.network { + color: var(--color-info); opacity: 1; } diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss index 98f8bfa27a..a200005ca1 100644 --- a/res/rml/overlay.rcss +++ b/res/rml/overlay.rcss @@ -8,10 +8,10 @@ body { height: 100%; margin: 0; padding: 0; - font-family: "Fira Sans"; + font-family: var(--font-family-body); font-weight: normal; - font-size: 20dp; - color: #E0DBC8; + font-size: var(--font-size-2xl); + color: var(--color-text); display: flex; flex-direction: column; justify-content: flex-end; @@ -24,16 +24,22 @@ fps, pipeline-progress, toast { position: absolute; - border: 1dp #92875B; - background-color: rgba(21, 22, 16, 80%); + border-width: 1dp; + background-color: rgba(var(--color-surface-rgb), 80%); +} + +fps, +pipeline-progress { + border-color: var(--color-border); } toast { + border-color: var(--toast-border-color); top: 40dp; right: 40dp; display: flex; flex-flow: column; - border-radius: 14dp; + border-radius: var(--radius-window); overflow: hidden; backdrop-filter: blur(5dp); box-shadow: 0 0 15dp 3dp; @@ -41,8 +47,8 @@ toast { transform: scale(0.9); transform-origin: center; transition: filter transform 0.2s cubic-in-out; - padding: 18dp 24dp; - gap: 8dp; + padding: 18dp var(--space-xl); + gap: var(--space-sm); } toast[open] { @@ -50,15 +56,6 @@ toast[open] { transform: scale(1); } -/*toast:hover { - cursor: pointer; - background-color: rgba(61, 59, 36, 80%); -} - -toast:active { - background-color: rgba(45, 43, 26, 80%); -}*/ - b { font-weight: bold; } @@ -67,14 +64,14 @@ toast heading { display: flex; gap: 18dp; align-items: center; - font-family: "Fira Sans Condensed"; - font-size: 18dp; + font-family: var(--font-family-heading); + font-size: var(--font-size-xl); font-weight: bold; text-transform: uppercase; - color: #92875B; + color: var(--toast-heading-color); } -toast heading > span { +toast heading > toast-title { flex: 1 0 auto; } @@ -82,13 +79,13 @@ toast heading > row { flex: 1 0 auto; display: flex; align-items: center; - gap: 4dp; + gap: var(--space-xs); } toast message { display: flex; flex-flow: column; - gap: 8dp; + gap: var(--space-sm); } toast message row { @@ -99,6 +96,50 @@ toast message row.muted { opacity: 0.5; } +toast.mod-installed row { + align-items: center; + gap: var(--space-md); +} + +mod-icon { + flex: 0 0 42dp; + width: 42dp; + height: 42dp; + overflow: hidden; + border-radius: var(--radius-panel); + background-color: rgba(var(--color-control-rgb), 45%); + color: rgba(var(--color-text-rgb), 45%); + font-family: var(--font-family-icons); + font-size: var(--font-size-4xl); + decorator: text("" center center); +} + +mod-icon img { + width: 100%; + height: 100%; + border-radius: var(--radius-panel); +} + +mod-info { + display: flex; + flex-flow: column; + min-width: 0; + gap: var(--space-xs); +} + +mod-name { + color: var(--color-white); +} + +toast.mod-installed small { + font-size: var(--font-size-sm); + color: rgba(var(--color-text-rgb), 55%); +} + +toast.mod-installed small.version { + margin-left: var(--space-sm); +} + progress { height: 4dp; position: absolute; @@ -108,7 +149,7 @@ progress { } progress fill { - background-color: rgba(194, 164, 45, 80%); + background-color: rgba(var(--color-accent-rgb), 80%); } pipeline-progress { @@ -119,8 +160,8 @@ pipeline-progress { z-index: 100; min-width: 260dp; max-width: 90%; - padding: 10dp 16dp 12dp; - border-radius: 7dp; + padding: 10dp var(--space-lg) var(--space-md); + border-radius: var(--radius-control); overflow: hidden; filter: opacity(0); transition: filter 0.2s linear-in-out; @@ -134,8 +175,8 @@ pipeline-progress[open] { pipeline-status { display: flex; align-items: center; - gap: 8dp; - font-size: 18dp; + gap: var(--space-sm); + font-size: var(--font-size-xl); font-weight: normal; white-space: nowrap; } @@ -145,26 +186,16 @@ icon.pipeline-spinner { height: 1.2em; line-height: 1.2em; font-size: 1.2em; - color: #C2A42D; + color: var(--color-accent); text-align: center; transform-origin: center; animation: 1s linear infinite pipeline-spinner-spin; } -toast.achievement { - border: 1dp #C2A42D; -} - -toast.achievement heading { - color: #C2A42D; -} - +toast.achievement, toast.warning { - border: 1dp #C2A42D; -} - -toast.warning heading { - color: #C2A42D; + --toast-border-color: var(--color-accent); + --toast-heading-color: var(--color-accent); } toast.controller-warning { @@ -181,8 +212,8 @@ toast.controller-warning[open] { transform: translateX(-50%) scale(1); } -toast.controller-warning heading { - color: #C2A42D; +toast.controller-warning { + --toast-heading-color: var(--color-accent); } toast.menu-notification { @@ -209,7 +240,7 @@ toast.menu-notification message row { } icon { - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; display: inline-block; vertical-align: middle; @@ -243,13 +274,20 @@ icon.warning { decorator: text("" center center); } +icon.download-done { + width: 1.2em; + height: 1.2em; + font-size: 1.2em; + decorator: text("" center center); +} + fps { display: none; z-index: 99; - font-size: 18dp; + font-size: var(--font-size-xl); font-weight: bold; - padding: 9dp 12dp; - border-radius: 7dp; + padding: 9dp var(--space-md); + border-radius: var(--radius-control); pointer-events: none; white-space: nowrap; } @@ -260,12 +298,12 @@ speedrun-timer { bottom: 0; right: 0; z-index: 99; - background-color: rgba(0, 0, 0, 65%); - padding: 2dp 4dp; + background-color: rgba(var(--color-black-rgb), 65%); + padding: var(--space-2xs) var(--space-xs); pointer-events: none; - font-family: "Noto Mono"; - font-size: 16dp; - color: #ffffff; + font-family: var(--font-family-monospace); + font-size: var(--font-size-md); + color: var(--color-white); white-space: nowrap; } @@ -329,7 +367,7 @@ logo img { left: 0; width: 100%; height: 100%; - filter: drop-shadow(#0008 0 0 14dp); + filter: drop-shadow(rgba(var(--color-black-rgb), 53.333333%) 0 0 14dp); transform-origin: center; } diff --git a/res/rml/popover.rcss b/res/rml/popover.rcss index 1db4f64a05..2084b132f0 100644 --- a/res/rml/popover.rcss +++ b/res/rml/popover.rcss @@ -3,6 +3,8 @@ } body { + --button-background: rgba(var(--color-control-rgb), 35%); + width: 100%; height: 100%; z-index: 10; @@ -12,14 +14,15 @@ popover { position: absolute; display: flex; flex-flow: column; - font-family: "Fira Sans"; - font-size: 14dp; - color: #E0DBC8; - border-radius: 14dp; - border: 2dp #92875B; - background-color: rgba(21, 22, 16, 96%); + font-family: var(--font-family-body); + font-size: var(--font-size-sm); + color: var(--color-text); + border-radius: var(--radius-window); + border-width: 2dp; + border-color: var(--color-border); + background-color: rgba(var(--color-surface-rgb), 96%); backdrop-filter: blur(5dp); - box-shadow: 0 6dp 24dp 2dp rgba(0, 0, 0, 55%); + box-shadow: 0 6dp 24dp 2dp rgba(var(--color-black-rgb), 55%); filter: opacity(0); transform: scale(0.95); transform-origin: center; @@ -43,8 +46,8 @@ color-sv { position: relative; width: 240dp; height: 150dp; - border-radius: 8dp; - box-shadow: rgba(146, 135, 91, 50%) 0 0 0 1dp; + border-radius: var(--radius-panel); + box-shadow: rgba(var(--color-border-rgb), 50%) 0 0 0 1dp; drag: drag; focus: auto; } @@ -55,8 +58,8 @@ color-alpha { position: relative; width: 240dp; height: 14dp; - border-radius: 7dp; - box-shadow: rgba(146, 135, 91, 50%) 0 0 0 1dp; + border-radius: var(--radius-control); + box-shadow: rgba(var(--color-border-rgb), 50%) 0 0 0 1dp; drag: drag; focus: auto; } @@ -64,13 +67,13 @@ color-alpha { color-sv:focus-visible, color-hue:focus-visible, color-alpha:focus-visible { - box-shadow: #C2A42D 0 0 0 2dp; + box-shadow: var(--color-accent) 0 0 0 2dp; } color-sv.adjusting, color-hue.adjusting, color-alpha.adjusting { - box-shadow: #FFFFFF 0 0 0 3dp; + box-shadow: var(--color-white) 0 0 0 3dp; } color-hue { @@ -82,20 +85,21 @@ color-cursor { position: absolute; width: 14dp; height: 14dp; - border-radius: 7dp; - border: 2dp #ffffff; - box-shadow: 0 0 4dp 1dp rgba(0, 0, 0, 70%); + border-radius: var(--radius-control); + border-width: 2dp; + border-color: var(--color-white); + box-shadow: 0 0 4dp 1dp rgba(var(--color-black-rgb), 70%); pointer-events: none; } color-heading { display: block; - margin-top: 2dp; - font-family: "Fira Sans Condensed"; + margin-top: var(--space-2xs); + font-family: var(--font-family-heading); font-weight: bold; - font-size: 13dp; + font-size: var(--font-size-xs); text-transform: uppercase; - color: rgba(224, 219, 200, 55%); + color: rgba(var(--color-text-rgb), 55%); } color-presets { @@ -114,7 +118,7 @@ button.color-swatch-button { width: 25dp; height: 25dp; padding: 0; - border-radius: 7dp; + border-radius: var(--radius-control); } color-chip { @@ -123,62 +127,124 @@ color-chip { width: 20dp; height: 20dp; border-radius: 5dp; - box-shadow: rgba(255, 255, 255, 45%) 0 0 0 1dp; + box-shadow: rgba(var(--color-white-rgb), 45%) 0 0 0 1dp; } button.color-swatch-button color-chip { width: 25dp; height: 25dp; - border-radius: 7dp; + border-radius: var(--radius-control); } color-chip.empty, color-swatch.empty { - background-color: rgba(224, 219, 200, 12%); - decorator: linear-gradient(135deg, rgba(224, 219, 200, 0) 45%, - rgba(194, 164, 45, 70%) 48%, rgba(194, 164, 45, 70%) 52%, - rgba(224, 219, 200, 0) 55%); + background-color: rgba(var(--color-text-rgb), 12%); + decorator: linear-gradient(135deg, rgba(var(--color-text-rgb), 0) 45%, + rgba(var(--color-accent-rgb), 70%) 48%, rgba(var(--color-accent-rgb), 70%) 52%, + rgba(var(--color-text-rgb), 0) 55%); } color-footer { display: flex; align-items: center; gap: 6dp; - padding-top: 2dp; + padding-top: var(--space-2xs); } button { - background-color: rgba(17, 16, 10, 35%); - padding: 4dp 8dp; - border-radius: 8dp; - box-shadow: rgba(146, 135, 91, 30%) 0 0 0 1dp; - color: #E0DBC8; + background-color: var(--button-background); + padding: var(--space-xs) var(--space-sm); + border-radius: var(--radius-panel); + box-shadow: rgba(var(--color-border-rgb), 30%) 0 0 0 1dp; + color: var(--button-color); cursor: pointer; focus: auto; } button:hover, button:focus-visible { - background-color: rgba(204, 184, 119, 20%); - box-shadow: #C2A42D 0 0 0 2dp; + background-color: var(--button-background-hover); + box-shadow: var(--color-accent) 0 0 0 2dp; } button:active { - background-color: rgba(204, 184, 119, 40%); + background-color: var(--button-background-active); } color-value { display: block; flex: 1 1 auto; text-align: right; - font-family: "Noto Mono"; - font-size: 12dp; - color: #FFFFFF; + font-family: var(--font-family-monospace); + font-size: var(--font-size-2xs); + color: var(--color-white); cursor: pointer; focus: auto; } color-value:hover, color-value:focus-visible { - color: #C2A42D; + color: var(--color-accent); +} + +popover.context-menu { + min-width: 200dp; + max-width: 90%; + max-height: 90%; + overflow-y: auto; + padding: 6dp; + gap: 2dp; + transform-origin: left top; +} + +.context-menu button { + display: flex; + align-items: center; + gap: 10dp; + padding: 8dp 12dp; + flex: 0 0 auto; + white-space: nowrap; + --button-background: transparent; + background-color: var(--button-background); + box-shadow: none; + transition: background-color 0.1s linear-in-out; +} + +.context-menu button:not(:disabled):hover, +.context-menu button:not(:disabled):focus-visible { + background-color: var(--button-background-hover); + box-shadow: none; +} + +.context-menu button:not(:disabled):active { + background-color: var(--button-background-active); +} + +.context-menu button:disabled { + background-color: transparent; + box-shadow: none; + opacity: 0.4; + cursor: unavailable; +} + +.context-menu button.destructive { + color: var(--color-error); +} + +.context-menu icon { + display: block; + flex: 0 0 1em; + width: 1em; + height: 1em; + font-family: var(--font-family-icons); + font-weight: normal; + line-height: 1; +} + +menu-separator { + display: block; + flex: 0 0 1dp; + height: 1dp; + margin: 4dp 6dp; + background-color: var(--color-border); } diff --git a/res/rml/popup.rcss b/res/rml/popup.rcss index effc80344d..1ed1652ecd 100644 --- a/res/rml/popup.rcss +++ b/res/rml/popup.rcss @@ -8,10 +8,10 @@ body { height: 100%; margin: 0; padding: 0; - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; - font-size: 18dp; - color: #E0DBC8; + font-size: var(--font-size-xl); + color: var(--color-text); } button { @@ -23,11 +23,12 @@ popup { width: 100%; display: flex; align-items: stretch; - height: 64dp; - background-color: rgba(21, 22, 16, 80%); - border-bottom: 2dp #92875B; + height: var(--toolbar-height); + background-color: rgba(var(--color-surface-rgb), 80%); + border-bottom-width: 2dp; + border-bottom-color: var(--color-border); backdrop-filter: blur(5dp); - transform: translateY(-64dp); + transform: translateY(var(--toolbar-hidden-offset)); transition: transform 0.2s cubic-in-out; } @@ -41,5 +42,5 @@ popup tab-bar { popup tab-bar tab { opacity: 0.35; - color: #E0DBC8; + color: var(--color-text); } diff --git a/res/rml/prelaunch.rcss b/res/rml/prelaunch.rcss index 6cc5da65ae..8d98577ad5 100644 --- a/res/rml/prelaunch.rcss +++ b/res/rml/prelaunch.rcss @@ -1,32 +1,40 @@ -*, *:before, *:after { - box-sizing: border-box; -} - body { + --color-prelaunch-accent: #FEE685; + --color-prelaunch-muted: #A6A09B; + --color-disc-error: #FFC9C9; + --color-disc-mismatch: #FFD6A7; + --color-ready: #D8F999; + --menu-button-decorator: horizontal-gradient(rgba(var(--color-black-rgb), 0%) rgba(var(--color-black-rgb), 0%)); + --menu-button-decorator-hover: horizontal-gradient(#FEE685FF #FEE68500); + width: 100%; height: 100%; - font-family: "Fira Sans"; + font-family: var(--font-family-body); font-weight: normal; - font-size: 20dp; - color: #FFFFFF; + font-size: var(--font-size-2xl); + color: var(--color-white); filter: opacity(0); transition: filter 1s 0.2s linear-in-out; z-index: -1; } -.gradient { +body.mirrored { + --menu-button-decorator-hover: horizontal-gradient(#FEE68500 #FEE685FF); +} + +prelaunch-gradient { position: absolute; width: 100%; height: 100%; /* The color gradient from the Figma bands really badly. A fully black gradient does as well, but not as badly. */ - decorator: horizontal-gradient(#000000FF #00000000); + decorator: horizontal-gradient(rgba(var(--color-black-rgb), 100%) rgba(var(--color-black-rgb), 0%)); } -body.mirrored .gradient { - decorator: horizontal-gradient(#00000000 #000000FF); +body.mirrored prelaunch-gradient { + decorator: horizontal-gradient(rgba(var(--color-black-rgb), 0%) rgba(var(--color-black-rgb), 100%)); } -.background { +prelaunch-background { position: absolute; width: 100%; height: 100%; @@ -39,11 +47,11 @@ body[open] { filter: opacity(1); } -body[open] .background { +body[open] prelaunch-background { opacity: 1; } -body.disc-ready .background { +body.disc-ready prelaunch-background { opacity: 0; } @@ -84,7 +92,7 @@ hero { display: flex; flex-direction: column; align-items: flex-start; - gap: 4dp; + gap: var(--space-xs); } body.mirrored hero { @@ -96,8 +104,8 @@ hero img { } eyebrow { - font-family: "Alegreya SC"; - font-size: 32dp; + font-family: var(--font-family-display); + font-size: var(--font-size-6xl); } @media (min-width: 1216dp) { @@ -107,36 +115,36 @@ eyebrow { } } -eyebrow span { +eyebrow studio-name { font-weight: bold; } -#menu-list { +menu-list { display: flex; flex-direction: column; - gap: 12dp; + gap: var(--space-md); align-items: flex-start; } #menu-list button { width: 428dp; height: 54dp; - padding: 8dp 16dp; - border-radius: 8dp; + padding: var(--space-sm) var(--space-lg); + border-radius: var(--radius-panel); text-align: left; text-transform: uppercase; - font-family: "Fira Sans Condensed"; - font-size: 32dp; + font-family: var(--font-family-heading); + font-size: var(--font-size-6xl); font-weight: normal; cursor: pointer; /* Define a fully transparent gradient as the default state, otherwise a white flash occurs */ - decorator: horizontal-gradient(#00000000 #00000000); + decorator: var(--menu-button-decorator); } #menu-list button:disabled { opacity: 0.75; cursor: default; - decorator: horizontal-gradient(#00000000 #00000000); + decorator: var(--menu-button-decorator); } #menu-list button.anim-done { @@ -157,7 +165,7 @@ eyebrow span { width: 100%; height: 100%; overflow: hidden; - border-radius: 8dp; + border-radius: var(--radius-panel); pointer-events: none; z-index: 0; } @@ -169,7 +177,7 @@ eyebrow span { left: 0; width: 100%; height: 100%; - padding: 8dp 16dp; + padding: var(--space-sm) var(--space-lg); opacity: 0; text-overflow: ellipsis; white-space: nowrap; @@ -189,8 +197,8 @@ eyebrow span { height: 54dp; align-items: center; justify-content: center; - color: #FFFFFF; - font-family: "Material Symbols Rounded"; + color: var(--color-white); + font-family: var(--font-family-icons); font-weight: normal; font-size: 30dp; z-index: 1; @@ -213,8 +221,8 @@ eyebrow span { #menu-list button:hover, #menu-list button:focus-visible { - color: black; - decorator: horizontal-gradient(#FEE685FF #FEE68500); + color: var(--color-black); + decorator: var(--menu-button-decorator-hover); } body.mirrored #menu-list { @@ -225,11 +233,6 @@ body.mirrored #menu-list button { text-align: right; } -body.mirrored #menu-list button:hover, -body.mirrored #menu-list button:focus-visible { - decorator: horizontal-gradient(#FEE68500 #FEE685FF); -} - disc-info { position: absolute; left: 96dp; @@ -237,8 +240,8 @@ disc-info { bottom: 72dp; display: flex; flex-direction: column; - gap: 12dp; - font-size: 24dp; + gap: var(--space-md); + font-size: var(--font-size-4xl); font-effect: glow(0dp 4dp 0dp 4dp black); text-align: left; } @@ -256,9 +259,9 @@ version-info { bottom: 72dp; display: flex; flex-direction: column; - gap: 12dp; + gap: var(--space-md); text-align: right; - font-size: 24dp; + font-size: var(--font-size-4xl); font-effect: glow(0dp 4dp 0dp 4dp black); text-align: right; } @@ -272,40 +275,40 @@ body.mirrored version-info { #disc-status { display: flex; align-items: center; - gap: 8dp; + gap: var(--space-sm); } #disc-status[status=good] { - color: #D8F999; + color: var(--color-ready); } #disc-status[status=bad] { - color: #FFC9C9; + color: var(--color-disc-error); } #disc-status[status=verifying] { - color: #FFFFFF; + color: var(--color-white); } #disc-status[status=mismatch] { - color: #FFD6A7; + color: var(--color-disc-mismatch); } #disc-status[status=unknown] { - color: rgba(224, 219, 200, 65%); + color: rgba(var(--color-text-rgb), 65%); } #disc-status[status=pending] { - color: #FEE685; + color: var(--color-prelaunch-accent); } #disc-status icon { display: none; width: 24dp; height: 24dp; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; - font-size: 24dp; + font-size: var(--font-size-4xl); } #disc-status[status] icon { @@ -337,24 +340,24 @@ body.mirrored version-info { } #disc-version { - font-size: 20dp; + font-size: var(--font-size-2xl); } -.update { +update-status { display: none; - color: #A6A09B; + color: var(--color-prelaunch-muted); align-items: center; justify-content: flex-end; - gap: 8dp; - font-size: 20dp; + gap: var(--space-sm); + font-size: var(--font-size-2xl); } -.update[state=checking], -.update[state=failed] { +update-status[state=checking], +update-status[state=failed] { display: block; } -.update[state=available] { +update-status[state=available] { display: flex; } @@ -364,33 +367,33 @@ body.mirrored version-info { padding: 0dp; border-width: 0dp; background-color: transparent; - color: #D8F999; + color: var(--color-ready); cursor: pointer; text-transform: uppercase; font-weight: bold; - decorator: horizontal-gradient(#00000000 #00000000); + decorator: var(--menu-button-decorator); } -.update[state=available] #update-download { +update-status[state=available] #update-download { display: flex; align-items: center; - gap: 2dp; + gap: var(--space-2xs); } #update-download icon { display: block; width: 18dp; height: 18dp; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; decorator: text("" center center); } -.detail { - color: #A6A09B; +disc-version { + color: var(--color-prelaunch-muted); } -body.mirrored .update { +body.mirrored update-status { justify-content: flex-start; } @@ -436,12 +439,20 @@ body.animate-in .intro-item { /* Mobile layout */ @media (max-height: 640dp) { - .gradient { - decorator: horizontal-gradient(#00000000 #000000FF); + body { + --menu-button-decorator-hover: horizontal-gradient(#FEE68500 #FEE685FF); } - body.mirrored .gradient { - decorator: horizontal-gradient(#000000FF #00000000); + body.mirrored { + --menu-button-decorator-hover: horizontal-gradient(#FEE685FF #FEE68500); + } + + prelaunch-gradient { + decorator: horizontal-gradient(rgba(var(--color-black-rgb), 0%) rgba(var(--color-black-rgb), 100%)); + } + + body.mirrored prelaunch-gradient { + decorator: horizontal-gradient(rgba(var(--color-black-rgb), 100%) rgba(var(--color-black-rgb), 0%)); } menu { @@ -453,7 +464,7 @@ body.animate-in .intro-item { flex-direction: row; align-items: center; justify-content: space-between; - gap: 16dp; + gap: var(--space-lg); } body.mirrored menu { @@ -466,7 +477,7 @@ body.animate-in .intro-item { flex: 1 1 0; min-width: 0; max-width: 48%; - margin-left: 32dp; + margin-left: var(--space-2xl); } body.mirrored hero { @@ -490,11 +501,6 @@ body.animate-in .intro-item { text-align: right; } - #menu-list button:hover, - #menu-list button:focus-visible { - decorator: horizontal-gradient(#FEE68500 #FEE685FF); - } - body.mirrored #menu-list { align-items: flex-start; } @@ -503,11 +509,6 @@ body.animate-in .intro-item { text-align: left; } - body.mirrored #menu-list button:hover, - body.mirrored #menu-list button:focus-visible { - decorator: horizontal-gradient(#FEE685FF #FEE68500); - } - eyebrow { display: none; } @@ -518,8 +519,8 @@ body.animate-in .intro-item { bottom: 32dp; top: auto; text-align: right; - font-size: 16dp; - gap: 8dp; + font-size: var(--font-size-md); + gap: var(--space-sm); } #disc-status { @@ -527,11 +528,11 @@ body.animate-in .intro-item { } #disc-status icon { - font-size: 20dp; + font-size: var(--font-size-2xl); } #disc-version { - font-size: 16dp; + font-size: var(--font-size-md); } version-info { @@ -540,12 +541,12 @@ body.animate-in .intro-item { bottom: auto; top: 32dp; text-align: right; - font-size: 16dp; - gap: 8dp; + font-size: var(--font-size-md); + gap: var(--space-sm); } - .update { - font-size: 16dp; + update-status { + font-size: var(--font-size-md); } body.mirrored disc-info { diff --git a/res/rml/tabbing.rcss b/res/rml/tabbing.rcss index 8f42dd84d5..862d79a5df 100644 --- a/res/rml/tabbing.rcss +++ b/res/rml/tabbing.rcss @@ -18,17 +18,18 @@ tab-bar scrollbarhorizontal sliderbar { tab-bar tab { flex: 0 0 auto; - padding: 0 24dp; - line-height: 64dp; + padding: 0 var(--space-xl); + line-height: var(--toolbar-height); white-space: nowrap; - decorator: vertical-gradient(#c2a42d00 #c2a42d00); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 0%) rgba(var(--color-accent-rgb), 0%)); transition: decorator 0.1s linear-in-out, opacity 0.1s linear-in-out; cursor: pointer; } tab-bar tab:selected { opacity: 1; - border-bottom: 4dp #C2A42D; + border-bottom-width: 4dp; + border-bottom-color: var(--color-accent); font-effect: glow(0dp 4dp 0dp 4dp black); } @@ -36,17 +37,17 @@ tab-bar tab:focus-visible, tab-bar tab:hover { opacity: 1; font-effect: glow(0dp 4dp 0dp 4dp black); - decorator: vertical-gradient(#c2a42d00 #c2a42d26); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 0) rgba(var(--color-accent-rgb), 38)); } tab-bar tab:active { - decorator: vertical-gradient(#c2a42d10 #c2a42d40); + decorator: vertical-gradient(rgba(var(--color-accent-rgb), 16) rgba(var(--color-accent-rgb), 64)); } tab-bar[closable] tab-end-spacer { display: block; - flex: 0 0 64dp; - width: 64dp; + flex: 0 0 var(--toolbar-height); + width: var(--toolbar-height); pointer-events: none; } @@ -56,13 +57,13 @@ window > close { position: fixed; top: 8dp; right: 8dp; - z-index: 1; + z-index: 2; width: 48dp; height: 48dp; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; - font-size: 24dp; - color: rgba(224, 219, 200, 70%); + font-size: var(--font-size-4xl); + color: rgba(var(--color-text-rgb), 70%); backdrop-filter: blur(2dp); border-radius: 6dp; decorator: text("" center center); @@ -74,8 +75,8 @@ tab-bar[closable] close:hover, tab-bar[closable] close:focus-visible, window > close:hover, window > close:focus-visible { - color: #fff; - background-color: rgba(194, 164, 45, 24%); + color: var(--color-white); + background-color: rgba(var(--color-accent-rgb), 24%); } window > close { @@ -85,6 +86,6 @@ window > close { tab-bar[closable] close:active, window > close:active { - color: #fff; - background-color: rgba(194, 164, 45, 40%); + color: var(--color-white); + background-color: rgba(var(--color-accent-rgb), 40%); } diff --git a/res/rml/theme.rcss b/res/rml/theme.rcss new file mode 100644 index 0000000000..8d49f32a9c --- /dev/null +++ b/res/rml/theme.rcss @@ -0,0 +1,80 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + --font-family-body: Fira Sans; + --font-family-heading: Fira Sans Condensed; + --font-family-monospace: Noto Mono; + --font-family-icons: Material Symbols Rounded; + --font-family-display: Alegreya SC; + + --font-size-3xs: 11dp; + --font-size-2xs: 12dp; + --font-size-xs: 13dp; + --font-size-sm: 14dp; + --font-size-base: 15dp; + --font-size-md: 16dp; + --font-size-lg: 17dp; + --font-size-xl: 18dp; + --font-size-2xl: 20dp; + --font-size-3xl: 22dp; + --font-size-4xl: 24dp; + --font-size-5xl: 28dp; + --font-size-6xl: 32dp; + + --space-2xs: 2dp; + --space-xs: 4dp; + --space-sm: 8dp; + --space-md: 12dp; + --space-lg: 16dp; + --space-xl: 24dp; + --space-2xl: 32dp; + + --radius-small: 4dp; + --radius-control: 7dp; + --radius-panel: 8dp; + --radius-window: 14dp; + + --toolbar-height: 64dp; + --toolbar-hidden-offset: -64dp; + + --color-text-rgb: 224, 219, 200; + --color-text: rgb(var(--color-text-rgb)); + --color-accent-rgb: 194, 164, 45; + --color-accent: rgb(var(--color-accent-rgb)); + --color-border-rgb: 146, 135, 91; + --color-border: rgb(var(--color-border-rgb)); + --color-surface-rgb: 21, 22, 16; + --color-interactive-rgb: 204, 184, 119; + --color-control-rgb: 17, 16, 10; + --color-neutral-rgb: 217, 217, 217; + --color-neutral: rgb(var(--color-neutral-rgb)); + --color-white-rgb: 255, 255, 255; + --color-white: rgb(var(--color-white-rgb)); + --color-black-rgb: 0, 0, 0; + --color-black: rgb(var(--color-black-rgb)); + + --color-success-rgb: 68, 204, 85; + --color-success: rgb(var(--color-success-rgb)); + --color-info-rgb: 111, 183, 239; + --color-info: rgb(var(--color-info-rgb)); + --color-warning-rgb: 255, 168, 38; + --color-warning: rgb(var(--color-warning-rgb)); + --color-error-rgb: 204, 68, 68; + --color-error: rgb(var(--color-error-rgb)); + + --color-progress-done: #44AA22; + --color-progress-ongoing: #2255BB; + --color-danger-border: #852221; + --color-danger-heading: #B3261E; + + --button-color: var(--color-text); + --button-background: rgba(var(--color-control-rgb), 20%); + --button-background-hover: rgba(var(--color-interactive-rgb), 20%); + --button-background-selected: rgba(var(--color-interactive-rgb), 40%); + --button-background-active: rgba(var(--color-interactive-rgb), 40%); + + --toast-border-color: var(--color-border); + --toast-heading-color: var(--color-border); +} diff --git a/res/rml/touch_controls.rcss b/res/rml/touch_controls.rcss index 4c8057d6c0..85320315bf 100644 --- a/res/rml/touch_controls.rcss +++ b/res/rml/touch_controls.rcss @@ -1,16 +1,28 @@ -*, *:before, *:after { - box-sizing: border-box; -} - body { + --color-oil-border: rgba(42, 32, 18, 82%); + --color-oil-background: rgba(18, 14, 10, 70%); + --color-oil-fill: rgb(255, 232, 74); + --color-button-a: rgba(34, 112, 123, 62%); + --color-button-b: rgba(161, 61, 66, 58%); + --color-button-x: rgba(83, 115, 151, 56%); + --color-button-y: rgba(113, 91, 150, 54%); + --color-stick-background: rgba(18, 20, 24, 35%); + --color-stick-knob: rgba(238, 236, 226, 55%); + + --button-color: rgba(248, 244, 232, 90%); + --button-background: rgba(22, 24, 28, 48%); + --button-background-active: rgba(63, 78, 90, 68%); + --button-border-color: rgba(var(--color-white-rgb), 22%); + --button-border-color-active: rgba(var(--color-white-rgb), 48%); + width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; - color: rgba(248, 244, 232, 90%); + color: var(--button-color); z-index: 1; filter: opacity(0); transition: filter 0.2s linear-in-out; @@ -30,12 +42,11 @@ button { justify-content: center; decorator: none; padding: 0; - border: 1dp rgba(255, 255, 255, 22%); - background-color: rgba(22, 24, 28, 48%); - color: rgba(248, 244, 232, 90%); + border-width: 1dp; + border-color: var(--button-border-color); + background-color: var(--button-background); + color: var(--button-color); text-align: center; - /* backdrop-filter: blur(7dp); */ - /* box-shadow: 0 6dp 18dp rgba(0, 0, 0, 28%); */ transform-origin: center; transition: background-color border-color filter transform 0.08s linear-in-out, opacity 0.2s linear-in-out; @@ -43,8 +54,8 @@ button { button.pressed, button.active { - background-color: rgba(63, 78, 90, 68%); - border-color: rgba(255, 255, 255, 48%); + background-color: var(--button-background-active); + border-color: var(--button-border-color-active); filter: brightness(1.18); } @@ -68,9 +79,9 @@ button icon { button icon glyph { display: block; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; - font-size: 24dp; + font-size: var(--font-size-4xl); line-height: 1; } @@ -92,9 +103,9 @@ button icon glyph { position: absolute; } -.trigger-l.active { - background-color: rgba(57, 116, 133, 74%); - border-color: rgba(128, 222, 234, 72%); +.trigger-l { + --button-background-active: rgba(57, 116, 133, 74%); + --button-border-color-active: rgba(128, 222, 234, 72%); } .trigger, @@ -103,12 +114,14 @@ button icon glyph { } .trigger { - font-size: 22dp; + font-size: var(--font-size-3xl); } .button-z { - background-color: rgba(118, 79, 158, 58%); - border-color: rgba(203, 170, 255, 36%); + --button-background: rgba(118, 79, 158, 58%); + --button-background-active: rgba(139, 91, 187, 82%); + --button-border-color: rgba(203, 170, 255, 36%); + --button-border-color-active: rgba(220, 194, 255, 70%); } .midna-icon { @@ -121,7 +134,7 @@ button icon glyph { .button-z.has-icon span, .face.has-item span { position: absolute; - font-size: 13dp; + font-size: var(--font-size-xs); line-height: 1; } @@ -130,20 +143,14 @@ button icon glyph { bottom: 7dp; } -.button-z.pressed { - background-color: rgba(139, 91, 187, 82%); - border-color: rgba(220, 194, 255, 70%); -} - action-bar { position: absolute; display: flex; align-items: center; - border: 1dp rgba(255, 255, 255, 22%); + border-width: 1dp; + border-color: var(--button-border-color); border-radius: 23dp; - background-color: rgba(22, 24, 28, 48%); - /* backdrop-filter: blur(7dp); */ - /* box-shadow: 0 -6dp 18dp rgba(0, 0, 0, 28%); */ + background-color: var(--button-background); overflow: hidden; opacity: 1; transform-origin: center; @@ -175,7 +182,7 @@ action-bar:hidden separator { } .utility.pressed { - background-color: rgba(63, 78, 90, 68%); + background-color: var(--button-background-active); } .utility.pressed, @@ -185,7 +192,7 @@ action-bar:hidden separator { .skip { z-index: 1; - border-color: rgba(255, 255, 255, 36%); + border-color: rgba(var(--color-white-rgb), 36%); } separator { @@ -193,7 +200,7 @@ separator { flex: 0 0 1dp; width: 1dp; height: 24dp; - background-color: rgba(255, 255, 255, 18%); + background-color: rgba(var(--color-white-rgb), 18%); opacity: 1; transition: opacity 0.2s linear-in-out; } @@ -201,7 +208,7 @@ separator { .face { position: absolute; border-radius: 29dp; - font-size: 24dp; + font-size: var(--font-size-4xl); overflow: visible; } @@ -219,10 +226,10 @@ separator { min-width: 17dp; height: 15dp; padding: 1dp 3dp; - border-radius: 7dp; - background-color: rgba(0, 0, 0, 52%); - color: rgba(255, 255, 255, 92%); - font-size: 12dp; + border-radius: var(--radius-control); + background-color: rgba(var(--color-black-rgb), 52%); + color: rgba(var(--color-white-rgb), 92%); + font-size: var(--font-size-2xs); line-height: 13dp; text-align: center; } @@ -233,11 +240,10 @@ separator { bottom: -5dp; width: 34dp; height: 8dp; - padding: 2dp; - border: 1dp rgba(42, 32, 18, 82%); - border-radius: 4dp; - background-color: rgba(18, 14, 10, 70%); - /* box-shadow: 0 2dp 6dp rgba(0, 0, 0, 35%); */ + padding: var(--space-2xs); + border: 1dp var(--color-oil-border); + border-radius: var(--radius-small); + background-color: var(--color-oil-background); } oil-fill { @@ -245,31 +251,31 @@ oil-fill { width: 0%; height: 100%; border-radius: 2dp; - background-color: rgb(255, 232, 74); + background-color: var(--color-oil-fill); } .face.has-item span { right: 6dp; bottom: 6dp; - color: rgba(255, 255, 255, 88%); + color: rgba(var(--color-white-rgb), 88%); } .face.a { border-radius: 37dp; font-size: 31dp; - background-color: rgba(34, 112, 123, 62%); + background-color: var(--color-button-a); } .face.b { - background-color: rgba(161, 61, 66, 58%); + background-color: var(--color-button-b); } .face.x { - background-color: rgba(83, 115, 151, 56%); + background-color: var(--color-button-x); } .face.y { - background-color: rgba(113, 91, 150, 54%); + background-color: var(--color-button-y); } button.control.docked-top, @@ -306,10 +312,9 @@ touch-stick { width: 124dp; height: 124dp; border-radius: 62dp; - background-color: rgba(18, 20, 24, 35%); - border: 1dp rgba(255, 255, 255, 20%); - /* backdrop-filter: blur(7dp); */ - /* box-shadow: 0 8dp 24dp rgba(0, 0, 0, 24%); */ + background-color: var(--color-stick-background); + border-width: 1dp; + border-color: rgba(var(--color-white-rgb), 20%); opacity: 0; pointer-events: none; transition: opacity 0.18s linear-in-out; @@ -326,7 +331,8 @@ stick-ring { width: 88dp; height: 88dp; border-radius: 44dp; - border: 1dp rgba(255, 255, 255, 18%); + border-width: 1dp; + border-color: rgba(var(--color-white-rgb), 18%); } stick-knob { @@ -334,6 +340,7 @@ stick-knob { width: 48dp; height: 48dp; border-radius: 24dp; - background-color: rgba(238, 236, 226, 55%); - border: 1dp rgba(255, 255, 255, 45%); + background-color: var(--color-stick-knob); + border-width: 1dp; + border-color: rgba(var(--color-white-rgb), 45%); } diff --git a/res/rml/touch_controls_editor.rcss b/res/rml/touch_controls_editor.rcss index 2ca99d935a..dbf0ae74e4 100644 --- a/res/rml/touch_controls_editor.rcss +++ b/res/rml/touch_controls_editor.rcss @@ -1,5 +1,10 @@ body.touch-editor { - background-color: rgba(4, 6, 8, 34%); + --color-editor-backdrop: rgba(4, 6, 8, 34%); + --color-editor-handle: rgba(34, 37, 42, 86%); + --color-editor-accent-rgb: 255, 232, 128; + --color-editor-highlight-rgb: 255, 244, 190; + + background-color: var(--color-editor-backdrop); z-index: 8; } @@ -14,7 +19,7 @@ body.touch-editor .control:hover, body.touch-editor action-bar:hover, body.touch-editor .control.editor-selected, body.touch-editor action-bar.editor-selected { - border-color: rgba(255, 232, 128, 80%); + border-color: rgba(var(--color-editor-accent-rgb), 80%); filter: brightness(1.15); } @@ -27,8 +32,9 @@ selection-frame { display: none; position: absolute; z-index: 20; - border: 2dp rgba(255, 232, 128, 88%); - background-color: rgba(255, 232, 128, 7%); + border-width: 2dp; + border-color: rgba(var(--color-editor-accent-rgb), 88%); + background-color: rgba(var(--color-editor-accent-rgb), 7%); pointer-events: none; } @@ -41,9 +47,10 @@ resize-handle { position: absolute; width: 22dp; height: 22dp; - border: 2dp rgba(255, 244, 190, 96%); + border-width: 2dp; + border-color: rgba(var(--color-editor-highlight-rgb), 96%); border-radius: 11dp; - background-color: rgba(34, 37, 42, 86%); + background-color: var(--color-editor-handle); pointer-events: auto; } @@ -90,6 +97,12 @@ resize-handle.corner.bottom { } editor-toolbar { + --button-background: rgba(17, 19, 24, 88%); + --button-background-hover: rgba(78, 85, 96, 92%); + --button-border-color: rgba(var(--color-white-rgb), 26%); + --button-border-color-hover: rgba(var(--color-editor-highlight-rgb), 92%); + --button-color: rgba(255, 250, 232, 94%); + display: flex; position: absolute; left: 24dp; @@ -98,7 +111,7 @@ editor-toolbar { z-index: 30; height: 48dp; margin-top: -24dp; - gap: 8dp; + gap: var(--space-sm); justify-content: center; pointer-events: auto; } @@ -108,12 +121,13 @@ editor-toolbar button.editor-command { min-width: 96dp; height: 48dp; padding: 0 14dp; - border-radius: 8dp; - border: 1dp rgba(255, 255, 255, 26%); - background-color: rgba(17, 19, 24, 88%); - color: rgba(255, 250, 232, 94%); - font-family: "Fira Sans"; - font-size: 18dp; + border-radius: var(--radius-panel); + border-width: 1dp; + border-color: var(--button-border-color); + background-color: var(--button-background); + color: var(--button-color); + font-family: var(--font-family-body); + font-size: var(--font-size-xl); line-height: 48dp; opacity: 1; cursor: pointer; @@ -127,12 +141,12 @@ editor-toolbar button.editor-command span { } editor-toolbar button.editor-command.primary { - border-color: rgba(255, 232, 128, 70%); - background-color: rgba(96, 82, 38, 90%); + --button-border-color: rgba(var(--color-editor-accent-rgb), 70%); + --button-background: rgba(96, 82, 38, 90%); } editor-toolbar button.editor-command:hover, editor-toolbar button.editor-command:focus-visible { - border-color: rgba(255, 244, 190, 92%); - background-color: rgba(78, 85, 96, 92%); + border-color: var(--button-border-color-hover); + background-color: var(--button-background-hover); } diff --git a/res/rml/tuner.rcss b/res/rml/tuner.rcss index 86dd2043f6..5e0d96e855 100644 --- a/res/rml/tuner.rcss +++ b/res/rml/tuner.rcss @@ -1,90 +1,87 @@ -*, *:before, *:after { - box-sizing: border-box; -} - body { overflow: visible; width: 100%; height: 100%; margin: 0; padding: 0; - font-family: "Fira Sans Condensed"; - font-size: 24dp; - color: #FFFFFF; + font-family: var(--font-family-heading); + font-size: var(--font-size-4xl); + color: var(--color-white); display: flex; flex-direction: column; justify-content: flex-end; align-items: stretch; } -.tuner-root { +tuner-root { width: 100%; min-height: 45%; display: flex; flex-direction: column; justify-content: flex-end; align-items: stretch; - decorator: vertical-gradient(#00000000 #151610F2); + decorator: vertical-gradient(rgba(var(--color-black-rgb), 0%) rgba(var(--color-surface-rgb), 242)); filter: opacity(0); transition: filter 0.2s linear-in-out; } -.tuner-root[open] { +tuner-root[open] { filter: opacity(1); } -.tuner { +graphics-tuner { width: 100%; max-width: 1216dp; margin-left: auto; margin-right: auto; display: flex; flex-direction: column; - gap: 24dp; + gap: var(--space-xl); padding: 48dp 64dp; } @media (max-height: 800dp) { - .tuner-root { + tuner-root { min-height: 38%; } - .tuner { - gap: 16dp; - padding: 32dp 48dp; + graphics-tuner { + gap: var(--space-lg); + padding: var(--space-2xl) 48dp; } } -.header { +tuner-header { display: flex; justify-content: space-between; align-items: center; - gap: 24dp; + gap: var(--space-xl); } -.carousel-container { +carousel-container { flex: 1 1 auto; display: flex; justify-content: flex-end; min-width: 0; } -.description { - font-size: 18dp; +tuner-description { + font-size: var(--font-size-xl); line-height: 22dp; - color: rgba(255, 255, 255, 50%); + color: rgba(var(--color-white-rgb), 50%); } -.divider { +tuner-divider { margin: 1dp 0; - border-top: 1dp rgba(217, 217, 217, 50%); + border-top-width: 1dp; + border-top-color: rgba(var(--color-neutral-rgb), 50%); } -.footer { +tuner-footer { display: flex; justify-content: space-between; align-items: center; - gap: 24dp; + gap: var(--space-xl); } footer-button { @@ -94,12 +91,12 @@ footer-button { border: 0; padding: 0; background-color: transparent; - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; - font-size: 20dp; + font-size: var(--font-size-2xl); line-height: 24dp; text-transform: uppercase; - color: #FFFFFF; + color: var(--color-white); opacity: 1; cursor: pointer; } @@ -112,20 +109,20 @@ footer-button.reset { text-align: right; } -.stepped-carousel { +stepped-carousel { display: flex; align-items: center; justify-content: center; - gap: 16dp; + gap: var(--space-lg); width: auto; min-width: 246dp; padding: 0; background-color: transparent; - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; } -.stepped-carousel-value { +stepped-carousel-value { line-height: 29dp; min-width: 166dp; text-align: center; @@ -142,6 +139,6 @@ footer-button.reset { background-color: transparent; opacity: 1; cursor: pointer; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; } diff --git a/res/rml/window.rcss b/res/rml/window.rcss index 98b63dfcd9..f2f2574750 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -1,17 +1,13 @@ -*, *:before, *:after { - box-sizing: border-box; -} - body { display: flex; width: 100%; height: 100%; padding: 64dp; - font-family: "Fira Sans"; + font-family: var(--font-family-body); font-weight: normal; font-style: normal; - font-size: 15dp; - color: #E0DBC8; + font-size: var(--font-size-base); + color: var(--color-text); } b { @@ -27,12 +23,13 @@ window { max-width: 1088dp; max-height: 768dp; margin: auto; - border-radius: 14dp; + border-radius: var(--radius-window); overflow: hidden; - border: 2dp #92875B; + border-width: 2dp; + border-color: var(--color-border); backdrop-filter: blur(5dp); box-shadow: 0 0 25dp 5dp; - background-color: rgba(21, 22, 16, 90%); + background-color: rgba(var(--color-surface-rgb), 90%); filter: opacity(0); transform: scale(0.9); transform-origin: center; @@ -61,7 +58,7 @@ window[open] { @media (max-height: 640dp) { body { - padding: 16dp; + padding: var(--space-lg); } window { box-shadow: none; @@ -70,7 +67,7 @@ window[open] { @media (max-width: 768dp) { body { - padding: 16dp; + padding: var(--space-lg); } window.modal { width: 100%; @@ -79,13 +76,14 @@ window[open] { } window tab-bar { - flex: 0 0 64dp; - height: 64dp; - background-color: rgba(217, 217, 217, 10%); - font-family: "Fira Sans Condensed"; + flex: 0 0 var(--toolbar-height); + height: var(--toolbar-height); + background-color: rgba(var(--color-neutral-rgb), 10%); + font-family: var(--font-family-heading); font-weight: bold; - font-size: 18dp; - border-bottom: 2dp #92875B; + font-size: var(--font-size-xl); + border-bottom-width: 2dp; + border-bottom-color: var(--color-border); } window tab-bar tab { @@ -106,14 +104,15 @@ window content pane { flex: 1 1 0; min-width: 0; min-height: 0; - padding: 24dp; - gap: 8dp; + padding: var(--space-xl); + gap: var(--space-sm); overflow: hidden auto; - font-size: 20dp; + font-size: var(--font-size-2xl); } window content pane:not(:last-of-type) { - border-right: 1dp #92875B; + border-right-width: 1dp; + border-right-color: var(--color-border); } window content pane > * { @@ -129,7 +128,7 @@ ui-list { } window content pane > ui-list, -.modal-content pane > ui-list { +modal-content pane > ui-list { flex: 1 1 0; min-width: 0; min-height: 0; @@ -146,7 +145,7 @@ ui-list-viewport { ui-list-content { display: flex; flex-flow: column; - gap: 8dp; + gap: var(--space-sm); min-width: 0; } @@ -156,7 +155,7 @@ ui-list-content > button.ui-list-row { ui-list-empty { display: block; - padding: 16dp; + padding: var(--space-lg); text-align: center; opacity: 0.45; } @@ -176,33 +175,33 @@ window content pane > ui-list:last-child { window content pane > ui-list ui-list-content, window content pane > ui-list ui-list-empty { - padding-left: 24dp; - padding-right: 24dp; + padding-left: var(--space-xl); + padding-right: var(--space-xl); } window content pane > ui-list:first-child ui-list-content, window content pane > ui-list:first-child ui-list-empty { - padding-top: 24dp; + padding-top: var(--space-xl); } window content pane > ui-list:last-child ui-list-content, window content pane > ui-list:last-child ui-list-empty { - padding-bottom: 24dp; + padding-bottom: var(--space-xl); } window content pane:last-of-type > div { line-height: 1.625; } -.data-folder-current { +data-folder-current { display: block; - font-size: 16dp; - color: rgba(224, 219, 200, 65%); + font-size: var(--font-size-md); + color: rgba(var(--color-text-rgb), 65%); } scrollbarvertical { width: 8dp; - margin: 4dp 4dp 4dp 0; + margin: var(--space-xs) var(--space-xs) var(--space-xs) 0; } scrollbarvertical sliderarrowdec, @@ -218,14 +217,14 @@ scrollbarvertical slidertrack { scrollbarvertical sliderbar { width: 8dp; min-height: 24dp; - background-color: rgba(224, 219, 200, 45%); + background-color: rgba(var(--color-text-rgb), 45%); border-radius: 2dp; transition: background-color 0.2s cubic-in-out; } scrollbarvertical sliderbar:hover, scrollbarvertical sliderbar:active { - background-color: rgba(194, 164, 45, 80%); + background-color: rgba(var(--color-accent-rgb), 80%); } scrollbarhorizontal { @@ -244,26 +243,27 @@ scrollbarhorizontal sliderbar { height: 0; } -.section-heading { - font-family: "Fira Sans Condensed"; +section-heading { + font-family: var(--font-family-heading); font-weight: bold; text-transform: uppercase; - font-size: 22dp; + font-size: var(--font-size-3xl); opacity: 0.25; } -.section-heading:not(:first-of-type) { - padding-top: 12dp; +section-heading:not(:first-of-type) { + padding-top: var(--space-md); } button { text-align: center; - background-color: rgba(17, 16, 10, 20%); + color: var(--button-color); + background-color: var(--button-background); opacity: 0.9; - padding: 8dp 16dp; - border-radius: 14dp; - box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp; - font-size: 20dp; + padding: var(--space-sm) var(--space-lg); + border-radius: var(--radius-window); + box-shadow: rgba(var(--color-border-rgb), 25%) 0 0 0 1dp; + font-size: var(--font-size-2xl); transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out; cursor: pointer; focus: auto; @@ -271,19 +271,19 @@ button { button:not(:disabled):hover, button:not(:disabled):focus-visible { - background-color: rgba(204, 184, 119, 20%); - box-shadow: #C2A42D 0 0 0 2dp; + background-color: var(--button-background-hover); + box-shadow: var(--color-accent) 0 0 0 2dp; } button:not(:disabled):selected { opacity: 1; - background-color: rgba(204, 184, 119, 40%); + background-color: var(--button-background-selected); } button:not(:disabled):active { opacity: 1; - background-color: rgba(204, 184, 119, 40%); - box-shadow: #C2A42D 0 0 0 2dp; + background-color: var(--button-background-active); + box-shadow: var(--color-accent) 0 0 0 2dp; } button:disabled { @@ -299,12 +299,13 @@ button.modal-btn { select-button { display: flex; align-items: center; - gap: 8dp; - background-color: rgba(17, 16, 10, 20%); + gap: var(--space-sm); + color: var(--button-color); + background-color: var(--button-background); opacity: 0.9; - padding: 8dp 16dp; - border-radius: 14dp; - box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp; + padding: var(--space-sm) var(--space-lg); + border-radius: var(--radius-window); + box-shadow: rgba(var(--color-border-rgb), 25%) 0 0 0 1dp; transition: background-color 0.1s linear-in-out, opacity 0.1s linear-in-out; cursor: pointer; focus: auto; @@ -312,19 +313,19 @@ select-button { select-button:not(:disabled):hover, select-button:not(:disabled):focus-visible { - background-color: rgba(204, 184, 119, 20%); - box-shadow: #C2A42D 0 0 0 2dp; + background-color: var(--button-background-hover); + box-shadow: var(--color-accent) 0 0 0 2dp; } select-button:not(:disabled):selected { opacity: 1; - background-color: rgba(204, 184, 119, 40%); + background-color: var(--button-background-selected); } select-button:not(:disabled):active { opacity: 1; - background-color: rgba(204, 184, 119, 40%); - box-shadow: #C2A42D 0 0 0 2dp; + background-color: var(--button-background-active); + box-shadow: var(--color-accent) 0 0 0 2dp; } select-button:disabled { @@ -333,9 +334,9 @@ select-button:disabled { } select-button key { - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; - font-size: 18dp; + font-size: var(--font-size-xl); text-transform: uppercase; flex: 0 1 auto; } @@ -343,7 +344,7 @@ select-button key { select-button value { flex: 1 1 auto; text-align: right; - font-size: 20dp; + font-size: var(--font-size-2xl); } select-button value.modified { @@ -352,7 +353,7 @@ select-button value.modified { select-button input { text-align: right; - font-size: 20dp; + font-size: var(--font-size-2xl); } select-button.group-button icon { @@ -360,8 +361,8 @@ select-button.group-button icon { margin-left: auto; width: 24dp; height: 24dp; - font-size: 24dp; - color: inherit; + font-size: var(--font-size-4xl); + color: var(--button-color); decorator: text("" center center); } @@ -371,8 +372,8 @@ select-button.group-button value { select-button.color-input value { min-width: 0; - font-family: "Noto Mono"; - font-size: 15dp; + font-family: var(--font-family-monospace); + font-size: var(--font-size-base); } select-button.color-input color-swatch { @@ -380,19 +381,19 @@ select-button.color-input color-swatch { flex: 0 0 48dp; width: 48dp; height: 24dp; - border-radius: 7dp; - box-shadow: rgba(255, 255, 255, 45%) 0 0 0 1dp; + border-radius: var(--radius-control); + box-shadow: rgba(var(--color-white-rgb), 45%) 0 0 0 1dp; } select-button.color-input color-swatch.empty { - background-color: rgba(224, 219, 200, 12%); - decorator: linear-gradient(135deg, rgba(224, 219, 200, 0) 45%, rgba(194, 164, 45, 70%) 48%, rgba(194, 164, 45, 70%) 52%, rgba(224, 219, 200, 0) 55%); + background-color: rgba(var(--color-text-rgb), 12%); + decorator: linear-gradient(135deg, rgba(var(--color-text-rgb), 0) 45%, rgba(var(--color-accent-rgb), 70%) 48%, rgba(var(--color-accent-rgb), 70%) 52%, rgba(var(--color-text-rgb), 0) 55%); } icon { width: 1em; height: 1em; - font-family: "Material Symbols Rounded"; + font-family: var(--font-family-icons); font-weight: normal; display: inline-block; vertical-align: middle; @@ -410,6 +411,10 @@ icon.verifying { decorator: text("" center center); } +icon.download { + decorator: text("" center center); +} + icon.celebration { decorator: text("" center center); } @@ -418,73 +423,74 @@ icon.question-mark { decorator: text("" center center); } -.achievement-total { +achievement-total { position: absolute; top: 0; - right: 64dp; - height: 64dp; - line-height: 64dp; - font-family: "Fira Sans Condensed"; + right: var(--toolbar-height); + height: var(--toolbar-height); + line-height: var(--toolbar-height); + font-family: var(--font-family-heading); font-weight: bold; - font-size: 16dp; - color: rgba(224, 219, 200, 55%); + font-size: var(--font-size-md); + color: rgba(var(--color-text-rgb), 55%); pointer-events: none; } -.achievement-row { +achievement-row { display: flex; align-items: flex-start; gap: 10dp; - padding: 12dp 0; - border-bottom: 1dp rgba(146, 135, 91, 30%); + padding: var(--space-md) 0; + border-bottom-width: 1dp; + border-bottom-color: rgba(var(--color-border-rgb), 30%); } -.achievement-info { +achievement-info { display: block; flex: 1 1 0; min-width: 0; } -.achievement-header { +achievement-header { display: flex; align-items: center; } -.achievement-name { +achievement-name { flex: 1; font-weight: bold; } -.achievement-name.unlocked { - color: #ffa826; +achievement-name.unlocked { + color: var(--color-warning); } -.achievement-badge { - font-size: 14dp; +achievement-badge { + font-size: var(--font-size-sm); opacity: 0.7; } -.achievement-badge.unlocked { - color: #44cc55; +achievement-badge.unlocked { + color: var(--color-success); opacity: 1; } -.achievement-badge.locked { - color: #cc4444; +achievement-badge.locked { + color: var(--color-error); opacity: 1; } .achievement-desc { display: block; - color: rgba(224, 219, 200, 55%); - font-size: 16dp; - margin: 4dp 0 0 0; + color: rgba(var(--color-text-rgb), 55%); + font-size: var(--font-size-md); + margin: var(--space-xs) 0 0 0; } -.achievement-progress { +achievement-progress { display: block; - font-size: 13dp; - color: rgba(224, 219, 200, 45%); + font-size: var(--font-size-xs); + color: rgba(var(--color-text-rgb), 45%); } progress { @@ -492,32 +498,32 @@ progress { width: 100%; height: 6dp; border-radius: 3dp; - background-color: rgba(255, 255, 255, 10%); - margin: 6dp 0 2dp 0; + background-color: rgba(var(--color-white-rgb), 10%); + margin: 6dp 0 var(--space-2xs) 0; } progress fill { - background-color: rgba(194, 164, 45, 80%); + background-color: rgba(var(--color-accent-rgb), 80%); border-radius: 3dp; } progress.progress-done fill { - background-color: #44aa22; + background-color: var(--color-progress-done); } progress.progress-ongoing fill { - background-color: #2255bb; + background-color: var(--color-progress-ongoing); } button.achievement-clear { flex: 0 0 auto; align-self: center; - font-size: 14dp; - padding: 2dp 8dp; + font-size: var(--font-size-sm); + padding: var(--space-2xs) var(--space-sm); opacity: 0.45; } -.preset-grid { +preset-grid { display: flex; flex-direction: row; gap: 20dp; @@ -526,24 +532,24 @@ button.achievement-clear { width: 100%; } -.preset-col { +preset-option { display: flex; flex-flow: column; - gap: 12dp; + gap: var(--space-md); flex: 1 1 0; } -.preset-desc { +preset-description { display: block; - font-size: 16dp; + font-size: var(--font-size-md); text-align: center; } -.modal-dialog { +modal-dialog { display: flex; flex-direction: column; align-items: flex-start; - padding: 24dp; + padding: var(--space-xl); gap: 20dp; flex: 0 1 auto; min-height: 0; @@ -553,55 +559,55 @@ button.achievement-clear { } window.modal.danger { - border: 2dp #852221; + border: 2dp var(--color-danger-border); } -.modal-header { +modal-header { display: flex; flex-direction: row; align-items: center; justify-content: space-between; width: 100%; flex: 0 0 auto; - gap: 16dp; + gap: var(--space-lg); } -.modal-header icon { - font-size: 24dp; - color: #92875B; +modal-header icon { + font-size: var(--font-size-4xl); + color: var(--color-border); } -.modal-title { +modal-title { display: block; - font-family: "Fira Sans Condensed"; + font-family: var(--font-family-heading); font-weight: bold; text-transform: uppercase; - font-size: 18dp; - color: #92875B; + font-size: var(--font-size-xl); + color: var(--color-border); flex: 1 1 auto; } -window.modal.danger .modal-title, -window.modal.danger .modal-header icon { - color: #B3261E; +window.modal.danger modal-title, +window.modal.danger modal-header icon { + color: var(--color-danger-heading); } -.modal-body { +modal-body { display: block; width: 100%; flex: 0 0 auto; min-width: 0; - font-size: 20dp; - color: #FFFFFF; + font-size: var(--font-size-2xl); + color: var(--color-white); font-weight: normal; } -.modal-body span.tip { - font-size: 14dp; - color: #92875B; +modal-body modal-tip { + font-size: var(--font-size-sm); + color: var(--color-border); } -.modal-content { +modal-content { display: none; width: 100%; flex: 1 1 auto; @@ -609,88 +615,367 @@ window.modal.danger .modal-header icon { overflow: hidden; } -.modal-content.active { +modal-content.active { display: flex; flex-direction: column; } -.modal-content pane { +modal-content pane { display: flex; flex: 1 1 auto; flex-direction: column; min-height: 0; width: 100%; - gap: 8dp; + gap: var(--space-sm); overflow: hidden auto; } -.modal-content pane > * { +modal-content pane > * { flex: 0 0 auto; } -.verification-progress { +window.modal.install-queue { + max-height: 768dp; +} + +window.modal.drop-install { + max-height: 720dp; +} + +window.modal.drop-install package-row { + padding-right: 0; +} + +window.modal.install-queue modal-body { + display: none; +} + +window.modal.install-queue modal-content pane { + gap: 0; + padding-right: 14dp; + padding-bottom: 6dp; +} + +package-row { + display: flex; + flex-direction: row; + align-items: flex-start; + position: relative; + width: 100%; + gap: 10dp; + padding: var(--space-md) 0; + border-bottom-width: 1dp; + border-bottom-color: rgba(var(--color-border-rgb), 30%); +} + +package-row:last-child { + border-bottom-width: 0; +} + +package-row > mod-icon { + display: none; + flex: 0 0 36dp; + width: 36dp; + height: 36dp; + margin-top: var(--space-2xs); + overflow: hidden; + border-radius: var(--radius-panel); + background-color: rgba(var(--color-control-rgb), 45%); + color: rgba(var(--color-text-rgb), 45%); + font-family: var(--font-family-icons); + font-size: var(--font-size-4xl); + decorator: text("" center center); +} + +package-row > mod-icon.visible { + display: block; +} + +package-row > mod-icon.has-image { + background-color: transparent; +} + +package-row.paused > mod-icon, +package-row.retrying > mod-icon, +package-row.failed > mod-icon { + filter: grayscale(1); + opacity: 0.55; +} + +package-row > section { + display: flex; + flex-direction: column; + flex: 1 1 0; + min-width: 0; + margin: 0; + padding: 0; +} + +package-row > section > header { + display: flex; + width: 100%; + gap: var(--space-sm); + align-items: center; + margin: 0; + padding: 0 0 6dp 0; +} + +package-row h3 { + display: flex; + align-items: baseline; + flex: 1 1 auto; + min-width: 0; + gap: var(--space-xs); + margin: 0; + padding: 0; + overflow: hidden; +} + +package-row h3 > span { + display: block; + flex: 0 1 auto; + min-width: 0; + font-weight: bold; + color: var(--color-white); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +package-row h3 > small { + display: block; + flex: 0 0 auto; + font-size: var(--font-size-xs); + font-weight: normal; + color: rgba(var(--color-text-rgb), 50%); +} + +package-row header > small { + display: block; + flex: 0 0 auto; + font-size: var(--font-size-sm); + color: rgba(var(--color-text-rgb), 60%); +} + +package-row.downloading header > small { + color: var(--color-accent); +} + +package-row.retrying header > small { + color: var(--color-warning); +} + +package-row.failed header > small, +package-row.failed footer > small { + color: var(--color-error); +} + +package-row.installed header > small, +package-row.installed footer > small { + color: var(--color-success); +} + +package-row progress { + width: 100%; + height: 6dp; + margin: 0 0 var(--space-sm) 0; + border-radius: 3dp; +} + +package-row.failed progress fill { + background-color: var(--color-error); +} + +package-row.paused progress fill { + background-color: rgba(var(--color-text-rgb), 35%); +} + +package-row.retrying progress fill { + background-color: rgba(var(--color-warning-rgb), 55%); +} + +package-row.installed progress fill { + background-color: var(--color-success); +} + +package-row footer { + display: flex; + align-items: flex-start; + width: 100%; + min-width: 0; + gap: var(--space-sm); + margin: 0; + padding: 0; +} + +package-row footer > small { + display: block; + flex: 1 1 0; + min-width: 0; + font-size: var(--font-size-xs); + line-height: 1; + color: rgba(var(--color-text-rgb), 45%); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +package-row nav { + display: flex; + flex: 0 0 auto; + gap: 6dp; + margin: 0; + padding: 0; +} + +package-row nav > button.icon-action { + align-items: center; + justify-content: center; + flex: 0 0 30dp; + width: 30dp; + min-width: 30dp; + height: 26dp; + padding: 0; + border-radius: 14dp; + font-size: var(--font-size-md); +} + +package-row nav > button.icon-action icon { + flex: 0 0 var(--font-size-md); + width: var(--font-size-md); + height: var(--font-size-md); + font-size: var(--font-size-md); + line-height: 1; +} + +package-row.installed { + align-items: center; +} + +package-row.installed > mod-icon { + margin-top: 0; +} + +package-row.installed > section { + padding-right: 38dp; +} + +package-row.installed header > small { + display: none; +} + +package-row.installed nav { + position: absolute; + top: 17dp; + right: 0; +} + +package-row.installed nav > button.icon-action { + opacity: 0.45; +} + +verification-progress { display: flex; flex-direction: column; gap: 10dp; width: 100%; } -.verification-file { +verification-file { display: block; - font-size: 17dp; - color: #FFFFFF; + font-size: var(--font-size-lg); + color: var(--color-white); } progress.verification-progress-bar { height: 8dp; - margin: 2dp 0 0 0; + margin: var(--space-2xs) 0 0 0; } -.verification-detail { +verification-detail { display: block; - font-size: 14dp; - color: rgba(224, 219, 200, 65%); + font-size: var(--font-size-sm); + color: rgba(var(--color-text-rgb), 65%); } -.modal-actions { +modal-actions { display: flex; flex-direction: row; flex-wrap: nowrap; align-items: stretch; - gap: 12dp; + gap: var(--space-md); width: 100%; flex: 0 0 auto; - padding-top: 4dp; + padding-top: var(--space-xs); } -.modal-actions-vertical { +modal-actions.vertical { flex-direction: column; align-items: stretch; } -.modal-actions-vertical button.modal-btn { +modal-actions.vertical button.modal-btn { flex: 0 0 auto; width: 100%; } @media (max-height: 640dp) { - .modal-dialog { - padding: 16dp; - gap: 12dp; + modal-dialog { + padding: var(--space-lg); + gap: var(--space-md); } - .modal-body { - font-size: 17dp; + modal-body { + font-size: var(--font-size-lg); } } @media (max-width: 640dp) { - .modal-actions { + modal-actions { flex-direction: column; } - .modal-actions button.modal-btn { + modal-actions button.modal-btn { flex: 0 0 auto; width: 100%; } } + +button.icon-button { + display: flex; + align-items: center; + justify-content: center; + width: 44dp; + height: 44dp; + padding: var(--space-sm); + box-sizing: border-box; + flex-shrink: 0; + font-size: var(--font-size-5xl); +} + +button.icon-button icon { + display: block; + flex-shrink: 0; + line-height: 1; + pointer-events: none; +} + +ui-tooltip { + display: none; + position: absolute; + z-index: 1000; + max-width: 240dp; + padding: 6dp 10dp; + border: 1dp var(--color-border); + border-radius: var(--radius-panel); + background-color: rgba(var(--color-surface-rgb), 96%); + color: var(--color-text); + font-size: var(--font-size-md); + word-break: break-word; + pointer-events: none; + focus: none; +} + +ui-tooltip.visible { + display: block; +} diff --git a/sdk/include/mods/api.h b/sdk/include/mods/api.h index 3fde6c9f6a..3015130ae3 100644 --- a/sdk/include/mods/api.h +++ b/sdk/include/mods/api.h @@ -42,6 +42,8 @@ extern "C" { #define MOD_ABI_VERSION 1u #define MOD_ERROR_MESSAGE_SIZE 512u +#define DUSKLIGHT_SERVICE_ID_PREFIX "dev.twilitrealm.dusklight." + typedef struct ModContext ModContext; typedef enum ModResult { diff --git a/sdk/include/mods/meta.hpp b/sdk/include/mods/meta.hpp index edf226a266..6028a3df9c 100644 --- a/sdk/include/mods/meta.hpp +++ b/sdk/include/mods/meta.hpp @@ -13,11 +13,15 @@ * modmeta records. Each IMPORT_SERVICE/EXPORT_SERVICE/DEFINE_HOOK use places one * constant-initialized record object in the metadata section. */ +#if defined(_MSVC_LANG) && !defined(__clang__) +#define MOD_META_NO_ASAN +#else #if defined(__has_attribute) && __has_attribute(no_sanitize) #define MOD_META_NO_ASAN __attribute__((no_sanitize("address"))) #else #define MOD_META_NO_ASAN #endif +#endif #if defined(_WIN32) #pragma section("modmeta$a", read, write) diff --git a/sdk/include/mods/svc/actor.h b/sdk/include/mods/svc/actor.h index 5876ddcaf2..0048b70332 100644 --- a/sdk/include/mods/svc/actor.h +++ b/sdk/include/mods/svc/actor.h @@ -3,7 +3,7 @@ #include #include -#define ACTOR_SERVICE_ID "dev.twilitrealm.dusklight.actor" +#define ACTOR_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "actor" #define ACTOR_SERVICE_MAJOR 1u #define ACTOR_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/camera.h b/sdk/include/mods/svc/camera.h index 1dccecfba6..a901c2b739 100644 --- a/sdk/include/mods/svc/camera.h +++ b/sdk/include/mods/svc/camera.h @@ -6,7 +6,7 @@ #include #endif -#define CAMERA_SERVICE_ID "dev.twilitrealm.dusklight.camera" +#define CAMERA_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "camera" #define CAMERA_SERVICE_MAJOR 1u #define CAMERA_SERVICE_MINOR 1u diff --git a/sdk/include/mods/svc/config.h b/sdk/include/mods/svc/config.h index fb04f6e9ea..55a0bca131 100644 --- a/sdk/include/mods/svc/config.h +++ b/sdk/include/mods/svc/config.h @@ -6,7 +6,7 @@ #include #endif -#define CONFIG_SERVICE_ID "dev.twilitrealm.dusklight.config" +#define CONFIG_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "config" #define CONFIG_SERVICE_MAJOR 1u #define CONFIG_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/file.h b/sdk/include/mods/svc/file.h index 4d5c82b5a1..0383c54066 100644 --- a/sdk/include/mods/svc/file.h +++ b/sdk/include/mods/svc/file.h @@ -6,7 +6,7 @@ #include #endif -#define FILE_SERVICE_ID "dev.twilitrealm.dusklight.file" +#define FILE_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "file" #define FILE_SERVICE_MAJOR 1u #define FILE_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/flow.h b/sdk/include/mods/svc/flow.h index a9028df0a4..1099d86fd9 100644 --- a/sdk/include/mods/svc/flow.h +++ b/sdk/include/mods/svc/flow.h @@ -6,7 +6,7 @@ #include #endif -#define FLOW_SERVICE_ID "dev.twilitrealm.dusklight.flow" +#define FLOW_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "flow" #define FLOW_SERVICE_MAJOR 1u #define FLOW_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/game.h b/sdk/include/mods/svc/game.h index 32de49903f..61a10968e6 100644 --- a/sdk/include/mods/svc/game.h +++ b/sdk/include/mods/svc/game.h @@ -15,7 +15,7 @@ * ordinary version check then fails mods built against the old epoch with a clear message instead * of letting them corrupt memory. */ -#define GAME_SERVICE_ID "dev.twilitrealm.dusklight.game" +#define GAME_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "game" #define GAME_SERVICE_MAJOR 2u #define GAME_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/game_mode.h b/sdk/include/mods/svc/game_mode.h index 8ccc5d64f1..d17a8f003c 100644 --- a/sdk/include/mods/svc/game_mode.h +++ b/sdk/include/mods/svc/game_mode.h @@ -3,7 +3,7 @@ #include #include -#define GAME_MODE_SERVICE_ID "dev.twilitrealm.dusklight.gamemode" +#define GAME_MODE_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "gamemode" #define GAME_MODE_SERVICE_MAJOR 1u #define GAME_MODE_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/gfx.h b/sdk/include/mods/svc/gfx.h index 0eee026984..787958716b 100644 --- a/sdk/include/mods/svc/gfx.h +++ b/sdk/include/mods/svc/gfx.h @@ -31,7 +31,7 @@ * should be released in mod_shutdown. The device outlives all mods. */ -#define GFX_SERVICE_ID "dev.twilitrealm.dusklight.gfx" +#define GFX_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "gfx" #define GFX_SERVICE_MAJOR 1u #define GFX_SERVICE_MINOR 2u diff --git a/sdk/include/mods/svc/hook.h b/sdk/include/mods/svc/hook.h index 288db3cc14..30b199b386 100644 --- a/sdk/include/mods/svc/hook.h +++ b/sdk/include/mods/svc/hook.h @@ -20,7 +20,7 @@ * (file-local statics included). */ -#define HOOK_SERVICE_ID "dev.twilitrealm.dusklight.hook" +#define HOOK_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "hook" #define HOOK_SERVICE_MAJOR 1u #define HOOK_SERVICE_MINOR 1u diff --git a/sdk/include/mods/svc/host.h b/sdk/include/mods/svc/host.h index 45d89eea3e..2b751f34c3 100644 --- a/sdk/include/mods/svc/host.h +++ b/sdk/include/mods/svc/host.h @@ -11,7 +11,7 @@ * Always available; every other service can be reached from it. */ -#define HOST_SERVICE_ID "dev.twilitrealm.dusklight.host" +#define HOST_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "host" #define HOST_SERVICE_MAJOR 2u #define HOST_SERVICE_MINOR 2u diff --git a/sdk/include/mods/svc/http.h b/sdk/include/mods/svc/http.h index fd4d9a98db..2c880b3b80 100644 --- a/sdk/include/mods/svc/http.h +++ b/sdk/include/mods/svc/http.h @@ -6,7 +6,7 @@ #include #endif -#define HTTP_SERVICE_ID "dev.twilitrealm.dusklight.http" +#define HTTP_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "http" #define HTTP_SERVICE_MAJOR 1u #define HTTP_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/item.h b/sdk/include/mods/svc/item.h index ff4f31b547..58cf7743e4 100644 --- a/sdk/include/mods/svc/item.h +++ b/sdk/include/mods/svc/item.h @@ -6,7 +6,7 @@ #include #endif -#define ITEM_SERVICE_ID "dev.twilitrealm.dusklight.item" +#define ITEM_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "item" #define ITEM_SERVICE_MAJOR 2u #define ITEM_SERVICE_MINOR 3u diff --git a/sdk/include/mods/svc/log.h b/sdk/include/mods/svc/log.h index 1040710492..2401b4dccf 100644 --- a/sdk/include/mods/svc/log.h +++ b/sdk/include/mods/svc/log.h @@ -11,7 +11,7 @@ * (prefixed with its ID). */ -#define LOG_SERVICE_ID "dev.twilitrealm.dusklight.log" +#define LOG_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "log" #define LOG_SERVICE_MAJOR 1u #define LOG_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/message.h b/sdk/include/mods/svc/message.h index b170e6f09b..d3833d904e 100644 --- a/sdk/include/mods/svc/message.h +++ b/sdk/include/mods/svc/message.h @@ -6,7 +6,7 @@ #include #endif -#define MESSAGE_SERVICE_ID "dev.twilitrealm.dusklight.message" +#define MESSAGE_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "message" #define MESSAGE_SERVICE_MAJOR 1u #define MESSAGE_SERVICE_MINOR 1u diff --git a/sdk/include/mods/svc/net.h b/sdk/include/mods/svc/net.h index 2c5e9a531a..9202ff50ad 100644 --- a/sdk/include/mods/svc/net.h +++ b/sdk/include/mods/svc/net.h @@ -6,7 +6,7 @@ #include #endif -#define NET_SERVICE_ID "dev.twilitrealm.dusklight.net" +#define NET_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "net" #define NET_SERVICE_MAJOR 1u #define NET_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/overlay.h b/sdk/include/mods/svc/overlay.h index ea46bc8798..ee4318ea4f 100644 --- a/sdk/include/mods/svc/overlay.h +++ b/sdk/include/mods/svc/overlay.h @@ -6,7 +6,7 @@ #include #endif -#define OVERLAY_SERVICE_ID "dev.twilitrealm.dusklight.overlay" +#define OVERLAY_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "overlay" #define OVERLAY_SERVICE_MAJOR 1u #define OVERLAY_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/resource.h b/sdk/include/mods/svc/resource.h index 60b609d2a2..516dcab20a 100644 --- a/sdk/include/mods/svc/resource.h +++ b/sdk/include/mods/svc/resource.h @@ -12,7 +12,7 @@ * for temporary storage. */ -#define RESOURCE_SERVICE_ID "dev.twilitrealm.dusklight.resource" +#define RESOURCE_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "resource" #define RESOURCE_SERVICE_MAJOR 1u #define RESOURCE_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/save.h b/sdk/include/mods/svc/save.h index 0d6943f96a..d9eb7432be 100644 --- a/sdk/include/mods/svc/save.h +++ b/sdk/include/mods/svc/save.h @@ -6,7 +6,7 @@ #include #endif -#define SAVE_SERVICE_ID "dev.twilitrealm.dusklight.save" +#define SAVE_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "save" #define SAVE_SERVICE_MAJOR 1u #define SAVE_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/stage.h b/sdk/include/mods/svc/stage.h index a9f8198919..9dbd94cff1 100644 --- a/sdk/include/mods/svc/stage.h +++ b/sdk/include/mods/svc/stage.h @@ -6,7 +6,7 @@ #include #endif -#define STAGE_SERVICE_ID "dev.twilitrealm.dusklight.stage" +#define STAGE_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "stage" #define STAGE_SERVICE_MAJOR 1u #define STAGE_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/texture.h b/sdk/include/mods/svc/texture.h index dac73fbade..0de813cd04 100644 --- a/sdk/include/mods/svc/texture.h +++ b/sdk/include/mods/svc/texture.h @@ -6,7 +6,7 @@ #include #endif -#define TEXTURE_SERVICE_ID "dev.twilitrealm.dusklight.texture" +#define TEXTURE_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "texture" #define TEXTURE_SERVICE_MAJOR 1u #define TEXTURE_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/ui.h b/sdk/include/mods/svc/ui.h index d0badfda05..bcac0c53fa 100644 --- a/sdk/include/mods/svc/ui.h +++ b/sdk/include/mods/svc/ui.h @@ -8,7 +8,7 @@ #include #endif -#define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui" +#define UI_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "ui" #define UI_SERVICE_MAJOR 2u #define UI_SERVICE_MINOR 2u diff --git a/sdk/include/mods/svc/websocket.h b/sdk/include/mods/svc/websocket.h index bfc12d8fca..abd36410ef 100644 --- a/sdk/include/mods/svc/websocket.h +++ b/sdk/include/mods/svc/websocket.h @@ -7,7 +7,7 @@ #include #endif -#define WEBSOCKET_SERVICE_ID "dev.twilitrealm.dusklight.websocket" +#define WEBSOCKET_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "websocket" #define WEBSOCKET_SERVICE_MAJOR 1u #define WEBSOCKET_SERVICE_MINOR 0u diff --git a/sdk/include/mods/svc/window.h b/sdk/include/mods/svc/window.h index 7cee8a5abe..843e388ed8 100644 --- a/sdk/include/mods/svc/window.h +++ b/sdk/include/mods/svc/window.h @@ -8,7 +8,7 @@ #include -#define WINDOW_SERVICE_ID "dev.twilitrealm.dusklight.window" +#define WINDOW_SERVICE_ID DUSKLIGHT_SERVICE_ID_PREFIX "window" #define WINDOW_SERVICE_MAJOR 1u #define WINDOW_SERVICE_MINOR 0u diff --git a/src/Z2AudioLib/Z2Audience.cpp b/src/Z2AudioLib/Z2Audience.cpp index 0a2d52540c..831d356d2f 100644 --- a/src/Z2AudioLib/Z2Audience.cpp +++ b/src/Z2AudioLib/Z2Audience.cpp @@ -746,16 +746,26 @@ f32 Z2Audience::calcRelPosPan(const Vec& param_0, int camID) { f32 Z2Audience::calcRelPosDolby(const Vec& param_0, int camID) { f32 fVar1 = param_0.z + mAudioCamera[camID].getDolbyCenterZ(); #if TARGET_PC - if (dusk::audio::EnableHrtf) { + const auto mode = dusk::getSettings().audio.outputMode.getValue(); + if (mode >= dusk::AudioOutputMode::StereoHeadphones) { // Normalize the direction so result is purely front/back orientation, // independent of how far away the sound is - f32 lenSq = param_0.x * param_0.x + param_0.y * param_0.y + param_0.z * param_0.z; + f32 lenSq = param_0.x * param_0.x + param_0.z * param_0.z; + if (mode == dusk::AudioOutputMode::StereoHeadphones) { + // original HRTF math + lenSq += param_0.y * param_0.y; + } if (lenSq < 0.0001f) { return 0.5f; } f32 zNorm = param_0.z / sqrtf(lenSq); f32 t = (zNorm + 1.0f) * 0.5f; - return 0.5f - 0.5f * cosf(t * static_cast(M_PI)); + if (mode == dusk::AudioOutputMode::StereoHeadphones) { + // original HRTF math + return 0.5f - 0.5f * cosf(t * static_cast(M_PI)); + } else { + return t; + } } #endif if (fVar1 > mSetting.mDolbyBehindDistanceMax) { diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp index 4d1bf182db..02552022a8 100644 --- a/src/d/actor/d_a_alink.cpp +++ b/src/d/actor/d_a_alink.cpp @@ -53,11 +53,25 @@ #if TARGET_PC #include "dusk/action_bindings.h" -#include "dusk/frame_interpolation.h" +#include "dusk/interp/dual_buffer.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/settings.h" #include "res/Object/Alink.h" #include #include + +static const int IRON_BALL_CHAIN_COUNT = 102; +static const int HS_CHAIN_ANCHOR_COUNT = 4; + +namespace { +struct AlinkInterp { + dusk::interp::DualBuffer ib_pos; + dusk::interp::DualBuffer ib_angle; + dusk::interp::DualBuffer ib_hand; + cXyz hs_draw[HS_CHAIN_ANCHOR_COUNT]; + dusk::interp::DualBuffer hs_chain{hs_draw}; +}; +} // namespace #endif static int daAlink_Create(fopAc_ac_c* i_this); @@ -5993,7 +6007,7 @@ void daAlink_c::setItemMatrix(int param_0) { mDoMtx_stack_c::XrotS(-0x8000); #ifdef TARGET_PC - if (dusk::frame_interp::is_enabled()) { + if (dusk::interp::is_enabled()) { Mtx boot_mtx; mDoMtx_concat(mpLinkModel->getAnmMtx(0x18), mDoMtx_stack_c::get(), boot_mtx); mpLinkBootModels[1]->setAnmMtx(1, boot_mtx); @@ -14816,10 +14830,13 @@ void daAlink_c::deleteEquipItem(BOOL i_isPlaySound, BOOL i_isDeleteKantera) { mIronBallChainAngle = NULL; field_0x3848 = NULL; #if TARGET_PC - mIBChainInterpPrevValid = false; - mIBChainInterpCurrValid = false; - mHsChainInterpPrevValid = false; - mHsChainInterpCurrValid = false; + { + auto& interp = dusk::interp::get(this); + interp.ib_pos.reset(); + interp.ib_angle.reset(); + interp.ib_hand.reset(); + interp.hs_chain.reset(); + } #endif field_0x0774 = NULL; field_0x0778 = NULL; @@ -19792,36 +19809,21 @@ int daAlink_c::draw() { dComIfGd_getOpaListDark()->entryImm(mpHookChain, 0); #if TARGET_PC - if (dusk::frame_interp::is_enabled()) { + if (dusk::interp::is_enabled()) { + auto& interp = dusk::interp::get(this); if (mEquipItem == dItemNo_IRONBALL_e && mIronBallChainPos != NULL && mIronBallChainAngle != NULL) { - if (mIBChainInterpCurrValid) { - memcpy(mIBChainInterpPrevPos, mIBChainInterpCurrPos, IRON_BALL_CHAIN_COUNT * sizeof(cXyz)); - memcpy(mIBChainInterpPrevAngle, mIBChainInterpCurrAngle, IRON_BALL_CHAIN_COUNT * sizeof(csXyz)); - mIBChainInterpPrevHandRoot = mIBChainInterpCurrHandRoot; - mIBChainInterpPrevValid = true; - } - - memcpy(mIBChainInterpCurrPos, mIronBallChainPos, IRON_BALL_CHAIN_COUNT * sizeof(cXyz)); - memcpy(mIBChainInterpCurrAngle, mIronBallChainAngle, IRON_BALL_CHAIN_COUNT * sizeof(csXyz)); - mIBChainInterpCurrHandRoot = mHookshotTopPos; - mIBChainInterpCurrValid = true; - - dusk::frame_interp::add_interpolation_callback(&ironBallChainInterpCallback, this); + interp.ib_pos.writeback(mIronBallChainPos, IRON_BALL_CHAIN_COUNT); + interp.ib_angle.writeback(mIronBallChainAngle, IRON_BALL_CHAIN_COUNT); + interp.ib_hand.writeback(&mHookshotTopPos, 1); } else { - if (mHsChainInterpCurrValid) { - mHsChainInterpPrevTop = mHsChainInterpCurrTop; - mHsChainInterpPrevRoot = mHsChainInterpCurrRoot; - mHsChainInterpPrevSubRoot = mHsChainInterpCurrSubRoot; - mHsChainInterpPrevSubTop = mHsChainInterpCurrSubTop; - mHsChainInterpPrevValid = true; - } - mHsChainInterpCurrTop = mHookshotTopPos; - mHsChainInterpCurrRoot = mHeldItemRootPos; - mHsChainInterpCurrSubRoot = field_0x3810; - mHsChainInterpCurrSubTop = mIronBallBgChkPos; - mHsChainInterpCurrValid = true; + cXyz hsAnchors[HS_CHAIN_ANCHOR_COUNT]; + hsAnchors[0] = mHookshotTopPos; + hsAnchors[1] = mHeldItemRootPos; + hsAnchors[2] = field_0x3810; + hsAnchors[3] = mIronBallBgChkPos; + interp.hs_chain.capture_and_schedule(hsAnchors, HS_CHAIN_ANCHOR_COUNT); } } #endif diff --git a/src/d/actor/d_a_alink_hook.inc b/src/d/actor/d_a_alink_hook.inc index cbc8bdc664..9e4e7090f8 100644 --- a/src/d/actor/d_a_alink_hook.inc +++ b/src/d/actor/d_a_alink_hook.inc @@ -10,7 +10,8 @@ #include "JSystem/J3DGraphBase/J3DMaterial.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/dual_buffer.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/settings.h" static const int HS_CHAIN_MAX_LINKS = 600; @@ -137,15 +138,15 @@ void daAlink_c::hsChainShape_c::draw() { } else { #if TARGET_PC cXyz hsInterpTop, hsInterpRoot, hsInterpSubRoot, hsInterpSubTop; - if (dusk::frame_interp::is_enabled() && alink->mHsChainInterpPrevValid && alink->mHsChainInterpCurrValid) { - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - hsInterpTop = alink->mHsChainInterpPrevTop + (alink->mHsChainInterpCurrTop - alink->mHsChainInterpPrevTop) * alpha; - hsInterpRoot = alink->mHsChainInterpPrevRoot + (alink->mHsChainInterpCurrRoot - alink->mHsChainInterpPrevRoot) * alpha; - hsInterpSubRoot = alink->mHsChainInterpPrevSubRoot + (alink->mHsChainInterpCurrSubRoot - alink->mHsChainInterpPrevSubRoot) * alpha; - hsInterpSubTop = alink->mHsChainInterpPrevSubTop + (alink->mHsChainInterpCurrSubTop - alink->mHsChainInterpPrevSubTop) * alpha; + auto& hsInterp = dusk::interp::get(alink); + if (dusk::interp::is_enabled() && hsInterp.hs_chain.ready()) { + hsInterpTop = hsInterp.hs_draw[0]; + hsInterpRoot = hsInterp.hs_draw[1]; + hsInterpSubRoot = hsInterp.hs_draw[2]; + hsInterpSubTop = hsInterp.hs_draw[3]; } else { - hsInterpTop = alink->getHsChainTopPos(); - hsInterpRoot = alink->getHsChainRootPos(); + hsInterpTop = alink->getHsChainTopPos(); + hsInterpRoot = alink->getHsChainRootPos(); hsInterpSubRoot = alink->getHsSubChainRootPos(); hsInterpSubTop = alink->getHsSubChainTopPos(); } @@ -243,31 +244,6 @@ void daAlink_c::hsChainShape_c::draw() { } } -#if TARGET_PC -static void ironBallChainInterpCallback(bool isSimFrame, void* pUserWork) { - static_cast(pUserWork)->onIronBallChainInterpCallback(); -} - -void daAlink_c::onIronBallChainInterpCallback() { - if (!mIBChainInterpPrevValid || !mIBChainInterpCurrValid) { - return; - } - if (mIronBallChainPos == NULL || mIronBallChainAngle == NULL) { - return; - } - - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - - for (int i = 0; i < IRON_BALL_CHAIN_COUNT; i++) { - mIronBallChainPos[i] = mIBChainInterpPrevPos[i] + (mIBChainInterpCurrPos[i] - mIBChainInterpPrevPos[i]) * alpha; - mIronBallChainAngle[i].x = mIBChainInterpPrevAngle[i].x + (s16)((s16)(mIBChainInterpCurrAngle[i].x - mIBChainInterpPrevAngle[i].x) * alpha); - mIronBallChainAngle[i].y = mIBChainInterpPrevAngle[i].y + (s16)((s16)(mIBChainInterpCurrAngle[i].y - mIBChainInterpPrevAngle[i].y) * alpha); - mIronBallChainAngle[i].z = mIBChainInterpPrevAngle[i].z + (s16)((s16)(mIBChainInterpCurrAngle[i].z - mIBChainInterpPrevAngle[i].z) * alpha); - } - mHookshotTopPos = mIBChainInterpPrevHandRoot + (mIBChainInterpCurrHandRoot - mIBChainInterpPrevHandRoot) * alpha; -} -#endif - void daAlink_c::hookshotAtHitCallBack(dCcD_GObjInf* i_atObjInf, fopAc_ac_c* i_tgActor, dCcD_GObjInf* i_tgObjInf) { if (i_tgActor != NULL && fopAcM_IsActor(i_tgActor) && !i_tgObjInf->ChkTgHookshotThrough()) { diff --git a/src/d/actor/d_a_alink_horse.inc b/src/d/actor/d_a_alink_horse.inc index 7389decb73..f271ed401e 100644 --- a/src/d/actor/d_a_alink_horse.inc +++ b/src/d/actor/d_a_alink_horse.inc @@ -340,18 +340,31 @@ void daAlink_c::setHorseStirrup() { mDoMtx_stack_c::copy(mpLinkModel->getAnmMtx(field_0x30bc)); mDoMtx_stack_c::transM(-2.0f, -11.0f, 1.5f); mDoMtx_stack_c::ZXYrotM(0, -0x8000, 0x4000); +#if TARGET_PC + horse->m_model->setAnmMtx(0x17, mDoMtx_stack_c::get()); +#else mDoMtx_copy(mDoMtx_stack_c::get(), horse->getLeftStirrupMtx()); +#endif } if (field_0x2fab & 2) { mDoMtx_stack_c::copy(mpLinkModel->getAnmMtx(field_0x30be)); mDoMtx_stack_c::transM(-2.0f, 11.0f, 1.5f); mDoMtx_stack_c::ZrotM(-0x4000); +#if TARGET_PC + horse->m_model->setAnmMtx(0x19, mDoMtx_stack_c::get()); +#else mDoMtx_copy(mDoMtx_stack_c::get(), horse->getRightStirrupMtx()); +#endif } if (field_0x2fab & 3) { horse->calcWeightEnvMtx(); +#if TARGET_PC + for (u16 i = 0; i < horse->m_model->getModelData()->getWEvlpMtxNum(); i++) { + dusk::interp::record_final_mtx(horse->m_model->getWeightAnmMtx(i)); + } +#endif } int hand_type = getReinHandType(); diff --git a/src/d/actor/d_a_b_gnd.cpp b/src/d/actor/d_a_b_gnd.cpp index 80cca37f6e..42eeac303a 100644 --- a/src/d/actor/d_a_b_gnd.cpp +++ b/src/d/actor/d_a_b_gnd.cpp @@ -17,10 +17,10 @@ #include "Z2AudioLib/Z2Instances.h" -#include "dusk/frame_interpolation.h" -#include "dusk/settings.h" #if TARGET_PC #include "dusk/achievements.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/settings.h" #endif class daB_GND_HIO_c : public JORReflexible { @@ -285,30 +285,6 @@ static int h_nodeCallBack(J3DJoint* i_joint, int param_2) { return 1; } -#if TARGET_PC -static void b_gnd_rein_interp_callback(bool isSimFrame, void* pUserWork) { - b_gnd_class* i_this = (b_gnd_class*)pUserWork; - if (!i_this->mReinsInterpPrevValid || !i_this->mReinsInterpCurrValid) { - return; - } - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - for (int r = 0; r < 2; r++) { - cXyz* dst = i_this->mHorseReins[r].getPos(0); - for (int i = 0; i < 16; i++) { - const cXyz& p0 = i_this->mReinsInterpPrev[r][i]; - const cXyz& p1 = i_this->mReinsInterpCurr[r][i]; - dst[i] = p0 + (p1 - p0) * alpha; - } - } - cXyz* dst = i_this->field_0x21e8.getPos(0); - for (int i = 0; i < 2; i++) { - const cXyz& p0 = i_this->mReinsTexInterpPrev[i]; - const cXyz& p1 = i_this->mReinsTexInterpCurr[i]; - dst[i] = p0 + (p1 - p0) * alpha; - } -} -#endif - static int daB_GND_Draw(b_gnd_class* i_this) { fopAc_ac_c* a_this = (fopAc_ac_c*)i_this; @@ -396,21 +372,6 @@ static int daB_GND_Draw(b_gnd_class* i_this) { i_this->field_0x21e8.update(2, l_color, &a_this->tevStr); dComIfGd_set3DlineMat(&i_this->field_0x21e8); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->mReinsInterpCurrValid) { - memcpy(i_this->mReinsInterpPrev, i_this->mReinsInterpCurr, sizeof(i_this->mReinsInterpCurr)); - memcpy(i_this->mReinsTexInterpPrev, i_this->mReinsTexInterpCurr, sizeof(i_this->mReinsTexInterpCurr)); - i_this->mReinsInterpPrevValid = true; - } - for (int r = 0; r < 2; r++) { - memcpy(i_this->mReinsInterpCurr[r], i_this->mHorseReins[r].getPos(0), 16 * sizeof(cXyz)); - } - memcpy(i_this->mReinsTexInterpCurr, i_this->field_0x21e8.getPos(0), 2 * sizeof(cXyz)); - i_this->mReinsInterpCurrValid = true; - dusk::frame_interp::add_interpolation_callback(&b_gnd_rein_interp_callback, i_this); - } -#endif } return 1; @@ -3791,7 +3752,7 @@ static void demo_camera(b_gnd_class* i_this) { i_this->mDemoCamSyncTicks = 2; } if (i_this->mDemoCamSyncTicks > 0) { - dusk::frame_interp::request_presentation_sync(); + dusk::interp::request_presentation_sync(); i_this->mDemoCamSyncTicks--; } #endif diff --git a/src/d/actor/d_a_balloon_2D.cpp b/src/d/actor/d_a_balloon_2D.cpp index a9d6d0db4f..5d5923abc3 100644 --- a/src/d/actor/d_a_balloon_2D.cpp +++ b/src/d/actor/d_a_balloon_2D.cpp @@ -6,7 +6,6 @@ #include "d/dolzel_rel.h" // IWYU pragma: keep #include "d/actor/d_a_balloon_2D.h" -#include "dusk/frame_interpolation.h" #include "JSystem/J2DGraph/J2DGrafContext.h" #include "JSystem/J2DGraph/J2DScreen.h" #include "JSystem/J2DGraph/J2DTextBox.h" @@ -21,6 +20,10 @@ #include "m_Do/m_Do_lib.h" #include +#if TARGET_PC +#include "dusk/interp/frame_interpolation.h" +#endif + class daBalloon2D_HIO_c : public mDoHIO_entry_c { public: inline daBalloon2D_HIO_c() { @@ -439,12 +442,9 @@ void daBalloon2D_c::setComboAlpha() { void daBalloon2D_c::drawAddScore() { for (s32 i = 19; i >= 0; i--) { if (field_0x5f8[i].field_0xe != 0) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - field_0x5f8[i].field_0xe--; - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + field_0x5f8[i].field_0xe--; + IF_DUSK_BLOCK_END s32 score3; s32 score2; s32 score = field_0x5f8[i].field_0xc; @@ -452,13 +452,10 @@ void daBalloon2D_c::drawAddScore() { u8 local_88 = 0xff; f32 dVar11 = 30.0f; f32 dVar9 = 30.0f; -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - field_0x5f8[i].field_0x0.x += cM_ssin(temp0) * 0.3f; - field_0x5f8[i].field_0x0.y -= 1.0f; - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + field_0x5f8[i].field_0x0.x += cM_ssin(temp0) * 0.3f; + field_0x5f8[i].field_0x0.y -= 1.0f; + IF_DUSK_BLOCK_END if (field_0x5f8[i].field_0xe < 10) { f32 fVar5 = field_0x5f8[i].field_0xe / 10.0f; local_88 = fVar5 * 255.0f; diff --git a/src/d/actor/d_a_e_bee.cpp b/src/d/actor/d_a_e_bee.cpp index 09909f9e0a..3cfadae812 100644 --- a/src/d/actor/d_a_e_bee.cpp +++ b/src/d/actor/d_a_e_bee.cpp @@ -15,6 +15,10 @@ #include "SSystem/SComponent/c_math.h" #include "Z2AudioLib/Z2Instances.h" +#if TARGET_PC +#include "dusk/interp/frame_interpolation.h" +#endif + static bool hio_set; static daE_Bee_HIO_c l_HIO; @@ -50,6 +54,26 @@ static int daE_Bee_Draw(e_bee_class* i_this) { return 1; } +#if TARGET_PC +static void bee_interp(bee_s* i_bee) { + if (!dusk::interp::is_enabled()) { + return; + } + + J3DModel* models[] = { + i_bee->mpModel1, + i_bee->mpModel2, + i_bee->mpModel3, + i_bee->mpModel4, + }; + MtxP mtx = mDoMtx_stack_c::get(); + for (int i = 0; i < 4; i++) { + models[i]->setBaseTRMtx(mtx); + models[i]->calc(); + } +} +#endif + static void bee_mtxset(bee_s* i_bee) { mDoMtx_stack_c::transS(i_bee->mPos.x, i_bee->mPos.y, i_bee->mPos.z); mDoMtx_stack_c::YrotM(i_bee->mAngle.y); @@ -62,6 +86,7 @@ static void bee_mtxset(bee_s* i_bee) { } else { i_bee->mpModel2->setBaseTRMtx(mDoMtx_stack_c::get()); } + IF_DUSK(bee_interp(i_bee)); } static void bee_ground_ang_set(bee_s* i_bee) { @@ -332,6 +357,7 @@ static void bee_nest_action(e_bee_class* i_this, bee_s* i_bee, s8 i_nestHealth) i_bee->mpModel4->setBaseTRMtx(mDoMtx_stack_c::get()); } } + IF_DUSK(bee_interp(i_bee)); if (i_nestHealth == 1) { i_bee->mAction = bee_s::ACT_FLY; diff --git a/src/d/actor/d_a_e_db.cpp b/src/d/actor/d_a_e_db.cpp index dcc3849756..65f91843a3 100644 --- a/src/d/actor/d_a_e_db.cpp +++ b/src/d/actor/d_a_e_db.cpp @@ -10,10 +10,6 @@ #include "f_op/f_op_kankyo_mng.h" #include "f_op/f_op_actor_enemy.h" -#if TARGET_PC -#include "dusk/frame_interpolation.h" -#endif - class daE_DB_HIO_c : public JORReflexible { public: daE_DB_HIO_c(); @@ -70,22 +66,6 @@ static BOOL leaf_anm_init(e_db_class* i_this, int i_anm, f32 i_morf, u8 i_mode, return FALSE; } -#if TARGET_PC -static void daE_DB_interp_callback(bool isSimFrame, void* pUserWork) { - e_db_class* i_this = (e_db_class*)pUserWork; - if (!i_this->mStalkLineInterpPrevValid || !i_this->mStalkLineInterpCurrValid) { - return; - } - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - cXyz* dst = i_this->stalkLine.getPos(0); - for (int i = 0; i < 12; i++) { - const cXyz& p0 = i_this->mStalkLineInterpPrev[i]; - const cXyz& p1 = i_this->mStalkLineInterpCurr[i]; - dst[i] = p0 + (p1 - p0) * alpha; - } -} -#endif - static int daE_DB_Draw(e_db_class* i_this) { fopAc_ac_c* actor = &i_this->enemy; @@ -115,17 +95,6 @@ static int daE_DB_Draw(e_db_class* i_this) { static GXColor l_color = {0x14, 0x0F, 0x00, 0xFF}; i_this->stalkLine.update(12, l_color, &actor->tevStr); dComIfGd_set3DlineMat(&i_this->stalkLine); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->mStalkLineInterpCurrValid) { - memcpy(i_this->mStalkLineInterpPrev, i_this->mStalkLineInterpCurr, sizeof(i_this->mStalkLineInterpCurr)); - i_this->mStalkLineInterpPrevValid = true; - } - memcpy(i_this->mStalkLineInterpCurr, i_this->stalkLine.getPos(0), 12 * sizeof(cXyz)); - i_this->mStalkLineInterpCurrValid = true; - dusk::frame_interp::add_interpolation_callback(&daE_DB_interp_callback, i_this); - } -#endif for (int i = 1; i < 11; i++) { if (i_this->thornModel[i] != NULL) { diff --git a/src/d/actor/d_a_e_hb.cpp b/src/d/actor/d_a_e_hb.cpp index f0dfbda3a6..b39f4750a1 100644 --- a/src/d/actor/d_a_e_hb.cpp +++ b/src/d/actor/d_a_e_hb.cpp @@ -9,10 +9,6 @@ #include "d/actor/d_a_e_hb_leaf.h" #include "f_op/f_op_actor_enemy.h" -#if TARGET_PC -#include "dusk/frame_interpolation.h" -#endif - enum daE_HB_ACTION { ACTION_STAY, ACTION_APPEAR, @@ -68,22 +64,6 @@ static BOOL leaf_anm_init(e_hb_class* i_this, int i_anm, f32 i_morf, u8 i_mode, return FALSE; } -#if TARGET_PC -static void daE_HB_interp_callback(bool isSimFrame, void* pUserWork) { - e_hb_class* i_this = (e_hb_class*)pUserWork; - if (!i_this->mStalkLineInterpPrevValid || !i_this->mStalkLineInterpCurrValid) { - return; - } - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - cXyz* dst = i_this->stalkLine.getPos(0); - for (int i = 0; i < 12; i++) { - const cXyz& p0 = i_this->mStalkLineInterpPrev[i]; - const cXyz& p1 = i_this->mStalkLineInterpCurr[i]; - dst[i] = p0 + (p1 - p0) * alpha; - } -} -#endif - static int daE_HB_Draw(e_hb_class* i_this) { fopAc_ac_c* actor = &i_this->enemy; @@ -102,17 +82,6 @@ static int daE_HB_Draw(e_hb_class* i_this) { static GXColor l_color = {0x14, 0x0F, 0x00, 0xFF}; i_this->stalkLine.update(12, l_color, &actor->tevStr); dComIfGd_set3DlineMat(&i_this->stalkLine); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->mStalkLineInterpCurrValid) { - memcpy(i_this->mStalkLineInterpPrev, i_this->mStalkLineInterpCurr, sizeof(i_this->mStalkLineInterpCurr)); - i_this->mStalkLineInterpPrevValid = true; - } - memcpy(i_this->mStalkLineInterpCurr, i_this->stalkLine.getPos(0), 12 * sizeof(cXyz)); - i_this->mStalkLineInterpCurrValid = true; - dusk::frame_interp::add_interpolation_callback(&daE_HB_interp_callback, i_this); - } -#endif for (int i = 1; i < 11; i++) { if (i_this->thornModel[i] != NULL) { diff --git a/src/d/actor/d_a_e_mb.cpp b/src/d/actor/d_a_e_mb.cpp index 1fff337b33..9eaef0cc1c 100644 --- a/src/d/actor/d_a_e_mb.cpp +++ b/src/d/actor/d_a_e_mb.cpp @@ -12,8 +12,6 @@ #include "d/d_bomb.h" #include "c/c_damagereaction.h" #include "Z2AudioLib/Z2Instances.h" -#include "dusk/frame_interpolation.h" -#include "dusk/settings.h" #define ACTION_STANDBY 0 #define ACTION_WALK1 1 @@ -65,22 +63,6 @@ static void anm_init(e_mb_class* i_this, int i_anmID, f32 i_morf, u8 i_attr, f32 i_this->mAnm = i_anmID; } -#if TARGET_PC -static void e_mb_rope_interp_callback(bool isSimFrame, void* pUserWork) { - e_mb_class* i_this = (e_mb_class*)pUserWork; - if (!i_this->mRopeInterpPrevValid || !i_this->mRopeInterpCurrValid) { - return; - } - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - cXyz* dst = i_this->mRopeMat.getPos(0); - for (int i = 0; i < 16; i++) { - const cXyz& p0 = i_this->mRopeInterpPrev[i]; - const cXyz& p1 = i_this->mRopeInterpCurr[i]; - dst[i] = p0 + (p1 - p0) * alpha; - } -} -#endif - static int daE_MB_Draw(e_mb_class* i_this) { fopAc_ac_c* a_this = (fopAc_ac_c*)i_this; @@ -104,17 +86,6 @@ static int daE_MB_Draw(e_mb_class* i_this) { static GXColor l_color = {0x14, 0x0F, 0x00, 0xFF}; i_this->mRopeMat.update(16, l_color, &a_this->tevStr); dComIfGd_set3DlineMat(&i_this->mRopeMat); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->mRopeInterpCurrValid) { - memcpy(i_this->mRopeInterpPrev, i_this->mRopeInterpCurr, sizeof(i_this->mRopeInterpCurr)); - i_this->mRopeInterpPrevValid = true; - } - memcpy(i_this->mRopeInterpCurr, i_this->mRopeMat.getPos(0), 16 * sizeof(cXyz)); - i_this->mRopeInterpCurrValid = true; - dusk::frame_interp::add_interpolation_callback(&e_mb_rope_interp_callback, i_this); - } -#endif return 1; } diff --git a/src/d/actor/d_a_e_s1.cpp b/src/d/actor/d_a_e_s1.cpp index cd48a103e1..3c7c6700e5 100644 --- a/src/d/actor/d_a_e_s1.cpp +++ b/src/d/actor/d_a_e_s1.cpp @@ -14,8 +14,6 @@ #include "d/d_s_play.h" #include "f_op/f_op_actor_enemy.h" #include "f_op/f_op_camera_mng.h" -#include "dusk/frame_interpolation.h" -#include "dusk/settings.h" #include class daE_S1_HIO_c { @@ -101,32 +99,6 @@ static void anm_init(e_s1_class* i_this, int i_resNo, f32 i_morf, u8 i_attr, f32 i_this->mAnm = i_resNo; } -#if TARGET_PC -static void daE_S1_interp_callback(bool isSimFrame, void* pUserWork) { - e_s1_class* i_this = (e_s1_class*)pUserWork; - if (!i_this->mHairInterpPrevValid || !i_this->mHairInterpCurrValid) { - return; - } - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - for (int s = 0; s < e_s1_class::HAIR_STRAND_COUNT; s++) { - cXyz* dst = i_this->mLineMat.getPos(s); - for (int i = 0; i < e_s1_class::HAIR_SEGMENT_COUNT; i++) { - int idx = s * e_s1_class::HAIR_SEGMENT_COUNT + i; - const cXyz& p0 = i_this->mHairInterpPrev[idx]; - const cXyz& p1 = i_this->mHairInterpCurr[idx]; - dst[i] = p0 + (p1 - p0) * alpha; - } - } - GXColor line_color; - line_color.r = JREG_S(0) + 5; - line_color.g = JREG_S(1) + 10; - line_color.b = JREG_S(2) + 10; - line_color.a = 0xFF; - - i_this->mLineMat.update(16, line_color, &i_this->tevStr); -} -#endif - static int daE_S1_Draw(e_s1_class* i_this) { if (i_this->field_0x306c != 0) { return 1; @@ -160,22 +132,6 @@ static int daE_S1_Draw(e_s1_class* i_this) { i_this->mLineMat.update(16, line_color, &i_this->tevStr); dComIfGd_set3DlineMatDark(&i_this->mLineMat); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->mHairInterpCurrValid) { - memcpy(i_this->mHairInterpPrev, i_this->mHairInterpCurr, sizeof(i_this->mHairInterpCurr)); - i_this->mHairInterpPrevValid = true; - } - for (int s = 0; s < e_s1_class::HAIR_STRAND_COUNT; s++) { - cXyz* src = i_this->mLineMat.getPos(s); - memcpy(&i_this->mHairInterpCurr[s * e_s1_class::HAIR_SEGMENT_COUNT], src, - e_s1_class::HAIR_SEGMENT_COUNT * sizeof(cXyz)); - } - i_this->mHairInterpCurrValid = true; - dusk::frame_interp::add_interpolation_callback(&daE_S1_interp_callback, i_this); - } -#endif - dComIfGd_setList(); return 1; } @@ -2193,11 +2149,6 @@ static int daE_S1_Create(fopAc_ac_c* i_this) { return cPhs_ERROR_e; } -#if TARGET_PC - a_this->mHairInterpPrevValid = false; - a_this->mHairInterpCurrValid = false; -#endif - OS_REPORT("//////////////E_S1 SET 2 !!\n"); if (path_no != 0xFF) { diff --git a/src/d/actor/d_a_e_sm2.cpp b/src/d/actor/d_a_e_sm2.cpp index 808301527e..96f718ce0b 100644 --- a/src/d/actor/d_a_e_sm2.cpp +++ b/src/d/actor/d_a_e_sm2.cpp @@ -15,7 +15,7 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif class daE_SM2_HIO_c : public fOpAcm_HIO_entry_c { @@ -81,7 +81,7 @@ static int nodeCallBack(J3DJoint* i_joint, int param_1) { } #if TARGET_PC -static void daE_SM2_interp_callback(bool isSimFrame, void* pUserWork) { +static void daE_SM2_interp_callback(void* pUserWork) { e_sm2_class* i_this = static_cast(pUserWork); if (i_this == NULL) { return; @@ -133,7 +133,7 @@ static int daE_SM2_Draw(e_sm2_class* i_this) { fopAc_ac_c* actor = (fopAc_ac_c*)&i_this->enemy; #if TARGET_PC - dusk::frame_interp::add_interpolation_callback(&daE_SM2_interp_callback, i_this); + dusk::interp::add_interpolation_callback(&daE_SM2_interp_callback, i_this); #endif g_env_light.settingTevStruct(0, &actor->current.pos, &actor->tevStr); diff --git a/src/d/actor/d_a_e_wb.cpp b/src/d/actor/d_a_e_wb.cpp index a7f2e4f7b3..cf9e4ccbf7 100644 --- a/src/d/actor/d_a_e_wb.cpp +++ b/src/d/actor/d_a_e_wb.cpp @@ -18,10 +18,12 @@ #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_graphic.h" #include "res/Object/Always.h" -#include "dusk/dusk.h" -#include "dusk/frame_interpolation.h" #include +#if TARGET_PC +#include "dusk/dusk.h" +#include "dusk/interp/frame_interpolation.h" +#endif class daE_WB_HIO_c : public JORReflexible { public: @@ -186,30 +188,6 @@ static bool hio_set; static daE_WB_HIO_c l_HIO; -#if TARGET_PC -static void e_wb_rein_interp_callback(bool isSimFrame, void* pUserWork) { - e_wb_class* i_this = (e_wb_class*)pUserWork; - if (!i_this->himo_interp_prev_valid || !i_this->himo_interp_curr_valid) { - return; - } - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - for (int r = 0; r < 2; r++) { - cXyz* dst = i_this->himo_mat[r].getPos(0); - for (int i = 0; i < 16; i++) { - const cXyz& p0 = i_this->himo_mat_interp_prev[r][i]; - const cXyz& p1 = i_this->himo_mat_interp_curr[r][i]; - dst[i] = p0 + (p1 - p0) * alpha; - } - } - cXyz* dst = i_this->himo_tex.getPos(0); - for (int i = 0; i < 2; i++) { - const cXyz& p0 = i_this->himo_tex_interp_prev[i]; - const cXyz& p1 = i_this->himo_tex_interp_curr[i]; - dst[i] = p0 + (p1 - p0) * alpha; - } -} -#endif - static void himo_control1(e_wb_class* i_this, cXyz* i_pos, int i_no, s8 param_3) { fopEn_enemy_c* enemy = &i_this->enemy; cXyz mae, ato; @@ -534,21 +512,6 @@ static int daE_WB_Draw(e_wb_class* i_this) { dComIfGd_set3DlineMat(&i_this->himo_mat[1]); i_this->himo_tex.update(2, l_color, &actor->tevStr); dComIfGd_set3DlineMat(&i_this->himo_tex); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->himo_interp_curr_valid) { - memcpy(i_this->himo_mat_interp_prev, i_this->himo_mat_interp_curr, sizeof(i_this->himo_mat_interp_curr)); - memcpy(i_this->himo_tex_interp_prev, i_this->himo_tex_interp_curr, sizeof(i_this->himo_tex_interp_curr)); - i_this->himo_interp_prev_valid = true; - } - for (int r = 0; r < 2; r++) { - memcpy(i_this->himo_mat_interp_curr[r], i_this->himo_mat[r].getPos(0), 16 * sizeof(cXyz)); - } - memcpy(i_this->himo_tex_interp_curr, i_this->himo_tex.getPos(0), 2 * sizeof(cXyz)); - i_this->himo_interp_curr_valid = true; - dusk::frame_interp::add_interpolation_callback(&e_wb_rein_interp_callback, i_this); - } -#endif } return 1; @@ -4542,7 +4505,7 @@ static void demo_camera(e_wb_class* i_this) { i_this->demo_cam_way_spd.z = fabsf(i_this->demo_cam_way.z - i_this->demo_cam_ctr.z); i_this->demo_cam_morf = 0; pla->setPlayerPosAndAngle(&pla->current.pos, pla->shape_angle.y - 4000, 0); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } if (i_this->demo_timer == 345) { daPy_getPlayerActorClass()->setThrowDamage(boss->enemy.shape_angle.y - 8000 + TREG_S(8), @@ -4789,7 +4752,7 @@ static void demo_camera(e_wb_class* i_this) { i_this->demo_cam_eye.x += 300.0f + VREG_F(8); i_this->demo_cam_eye.y += 150.0f + VREG_F(9); i_this->demo_cam_eye.z -= 1400.0f + VREG_F(10); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } } else { i_this->demo_cam_eye = enemy->current.pos; @@ -5050,7 +5013,7 @@ static void demo_camera(e_wb_class* i_this) { i_this->demo_cam_sync_ticks = 2; } if (i_this->demo_cam_sync_ticks > 0) { - dusk::frame_interp::request_presentation_sync(); + dusk::interp::request_presentation_sync(); i_this->demo_cam_sync_ticks--; } #endif diff --git a/src/d/actor/d_a_e_yd.cpp b/src/d/actor/d_a_e_yd.cpp index 52a5be7fdc..bdc7c527e2 100644 --- a/src/d/actor/d_a_e_yd.cpp +++ b/src/d/actor/d_a_e_yd.cpp @@ -12,10 +12,6 @@ #include "d/d_cc_uty.h" #include "f_op/f_op_actor_enemy.h" -#if TARGET_PC -#include "dusk/frame_interpolation.h" -#endif - class daE_YD_HIO_c { public: daE_YD_HIO_c(); @@ -77,22 +73,6 @@ static s32 leaf_anm_init(e_yd_class* i_this, int param_1, f32 param_2, u8 param_ return false; } -#if TARGET_PC -static void daE_YD_interp_callback(bool isSimFrame, void* pUserWork) { - e_yd_class* i_this = (e_yd_class*)pUserWork; - if (!i_this->mLineMatInterpPrevValid || !i_this->mLineMatInterpCurrValid) { - return; - } - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - cXyz* dst = i_this->mLineMat.getPos(0); - for (int i = 0; i < 12; i++) { - const cXyz& p0 = i_this->mLineMatInterpPrev[i]; - const cXyz& p1 = i_this->mLineMatInterpCurr[i]; - dst[i] = p0 + (p1 - p0) * alpha; - } -} -#endif - static s32 daE_YD_Draw(e_yd_class* i_this) { static GXColor l_color = { 0x14, 0x0F, 0x00, 0xFF }; @@ -106,17 +86,6 @@ static s32 daE_YD_Draw(e_yd_class* i_this) { i_this->mpMorf->entryDL(); i_this->mLineMat.update(12, l_color, &i_this->actor.tevStr); dComIfGd_set3DlineMat(&i_this->mLineMat); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->mLineMatInterpCurrValid) { - memcpy(i_this->mLineMatInterpPrev, i_this->mLineMatInterpCurr, sizeof(i_this->mLineMatInterpCurr)); - i_this->mLineMatInterpPrevValid = true; - } - memcpy(i_this->mLineMatInterpCurr, i_this->mLineMat.getPos(0), 12 * sizeof(cXyz)); - i_this->mLineMatInterpCurrValid = true; - dusk::frame_interp::add_interpolation_callback(&daE_YD_interp_callback, i_this); - } -#endif for (s32 i = 1; i < 11; i++) { if (i_this->field_0x77c[i] != 0) { g_env_light.setLightTevColorType_MAJI(i_this->field_0x77c[i], &i_this->actor.tevStr); diff --git a/src/d/actor/d_a_e_yg.cpp b/src/d/actor/d_a_e_yg.cpp index 71756d6f57..e6bcd05098 100644 --- a/src/d/actor/d_a_e_yg.cpp +++ b/src/d/actor/d_a_e_yg.cpp @@ -10,8 +10,6 @@ #include "f_op/f_op_kankyo_mng.h" #include "d/actor/d_a_obj_carry.h" #include "Z2AudioLib/Z2Instances.h" -#include "dusk/frame_interpolation.h" -#include "dusk/settings.h" #include "f_op/f_op_actor_enemy.h" enum E_yg_RES_File_ID { @@ -136,33 +134,6 @@ static BOOL pl_check(e_yg_class* i_this, f32 i_dist) { return FALSE; } -#if TARGET_PC -static void daE_YG_interp_callback(bool isSimFrame, void* pUserWork) { - e_yg_class* i_this = (e_yg_class*)pUserWork; - fopAc_ac_c* actor = (fopAc_ac_c*)&i_this->actor; - if (!i_this->mTentacleInterpPrevValid || !i_this->mTentacleInterpCurrValid) { - return; - } - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - for (int s = 0; s < e_yg_class::TENTACLE_STRAND_COUNT; s++) { - cXyz* dst = i_this->mLineMat.getPos(s); - for (int i = 0; i < e_yg_class::TENTACLE_SEGMENT_COUNT; i++) { - int idx = s * e_yg_class::TENTACLE_SEGMENT_COUNT + i; - const cXyz& p0 = i_this->mTentacleInterpPrev[idx]; - const cXyz& p1 = i_this->mTentacleInterpCurr[idx]; - dst[i] = p0 + (p1 - p0) * alpha; - } - } - GXColor color; - color.r = JREG_S(0) + 20; - color.g = JREG_S(1) + 20; - color.b = JREG_S(2) + 20; - color.a = 0xFF; - - i_this->mLineMat.update(10, color, &actor->tevStr); -} -#endif - static int daE_YG_Draw(e_yg_class* i_this) { if (i_this->mDispFlag) { return 1; @@ -190,22 +161,6 @@ static int daE_YG_Draw(e_yg_class* i_this) { i_this->mLineMat.update(10, color, &actor->tevStr); dComIfGd_set3DlineMatDark(&i_this->mLineMat); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->mTentacleInterpCurrValid) { - memcpy(i_this->mTentacleInterpPrev, i_this->mTentacleInterpCurr, sizeof(i_this->mTentacleInterpCurr)); - i_this->mTentacleInterpPrevValid = true; - } - for (int s = 0; s < e_yg_class::TENTACLE_STRAND_COUNT; s++) { - cXyz* src = i_this->mLineMat.getPos(s); - memcpy(&i_this->mTentacleInterpCurr[s * e_yg_class::TENTACLE_SEGMENT_COUNT], src, - e_yg_class::TENTACLE_SEGMENT_COUNT * sizeof(cXyz)); - } - i_this->mTentacleInterpCurrValid = true; - dusk::frame_interp::add_interpolation_callback(&daE_YG_interp_callback, i_this); - } -#endif - dComIfGd_setList(); return 1; @@ -1424,11 +1379,6 @@ static cPhs_Step daE_YG_Create(fopAc_ac_c* actor) { return cPhs_ERROR_e; } -#if TARGET_PC - i_this->mTentacleInterpPrevValid = false; - i_this->mTentacleInterpCurrValid = false; -#endif - if (!hio_set) { i_this->mIsFirstSpawn = 1; hio_set = true; diff --git a/src/d/actor/d_a_e_yh.cpp b/src/d/actor/d_a_e_yh.cpp index fb00a4bab1..55f175d78e 100644 --- a/src/d/actor/d_a_e_yh.cpp +++ b/src/d/actor/d_a_e_yh.cpp @@ -12,10 +12,6 @@ #include "f_op/f_op_actor_enemy.h" #include "f_op/f_op_kankyo_mng.h" -#if TARGET_PC -#include "dusk/frame_interpolation.h" -#endif - class daE_YH_HIO_c : public JORReflexible { public: daE_YH_HIO_c(); @@ -89,22 +85,6 @@ static BOOL leaf_anm_init(e_yh_class* i_this, int param_2, f32 param_3, u8 param return FALSE; } -#if TARGET_PC -static void daE_YH_interp_callback(bool isSimFrame, void* pUserWork) { - e_yh_class* i_this = (e_yh_class*)pUserWork; - if (!i_this->mLineInterpPrevValid || !i_this->mLineInterpCurrValid) { - return; - } - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - cXyz* dst = i_this->mLine.getPos(0); - for (int i = 0; i < 12; i++) { - const cXyz& p0 = i_this->mLineInterpPrev[i]; - const cXyz& p1 = i_this->mLineInterpCurr[i]; - dst[i] = p0 + (p1 - p0) * alpha; - } -} -#endif - static int daE_YH_Draw(e_yh_class* i_this) { fopAc_ac_c* a_this = (fopAc_ac_c*)i_this; @@ -134,17 +114,6 @@ static int daE_YH_Draw(e_yh_class* i_this) { i_this->mLine.update(12, l_color, &a_this->tevStr); dComIfGd_set3DlineMat(&i_this->mLine); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->mLineInterpCurrValid) { - memcpy(i_this->mLineInterpPrev, i_this->mLineInterpCurr, sizeof(i_this->mLineInterpCurr)); - i_this->mLineInterpPrevValid = true; - } - memcpy(i_this->mLineInterpCurr, i_this->mLine.getPos(0), 12 * sizeof(cXyz)); - i_this->mLineInterpCurrValid = true; - dusk::frame_interp::add_interpolation_callback(&daE_YH_interp_callback, i_this); - } -#endif for (int i = 1; i < 11; i++) { if (i_this->mModels[i] != NULL) { diff --git a/src/d/actor/d_a_horse.cpp b/src/d/actor/d_a_horse.cpp index 72b1fb1e7b..b738460bb8 100644 --- a/src/d/actor/d_a_horse.cpp +++ b/src/d/actor/d_a_horse.cpp @@ -20,23 +20,6 @@ #include #include -#if TARGET_PC -#include "dusk/dusk.h" -#include "dusk/frame_interpolation.h" - -namespace { -// FRAME INTERP NOTE: Sim tick control point snapshots for interpolation -constexpr int kHorseReinSimMax = 75; -cXyz s_horseReinSimPrev[kHorseReinSimMax]; -cXyz s_horseReinSimCurr[kHorseReinSimMax]; -int s_horseReinSimNumPrev; -int s_horseReinSimNumCurr; -bool s_horseReinSimPrevValid; -bool s_horseReinSimCurrValid; -uint64_t s_horseReinSimRolledSeq; -} // namespace -#endif - #define ANM_HS_BACK_WALK 6 #define ANM_HS_WALK_START 7 #define ANM_HS_EXCITEMENT 8 @@ -3033,24 +3016,6 @@ void daHorse_c::copyReinPos() { for (i = rein->field_0x8[0] - 1; i >= 0; i--, pos_p++) { *pos_p = rein->field_0x0[0][i]; } -#if TARGET_PC - if (field_0x1204 > 0) { - const uint64_t simSeq = dusk::frame_interp::sim_tick_seq(); - if (simSeq != s_horseReinSimRolledSeq) { - s_horseReinSimRolledSeq = simSeq; - if (s_horseReinSimCurrValid && s_horseReinSimNumCurr > 0) { - memcpy(s_horseReinSimPrev, s_horseReinSimCurr, s_horseReinSimNumCurr * sizeof(cXyz)); - s_horseReinSimNumPrev = s_horseReinSimNumCurr; - s_horseReinSimPrevValid = true; - } - } - memcpy(s_horseReinSimCurr, m_reinLine.getPos(0), field_0x1204 * sizeof(cXyz)); - s_horseReinSimNumCurr = field_0x1204; - s_horseReinSimCurrValid = true; - } else { - s_horseReinSimCurrValid = false; - } -#endif } void daHorse_c::setReinPosHandSubstance(int param_0) { @@ -3162,30 +3127,6 @@ void daHorse_c::setReinPosNormalSubstance() { copyReinPos(); } -#if TARGET_PC -void daHorse_c::lerpControlPoints(f32 alpha) { - // FRAME INTERP NOTE: Currently only lerping points for Epona's reins. Need a more global solution. - if (!dusk::frame_interp::is_enabled() || !s_horseReinSimPrevValid || !s_horseReinSimCurrValid) { - return; - } - const int nCurr = s_horseReinSimNumCurr; - const int nPrev = s_horseReinSimNumPrev; - if (nCurr <= 0) { - return; - } - int n = nPrev < nCurr ? nPrev : nCurr; - if (n <= 0 || n > kHorseReinSimMax) { - return; - } - cXyz* dst = m_reinLine.getPos(0); - for (int i = 0; i < n; i++) { - const cXyz& p0 = s_horseReinSimPrev[i]; - const cXyz& p1 = s_horseReinSimCurr[i]; - dst[i] = p0 + (p1 - p0) * alpha; - } -} -#endif - void daHorse_c::bgCheck() { if (m_procID != PROC_LARGE_DAMAGE_e) { static DUSK_CONSTEXPR cXyz localCenterPos(0.0f, 100.0f, 0.0f); diff --git a/src/d/actor/d_a_mant.cpp b/src/d/actor/d_a_mant.cpp index 3f9c2deae1..4f175e794f 100644 --- a/src/d/actor/d_a_mant.cpp +++ b/src/d/actor/d_a_mant.cpp @@ -12,7 +12,7 @@ #if TARGET_PC #include "dusk/dvd_asset.hpp" -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include @@ -212,8 +212,8 @@ void daMant_packet_c::draw() { MtxP src25 = model->getAnmMtx(25); Mtx joint_34_scratch; Mtx joint_25_scratch; - MtxP joint_34 = dusk::frame_interp::lookup_replacement(src34, joint_34_scratch) ? joint_34_scratch : src34; - MtxP joint_25 = dusk::frame_interp::lookup_replacement(src25, joint_25_scratch) ? joint_25_scratch : src25; + MtxP joint_34 = dusk::interp::lookup_replacement(src34, joint_34_scratch) ? joint_34_scratch : src34; + MtxP joint_25 = dusk::interp::lookup_replacement(src25, joint_25_scratch) ? joint_25_scratch : src25; cXyz presented_anchor_a; cXyz presented_anchor_b; @@ -232,7 +232,7 @@ void daMant_packet_c::draw() { } } - const f32 step = dusk::frame_interp::get_interpolation_step(); + const f32 step = dusk::interp::get_interpolation_step(); for (int i = 0; i < 169; ++i) { cXyz curr_local; MTXMultVec(curr_frame_inverse, &curr_pos[i], &curr_local); diff --git a/src/d/actor/d_a_mg_rod.cpp b/src/d/actor/d_a_mg_rod.cpp index 6cf1e76024..53dba36b74 100644 --- a/src/d/actor/d_a_mg_rod.cpp +++ b/src/d/actor/d_a_mg_rod.cpp @@ -26,7 +26,6 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" #include "dusk/mods/item.hpp" #include "dusk/settings.h" #include "dusk/version.hpp" @@ -183,25 +182,6 @@ static int Worm_nodeCallBack(J3DJoint* i_joint, int param_1) { return 1; } -#if TARGET_PC -static void dmg_rod_interp_callback(bool isSimFrame, void* pUserWork) { - dmg_rod_class* i_this = (dmg_rod_class*)pUserWork; - if (!i_this->mLineInterpPrevValid || !i_this->mLineInterpCurrValid) { - return; - } - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - const int count = i_this->kind == MG_ROD_KIND_LURE ? MG_ROD_LURE_LINE_LEN : MG_ROD_UKI_LINE_LEN; - cXyz* dst = i_this->linemat.getPos(0); - for (int i = 0; i < count; i++) { - const cXyz& p0 = i_this->mLineInterpPrev[i]; - const cXyz& p1 = i_this->mLineInterpCurr[i]; - dst[i] = p0 + (p1 - p0) * alpha; - } - static GXColor l_color = {0xFF, 0xFF, 0x96, 0xFF}; - i_this->linemat.update(count, l_color, &i_this->actor.tevStr); -} -#endif - static int dmg_rod_Draw(dmg_rod_class* i_this) { int unused; fopAc_ac_c* actor = &i_this->actor; @@ -242,18 +222,6 @@ static int dmg_rod_Draw(dmg_rod_class* i_this) { i_this->linemat.update(MG_ROD_LURE_LINE_LEN, l_color, &i_this->actor.tevStr); dComIfGd_set3DlineMat(&i_this->linemat); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->mLineInterpCurrValid) { - memcpy(i_this->mLineInterpPrev, i_this->mLineInterpCurr, MG_ROD_LURE_LINE_LEN * sizeof(cXyz)); - i_this->mLineInterpPrevValid = true; - } - memcpy(i_this->mLineInterpCurr, i_this->linemat.getPos(0), MG_ROD_LURE_LINE_LEN * sizeof(cXyz)); - i_this->mLineInterpCurrValid = true; - dusk::frame_interp::add_interpolation_callback(&dmg_rod_interp_callback, i_this); - } -#endif - model = i_this->rod_modelMorf->getModel(); g_env_light.setLightTevColorType_MAJI(model, &i_this->actor.tevStr); i_this->rod_modelMorf->entryDL(); @@ -278,18 +246,6 @@ static int dmg_rod_Draw(dmg_rod_class* i_this) { i_this->linemat.update(MG_ROD_UKI_LINE_LEN, l_color, &i_this->actor.tevStr); dComIfGd_set3DlineMat(&i_this->linemat); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->mLineInterpCurrValid) { - memcpy(i_this->mLineInterpPrev, i_this->mLineInterpCurr, MG_ROD_UKI_LINE_LEN * sizeof(cXyz)); - i_this->mLineInterpPrevValid = true; - } - memcpy(i_this->mLineInterpCurr, i_this->linemat.getPos(0), MG_ROD_UKI_LINE_LEN * sizeof(cXyz)); - i_this->mLineInterpCurrValid = true; - dusk::frame_interp::add_interpolation_callback(&dmg_rod_interp_callback, i_this); - } -#endif - for (int i = 0; i < 15; i++) { g_env_light.setLightTevColorType_MAJI(i_this->rod_uki_model[i], &actor->tevStr); mDoExt_modelUpdateDL(i_this->rod_uki_model[i]); @@ -6481,11 +6437,6 @@ static int dmg_rod_Create(fopAc_ac_c* i_this) { return cPhs_ERROR_e; } -#if TARGET_PC - rod->mLineInterpPrevValid = false; - rod->mLineInterpCurrValid = false; -#endif - OS_REPORT("//////////////MG_ROD SET 2 !!\n"); if (!hio_set) { rod->HIOInit = TRUE; diff --git a/src/d/actor/d_a_midna.cpp b/src/d/actor/d_a_midna.cpp index 47d3da98c8..794dbe1d64 100644 --- a/src/d/actor/d_a_midna.cpp +++ b/src/d/actor/d_a_midna.cpp @@ -16,7 +16,7 @@ #include "d/d_debug_viewer.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif static f32 dummy_lit_3777(int idx, u8 foo) { @@ -1110,10 +1110,10 @@ void daMidna_c::setBodyPartMatrix() { mpModel->setAnmMtx(i, mpShadowModel->getAnmMtx(i)); } mpModel->calcWeightEnvelopeMtx(); -#ifdef TARGET_PC +#if TARGET_PC // FRAME INTERP NOTE: Record weight envelopes for Midna here, as they are otherwise missed causing distortion for (u16 i = 0; i < mpModel->getModelData()->getWEvlpMtxNum(); i++) { - dusk::frame_interp::record_final_mtx(mpModel->getWeightAnmMtx(i)); + dusk::interp::record_final_mtx(mpModel->getWeightAnmMtx(i)); } #endif } diff --git a/src/d/actor/d_a_mirror.cpp b/src/d/actor/d_a_mirror.cpp index d02d81d834..123279e8af 100644 --- a/src/d/actor/d_a_mirror.cpp +++ b/src/d/actor/d_a_mirror.cpp @@ -14,10 +14,6 @@ #include #include "m_Do/m_Do_lib.h" -#if TARGET_PC -#include "dusk/frame_interpolation.h" -#endif - static BOOL daMirror_c_createHeap(fopAc_ac_c* i_this) { return ((daMirror_c*)i_this)->createHeap(); } diff --git a/src/d/actor/d_a_npc_ne.cpp b/src/d/actor/d_a_npc_ne.cpp index de48a76851..530654400f 100644 --- a/src/d/actor/d_a_npc_ne.cpp +++ b/src/d/actor/d_a_npc_ne.cpp @@ -21,7 +21,7 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif static home_path_pnt home_path[38] = { @@ -2659,7 +2659,7 @@ static void demo_camera(npc_ne_class* i_this) { i_this->mCameraFovY = 55.0f; camera->mCamera.SetTrimSize(3); daPy_getPlayerActorClass()->changeOriginalDemo(); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); // fallthrough case 2: @@ -2688,7 +2688,7 @@ static void demo_camera(npc_ne_class* i_this) { if (i_this->mDemoCounter == 0) { i_this->mCameraCenter1.set(387.0f, 133.0f, -866.0f); i_this->mCameraEye1.set(284.0f, 208.0f, -585.0f); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } if (i_this->mDemoCounter == 12) { @@ -2725,7 +2725,7 @@ static void demo_camera(npc_ne_class* i_this) { i_this->mCameraFovY = 45.0f; camera->mCamera.SetTrimSize(3); daPy_getPlayerActorClass()->changeOriginalDemo(); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); // fallthrough case 11: @@ -2806,10 +2806,10 @@ static void demo_camera(npc_ne_class* i_this) { MtxPosition(&vec, &i_this->mCameraEye2); i_this->mCameraEye2 += player->current.pos; player->changeDemoParam2(2); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } else if (i_this->mDemoCounter == 120) { player->changeDemoParam2(0); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } } } @@ -2862,7 +2862,7 @@ static void demo_camera(npc_ne_class* i_this) { i_this->mCameraCenter1 = _this->current.pos; i_this->mCameraCenter1.y += 20.0f; i_this->mCameraFovY = 55.0f; - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } camera->mCamera.Set(i_this->mCameraCenter1, i_this->mCameraEye1, diff --git a/src/d/actor/d_a_npc_toby.cpp b/src/d/actor/d_a_npc_toby.cpp index 120cfac318..d42787e2a2 100644 --- a/src/d/actor/d_a_npc_toby.cpp +++ b/src/d/actor/d_a_npc_toby.cpp @@ -17,7 +17,7 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif DUSK_GAME_DATA const daNpc_Toby_HIOParam daNpc_Toby_Param_c::m = { @@ -1402,7 +1402,7 @@ int daNpc_Toby_c::cutRepairSCannon(int arg0) { old.pos = current.pos; setAngle(cM_deg2s(5.0f * f32(mPath.getArg0()))); mEventTimer = mPath.getArg2(); - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } } else if (!mHide) { mHide = 1; diff --git a/src/d/actor/d_a_npc_zra.cpp b/src/d/actor/d_a_npc_zra.cpp index 9d97a3ac2c..f77200c514 100644 --- a/src/d/actor/d_a_npc_zra.cpp +++ b/src/d/actor/d_a_npc_zra.cpp @@ -20,6 +20,10 @@ #include "d/actor/d_a_obj_zraMark.h" #include +#if TARGET_PC +#include "dusk/interp/frame_interpolation.h" +#endif + static NPC_ZRA_HIO_CLASS l_HIO; DUSK_GAME_DATA daNpc_zrA_HIOParam const daNpc_zrA_Param_c::m = { @@ -686,6 +690,69 @@ int daNpc_zrA_c::Execute() { return execute(); } +#if TARGET_PC +void daNpc_zrA_interp_callback(void* pUserWork) { + daNpc_zrA_c* i_this = static_cast(pUserWork); + if (i_this == NULL || i_this->mAnm_p == NULL || i_this->checkHide()) { + return; + } + + J3DModel* model = i_this->mAnm_p->getModel(); + if (model == NULL) { + return; + } + + J3DModelData* model_data = model->getModelData(); + model_data->getMaterialNodePointer(1)->setMaterialAnm(i_this->mpMatAnm); + + if (i_this->mTwilight) { + g_env_light.settingTevStruct(4, &i_this->current.pos, &i_this->tevStr); + } else { + g_env_light.settingTevStruct(0, &i_this->current.pos, &i_this->tevStr); + } + g_env_light.setLightTevColorType_MAJI(model, &i_this->tevStr); + + if (i_this->mWaterAnmFlags & daNpcF_c::ANM_PLAY_BTK) { + i_this->mWaterBtkAnm.entry(model_data); + } + if (i_this->mWaterAnmFlags & daNpcF_c::ANM_PLAY_BPK) { + i_this->mWaterBpkAnm.entry(model_data); + } + if (i_this->mAnmFlags & daNpcF_c::ANM_PLAY_BTP) { + i_this->mBtpAnm.entry(model_data); + } + if (i_this->mAnmFlags & daNpcF_c::ANM_PLAY_BTK) { + i_this->mBtkAnm.entry(model_data); + } + if (i_this->mAnmFlags & daNpcF_c::ANM_PLAY_BRK) { + i_this->mBrkAnm.entry(model_data); + } + + if (!i_this->mHide && !i_this->mTwilight) { + fopAcM_setEffectMtx(i_this, model_data); + } + + model->calcMaterial(); + model->diff(); + + if (i_this->mAnmFlags & daNpcF_c::ANM_PLAY_BTP) { + i_this->mBtpAnm.remove(model_data); + } + if (i_this->mAnmFlags & daNpcF_c::ANM_PLAY_BTK) { + i_this->mBtkAnm.remove(model_data); + } + if (i_this->mAnmFlags & daNpcF_c::ANM_PLAY_BRK) { + i_this->mBrkAnm.remove(model_data); + } + if (i_this->mWaterAnmFlags & daNpcF_c::ANM_PLAY_BPK) { + i_this->mWaterBpkAnm.remove(model_data); + } + if (i_this->mWaterAnmFlags & daNpcF_c::ANM_PLAY_BTK) { + i_this->mWaterBtkAnm.remove(model_data); + } +} +#endif + int daNpc_zrA_c::Draw() { BOOL bvar2 = false; J3DModel* model = mAnm_p->getModel(); @@ -737,6 +804,8 @@ int daNpc_zrA_c::Draw() { mAnm_p->entryDL(); } + IF_DUSK(dusk::interp::add_interpolation_callback(&daNpc_zrA_interp_callback, this)); + if (mAnmFlags & ANM_PLAY_BTP) { mBtpAnm.remove(model_data); } diff --git a/src/d/actor/d_a_obj_fchain.cpp b/src/d/actor/d_a_obj_fchain.cpp index b4d473c424..cf2470fc17 100644 --- a/src/d/actor/d_a_obj_fchain.cpp +++ b/src/d/actor/d_a_obj_fchain.cpp @@ -10,10 +10,15 @@ #include "JSystem/J3DGraphBase/J3DDrawBuffer.h" #include "SSystem/SComponent/c_math.h" #include "d/d_com_inf_game.h" -#include "dusk/frame_interpolation.h" -#include "dusk/settings.h" #include +#if TARGET_PC +#include "dusk/interp/dual_buffer.h" + +static const int CHAIN_COUNT = 22; +typedef dusk::interp::DualBuffer ChainInterp; +#endif + static char const l_arcName[] = "Fchain"; int daObjFchain_c::createHeap() { @@ -67,10 +72,6 @@ int daObjFchain_c::create() { local_48++; } rv = cPhs_COMPLEATE_e; -#if TARGET_PC - mChainInterpPrevValid = false; - mChainInterpCurrValid = false; -#endif break; } return rv; @@ -295,26 +296,6 @@ void daObjFchain_shape_c::draw() { } } -#if TARGET_PC -static void fchain_interp_callback(bool isSimFrame, void* pUserWork) { - static_cast(pUserWork)->onInterpCallback(); -} - -void daObjFchain_c::onInterpCallback() { - if (!mChainInterpPrevValid || !mChainInterpCurrValid) { - return; - } - - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - - for (int i = 0; i < CHAIN_COUNT; i++) { - const cXyz& p0 = mChainInterpPrev[i]; - const cXyz& p1 = mChainInterpCurr[i]; - field_0x694[i] = p0 + (p1 - p0) * alpha; - } -} -#endif - int daObjFchain_c::draw() { if (field_0x584 != 0) { g_env_light.settingTevStruct(0, ¤t.pos, &tevStr); @@ -324,18 +305,7 @@ int daObjFchain_c::draw() { } dComIfGd_getOpaListDark()->entryImm(&mShape, 0); -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (mChainInterpCurrValid) { - memcpy(mChainInterpPrev, mChainInterpCurr, sizeof(mChainInterpCurr)); - mChainInterpPrevValid = true; - } - - memcpy(mChainInterpCurr, field_0x694, sizeof(mChainInterpCurr)); - mChainInterpCurrValid = true; - dusk::frame_interp::add_interpolation_callback(&fchain_interp_callback, this); - } -#endif + IF_DUSK(dusk::interp::get(this).writeback(field_0x694, CHAIN_COUNT)); } return 1; } diff --git a/src/d/actor/d_a_obj_item.cpp b/src/d/actor/d_a_obj_item.cpp index e06eb37e50..cfd7963207 100644 --- a/src/d/actor/d_a_obj_item.cpp +++ b/src/d/actor/d_a_obj_item.cpp @@ -17,7 +17,7 @@ #include "m_Do/m_Do_mtx.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif static f32 Reflect(cXyz* i_vec, cBgS_PolyInfo const& i_polyinfo, f32 i_scale) { @@ -36,7 +36,7 @@ static f32 Reflect(cXyz* i_vec, cBgS_PolyInfo const& i_polyinfo, f32 i_scale) { } #if TARGET_PC -static void d_a_obj_item_interp_callback(bool isSimFrame, void* pUserWork) { +static void d_a_obj_item_interp_callback(void* pUserWork) { daItem_c* item = static_cast(pUserWork); if (item == NULL || item->mpModel == NULL || !item->chkDraw()) { return; @@ -206,7 +206,7 @@ void daItem_c::CreateInit() { initBaseMtx(); animPlay(1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f); - if (m_itemNo == dItemNo_BOOMERANG_e IF_DUSK(&&!mItemOverridden)) { + if (DUSK_IF_ELSE(mOriginalItemNo, m_itemNo) == dItemNo_BOOMERANG_e) { itemGetNextExecute(); } else if ((m_itemNo == dItemNo_ORANGE_RUPEE_e || m_itemNo == dItemNo_SILVER_RUPEE_e) && mSparkleEmtr.getEmitter() == NULL) @@ -412,7 +412,7 @@ int daItem_c::_daItem_draw() { } #if TARGET_PC - dusk::frame_interp::add_interpolation_callback(&d_a_obj_item_interp_callback, this); + dusk::interp::add_interpolation_callback(&d_a_obj_item_interp_callback, this); #endif if (chkDraw()) { @@ -563,7 +563,7 @@ void daItem_c::procWaitGetDemoEvent() { dComIfGp_event_setItemPartnerId(m_item_id); } } else { - if (m_itemNo == dItemNo_BOOMERANG_e IF_DUSK(&&!mItemOverridden)) { + if (DUSK_IF_ELSE(mOriginalItemNo, m_itemNo) == dItemNo_BOOMERANG_e) { fopAcM_orderItemEvent(this, 0, 0); eventInfo.onCondition(dEvtCnd_CANGETITEM_e); return; @@ -787,32 +787,44 @@ void daItem_c::mode_wait() { mAcch.SetGrndNone(); } - switch (m_itemNo) { - case dItemNo_HEART_e: - itemActionForHeart(); - break; - case dItemNo_ARROW_10_e: - case dItemNo_ARROW_20_e: - case dItemNo_ARROW_30_e: - case dItemNo_ARROW_1_e: - case dItemNo_PACHINKO_SHOT_e: - case dItemNo_LIGHT_ARROW_e: - itemActionForArrow(); - break; - case dItemNo_BOOMERANG_e: +#if TARGET_PC + if (mOriginalItemNo == dItemNo_BOOMERANG_e) { itemActionForBoomerang(); - break; - case dItemNo_GREEN_RUPEE_e: - case dItemNo_BLUE_RUPEE_e: - case dItemNo_YELLOW_RUPEE_e: - case dItemNo_RED_RUPEE_e: - case dItemNo_PURPLE_RUPEE_e: - case dItemNo_ORANGE_RUPEE_e: - case dItemNo_SILVER_RUPEE_e: - default: - itemActionForRupee(); - break; + } else { +#endif + switch (m_itemNo) { + case dItemNo_HEART_e: + itemActionForHeart(); + break; + case dItemNo_ARROW_10_e: + case dItemNo_ARROW_20_e: + case dItemNo_ARROW_30_e: + case dItemNo_ARROW_1_e: + case dItemNo_PACHINKO_SHOT_e: + case dItemNo_LIGHT_ARROW_e: + itemActionForArrow(); + break; + case dItemNo_BOOMERANG_e: + // The boomerang check is already handled above, so if we got here, it's guaranteed to be + // an override. Fallthrough to the rupee action +#if !TARGET_PC + itemActionForBoomerang(); + break; +#endif + case dItemNo_GREEN_RUPEE_e: + case dItemNo_BLUE_RUPEE_e: + case dItemNo_YELLOW_RUPEE_e: + case dItemNo_RED_RUPEE_e: + case dItemNo_PURPLE_RUPEE_e: + case dItemNo_ORANGE_RUPEE_e: + case dItemNo_SILVER_RUPEE_e: + default: + itemActionForRupee(); + break; + } +#if TARGET_PC } +#endif if (field_0x9c0 == 0 && mAcch.ChkWaterHit() && mAcch.m_wtr.GetHeight() > current.pos.y) { mode_water_init(); @@ -868,55 +880,64 @@ void daItem_c::itemGetNextExecute() { setFlag(FLAG_INIT_GET_ITEM_e); BOOL haveItem = false; - switch (m_itemNo) { - case dItemNo_HEART_e: - case dItemNo_GREEN_RUPEE_e: - case dItemNo_ARROW_10_e: - case dItemNo_ARROW_20_e: - case dItemNo_ARROW_30_e: - case dItemNo_ARROW_1_e: - procInitSimpleGetDemo(); - itemGet(); - break; - case dItemNo_BLUE_RUPEE_e: - case dItemNo_YELLOW_RUPEE_e: - case dItemNo_RED_RUPEE_e: - case dItemNo_PURPLE_RUPEE_e: - case dItemNo_ORANGE_RUPEE_e: - case dItemNo_SILVER_RUPEE_e: - case dItemNo_PACHINKO_SHOT_e: - if (daPy_getPlayerActorClass()->checkCanoeRide() || - daPy_getPlayerActorClass()->checkHorseRide()) - { - if (checkItemGet(m_itemNo, 1)) { - haveItem = true; - } - procInitSimpleGetDemo(); - itemGet(); - - if (!haveItem) { - dComIfGs_offItemFirstBit(m_itemNo); - } - } else if (!checkItemGet(m_itemNo, 1)) { - procInitGetDemoEvent(); - } else { - procInitSimpleGetDemo(); - itemGet(); - } - break; - case dItemNo_BOOMERANG_e: - procInitGetDemoEvent(); - break; - default: #if TARGET_PC - if (mItemOverridden) { + // Always call demo event for the original boomerang check + if (mOriginalItemNo == dItemNo_BOOMERANG_e) { + procInitGetDemoEvent(); + } else { +#endif + switch (m_itemNo) { + case dItemNo_HEART_e: + case dItemNo_GREEN_RUPEE_e: + case dItemNo_ARROW_10_e: + case dItemNo_ARROW_20_e: + case dItemNo_ARROW_30_e: + case dItemNo_ARROW_1_e: + procInitSimpleGetDemo(); + itemGet(); + break; + case dItemNo_BLUE_RUPEE_e: + case dItemNo_YELLOW_RUPEE_e: + case dItemNo_RED_RUPEE_e: + case dItemNo_PURPLE_RUPEE_e: + case dItemNo_ORANGE_RUPEE_e: + case dItemNo_SILVER_RUPEE_e: + case dItemNo_PACHINKO_SHOT_e: + if (daPy_getPlayerActorClass()->checkCanoeRide() || + daPy_getPlayerActorClass()->checkHorseRide()) + { + if (checkItemGet(m_itemNo, 1)) { + haveItem = true; + } + procInitSimpleGetDemo(); + itemGet(); + + if (!haveItem) { + dComIfGs_offItemFirstBit(m_itemNo); + } + } else if (!checkItemGet(m_itemNo, 1)) { + procInitGetDemoEvent(); + } else { + procInitSimpleGetDemo(); + itemGet(); + } + break; + case dItemNo_BOOMERANG_e: procInitGetDemoEvent(); break; - } + default: +#if TARGET_PC + if (mItemOverridden) { + procInitGetDemoEvent(); + break; + } #endif - // "[daItem_c] Get process not defined[%d]\n" - OS_REPORT_ERROR("[daItem_c]ゲット処理が定義されていません[%d]\n", m_itemNo); + // "[daItem_c] Get process not defined[%d]\n" + OS_REPORT_ERROR("[daItem_c]ゲット処理が定義されていません[%d]\n", m_itemNo); + } +#if TARGET_PC } +#endif fopAcM_onItem(this, mItemBitNo); mCcCyl.SetTgType(0); @@ -968,6 +989,13 @@ void daItem_c::itemGet() { execItemGet(m_itemNo IF_DUSK_ARG(mItemGiveTag) IF_DUSK_ARG(this)); break; case dItemNo_BOOMERANG_e: +#if TARGET_PC + if (mItemOverridden) { + mDoAud_seStart(Z2SE_CONSUMP_ITEM_GET, NULL, 0, 0); + execItemGet(m_itemNo, mItemGiveTag, this); + break; + } +#endif break; case dItemNo_ARROW_10_e: case dItemNo_ARROW_20_e: @@ -1297,7 +1325,7 @@ void daItem_c::initSpeed(BOOL i_noTypeChk) { u8 type = daItem_prm::getType(this); if (!i_noTypeChk) { - if (type == TYPE_WAIT_e || type == TYPE_BOOM_HIT_e || m_itemNo == dItemNo_BOOMERANG_e) { + if (type == TYPE_WAIT_e || type == TYPE_BOOM_HIT_e || DUSK_IF_ELSE(mOriginalItemNo, m_itemNo) == dItemNo_BOOMERANG_e) { y_speed = 0.0f; speedf = 0.0f; } else if (type == TYPE_LAUNCH_NO_RND_e || type == TYPE_FIXED_PLACE_e) { diff --git a/src/d/actor/d_a_obj_keyhole.cpp b/src/d/actor/d_a_obj_keyhole.cpp index d56f39f40b..8b120f77e3 100644 --- a/src/d/actor/d_a_obj_keyhole.cpp +++ b/src/d/actor/d_a_obj_keyhole.cpp @@ -10,9 +10,6 @@ #include "d/d_s_play.h" #include "d/actor/d_a_player.h" #include "Z2AudioLib/Z2Instances.h" -#if TARGET_PC -#include "dusk/frame_interpolation.h" -#endif daObj_Keyhole_HIO_c::daObj_Keyhole_HIO_c() { id = -1; @@ -56,21 +53,6 @@ static int daObj_Keyhole_Draw(obj_keyhole_class* i_this) { for (int i = 0; i < 6; i++) { kh_chain_s* chain_s = &i_this->chain_s[i]; for (int j = 0; j < i_this->chain_num; j++) { -#if TARGET_PC - if (dusk::frame_interp::is_enabled() && i_this->mChainInterpPrevValid && i_this->mChainInterpCurrValid) { - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - Mtx mtx; - const f32* p0 = (const f32*)i_this->mChainInterpPrev[i][j]; - const f32* p1 = (const f32*)i_this->mChainInterpCurr[i][j]; - f32* dst = (f32*)mtx; - for (int k = 0; k < 12; k++) { - dst[k] = p0[k] + (p1[k] - p0[k]) * alpha; - } - chain_s->model[j]->setBaseTRMtx(mtx); - g_env_light.setLightTevColorType_MAJI(chain_s->model[j], &actor->tevStr); - mDoExt_modelUpdateDL(chain_s->model[j]); - } else -#endif dComIfGp_entrySimpleModel(chain_s->model[j], fopAcM_GetRoomNo(actor)); } } @@ -388,21 +370,6 @@ static void chain_move(obj_keyhole_class* i_this) { ANGLE_ADD(sp8, TREG_S(0) + 0x3D00); } } - -#if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (i_this->mChainInterpCurrValid) { - memcpy(i_this->mChainInterpPrev, i_this->mChainInterpCurr, sizeof(i_this->mChainInterpCurr)); - i_this->mChainInterpPrevValid = true; - } - for (int i = 0; i < 6; i++) { - for (int j = 0; j < i_this->chain_num; j++) { - MTXCopy(i_this->chain_s[i].model[j]->getBaseTRMtx(), i_this->mChainInterpCurr[i][j]); - } - } - i_this->mChainInterpCurrValid = true; - } -#endif } static void open(obj_keyhole_class* i_this) { @@ -783,11 +750,6 @@ static int daObj_Keyhole_Create(fopAc_ac_c* a_this) { return cPhs_ERROR_e; } -#if TARGET_PC - i_this->mChainInterpPrevValid = false; - i_this->mChainInterpCurrValid = false; -#endif - OS_REPORT("//////////////OBJ_KEYHOLE SET 2 !!\n"); if (i_this->arg0 == 3) { diff --git a/src/d/actor/d_a_obj_klift00.cpp b/src/d/actor/d_a_obj_klift00.cpp index fbccb64edb..8a0eb5e38e 100644 --- a/src/d/actor/d_a_obj_klift00.cpp +++ b/src/d/actor/d_a_obj_klift00.cpp @@ -11,8 +11,20 @@ #include "d/d_bg_w.h" #include "d/d_cc_uty.h" #include "d/d_com_inf_game.h" -#include "dusk/frame_interpolation.h" -#include "dusk/settings.h" + +#if TARGET_PC +#include "dusk/interp/dual_buffer.h" +#include "dusk/interp/frame_interpolation.h" + +static const int CHAIN_INTERP_MAX = 64; + +namespace { +struct KLiftInterp { + cXyz draw[CHAIN_INTERP_MAX]; + dusk::interp::DualBuffer chain{draw}; +}; +} // namespace +#endif struct daObjKLift00_HIO_c : public mDoHIO_entry_c { daObjKLift00_HIO_c(); @@ -297,11 +309,6 @@ int daObjKLift00_c::Create() { if(getLock()) mStopSwingingFrames = 5; -#if TARGET_PC - mChainInterpPrevValid = false; - mChainInterpCurrValid = false; -#endif - return 1; } @@ -444,23 +451,20 @@ int daObjKLift00_c::Execute(Mtx** i_mtx) { } #if TARGET_PC -static void klift00_interp_callback(bool isSimFrame, void* pUserWork) { - static_cast(pUserWork)->onInterpCallback(); +static void klift00_interp_post(void* pUserWork) { + static_cast(pUserWork)->onInterpPresentation(); } -void daObjKLift00_c::onInterpCallback() { - if (!mChainInterpPrevValid || !mChainInterpCurrValid) { - return; - } - - const f32 alpha = dusk::frame_interp::get_interpolation_step(); - cXyz savedPositions[64]; +void daObjKLift00_c::onInterpPresentation() { + cXyz savedPositions[CHAIN_INTERP_MAX]; for (int i = 0; i < mNumChains; i++) { savedPositions[i] = mChainPositions[i].mCurrentPos; - const cXyz& p0 = mChainInterpPrev[i]; - const cXyz& p1 = mChainInterpCurr[i]; - mChainPositions[i].mCurrentPos = p0 + (p1 - p0) * alpha; + } + + auto& interp = dusk::interp::get(this); + for (int i = 0; i < mNumChains; i++) { + mChainPositions[i].mCurrentPos = interp.draw[i]; } setMtx(); @@ -493,18 +497,12 @@ int daObjKLift00_c::Draw() { dComIfGd_setList(); #if TARGET_PC - if (dusk::frame_interp::is_enabled()) { - if (mChainInterpCurrValid) { - memcpy(mChainInterpPrev, mChainInterpCurr, mNumChains * sizeof(cXyz)); - mChainInterpPrevValid = true; - } - + if (dusk::interp::is_enabled() && mNumChains > 0 && mNumChains <= CHAIN_INTERP_MAX) { + cXyz curr[CHAIN_INTERP_MAX]; for (int i = 0; i < mNumChains; i++) { - mChainInterpCurr[i] = mChainPositions[i].mCurrentPos; + curr[i] = mChainPositions[i].mCurrentPos; } - - mChainInterpCurrValid = true; - dusk::frame_interp::add_interpolation_callback(&klift00_interp_callback, this); + dusk::interp::get(this).chain.capture_and_schedule(curr, mNumChains, &klift00_interp_post, this); } #endif diff --git a/src/d/actor/d_a_obj_lv8Lift.cpp b/src/d/actor/d_a_obj_lv8Lift.cpp index 9ac8206f6b..b77e098487 100644 --- a/src/d/actor/d_a_obj_lv8Lift.cpp +++ b/src/d/actor/d_a_obj_lv8Lift.cpp @@ -11,7 +11,7 @@ #include "d/d_bg_w.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif daL8Lift_HIO_c::daL8Lift_HIO_c() { @@ -385,7 +385,7 @@ void daL8Lift_c::setNextPoint() { } #if TARGET_PC -void daL8Lift_interp_callback(bool isSimFrame, void* pUserWork) { +void daL8Lift_interp_callback(void* pUserWork) { daL8Lift_c* lift = static_cast(pUserWork); if (lift == NULL || lift->mpModel == NULL) { return; @@ -419,7 +419,7 @@ void daL8Lift_interp_callback(bool isSimFrame, void* pUserWork) { int daL8Lift_c::Draw() { #if TARGET_PC - dusk::frame_interp::add_interpolation_callback(&daL8Lift_interp_callback, this); + dusk::interp::add_interpolation_callback(&daL8Lift_interp_callback, this); #endif g_env_light.settingTevStruct(16, ¤t.pos, &tevStr); diff --git a/src/d/actor/d_a_obj_lv8OptiLift.cpp b/src/d/actor/d_a_obj_lv8OptiLift.cpp index e367b89b36..b63b0979f8 100644 --- a/src/d/actor/d_a_obj_lv8OptiLift.cpp +++ b/src/d/actor/d_a_obj_lv8OptiLift.cpp @@ -12,7 +12,7 @@ #include "d/d_path.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif daOptiLift_HIO_c::daOptiLift_HIO_c() { @@ -417,7 +417,7 @@ void daOptiLift_c::setNextPoint() { } #if TARGET_PC -static void daOptiLift_interp_callback(bool isSimFrame, void* pUserWork) { +static void daOptiLift_interp_callback(void* pUserWork) { daOptiLift_c* lift = static_cast(pUserWork); if (lift == NULL || lift->mpModel == NULL) { return; @@ -451,7 +451,7 @@ static void daOptiLift_interp_callback(bool isSimFrame, void* pUserWork) { int daOptiLift_c::Draw() { #if TARGET_PC - dusk::frame_interp::add_interpolation_callback(&daOptiLift_interp_callback, this); + dusk::interp::add_interpolation_callback(&daOptiLift_interp_callback, this); #endif g_env_light.settingTevStruct(0x10, ¤t.pos, &tevStr); diff --git a/src/d/actor/d_a_title.cpp b/src/d/actor/d_a_title.cpp index fd550070f3..0d011f9213 100644 --- a/src/d/actor/d_a_title.cpp +++ b/src/d/actor/d_a_title.cpp @@ -19,7 +19,7 @@ #include "m_Do/m_Do_graphic.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/version.hpp" #endif @@ -169,7 +169,7 @@ int daTitle_c::Execute() { } #ifdef TARGET_PC - if (!dusk::frame_interp::is_enabled()) { + if (!dusk::interp::is_enabled()) { #endif dMenu_Collect3D_c::setViewPortOffsetY(0.0f); #ifdef TARGET_PC @@ -352,7 +352,7 @@ void daTitle_c::fastLogoDispInit() { mWaitTimer = 30; mProcID = 5; - IF_DUSK(dusk::frame_interp::request_presentation_sync()); + IF_DUSK(dusk::interp::request_presentation_sync()); } void daTitle_c::fastLogoDisp() { diff --git a/src/d/actor/d_flower.inc b/src/d/actor/d_flower.inc index 91eba47906..e42cc7ded0 100644 --- a/src/d/actor/d_flower.inc +++ b/src/d/actor/d_flower.inc @@ -6,7 +6,7 @@ #include "SSystem/SComponent/c_counter.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif const u16 l_J_Ohana00_64TEX__width = 63; @@ -1165,16 +1165,12 @@ void dFlower_packet_c::draw() { GXSetChanAmbColor(GX_COLOR0A0, sp64); 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(&sp44->m_modelMtx, flower_mtx)) { - cMtx_concat(j3dSys.getViewMtx(), flower_mtx, flower_mtx); - GXLoadPosMtxImm(flower_mtx, 0); - } else +#if TARGET_PC + Mtx flowerMtx; + GXLoadPosMtxImm(get_model_mtx(sp44->m_modelMtx, flowerMtx), 0); +#else + GXLoadPosMtxImm(sp44->m_modelMtx, 0); #endif - { - GXLoadPosMtxImm(sp44->m_modelMtx, 0); - } GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0); #if TARGET_PC @@ -1324,16 +1320,12 @@ void dFlower_packet_c::draw() { sp30++; 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(&sp34->m_modelMtx, flower_mtx)) { - cMtx_concat(j3dSys.getViewMtx(), flower_mtx, flower_mtx); - GXLoadPosMtxImm(flower_mtx, 0); - } else +#if TARGET_PC + Mtx flowerMtx; + GXLoadPosMtxImm(get_model_mtx(sp34->m_modelMtx, flowerMtx), 0); +#else + GXLoadPosMtxImm(sp34->m_modelMtx, 0); #endif - { - GXLoadPosMtxImm(sp34->m_modelMtx, 0); - } GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0); #if TARGET_PC GXLoadTexObj(&mTexObj_l_J_Ohana01_64128_0419TEX, GX_TEXMAP0); @@ -1463,9 +1455,13 @@ void dFlower_packet_c::update() { mDoMtx_stack_c::copy(temp_r28); mDoMtx_stack_c::scaleM(temp_f31, temp_f31, temp_f31); +#if TARGET_PC + cMtx_copy(temp_r28, data_p->m_modelMtx); +#else cMtx_concat(j3dSys.getViewMtx(), temp_r28, data_p->m_modelMtx); +#endif #ifdef TARGET_PC - dusk::frame_interp::record_final_mtx(temp_r28, data_p->m_modelMtx); + dusk::interp::record_final_mtx(temp_r28, data_p->m_modelMtx); #endif } } diff --git a/src/d/actor/d_grass.inc b/src/d/actor/d_grass.inc index 780a7a052d..fd911083f4 100644 --- a/src/d/actor/d_grass.inc +++ b/src/d/actor/d_grass.inc @@ -13,7 +13,7 @@ #if TARGET_PC #include "dusk/dvd_asset.hpp" -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" using GameVersion = dusk::version::GameVersion; @@ -608,11 +608,12 @@ dGrass_packet_c::dGrass_packet_c() { #if TARGET_PC static MtxP get_model_mtx(Mtx modelMtx, Mtx storage) { - if (dusk::frame_interp::lookup_replacement(modelMtx, storage)) { + if (dusk::interp::lookup_replacement(modelMtx, storage)) { cMtx_concat(j3dSys.getViewMtx(), storage, storage); - return storage; + } else { + cMtx_concat(j3dSys.getViewMtx(), modelMtx, storage); } - return modelMtx; + return storage; } static void transform_positions( @@ -1193,16 +1194,12 @@ void dGrass_packet_c::draw() { GXSetChanAmbColor(GX_COLOR0A0, sp38); if (!cLib_checkBit(var_r29->field_0x01, 2)) { -#ifdef TARGET_PC - Mtx grass_mtx; - if (dusk::frame_interp::lookup_replacement(reinterpret_cast(&var_r29->m_modelMtx), grass_mtx)) { - cMtx_concat(j3dSys.getViewMtx(), grass_mtx, grass_mtx); - GXLoadPosMtxImm(grass_mtx, 0); - } else +#if TARGET_PC + Mtx grassMtx; + GXLoadPosMtxImm(get_model_mtx(var_r29->m_modelMtx, grassMtx), 0); +#else + GXLoadPosMtxImm(var_r29->m_modelMtx, 0); #endif - { - GXLoadPosMtxImm(var_r29->m_modelMtx, 0); - } GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0); if (var_r29->field_0x05 <= 3 || var_r29->field_0x05 >= 10) { if (var_r29->field_0x02 < -1) { @@ -1445,7 +1442,11 @@ void dGrass_packet_c::update() { } } +#if TARGET_PC + cMtx_copy(mDoMtx_stack_c::get(), data_p->m_modelMtx); +#else cMtx_concat(j3dSys.getViewMtx(), mDoMtx_stack_c::get(), data_p->m_modelMtx); +#endif } else { mDoMtx_stack_c::transS(data_p->m_pos.x, data_p->m_pos.y, data_p->m_pos.z); mDoMtx_stack_c::YrotM(i * 3535); @@ -1456,11 +1457,13 @@ void dGrass_packet_c::update() { f32 scale = ((((s16)data_p->m_pos.x * 3535) & 0xFFF) / 4096.0f) * 0.3f + 0.7f; mDoMtx_stack_c::scaleM(scale, scale, scale); +#if TARGET_PC + cMtx_copy(mDoMtx_stack_c::get(), data_p->m_modelMtx); +#else cMtx_concat(j3dSys.getViewMtx(), mDoMtx_stack_c::get(), data_p->m_modelMtx); - } -#ifdef TARGET_PC - dusk::frame_interp::record_final_mtx(mDoMtx_stack_c::get(), data_p->m_modelMtx); #endif + } + IF_DUSK(dusk::interp::record_final_mtx(mDoMtx_stack_c::get(), data_p->m_modelMtx)); } } data_p++; diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index 9bb5b6f383..feb7148cc0 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -32,7 +32,9 @@ #include "dusk/action_bindings.h" #include "dusk/camera_operators.hpp" #include "dusk/commands.hpp" -#include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" +#include "dusk/interp/camera.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/logging.h" #include "dusk/mouse.h" #include "dusk/settings.h" @@ -10503,13 +10505,13 @@ bool dCamera_c::eventCamera(s32 param_0) { #endif #if TARGET_PC - if (dusk::frame_interp::is_enabled()) { + if (dusk::interp::is_enabled()) { switch (var_r29) { case 3: case 4: case 5: case 12: - dusk::frame_interp::request_presentation_sync(); + dusk::interp::request_presentation_sync(); break; default: DuskLog.debug( @@ -11359,7 +11361,7 @@ void widezoom_correction(camera_process_class* i_this, float trim_height) { trim_width = FB_WIDTH_BASE / 2.0f * (1.0f - target_ar_real / current_ar); } - if (dusk::frame_interp::is_sim_frame()) { + if (dusk::game_clock::is_sim_frame()) { constexpr auto base_ar = static_cast(FB_WIDTH_BASE) / static_cast(FB_HEIGHT_BASE); const auto ar_corr = base_ar / std::min(current_ar, target_ar_real); @@ -11397,8 +11399,8 @@ static int camera_execute(camera_process_class* i_this) { #ifdef TARGET_PC widezoom_correction(i_this, i_this->mCamera.TrimHeight()); - if (dusk::frame_interp::is_enabled()) { - dusk::frame_interp::add_interpolation_callback([](bool _, void* pUserWork) { + if (dusk::interp::is_enabled()) { + dusk::interp::add_interpolation_callback([](void* pUserWork) { const auto i_this = static_cast(pUserWork); const auto camera = &i_this->mCamera; @@ -11407,7 +11409,7 @@ static int camera_execute(camera_process_class* i_this) { if (camera->mCurState != 2 && trim_size >= 0 && trim_size <= 3) { // derive trim height at previous tick using current camera state const auto target = get_target_trim_height(i_this); - const auto step = dusk::frame_interp::get_interpolation_step(); + const auto step = dusk::interp::get_interpolation_step(); const auto cur = camera->TrimHeight(); const auto prev = (4.0f * cur - target) / 3.0f; const auto trim_height = prev + (cur - prev) * step; @@ -11418,10 +11420,10 @@ static int camera_execute(camera_process_class* i_this) { } // record new camera for our sim frame - dusk::frame_interp::record_camera(i_this, get_camera_id(i_this)); + dusk::interp::record_camera(i_this, get_camera_id(i_this)); // interpolate the view now so that this sim frame's view matrix matches what // we'll be rendering with later - dusk::frame_interp::interp_view(&i_this->view); + dusk::interp::interp_view(&i_this->view); #endif view_setup(i_this); diff --git a/src/d/d_drawlist.cpp b/src/d/d_drawlist.cpp index fd1a82ba2f..b5ed876126 100644 --- a/src/d/d_drawlist.cpp +++ b/src/d/d_drawlist.cpp @@ -14,7 +14,8 @@ #include "m_Do/m_Do_mtx.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/logging.h" #include "helpers/gx_helper.h" @@ -1081,7 +1082,7 @@ void dDlst_shadowReal_c::reset() { void dDlst_shadowReal_c::imageDraw(Mtx param_0) { #ifdef TARGET_PC Mtx render_proj_mtx; - if (dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 2), render_proj_mtx)) { + if (dusk::interp::lookup_replacement(getInterpKey(mpModels[0], 2), render_proj_mtx)) { GXSetProjection(render_proj_mtx, GX_ORTHOGRAPHIC); } else #endif @@ -1102,7 +1103,7 @@ void dDlst_shadowReal_c::imageDraw(Mtx param_0) { shape_pkt = (*models)->getShapePacket(j); #ifdef TARGET_PC Mtx view_mtx; - if (dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 1), view_mtx)) { + if (dusk::interp::lookup_replacement(getInterpKey(mpModels[0], 1), view_mtx)) { shape_pkt->setBaseMtxPtr(&view_mtx); } else #endif @@ -1131,8 +1132,8 @@ void dDlst_shadowReal_c::draw() { GXSetCurrentMtx(GX_PNMTX0); #ifdef TARGET_PC Mtx view_mtx, recv_proj_mtx; - const auto have_view_mtx = dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 1), view_mtx); - const auto have_recv_proj_mtx = dusk::frame_interp::lookup_replacement(getInterpKey(mpModels[0], 3), recv_proj_mtx); + const auto have_view_mtx = dusk::interp::lookup_replacement(getInterpKey(mpModels[0], 1), view_mtx); + const auto have_recv_proj_mtx = dusk::interp::lookup_replacement(getInterpKey(mpModels[0], 3), recv_proj_mtx); if (have_view_mtx && have_recv_proj_mtx) { cMtx_concat(recv_proj_mtx, view_mtx, recv_proj_mtx); GXLoadTexMtxImm(recv_proj_mtx, GX_TEXMTX0, GX_MTX3x4); @@ -1300,9 +1301,9 @@ u8 dDlst_shadowReal_c::setShadowRealMtx(cXyz* param_0, cXyz* param_1, f32 param_ #ifdef TARGET_PC const auto keybase = mpModels[0]; - dusk::frame_interp::record_final_mtx(mViewMtx, getInterpKey(keybase, 1)); - dusk::frame_interp::record_final_mtx(mRenderProjMtx, getInterpKey(keybase, 2)); - dusk::frame_interp::record_final_mtx(mReceiverProjMtx, getInterpKey(keybase, 3)); + dusk::interp::record_final_mtx(mViewMtx, getInterpKey(keybase, 1)); + dusk::interp::record_final_mtx(mRenderProjMtx, getInterpKey(keybase, 2)); + dusk::interp::record_final_mtx(mReceiverProjMtx, getInterpKey(keybase, 3)); #endif cMtx_concat(mReceiverProjMtx, mViewMtx, mReceiverProjMtx); return r29; @@ -1364,6 +1365,16 @@ bool dDlst_shadowReal_c::add(J3DModel* i_model) { return true; } +#if TARGET_PC +static MtxP get_simple_shadow_mtx(Mtx worldMtx, const void* key, Mtx storage) { + if (!dusk::interp::lookup_replacement(key, storage)) { + cMtx_copy(worldMtx, storage); + } + cMtx_concat(j3dSys.getViewMtx(), storage, storage); + return storage; +} +#endif + void dDlst_shadowSimple_c::draw() { static GXColor l_color = {0, 0, 0, 64}; l_color.a = mAlpha; @@ -1371,31 +1382,23 @@ void dDlst_shadowSimple_c::draw() { GXSetTevColor(GX_TEVREG0, l_color); GXClearVtxDesc(); GXSetVtxDesc(GX_VA_POS, GX_INDEX8); -#ifdef TARGET_PC - Mtx volume_mtx; - if (dusk::frame_interp::lookup_replacement(mVolumeMtxKey, volume_mtx)) { - cMtx_concat(j3dSys.getViewMtx(), volume_mtx, volume_mtx); - GXLoadPosMtxImm(volume_mtx, GX_PNMTX0); - } else +#if TARGET_PC + Mtx volumeMtx; + GXLoadPosMtxImm(get_simple_shadow_mtx(mVolumeMtx, mVolumeMtxKey, volumeMtx), GX_PNMTX0); +#else + GXLoadPosMtxImm(mVolumeMtx, GX_PNMTX0); #endif - { - GXLoadPosMtxImm(mVolumeMtx, GX_PNMTX0); - } GXSetCurrentMtx(GX_PNMTX0); GXCallDisplayList(l_frontMat, 0x40); GXCallDisplayList(l_shadowVolumeDL, 0x40); GXCallDisplayList(l_backSubMat, 0x20); GXCallDisplayList(l_shadowVolumeDL, 0x40); -#ifdef TARGET_PC - Mtx shadow_mtx; - if (dusk::frame_interp::lookup_replacement(mMtxKey, shadow_mtx)) { - cMtx_concat(j3dSys.getViewMtx(), shadow_mtx, shadow_mtx); - GXLoadPosMtxImm(shadow_mtx, GX_PNMTX1); - } else +#if TARGET_PC + Mtx shadowMtx; + GXLoadPosMtxImm(get_simple_shadow_mtx(mMtx, mMtxKey, shadowMtx), GX_PNMTX1); +#else + GXLoadPosMtxImm(mMtx, GX_PNMTX1); #endif - { - GXLoadPosMtxImm(mMtx, GX_PNMTX1); - } GXSetCurrentMtx(GX_PNMTX1); if (mpTexObj != NULL) { @@ -1450,9 +1453,11 @@ void dDlst_shadowSimple_c::set(cXyz* param_0, f32 param_1, f32 param_2, cXyz* pa mDoMtx_stack_c::scaleM(param_2, f30 + f30 + 16.0f, param_2 * param_5); #if TARGET_PC mVolumeMtxKey = getInterpKey(param_0, 0x1); - dusk::frame_interp::record_final_mtx(mDoMtx_stack_c::get(), mVolumeMtxKey); -#endif + dusk::interp::record_final_mtx(mDoMtx_stack_c::get(), mVolumeMtxKey); + cMtx_copy(mDoMtx_stack_c::get(), mVolumeMtx); +#else cMtx_concat(j3dSys.getViewMtx(), mDoMtx_stack_c::get(), mVolumeMtx); +#endif f32 f31 = JMAFastSqrt(1.0f - param_3->x * param_3->x); f32 f29; f32 f28; @@ -1477,11 +1482,13 @@ void dDlst_shadowSimple_c::set(cXyz* param_0, f32 param_1, f32 param_2, cXyz* pa mDoMtx_stack_c::get()[2][3] = param_0->z; mDoMtx_stack_c::YrotM(param_4); mDoMtx_stack_c::scaleM(param_2, 1.0f, param_2 * param_5); -#ifdef TARGET_PC +#if TARGET_PC mMtxKey = getInterpKey(param_0, 0x2); - dusk::frame_interp::record_final_mtx(mDoMtx_stack_c::get(), mMtxKey); -#endif + dusk::interp::record_final_mtx(mDoMtx_stack_c::get(), mMtxKey); + cMtx_copy(mDoMtx_stack_c::get(), mMtx); +#else cMtx_concat(j3dSys.getViewMtx(), mDoMtx_stack_c::get(), mMtx); +#endif mpTexObj = param_6; } @@ -1642,7 +1649,7 @@ void dDlst_shadowControl_c::draw(Mtx param_0) { GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); #ifdef TARGET_PC Mtx draw_mtx; - if (dusk::frame_interp::lookup_replacement(param_0, draw_mtx)) { + if (dusk::interp::lookup_replacement(param_0, draw_mtx)) { GXLoadPosMtxImm(draw_mtx, GX_PNMTX0); } else { #endif @@ -1991,7 +1998,7 @@ void dDlst_list_c::drawXluListItem3d() { } int dDlst_list_c::set(dDlst_base_c**& p_start, dDlst_base_c**& p_end, dDlst_base_c* p_newDlst) { - if (p_start >= p_end) { + if (p_start >= p_end IF_DUSK(|| !dusk::game_clock::is_sim_frame())) { return 0; } *p_start = p_newDlst; @@ -2074,10 +2081,10 @@ void dDlst_list_c::calcWipe() { } #if TARGET_PC -void dDlst_list_c::refresh3DlineMats(const cXyz& eye) { +void dDlst_list_c::refresh3DlineMats() { for (int i = 0; i < 3; i++) { for (mDoExt_3DlineMat_c* mat = m3DLineMatSortPacket[i].getFirstMat(); mat != NULL; mat = mat->field_0x4) { - mat->refreshGeometryForPresentationEye(eye); + mat->refreshGeometryForPresentation(); } } } diff --git a/src/d/d_kankyo.cpp b/src/d/d_kankyo.cpp index 1201da30e9..b8e397e0ad 100644 --- a/src/d/d_kankyo.cpp +++ b/src/d/d_kankyo.cpp @@ -32,11 +32,10 @@ #include "JSystem/JKernel/JKRSolidHeap.h" #include #include + #if TARGET_PC -#include "dusk/imgui/ImGuiBloomWindow.hpp" -#include "dusk/settings.h" -#include "dusk/frame_interpolation.h" #include "dusk/game_clock.h" +#include "dusk/imgui/ImGuiBloomWindow.hpp" static f32 timeScale = 1.0f; #endif diff --git a/src/d/d_kankyo_rain.cpp b/src/d/d_kankyo_rain.cpp index 02dcd94d68..dc01322291 100644 --- a/src/d/d_kankyo_rain.cpp +++ b/src/d/d_kankyo_rain.cpp @@ -13,9 +13,9 @@ #include "m_Do/m_Do_lib.h" #include -#include "dusk/version.hpp" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/version.hpp" #endif static void vectle_calc(DOUBLE_POS* i_pos, cXyz* o_out) { @@ -129,24 +129,73 @@ static GXTexObj* load_cached_tex(CachedTexObjs& cache, ResTIMG* img, GXTexMap } #endif +#if TARGET_PC +static void dKyr_place_sun(camera_class* camera, cXyz* o_sunpos) { + cXyz lightDir; + u32 stage_type = dStage_stagInfo_GetSTType(dComIfGp_getStage()->getStagInfo()); + if (g_env_light.base_light.mColor.r == 0 && stage_type != ST_ROOM) { + dKyr_get_vectle_calc(&camera->view.lookat.eye, &g_env_light.base_light.mPosition, + &lightDir); + } else { + dKyr_get_vectle_calc(&camera->view.lookat.eye, &g_env_light.sun_light_pos, &lightDir); + } + o_sunpos->x = camera->view.lookat.eye.x + 8000.0f * lightDir.x; + o_sunpos->y = camera->view.lookat.eye.y + 8000.0f * lightDir.y; + o_sunpos->z = camera->view.lookat.eye.z + 8000.0f * lightDir.z; +} + +static void dKyr_place_lenzflare(camera_class* camera, cXyz* sunpos, cXyz* o_positions) { + cXyz eyeVect; + cXyz sunDirSmth; + cXyz camFwd; + + dKy_set_eyevect_calc(camera, &eyeVect, 4000.0f, 4000.0f); + dKyr_get_vectle_calc(&eyeVect, sunpos, &sunDirSmth); + o_positions[0] = *sunpos; + o_positions[1] = *sunpos; + + dKyr_get_vectle_calc(&camera->view.lookat.eye, &camera->view.lookat.center, &camFwd); + + for (int i = 2; i < 8; i++) { + if (i == 2) { + f32 size = 250.0f + 600.0f * sunDirSmth.abs(camFwd); + o_positions[i].x = sunpos->x - sunDirSmth.x * size * i; + o_positions[i].y = sunpos->y - sunDirSmth.y * size * i; + o_positions[i].z = sunpos->z - sunDirSmth.z * size * i; + } else { + f32 size = 250.0f + 110.0f * sunDirSmth.abs(camFwd); + o_positions[i].x = sunpos->x - (4100.0f * sunDirSmth.x + sunDirSmth.x * size * i); + o_positions[i].y = sunpos->y - (4100.0f * sunDirSmth.y + sunDirSmth.y * size * i); + o_positions[i].z = sunpos->z - (4100.0f * sunDirSmth.z + sunDirSmth.z * size * i); + } + } +} +#endif + void dKyr_lenzflare_move() { dKankyo_sun_Packet* sun_packet = g_env_light.mpSunPacket; dKankyo_sunlenz_Packet* lenz_packet = g_env_light.mpSunLenzPacket; camera_process_class* camera = dComIfGp_getCamera(0); +#if !TARGET_PC cXyz eyeVect; cXyz field_0x3c; cXyz sunDirSmth; cXyz camFwd; +#endif if (sun_packet->mVisibility < 0.0001f) { return; } +#if TARGET_PC + dKyr_place_lenzflare(camera, sun_packet->mPos, lenz_packet->mPositions); +#else dKy_set_eyevect_calc(camera, &eyeVect, 4000.0f, 4000.0f); dKyr_get_vectle_calc(&eyeVect, sun_packet->mPos, &sunDirSmth); lenz_packet->mPositions[0] = sun_packet->mPos[0]; lenz_packet->mPositions[1] = sun_packet->mPos[0]; +#endif cXyz vect; cXyz proj; @@ -162,6 +211,7 @@ void dKyr_lenzflare_move() { lenz_packet->field_0x94 *= S2DEG_CONSTANT; // convert from short angle to degrees lenz_packet->field_0x94 += 180.0f; +#if !TARGET_PC dKyr_get_vectle_calc(&camera->view.lookat.eye, &camera->view.lookat.center, &camFwd); for (int i = 2; i < 8; i++) { @@ -177,6 +227,7 @@ void dKyr_lenzflare_move() { lenz_packet->mPositions[i].z = sun_packet->mPos[0].z - (4100.0f * sunDirSmth.z + sunDirSmth.z * size * i); } } +#endif } static BOOL dKyr_moon_arrival_check() { @@ -205,6 +256,10 @@ void dKyr_sun_move() { u32 stage_type = dStage_stagInfo_GetSTType(dComIfGp_getStage()->getStagInfo()); +#if TARGET_PC + dKyr_place_sun(camera_p2, &sun_packet->mPos[0]); + dKyr_get_vectle_calc(&camera_p2->view.lookat.eye, &sun_packet->mPos[0], &lightDir); +#else if (g_env_light.base_light.mColor.r == 0 && stage_type != ST_ROOM) { dKyr_get_vectle_calc(&camera_p2->view.lookat.eye, &g_env_light.base_light.mPosition, &lightDir); @@ -215,6 +270,7 @@ void dKyr_sun_move() { sun_packet->mPos[0].x = camera_p2->view.lookat.eye.x + 8000.0f * lightDir.x; sun_packet->mPos[0].y = camera_p2->view.lookat.eye.y + 8000.0f * lightDir.y; sun_packet->mPos[0].z = camera_p2->view.lookat.eye.z + 8000.0f * lightDir.z; +#endif f32 horizon_y = (sun_packet->mPos[0].y - camera_p2->view.lookat.eye.y) / 8000.0f; if (horizon_y < 0.0f) { @@ -2414,15 +2470,17 @@ void dKyr_drawSun(Mtx drawMtx, cXyz* ppos, GXColor& unused, u8** tex) { sunpos.y = ppos->y; sunpos.z = ppos->z; + IF_DUSK(dKyr_place_sun(camera, &sunpos)); + u32 stage_type = dStage_stagInfo_GetSTType(dComIfGp_getStage()->getStagInfo()); if (g_env_light.base_light.mColor.r == 0 && stage_type != ST_ROOM) { if (g_env_light.daytime > 285.0f || g_env_light.daytime < 105.0f) { draw_moon = false; } - spB4.x = ppos->x; - spB4.y = ppos->y; - spB4.z = ppos->z; + spB4.x = DUSK_IF_ELSE(sunpos.x, ppos->x); + spB4.y = DUSK_IF_ELSE(sunpos.y, ppos->y); + spB4.z = DUSK_IF_ELSE(sunpos.z, ppos->z); } else { if (strcmp(dComIfGp_getStartStageName(), "F_SP200") == 0 && dComIfG_play_c::getLayerNo(0) == 0) { spB4 = envlight->moon_pos; @@ -2588,7 +2646,8 @@ void dKyr_drawSun(Mtx drawMtx, cXyz* ppos, GXColor& unused, u8** tex) { }; if (strcmp(dComIfGp_getStartStageName(), "F_SP200") != 0) { - dKyr_get_vectle_calc(&camera->view.lookat.eye, &camera->view.lookat.center, &camfwd); + dKyr_get_vectle_calc(&camera->view.lookat.eye, + &camera->view.lookat.center, &camfwd); f32 cam_distXZ = JMAFastSqrt((camfwd.x * camfwd.x) + (camfwd.z * camfwd.z)); f32 cam_theta = atan2f(camfwd.x, camfwd.z); f32 cam_phi = atan2f(camfwd.y, cam_distXZ); @@ -2783,6 +2842,13 @@ void dKyr_drawLenzflare(Mtx drawMtx, cXyz* ppos, GXColor& param_2, u8** tex) { f32 spA8 = sun_packet->mVisibility * sun_packet->mVisibility; if (!(sun_visibility < 0.1f)) { +#if TARGET_PC + cXyz sunpos; + cXyz positions[8]; + dKyr_place_sun(camera, &sunpos); + dKyr_place_lenzflare(camera, &sunpos, positions); + ppos = positions; +#endif dKy_set_eyevect_calc2(camera, &spFC, 8000.0f, 8000.0f); GXColor color_reg0; @@ -2962,25 +3028,25 @@ void dKyr_drawLenzflare(Mtx drawMtx, cXyz* ppos, GXColor& param_2, u8** tex) { spE4.y = sp9C; spE4.z = 0.0f; cMtx_multVec(camMtx, &spE4, &spD8); - pos[0].x = sun_packet->mPos[0].x + spD8.x; - pos[0].y = sun_packet->mPos[0].y + spD8.y; - pos[0].z = sun_packet->mPos[0].z + spD8.z; + pos[0].x = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).x + spD8.x; + pos[0].y = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).y + spD8.y; + pos[0].z = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).z + spD8.z; spE4.x = sp98; spE4.y = sp94; spE4.z = 0.0f; cMtx_multVec(camMtx, &spE4, &spD8); - pos[1].x = sun_packet->mPos[0].x + spD8.x; - pos[1].y = sun_packet->mPos[0].y + spD8.y; - pos[1].z = sun_packet->mPos[0].z + spD8.z; + pos[1].x = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).x + spD8.x; + pos[1].y = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).y + spD8.y; + pos[1].z = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).z + spD8.z; spE4.x = sp90; spE4.y = sp8C; spE4.z = 0.0f; cMtx_multVec(camMtx, &spE4, &spD8); - pos[2].x = sun_packet->mPos[0].x + spD8.x; - pos[2].y = sun_packet->mPos[0].y + spD8.y; - pos[2].z = sun_packet->mPos[0].z + spD8.z; + pos[2].x = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).x + spD8.x; + pos[2].y = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).y + spD8.y; + pos[2].z = DUSK_IF_ELSE(sunpos, sun_packet->mPos[0]).z + spD8.z; GXBegin(GX_TRIANGLES, GX_VTXFMT0, 3); GXPosition3f32(pos[0].x, pos[0].y, pos[0].z); @@ -6207,12 +6273,9 @@ static void dKyr_evil_draw2(Mtx drawMtx, u8** tex) { dKyr_set_btitex(&texobj, (ResTIMG*)tex[1]); #endif -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - rot += 0.7f; - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + rot += 0.7f; + IF_DUSK_BLOCK_END MTXRotRad(rotMtx, 'Z', DEG_TO_RAD(rot)); MTXConcat(camMtx, rotMtx, camMtx); @@ -6451,12 +6514,9 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { dKyr_set_btitex(&texobj, (ResTIMG*)tex[0]); #endif -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - rot += 1.0f; - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + rot += 1.0f; + IF_DUSK_BLOCK_END MTXRotRad(rotMtx, 'Z', DEG_TO_RAD(rot)); MTXConcat(camMtx, rotMtx, camMtx); diff --git a/src/d/d_menu_dmap.cpp b/src/d/d_menu_dmap.cpp index b84b596aff..f8514e3817 100644 --- a/src/d/d_menu_dmap.cpp +++ b/src/d/d_menu_dmap.cpp @@ -26,7 +26,7 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/settings.h" #include "dusk/version.hpp" #include "helpers/string.hpp" @@ -1112,7 +1112,7 @@ void dMenu_DmapBg_c::draw() { -35.0f + (local_224.x - local_218.x), -35.0f + (local_224.y - local_218.y)); #if TARGET_PC - if (!dusk::frame_interp::is_enabled()) { + if (!dusk::interp::is_enabled()) { field_0xdda = 0; } #else @@ -2774,7 +2774,7 @@ void dMenu_Dmap_c::zoomIn_proc() { void dMenu_Dmap_c::zoomOut_init_proc() { #if TARGET_PC - if (dusk::frame_interp::is_enabled()) { + if (dusk::interp::is_enabled()) { mpDrawBg->resetScrollArrowMask(); } #endif diff --git a/src/d/d_menu_fmap.cpp b/src/d/d_menu_fmap.cpp index c2809c4e43..57c329a61f 100644 --- a/src/d/d_menu_fmap.cpp +++ b/src/d/d_menu_fmap.cpp @@ -24,7 +24,7 @@ #include "d/actor/d_a_midna.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/memory.h" #include "dusk/version.hpp" #include "helpers/string.hpp" @@ -1152,7 +1152,7 @@ void dMenu_Fmap_c::zoom_spot_to_region_init() { field_0x1ec = 1.0f; #if TARGET_PC // Frame interp note: field_0x122d used to be set every draw, causing flickering. Do it here instead. - if (dusk::frame_interp::is_enabled()) { + if (dusk::interp::is_enabled()) { mpDraw2DBack->resetScrollArrowMask(); } #endif diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index 125bd91f39..58db87ee68 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -20,7 +20,7 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/ui/touch_controls.hpp" #include "dusk/version.hpp" @@ -358,13 +358,10 @@ void dMenu_Fmap2DBack_c::draw() { scrollAreaDraw(); } -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - blinkMove(30); - moveLightDropAnime(); - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + blinkMove(30); + moveLightDropAnime(); + IF_DUSK_BLOCK_END setCenterPosX(field_0x11dc, 1); drawIcon(mTransX, mTransZ, mAlphaRate, field_0xfa8 * mSpotTextureFadeAlpha); @@ -399,16 +396,13 @@ void dMenu_Fmap2DBack_c::draw() { (mArrowPos3DZ + control_ypos + fVar3) - fVar5, &mArrowPos2DX, &mArrowPos2DY); -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - field_0x11e0 -= g_fmapHIO.mCursorSpeed; + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + field_0x11e0 -= g_fmapHIO.mCursorSpeed; - if (field_0x11e0 < 0.0f) { - field_0x11e0 += 360.0f; - } + if (field_0x11e0 < 0.0f) { + field_0x11e0 += 360.0f; } + IF_DUSK_BLOCK_END mpPointParent->getPanePtr()->rotate(mpPointParent->getSizeX() / 2.0f, mpPointParent->getSizeY() / 2.0f, ROTATE_Z, @@ -448,7 +442,7 @@ void dMenu_Fmap2DBack_c::draw() { if (field_0x122d) { mpMeterHaihai->drawHaihai(field_0x122d); #if TARGET_PC - if (!dusk::frame_interp::is_enabled()) { + if (!dusk::interp::is_enabled()) { field_0x122d = 0; } #else @@ -1868,21 +1862,18 @@ void dMenu_Fmap2DBack_c::calcBlink() { t * (g_fmapHIO.mMapBlink[i + 1].mUnselectedRegion.mBlinkSpeed - g_fmapHIO.mMapBlink[i].mUnselectedRegion.mBlinkSpeed); -#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; - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + field_0x1218++; + if (field_0x1218 >= selected_blink_speed) { + field_0x1218 = 0; } + field_0x121a++; + if (field_0x121a >= unselected_blink_speed) { + field_0x121a = 0; + } + IF_DUSK_BLOCK_END + f32 t_selected = 0.0f; f32 t_unselected = 0.0f; diff --git a/src/d/d_menu_option.cpp b/src/d/d_menu_option.cpp index 7f18fa426b..6790684e7b 100644 --- a/src/d/d_menu_option.cpp +++ b/src/d/d_menu_option.cpp @@ -876,7 +876,7 @@ void dMenu_Option_c::vib_init() { void dMenu_Option_c::vib_move() { bool upTrigger = mpStick->checkUpTrigger(); - bool downTrigger = mpStick->checkDownTrigger(); + IF_NOT_DUSK(bool downTrigger =) mpStick->checkDownTrigger(); bool leftTrigger = checkLeftTrigger(); bool rightTrigger = checkRightTrigger(); @@ -891,10 +891,14 @@ void dMenu_Option_c::vib_move() { field_0x3ef = PROC_ATTEN_e; #endif Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); - } else if (downTrigger) { + } +#ifndef TARGET_PC + else if (downTrigger) { field_0x3ef = OPTION_SELECT(PROC_SOUND_e); Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); - } else if (leftTrigger) { + } +#endif + else if (leftTrigger) { if (isRumbleSupported()) { if (field_0x3ea == 0) { field_0x3ea = 1; @@ -1369,8 +1373,7 @@ void dMenu_Option_c::calibration_close2_move() { void dMenu_Option_c::menuVisible() { for (int i = 0; i < 6; i++) { - if (i < OPTION_SELECT(PROC_CHANGE_MOVE_e)) - { + if (i < OPTION_SELECT(DUSK_IF_ELSE(PROC_SOUND_e, PROC_CHANGE_MOVE_e))) { menuShow(i); } else { menuHide(i); @@ -2478,7 +2481,7 @@ bool dMenu_Option_c::isRumbleSupported() { #if TARGET_PC bool dMenu_Option_c::pointerConfirmSelect() { dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options); - for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 4 : 3); ++i) { + for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 3 : 2); ++i) { if (dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) { dusk::menu_pointer::set_hover_target(i); return false; @@ -2502,7 +2505,7 @@ bool dMenu_Option_c::pointerConfirmSelect() { bool dMenu_Option_c::dpdMenuMove() { #if TARGET_PC dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options); - for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 4 : 3); ++i) { + for (u8 i = 0; i < (dusk::version::isRegionJpn() ? 3 : 2); ++i) { if (!dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) { continue; } @@ -2582,20 +2585,6 @@ bool dMenu_Option_c::dpdMenuMove() { -1.0f, 0); } return true; - case PROC_SOUND_e: - if (field_0x3e9 == 0) { - field_0x3e9 = 2; - } else { - field_0x3e9--; - } - field_0x3da = 5; - mDoAud_setOutputMode(dMo_soundMode[field_0x3e9]); - setSoundMode(dMo_soundMode[field_0x3e9]); - field_0x3ef = OPTION_SELECT(PROC_CHANGE_MOVE_e); - field_0x3f5 = OPTION_SELECT(PROC_SOUND_e); - Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, - -1.0f, 0); - return true; } } #endif diff --git a/src/d/d_menu_ring.cpp b/src/d/d_menu_ring.cpp index 2b05a46fae..73832f8918 100644 --- a/src/d/d_menu_ring.cpp +++ b/src/d/d_menu_ring.cpp @@ -30,8 +30,8 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" #include "dusk/game_clock.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/menu_pointer.h" #include "dusk/settings.h" #include "dusk/ui/touch_controls.hpp" @@ -760,13 +760,13 @@ void dMenu_Ring_c::_draw() { f32 simX = 0.0f; f32 simY = 0.0f; bool restoreSimPos = false; - if (dusk::frame_interp::is_enabled() && mAlphaRate >= 1.0f) { + if (dusk::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()) { + if (dusk::interp::get_ui_tick_pending()) { mCursorInterpPrevX = mCursorInterpCurrX; mCursorInterpPrevY = mCursorInterpCurrY; mCursorInterpPrevAngle = mCursorInterpCurrAngle; @@ -790,7 +790,7 @@ void dMenu_Ring_c::_draw() { } } if (mCursorInterpInit) { - const f32 step = dusk::frame_interp::get_interpolation_step(); + const f32 step = dusk::interp::get_interpolation_step(); if (mCursorInterpPrevAngular && mCursorInterpCurrAngular) { const s16 delta = mCursorInterpCurrAngle - mCursorInterpPrevAngle; const s16 lerpedAngle = mCursorInterpPrevAngle + (s16)(delta * step); diff --git a/src/d/d_menu_save.cpp b/src/d/d_menu_save.cpp index aaa5fe622b..a2b1a5dd2c 100644 --- a/src/d/d_menu_save.cpp +++ b/src/d/d_menu_save.cpp @@ -22,7 +22,7 @@ #include "m_Do/m_Do_graphic.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/menu_pointer.h" #include "dusk/mods/svc/save.hpp" #include "dusk/settings.h" @@ -828,44 +828,38 @@ void dMenu_save_c::saveSelAnm() { } void dMenu_save_c::selFileWakuAnm() { -#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(); - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + mFileWakuAnmFrame += 2; + if (mFileWakuAnmFrame >= mpFileWakuAnm->getFrameMax()) { + mFileWakuAnmFrame -= mpFileWakuAnm->getFrameMax(); } + + mFileWakuRotAnmFrame += 2; + if (mFileWakuRotAnmFrame >= mpFileWakuRotAnm->getFrameMax()) { + mFileWakuRotAnmFrame -= mpFileWakuRotAnm->getFrameMax(); + } + IF_DUSK_BLOCK_END mpFileWakuAnm->setFrame(mFileWakuAnmFrame); mpFileWakuRotAnm->setFrame(mFileWakuRotAnmFrame); } void dMenu_save_c::bookIconAnm() { -#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(); - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + 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(); + } + IF_DUSK_BLOCK_END field_0x150->setFrame(field_0x154); field_0x158->setFrame(field_0x15c); field_0x160->setFrame(field_0x164); diff --git a/src/d/d_menu_window.cpp b/src/d/d_menu_window.cpp index 0e05e8ac4e..80f374cf46 100644 --- a/src/d/d_menu_window.cpp +++ b/src/d/d_menu_window.cpp @@ -27,7 +27,7 @@ #include "m_Do/m_Do_controller_pad.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif class dDlst_MENU_CAPTURE_c : public dDlst_base_c { @@ -126,7 +126,7 @@ public: void setCaptureFlag() { mFlag = 1; #ifdef TARGET_PC - dusk::frame_interp::request_presentation_sync(); + dusk::interp::request_presentation_sync(); #endif } diff --git a/src/d/d_meter2_draw.cpp b/src/d/d_meter2_draw.cpp index bcefa0a3dc..742fe6762d 100644 --- a/src/d/d_meter2_draw.cpp +++ b/src/d/d_meter2_draw.cpp @@ -23,7 +23,7 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/settings.h" #include "dusk/ui/icon_provider.hpp" #include "dusk/version.hpp" @@ -769,42 +769,38 @@ void dMeter2Draw_c::draw() { if (field_0x756 >= 0) { var_f29 = g_drawHIO.mLightDrop.mDropPikariAnimSpeed_Completed; int temp_r5_2 = g_drawHIO.mLightDrop.mPikariInterval * 15; -#ifdef TARGET_PC - // FRAME INTERP NOTE: Set even if not advancing - var_f28 = g_drawHIO.mLightDrop.mPikariScaleComplete; - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (field_0x756 <= temp_r5_2) { - int temp_r4 = (field_0x756 % g_drawHIO.mLightDrop.mPikariInterval); - int temp_r3_5 = field_0x756 / g_drawHIO.mLightDrop.mPikariInterval; + IF_DUSK(var_f28 = g_drawHIO.mLightDrop.mPikariScaleComplete); // FRAME INTERP NOTE: Set even if not advancing + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + if (field_0x756 <= temp_r5_2) { + int temp_r4 = (field_0x756 % g_drawHIO.mLightDrop.mPikariInterval); + int temp_r3_5 = field_0x756 / g_drawHIO.mLightDrop.mPikariInterval; - if (temp_r4 == 0 && field_0x62c[temp_r3_5] == 0.0f) { - field_0x62c[temp_r3_5] = 18.0f; - } + if (temp_r4 == 0 && field_0x62c[temp_r3_5] == 0.0f) { + field_0x62c[temp_r3_5] = 18.0f; + } - var_f28 = g_drawHIO.mLightDrop.mPikariScaleComplete; - field_0x756++; - } else { - int temp_r5_3 = temp_r5_2 + 1; + var_f28 = g_drawHIO.mLightDrop.mPikariScaleComplete; + field_0x756++; + } else { + int temp_r5_3 = temp_r5_2 + 1; - if (field_0x756 == temp_r5_3) { - if (field_0x62c[15] == 0.0f) { - field_0x756++; - } - var_f28 = g_drawHIO.mLightDrop.mPikariScaleComplete; - } else if (field_0x756 >= g_drawHIO.mLightDrop.field_0x54 + temp_r5_3) { - for (int i = 0; i < 16; i++) { - field_0x62c[i] = 18.0f - var_f29; - field_0x66c[i] = 18.0f - g_drawHIO.mLightDrop.mPikariLoopAnimSpeed; - } - - field_0x756 = -1; - } else { + if (field_0x756 == temp_r5_3) { + if (field_0x62c[15] == 0.0f) { field_0x756++; } + var_f28 = g_drawHIO.mLightDrop.mPikariScaleComplete; + } else if (field_0x756 >= g_drawHIO.mLightDrop.field_0x54 + temp_r5_3) { + for (int i = 0; i < 16; i++) { + field_0x62c[i] = 18.0f - var_f29; + field_0x66c[i] = 18.0f - g_drawHIO.mLightDrop.mPikariLoopAnimSpeed; + } + + field_0x756 = -1; + } else { + field_0x756++; } } + IF_DUSK_BLOCK_END } for (int i = 0; i < 16; i++) { @@ -1494,26 +1490,23 @@ void dMeter2Draw_c::drawPikari(f32 i_posX, f32 i_posY, f32* i_framep, f32 i_scal if (param_9 != 3 && param_9 != 4 && param_9 != 5 && dMsgObject_isTalkNowCheck()) { *i_framep = 0.0f; } else { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - *i_framep += param_8; - if (*i_framep > var_f31) { - if (param_9 == 1 || param_9 == 2 || param_9 == 3) { - *i_framep = 18.0f; - } else { - *i_framep = 0.0f; - } - } - - if (*i_framep == 18.0f && param_9 == 1) { - mDoAud_seStart(Z2SE_NAVI_BLINK, NULL, 0, 0); - } else if (*i_framep == 18.0f && param_9 == 2) { - mDoAud_seStart(Z2SE_SY_ITEM_COMBINE_ICON, NULL, 0, 0); + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + *i_framep += param_8; + if (*i_framep > var_f31) { + if (param_9 == 1 || param_9 == 2 || param_9 == 3) { + *i_framep = 18.0f; + } else { + *i_framep = 0.0f; } } + if (*i_framep == 18.0f && param_9 == 1) { + mDoAud_seStart(Z2SE_NAVI_BLINK, NULL, 0, 0); + } else if (*i_framep == 18.0f && param_9 == 2) { + mDoAud_seStart(Z2SE_SY_ITEM_COMBINE_ICON, NULL, 0, 0); + } + IF_DUSK_BLOCK_END + playPikariBckAnimation(*i_framep); playPikariBpkAnimation(*i_framep); diff --git a/src/d/d_meter_button.cpp b/src/d/d_meter_button.cpp index ac11d183ce..5c4207b2c8 100644 --- a/src/d/d_meter_button.cpp +++ b/src/d/d_meter_button.cpp @@ -19,7 +19,7 @@ #include #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/version.hpp" #include "helpers/string.hpp" #endif @@ -294,7 +294,7 @@ void dMeterButton_c::draw() { s16 temp_r6 = g_drawHIO.mEmpButton.mRepeatHitFrameNum; s16 temp_r6_2 = g_drawHIO.mEmpButton.mRepeatHitFrameNum / 2; - IF_DUSK_BLOCK(dusk::frame_interp::get_ui_tick_pending()) + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) field_0x4b8[i]++; if (field_0x4b8[i] >= temp_r6) { @@ -379,7 +379,7 @@ void dMeterButton_c::draw() { if (var_r3) { #if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) { + if (dusk::interp::get_ui_tick_pending()) { mWasListen[i] = var_r22; mWasRepeat[i] = var_r23; } else { @@ -388,7 +388,7 @@ void dMeterButton_c::draw() { } #endif if (var_r22) { - if (field_0x2e8[i] == 18.0f IF_DUSK(&& dusk::frame_interp::get_ui_tick_pending())) + if (field_0x2e8[i] == 18.0f IF_DUSK(&& dusk::interp::get_ui_tick_pending())) { mDoAud_seStart(Z2SE_SY_HINT_BUTTON_BLINK, NULL, 0, 0); } diff --git a/src/d/d_meter_haihai.cpp b/src/d/d_meter_haihai.cpp index 5ea5dad5b7..59a69ed61e 100644 --- a/src/d/d_meter_haihai.cpp +++ b/src/d/d_meter_haihai.cpp @@ -11,7 +11,10 @@ #include "d/d_com_inf_game.h" #include "d/d_meter_HIO.h" #include "d/d_pane_class.h" -#include "dusk/frame_interpolation.h" + +#if TARGET_PC +#include "dusk/interp/frame_interpolation.h" +#endif dMeterHaihai_c::dMeterHaihai_c(u8 i_type) { mType = i_type; @@ -287,20 +290,17 @@ void dMeterHaihai_c::updateHaihai() { void dMeterHaihai_c::playBckAnime(J2DAnmTransformKey* i_bck) { if (checkPlayAnime(1)) { if (i_bck != NULL) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (mType == 4) { - mBckFrame += g_drawHIO.mWiiLockArrowBCKAnimSpeed; - } else { - mBckFrame += g_drawHIO.mScrollArrowBCKAnimSpeed; - } - - if (mBckFrame >= i_bck->getFrameMax()) { - mBckFrame -= i_bck->getFrameMax(); - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + if (mType == 4) { + mBckFrame += g_drawHIO.mWiiLockArrowBCKAnimSpeed; + } else { + mBckFrame += g_drawHIO.mScrollArrowBCKAnimSpeed; } + + if (mBckFrame >= i_bck->getFrameMax()) { + mBckFrame -= i_bck->getFrameMax(); + } + IF_DUSK_BLOCK_END } else { mBtkFrame = 1.0f; } @@ -315,20 +315,17 @@ void dMeterHaihai_c::playBckAnime(J2DAnmTransformKey* i_bck) { void dMeterHaihai_c::playBtkAnime(J2DAnmTextureSRTKey* i_btk) { if (checkPlayAnime(2)) { if (i_btk != NULL) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (mType == 4) { - mBtkFrame += g_drawHIO.mWiiLockArrowBTKAnimSpeed; - } else { - mBtkFrame += g_drawHIO.mScrollArrowBTKAnimSpeed; - } - - if (mBtkFrame >= i_btk->getFrameMax()) { - mBtkFrame -= i_btk->getFrameMax(); - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + if (mType == 4) { + mBtkFrame += g_drawHIO.mWiiLockArrowBTKAnimSpeed; + } else { + mBtkFrame += g_drawHIO.mScrollArrowBTKAnimSpeed; } + + if (mBtkFrame >= i_btk->getFrameMax()) { + mBtkFrame -= i_btk->getFrameMax(); + } + IF_DUSK_BLOCK_END } else { mBtkFrame = 1.0f; } @@ -342,20 +339,17 @@ void dMeterHaihai_c::playBtkAnime(J2DAnmTextureSRTKey* i_btk) { void dMeterHaihai_c::playBpkAnime(J2DAnmColor* i_bpk) { if (checkPlayAnime(0)) { if (i_bpk != NULL) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (mType == 4) { - mBpkFrame += g_drawHIO.mWiiLockArrowBPKAnimSpeed; - } else { - mBpkFrame += g_drawHIO.mScrollArrowBPKAnimSpeed; - } - - if (mBpkFrame >= i_bpk->getFrameMax()) { - mBpkFrame -= i_bpk->getFrameMax(); - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + if (mType == 4) { + mBpkFrame += g_drawHIO.mWiiLockArrowBPKAnimSpeed; + } else { + mBpkFrame += g_drawHIO.mScrollArrowBPKAnimSpeed; } + + if (mBpkFrame >= i_bpk->getFrameMax()) { + mBpkFrame -= i_bpk->getFrameMax(); + } + IF_DUSK_BLOCK_END } else { mBpkFrame = 1.0f; } diff --git a/src/d/d_meter_string.cpp b/src/d/d_meter_string.cpp index c99e7c564e..3bc5795dc0 100644 --- a/src/d/d_meter_string.cpp +++ b/src/d/d_meter_string.cpp @@ -16,9 +16,12 @@ #include "d/d_meter2_info.h" #include "d/d_meter_HIO.h" #include "d/d_pane_class.h" -#include "dusk/frame_interpolation.h" #include +#if TARGET_PC +#include "dusk/interp/frame_interpolation.h" +#endif + dMeterString_c::dMeterString_c(int i_stringID) { mpMapArchive = dComIfGp_getAllMapArchive(); field_0x28 = 0; @@ -106,27 +109,22 @@ void dMeterString_c::draw() { f32 var_f30 = 1.0f; if (mAnimFrame < 60.0f) { -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - mAnimFrame += g_drawHIO.mMiniGame.mReadyFightTextAnimSpeed; - if (mAnimFrame > 60.0f) { - mAnimFrame = 60.0f; - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + mAnimFrame += g_drawHIO.mMiniGame.mReadyFightTextAnimSpeed; + if (mAnimFrame > 60.0f) { + mAnimFrame = 60.0f; } + IF_DUSK_BLOCK_END playBckAnimation(mAnimFrame); } else if (mAnimFrame < (f32)g_drawHIO.mMiniGame.mReadyFightTextWaitFrames + 60.0f) { -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - mAnimFrame += var_f30; + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + mAnimFrame += var_f30; + IF_DUSK_BLOCK_END } else if (mAnimFrame < var_f31) { -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - mAnimFrame += var_f30; + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + mAnimFrame += var_f30; + IF_DUSK_BLOCK_END var_f30 = acc(g_drawHIO.mMiniGame.field_0x172, var_f31 - mAnimFrame, 0); } @@ -139,23 +137,17 @@ void dMeterString_c::draw() { if (mPikariAnimFrame > 0.0f) { drawPikari(); - } else if (mPikariAnimFrame == -1.0f && -#if TARGET_PC - dusk::frame_interp::get_ui_tick_pending() && -#endif + } else if (mPikariAnimFrame == -1.0f IF_DUSK(&& dusk::interp::get_ui_tick_pending()) && mAnimFrame > g_drawHIO.mMiniGame.mReadyFightPikariAppearFrames) { mPikariAnimFrame = 18.0f - g_drawHIO.mMiniGame.mReadyFightPikariAnimSpeed; } -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (mAnimFrame >= var_f31) { - dMeter2Info_resetMeterString(); - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + if (mAnimFrame >= var_f31) { + dMeter2Info_resetMeterString(); } + IF_DUSK_BLOCK_END } } } diff --git a/src/d/d_model.cpp b/src/d/d_model.cpp index bdff472367..c236b5f2db 100644 --- a/src/d/d_model.cpp +++ b/src/d/d_model.cpp @@ -6,7 +6,7 @@ #include "d/d_com_inf_game.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif void dMdl_c::draw() { @@ -52,7 +52,7 @@ void dMdl_c::entryObj(dMdl_obj_c* i_obj) { #ifdef TARGET_PC // if field_0x1a is false, this dMdl_c is not in the drawlist // if true, we need to make sure with interp enabled - if (dusk::frame_interp::is_enabled() && field_0x1a) { + if (dusk::interp::is_enabled() && field_0x1a) { auto pkt = dComIfGd_getListPacket()->mpBuffer[0]; while (pkt && pkt != this) { pkt = pkt->getNextPacket(); diff --git a/src/d/d_msg_out_font.cpp b/src/d/d_msg_out_font.cpp index 57b85b18b2..ce5e97c2d0 100644 --- a/src/d/d_msg_out_font.cpp +++ b/src/d/d_msg_out_font.cpp @@ -8,7 +8,7 @@ #include "f_op/f_op_msg_mng.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #include "dusk/version.hpp" #endif @@ -319,7 +319,7 @@ void COutFont_c::draw(J2DTextBox* i_textbox, f32 param_1, f32 param_2, f32 param } #if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) { + if (dusk::interp::get_ui_tick_pending()) { for (int i = 0; i < 70; i++) { sp256[i] = -1; } @@ -528,7 +528,7 @@ void COutFont_c::draw(J2DTextBox* i_textbox, f32 param_1, f32 param_2, f32 param case 20: case 21: case 22: - IF_DUSK_BLOCK(dusk::frame_interp::get_ui_tick_pending()) + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) field_0x1b4[type]++; if (field_0x1b4[type] >= 28) { field_0x1b4[type] = 0; diff --git a/src/d/d_msg_scrn_howl.cpp b/src/d/d_msg_scrn_howl.cpp index 4e5c59546c..c87c4bbb0f 100644 --- a/src/d/d_msg_scrn_howl.cpp +++ b/src/d/d_msg_scrn_howl.cpp @@ -21,8 +21,7 @@ #include "m_Do/m_Do_graphic.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" -#include "dusk/settings.h" +#include "dusk/interp/frame_interpolation.h" #endif // POSIX already defines a macro with this name, but we know that this specific name is @@ -596,15 +595,12 @@ void dMsgScrnHowl_c::drawWave() { f17 = local_60; f18 = local_64; } else { -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - field_0x2134++; - if (field_0x2134 > 30) { - field_0x2134 = 0; - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + field_0x2134++; + if (field_0x2134 > 30) { + field_0x2134 = 0; } + IF_DUSK_BLOCK_END if (field_0x2134 < 15) { local_dc = field_0x2134 / 15.0f; } else { diff --git a/src/d/d_msg_scrn_light.cpp b/src/d/d_msg_scrn_light.cpp index 1f6a176066..0d9f409978 100644 --- a/src/d/d_msg_scrn_light.cpp +++ b/src/d/d_msg_scrn_light.cpp @@ -8,7 +8,7 @@ #include "d/d_pane_class.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/interp/frame_interpolation.h" #endif class dMsgScrnLight_HIO_c { @@ -206,15 +206,12 @@ void dMsgScrnLight_c::draw(f32* i_anmFrame, f32 i_posX, f32 i_posY, f32 i_scaleX } if (mPlayAnim) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - *i_anmFrame += 1.0f; - if (*i_anmFrame >= mpBck->getFrameMax()) { - *i_anmFrame = 0.0f; - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + *i_anmFrame += 1.0f; + if (*i_anmFrame >= mpBck->getFrameMax()) { + *i_anmFrame = 0.0f; } + IF_DUSK_BLOCK_END mBckFrame = *i_anmFrame; mBpkFrame = *i_anmFrame; @@ -229,16 +226,13 @@ void dMsgScrnLight_c::draw(f32* i_anmFrame, f32 i_posX, f32 i_posY, f32 i_scaleX mpParent_c->setBlackWhite(i_black, i_white); if (mPlayAnim) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - *i_anmFrame += i_anmRate; + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + *i_anmFrame += i_anmRate; - if (*i_anmFrame >= mpBck->getFrameMax()) { - *i_anmFrame = 0.0f; - } + if (*i_anmFrame >= mpBck->getFrameMax()) { + *i_anmFrame = 0.0f; } + IF_DUSK_BLOCK_END mBckFrame = *i_anmFrame; mBpkFrame = *i_anmFrame; diff --git a/src/d/d_particle.cpp b/src/d/d_particle.cpp index 9e443aa81a..d1e175dfc1 100644 --- a/src/d/d_particle.cpp +++ b/src/d/d_particle.cpp @@ -26,7 +26,7 @@ #include "SSystem/SComponent/c_math.h" #if TARGET_PC -#include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" #include #endif @@ -1976,7 +1976,7 @@ void dPa_light8PcallBack::draw(JPABaseEmitter* param_1, JPABaseParticle* param_2 JGeometry::TVec3 local_160; JGeometry::TVec3 local_16c; #if TARGET_PC - if (dusk::frame_interp::is_sim_frame()) + if (dusk::game_clock::is_sim_frame()) #endif { dPa_setWindPower(param_2); diff --git a/src/d/d_select_cursor.cpp b/src/d/d_select_cursor.cpp index 640f8072cd..4fcc6fd5f8 100644 --- a/src/d/d_select_cursor.cpp +++ b/src/d/d_select_cursor.cpp @@ -5,9 +5,12 @@ #include "d/d_com_inf_game.h" #include "JSystem/J2DGraph/J2DAnimation.h" #include "JSystem/J2DGraph/J2DAnmLoader.h" -#include "dusk/frame_interpolation.h" #include +#if TARGET_PC +#include "dusk/interp/frame_interpolation.h" +#endif + dSelect_cursorHIO_c::dSelect_cursorHIO_c() { field_0x8 = 1.0f; mXAxisExpansion = 1.0f; @@ -281,21 +284,18 @@ void dSelect_cursor_c::update() { if (mUpdateFlag) { if (field_0x30) { if (chkPlayAnime(0)) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (mNameIdx == 1) { - field_0x44 += mpCursorHIO->field_0x8 * fVar1; - } else { - field_0x44 += fVar1; - } - - if (field_0x44 >= field_0x30->getFrameMax()) { - field_0x44 -= field_0x30->getFrameMax(); - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + if (mNameIdx == 1) { + field_0x44 += mpCursorHIO->field_0x8 * fVar1; + } else { + field_0x44 += fVar1; } + if (field_0x44 >= field_0x30->getFrameMax()) { + field_0x44 -= field_0x30->getFrameMax(); + } + IF_DUSK_BLOCK_END + field_0x30->setFrame(field_0x44); setBpkAnimation(field_0x30); } else { @@ -310,19 +310,16 @@ void dSelect_cursor_c::update() { for (int i = 0; i < 2; i++) { if (field_0x34[i]) { if ((i == 0 && chkPlayAnime(2)) || (i == 1 && chkPlayAnime(3))) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (mNameIdx == 1) { - field_0x48[i] += mpCursorHIO->field_0x8 * fVar1; - } else { - field_0x48[i] += fVar1; - } - if (field_0x48[i] >= field_0x34[i]->getFrameMax()) { - field_0x48[i] -= field_0x34[i]->getFrameMax(); - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + if (mNameIdx == 1) { + field_0x48[i] += mpCursorHIO->field_0x8 * fVar1; + } else { + field_0x48[i] += fVar1; } + if (field_0x48[i] >= field_0x34[i]->getFrameMax()) { + field_0x48[i] -= field_0x34[i]->getFrameMax(); + } + IF_DUSK_BLOCK_END field_0x34[i]->setFrame(field_0x48[i]); } @@ -331,19 +328,16 @@ void dSelect_cursor_c::update() { } if (field_0x2C && chkPlayAnime(1)) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (mNameIdx == 1) { - field_0x40 += mpCursorHIO->field_0x8 * fVar1; - } else { - field_0x40 += fVar1; - } - if (field_0x40 >= field_0x2C->getFrameMax()) { - field_0x40 -= field_0x2C->getFrameMax(); - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + if (mNameIdx == 1) { + field_0x40 += mpCursorHIO->field_0x8 * fVar1; + } else { + field_0x40 += fVar1; } + if (field_0x40 >= field_0x2C->getFrameMax()) { + field_0x40 -= field_0x2C->getFrameMax(); + } + IF_DUSK_BLOCK_END field_0x2C->setFrame(field_0x40); setBckAnimation(field_0x2C); @@ -351,12 +345,9 @@ void dSelect_cursor_c::update() { } if (chkPlayAnime(1) && mNameIdx == 0) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - setCursorAnimation(); - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + setCursorAnimation(); + IF_DUSK_BLOCK_END } mpScreen->animation(); diff --git a/src/d/d_select_icon.cpp b/src/d/d_select_icon.cpp index 32be9c15e2..e55beaf54c 100644 --- a/src/d/d_select_icon.cpp +++ b/src/d/d_select_icon.cpp @@ -2,31 +2,28 @@ #include "d/d_select_icon.h" #include "JSystem/J2DGraph/J2DAnimation.h" -#include "dusk/frame_interpolation.h" + +#if TARGET_PC +#include "dusk/interp/frame_interpolation.h" +#endif dSi_HIO_c::dSi_HIO_c() {} void dSelect_icon_c::animation() { if (field_0x10->getAlpha() != 0) { -#ifdef TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - field_0x20 += field_0x2c; - if (field_0x20 >= field_0x1c->getFrameMax()) { - field_0x20 = 0.0f; - } - field_0x1c->setFrame(field_0x20); - - field_0x28 += field_0x2c; - if (field_0x28 >= field_0x24->getFrameMax()) { - field_0x28 = 0.0f; - } + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + field_0x20 += field_0x2c; + if (field_0x20 >= field_0x1c->getFrameMax()) { + field_0x20 = 0.0f; } -#ifdef TARGET_PC - // FRAME INTERP NOTE: Set even if not advancing field_0x1c->setFrame(field_0x20); -#endif + + field_0x28 += field_0x2c; + if (field_0x28 >= field_0x24->getFrameMax()) { + field_0x28 = 0.0f; + } + IF_DUSK_BLOCK_END + IF_DUSK(field_0x1c->setFrame(field_0x20)); // FRAME INTERP NOTE: Set even if not advancing field_0x24->setFrame(field_0x28); field_0x8->animation(); diff --git a/src/d/d_timer.cpp b/src/d/d_timer.cpp index e07cc3af80..df1a3ef104 100644 --- a/src/d/d_timer.cpp +++ b/src/d/d_timer.cpp @@ -23,8 +23,10 @@ #include "m_Do/m_Do_lib.h" #include -#include "dusk/frame_interpolation.h" +#if TARGET_PC +#include "dusk/interp/frame_interpolation.h" #include "dusk/version.hpp" +#endif static int dTimer_createStart2D(s32 param_0, u16 param_1); @@ -1340,23 +1342,20 @@ void dDlst_TimerScrnDraw_c::draw() { ((f32)g_drawHIO.mMiniGame.mGetInTextWaitFrames + 60.0f); for (int i = 0; i < 51; i++) { -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (m_getin_info[i].bck_frame > 0.0f && m_getin_info[i].bck_frame < temp) { - if (m_getin_info[i].bck_frame < 60.0f) { - m_getin_info[i].bck_frame += g_drawHIO.mMiniGame.mGetInTextAnimSpeed; - if (m_getin_info[i].bck_frame > 60.0f) { - m_getin_info[i].bck_frame = 60.0f; - } - } else if (m_getin_info[i].bck_frame < g_drawHIO.mMiniGame.mGetInTextWaitFrames + 60.0f) { - m_getin_info[i].bck_frame++; - } else if (m_getin_info[i].bck_frame < temp) { - m_getin_info[i].bck_frame++; + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + if (m_getin_info[i].bck_frame > 0.0f && m_getin_info[i].bck_frame < temp) { + if (m_getin_info[i].bck_frame < 60.0f) { + m_getin_info[i].bck_frame += g_drawHIO.mMiniGame.mGetInTextAnimSpeed; + if (m_getin_info[i].bck_frame > 60.0f) { + m_getin_info[i].bck_frame = 60.0f; } + } else if (m_getin_info[i].bck_frame < g_drawHIO.mMiniGame.mGetInTextWaitFrames + 60.0f) { + m_getin_info[i].bck_frame++; + } else if (m_getin_info[i].bck_frame < temp) { + m_getin_info[i].bck_frame++; } } + IF_DUSK_BLOCK_END if (m_getin_info[i].bck_frame > 0.0f && m_getin_info[i].bck_frame < temp) { f32 var_f29 = 1.0f; @@ -1396,20 +1395,17 @@ void dDlst_TimerScrnDraw_c::draw() { if (m_getin_info[i].pikari_frame > 0.0f) { drawPikari(i); } else if (m_getin_info[i].pikari_frame == -1.0f) { -#if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) -#endif - { - if (m_getin_info[i].field_0xc == 0) { - if (m_getin_info[i].bck_frame > g_drawHIO.mMiniGame.mGetInPikariAppearFrames) { - m_getin_info[i].pikari_frame = - 18.0f - g_drawHIO.mMiniGame.mGetInPikariAnimSpeed; - } - } else if (m_getin_info[i].bck_frame > g_drawHIO.mMiniGame.mStartPikariAppearFrames) { + IF_DUSK_BLOCK(dusk::interp::get_ui_tick_pending()) + if (m_getin_info[i].field_0xc == 0) { + if (m_getin_info[i].bck_frame > g_drawHIO.mMiniGame.mGetInPikariAppearFrames) { m_getin_info[i].pikari_frame = - 18.0f - g_drawHIO.mMiniGame.mStartPikariAnimSpeed; + 18.0f - g_drawHIO.mMiniGame.mGetInPikariAnimSpeed; } + } else if (m_getin_info[i].bck_frame > g_drawHIO.mMiniGame.mStartPikariAppearFrames) { + m_getin_info[i].pikari_frame = + 18.0f - g_drawHIO.mMiniGame.mStartPikariAnimSpeed; } + IF_DUSK_BLOCK_END } } } diff --git a/src/dusk/archive.cpp b/src/dusk/archive.cpp new file mode 100644 index 0000000000..a749d5432b --- /dev/null +++ b/src/dusk/archive.cpp @@ -0,0 +1,145 @@ +#include "archive.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace dusk::archive { +namespace { + +constexpr std::array ZipMagic{'P', 'K', '\x03', '\x04'}; + +PackageFormat detect_package_format(mz_zip_archive& zip) { + size_t modManifests = 0; + for (mz_uint index = 0, count = mz_zip_reader_get_num_files(&zip); index < count; ++index) { + mz_zip_archive_file_stat stat{}; + if (!mz_zip_reader_file_stat(&zip, index, &stat) || + mz_zip_reader_is_file_a_directory(&zip, index)) + { + continue; + } + const std::string_view name{stat.m_filename}; + modManifests += name == "mod.json"; + } + if (modManifests == 1) { + return PackageFormat::Mod; + } + return PackageFormat::Unknown; +} + +} // namespace + +struct ZipArchive::Impl { + ~Impl() { + if (open) { + mz_zip_reader_end(&zip); + } + } + + static size_t read_zip( + void* opaque, mz_uint64 offset, void* buffer, const size_t size) { + auto& archive = *static_cast(opaque); + std::error_code error; + return archive.file.read_at(offset, {static_cast(buffer), size}, error); + } + + borealis::io::RandomAccessFile file; + mz_zip_archive zip{}; + PackageFormat format = PackageFormat::Unknown; + bool open = false; + std::mutex mutex; +}; + +ZipArchive::ZipArchive(const std::filesystem::path& path) : m_impl{std::make_unique()} { + auto opened = borealis::io::RandomAccessFile::open(path); + if (opened.status != borealis::io::Status::Ok) { + throw std::runtime_error(opened.message); + } + m_impl->file = std::move(opened.file); + + std::array header{}; + std::error_code error; + const auto read = m_impl->file.read_at( + 0, {reinterpret_cast(header.data()), header.size()}, error); + if (error) { + throw std::runtime_error(fmt::format("Reading ZIP magic failed: {}", error.message())); + } + if (read != header.size() || header != ZipMagic) { + throw std::runtime_error("File does not have ZIP magic"); + } + + m_impl->zip.m_pRead = Impl::read_zip; + m_impl->zip.m_pIO_opaque = m_impl.get(); + if (!mz_zip_reader_init(&m_impl->zip, m_impl->file.size(), 0)) { + const auto zipError = mz_zip_get_last_error(&m_impl->zip); + throw std::runtime_error( + fmt::format("Opening ZIP failed: {}", mz_zip_get_error_string(zipError))); + } + m_impl->open = true; + m_impl->format = detect_package_format(m_impl->zip); +} + +ZipArchive::~ZipArchive() = default; + +ZipArchive::ZipArchive(ZipArchive&&) noexcept = default; +ZipArchive& ZipArchive::operator=(ZipArchive&&) noexcept = default; + +PackageFormat ZipArchive::package_format() const noexcept { + return m_impl->format; +} + +std::vector ZipArchive::read_file(const std::string_view name) { + std::lock_guard lock{m_impl->mutex}; + const std::string fileName{name}; + size_t size = 0; + void* extracted = mz_zip_reader_extract_file_to_heap(&m_impl->zip, fileName.c_str(), &size, 0); + if (extracted == nullptr) { + throw std::runtime_error(fmt::format("File does not exist: {}", name)); + } + + const std::unique_ptr owner{extracted, &mz_free}; + const std::span data{static_cast(owner.get()), size}; + std::vector result; + result.assign(data.begin(), data.end()); + return result; +} + +std::vector ZipArchive::file_names() { + std::lock_guard lock{m_impl->mutex}; + std::vector results; + for (mz_uint index = 0, count = mz_zip_reader_get_num_files(&m_impl->zip); index < count; + ++index) + { + mz_zip_archive_file_stat stat{}; + if (!mz_zip_reader_file_stat(&m_impl->zip, index, &stat) || + mz_zip_reader_is_file_a_directory(&m_impl->zip, index)) + { + continue; + } + results.emplace_back(stat.m_filename); + } + return results; +} + +size_t ZipArchive::file_size(const std::string_view name) { + std::lock_guard lock{m_impl->mutex}; + const std::string fileName{name}; + const auto index = mz_zip_reader_locate_file(&m_impl->zip, fileName.c_str(), nullptr, 0); + if (index < 0) { + throw std::runtime_error(fmt::format("Unable to locate file in ZIP: {}", name)); + } + + mz_zip_archive_file_stat stat{}; + if (!mz_zip_reader_file_stat(&m_impl->zip, static_cast(index), &stat)) { + throw std::runtime_error(fmt::format("Unable to inspect file in ZIP: {}", name)); + } + return static_cast(stat.m_uncomp_size); +} + +} // namespace dusk::archive diff --git a/src/dusk/archive.hpp b/src/dusk/archive.hpp new file mode 100644 index 0000000000..2e1003d49c --- /dev/null +++ b/src/dusk/archive.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace dusk::archive { + +enum class PackageFormat { + Unknown, + Mod, + // Save, +}; + +class ZipArchive { +public: + explicit ZipArchive(const std::filesystem::path& path); + ~ZipArchive(); + + ZipArchive(ZipArchive&&) noexcept; + ZipArchive& operator=(ZipArchive&&) noexcept; + ZipArchive(const ZipArchive&) = delete; + ZipArchive& operator=(const ZipArchive&) = delete; + + PackageFormat package_format() const noexcept; + std::vector read_file(std::string_view name); + std::vector file_names(); + size_t file_size(std::string_view name); + +private: + struct Impl; + std::unique_ptr m_impl; +}; + +} // namespace dusk::archive diff --git a/src/dusk/audio/DuskAudioSystem.cpp b/src/dusk/audio/DuskAudioSystem.cpp index 13275ed830..a3bfe40252 100644 --- a/src/dusk/audio/DuskAudioSystem.cpp +++ b/src/dusk/audio/DuskAudioSystem.cpp @@ -20,7 +20,7 @@ using namespace dusk::audio; static OutputSubframe OutBuffer; -static std::array OutInterleaveBuffer; +static std::array OutInterleaveBufferFull; static SDL_AudioStream* PlaybackStream; @@ -43,21 +43,55 @@ static int RenderNewAudioFrame(); /** * Render an audio subframe and output it to SDL3. */ -static void RenderAudioSubframe(); +static int RenderAudioSubframe(); -static void InitSDL3Output() { - SDL_Init(SDL_INIT_AUDIO); +static size_t GetChannelCountForOutputMode(dusk::AudioOutputMode config) { + switch (config) { + default: + case dusk::AudioOutputMode::StereoSpeakers: + case dusk::AudioOutputMode::StereoHeadphones: + return 2; + case dusk::AudioOutputMode::Surround6ch: + return 6; + case dusk::AudioOutputMode::Surround8ch: + return 8; + } +} - constexpr SDL_AudioSpec spec = { +static bool InitSDL3Output() { + const auto speakerConfig = dusk::getSettings().audio.outputMode.getValue(); + const auto desiredChannelCount = GetChannelCountForOutputMode(speakerConfig); + const bool hrtf = speakerConfig == dusk::AudioOutputMode::StereoHeadphones; + + if (PlaybackStream && desiredChannelCount == OutChannelCount) { + JASCriticalSection section; + EnableHrtf = hrtf; + return false; + } + + if (PlaybackStream) { + SDL_PauseAudioStreamDevice(PlaybackStream); + SDL_DestroyAudioStream(PlaybackStream); + } else { + SDL_Init(SDL_INIT_AUDIO); + } + + const SDL_AudioSpec spec = { SDL_AUDIO_F32, - 2, + static_cast(desiredChannelCount), SampleRate, }; - PlaybackStream = SDL_OpenAudioDeviceStream( - SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, - &spec, - &GetNewAudio, - nullptr); + SDL_AudioStream* newStream = + SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, &GetNewAudio, nullptr); + + { + JASCriticalSection section; + EnableHrtf = hrtf; + OutChannelCount = desiredChannelCount; + PlaybackStream = newStream; + } + + return true; } void dusk::audio::Initialize() { @@ -72,6 +106,22 @@ void dusk::audio::Initialize() { SDL_ResumeAudioStreamDevice(PlaybackStream); } +void dusk::audio::Reinitialize() { + // don't re-init unless we've initialized first (using PlaybackStream being set as proxy) + if (PlaybackStream && InitSDL3Output()) { + SDL_ResumeAudioStreamDevice(PlaybackStream); + } +} + +void dusk::audio::Shutdown() { + if (PlaybackStream) { + SDL_DestroyAudioStream(PlaybackStream); + PlaybackStream = nullptr; + } + + SDL_QuitSubSystem(SDL_INIT_AUDIO); +} + void dusk::audio::SetMasterVolume(const f32 value) { JASCriticalSection section; @@ -113,53 +163,58 @@ int RenderNewAudioFrame() { ZoneScoped; JASCriticalSection section; const u32 countSubframes = JASDriver::getSubFrames(); + int bytesWritten = 0; JASAudioThread::setDSPSyncCount(countSubframes); for (u32 i = 0; i < countSubframes; i++) { - RenderAudioSubframe(); + bytesWritten += RenderAudioSubframe(); JASAudioThread::snIntCount -= 1; } - return static_cast(countSubframes) * sizeof(OutputSubframe); + return bytesWritten; } static void InterleaveOutputData(const OutputSubframe& data, std::span target) { - assert(target.size() >= data.channels[0].size() * OutputSubframe::NUM_CHANNELS); + assert(target.size() >= data.channels[0].size() * OutChannelCount); size_t outPos = 0; for (size_t inPos = 0; inPos < data.channels[0].size(); inPos++) { - for (size_t channelIdx = 0; channelIdx < OutputSubframe::NUM_CHANNELS; channelIdx++) { + for (size_t channelIdx = 0; channelIdx < OutChannelCount; channelIdx++) { target[outPos++] = data.channels[channelIdx][inPos]; } } } -void RenderAudioSubframe() { +int RenderAudioSubframe() { ZoneScoped; OutBuffer = {}; JASDriver::updateDSP(); DspRender(OutBuffer); + std::span OutInterleaveBuffer{OutInterleaveBufferFull.data(), static_cast(DSP_SUBFRAME_SIZE * OutChannelCount)}; InterleaveOutputData(OutBuffer, OutInterleaveBuffer); if (JASDriver::extMixCallback != nullptr && JASDriver::sMixMode == MIX_MODE_INTERLEAVE) { - static_assert(OutputSubframe::NUM_CHANNELS == 2); // This code only works with Stereo so far. // NOTE: In the real game, this gets called on the entire audio frame, rather than the subframe. // That's probably more efficient, but I didn't wanna change the code to calculate the // entire audio buffers at once. // This is only used for the movie player, and it seems to work fine with the smaller calls. const auto mixData = JASDriver::extMixCallback(DSP_SUBFRAME_SIZE); if (mixData) { - for (int i = 0; i < OutInterleaveBuffer.size(); i++) { - OutInterleaveBuffer[i] += static_cast(mixData[i]) / static_cast(0x7FFF); + for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) { + const auto oi = i * OutChannelCount; + OutInterleaveBuffer[oi] += static_cast(mixData[i * 2]) / 32767.0f; + OutInterleaveBuffer[oi + 1] += static_cast(mixData[i * 2 + 1]) / 32767.0f; } } } - SDL_PutAudioStreamData(PlaybackStream, &OutInterleaveBuffer, sizeof(OutInterleaveBuffer)); + auto bytesToWrite = OutInterleaveBuffer.size_bytes(); + SDL_PutAudioStreamData(PlaybackStream, OutInterleaveBuffer.data(), bytesToWrite); + return bytesToWrite; } u32 dusk::audio::GetResetCount(int channelIdx) { diff --git a/src/dusk/audio/DuskAudioSystem.h b/src/dusk/audio/DuskAudioSystem.h index 780187f9d2..8a9f9634b7 100644 --- a/src/dusk/audio/DuskAudioSystem.h +++ b/src/dusk/audio/DuskAudioSystem.h @@ -19,6 +19,10 @@ namespace dusk::audio { */ void Initialize(); + void Reinitialize(); + + void Shutdown(); + void SetEnableReverb(bool value); void SetMasterVolume(f32 value); diff --git a/src/dusk/audio/DuskDsp.cpp b/src/dusk/audio/DuskDsp.cpp index 3aea53d9c9..cb177c53b0 100644 --- a/src/dusk/audio/DuskDsp.cpp +++ b/src/dusk/audio/DuskDsp.cpp @@ -16,6 +16,7 @@ #include #include #include +#include using namespace dusk::audio; @@ -50,7 +51,7 @@ bool dusk::audio::EnableReverb = true; bool dusk::audio::DumpAudio = false; bool dusk::audio::EnableHrtf = false; f32 dusk::audio::HrtfGain = 0.5f; - +u8 dusk::audio::OutChannelCount = 0; // 3dB at 5kHz. static constexpr f32 HRTF_LP_K = 0.75f; @@ -101,28 +102,6 @@ static u32 ConvertSamplesToDataLength(const JASDsp::TChannel& channel, u32 sampl return (samples / channel.mSamplesPerBlock) * BlockBytes(channel); } -/** - * Render the audio data contributed by a single DSP channel. Reads & decodes new input samples. - */ -static void RenderChannel( - JASDsp::TChannel& channel, - 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. - */ -constexpr static int PitchToSampleRate(u16 value) { - return static_cast(static_cast(SampleRate) * value / 4096); -} - /** * Reset state for a DSP channel between independent playbacks. */ @@ -164,6 +143,12 @@ static void MixSubframe(DspSubframe& dst, const DspSubframe& src) { } } +static void MixOutputSubframe(OutputSubframe& dst, const OutputSubframe& src) { + for (int i = 0; i < OutChannelCount; i++) { + MixSubframe(dst.channels[i], src.channels[i]); + } +} + enum class OscType : u16 { SQUARE_WAVE_PW_50 = 0, SAW_WAVE = 1, @@ -203,16 +188,14 @@ static void GenerateEvolvingHarmonic() { } } - static void RenderOscChannel( JASDsp::TChannel& channel, ChannelAuxData& channelAux, - OutputSubframe& subframe) { + DspSubframe& buf) { if (channel.mResetFlag) ResetChannel(channel, channelAux); const u32 pitch = channel.mPitch; - DspSubframe buf = {}; const auto oscType = static_cast(channel.mBytesPerBlock); switch (oscType) { @@ -270,142 +253,6 @@ static void RenderOscChannel( 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) { - sDumpWasActive = DumpAudio; - if (DumpAudio) { - OpenChannelDumpFiles(); - } else { - CloseChannelDumpFiles(); - } - } - - GenerateEvolvingHarmonic(); - - std::span channels(JASDsp::CH_BUF, DSP_CHANNELS); - - DspSubframe reverbInputL = {}; - DspSubframe reverbInputR = {}; - bool anyReverbInput = false; - - DspSubframe surroundBus = {}; - bool anySurroundInput = false; - - for (int i = 0; i < channels.size(); i++) { - auto& channel = channels[i]; - auto& channelAux = ChannelAux[i]; - - if (!channel.mIsActive) { - continue; - } - else if (channel.mPauseFlag) { - // Not really sure what the practical difference between pause and - // deactivation is. Either avoids clearing state or allows the DSP to avoid popping? - continue; - } - else if (channel.mForcedStop) { - channel.mIsFinished = true; - continue; - } - - OutputSubframe channelSubframe = {}; - if (channel.mWaveAramAddress == 0 && !channel.mAramBaseAddress) { - 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. - // this way the reverb's internal buffers accumulate energy proportional to mAutoMixerFxMix, - // so any tail always decays at the correct level regardless of mAutoMixerFxMix changes - // prevents transients when the next sound starts playing with a different reverb level - // 600.0f was pulled out of my ass and just sounds good enough for console - f32 inputGain = (channel.mAutoMixerFxMix >> 8) / 600.0f; - if (inputGain > 0) { - anyReverbInput = true; - for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { - reverbInputL[j] += channelSubframe.channels[0][j] * inputGain; - reverbInputR[j] += channelSubframe.channels[1][j] * inputGain; - } - } - } - - if (EnableHrtf && channel.mAutoMixerBeenSet) { - f32 dolby = (channel.mAutoMixerPanDolby & 0xFF) / 127.0f; - if (dolby > 0.0f) { - anySurroundInput = true; - f32 extract = dolby * HRTF_EXTRACT_MAX; - f32 frontScale = 1.0f - extract; - for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { - f32 mono = (channelSubframe.channels[0][j] + channelSubframe.channels[1][j]) * 0.5f; - surroundBus[j] += mono * extract; - channelSubframe.channels[0][j] *= frontScale; - channelSubframe.channels[1][j] *= frontScale; - } - } - } - - if (DumpAudio && sChannelDumpFiles[i]) { - f32 interleaved[DSP_SUBFRAME_SIZE * 2]; - for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { - interleaved[j * 2 + 0] = channelSubframe.channels[0][j]; - interleaved[j * 2 + 1] = channelSubframe.channels[1][j]; - } - fwrite(interleaved, sizeof(f32), DSP_SUBFRAME_SIZE * 2, sChannelDumpFiles[i]); - } - - for (int o = 0; o < subframe.channels.size(); o++) { - MixSubframe(subframe.channels[o], channelSubframe.channels[o]); - } - } - - if (EnableReverb && (anyReverbInput || ReverbHasTail)) { - // Equivalent to -80 dBFS: rms = 1e-4, rms^2 = 1e-8, sumSq = 2 * N * 1e-8 - constexpr f32 REVERB_ENERGY_EPSILON = 2.0f * DSP_SUBFRAME_SIZE * 1e-8f; - f32 wetEnergy = SharedReverb.processmix( - reverbInputL.data(), reverbInputR.data(), - subframe.channels[0].data(), subframe.channels[1].data(), - DSP_SUBFRAME_SIZE, 1, 1.0f - ); - ReverbHasTail = wetEnergy >= REVERB_ENERGY_EPSILON; - } - - if (EnableHrtf && anySurroundInput) { - // Two-pole LPF: -12 dB/oct above 3 kHz - for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { - sHrtfLp1 = (1.0f - HRTF_LP_K) * sHrtfLp1 + HRTF_LP_K * surroundBus[j]; - sHrtfLp2 = (1.0f - HRTF_LP_K) * sHrtfLp2 + HRTF_LP_K * sHrtfLp1; - surroundBus[j] = sHrtfLp2; - } - - // Mix into L and R - // L gets the filtered signal directly; R gets it allpass for mild decorrelation - for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { - f32 s = surroundBus[j]; - - subframe.channels[0][j] += s * HrtfGain; - - f32 r = -HRTF_ALLPASS_G * s + sHrtfApIn1 + HRTF_ALLPASS_G * sHrtfApOut1; - sHrtfApIn1 = s; - sHrtfApOut1 = r; - subframe.channels[1][j] += r * HrtfGain; - } - } - - for (auto& channel : subframe.channels) { - ApplyVolume(channel, channel, PrevMasterVolume, MasterVolume); - } - PrevMasterVolume = MasterVolume; } /** @@ -540,135 +387,19 @@ static void FillDecodeBuf(JASDsp::TChannel& channel, ChannelAuxData& aux, int ne } /** - * Get the expected BusConnect value needed to define the given output channel in a DSP channel. - */ -constexpr u16 GetBusConnect(const OutputChannel channel) { - switch (channel) { - // TODO: This is a guess for now. - case OutputChannel::LEFT: - return 0x0D00; - case OutputChannel::RIGHT: - return 0x0D60; - default: - CRASH("Invalid output channel!"); - } -} - -/** - * For a DSP channel the JASDsp::OutputChannelConfig value targeting the given output channel. - * Returns null if the DSP channel does not output to this output channel. - */ -static const JASDsp::OutputChannelConfig* GetOutputConfig( - const JASDsp::TChannel& sourceChannel, - OutputChannel channel) { - - auto busConnect = GetBusConnect(channel); - for (const auto& mOutputChannel : sourceChannel.mOutputChannels) { - auto config = &mOutputChannel; - if (config->mBusConnect == busConnect) { - return config; - } - } - - return nullptr; -} - -struct VolumeValue { - f32 Target; - f32 Init; -}; - -/** - * Get the volume that the given DSP channel should render to the given output channel at. - */ -static VolumeValue GetVolumeForOutputChannel( - const JASDsp::TChannel& sourceChannel, - OutputChannel outputChannel) { - - u16 volume; - u16 initVolume; - f32 panValue = 1; - if (sourceChannel.mAutoMixerBeenSet) { - volume = sourceChannel.mAutoMixerVolume; - initVolume = sourceChannel.mAutoMixerInitVolume; - - auto autoMixerPan = static_cast(sourceChannel.mAutoMixerPanDolby >> 8) / 127; - - switch (outputChannel) { - case OutputChannel::LEFT: - panValue = 1 - autoMixerPan; - break; - case OutputChannel::RIGHT: - panValue = autoMixerPan; - break; - default: - CRASH("Unhandled output channel: OutputChannel"); - } - - } else { - auto config = GetOutputConfig(sourceChannel, outputChannel); - if (config == nullptr) { - return {0, 0}; - } - - volume = config->mTargetVolume; - initVolume = config->mCurrentVolume; - } - - // TODO: interpolate to avoid popping. - f32 targetRatio = VolumeFromU16(volume); - targetRatio *= panValue; - - f32 initRatio = VolumeFromU16(initVolume); - initRatio *= panValue; - - return {targetRatio, initRatio}; -} - -/** - * Given decoded & resampled input samples, render a DSP channel to a given output channel. - */ -static void RenderOutputChannel( - const JASDsp::TChannel& sourceChannel, - ChannelAuxData& aux, - OutputChannel outputChannel, - const std::span inputSamples, - OutputSubframe& fullOutputSubframe) { - - auto& outputSubframe = fullOutputSubframe[outputChannel]; - assert(inputSamples.size() <= outputSubframe.size()); - - auto volume = GetVolumeForOutputChannel(sourceChannel, outputChannel); - - f32 targetVolume = volume.Target; - auto& prevVolume = aux.PrevVolume(outputChannel); - if (std::isnan(prevVolume)) { - // Initialize previous volume to new volume on first render. - prevVolume = volume.Init; - } - - if (prevVolume == 0 && targetVolume == 0) { - return; - } - - ApplyVolume(outputSubframe, inputSamples, prevVolume, targetVolume); - prevVolume = targetVolume; -} - -/** - * Fetch, decode, resample, output + * Render the audio data contributed by a single DSP channel. Reads & decodes new input samples. */ static void RenderChannel( JASDsp::TChannel& channel, ChannelAuxData& channelAux, - OutputSubframe& subframe) { + DspSubframe& buf) { if (channel.mResetFlag) { ResetChannel(channel, channelAux); } // how many input samples we step per output sample, aka the resampling ratio - f32 step = (f32)PitchToSampleRate(channel.mPitch) / SampleRate; + auto step = static_cast(channel.mPitch) / 4096.0f; // how many input samples to resample to DSP_SUBFRAME_SIZE output samples int needed = static_cast(channelAux.resamplePos + DSP_SUBFRAME_SIZE * step) + 2; @@ -680,7 +411,6 @@ static void RenderChannel( channel.mIsFinished = true; } - DspSubframe audioLoadBuffer = {}; f32 pos = channelAux.resamplePos; s16 prev = channelAux.resamplePrev; s16 next = channelAux.decodeBufCount > 0 ? channelAux.decodeBuf[0] : prev; @@ -688,7 +418,7 @@ static void RenderChannel( // linear resampling and f32 conversion for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) { - audioLoadBuffer[i] = (prev + pos * (next - prev)) / 32768.0f; + buf[i] = (prev + pos * (next - prev)) / 32768.0f; pos += step; while (pos >= 1.0f) { pos -= 1.0f; @@ -706,7 +436,7 @@ static void RenderChannel( // IIR part 1, low-pass: out[n] = (in[n] - in[n-1]) * (coeff/128) + out[n-1] if (s16 coeff = channel.iir_filter_params[4]; coeff != 0) { - for (f32& sample : audioLoadBuffer) { + for (f32& sample : buf) { f32 out = std::clamp( (sample - channelAux.prev_lp_in) * ((f32)coeff / 128.0f) + channelAux.prev_lp_out, -1.0f, 1.0f ); @@ -718,7 +448,7 @@ static void RenderChannel( // IIR part 2, biquad: out[n] = (b1*in[n-1] + b2*in[n-2] + a1*out[n-1] + a2*out[n-2]) / 32768 if ((channel.mFilterMode & 0x20) != 0) { - for (f32& sample : audioLoadBuffer) { + for (f32& sample : buf) { f32 out = std::clamp(( channel.iir_filter_params[0] * channelAux.biq_in1 + // b1 channel.iir_filter_params[1] * channelAux.biq_in2 + // b2 @@ -741,28 +471,18 @@ static void RenderChannel( } channelAux.decodeBufCount = std::max(0, remainingDecodeBuf); - - auto hasReadSamples = std::span(audioLoadBuffer).subspan(0, DSP_SUBFRAME_SIZE); - - static_assert(OutputSubframe::NUM_CHANNELS == 2, "Keep RenderChannel in sync!"); - - RenderOutputChannel(channel, channelAux, OutputChannel::LEFT, hasReadSamples, subframe); - RenderOutputChannel(channel, channelAux, OutputChannel::RIGHT, hasReadSamples, subframe); } -void dusk::audio::DspInit() { - SharedReverb.setwet(1.0f); - SharedReverb.setdry(0.0f); - SharedReverb.setroomsize(0.5f); - SharedReverb.setdamp(0.7f); - SharedReverb.setwidth(1.0f); - SharedReverb.setmode(0.0f); - SharedReverb.mute(); -} +struct VolumeValue { + f32 Target; + f32 Init; +}; -void dusk::audio::ApplyVolume( +using VolumeArray = std::array; + +static void ApplyVolume( std::span dst, - const std::span src, + std::span src, const f32 startVolume, const f32 endVolume) { assert(dst.size() >= src.size()); @@ -778,3 +498,391 @@ void dusk::audio::ApplyVolume( } } } + +struct SpeakerPlacement { + OutputChannel channel; + f32 angle; +}; + +struct SpeakerPair { + OutputChannel first; + OutputChannel second; + f32 lo; + f32 span; +}; + +template +constexpr auto BuildSpeakerPairs(const std::array& config) { + std::array pairs = {}; + constexpr f32 kDegToRad = std::numbers::pi_v / 180.0f; + + for (std::size_t i = 0; i < N; i++) { + std::size_t j = (i + 1) % N; + float lo = config[i].angle; + float hi = config[j].angle; + float span = hi - lo; + if (span < 0) span += 360.0f; + + pairs[i] = {config[i].channel, config[j].channel, lo * kDegToRad, span * kDegToRad}; + } + + return pairs; +} + +constexpr auto Placement6ch = std::to_array({ + // the "rear" channels are actually surround left/right, + // changed to match SDL order + {OutputChannel::FRONT_RIGHT, -30}, + {OutputChannel::FRONT_CENTER, 0}, + {OutputChannel::FRONT_LEFT, 30}, + {OutputChannel::REAR_LEFT, 120}, + {OutputChannel::REAR_RIGHT, -120}, +}); + +constexpr auto Placement8ch = std::to_array({ + {OutputChannel::SURROUND_RIGHT, -90}, + {OutputChannel::FRONT_RIGHT, -30}, + {OutputChannel::FRONT_CENTER, 0}, + {OutputChannel::FRONT_LEFT, 30}, + {OutputChannel::SURROUND_LEFT, 90}, + {OutputChannel::REAR_LEFT, 150}, + {OutputChannel::REAR_RIGHT, -150}, +}); + +constexpr auto Pairs6ch = BuildSpeakerPairs(Placement6ch); +constexpr auto Pairs8ch = BuildSpeakerPairs(Placement8ch); + +static void CalcStereoChannelVolumes( + const JASDsp::TChannel& voice, + VolumeArray& volumes) +{ + const auto volume = VolumeFromU16(voice.mAutoMixerVolume); + const auto initVolume = VolumeFromU16(voice.mAutoMixerInitVolume); + + const auto right = static_cast(voice.mAutoMixerPanDolby >> 8) / 127.0f; + const auto left = 1.0f - right; + + volumes[0] = {left * volume, left * initVolume}; + volumes[1] = {right * volume, right * initVolume}; +} + +static void CalcSurroundChannelVolumes( + const JASDsp::TChannel& voice, + VolumeArray& volumes) +{ + constexpr f32 kTurn = 2.0f * std::numbers::pi_v; + + const auto omniGain = 1.0f / static_cast(OutChannelCount - 1); + const auto pan = static_cast(voice.mAutoMixerPanDolby >> 8) / 63.5f - 1.0f; + const auto dolby = static_cast(voice.mAutoMixerPanDolby & 0xFF) / 63.5f - 1.0f; + const auto focus = std::min(std::sqrt(pan * pan + dolby * dolby), 1.0f); + + f32 angle = std::atan2(-pan, -dolby); + angle = std::fmod(angle, kTurn); + if (angle < 0) angle += kTurn; + + std::array gains = {}; + gains.fill(omniGain * (1.0f - focus)); + + using Pairs = std::span; + const auto pairs = OutChannelCount == 6 ? Pairs{Pairs6ch} : Pairs{Pairs8ch}; + + for (const auto& pair : pairs) { + const auto offset = std::fmod(angle - pair.lo + kTurn, kTurn); + + if (offset <= pair.span) { + const auto first = static_cast(pair.first); + const auto second = static_cast(pair.second); + const auto t = std::clamp(offset / pair.span, 0.0f, 1.0f); + + const auto firstGain = std::cos(t * std::numbers::pi_v / 2.0f); + const auto secondGain = std::sin(t * std::numbers::pi_v / 2.0f); + + gains[first] += focus * firstGain; + gains[second] += focus * secondGain; + + break; + } + } + + const auto volume = VolumeFromU16(voice.mAutoMixerVolume); + const auto initVolume = VolumeFromU16(voice.mAutoMixerInitVolume); + + for (size_t i = 0; i < OutChannelCount; i++) { + volumes[i].Target = volume * gains[i]; + volumes[i].Init = initVolume * gains[i]; + } +} + +static void ApplyPanning( + const JASDsp::TChannel& voice, + ChannelAuxData& aux, + const DspSubframe& input, + OutputSubframe& output) +{ + VolumeArray volumes = {}; + + if (voice.mAutoMixerBeenSet) { + if (OutChannelCount > 2) { + CalcSurroundChannelVolumes(voice, volumes); + } else { + CalcStereoChannelVolumes(voice, volumes); + } + } else { + for (const auto& outChannel : voice.mOutputChannels) { + std::optional ch; + + switch (outChannel.mBusConnect) { + case 0x0D00: + ch = OutputChannel::FRONT_LEFT; + break; + case 0x0D60: + ch = OutputChannel::FRONT_RIGHT; + break; + default: + break; + } + + if (ch) { + auto& v = volumes[static_cast(*ch)]; + v.Target = VolumeFromU16(outChannel.mTargetVolume); + v.Init = VolumeFromU16(outChannel.mCurrentVolume); + } + } + } + + for (size_t i = 0; i < OutChannelCount; i++) { + const auto ch = static_cast(i); + if (ch == OutputChannel::LFE) { + continue; + } + + const auto& volume = volumes[i]; + + const f32 targetVolume = volume.Target; + auto& prevVolume = aux.PrevVolume(ch); + + if (std::isnan(prevVolume)) { + // Initialize previous volume to new volume on first render. + prevVolume = volume.Init; + } + + if (prevVolume == 0 && targetVolume == 0) { + continue; + } + + ApplyVolume(output[ch], input, prevVolume, targetVolume); + prevVolume = targetVolume; + } +} + +static void DownmixSurroundToStereo( + const OutputSubframe& input, + OutputSubframe& output) +{ + auto& left = output.channels[0]; + auto& right = output.channels[1]; + for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) { + const auto fc = input.channels[2][i] * 0.5f; + left[i] = input.channels[0][i] + fc + input.channels[4][i]; + right[i] = input.channels[1][i] + fc + input.channels[5][i]; + if (OutChannelCount > 6) { + left[i] += input.channels[6][i]; + right[i] += input.channels[7][i]; + left[i] /= 3.5f; + right[i] /= 3.5f; + } else { + left[i] /= 2.5f; + right[i] /= 2.5f; + } + } +} + +static void UpmixStereoToSurroundInplace(OutputSubframe& buf) { + // pseudoinverse of downmix matrix + const auto w = OutChannelCount > 6 ? 1.0f / 12.0f : 1.0f / 8.0f; + for (int i = 0; i < DSP_SUBFRAME_SIZE; i++) { + const auto le = buf.channels[0][i] * (1.0f + w) + buf.channels[1][i] * -w; + const auto re = buf.channels[1][i] * (1.0f + w) + buf.channels[0][i] * -w; + const auto c = buf.channels[0][i] * 0.5f + buf.channels[1][i] * 0.5f; + /* FL */ buf.channels[0][i] = le; + /* FR */ buf.channels[1][i] = re; + /* FC */ buf.channels[2][i] = c; + /* LFE */ buf.channels[3][i] = 0.0f; + /* BL */ buf.channels[4][i] = le; + /* BR */ buf.channels[5][i] = re; + if (OutChannelCount > 6) { + /* SL */ buf.channels[6][i] = le; + /* SR */ buf.channels[7][i] = re; + } + } +} + +static void AccumulateReverbInput( + DspSubframe& dstL, DspSubframe& dstR, + const DspSubframe& srcL, const DspSubframe& srcR, + f32 gain) +{ + for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { + dstL[j] += srcL[j] * gain; + dstR[j] += srcR[j] * gain; + } +} + +void dusk::audio::DspInit() { + SharedReverb.setwet(1.0f); + SharedReverb.setdry(0.0f); + SharedReverb.setroomsize(0.5f); + SharedReverb.setdamp(0.7f); + SharedReverb.setwidth(1.0f); + SharedReverb.setmode(0.0f); + SharedReverb.mute(); +} + +void dusk::audio::DspRender(OutputSubframe& subframe) { + ZoneScoped; + if (DumpAudio != sDumpWasActive) { + sDumpWasActive = DumpAudio; + if (DumpAudio) { + OpenChannelDumpFiles(); + } else { + CloseChannelDumpFiles(); + } + } + + GenerateEvolvingHarmonic(); + + std::span voices(JASDsp::CH_BUF, DSP_CHANNELS); + + DspSubframe reverbInputL = {}; + DspSubframe reverbInputR = {}; + bool anyReverbInput = false; + + DspSubframe surroundBus = {}; + bool anySurroundInput = false; + + for (int i = 0; i < voices.size(); i++) { + auto& voice = voices[i]; + auto& aux = ChannelAux[i]; + + if (!voice.mIsActive) { + continue; + } + else if (voice.mPauseFlag) { + // Not really sure what the practical difference between pause and + // deactivation is. Either avoids clearing state or allows the DSP to avoid popping? + continue; + } + else if (voice.mForcedStop) { + voice.mIsFinished = true; + continue; + } + + DspSubframe monoBuf = {}; + if (voice.mWaveAramAddress == 0 && !channel.mAramBaseAddress) { + RenderOscChannel(voice, aux, monoBuf); + } else { + ValidateChannel(voice); + RenderChannel(voice, aux, monoBuf); + } + + OutputSubframe buf = {}; + ApplyPanning(voice, aux, monoBuf, buf); + + if (EnableReverb) { + // scale the input to the reverb rather than using wet/dry on the output. + // this way the reverb's internal buffers accumulate energy proportional to mAutoMixerFxMix, + // so any tail always decays at the correct level regardless of mAutoMixerFxMix changes + // prevents transients when the next sound starts playing with a different reverb level + // 600.0f was pulled out of my ass and just sounds good enough for console + f32 inputGain = (voice.mAutoMixerFxMix >> 8) / 600.0f; + if (inputGain > 0) { + anyReverbInput = true; + if (OutChannelCount > 2) { + OutputSubframe downmix; + DownmixSurroundToStereo(buf, downmix); + AccumulateReverbInput(reverbInputL, reverbInputR, downmix.channels[0], downmix.channels[1], inputGain); + } else { + AccumulateReverbInput(reverbInputL, reverbInputR, buf.channels[0], buf.channels[1], inputGain); + } + } + } + + if (EnableHrtf && voice.mAutoMixerBeenSet) { + f32 dolby = (voice.mAutoMixerPanDolby & 0xFF) / 127.0f; + if (dolby > 0.0f) { + anySurroundInput = true; + f32 extract = dolby * HRTF_EXTRACT_MAX; + f32 frontScale = 1.0f - extract; + for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { + f32 mono = (buf.channels[0][j] + buf.channels[1][j]) * 0.5f; + surroundBus[j] += mono * extract; + buf.channels[0][j] *= frontScale; + buf.channels[1][j] *= frontScale; + } + } + } + + if (DumpAudio && sChannelDumpFiles[i]) { + f32 interleaved[DSP_SUBFRAME_SIZE * 2]; + for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { + interleaved[j * 2 + 0] = buf.channels[0][j]; + interleaved[j * 2 + 1] = buf.channels[1][j]; + } + fwrite(interleaved, sizeof(f32), DSP_SUBFRAME_SIZE * 2, sChannelDumpFiles[i]); + } + + MixOutputSubframe(subframe, buf); + } + + if (EnableReverb && (anyReverbInput || ReverbHasTail)) { + // Equivalent to -80 dBFS: rms = 1e-4, rms^2 = 1e-8, sumSq = 2 * N * 1e-8 + constexpr f32 REVERB_ENERGY_EPSILON = 2.0f * DSP_SUBFRAME_SIZE * 1e-8f; + f32 wetEnergy = 0.0f; + if (OutChannelCount > 2) { + OutputSubframe reverbOut; + wetEnergy = SharedReverb.processreplace( + reverbInputL.data(), reverbInputR.data(), + reverbOut.channels[0].data(), reverbOut.channels[1].data(), + DSP_SUBFRAME_SIZE, 1, 1.0f + ); + UpmixStereoToSurroundInplace(reverbOut); + MixOutputSubframe(subframe, reverbOut); + } else { + wetEnergy = SharedReverb.processmix( + reverbInputL.data(), reverbInputR.data(), + subframe.channels[0].data(), subframe.channels[1].data(), + DSP_SUBFRAME_SIZE, 1, 1.0f + ); + } + ReverbHasTail = wetEnergy >= REVERB_ENERGY_EPSILON; + } + + if (EnableHrtf && anySurroundInput) { + // Two-pole LPF: -12 dB/oct above 3 kHz + for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { + sHrtfLp1 = (1.0f - HRTF_LP_K) * sHrtfLp1 + HRTF_LP_K * surroundBus[j]; + sHrtfLp2 = (1.0f - HRTF_LP_K) * sHrtfLp2 + HRTF_LP_K * sHrtfLp1; + surroundBus[j] = sHrtfLp2; + } + + // Mix into L and R + // L gets the filtered signal directly; R gets it allpass for mild decorrelation + for (int j = 0; j < DSP_SUBFRAME_SIZE; j++) { + f32 s = surroundBus[j]; + + subframe.channels[0][j] += s * HrtfGain; + + f32 r = -HRTF_ALLPASS_G * s + sHrtfApIn1 + HRTF_ALLPASS_G * sHrtfApOut1; + sHrtfApIn1 = s; + sHrtfApOut1 = r; + subframe.channels[1][j] += r * HrtfGain; + } + } + + for (int i = 0; i < OutChannelCount; i++) { + auto& channel = subframe.channels[i]; + ApplyVolume(channel, channel, PrevMasterVolume, MasterVolume); + } + PrevMasterVolume = MasterVolume; +} diff --git a/src/dusk/audio/DuskDsp.hpp b/src/dusk/audio/DuskDsp.hpp index 8d7b74a1d6..bef422b956 100644 --- a/src/dusk/audio/DuskDsp.hpp +++ b/src/dusk/audio/DuskDsp.hpp @@ -8,14 +8,20 @@ #include #include -#include namespace dusk::audio { constexpr int SampleRate = 32000; enum class OutputChannel : u8 { - LEFT, - RIGHT, + // same as SDL channel layout for 7.1 + FRONT_LEFT, + FRONT_RIGHT, + FRONT_CENTER, + LFE, + REAR_LEFT, + REAR_RIGHT, + SURROUND_LEFT, + SURROUND_RIGHT, OutputChannel_MAX }; @@ -122,16 +128,11 @@ namespace dusk::audio { return channel.mBytesPerBlock; } - /** - * Apply a volume level to audio data. - * Interpolates across the two provided volume levels to avoid clicking. - */ - void ApplyVolume(std::span dst, std::span src, f32 startVolume, f32 endVolume); - extern f32 MasterVolume; extern f32 PrevMasterVolume; extern bool EnableReverb; extern bool DumpAudio; extern bool EnableHrtf; extern f32 HrtfGain; + extern u8 OutChannelCount; } diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index b9bbe88b70..1749e964d7 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -316,6 +316,7 @@ template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; +template class ConfigImpl; template <> void ConfigImpl::loadFromJson( @@ -639,4 +640,4 @@ void shutdown() { s_activeChangeNotifications.clear(); } -} // namespace dusk::config \ No newline at end of file +} // namespace dusk::config diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp deleted file mode 100644 index ca30ec0db5..0000000000 --- a/src/dusk/frame_interpolation.cpp +++ /dev/null @@ -1,463 +0,0 @@ -#include "dusk/frame_interpolation.h" - -#include "f_op/f_op_camera_mng.h" -#include "m_Do/m_Do_graphic.h" -#include "mtx.h" - -#include - -namespace { - -struct Recording { - absl::flat_hash_map matrix_values; -}; - -bool s_initialized = false; - -bool g_enabled = false; -bool g_recording = false; -bool g_interpolating = false; -bool g_sync_presentation = false; - -float g_step = 0.0f; -bool g_is_sim_frame = false; -bool g_ui_tick_pending = false; -uint64_t g_sim_tick_seq = 0; - -Recording g_current_recording; -Recording g_previous_recording; - -absl::flat_hash_map g_replacements; - -struct CameraSnapshot { - cXyz eye{}; - cXyz center{}; - cXyz up{}; - s16 bank{}; - f32 fovy{}; - f32 aspect{}; - f32 near_{}; - f32 far_{}; - bool wideZoom{}; - bool valid{}; -}; - -CameraSnapshot s_cam_prev{}; -CameraSnapshot s_cam_curr{}; - -view_class s_presentation_view_backup{}; -int s_presentation_depth = 0; - -struct InterpolationCallBackWork { - dusk::frame_interp::InterpolationCallBack pCallBack; - void* pUserWork; -}; - -std::vector s_interpolationCallBackWork; - -void copy_view_to_snap(CameraSnapshot* dst, const view_class& v) { - dst->eye = v.lookat.eye; - dst->center = v.lookat.center; - dst->up = v.lookat.up; - dst->bank = v.bank; - dst->fovy = v.fovy; - dst->aspect = v.aspect; - dst->near_ = v.near_; - dst->far_ = v.far_; - dst->valid = true; -} - -inline void lerp_matrix(Mtx out, const Mtx lhs, const Mtx rhs, float step) { - for (size_t row = 0; row < 3; ++row) { - for (size_t col = 0; col < 4; ++col) { - const float l = lhs[row][col]; - out[row][col] = l + (rhs[row][col] - l) * step; - } - } -} - -inline void lerp_xyz(cXyz* out, const cXyz& lhs, const cXyz& rhs, float step) { - out->x = lhs.x + (rhs.x - lhs.x) * step; - out->y = lhs.y + (rhs.y - lhs.y) * step; - out->z = lhs.z + (rhs.z - lhs.z) * step; -} - -static s16 lerp_bank(s16 a, s16 b, f32 t) { - const f32 ra = S2RAD(a); - const f32 d = remainderf(S2RAD(b) - ra, 2.0f * static_cast(M_PI)); - return cAngle::Radian_to_SAngle(ra + d * t); -} - -inline bool matrix_differs(const Mtx lhs, const Mtx rhs, float epsilon = 0.0001f) { - for (size_t row = 0; row < 3; ++row) { - for (size_t col = 0; col < 4; ++col) { - if (std::abs(lhs[row][col] - rhs[row][col]) > epsilon) { - return true; - } - } - } - return false; -} - -const Mtx* resolve_replacement(const Mtx* source, Mtx* scratch) { - if (!g_interpolating || source == nullptr || dusk::frame_interp::presentation_sync_active()) { - return source; - } - - auto it = g_replacements.find(reinterpret_cast(source)); - if (it == g_replacements.end()) { - return source; - } - - MTXCopy(it->second, *scratch); - return scratch; -} - -bool has_recording_data(const Recording& recording) { - return !recording.matrix_values.empty(); -} - -void clear_replacements() { - g_replacements.clear(); -} - -} // namespace - -namespace dusk::frame_interp { -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); - ++g_sim_tick_seq; -} - -uint64_t sim_tick_seq() { - return g_sim_tick_seq; -} - -void begin_frame(FrameInterpMode mode, bool is_sim_frame, float step) { - g_enabled = mode != FrameInterpMode::Off; - g_is_sim_frame = is_sim_frame; - g_step = std::clamp(step, 0.0f, 1.0f); - if (!g_enabled) { - g_interpolating = false; - clear_replacements(); - } -} - -bool is_enabled() { - return g_enabled; -} - -bool is_sim_frame() { - return g_is_sim_frame; -} - -void begin_record() { - ensure_initialized(); - - if (!g_enabled) { - g_interpolating = false; - g_sync_presentation = false; - g_previous_recording = {}; - g_current_recording = {}; - clear_replacements(); - s_cam_prev.valid = false; - s_cam_curr.valid = false; - return; - } - - g_sync_presentation = false; - g_previous_recording = std::move(g_current_recording); - g_current_recording = {}; - g_recording = true; - g_interpolating = false; - clear_replacements(); - - ::camera_process_class* cam = dComIfGp_getCamera(0); - if (cam == nullptr) { - s_cam_prev.valid = false; - s_cam_curr.valid = false; - return; - } -} - -void end_record() { - g_recording = false; -} - -void interpolate() { - ensure_initialized(); - clear_replacements(); - g_interpolating = g_enabled && !g_recording && !g_sync_presentation && has_recording_data(g_current_recording); - if (!g_interpolating) { - return; - } - for (auto const& old : g_previous_recording.matrix_values) { - if (auto it = g_current_recording.matrix_values.find(old.first); - it != g_current_recording.matrix_values.end()) - { - lerp_matrix(g_replacements[old.first], old.second, it->second, g_step); - } - } -} - -void request_presentation_sync() { - ensure_initialized(); - if (!g_enabled) { - return; - } - g_sync_presentation = true; -} - -bool presentation_sync_active() { - if (!s_initialized || !g_enabled) { - return false; - } - return g_sync_presentation; -} - -float get_interpolation_step() { - ensure_initialized(); - return presentation_sync_active() ? 1.0f : g_step; -} - -void set_ui_tick_pending(bool value) { - if (g_ui_tick_pending == value) { return; } - g_ui_tick_pending = value; -} - -bool get_ui_tick_pending() { - ensure_initialized(); - return g_enabled ? g_ui_tick_pending : true; -} - -void record_final_mtx(Mtx m, const void* key) { - if (!s_initialized || !g_recording || m == nullptr) { - return; - } - - auto& it = g_current_recording.matrix_values[reinterpret_cast(key)]; - MTXCopy(m, it); -} - -void record_final_mtx(Mtx m) { - record_final_mtx(m, m); -} - -bool lookup_replacement(const void* key, Mtx out) { - if (presentation_sync_active() || !g_interpolating || key == nullptr) { - return false; - } - - auto it = g_replacements.find(reinterpret_cast(key)); - if (it == g_replacements.end()) { - return false; - } - - MTXCopy(it->second, out); - return true; -} - -bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out) { - if (presentation_sync_active() || !g_interpolating || lhs == nullptr || rhs == nullptr) { - return false; - } - - Mtx lhs_scratch; - Mtx rhs_scratch; - const Mtx* resolved_lhs = resolve_replacement(reinterpret_cast(lhs), &lhs_scratch); - const Mtx* resolved_rhs = resolve_replacement(reinterpret_cast(rhs), &rhs_scratch); - if (resolved_lhs == reinterpret_cast(lhs) && resolved_rhs == reinterpret_cast(rhs)) { - return false; - } - - MTXConcat(*resolved_lhs, *resolved_rhs, out); - return true; -} - -void record_camera(::camera_process_class* cam, int camera_id) { - if (!g_enabled || camera_id != 0 || cam == nullptr) { - return; - } - copy_view_to_snap(&s_cam_curr, cam->view); -#if WIDESCREEN_SUPPORT - s_cam_curr.wideZoom = mDoGph_gInf_c::isWideZoom(); -#endif -} - -void interp_view(::view_class* view) { - if (!g_enabled) - return; - - if (!s_cam_prev.valid || !s_cam_curr.valid) - return; - - const f32 step = get_interpolation_step(); - const bool is_cam_curr_authoritative = g_is_sim_frame && step <= 0.0f; - - cXyz eye; - cXyz center; - cXyz up; - if (is_cam_curr_authoritative) { - eye = s_cam_curr.eye; - center = s_cam_curr.center; - up = s_cam_curr.up; - } else { - lerp_xyz(&eye, s_cam_prev.eye, s_cam_curr.eye, step); - lerp_xyz(¢er, s_cam_prev.center, s_cam_curr.center, step); - lerp_xyz(&up, s_cam_prev.up, s_cam_curr.up, step); - } - if (!up.normalizeRS()) { - up = s_cam_curr.up; - up.normalizeRS(); - } - - view->lookat.eye = eye; - view->lookat.center = center; - view->lookat.up = up; - if (is_cam_curr_authoritative) { - view->bank = s_cam_curr.bank; - view->fovy = s_cam_curr.fovy; - view->aspect = s_cam_curr.aspect; - view->near_ = s_cam_curr.near_; - view->far_ = s_cam_curr.far_; - } else { - view->bank = lerp_bank(s_cam_prev.bank, s_cam_curr.bank, step); - view->fovy = s_cam_prev.fovy + (s_cam_curr.fovy - s_cam_prev.fovy) * step; - view->aspect = s_cam_prev.aspect + (s_cam_curr.aspect - s_cam_prev.aspect) * step; - view->near_ = s_cam_prev.near_ + (s_cam_curr.near_ - s_cam_prev.near_) * step; - view->far_ = s_cam_prev.far_ + (s_cam_curr.far_ - s_cam_prev.far_) * step; - } - - // FRAME INTERP TODO: It might be better if I rewired the game to not clear this flag until the - // next sim frame, but I don't care enough to right now -#if WIDESCREEN_SUPPORT - const f32 wide_step = is_cam_curr_authoritative ? 1.0f : step; - if (mDoGph_gInf_c::isWide() && !mDoGph_gInf_c::isWideZoom() && wide_step >= 0.5f ? s_cam_curr.wideZoom : s_cam_prev.wideZoom) { - mDoGph_gInf_c::onWideZoom(); - } -#endif -} - -static void run_interpolation_callbacks() { - for (size_t i = 0; i < s_interpolationCallBackWork.size(); i++) { - auto const& work = s_interpolationCallBackWork[i]; - work.pCallBack(g_is_sim_frame, work.pUserWork); - } -} - -void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork) { - if (!is_enabled() || s_presentation_depth > 0 || !g_is_sim_frame) - return; - - s_interpolationCallBackWork.emplace_back(pCallBack, pUserWork); -} - -void begin_presentation_camera() { - ensure_initialized(); - if (!g_enabled) { - return; - } - if (s_presentation_depth > 0) { - s_presentation_depth++; - return; - } - if (!s_cam_prev.valid || !s_cam_curr.valid) { - return; - } - - view_class* const view = dComIfGd_getView(); - if (view == nullptr) { - return; - } - - std::memcpy(&s_presentation_view_backup, view, sizeof(view_class)); - interp_view(view); - - // FRAME INTERP TODO: Largely copied from d_camera's camera_draw function from this point, got any better ideas? - C_MTXPerspective(view->projMtx, view->fovy, view->aspect, view->near_, view->far_); - mDoMtx_lookAt(view->viewMtx, &view->lookat.eye, &view->lookat.center, &view->lookat.up, view->bank); -#if WIDESCREEN_SUPPORT - mDoGph_gInf_c::setWideZoomProjection(view->projMtx); -#endif - j3dSys.setViewMtx(view->viewMtx); - cMtx_inverse(view->viewMtx, view->invViewMtx); - - bool camera_attention_status = dComIfGp_getCameraAttentionStatus(0) & 0x80; - Z2GetAudience()->setAudioCamera(view->viewMtx, view->lookat.eye, view->lookat.center, view->fovy, view->aspect, camera_attention_status, 0, false); - - dBgS_GndChk gndchk; - gndchk.OnWaterGrp(); - gndchk.SetPos(&view->lookat.eye); - f32 cross = dComIfG_Bgsp().GroundCross(&gndchk); - if (cross != -G_CM3D_F_INF) { - if (dComIfG_Bgsp().ChkGrpInf(gndchk, 0x100)) { - mDoAud_getCameraMapInfo(6); - } else { - mDoAud_getCameraMapInfo(dComIfG_Bgsp().GetMtrlSndId(gndchk)); - } - mDoAud_setCameraGroupInfo(dComIfG_Bgsp().GetGrpSoundId(gndchk)); - Vec spDC; - spDC.x = view->lookat.eye.x; - spDC.y = cross; - spDC.z = view->lookat.eye.z; - Z2AudioMgr::getInterface()->setCameraPolygonPos(&spDC); - } else { - Z2AudioMgr::getInterface()->setCameraPolygonPos(nullptr); - } - - MTXCopy(view->viewMtx, view->viewMtxNoTrans); - view->viewMtxNoTrans[0][3] = 0.0f; - view->viewMtxNoTrans[1][3] = 0.0f; - view->viewMtxNoTrans[2][3] = 0.0f; - cMtx_concatProjView(view->projMtx, view->viewMtx, view->projViewMtx); - - f32 far_; - f32 var_f30; - if (dComIfGp_getCameraAttentionStatus(0) & 8) { - far_ = view->far_; - } else { -#if DEBUG - if (g_envHIO.mOther.mAdjustCullFar != 0) { - var_f30 = g_envHIO.mOther.mCullFarValue; - } else -#endif - { - var_f30 = dStage_stagInfo_GetCullPoint(dComIfGp_getStageStagInfo()); - } - far_ = var_f30; - } - - mDoLib_clipper::setup(view->fovy, view->aspect, view->near_, far_); - - // FRAME INTERP NOTE: Removed the call to offWideZoom that was here, it causes problems with presentation during cutscenes. - - s_presentation_depth = 1; - - run_interpolation_callbacks(); -} - -void end_presentation_camera() { - if (s_presentation_depth == 0) { - return; - } - s_presentation_depth--; - if (s_presentation_depth > 0) { - return; - } - - view_class* const view = dComIfGd_getView(); - if (view != nullptr) { - std::memcpy(view, &s_presentation_view_backup, sizeof(view_class)); - } -} -} // namespace dusk::frame_interp diff --git a/src/dusk/frame_interpolation.h b/src/dusk/frame_interpolation.h deleted file mode 100644 index 5f2f3d134b..0000000000 --- a/src/dusk/frame_interpolation.h +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include "settings.h" - -class camera_process_class; -class view_class; - -#ifdef __cplusplus -namespace dusk { -namespace frame_interp { - -void ensure_initialized(); - -void begin_record(); -void end_record(); -void begin_sim_tick(); -uint64_t sim_tick_seq(); -void begin_frame(FrameInterpMode mode, bool is_sim_frame, float step); -void interpolate(); -float get_interpolation_step(); - -void request_presentation_sync(); -bool presentation_sync_active(); - -bool is_enabled(); - -// TODO: These should be phased out as UI is progressively updated to use game_clock -void set_ui_tick_pending(bool value); -bool get_ui_tick_pending(); - -bool is_sim_frame(); - -void record_camera(::camera_process_class* cam, int camera_id); -void interp_view(::view_class* view); -void record_final_mtx(Mtx m, const void *key); -void record_final_mtx(Mtx m); - -bool lookup_replacement(const void* key, Mtx out); -bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out); - -typedef void (*InterpolationCallBack)(bool isSimFrame, void* pUserWork); -// call on a sim tick, will get called during presentation -void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork); - -void begin_presentation_camera(); -void end_presentation_camera(); - -} // namespace frame_interp -} // namespace dusk -#endif diff --git a/src/dusk/game_clock.cpp b/src/dusk/game_clock.cpp index 178ec7803a..3a680fcca8 100644 --- a/src/dusk/game_clock.cpp +++ b/src/dusk/game_clock.cpp @@ -1,7 +1,5 @@ #include "dusk/game_clock.h" -#include "dusk/frame_interpolation.h" - #include #include @@ -13,18 +11,21 @@ namespace dusk::game_clock { using native_clock = aurora::time::native_clock; using game_clock = aurora::time::game_clock; -FrameTiming g_frameTiming; +FrameTiming g_frameTiming{.dt = kUiInitialDt}; namespace { bool s_initialized = false; -bool s_fixedStepActive = false; bool s_simTickActive = false; native_clock::time_point s_previousNativeSample{}; game_clock::time_point s_latestGameSample{}; game_clock::time_point s_currentSnapshotTime{}; game_clock::time_point s_pendingSimTime{}; - std::unordered_map s_intervalLastSample; +uint64_t s_presentationEpoch = 1; +bool s_timingModeInitialized = false; +bool s_previousSeparatePresentation = false; +bool s_previousInterpolating = false; +bool s_previousTimeStopped = false; constexpr game_clock::duration kSimPeriodDuration = std::chrono::duration_cast(std::chrono::duration(kSimPeriod)); @@ -49,6 +50,7 @@ void reset() { s_currentSnapshotTime = s_latestGameSample - kSimPeriodDuration; s_pendingSimTime = s_currentSnapshotTime; s_simTickActive = false; + ++s_presentationEpoch; } void set_sim_rate(float hz) { @@ -65,11 +67,15 @@ const FrameTiming& advance() { const auto nativeNow = native_clock::now(); const auto gameNow = game_clock::now(); const auto nativeFrameGap = nativeNow - s_previousNativeSample; + const auto gameFrameGap = gameNow - s_latestGameSample; s_previousNativeSample = nativeNow; s_latestGameSample = gameNow; auto& out = g_frameTiming; - out = {.dt = std::chrono::duration().count()}; + out = { + .dt = std::chrono::duration(gameFrameGap).count(), + .presentationEpoch = s_presentationEpoch, + }; const float timeScale = aurora::time::scale(); const bool interpolating = @@ -77,7 +83,21 @@ const FrameTiming& advance() { const bool separatePresentation = interpolating || timeScale != 1.0f; out.interpolating = interpolating; out.separatePresentation = separatePresentation; - s_fixedStepActive = separatePresentation; + + const bool timeStopped = timeScale == 0.0f; + const bool timingModeChanged = + s_timingModeInitialized && + (separatePresentation != s_previousSeparatePresentation || + interpolating != s_previousInterpolating || timeStopped != s_previousTimeStopped); + const bool abnormalGap = nativeFrameGap > kAbnormalGapResetThreshold; + if (timingModeChanged || abnormalGap) { + ++s_presentationEpoch; + out.presentationEpoch = s_presentationEpoch; + } + s_timingModeInitialized = true; + s_previousSeparatePresentation = separatePresentation; + s_previousInterpolating = interpolating; + s_previousTimeStopped = timeStopped; if (!separatePresentation) { s_currentSnapshotTime = gameNow; @@ -86,7 +106,7 @@ const FrameTiming& advance() { } const auto simulationTarget = interpolating ? gameNow - kSimPeriodDuration : gameNow; - if (timeScale == 0.f || nativeFrameGap > kAbnormalGapResetThreshold) { + if (timeStopped || abnormalGap) { s_currentSnapshotTime = simulationTarget; out.numSimTicks = 0; return out; @@ -109,8 +129,8 @@ const FrameTiming& advance() { } void begin_sim_tick() { - s_pendingSimTime = - s_fixedStepActive ? s_currentSnapshotTime + kSimPeriodDuration : s_latestGameSample; + s_pendingSimTime = g_frameTiming.separatePresentation ? s_currentSnapshotTime + kSimPeriodDuration : + s_latestGameSample; s_simTickActive = true; } @@ -123,6 +143,10 @@ void commit_sim_tick() { } } +bool is_sim_frame() { + return !g_frameTiming.separatePresentation || s_simTickActive; +} + float sample_interpolation_step() { const float step = std::chrono::duration(game_clock::now() - s_currentSnapshotTime).count() / diff --git a/src/dusk/game_clock.h b/src/dusk/game_clock.h index 5e71ea4c0f..3492456927 100644 --- a/src/dusk/game_clock.h +++ b/src/dusk/game_clock.h @@ -16,6 +16,8 @@ struct FrameTiming { bool separatePresentation; // Number of simulation ticks to run int numSimTicks; + // Changes whenever presentation history must be discarded and re-anchored. + uint64_t presentationEpoch; }; extern FrameTiming g_frameTiming; @@ -26,10 +28,12 @@ void begin_sim_tick(); void commit_sim_tick(); float sample_interpolation_step(); +bool is_sim_frame(); + float consume_interval(const void* consumer); // Sets the effective simulation rate through the game clock time scale. void set_sim_rate(float hz); float get_sim_rate(); -} // namespace dusk::game_clock +} // namespace dusk::game_clock diff --git a/src/dusk/hash.hpp b/src/dusk/hash.hpp new file mode 100644 index 0000000000..da88aa5547 --- /dev/null +++ b/src/dusk/hash.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include +#include +#include + +namespace dusk::hash { + +class Sha256 { +public: + void update(std::span bytes) { + mHasher.process(bytes.begin(), bytes.end()); + } + + std::string finish() { + mHasher.finish(); + return picosha2::get_hash_hex_string(mHasher); + } + +private: + picosha2::hash256_one_by_one mHasher; +}; + +} // namespace dusk::hash diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 97a13efc3d..badf7297c2 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -6,7 +6,6 @@ #include "dusk/action_bindings.h" #include "dusk/config.hpp" #include "dusk/data.hpp" -#include "dusk/frame_interpolation.h" #include "dusk/game_mode.hpp" #include "dusk/livesplit.h" #include "dusk/main.h" diff --git a/src/dusk/interp/camera.cpp b/src/dusk/interp/camera.cpp new file mode 100644 index 0000000000..19ab197214 --- /dev/null +++ b/src/dusk/interp/camera.cpp @@ -0,0 +1,218 @@ +#include "dusk/interp/camera.h" + +#include "dusk/game_clock.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/lerp.h" + +#include "d/d_com_inf_game.h" +#include "f_op/f_op_camera_mng.h" +#include "m_Do/m_Do_graphic.h" + +#include +#include + +namespace { + +struct CameraSnapshot { + cXyz eye{}; + cXyz center{}; + cXyz up{}; + s16 bank{}; + f32 fovy{}; + f32 aspect{}; + f32 near_{}; + f32 far_{}; + bool wideZoom{}; + bool valid{}; +}; + +CameraSnapshot s_camPrev{}; +CameraSnapshot s_camCurr{}; + +view_class s_presentationViewBackup{}; + +void copy_view_to_snap(CameraSnapshot* dst, const view_class& v) { + dst->eye = v.lookat.eye; + dst->center = v.lookat.center; + dst->up = v.lookat.up; + dst->bank = v.bank; + dst->fovy = v.fovy; + dst->aspect = v.aspect; + dst->near_ = v.near_; + dst->far_ = v.far_; + dst->valid = true; +} + +void apply_presented_view(view_class* view) { + // FRAME INTERP TODO: Largely copied from d_camera's camera_draw function from this point, got any + // better ideas? + C_MTXPerspective(view->projMtx, view->fovy, view->aspect, view->near_, view->far_); + mDoMtx_lookAt(view->viewMtx, &view->lookat.eye, &view->lookat.center, &view->lookat.up, + view->bank); +#if WIDESCREEN_SUPPORT + mDoGph_gInf_c::setWideZoomProjection(view->projMtx); +#endif + j3dSys.setViewMtx(view->viewMtx); + cMtx_inverse(view->viewMtx, view->invViewMtx); + + bool camera_attention_status = dComIfGp_getCameraAttentionStatus(0) & 0x80; + Z2GetAudience()->setAudioCamera(view->viewMtx, view->lookat.eye, view->lookat.center, view->fovy, + view->aspect, camera_attention_status, 0, false); + + dBgS_GndChk gndchk; + gndchk.OnWaterGrp(); + gndchk.SetPos(&view->lookat.eye); + f32 cross = dComIfG_Bgsp().GroundCross(&gndchk); + if (cross != -G_CM3D_F_INF) { + if (dComIfG_Bgsp().ChkGrpInf(gndchk, 0x100)) { + mDoAud_getCameraMapInfo(6); + } else { + mDoAud_getCameraMapInfo(dComIfG_Bgsp().GetMtrlSndId(gndchk)); + } + mDoAud_setCameraGroupInfo(dComIfG_Bgsp().GetGrpSoundId(gndchk)); + Vec spDC; + spDC.x = view->lookat.eye.x; + spDC.y = cross; + spDC.z = view->lookat.eye.z; + Z2AudioMgr::getInterface()->setCameraPolygonPos(&spDC); + } else { + Z2AudioMgr::getInterface()->setCameraPolygonPos(nullptr); + } + + MTXCopy(view->viewMtx, view->viewMtxNoTrans); + view->viewMtxNoTrans[0][3] = 0.0f; + view->viewMtxNoTrans[1][3] = 0.0f; + view->viewMtxNoTrans[2][3] = 0.0f; + cMtx_concatProjView(view->projMtx, view->viewMtx, view->projViewMtx); + + f32 far_; + f32 var_f30; + if (dComIfGp_getCameraAttentionStatus(0) & 8) { + far_ = view->far_; + } else { +#if DEBUG + if (g_envHIO.mOther.mAdjustCullFar != 0) { + var_f30 = g_envHIO.mOther.mCullFarValue; + } else +#endif + { + var_f30 = dStage_stagInfo_GetCullPoint(dComIfGp_getStageStagInfo()); + } + far_ = var_f30; + } + + mDoLib_clipper::setup(view->fovy, view->aspect, view->near_, far_); + + // FRAME INTERP NOTE: Removed the call to offWideZoom that was here, it causes problems with + // presentation during cutscenes. +} + +} // namespace + +namespace dusk::interp { + +void record_camera(::camera_process_class* cam, int camera_id) { + if (!is_enabled() || camera_id != 0 || cam == nullptr) { + return; + } + copy_view_to_snap(&s_camCurr, cam->view); +#if WIDESCREEN_SUPPORT + s_camCurr.wideZoom = mDoGph_gInf_c::isWideZoom(); +#endif +} + +void interp_view(::view_class* view) { + if (!is_enabled()) + return; + + if (!s_camPrev.valid || !s_camCurr.valid) + return; + + const f32 step = get_interpolation_step(); + const bool is_cam_curr_authoritative = game_clock::is_sim_frame() && step <= 0.0f; + + cXyz eye; + cXyz center; + cXyz up; + if (is_cam_curr_authoritative) { + eye = s_camCurr.eye; + center = s_camCurr.center; + up = s_camCurr.up; + } else { + lerp(eye, s_camPrev.eye, s_camCurr.eye, step); + lerp(center, s_camPrev.center, s_camCurr.center, step); + lerp(up, s_camPrev.up, s_camCurr.up, step); + } + if (!up.normalizeRS()) { + up = s_camCurr.up; + up.normalizeRS(); + } + + view->lookat.eye = eye; + view->lookat.center = center; + view->lookat.up = up; + if (is_cam_curr_authoritative) { + view->bank = s_camCurr.bank; + view->fovy = s_camCurr.fovy; + view->aspect = s_camCurr.aspect; + view->near_ = s_camCurr.near_; + view->far_ = s_camCurr.far_; + } else { + view->bank = lerp(s_camPrev.bank, s_camCurr.bank, step); + view->fovy = s_camPrev.fovy + (s_camCurr.fovy - s_camPrev.fovy) * step; + view->aspect = s_camPrev.aspect + (s_camCurr.aspect - s_camPrev.aspect) * step; + view->near_ = s_camPrev.near_ + (s_camCurr.near_ - s_camPrev.near_) * step; + view->far_ = s_camPrev.far_ + (s_camCurr.far_ - s_camPrev.far_) * step; + } + + // FRAME INTERP TODO: It might be better if I rewired the game to not clear this flag until the + // next sim frame, but I don't care enough to right now +#if WIDESCREEN_SUPPORT + const f32 wide_step = is_cam_curr_authoritative ? 1.0f : step; + if (mDoGph_gInf_c::isWide() && !mDoGph_gInf_c::isWideZoom() && + wide_step >= 0.5f ? s_camCurr.wideZoom : s_camPrev.wideZoom) + { + mDoGph_gInf_c::onWideZoom(); + } +#endif +} + +void camera_on_sim_tick() { + s_camPrev = std::move(s_camCurr); +} + +void camera_invalidate_snapshots() { + s_camPrev.valid = false; + s_camCurr.valid = false; +} + +void camera_on_begin_record() { + if (dComIfGp_getCamera(0) == nullptr) { + camera_invalidate_snapshots(); + } +} + +bool camera_apply_presentation() { + if (!s_camPrev.valid || !s_camCurr.valid) { + return false; + } + + view_class* const view = dComIfGd_getView(); + if (view == nullptr) { + return false; + } + + std::memcpy(&s_presentationViewBackup, view, sizeof(view_class)); + interp_view(view); + apply_presented_view(view); + return true; +} + +void camera_restore_presentation() { + view_class* const view = dComIfGd_getView(); + if (view != nullptr) { + std::memcpy(view, &s_presentationViewBackup, sizeof(view_class)); + } +} + +} // namespace dusk::interp diff --git a/src/dusk/interp/camera.h b/src/dusk/interp/camera.h new file mode 100644 index 0000000000..e4a1824979 --- /dev/null +++ b/src/dusk/interp/camera.h @@ -0,0 +1,13 @@ +#pragma once + +class camera_process_class; +class view_class; + +#ifdef __cplusplus +namespace dusk::interp { + +void record_camera(::camera_process_class* cam, int camera_id); +void interp_view(::view_class* view); + +} // namespace dusk::interp +#endif diff --git a/src/dusk/interp/dual_buffer.cpp b/src/dusk/interp/dual_buffer.cpp new file mode 100644 index 0000000000..be1160e6a6 --- /dev/null +++ b/src/dusk/interp/dual_buffer.cpp @@ -0,0 +1,65 @@ +#include "dusk/interp/dual_buffer.h" + +#include +#include + +namespace dusk::interp { +namespace { + +struct Slot { + const void* type; + void* ptr; + void (*destroy)(void*); +}; + +using OwnerMap = absl::flat_hash_map>; + +OwnerMap& owner_map() { + static OwnerMap s_map; + return s_map; +} + +} // namespace + +void* detail::acquire(const void* key, const void* type, void* (*make)(), void (*destroy)(void*)) { + const uintptr_t id = reinterpret_cast(key); + auto& slots = owner_map()[id]; + for (Slot& slot : slots) { + if (slot.type == type) { + return slot.ptr; + } + } + + void* ptr = make(); + slots.push_back({type, ptr, destroy}); + return ptr; +} + +void erase_owned_buffers(const void* key) { + if (key == nullptr) { + return; + } + + OwnerMap& stored = owner_map(); + auto it = stored.find(reinterpret_cast(key)); + if (it == stored.end()) { + return; + } + + for (Slot& slot : it->second) { + slot.destroy(slot.ptr); + } + stored.erase(it); +} + +void clear_owned_buffers() { + OwnerMap& stored = owner_map(); + for (auto& entry : stored) { + for (Slot& slot : entry.second) { + slot.destroy(slot.ptr); + } + } + stored.clear(); +} + +} // namespace dusk::interp diff --git a/src/dusk/interp/dual_buffer.h b/src/dusk/interp/dual_buffer.h new file mode 100644 index 0000000000..bbae6a3a01 --- /dev/null +++ b/src/dusk/interp/dual_buffer.h @@ -0,0 +1,123 @@ +#pragma once + +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/lerp.h" + +#include + +#ifdef __cplusplus +namespace dusk::interp { + +template +class DualBuffer { +public: + explicit DualBuffer(T* dst = NULL) + : m_prev_valid(false), + m_curr_valid(false), + m_count(0), + m_dst(dst), + m_post(NULL), + m_post_user(NULL) {} + + void bind(T* dst) { m_dst = dst; } + + void reset() { + m_prev_valid = false; + m_curr_valid = false; + m_count = 0; + } + + bool ready() const { return m_prev_valid && m_curr_valid; } + + void capture_and_schedule(const T* src, int count, void (*post)(void*) = NULL, + void* post_user = NULL) { + roll(); + capture(src, count); + schedule(post, post_user); + } + + void writeback(T* src_and_dst, int count, void (*post)(void*) = NULL, void* post_user = NULL) { + bind(src_and_dst); + capture_and_schedule(src_and_dst, count, post, post_user); + } + +private: + bool fits(int count) const { + if (count > capacity) { + return false; + } + return count > 0; + } + + void roll() { + if (!is_enabled() || !m_curr_valid || m_count <= 0) { + return; + } + std::memcpy(m_prev, m_curr, static_cast(m_count) * sizeof(T)); + m_prev_valid = true; + } + + void capture(const T* src, int count) { + if (!fits(count) || !is_enabled() || src == NULL) { + return; + } + std::memcpy(m_curr, src, static_cast(count) * sizeof(T)); + m_count = count; + m_curr_valid = true; + } + + void apply(T* dst, int count) const { + if (!fits(count) || dst == NULL || !ready()) { + return; + } + const f32 step = get_interpolation_step(); + for (int i = 0; i < count; ++i) { + lerp(dst[i], m_prev[i], m_curr[i], step); + } + } + + void schedule(void (*post)(void*) = NULL, void* post_user = NULL) { + if (!is_enabled() || m_dst == NULL || !fits(m_count)) { + return; + } + m_post = post; + m_post_user = post_user; + add_interpolation_callback(&present_trampoline, this); + } + + static void present_trampoline(void* user) { static_cast(user)->present(); } + + void present() { + apply(m_dst, m_count); + if (m_post != NULL) { + m_post(m_post_user); + } + } + + T m_prev[capacity]; + T m_curr[capacity]; + bool m_prev_valid; + bool m_curr_valid; + int m_count; + T* m_dst; + void (*m_post)(void*); + void* m_post_user; +}; + +namespace detail { +void* acquire(const void* key, const void* type, void* (*make)(), void (*destroy)(void*)); +} + +template +Record& get(const void* key) { + static const char token{}; + return *static_cast(detail::acquire( + key, &token, []() -> void* { return new Record; }, + [](void* p) { delete static_cast(p); })); +} + +void erase_owned_buffers(const void* key); +void clear_owned_buffers(); + +} // namespace dusk::interp +#endif diff --git a/src/dusk/interp/frame_interpolation.cpp b/src/dusk/interp/frame_interpolation.cpp new file mode 100644 index 0000000000..4edfee234a --- /dev/null +++ b/src/dusk/interp/frame_interpolation.cpp @@ -0,0 +1,293 @@ +#include "dusk/interp/frame_interpolation.h" + +#include "dusk/game_clock.h" +#include "dusk/interp/dual_buffer.h" +#include "dusk/interp/lerp.h" + +#include "mtx.h" + +#include +#include +#include + +namespace dusk::interp { +void camera_on_sim_tick(); +void camera_on_begin_record(); +bool camera_apply_presentation(); +void camera_restore_presentation(); +void camera_invalidate_snapshots(); +} // namespace dusk::interp + +namespace { + +struct Recording { + absl::flat_hash_map matrix_values; +}; + +bool s_recording = false; +bool s_replacementsActive = false; +bool s_syncPresentation = false; + +float s_step = 0.0f; +bool s_uiTickPending = false; +uint64_t s_simTickSeq = 0; +uint64_t s_observedPresentationEpoch = 0; + +Recording s_currentRecording; +Recording s_previousRecording; + +absl::flat_hash_map g_replacements; + +int s_presentationDepth = 0; + +const Mtx* resolve_replacement(const Mtx* source, Mtx* scratch) { + if (!s_replacementsActive || source == nullptr || dusk::interp::presentation_sync_active()) { + return source; + } + + auto it = g_replacements.find(reinterpret_cast(source)); + if (it == g_replacements.end()) { + return source; + } + + MTXCopy(it->second, *scratch); + return scratch; +} + +bool has_recording_data(const Recording& recording) { + return !recording.matrix_values.empty(); +} + +void clear_replacements() { + g_replacements.clear(); +} + +void interpolate_replacements() { + clear_replacements(); + s_replacementsActive = dusk::interp::is_enabled() && !s_recording && !s_syncPresentation && + has_recording_data(s_currentRecording); + if (!s_replacementsActive) { + return; + } + for (auto const& old : s_previousRecording.matrix_values) { + if (auto it = s_currentRecording.matrix_values.find(old.first); + it != s_currentRecording.matrix_values.end()) + { + dusk::interp::lerp(g_replacements[old.first], old.second, it->second, s_step); + } + } +} + +struct InterpolationCallBackWork { + dusk::interp::InterpolationCallBack pCallBack; + void* pUserWork; +}; + +std::vector s_interpolationCallBackWork; + +void clear_callbacks() { + s_interpolationCallBackWork.clear(); +} + +void callbacks_run() { + for (const auto& work : s_interpolationCallBackWork) { + if (work.pCallBack != nullptr) { + work.pCallBack(work.pUserWork); + } + } +} + +void clear_interpolation_history() { + s_recording = false; + s_replacementsActive = false; + s_syncPresentation = false; + s_previousRecording = {}; + s_currentRecording = {}; + clear_replacements(); + dusk::interp::clear_owned_buffers(); + clear_callbacks(); + dusk::interp::camera_invalidate_snapshots(); + s_presentationDepth = 0; +} + +} // namespace + +namespace dusk::interp { + +void begin_sim_tick() { + if (!is_enabled()) { + return; + } + + clear_callbacks(); + camera_on_sim_tick(); + ++s_simTickSeq; +} + +uint64_t sim_tick_seq() { + return s_simTickSeq; +} + +void begin_frame(float step) { + const game_clock::FrameTiming& timing = game_clock::g_frameTiming; + if (s_observedPresentationEpoch != timing.presentationEpoch) { + s_observedPresentationEpoch = timing.presentationEpoch; + clear_interpolation_history(); + } + + s_step = std::clamp(step, 0.0f, 1.0f); + if (!is_enabled()) { + clear_interpolation_history(); + } +} + +bool is_enabled() { + return game_clock::g_frameTiming.interpolating; +} + +bool should_capture() { + return is_enabled() && game_clock::is_sim_frame(); +} + +void begin_record() { + if (!is_enabled()) { + clear_interpolation_history(); + return; + } + + s_syncPresentation = false; + s_previousRecording = std::move(s_currentRecording); + s_currentRecording = {}; + s_recording = true; + s_replacementsActive = false; + clear_replacements(); + camera_on_begin_record(); +} + +void end_record() { + s_recording = false; +} + +void request_presentation_sync() { + if (!is_enabled()) { + return; + } + s_syncPresentation = true; +} + +bool presentation_sync_active() { + if (!is_enabled()) { + return false; + } + return s_syncPresentation; +} + +float get_interpolation_step() { + return presentation_sync_active() ? 1.0f : s_step; +} + +void set_ui_tick_pending(bool value) { + if (s_uiTickPending == value) { + return; + } + s_uiTickPending = value; +} + +bool get_ui_tick_pending() { + return is_enabled() ? s_uiTickPending : true; +} + +void record_final_mtx(Mtx m, const void* key) { + if (!s_recording || m == nullptr) { + return; + } + + auto& it = s_currentRecording.matrix_values[reinterpret_cast(key)]; + MTXCopy(m, it); +} + +void record_final_mtx(Mtx m) { + record_final_mtx(m, m); +} + +bool lookup_replacement(const void* key, Mtx out) { + if (presentation_sync_active() || !s_replacementsActive || key == nullptr) { + return false; + } + + auto it = g_replacements.find(reinterpret_cast(key)); + if (it == g_replacements.end()) { + return false; + } + + MTXCopy(it->second, out); + return true; +} + +bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out) { + if (presentation_sync_active() || !s_replacementsActive || lhs == nullptr || rhs == nullptr) { + return false; + } + + Mtx lhs_scratch; + Mtx rhs_scratch; + const Mtx* resolved_lhs = resolve_replacement(reinterpret_cast(lhs), &lhs_scratch); + const Mtx* resolved_rhs = resolve_replacement(reinterpret_cast(rhs), &rhs_scratch); + if (resolved_lhs == reinterpret_cast(lhs) && + resolved_rhs == reinterpret_cast(rhs)) + { + return false; + } + + MTXConcat(*resolved_lhs, *resolved_rhs, out); + return true; +} + +void begin_presentation(float step) { + begin_frame(step); + if (!is_enabled()) { + return; + } + + interpolate_replacements(); + + if (s_presentationDepth > 0) { + s_presentationDepth++; + return; + } + if (!camera_apply_presentation()) { + return; + } + + s_presentationDepth = 1; + callbacks_run(); +} + +void end_presentation() { + if (s_presentationDepth == 0) { + return; + } + s_presentationDepth--; + if (s_presentationDepth > 0) { + return; + } + + camera_restore_presentation(); +} + +bool is_presentation_active() { + return s_presentationDepth > 0; +} + +void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork) { + if (!is_enabled() || is_presentation_active() || !game_clock::is_sim_frame()) { + return; + } + if (pCallBack == nullptr) { + return; + } + + s_interpolationCallBackWork.push_back({pCallBack, pUserWork}); +} + +} // namespace dusk::interp diff --git a/src/dusk/interp/frame_interpolation.h b/src/dusk/interp/frame_interpolation.h new file mode 100644 index 0000000000..64ae2acec4 --- /dev/null +++ b/src/dusk/interp/frame_interpolation.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include + +#ifdef __cplusplus +namespace dusk::interp { + +void begin_record(); +void end_record(); +void begin_sim_tick(); +uint64_t sim_tick_seq(); +void begin_frame(float step); +float get_interpolation_step(); + +void request_presentation_sync(); +bool presentation_sync_active(); + +bool is_enabled(); + +bool should_capture(); + +// TODO: These should be phased out as UI is progressively updated to use game_clock +void set_ui_tick_pending(bool value); +bool get_ui_tick_pending(); + +void record_final_mtx(Mtx m, const void* key); +void record_final_mtx(Mtx m); + +bool lookup_replacement(const void* key, Mtx out); +bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out); + +void begin_presentation(float step); +void end_presentation(); +bool is_presentation_active(); + +typedef void (*InterpolationCallBack)(void* pUserWork); +void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork); + +} // namespace dusk::interp +#endif diff --git a/src/dusk/interp/lerp.h b/src/dusk/interp/lerp.h new file mode 100644 index 0000000000..1f05f070fa --- /dev/null +++ b/src/dusk/interp/lerp.h @@ -0,0 +1,53 @@ +#pragma once + +#include "SSystem/SComponent/c_angle.h" +#include "SSystem/SComponent/c_sxyz.h" +#include "SSystem/SComponent/c_xyz.h" + +#include +#include + +#ifdef __cplusplus +namespace dusk::interp { + +inline s16 lerp(s16 lhs, s16 rhs, float step) { + const f32 ra = S2RAD(lhs); + const f32 d = remainderf(S2RAD(rhs) - ra, 2.0f * M_PI); + return cAngle::Radian_to_SAngle(ra + d * step); +} + +inline u16 lerp(u16 lhs, u16 rhs, float step) { + return static_cast(lerp(static_cast(lhs), static_cast(rhs), step)); +} + +inline f32 lerp(f32 lhs, f32 rhs, float step) { + return lhs + (rhs - lhs) * step; +} + +inline u8 lerp(u8 lhs, u8 rhs, float step) { + return static_cast(std::lround(lerp(static_cast(lhs), static_cast(rhs), step))); +} + +inline void lerp(cXyz& out, const cXyz& lhs, const cXyz& rhs, float step) { + out.x = lerp(lhs.x, rhs.x, step); + out.y = lerp(lhs.y, rhs.y, step); + out.z = lerp(lhs.z, rhs.z, step); +} + +inline void lerp(csXyz& out, const csXyz& lhs, const csXyz& rhs, float step) { + out.x = lerp(lhs.x, rhs.x, step); + out.y = lerp(lhs.y, rhs.y, step); + out.z = lerp(lhs.z, rhs.z, step); +} + +inline void lerp(Mtx& out, const Mtx& lhs, const Mtx& rhs, float step) { + for (size_t row = 0; row < 3; ++row) { + for (size_t col = 0; col < 4; ++col) { + const float l = lhs[row][col]; + out[row][col] = l + (rhs[row][col] - l) * step; + } + } +} + +} // namespace dusk::interp +#endif diff --git a/src/dusk/interp/line.cpp b/src/dusk/interp/line.cpp new file mode 100644 index 0000000000..3d509debea --- /dev/null +++ b/src/dusk/interp/line.cpp @@ -0,0 +1,159 @@ +#include "dusk/interp/line.h" + +#include "dusk/interp/dual_buffer.h" +#include "dusk/interp/frame_interpolation.h" +#include "dusk/interp/lerp.h" + +#include "m_Do/m_Do_ext.h" + +#include +#include +#include +#include + +namespace { + +struct Record { + Record() + : store(nullptr), previous(nullptr), current(nullptr), sample_capacity(0), tick(0), + strand_count(0), point_count(0), previous_valid(false), current_valid(false) {} + + ~Record() { delete[] store; } + + Record(const Record&) = delete; + Record& operator=(const Record&) = delete; + + void invalidate() { + previous_valid = false; + current_valid = false; + } + + bool ensure(size_t required) { + if (required <= sample_capacity) { + return store != nullptr; + } + if (required > std::numeric_limits::max() / (2 * sizeof(cXyz))) { + return false; + } + + cXyz* next = new (std::nothrow) cXyz[required * 2]; + if (next == nullptr) { + return false; + } + + delete[] store; + store = next; + previous = store; + current = store + required; + sample_capacity = required; + invalidate(); + return true; + } + + cXyz* store; + cXyz* previous; + cXyz* current; + size_t sample_capacity; + uint64_t tick; + u16 strand_count; + u16 point_count; + bool previous_valid; + bool current_valid; +}; + +bool valid(const dusk::interp::line::Points& points) { + if (points.strands == nullptr || points.strand_count == 0 || points.point_count == 0) { + return false; + } + for (u16 strand = 0; strand < points.strand_count; ++strand) { + if (points.strands[strand].field_0x0 == nullptr) { + return false; + } + } + return true; +} + +bool same_layout(const Record& record, const dusk::interp::line::Points& points) { + return record.current_valid && record.strand_count == points.strand_count && + record.point_count == points.point_count; +} + +void copy_points(cXyz* destination, const dusk::interp::line::Points& points) { + const size_t points_size = static_cast(points.point_count) * sizeof(cXyz); + for (u16 strand = 0; strand < points.strand_count; ++strand) { + std::memcpy(destination + static_cast(strand) * points.point_count, + points.strands[strand].field_0x0, points_size); + } +} + +} // namespace + +namespace dusk::interp::line { + +void reset(const void* owner) { + erase_owned_buffers(owner); +} + +void capture(const void* owner, Points points) { + if (owner == nullptr || !should_capture()) { + return; + } + + Record& record = get(owner); + if (!valid(points)) { + record.invalidate(); + return; + } + + const size_t required = static_cast(points.strand_count) * points.point_count; + if (!record.ensure(required)) { + record.invalidate(); + return; + } + + const uint64_t tick = sim_tick_seq(); + const bool layout_matches = same_layout(record, points); + + if (layout_matches && tick == record.tick) { + copy_points(record.current, points); + return; + } + + if (layout_matches && tick == record.tick + 1) { + std::swap(record.previous, record.current); + record.previous_valid = true; + } else { + record.previous_valid = false; + } + + copy_points(record.current, points); + record.tick = tick; + record.strand_count = points.strand_count; + record.point_count = points.point_count; + record.current_valid = true; +} + +void write(const void* owner, Points points) { + if (owner == nullptr || !is_enabled() || !valid(points)) { + return; + } + + Record& record = get(owner); + if (!record.previous_valid || !same_layout(record, points) || + record.tick != sim_tick_seq()) + { + return; + } + + const f32 step = get_interpolation_step(); + for (u16 strand = 0; strand < points.strand_count; ++strand) { + cXyz* destination = points.strands[strand].field_0x0; + const size_t offset = static_cast(strand) * points.point_count; + for (u16 point = 0; point < points.point_count; ++point) { + lerp(destination[point], record.previous[offset + point], + record.current[offset + point], step); + } + } +} + +} // namespace dusk::interp::line diff --git a/src/dusk/interp/line.h b/src/dusk/interp/line.h new file mode 100644 index 0000000000..7b4f3b56bc --- /dev/null +++ b/src/dusk/interp/line.h @@ -0,0 +1,19 @@ +#pragma once + +#include "dolphin/types.h" + +class mDoExt_3Dline_c; + +namespace dusk::interp::line { + +struct Points { + mDoExt_3Dline_c* strands; + u16 strand_count; + u16 point_count; +}; + +void reset(const void* owner); +void capture(const void* owner, Points points); +void write(const void* owner, Points points); + +} // namespace dusk::interp::line diff --git a/src/dusk/iso_validate.hpp b/src/dusk/iso_validate.hpp index e5a8eac1ef..b7740b636f 100644 --- a/src/dusk/iso_validate.hpp +++ b/src/dusk/iso_validate.hpp @@ -38,7 +38,7 @@ using VerificationStatus = borealis::disc::Progress; struct DiscInfo { Platform platform = Platform::Unknown; Region region = Region::NorthAmerica; - std::uint8_t revision = 0; + uint8_t revision = 0; }; ValidationError inspect(const char* path, DiscInfo& info); diff --git a/src/dusk/mod_loader.hpp b/src/dusk/mod_loader.hpp index d07f0d6f2c..fc419e7367 100644 --- a/src/dusk/mod_loader.hpp +++ b/src/dusk/mod_loader.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include namespace dusk::mods { @@ -83,6 +84,19 @@ struct ModSearchDir { std::filesystem::path nativeLibDir; }; +struct ModOperation { + enum class State : u8 { + Pending, + Succeeded, + Failed, + }; + + State state = State::Pending; + std::string message; +}; + +using ModOperationHandle = std::shared_ptr; + struct ModMetaParsed { uint32_t abiVersion = 0; std::vector imports; @@ -177,6 +191,14 @@ enum class NativeModStatus : u8 { }; struct LoadedMod { + struct FileIdentity { + std::uintmax_t size = 0; + std::filesystem::file_time_type modified{}; + bool valid = false; + + bool operator==(const FileIdentity&) const = default; + }; + ModMetadata metadata; std::filesystem::path modPath; std::filesystem::path dir; @@ -186,8 +208,12 @@ struct LoadedMod { std::string dataDirUtf8; uint32_t searchDirIndex = 0; - // Native lib is dlopen'd in place and stays resident for the session. Reload is unsupported. - bool inPlace = false; + bool fromDirectory = false; + // Native lib is dlopen'd in place. + bool nativeInPlace = false; + bool hasUserPackage = false; + bool hasBundledCopy = false; + FileIdentity fileIdentity; std::unique_ptr> cvarIsEnabled; config::Subscription enabledSubscription = 0; @@ -229,8 +255,15 @@ struct LoadedMod { // Mods this mod imports services from, and mods importing services from this mod. std::vector dependencies; std::vector dependents; + + [[nodiscard]] bool is_enabled() const { + return cvarIsEnabled != nullptr && cvarIsEnabled->getValue(); + } + [[nodiscard]] bool activation_failed() const { return loadFailed || (is_enabled() && !active); } }; +struct PackageCandidate; + class ModLoader { public: static ModLoader& instance(); @@ -243,9 +276,19 @@ public: void request_enable(std::string_view id); void request_disable(std::string_view id); - void request_reload(std::string_view id); + ModOperationHandle request_reload(std::string_view id); + ModOperationHandle request_install(std::filesystem::path path); + ModOperationHandle request_uninstall(std::string_view id); + ModOperationHandle request_reactivate(std::string_view id); void notify_mod_failure(LoadedMod& mod, bool firstFailure); + [[nodiscard]] std::filesystem::path user_mods_dir() const; + [[nodiscard]] bool can_uninstall(const LoadedMod& mod) const; + [[nodiscard]] bool can_update(const LoadedMod& mod) const; + [[nodiscard]] LoadedMod* find_mod(std::string_view id); + [[nodiscard]] const LoadedMod* find_mod(std::string_view id) const; + [[nodiscard]] uint64_t generation() const noexcept { return m_generation; } + [[nodiscard]] auto mods() const { return m_mods | std::views::transform([](const auto& m) -> LoadedMod& { return *m; }); } @@ -255,10 +298,30 @@ public: } private: - enum class RequestKind : u8 { Enable, Disable, Reload }; - struct Request { + enum class LifecycleAction : u8 { Enable, Disable, Reactivate }; + struct LifecycleRequest { std::string modId; - RequestKind kind; + LifecycleAction action; + std::shared_ptr operation; + }; + struct InstallRequest { + std::filesystem::path stagedPath; + std::shared_ptr operation; + }; + struct ReloadRequest { + std::string modId; + std::shared_ptr operation; + }; + struct UninstallRequest { + std::string modId; + std::shared_ptr operation; + }; + using Request = std::variant; + + struct OperationResult { + bool success = true; + std::string message; + LoadedMod* mod = nullptr; }; // ModLoader::tick runs inside fapGm_Execute, so code from an unloading mod can still be // live on the stack (its frame unwinds after the tick). dlclose is therefore deferred to @@ -274,10 +337,12 @@ private: std::vector m_pendingRequests; std::vector m_pendingFailures; std::vector m_retiredNatives; + uint64_t m_generation = 0; bool m_initialized = false; bool m_startupComplete = false; - void try_load_mod(const std::filesystem::path& modPath, bool fromDir, uint32_t searchDirIndex); + LoadedMod* try_load_mod(const std::filesystem::path& modPath, bool fromDir, + uint32_t searchDirIndex, std::unique_ptr bundle = {}); void load_native(LoadedMod& mod, const std::string& dllEntry, const std::vector& runtimeEntries); bool load_native_if_present(LoadedMod& mod); @@ -296,20 +361,31 @@ private: [[nodiscard]] std::string describe_missing_import( const char* serviceId, uint16_t majorVersion, uint16_t minMinorVersion) const; - LoadedMod* find_mod(std::string_view id) const; void drain_retired_natives(); void apply_pending_requests(); + [[nodiscard]] OperationResult install_staged(const std::filesystem::path& path); + [[nodiscard]] OperationResult load_runtime_mod(const std::filesystem::path& path); + [[nodiscard]] OperationResult reload_runtime_mod( + LoadedMod& mod, const PackageCandidate* replacement = nullptr); + [[nodiscard]] OperationResult uninstall_runtime_mod(LoadedMod& mod); + [[nodiscard]] OperationResult runtime_result(LoadedMod& mod); + void forget_mod(LoadedMod& mod); void flush_toasts(); void on_enabled_changed(LoadedMod& mod); // Deactivates `target` (if needed) and its transitive dependents, optionally re-reads the // bundle from disk, then reactivates whatever the current cvar/provider state allows. - void apply_lifecycle_change(LoadedMod& target, bool reload); + void apply_lifecycle_change( + LoadedMod& target, bool reload, const PackageCandidate* replacement = nullptr); // `target` plus transitive active/suspended dependents, in m_mods (init) order. - std::vector collect_lifecycle_set(LoadedMod& target); + std::vector collect_lifecycle_set(LoadedMod& target) const; + void resume_lifecycle_set(const std::vector& mods); bool reload_bundle(LoadedMod& mod); bool ensure_native_loaded(LoadedMod& mod); }; +bool inspect_mod_bundle(const std::filesystem::path& path, ModMetadata& metadata, + std::string& error, bool* hasNative = nullptr) noexcept; + using ModIndex = std::ranges::range_difference_t().mods())>; } // namespace dusk::mods diff --git a/src/dusk/mods/catalog.cpp b/src/dusk/mods/catalog.cpp new file mode 100644 index 0000000000..4ba118cd5b --- /dev/null +++ b/src/dusk/mods/catalog.cpp @@ -0,0 +1,458 @@ +#include "catalog.hpp" + +#include "dusk/app_info.hpp" +#include "fmt/format.h" +#include "nlohmann/json.hpp" + +#include +#include +#include +#include +#include +#include + +#if defined(__APPLE__) +#include +#endif + +namespace dusk::mods::catalog { +namespace { + +using json = nlohmann::json; +using namespace std::chrono_literals; + +constexpr std::string_view catalogUrl = + "https://staging.twilitrealm.workers.dev/api/v1/games/dusklight/mods"; + +std::string_view sort_value(Sort sort) noexcept { + switch (sort) { + case Sort::Endorsements: + return "endorsements"; + case Sort::Updated: + return "updated"; + case Sort::Newest: + return "newest"; + case Sort::Name: + return "name"; + case Sort::Downloads: + default: + return "downloads"; + } +} + +std::string_view catalog_platform() noexcept { +#if defined(_WIN32) && defined(_M_ARM64) + return "windows-arm64"; +#elif defined(_WIN32) && defined(_M_X64) + return "windows-amd64"; +#elif defined(__ANDROID__) && defined(__aarch64__) + return "android-aarch64"; +#elif defined(__APPLE__) && TARGET_OS_IOS + return "ios-arm64"; +#elif defined(__APPLE__) && !TARGET_OS_TV && defined(__aarch64__) + return "macos-arm64"; +#elif defined(__APPLE__) && !TARGET_OS_TV && defined(__x86_64__) + return "macos-x86_64"; +#elif defined(__linux__) && defined(__aarch64__) + return "linux-aarch64"; +#elif defined(__linux__) && defined(__x86_64__) + return "linux-x86_64"; +#else + // The catalog rejects platforms outside its published package matrix. + return {}; +#endif +} + +std::string url_encode(std::string_view value) { + constexpr char hex[] = "0123456789ABCDEF"; + std::string encoded; + encoded.reserve(value.size()); + for (const unsigned char c : value) { + const bool unreserved = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || + c == '~'; + if (unreserved) { + encoded.push_back(static_cast(c)); + } else { + encoded.push_back('%'); + encoded.push_back(hex[c >> 4]); + encoded.push_back(hex[c & 0x0f]); + } + } + return encoded; +} + +void append_query(std::string& url, std::string_view name, std::string_view value) { + fmt::format_to(std::back_inserter(url), "{}{}={}", + url.find('?') == std::string::npos ? '?' : '&', name, url_encode(value)); +} + +std::string make_url(const Query& query) { + std::string url{catalogUrl}; + if (!query.search.empty()) { + append_query(url, "q", query.search); + } + if (!query.category.empty()) { + append_query(url, "category", query.category); + } + append_query(url, "sort", sort_value(query.sort)); + append_query(url, "page", fmt::format("{}", std::max(query.page, 1))); + if (query.thisDevice) { + const auto platform = catalog_platform(); + if (!platform.empty()) { + append_query(url, "platform", platform); + } + } + return url; +} + +std::string make_detail_url(std::string_view id) { + return fmt::format("{}/{}", catalogUrl, url_encode(id)); +} + +const json& required_field(const json& object, const char* name) { + if (!object.is_object()) { + throw std::runtime_error{"expected an object"}; + } + const auto iter = object.find(name); + if (iter == object.end()) { + throw std::runtime_error{fmt::format("missing field '{}'", name)}; + } + return *iter; +} + +std::string required_string(const json& object, const char* name) { + const auto& value = required_field(object, name); + if (!value.is_string()) { + throw std::runtime_error{fmt::format("field '{}' is not a string", name)}; + } + return value.get(); +} + +bool required_bool(const json& object, const char* name) { + const auto& value = required_field(object, name); + if (!value.is_boolean()) { + throw std::runtime_error{fmt::format("field '{}' is not a boolean", name)}; + } + return value.get(); +} + +uint64_t required_count(const json& object, const char* name) { + const auto& value = required_field(object, name); + if (value.is_number_unsigned()) { + return value.get(); + } + if (value.is_number_integer()) { + const auto count = value.get(); + if (count >= 0) { + return static_cast(count); + } + } + throw std::runtime_error{fmt::format("field '{}' is not a non-negative integer", name)}; +} + +int required_int(const json& object, const char* name) { + const uint64_t value = required_count(object, name); + if (value > static_cast(std::numeric_limits::max())) { + throw std::runtime_error{fmt::format("field '{}' is too large", name)}; + } + return static_cast(value); +} + +std::optional optional_string(const json& object, const char* name) { + const auto& value = required_field(object, name); + if (value.is_null()) { + return std::nullopt; + } + if (!value.is_string()) { + throw std::runtime_error{fmt::format("field '{}' is not a string or null", name)}; + } + return value.get(); +} + +uint16_t required_u16(const json& object, const char* name) { + const auto value = required_count(object, name); + if (value > std::numeric_limits::max()) { + throw std::runtime_error{fmt::format("field '{}' is too large", name)}; + } + return static_cast(value); +} + +Image parse_image(const json& value) { + const auto width = required_count(value, "width"); + const auto height = required_count(value, "height"); + if (width > std::numeric_limits::max() || + height > std::numeric_limits::max()) + { + throw std::runtime_error{"image dimensions are too large"}; + } + Image image{ + .width = static_cast(width), + .height = static_cast(height), + }; + const auto& sources = required_field(value, "sources"); + if (!sources.is_array()) { + throw std::runtime_error{"field 'sources' is not an array"}; + } + image.sources.reserve(sources.size()); + for (const auto& source : sources) { + const auto sourceWidth = required_count(source, "width"); + if (sourceWidth > std::numeric_limits::max()) { + throw std::runtime_error{"image source width is too large"}; + } + image.sources.push_back({ + .width = static_cast(sourceWidth), + .pngUrl = required_string(source, "png_url"), + }); + } + if (image.sources.empty()) { + throw std::runtime_error{"image has no sources"}; + } + return image; +} + +Category parse_category(const json& value) { + return { + .slug = required_string(value, "slug"), + .name = required_string(value, "name"), + .modCount = required_count(value, "mod_count"), + }; +} + +Category parse_mod_category(const json& value) { + return { + .slug = required_string(value, "slug"), + .name = required_string(value, "name"), + }; +} + +Tag parse_tag(const json& value) { + return { + .slug = required_string(value, "slug"), + .name = required_string(value, "name"), + }; +} + +Author parse_author(const json& value) { + return { + .name = required_string(value, "name"), + .handle = required_string(value, "handle"), + .official = required_bool(value, "official"), + }; +} + +Mod parse_mod(const json& value) { + Mod mod{ + .id = required_string(value, "id"), + .name = required_string(value, "name"), + .version = required_string(value, "version"), + .author = parse_author(required_field(value, "author")), + .summary = required_string(value, "summary"), + .downloads = required_count(value, "downloads"), + .endorsements = required_count(value, "endorsements"), + .publishedAt = required_string(value, "published_at"), + .updatedAt = required_string(value, "updated_at"), + .packageSize = required_count(value, "package_size"), + .containsNativeCode = required_bool(value, "contains_native_code"), + }; + + const auto& category = required_field(value, "category"); + if (!category.is_null()) { + mod.category = parse_mod_category(category); + } + + const auto& tags = required_field(value, "tags"); + if (!tags.is_array()) { + throw std::runtime_error{"field 'tags' is not an array"}; + } + mod.tags.reserve(tags.size()); + for (const auto& tag : tags) { + mod.tags.push_back(parse_tag(tag)); + } + + const auto& platforms = required_field(value, "supported_platforms"); + if (!platforms.is_array()) { + throw std::runtime_error{"field 'supported_platforms' is not an array"}; + } + mod.supportedPlatforms.reserve(platforms.size()); + for (const auto& platform : platforms) { + if (!platform.is_string()) { + throw std::runtime_error{"supported platform is not a string"}; + } + mod.supportedPlatforms.push_back(platform.get()); + } + + const auto& icon = required_field(value, "icon"); + if (!icon.is_null()) { + mod.icon = parse_image(icon); + } + const auto& banner = required_field(value, "banner"); + if (!banner.is_null()) { + mod.banner = parse_image(banner); + } + return mod; +} + +Detail parse_detail(std::string_view body) { + const json root = json::parse(body); + Detail detail{ + .mod = parse_mod(root), + .siteUrl = required_string(root, "site_url"), + .sourceUrl = optional_string(root, "source_url"), + .license = optional_string(root, "license"), + .descriptionHtml = required_string(root, "description_html"), + .changelogHtml = required_string(root, "changelog_html"), + }; + + const auto& download = required_field(root, "download"); + if (!download.is_object()) { + throw std::runtime_error{"field 'download' is not an object"}; + } + detail.download = { + .url = required_string(download, "url"), + .sha256 = required_string(download, "sha256"), + .size = required_count(download, "size"), + }; + + const auto& modAbi = required_field(root, "mod_abi"); + if (!modAbi.is_null()) { + const auto value = required_count(root, "mod_abi"); + if (value > std::numeric_limits::max()) { + throw std::runtime_error{"field 'mod_abi' is too large"}; + } + detail.modAbi = static_cast(value); + } + + const auto& screenshots = required_field(root, "screenshots"); + if (!screenshots.is_array()) { + throw std::runtime_error{"field 'screenshots' is not an array"}; + } + detail.screenshots.reserve(screenshots.size()); + for (const auto& screenshot : screenshots) { + detail.screenshots.push_back({ + .altText = required_string(screenshot, "alt_text"), + .image = parse_image(required_field(screenshot, "image")), + }); + } + + const auto& imports = required_field(root, "service_imports"); + if (!imports.is_array()) { + throw std::runtime_error{"field 'service_imports' is not an array"}; + } + detail.serviceImports.reserve(imports.size()); + for (const auto& import : imports) { + detail.serviceImports.push_back({ + .id = required_string(import, "id"), + .major = required_u16(import, "major"), + .minMinor = required_u16(import, "min_minor"), + .optional = required_bool(import, "optional"), + }); + } + return detail; +} + +Page parse_page(std::string_view body) { + const json root = json::parse(body); + const auto& game = required_field(root, "game"); + if (required_string(game, "id") != "dusklight") { + throw std::runtime_error{"catalog response is for a different game"}; + } + + Page page; + const auto& categories = required_field(root, "categories"); + if (!categories.is_array()) { + throw std::runtime_error{"field 'categories' is not an array"}; + } + page.categories.reserve(categories.size()); + for (const auto& category : categories) { + page.categories.push_back(parse_category(category)); + } + + const auto& mods = required_field(root, "mods"); + if (!mods.is_array()) { + throw std::runtime_error{"field 'mods' is not an array"}; + } + page.mods.reserve(mods.size()); + for (const auto& mod : mods) { + page.mods.push_back(parse_mod(mod)); + } + + const auto& pagination = required_field(root, "pagination"); + page.pagination = { + .page = required_int(pagination, "page"), + .pageSize = required_int(pagination, "page_size"), + .pageCount = required_int(pagination, "page_count"), + .total = required_count(pagination, "total"), + }; + return page; +} + +std::string api_error(const borealis::http::Response& response) { + try { + const auto body = json::parse(response.body); + const auto& error = required_field(body, "error"); + return required_string(error, "message"); + } catch (...) { + return fmt::format("The catalog returned HTTP {}.", response.statusCode); + } +} + +FetchResult finish_request(borealis::http::Result result) { + if (result.error != borealis::http::Error::None) { + return {.error = result.message.empty() ? "The catalog request failed." : + std::move(result.message)}; + } + if (result.response.statusCode != 200) { + return {.error = api_error(result.response)}; + } + try { + return {.page = parse_page(result.response.body)}; + } catch (const std::exception& exception) { + return {.error = fmt::format("The catalog response was invalid: {}", exception.what())}; + } catch (...) { + return {.error = "The catalog response was invalid."}; + } +} + +DetailFetchResult finish_detail_request(borealis::http::Result result) { + if (result.error != borealis::http::Error::None) { + return {.error = + result.message.empty() ? "The mod request failed." : std::move(result.message)}; + } + if (result.response.statusCode != 200) { + return {.error = api_error(result.response)}; + } + try { + return {.detail = parse_detail(result.response.body)}; + } catch (const std::exception& exception) { + return {.error = fmt::format("The mod response was invalid: {}", exception.what())}; + } catch (...) { + return {.error = "The mod response was invalid."}; + } +} + +borealis::http::Request make_request(std::string url) { + return { + .url = std::move(url), + .headers = + { + {.name = "User-Agent", .value = borealis::user_agent(dusk::AppInfo)}, + {.name = "Accept", .value = "application/json"}, + }, + .connectTimeout = 10s, + .idleTimeout = 10s, + .totalTimeout = 20s, + }; +} + +} // namespace + +borealis::Task fetch_page(Query query) { + return borealis::http::start(make_request(make_url(query))).map(finish_request); +} + +borealis::Task fetch_detail(std::string id) { + return borealis::http::start(make_request(make_detail_url(id))).map(finish_detail_request); +} + +} // namespace dusk::mods::catalog diff --git a/src/dusk/mods/catalog.hpp b/src/dusk/mods/catalog.hpp new file mode 100644 index 0000000000..5605cb6e9a --- /dev/null +++ b/src/dusk/mods/catalog.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace dusk::mods::catalog { + +enum class Sort { + Downloads, + Endorsements, + Updated, + Newest, + Name, +}; + +struct Query { + std::string search; + std::string category; + Sort sort = Sort::Downloads; + int page = 1; + bool thisDevice = true; +}; + +struct Category { + std::string slug; + std::string name; + uint64_t modCount = 0; +}; + +struct Tag { + std::string slug; + std::string name; +}; + +struct Author { + std::string name; + std::string handle; + bool official = false; +}; + +struct ImageSource { + uint32_t width = 0; + std::string pngUrl; +}; + +struct Image { + uint32_t width = 0; + uint32_t height = 0; + std::vector sources; +}; + +struct Mod { + std::string id; + std::string name; + std::string version; + Author author; + std::string summary; + std::optional category; + std::vector tags; + uint64_t downloads = 0; + uint64_t endorsements = 0; + std::string publishedAt; + std::string updatedAt; + uint64_t packageSize = 0; + bool containsNativeCode = false; + std::vector supportedPlatforms; + std::optional icon; + std::optional banner; +}; + +struct Screenshot { + std::string altText; + Image image; +}; + +struct ServiceImport { + std::string id; + uint16_t major = 0; + uint16_t minMinor = 0; + bool optional = false; +}; + +struct Download { + std::string url; + std::string sha256; + uint64_t size = 0; +}; + +struct Detail { + Mod mod; + std::string siteUrl; + std::optional sourceUrl; + std::optional license; + std::string descriptionHtml; + std::string changelogHtml; + Download download; + std::optional modAbi; + std::vector screenshots; + std::vector serviceImports; +}; + +struct Pagination { + int page = 1; + int pageSize = 0; + int pageCount = 0; + uint64_t total = 0; +}; + +struct Page { + std::vector categories; + std::vector mods; + Pagination pagination; +}; + +struct FetchResult { + std::optional page; + std::string error; +}; + +struct DetailFetchResult { + std::optional detail; + std::string error; +}; + +/** Fetches one filtered page from the Dusklight catalog. */ +borealis::Task fetch_page(Query query); + +/** Fetches the full catalog record for one mod. */ +borealis::Task fetch_detail(std::string id); + +} // namespace dusk::mods::catalog diff --git a/src/dusk/mods/loader/bundle_zip.cpp b/src/dusk/mods/loader/bundle_zip.cpp index 6075dd5dc1..630fa3ae97 100644 --- a/src/dusk/mods/loader/bundle_zip.cpp +++ b/src/dusk/mods/loader/bundle_zip.cpp @@ -1,69 +1,25 @@ #include "loader.hpp" -#include - -#include +#include namespace dusk::mods { -ModBundleZip::ModBundleZip(std::vector&& data) : zip_data(std::move(data)) { - if (!mz_zip_reader_init_mem(&res_zip, zip_data.data(), zip_data.size(), 0)) { - const auto error = mz_zip_get_last_error(&res_zip); - throw std::runtime_error( - fmt::format("Opening zip failed: {}", mz_zip_get_error_string(error))); +ModBundleZip::ModBundleZip(const std::filesystem::path& path) : m_archive{path} { + if (m_archive.package_format() != archive::PackageFormat::Mod) { + throw std::runtime_error("Archive is not a valid mod package"); } } -ModBundleZip::~ModBundleZip() { - mz_zip_reader_end(&res_zip); -} - std::vector ModBundleZip::readFile(const std::string& fileName) { - std::lock_guard lock{m_mutex}; - size_t size; - const auto ptr = mz_zip_reader_extract_file_to_heap(&res_zip, fileName.c_str(), &size, 0); - - if (!ptr) { - throw std::runtime_error(fmt::format("File does not exist: {}", fileName)); - } - - std::span data(static_cast(ptr), size); - std::vector vec(data.begin(), data.end()); - - mz_free(ptr); - - return vec; + return m_archive.read_file(fileName); } std::vector ModBundleZip::getFileNames() { - std::lock_guard lock{m_mutex}; - std::vector results; - - for (mz_uint i = 0, n = mz_zip_reader_get_num_files(&res_zip); i < n; ++i) { - mz_zip_archive_file_stat stat{}; - if (!mz_zip_reader_file_stat(&res_zip, i, &stat)) { - continue; - } - if (mz_zip_reader_is_file_a_directory(&res_zip, i)) { - continue; - } - - results.emplace_back(stat.m_filename); - } - - return results; + return m_archive.file_names(); } size_t ModBundleZip::getFileSize(const std::string& fileName) { - std::lock_guard lock{m_mutex}; - const auto idx = mz_zip_reader_locate_file(&res_zip, fileName.c_str(), nullptr, 0); - if (idx < 0) { - throw std::runtime_error(fmt::format("Unable to locate file in zip: {}", fileName)); - } - - mz_zip_archive_file_stat stat{}; - mz_zip_reader_file_stat(&res_zip, idx, &stat); - return stat.m_uncomp_size; + return m_archive.file_size(fileName); } } // namespace dusk::mods diff --git a/src/dusk/mods/loader/code_patch_macos.cpp b/src/dusk/mods/loader/code_patch_macos.cpp new file mode 100644 index 0000000000..64a7b4a606 --- /dev/null +++ b/src/dusk/mods/loader/code_patch_macos.cpp @@ -0,0 +1,223 @@ +#include "code_patch_macos.hpp" + +#include +#include +#include +#include +#include +#include + +#define PATCH_CODE \ + __attribute__((section("__TEXT,__code_patch,regular,pure_instructions"), noinline)) + +extern const char kPatchBegin[] asm("section$start$__TEXT$__code_patch"); +extern const char kPatchEnd[] asm("section$end$__TEXT$__code_patch"); + +namespace { + +constexpr unsigned kMaxThreads = 1024; +constexpr unsigned kMaxAttempts = 16; +constexpr size_t kMaxPatchSize = 16; +pthread_mutex_t sPatchMutex = PTHREAD_MUTEX_INITIALIZER; + +struct ThreadList { + thread_act_array_t threads = nullptr; + mach_msg_type_number_t count = 0; +}; + +PATCH_CODE void release_threads(ThreadList& list) { + for (unsigned i = 0; i < list.count; ++i) { + mach_port_deallocate(mach_task_self(), list.threads[i]); + } + if (list.threads != nullptr) { + vm_deallocate(mach_task_self(), reinterpret_cast(list.threads), + list.count * sizeof(thread_t)); + } +} + +PATCH_CODE kern_return_t read_pc(thread_t thread, uintptr_t& pc) { +#if defined(__aarch64__) + arm_thread_state64_t state{}; + mach_msg_type_number_t count = ARM_THREAD_STATE64_COUNT; + const auto result = thread_get_state( + thread, ARM_THREAD_STATE64, reinterpret_cast(&state), &count); + pc = arm_thread_state64_get_pc(state); +#elif defined(__x86_64__) + x86_thread_state64_t state{}; + mach_msg_type_number_t count = x86_THREAD_STATE64_COUNT; + const auto result = thread_get_state( + thread, x86_THREAD_STATE64, reinterpret_cast(&state), &count); + pc = state.__rip; +#else +#error Unsupported macOS architecture +#endif + return result; +} + +PATCH_CODE __attribute__((aligned(16384))) kern_return_t commit_patch(uintptr_t target, + const unsigned char* expected, const unsigned char* replacement, size_t size, uintptr_t page, + size_t pageSize, thread_t currentThread) { + ThreadList initial; + auto result = task_threads(mach_task_self(), &initial.threads, &initial.count); + if (result != KERN_SUCCESS) { + return result; + } + thread_t suspended[kMaxThreads]; + unsigned suspendedCount = 0; + if (initial.count > kMaxThreads) { + release_threads(initial); + return KERN_RESOURCE_SHORTAGE; + } + + for (unsigned i = 0; i < initial.count; ++i) { + const auto thread = initial.threads[i]; + if (thread == currentThread) { + continue; + } + result = thread_suspend(thread); + if (result != KERN_SUCCESS) { + break; + } + suspended[suspendedCount++] = thread; + uintptr_t pc = 0; + result = read_pc(thread, pc); + if (result != KERN_SUCCESS) { + break; + } + if (pc >= target && pc < target + size) { + result = KERN_ABORTED; + break; + } + } + + if (result == KERN_SUCCESS) { + ThreadList current; + result = task_threads(mach_task_self(), ¤t.threads, ¤t.count); + if (result == KERN_SUCCESS) { + for (unsigned i = 0; i < current.count; ++i) { + bool known = false; + for (unsigned j = 0; j < initial.count; ++j) { + known |= current.threads[i] == initial.threads[j]; + } + if (!known) { + result = KERN_ABORTED; + break; + } + } + } + release_threads(current); + } + + if (result == KERN_SUCCESS) { + const auto* bytes = reinterpret_cast(target); + for (size_t i = 0; i < size; ++i) { + if (bytes[i] != expected[i]) { + result = KERN_INVALID_VALUE; + break; + } + } + } + + if (result == KERN_SUCCESS) { + result = mach_vm_protect( + mach_task_self(), page, pageSize, false, VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY); + if (result == KERN_SUCCESS) { + auto* bytes = reinterpret_cast(target); + for (size_t i = 0; i < size; ++i) { + bytes[i] = replacement[i]; + } + sys_icache_invalidate(reinterpret_cast(target), size); + result = mach_vm_protect( + mach_task_self(), page, pageSize, false, VM_PROT_READ | VM_PROT_EXECUTE); + if (result != KERN_SUCCESS) { + for (size_t i = 0; i < size; ++i) { + bytes[i] = expected[i]; + } + sys_icache_invalidate(reinterpret_cast(target), size); + if (mach_vm_protect(mach_task_self(), page, pageSize, false, + VM_PROT_READ | VM_PROT_EXECUTE) != KERN_SUCCESS) + { + __builtin_trap(); // Can't resume into non-executable code + } + } + } + } + + for (unsigned i = 0; i < suspendedCount; ++i) { + if (thread_resume(suspended[i]) != KERN_SUCCESS) { + __builtin_trap(); + } + } + release_threads(initial); + return result; +} + +} // namespace + +extern "C" int commit_code_patch( + void* targetPointer, const void* expected, const void* replacement, size_t size) { + if (targetPointer == nullptr || expected == nullptr || replacement == nullptr || size == 0 || + size > kMaxPatchSize) + { + return KERN_INVALID_ARGUMENT; + } + const auto target = reinterpret_cast(targetPointer); + const size_t pageSize = vm_page_size; + if (target > UINTPTR_MAX - size - pageSize) { + return KERN_INVALID_ADDRESS; + } + const auto page = target & ~(pageSize - 1); + const size_t length = ((target + size + pageSize - 1) & ~(pageSize - 1)) - page; + if (page < reinterpret_cast(kPatchEnd) && + page + length > reinterpret_cast(kPatchBegin)) + { + return KERN_PROTECTION_FAILURE; + } + + unsigned char oldCode[kMaxPatchSize]; + unsigned char newCode[kMaxPatchSize]; + for (size_t i = 0; i < size; ++i) { + oldCode[i] = static_cast(expected)[i]; + newCode[i] = static_cast(replacement)[i]; + } + + pthread_mutex_lock(&sPatchMutex); + mach_vm_address_t region = page; + mach_vm_size_t regionSize = 0; + vm_region_basic_info_data_64_t info{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + auto result = mach_vm_region(mach_task_self(), ®ion, ®ionSize, VM_REGION_BASIC_INFO_64, + reinterpret_cast(&info), &count, &object); + if (object != MACH_PORT_NULL) { + mach_port_deallocate(mach_task_self(), object); + } + if (result == KERN_SUCCESS && (region > page || regionSize < page + length - region || + info.protection != (VM_PROT_READ | VM_PROT_EXECUTE))) + { + result = KERN_PROTECTION_FAILURE; + } + if (result == KERN_SUCCESS) { + const auto currentThread = mach_thread_self(); + uintptr_t pc = 0; + read_pc(currentThread, pc); + thread_suspend(MACH_PORT_NULL); + thread_resume(MACH_PORT_NULL); + vm_deallocate(mach_task_self(), 0, 0); + mach_port_deallocate(mach_task_self(), MACH_PORT_NULL); + sys_icache_invalidate(targetPointer, size); + result = mach_vm_protect(mach_task_self(), page, length, false, info.protection); + if (result == KERN_SUCCESS) { + for (unsigned attempt = 0; attempt < kMaxAttempts; ++attempt) { + result = commit_patch(target, oldCode, newCode, size, page, length, currentThread); + if (result != KERN_ABORTED || attempt + 1 == kMaxAttempts) { + break; + } + usleep(1000); + } + } + mach_port_deallocate(mach_task_self(), currentThread); + } + pthread_mutex_unlock(&sPatchMutex); + return result; +} diff --git a/src/dusk/mods/loader/code_patch_macos.hpp b/src/dusk/mods/loader/code_patch_macos.hpp new file mode 100644 index 0000000000..540e806404 --- /dev/null +++ b/src/dusk/mods/loader/code_patch_macos.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int commit_code_patch(void* target, const void* expected, const void* replacement, size_t size); + +#ifdef __cplusplus +} +#endif diff --git a/src/dusk/mods/loader/loader.cpp b/src/dusk/mods/loader/loader.cpp index 71b17b7cff..16c49f8c45 100644 --- a/src/dusk/mods/loader/loader.cpp +++ b/src/dusk/mods/loader/loader.cpp @@ -1,536 +1,80 @@ #include "loader.hpp" -#include "../manifest.hpp" #include "depgraph.hpp" +#include "manifest.hpp" #include "native_module.hpp" +#include "natives.hpp" +#include "packages.hpp" #if DUSK_HAS_PREPATCH #include "prepatch.hpp" #endif #include "dusk/config.hpp" #include "dusk/data.hpp" -#include "dusk/io.hpp" #include "dusk/logging.h" #include "dusk/mod_loader.hpp" #include "dusk/mods/log_buffer.hpp" +#include "dusk/mods/manifest.hpp" +#include "dusk/mods/path.hpp" +#include "dusk/mods/queue.hpp" #include "dusk/mods/svc/config.hpp" #include "dusk/mods/svc/hook.hpp" #include "dusk/mods/svc/registry.hpp" +#include "dusk/ui/mod_texture_provider.hpp" #include "dusk/ui/mods_window.hpp" #include "dusk/ui/ui.hpp" #include -#include -#include +#include +#include #include -#include #include #include -#include #include #include #include #include -using namespace std::string_literals; -using namespace std::string_view_literals; - -#if defined(_WIN32) -#if defined(_M_ARM64) -static constexpr std::string_view k_nativePlatform = "windows-arm64"sv; -#elif defined(_M_X64) -static constexpr std::string_view k_nativePlatform = "windows-amd64"sv; -#elif defined(_M_IX86) -static constexpr std::string_view k_nativePlatform = "windows-x86"sv; -#else -static constexpr std::string_view k_nativePlatform = ""sv; -#endif -static constexpr std::string_view k_nativeLibName = "mod.dll"sv; -#elif defined(__ANDROID__) -#if defined(__aarch64__) -static constexpr std::string_view k_nativePlatform = "android-aarch64"sv; -#elif defined(__x86_64__) -static constexpr std::string_view k_nativePlatform = "android-x86_64"sv; -#else -static constexpr std::string_view k_nativePlatform = ""sv; -#endif -static constexpr std::string_view k_nativeLibName = "mod.so"sv; -#elif defined(__APPLE__) -#include -#if TARGET_OS_IOS -static constexpr std::string_view k_nativePlatform = "ios-arm64"sv; -#elif TARGET_OS_TV -static constexpr std::string_view k_nativePlatform = "tvos-arm64"sv; -#elif defined(__aarch64__) -static constexpr std::string_view k_nativePlatform = "macos-arm64"sv; -#elif defined(__x86_64__) -static constexpr std::string_view k_nativePlatform = "macos-x86_64"sv; -#else -static constexpr std::string_view k_nativePlatform = ""sv; -#endif -static constexpr std::string_view k_nativeLibName = "mod.so"sv; -#elif defined(__linux__) -#if defined(__aarch64__) -static constexpr std::string_view k_nativePlatform = "linux-aarch64"sv; -#elif defined(__x86_64__) -static constexpr std::string_view k_nativePlatform = "linux-x86_64"sv; -#elif defined(__i386__) -static constexpr std::string_view k_nativePlatform = "linux-x86"sv; -#else -static constexpr std::string_view k_nativePlatform = ""sv; -#endif -static constexpr std::string_view k_nativeLibName = "mod.so"sv; -#else -static constexpr std::string_view k_nativePlatform = ""sv; -static constexpr std::string_view k_nativeLibName = ""sv; -#endif +namespace fs = std::filesystem; namespace dusk::mods { namespace { constexpr borealis::Log Log{"dusk::mods::loader"}; ModLoader g_modLoader; -constexpr std::string_view k_nativeLibDir = "lib/"sv; -class DirectoryRollback { -public: - ~DirectoryRollback() { - if (!mPath.empty()) { - std::error_code ec; - std::filesystem::remove_all(mPath, ec); - } - } - - void set_path(std::filesystem::path path) { mPath = std::move(path); } - void release() { mPath.clear(); } - -private: - std::filesystem::path mPath; -}; - -std::unique_ptr load_bundle(const std::filesystem::path& modPath, bool fromDir) { - if (fromDir) { - return std::make_unique(modPath); - } else { - std::vector data = io::FileStream::ReadAllBytes(modPath); - return std::make_unique(std::move(data)); +void complete_operation(const std::shared_ptr& operation, const bool success = true, + std::string message = {}) { + if (operation == nullptr) { + return; } + operation->state = success ? ModOperation::State::Succeeded : ModOperation::State::Failed; + operation->message = std::move(message); } -struct NativeRuntimeLocation { - std::string entry; - std::vector runtimeEntries; - bool anyLibs = false; -}; - -struct NativeLocateFailure { - NativeModStatus status; - std::string logMessage; -}; - -using NativeLocateResult = std::variant; - -bool has_native_library_extension(std::string_view name) { - const auto endsWith = [name](std::string_view extension) { - if (name.size() < extension.size()) { - return false; - } - const auto suffix = name.substr(name.size() - extension.size()); - return std::ranges::equal(suffix, extension, [](char lhs, char rhs) { - const auto lower = [](char value) { - return value >= 'A' && value <= 'Z' ? static_cast(value + ('a' - 'A')) : - value; - }; - return lower(lhs) == lower(rhs); - }); - }; - return endsWith(".dll"sv) || endsWith(".so"sv) || endsWith(".dylib"sv); -} - -NativeLocateResult locate_native_runtime(ModBundle& bundle) { - NativeRuntimeLocation result; - const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform); - const std::string nativeEntry = platformPrefix + std::string{k_nativeLibName}; - for (const auto& name : bundle.getFileNames()) { - if (name.find('/') == std::string::npos && has_native_library_extension(name)) { - return NativeLocateFailure{ - NativeModStatus::InvalidBundle, - fmt::format( - "native library '{}' found at the root (natives go in /lib/{{platform}})", - name), - }; - } - if (!name.starts_with(k_nativeLibDir)) { - continue; - } - - const std::string_view libPath{ - name.data() + k_nativeLibDir.size(), name.size() - k_nativeLibDir.size()}; - const auto platformEnd = libPath.find('/'); - if (platformEnd != std::string_view::npos) { - const auto entryName = libPath.substr(platformEnd + 1); - if (entryName.find('/') == std::string_view::npos && - (entryName == "mod.dll"sv || entryName == "mod.so"sv)) - { - result.anyLibs = true; - } - } - - if (!k_nativePlatform.empty() && name.starts_with(platformPrefix)) { - const std::string_view relativeName{ - name.data() + platformPrefix.size(), name.size() - platformPrefix.size()}; - if (!is_safe_resource_path(relativeName)) { - continue; - } - result.runtimeEntries.push_back(name); - } - if (name == nativeEntry) { - result.entry = name; - } +LoadedMod::FileIdentity file_identity(const fs::path& path) { + std::error_code error; + const bool isDirectory = fs::is_directory(path, error); + if (error) { + return {}; } - std::ranges::sort(result.runtimeEntries); - result.runtimeEntries.erase( - std::unique(result.runtimeEntries.begin(), result.runtimeEntries.end()), - result.runtimeEntries.end()); - return result; + const auto size = isDirectory ? 0 : fs::file_size(path, error); + if (error) { + return {}; + } + const auto modified = fs::last_write_time(path, error); + if (error) { + return {}; + } + return {.size = size, .modified = modified, .valid = true}; } + } // namespace ModLoader& ModLoader::instance() { return g_modLoader; } -class InvalidModDataException : public std::runtime_error { -public: - explicit InvalidModDataException(const std::string& msg) : runtime_error(msg) {} - explicit InvalidModDataException(const char* msg) : runtime_error(msg) {} -}; - -static void validate_mod_id(std::string_view const str) { - if (str.empty()) { - throw InvalidModDataException("Missing ID value in mod metadata!"); - } - - bool lastWasPeriod = false; - for (auto const chr : str) { - if (chr == '.') { - if (lastWasPeriod) { - throw InvalidModDataException("Cannot have two consecutive periods in mod ID!"); - } - lastWasPeriod = true; - continue; - } - - lastWasPeriod = false; - - if (chr == '_') - continue; - - if (chr >= '0' && chr <= '9') - continue; - - if (chr >= 'a' && chr <= 'z') - continue; - - if (chr >= 'A' && chr <= 'Z') - continue; - - throw InvalidModDataException( - fmt::format("Invalid character '{}' in mod ID. Valid characters are period, " - "underscore, and alphanumerics.", - chr)); - } -} - -static bool bundle_has_file(ModBundle& bundle, const std::string& path) { - try { - bundle.getFileSize(path); - return true; - } catch (const std::runtime_error&) { - return false; - } -} - -static std::string resolve_image_path(ModBundle& bundle, const std::string& modId, - std::string_view key, const std::string& manifestPath, const std::string& defaultPath) { - if (!manifestPath.empty()) { - if (!is_safe_resource_path(manifestPath)) { - log::write( - modId, LOG_LEVEL_WARN, "invalid {} path '{}' in mod.json", key, manifestPath); - } else if (!bundle_has_file(bundle, manifestPath)) { - log::write( - modId, LOG_LEVEL_WARN, "{} path '{}' not found in bundle", key, manifestPath); - } else { - return manifestPath; - } - } - if (bundle_has_file(bundle, defaultPath)) { - return defaultPath; - } - return {}; -} - -struct LoadedManifest { - ModMetadata metadata; - std::optional runtime; -}; - -static uint16_t parse_runtime_version_component(std::string_view text, std::string_view fieldName) { - uint32_t value = 0; - const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value); - if (text.empty() || error != std::errc{} || end != text.data() + text.size() || - value > UINT16_MAX) - { - throw InvalidModDataException(fmt::format("Invalid {} in runtime version pin", fieldName)); - } - return static_cast(value); -} - -static std::optional parse_runtime(const nlohmann::json& manifest) { - const auto field = manifest.find("runtime"); - if (field == manifest.end()) { - return std::nullopt; - } - if (!field->is_string()) { - throw InvalidModDataException("runtime must be a string"); - } - - const std::string pin = field->get(); - const auto at = pin.rfind('@'); - if (at == std::string::npos || at == 0 || at + 1 == pin.size() || pin.find('@') != at || - at >= MOD_META_SERVICE_ID_SIZE) - { - throw InvalidModDataException( - "runtime must be a service id followed by @major or @major.minor"); - } - - const std::string_view version{pin.data() + at + 1, pin.size() - at - 1}; - const auto dot = version.find('.'); - if (dot != std::string_view::npos && version.find('.', dot + 1) != std::string_view::npos) { - throw InvalidModDataException("runtime version pin has too many components"); - } - - DelegatedModRuntime result; - result.id = pin.substr(0, at); - result.major = parse_runtime_version_component( - dot == std::string_view::npos ? version : version.substr(0, dot), "major version"); - if (dot != std::string_view::npos) { - result.minMinor = parse_runtime_version_component(version.substr(dot + 1), "minor version"); - } - return result; -} - -static LoadedManifest load_manifest(const std::filesystem::path& modPath, ModBundle& bundle) { - const auto metaJson = bundle.readFile("mod.json"); - auto j = nlohmann::json::parse(metaJson); - - std::string metaId = j.value("id", ""); - std::string metaName = j.value("name", ""); - std::string metaVersion = j.value("version", ""); - std::string metaAuthor = j.value("author", ""); - std::string metaDescription = j.value("description", ""); - std::string metaIcon = j.value("icon", ""); - std::string metaBanner = j.value("banner", ""); - - validate_mod_id(metaId); - - if (metaName.empty()) { - metaName = borealis::io::fs_path_to_string(modPath.stem()); - } - if (metaVersion.empty()) { - metaVersion = "?"s; - } - if (metaAuthor.empty()) { - metaAuthor = "unknown"s; - } - - std::string iconPath = resolve_image_path(bundle, metaId, "icon", metaIcon, "res/icon.png"s); - std::string bannerPath = - resolve_image_path(bundle, metaId, "banner", metaBanner, "res/banner.png"s); - - return LoadedManifest{ - .metadata = - { - std::move(metaId), - std::move(metaName), - std::move(metaVersion), - std::move(metaAuthor), - std::move(metaDescription), - std::move(iconPath), - std::move(bannerPath), - }, - .runtime = parse_runtime(j), - }; -} - -// True if the first `capacity` bytes of `str` contain a NUL. -static bool terminated_within(const char* str, size_t capacity) { - return std::memchr(str, '\0', capacity) != nullptr; -} - -static bool parse_meta(NativeMod& native, LoadedMod& mod) { - const ModMeta* meta = native.meta; - if (meta->struct_size < sizeof(ModMeta)) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta descriptor has invalid size {}", - meta->struct_size); - mod.nativeStatus = NativeModStatus::InvalidMetadata; - return false; - } - const auto* cursor = static_cast(meta->records_begin); - const auto* end = static_cast(meta->records_end); - if (cursor == nullptr || end == nullptr || cursor > end || - (reinterpret_cast(cursor) & 7) != 0) - { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta section bounds are invalid"); - mod.nativeStatus = NativeModStatus::InvalidMetadata; - return false; - } - - ModMetaParsed parsed; - size_t headerCount = 0; - const auto invalid = [&](std::string_view why) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "invalid metadata record at offset {}: {}", - cursor - static_cast(meta->records_begin), why); - mod.nativeStatus = NativeModStatus::InvalidMetadata; - return false; - }; - - while (cursor < end) { - if (end - cursor < 8) { - return invalid("trailing bytes"); - } - uint64_t first = 0; - std::memcpy(&first, cursor, sizeof(first)); - if (first == 0) { // linker padding / bounds sentinel - cursor += 8; - continue; - } - - const auto* rec = reinterpret_cast(cursor); - const size_t size = rec->size; - if (size < 8 || size % 8 != 0 || size > static_cast(end - cursor)) { - return invalid("bad record size"); - } - - switch (rec->kind) { - case MOD_META_PAD: - break; - case MOD_META_HEADER: { - if (size < sizeof(ModMetaHeader)) { - return invalid("truncated header record"); - } - const auto* header = reinterpret_cast(rec); - ++headerCount; - parsed.abiVersion = header->abi_version; - break; - } - case MOD_META_IMPORT: { - if (size < sizeof(ModMetaImport)) { - return invalid("truncated import record"); - } - auto* record = reinterpret_cast(const_cast(cursor)); - if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) { - return invalid("unterminated import service id"); - } - parsed.imports.push_back(record); - break; - } - case MOD_META_EXPORT: { - if (size < sizeof(ModMetaExport)) { - return invalid("truncated export record"); - } - auto* record = reinterpret_cast(const_cast(cursor)); - if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) { - return invalid("unterminated export service id"); - } - parsed.exports.push_back(record); - break; - } - case MOD_META_HOOK_FN: { - if (size < sizeof(ModMetaHookFn)) { - return invalid("truncated hook record"); - } - parsed.hookFns.push_back( - reinterpret_cast(const_cast(cursor))); - break; - } - case MOD_META_HOOK_MEM: { - if (size <= sizeof(ModMetaHookMem)) { - return invalid("truncated hook record"); - } - auto* record = reinterpret_cast(const_cast(cursor)); - const char* strings = reinterpret_cast(cursor) + sizeof(ModMetaHookMem); - const size_t capacity = size - sizeof(ModMetaHookMem); - if (!terminated_within(strings, capacity)) { - return invalid("unterminated hook vtable symbol"); - } - const size_t vtableLen = std::char_traits::length(strings); - if (!terminated_within(strings + vtableLen + 1, capacity - vtableLen - 1)) { - return invalid("unterminated hook display name"); - } - parsed.hookMems.push_back(record); - break; - } - case MOD_META_HOOK_MEM_EXT: { - if (size <= sizeof(ModMetaHookMemExt)) { - return invalid("truncated extended hook record"); - } - auto* record = reinterpret_cast(const_cast(cursor)); - if (record->pmf_size <= MOD_META_HOOK_MEM_CAPACITY || - record->pmf_size > MOD_META_HOOK_MEM_EXT_CAPACITY || record->materialize == nullptr) - { - return invalid("bad extended hook member-pointer size"); - } - const char* strings = reinterpret_cast(cursor) + sizeof(ModMetaHookMemExt); - const size_t capacity = size - sizeof(ModMetaHookMemExt); - if (!terminated_within(strings, capacity)) { - return invalid("unterminated extended hook vtable symbol"); - } - const size_t vtableLen = std::char_traits::length(strings); - if (!terminated_within(strings + vtableLen + 1, capacity - vtableLen - 1)) { - return invalid("unterminated extended hook display name"); - } - parsed.hookMemExts.push_back(record); - break; - } - case MOD_META_HOOK_NAME: { - if (size <= sizeof(ModMetaHookName)) { - return invalid("truncated hook record"); - } - auto* record = reinterpret_cast(const_cast(cursor)); - const char* name = reinterpret_cast(cursor) + sizeof(ModMetaHookName); - if (!terminated_within(name, size - sizeof(ModMetaHookName))) { - return invalid("unterminated hook symbol name"); - } - parsed.hookNames.push_back(record); - break; - } - default: - // Additive record kinds may appear within a format version; skip them. - log::write(mod.metadata.id, LOG_LEVEL_DEBUG, "skipping unknown metadata record kind {}", - rec->kind); - break; - } - cursor += size; - } - - if (headerCount != 1) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expected 1 metadata header record, found {}", - headerCount); - mod.nativeStatus = NativeModStatus::InvalidMetadata; - return false; - } - if (parsed.abiVersion != MOD_ABI_VERSION) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expects ABI v{} but engine is v{}, skipping", - parsed.abiVersion, MOD_ABI_VERSION); - mod.nativeStatus = NativeModStatus::ApiVersionMismatch; - return false; - } - - native.parsed = std::move(parsed); - return true; -} - static std::string lifecycle_error_message( const char* fnName, const ModResult result, const ModError& error) { if (error.message[0] != '\0') { @@ -539,269 +83,6 @@ static std::string lifecycle_error_message( return fmt::format("{} failed with result {}", fnName, static_cast(result)); } -static std::string native_status_message(const NativeModStatus status) { - switch (status) { - case NativeModStatus::BuildDisabled: - return "Code mods are disabled on this Dusklight build"; - case NativeModStatus::ModMissingPlatform: - return fmt::format("Mod not supported on this platform ({})", k_nativePlatform); - case NativeModStatus::ApiVersionMismatch: - // TODO: differentiate whether mod or Dusklight is out of date - return "Mod ABI version mismatch"; - case NativeModStatus::MissingExport: - return "Missing required mod API exports"; - case NativeModStatus::InvalidMetadata: - return "Invalid mod metadata records"; - case NativeModStatus::InvalidBundle: - return "Invalid mod bundle layout (old mod?)"; - case NativeModStatus::Unknown: - return "Unknown mod load failure"; - case NativeModStatus::None: - case NativeModStatus::Loaded: - break; - } - return "native mod failed to load"; -} - -std::filesystem::path ModLoader::external_native_lib_path(const LoadedMod& mod) const { - namespace fs = std::filesystem; - if (k_nativeLibName.empty()) { - return {}; - } - const auto& libDir = m_searchDirs[mod.searchDirIndex].nativeLibDir; - if (libDir.empty()) { - return {}; - } - fs::path path = libDir / fs::path(mod.metadata.id + borealis::io::fs_path_to_string( - fs::path(k_nativeLibName).extension())); - std::error_code ec; - if (!fs::is_regular_file(path, ec)) { - return {}; - } - return path; -} - -void ModLoader::load_native( - LoadedMod& mod, const std::string& dllEntry, const std::vector& runtimeEntries) { - if (!EnableCodeMods) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "Code mods are not available in this build"); - mod.nativeStatus = NativeModStatus::BuildDisabled; - return; - } - - namespace fs = std::filesystem; - - const fs::path cacheDir = m_cacheDir / mod.metadata.id; - const fs::path scratchDir = cacheDir / "data"; - std::error_code ec; - fs::create_directories(scratchDir, ec); - if (ec) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to create mod directory {}: {}", - data::abbreviated_path_string(scratchDir), ec.message()); - return; - } - mod.dir = fs::absolute(scratchDir); - mod.dirUtf8 = borealis::io::fs_path_to_string(mod.dir); - - fs::path libPath; - fs::path runtimeDir; - DirectoryRollback runtimeDirRollback; - if (mod.inPlace) { - if (!dllEntry.empty()) { - libPath = mod.modPath / dllEntry; - } else if (auto external = external_native_lib_path(mod); !external.empty()) { - libPath = std::move(external); - } else { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, - "no native library named {} found; skipping", k_nativeLibName); - mod.nativeStatus = NativeModStatus::ModMissingPlatform; - return; - } - runtimeDir = libPath.parent_path(); - } else { - if (dllEntry.empty()) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, - "no native library named {} found; skipping", k_nativeLibName); - mod.nativeStatus = NativeModStatus::ModMissingPlatform; - return; - } - - // Every generation gets a new directory. The main module and all of its runtime - // libraries therefore have fresh paths and can coexist with a previous generation - // that is still unwinding after a reload. - runtimeDir = cacheDir / fmt::format("g{}", ++mod.cacheGeneration); - runtimeDirRollback.set_path(runtimeDir); - fs::create_directories(runtimeDir, ec); - if (ec) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, - "failed to create native runtime directory {}: {}", - data::abbreviated_path_string(runtimeDir), ec.message()); - return; - } - - const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform); - for (const auto& entry : runtimeEntries) { - if (!entry.starts_with(platformPrefix)) { - continue; - } - const std::string_view relativeName{ - entry.data() + platformPrefix.size(), entry.size() - platformPrefix.size()}; - if (!is_safe_resource_path(relativeName)) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, - "unsafe native runtime path '{}'; skipping", entry); - return; - } - - const fs::path outputPath = runtimeDir / fs::path{relativeName}; - fs::create_directories(outputPath.parent_path(), ec); - if (ec) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, - "failed to create directory for {}: {}", entry, ec.message()); - return; - } - - std::vector data; - try { - data = mod.bundle->readFile(entry); - } catch (const std::exception& e) { - log::write( - mod.metadata.id, LOG_LEVEL_ERROR, "failed to extract {}: {}", entry, e.what()); - return; - } - - std::ofstream out(outputPath, std::ios::binary | std::ios::out); - if (!out) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry); - return; - } - out.write(reinterpret_cast(data.data()), - static_cast(data.size())); - if (!out) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry); - return; - } - } - - libPath = runtimeDir / fs::path{dllEntry}.filename(); - } - - auto nativeMod = std::make_unique(); - try { - nativeMod->handle = std::make_unique(libPath); - } catch (const std::runtime_error& e) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to open {}: {}", - data::abbreviated_path_string(libPath), e.what()); - return; - } - - nativeMod->meta = nativeMod->handle->LookupSymbol("mod_meta"); - nativeMod->contextSymbol = nativeMod->handle->LookupSymbol("mod_ctx"); - nativeMod->fn_initialize = nativeMod->handle->LookupSymbol("mod_initialize"); - nativeMod->fn_update = nativeMod->handle->LookupSymbol("mod_update"); - nativeMod->fn_shutdown = nativeMod->handle->LookupSymbol("mod_shutdown"); - - if (!nativeMod->meta || !nativeMod->contextSymbol || !nativeMod->fn_initialize || - !nativeMod->fn_update || !nativeMod->fn_shutdown) - { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, - "{} missing required mod API exports; skipping", - data::abbreviated_path_string(libPath)); - mod.nativeStatus = NativeModStatus::MissingExport; - return; - } - - if (!parse_meta(*nativeMod, mod)) { - return; - } - - if (nativeMod->contextSymbol == nullptr) { - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "missing required mod_ctx export"); - mod.nativeStatus = NativeModStatus::MissingExport; - return; - } - *nativeMod->contextSymbol = mod.context.get(); - - mod.nativePath = fs::absolute(libPath); - mod.nativeDir = fs::absolute(runtimeDir); - mod.nativeDirUtf8 = borealis::io::fs_path_to_string(mod.nativeDir); - mod.native = std::move(nativeMod); - mod.nativeStatus = NativeModStatus::Loaded; - runtimeDirRollback.release(); -} - -bool ModLoader::load_native_if_present(LoadedMod& mod) { - const auto result = locate_native_runtime(*mod.bundle); - if (const auto* failure = std::get_if(&result)) { - mod.nativeStatus = failure->status; - log::write(mod.metadata.id, LOG_LEVEL_ERROR, "{}", failure->logMessage); - fail_mod(mod, MOD_ERROR, native_status_message(failure->status)); - return false; - } - - const auto& native = std::get(result); - if (mod.runtime.has_value() && - (native.anyLibs || (mod.inPlace && !external_native_lib_path(mod).empty()))) - { - mod.nativeStatus = NativeModStatus::InvalidBundle; - fail_mod(mod, MOD_CONFLICT, "A mod cannot declare both runtime and native code"); - return false; - } - if (!native.anyLibs && !(mod.inPlace && !external_native_lib_path(mod).empty())) { - mod.nativeStatus = NativeModStatus::None; - return true; - } - - mod.nativeStatus = NativeModStatus::Unknown; - load_native(mod, native.entry, native.runtimeEntries); - if (mod.nativeStatus != NativeModStatus::Loaded) { - fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); - return false; - } - return true; -} - -void ModLoader::unload_native(LoadedMod& mod) { - if (!mod.native || mod.inPlace) { - return; - } - // Deferred dlclose: this mod's code may still be on the stack below the current tick - m_retiredNatives.push_back({std::move(mod.native), std::move(mod.nativeDir)}); - mod.nativePath.clear(); - mod.nativeDir.clear(); - mod.nativeDirUtf8.clear(); -} - -void ModLoader::drain_retired_natives() { - for (auto& retired : m_retiredNatives) { - retired.native.reset(); - if (!retired.directory.empty()) { - std::error_code ec; - std::filesystem::remove_all(retired.directory, ec); - } - } - m_retiredNatives.clear(); -} - -static ModManifestInfo build_manifest_info(const ModMetaParsed& parsed) { - ModManifestInfo info; - info.imports.reserve(parsed.imports.size()); - for (const auto* record : parsed.imports) { - if (!svc::valid_service_id(record->service_id.chars)) { - continue; - } - info.imports.push_back({record->service_id.chars, record->major_version, - record->min_minor_version, (record->rec.flags & SERVICE_IMPORT_OPTIONAL) == 0}); - } - info.exports.reserve(parsed.exports.size()); - for (const auto* record : parsed.exports) { - if (!svc::valid_service_id(record->service_id.chars)) { - continue; - } - info.exports.push_back({record->service_id.chars, record->major_version}); - } - return info; -} - std::string escape_mod_id_for_config(std::string_view const id) { std::string buf; @@ -851,16 +132,16 @@ static void warn_unpublished_deferred_exports(const LoadedMod& mod) { } } -void ModLoader::try_load_mod( - const std::filesystem::path& modPath, bool fromDir, uint32_t searchDirIndex) { - namespace fs = std::filesystem; - - std::unique_ptr bundle; - try { - bundle = load_bundle(modPath, fromDir); - } catch (const std::exception& e) { - Log.error("Failed to open {} bundle: {}", data::abbreviated_path_string(modPath), e.what()); - return; +LoadedMod* ModLoader::try_load_mod(const fs::path& modPath, bool fromDir, uint32_t searchDirIndex, + std::unique_ptr bundle) { + if (bundle == nullptr) { + try { + bundle = load_bundle(modPath, fromDir); + } catch (const std::exception& e) { + Log.error( + "Failed to open {} bundle: {}", data::abbreviated_path_string(modPath), e.what()); + return nullptr; + } } LoadedManifest manifest; @@ -868,7 +149,7 @@ void ModLoader::try_load_mod( manifest = load_manifest(modPath, *bundle); } catch (const std::exception& e) { Log.error("bad mod.json in {}: {}", data::abbreviated_path_string(modPath), e.what()); - return; + return nullptr; } if (const auto* existing = find_mod(manifest.metadata.id)) { @@ -881,7 +162,7 @@ void ModLoader::try_load_mod( log::write(manifest.metadata.id, LOG_LEVEL_ERROR, "duplicate mod id, not loading {}", data::abbreviated_path_string(modPath)); } - return; + return nullptr; } const auto& inserted = m_mods.emplace_back(std::make_unique()); @@ -889,7 +170,9 @@ void ModLoader::try_load_mod( mod.active = true; mod.modPath = fs::absolute(modPath); mod.searchDirIndex = searchDirIndex; - mod.inPlace = m_searchDirs[searchDirIndex].inPlaceNative && fromDir; + mod.fromDirectory = fromDir; + mod.nativeInPlace = m_searchDirs[searchDirIndex].inPlaceNative && fromDir; + mod.fileIdentity = file_identity(modPath); mod.metadata = std::move(manifest.metadata); mod.runtime = std::move(manifest.runtime); mod.bundle = std::move(bundle); @@ -919,6 +202,7 @@ void ModLoader::try_load_mod( log::write(mod.metadata.id, LOG_LEVEL_INFO, "found '{}' v{} by {} ({})", mod.metadata.name, mod.metadata.version, mod.metadata.author, data::abbreviated_path_string(modPath)); + return &mod; } bool ModLoader::activate_mod(LoadedMod& mod) { @@ -1072,55 +356,30 @@ void ModLoader::init() { m_cacheDir = m_searchDirs.front().path / ".cache"; } - namespace fs = std::filesystem; std::error_code ec; // Stale libs from previous sessions (see load_native). fs::remove_all(m_cacheDir, ec); - for (size_t dirIndex = 0; dirIndex < m_searchDirs.size(); ++dirIndex) { - const auto& searchDir = m_searchDirs[dirIndex]; - - // --mods can point the user dir at the bundled dir; don't scan the same dir twice. - bool alreadyScanned = false; - for (size_t earlier = 0; earlier < dirIndex && !alreadyScanned; ++earlier) { - alreadyScanned = fs::equivalent(m_searchDirs[earlier].path, searchDir.path, ec); - } - if (alreadyScanned) { + const auto packages = scan_packages(m_searchDirs); + for (const auto& package : packages) { + const auto* selected = select_package(packages, package.metadata.id); + if (selected != &package) { + log::write(package.metadata.id, LOG_LEVEL_INFO, "{} v{} shadowed by {} v{}", + data::abbreviated_path_string(package.path), package.metadata.version, + data::abbreviated_path_string(selected->path), selected->metadata.version); continue; } - - if (!fs::is_directory(searchDir.path)) { - if (dirIndex == 0) { - Log.info( - "mods directory '{}' not found", data::abbreviated_path_string(searchDir.path)); - } else { - Log.debug( - "mods directory '{}' not found", data::abbreviated_path_string(searchDir.path)); - } - continue; - } - - std::vector entries; - for (auto& e : fs::directory_iterator(searchDir.path, ec)) { - if (e.is_directory() && std::filesystem::exists(e.path() / "mod.json")) { - entries.push_back(e); - } else if (e.is_regular_file() && e.path().extension() == ".dusk") { - entries.push_back(e); - } - } - std::sort(entries.begin(), entries.end(), - [](const fs::directory_entry& a, const fs::directory_entry& b) { - return a.path().filename() < b.path().filename(); - }); - - for (auto& entry : entries) { - try_load_mod(entry.path(), entry.is_directory(), static_cast(dirIndex)); + if (auto* mod = try_load_mod(package.path, package.fromDirectory, package.searchDirIndex)) { + record_package_sources(*mod, packages); } } if (m_mods.empty()) { + init_services(); Log.info("no mods found"); + svc::modules_lifecycle_applied(); + m_startupComplete = true; return; } @@ -1185,10 +444,19 @@ void ModLoader::init() { m_startupComplete = true; } -LoadedMod* ModLoader::find_mod(std::string_view id) const { - for (auto& mod : mods()) { - if (mod.metadata.id == id) { - return &mod; +LoadedMod* ModLoader::find_mod(std::string_view id) { + for (auto& mod : m_mods) { + if (mod->metadata.id == id) { + return mod.get(); + } + } + return nullptr; +} + +const LoadedMod* ModLoader::find_mod(std::string_view id) const { + for (const auto& mod : m_mods) { + if (mod->metadata.id == id) { + return mod.get(); } } return nullptr; @@ -1206,8 +474,61 @@ void ModLoader::request_disable(std::string_view id) { } } -void ModLoader::request_reload(std::string_view id) { - m_pendingRequests.push_back({std::string{id}, RequestKind::Reload}); +ModOperationHandle ModLoader::request_reload(std::string_view id) { + auto operation = std::make_shared(); + if (find_mod(id) != nullptr) { + m_pendingRequests.push_back(ReloadRequest{ + .modId = std::string{id}, + .operation = operation, + }); + } else { + complete_operation(operation, false, "The mod is no longer installed"); + } + return operation; +} + +ModOperationHandle ModLoader::request_install(fs::path path) { + auto operation = std::make_shared(); + m_pendingRequests.push_back(InstallRequest{ + .stagedPath = std::move(path), + .operation = operation, + }); + return operation; +} + +ModOperationHandle ModLoader::request_uninstall(std::string_view id) { + auto operation = std::make_shared(); + if (find_mod(id) != nullptr) { + m_pendingRequests.push_back(UninstallRequest{ + .modId = std::string{id}, + .operation = operation, + }); + } else { + complete_operation(operation); + } + return operation; +} + +ModOperationHandle ModLoader::request_reactivate(std::string_view id) { + auto operation = std::make_shared(); + m_pendingRequests.push_back(LifecycleRequest{ + .modId = std::string{id}, + .action = LifecycleAction::Reactivate, + .operation = operation, + }); + return operation; +} + +fs::path ModLoader::user_mods_dir() const { + return m_searchDirs.empty() ? fs::path{} : m_searchDirs.front().path; +} + +bool ModLoader::can_uninstall(const LoadedMod& mod) const { + return mod.hasUserPackage; +} + +bool ModLoader::can_update(const LoadedMod& mod) const { + return mod.searchDirIndex != 0 || (!mod.fromDirectory && can_uninstall(mod)); } void ModLoader::notify_mod_failure(LoadedMod& mod, bool firstFailure) { @@ -1218,7 +539,10 @@ void ModLoader::notify_mod_failure(LoadedMod& mod, bool firstFailure) { if (!m_startupComplete) { return; } - m_pendingRequests.push_back({mod.metadata.id, RequestKind::Disable}); + m_pendingRequests.emplace_back(LifecycleRequest{ + .modId = mod.metadata.id, + .action = LifecycleAction::Disable, + }); } void ModLoader::flush_toasts() { @@ -1251,7 +575,7 @@ void ModLoader::flush_toasts() { ui::push_toast(std::move(toast)); } -std::vector ModLoader::collect_lifecycle_set(LoadedMod& target) { +std::vector ModLoader::collect_lifecycle_set(LoadedMod& target) const { std::vector included{&target}; std::vector pending{&target}; while (!pending.empty()) { @@ -1280,15 +604,7 @@ std::vector ModLoader::collect_lifecycle_set(LoadedMod& target) { return ordered; } -bool ModLoader::ensure_native_loaded(LoadedMod& mod) { - if (mod.native || mod.nativeStatus == NativeModStatus::None) { - return true; - } - return load_native_if_present(mod); -} - bool ModLoader::reload_bundle(LoadedMod& mod) { - namespace fs = std::filesystem; log::write(mod.metadata.id, LOG_LEVEL_INFO, "reloading from {}", data::abbreviated_path_string(mod.modPath)); @@ -1314,6 +630,7 @@ bool ModLoader::reload_bundle(LoadedMod& mod) { mod.runtime = std::move(newManifest.runtime); // In-flight readers of the old bundle keep it alive through their shared_ptr. mod.bundle = std::move(newBundle); + mod.fileIdentity = file_identity(mod.modPath); mod.loadFailed = false; mod.failureReason.clear(); @@ -1343,7 +660,48 @@ bool ModLoader::reload_bundle(LoadedMod& mod) { return true; } -void ModLoader::apply_lifecycle_change(LoadedMod& target, const bool reload) { +void ModLoader::resume_lifecycle_set(const std::vector& affected) { + for (auto* mod : affected) { + if (mod->active || mod->loadFailed || !mod->cvarIsEnabled->getValue()) { + continue; + } + if (!ensure_native_loaded(*mod)) { + continue; + } + if (mod->native && !mod->servicesRegistered) { + if (register_static_service_exports(*mod)) { + mod->servicesRegistered = true; + } else { + log::write(mod->metadata.id, LOG_LEVEL_ERROR, "failed to register service exports"); + deactivate_mod(*mod); + } + } + } + + for (auto* mod : affected) { + if (mod->active || mod->loadFailed || !mod->cvarIsEnabled->getValue()) { + continue; + } + if (!required_deps_active(*mod)) { + mod->suspendedByProvider = true; + log::write( + mod->metadata.id, LOG_LEVEL_INFO, "suspended: a required provider is disabled"); + continue; + } + mod->suspendedByProvider = false; + activate_mod(*mod); + } + + for (auto* mod : affected) { + if (!mod->active && mod->servicesRegistered) { + svc::remove_services_for_provider(*mod); + mod->servicesRegistered = false; + } + } +} + +void ModLoader::apply_lifecycle_change( + LoadedMod& target, const bool reload, const PackageCandidate* replacement) { auto affected = collect_lifecycle_set(target); // Dependents first (reverse init order), like shutdown. @@ -1363,6 +721,17 @@ void ModLoader::apply_lifecycle_change(LoadedMod& target, const bool reload) { } } + if (replacement != nullptr) { + target.modPath = replacement->path; + target.searchDirIndex = replacement->searchDirIndex; + target.fromDirectory = replacement->fromDirectory; + target.nativeInPlace = + replacement->fromDirectory && m_searchDirs[replacement->searchDirIndex].inPlaceNative; + std::stable_sort(m_mods.begin(), m_mods.end(), + [](const auto& a, const auto& b) { return a->searchDirIndex > b->searchDirIndex; }); + loader::sort_mods(m_mods); + } + if (reload) { // On failure the target is failed and stays down; dependents get resume attempts below // and suspend against the failed provider where required. @@ -1380,48 +749,7 @@ void ModLoader::apply_lifecycle_change(LoadedMod& target, const bool reload) { affected = std::move(reordered); } - // Mirror startup: publish every candidate's static exports before any of them initialize, - // so importers within the set resolve providers regardless of initialization order - // (optional cycles rely on this). - for (auto* mod : affected) { - if (mod->active || mod->loadFailed || !mod->cvarIsEnabled->getValue()) { - continue; - } - if (!ensure_native_loaded(*mod)) { - continue; - } - if (mod->native && !mod->servicesRegistered) { - if (register_static_service_exports(*mod)) { - mod->servicesRegistered = true; - } else { - log::write(mod->metadata.id, LOG_LEVEL_ERROR, "failed to register service exports"); - deactivate_mod(*mod); - } - } - } - - // Providers first (init order). The target is naturally first among the affected mods. - for (auto* mod : affected) { - if (mod->active || mod->loadFailed || !mod->cvarIsEnabled->getValue()) { - continue; - } - if (!required_deps_active(*mod)) { - mod->suspendedByProvider = true; - log::write( - mod->metadata.id, LOG_LEVEL_INFO, "suspended: a required provider is disabled"); - continue; - } - mod->suspendedByProvider = false; - activate_mod(*mod); - } - - // Mods that stayed down must not leave their exports resolvable. - for (auto* mod : affected) { - if (!mod->active && mod->servicesRegistered) { - svc::remove_services_for_provider(*mod); - mod->servicesRegistered = false; - } - } + resume_lifecycle_set(affected); } void ModLoader::on_enabled_changed(LoadedMod& mod) { @@ -1435,13 +763,325 @@ void ModLoader::on_enabled_changed(LoadedMod& mod) { } if (mod.suspendedByProvider) { if (!mod.cvarIsEnabled->getValue()) { - // The user disabled a suspended mod; stop waiting for its providers. mod.suspendedByProvider = false; } return; } - m_pendingRequests.push_back({mod.metadata.id, - mod.cvarIsEnabled->getValue() ? RequestKind::Enable : RequestKind::Disable}); + m_pendingRequests.push_back(LifecycleRequest{ + .modId = mod.metadata.id, + .action = + mod.cvarIsEnabled->getValue() ? LifecycleAction::Enable : LifecycleAction::Disable, + }); +} + +void ModLoader::forget_mod(LoadedMod& mod) { + auto affected = collect_lifecycle_set(mod); + std::vector blocked; + std::vector pending{&mod}; + while (!pending.empty()) { + auto* provider = pending.back(); + pending.pop_back(); + for (const auto& edge : provider->dependents) { + if (!edge.required || edge.mod == nullptr || + std::ranges::find(blocked, edge.mod) != blocked.end()) + { + continue; + } + blocked.push_back(edge.mod); + pending.push_back(edge.mod); + } + } + + for (auto* affectedMod : affected | std::views::reverse) { + const bool wasActive = affectedMod->active; + if (affectedMod->active || affectedMod->initialized || affectedMod->native != nullptr) { + log::write(affectedMod->metadata.id, LOG_LEVEL_INFO, "deactivating mod"); + deactivate_mod(*affectedMod); + } + if (affectedMod != &mod && wasActive) { + affectedMod->suspendedByProvider = true; + } + } + + const std::string modId = mod.metadata.id; + auto* removedMod = &mod; + if (mod.enabledSubscription != 0) { + config::unsubscribe(mod.enabledSubscription); + mod.enabledSubscription = 0; + } + unregister(*mod.cvarIsEnabled); + + std::vector remaining; + remaining.reserve(affected.size()); + for (auto* affectedMod : affected) { + if (affectedMod != &mod) { + remaining.push_back(affectedMod); + } + } + std::erase_if( + m_mods, [removedMod](const auto& candidate) { return candidate.get() == removedMod; }); + loader::sort_mods(m_mods); + + std::vector resumable; + for (auto* affectedMod : remaining) { + const bool needsRemovedProvider = std::ranges::find(blocked, affectedMod) != blocked.end(); + affectedMod->suspendedByProvider = needsRemovedProvider; + if (needsRemovedProvider) { + log::write(affectedMod->metadata.id, LOG_LEVEL_INFO, + "suspended: required provider '{}' was removed", modId); + } else { + resumable.push_back(affectedMod); + } + } + resume_lifecycle_set(resumable); + + ++m_generation; + log::write(modId, LOG_LEVEL_INFO, "forgot removed package"); +} + +ModLoader::OperationResult ModLoader::load_runtime_mod(const fs::path& requestedPath) { + const auto path = fs::absolute(requestedPath).lexically_normal(); + std::error_code error; + const auto status = fs::status(path, error); + if (error || !fs::exists(status)) { + return { + .success = false, + .message = error ? fmt::format("Could not inspect the package: {}", error.message()) : + "The package was not found", + }; + } + + const bool fromDir = fs::is_directory(status); + std::unique_ptr bundle; + std::optional metadata; + try { + bundle = load_bundle(path, fromDir); + metadata = load_manifest(path, *bundle).metadata; + } catch (const std::exception& exception) { + return { + .success = false, + .message = fmt::format("Invalid mod package: {}", exception.what()), + }; + } + if (const auto* duplicate = find_mod(metadata->id)) { + return { + .success = false, + .message = fmt::format("A mod with this ID is already loaded from {}", + data::abbreviated_path_string(duplicate->modPath)), + }; + } + auto* mod = try_load_mod(path, fromDir, 0, std::move(bundle)); + if (mod == nullptr) { + return { + .success = false, + .message = "The mod could not be loaded", + }; + } + + mod->enabledSubscription = Register( + *mod->cvarIsEnabled, [this, mod](const bool&, const bool&) { on_enabled_changed(*mod); }); + loader::sort_mods(m_mods); + if (!mod->cvarIsEnabled->getValue()) { + mod->active = false; + mod->suspendedByProvider = false; + log::write(mod->metadata.id, LOG_LEVEL_INFO, "installed disabled by config"); + } else if (!mod->loadFailed) { + mod->active = false; + apply_lifecycle_change(*mod, false); + } + ++m_generation; + log::write(mod->metadata.id, LOG_LEVEL_INFO, "installed at runtime"); + return runtime_result(*mod); +} + +ModLoader::OperationResult ModLoader::reload_runtime_mod( + LoadedMod& mod, const PackageCandidate* replacement) { + if (mod.nativeInPlace && replacement == nullptr) { + return { + .success = false, + .message = "An in-place native library cannot be reloaded", + .mod = &mod, + }; + } + apply_lifecycle_change(mod, true, replacement); + ++m_generation; + return runtime_result(mod); +} + +ModLoader::OperationResult ModLoader::uninstall_runtime_mod(LoadedMod& mod) { + std::vector packages; + try { + packages = scan_packages(m_searchDirs); + } catch (const std::exception& exception) { + return {.success = false, .message = exception.what()}; + } + record_package_sources(mod, packages); + if (!can_uninstall(mod)) { + return {.success = false, .message = "No installed package to remove"}; + } + + std::string removalError; + std::erase_if(packages, [&](const auto& package) { + if (package.metadata.id != mod.metadata.id || package.searchDirIndex != 0 || + package.fromDirectory || package.symlink) + { + return false; + } + std::error_code error; + fs::remove(package.path, error); + if (error) { + removalError = fmt::format("Could not remove {}: {}", + data::abbreviated_path_string(package.path), error.message()); + } + return !error; + }); + + OperationResult result; + if (const auto* selected = select_package(packages, mod.metadata.id)) { + if (selected->path != mod.modPath) { + result = reload_runtime_mod(mod, selected); + } else { + result.mod = &mod; + ++m_generation; + } + record_package_sources(mod, packages); + } else { + forget_mod(mod); + } + if (!removalError.empty()) { + result.success = false; + result.message = std::move(removalError); + } + return result; +} + +ModLoader::OperationResult ModLoader::runtime_result(LoadedMod& mod) { + if (mod.loadFailed) { + return { + .success = false, + .message = mod.failureReason.empty() ? "Mod failed to activate" : mod.failureReason, + .mod = &mod, + }; + } + if (mod.cvarIsEnabled->getValue() && !mod.active) { + return { + .success = false, + .message = "A required provider is unavailable", + .mod = &mod, + }; + } + if (!mod.cvarIsEnabled->getValue()) { + return { + .message = "Installed, disabled by config", + .mod = &mod, + }; + } + return {.mod = &mod}; +} + +ModLoader::OperationResult ModLoader::install_staged(const fs::path& requestedPath) { + if (m_searchDirs.empty()) { + return { + .success = false, + .message = "No writable mods directory is configured", + }; + } + std::error_code error; + const auto userDir = fs::weakly_canonical(m_searchDirs.front().path, error); + if (error) { + return { + .success = false, + .message = + fmt::format("Could not resolve the user mods directory: {}", error.message()), + }; + } + const auto stagingDir = userDir / ".staging"; + const auto path = fs::weakly_canonical(requestedPath, error); + const bool stagedName = path.extension() == ".part" && path.stem().extension() == ".dusk"; + if (error || path.parent_path() != stagingDir || !stagedName || + !fs::is_regular_file(path, error)) + { + Log.error("refusing staged install from {}", data::abbreviated_path_string(requestedPath)); + return { + .success = false, + .message = "The package is not in the mod staging directory", + }; + } + + ModMetadata metadata; + std::string validationError; + if (!inspect_mod_bundle(path, metadata, validationError)) { + return { + .success = false, + .message = fmt::format("Invalid mod package: {}", validationError), + }; + } + + const auto destination = userDir / fmt::format("{}.dusk", safe_filename(metadata.id)); + auto* installed = find_mod(metadata.id); + if (installed != nullptr && !can_update(*installed)) { + return { + .success = false, + .message = "Cannot install mod over a development directory", + }; + } + + for (const auto& mod : mods()) { + if (mod.metadata.id != metadata.id && fs::equivalent(mod.modPath, destination, error)) { + return { + .success = false, + .message = "The destination filename belongs to a different mod", + }; + } + } + error.clear(); + + if (!borealis::update::parse_version(metadata.version)) { + return {.success = false, .message = "The package version is invalid"}; + } + std::vector packages; + try { + packages = scan_packages(m_searchDirs); + } catch (const std::exception& exception) { + return {.success = false, .message = exception.what()}; + } + if (const auto* selected = select_package(packages, metadata.id); + selected && compare_package_versions(metadata.version, selected->metadata.version) < 0) + { + return { + .success = false, + .message = fmt::format( + "A newer version ({}) is already installed", selected->metadata.version), + }; + } + + const auto packageResult = install_package(path, destination, metadata.id); + if (!packageResult.replaced) { + return {.success = false, .message = packageResult.error}; + } + std::erase_if(packages, [&](const auto& package) { + if (package.metadata.id != metadata.id || package.searchDirIndex != 0 || + package.fromDirectory) + { + return false; + } + std::error_code statusError; + return fs::equivalent(package.path, destination, statusError) || + !fs::exists(package.path, statusError); + }); + packages.push_back({.path = destination, .metadata = metadata}); + const auto* selected = select_package(packages, metadata.id); + auto result = installed != nullptr ? reload_runtime_mod(*installed, selected) : + load_runtime_mod(selected->path); + if (result.mod != nullptr) { + record_package_sources(*result.mod, packages); + } + + if (result.success && !packageResult.error.empty()) { + result.success = false; + result.message = packageResult.error; + } + return result; } void ModLoader::apply_pending_requests() { @@ -1452,16 +1092,90 @@ void ModLoader::apply_pending_requests() { return; } - // Coalesce per mod, last request wins. Failures during apply re-enqueue for next tick. const auto requests = std::exchange(m_pendingRequests, {}); - std::vector coalesced; + std::vector coalesced; for (const auto& request : requests) { - const auto existing = std::ranges::find_if( - coalesced, [&](const Request& r) { return r.modId == request.modId; }); + if (const auto* install = std::get_if(&request)) { + auto result = install_staged(install->stagedPath); + if (result.success && result.mod != nullptr) { + const auto& metadata = result.mod->metadata; + const std::string iconRml = + metadata.iconPath.empty() ? + std::string{} : + fmt::format(R"()", + ui::escape(ui::mod_image_source(*result.mod, metadata.iconPath))); + ui::push_toast({ + .type = "mod-installed", + .title = "Mod installed", + .content = fmt::format( + R"({}{}v{}{})", + iconRml, ui::escape(metadata.name), ui::escape(metadata.version), + ui::escape(metadata.author)), + .duration = std::chrono::seconds{4}, + }); + } else if (!result.success && result.mod == nullptr) { + ui::push_toast({ + .type = "warning", + .title = "Mod install failed", + .content = + result.message.empty() ? "The loader rejected the package" : result.message, + .duration = std::chrono::seconds{6}, + }); + } + if (!result.success && !m_searchDirs.empty()) { + // request_install owns only files under the configured staging directory. + std::error_code error; + const auto userDir = fs::weakly_canonical(m_searchDirs.front().path, error); + if (!error) { + const auto stagedPath = fs::weakly_canonical(install->stagedPath, error); + if (!error && stagedPath.parent_path() == userDir / ".staging") { + fs::remove(stagedPath, error); + } + } + } + complete_operation(install->operation, result.success, std::move(result.message)); + continue; + } + if (const auto* reload = std::get_if(&request)) { + auto* mod = find_mod(reload->modId); + if (mod == nullptr) { + complete_operation(reload->operation, false, "The mod is no longer installed"); + continue; + } + auto result = reload_runtime_mod(*mod); + complete_operation(reload->operation, result.success, std::move(result.message)); + continue; + } + if (const auto* uninstall = std::get_if(&request)) { + auto* mod = find_mod(uninstall->modId); + if (mod == nullptr) { + complete_operation(uninstall->operation); + continue; + } + const auto removedName = mod->metadata.name; + const auto removedId = mod->metadata.id; + auto result = uninstall_runtime_mod(*mod); + if (result.success) { + queue::remove_by_mod_id(removedId); + ui::push_toast({ + .title = result.mod != nullptr ? "User update removed" : "Mod uninstalled", + .content = removedName, + .duration = std::chrono::seconds{2}, + }); + } + complete_operation(uninstall->operation, result.success, std::move(result.message)); + continue; + } + + const auto& lifecycle = std::get(request); + const auto existing = + std::ranges::find(coalesced, lifecycle.modId, &LifecycleRequest::modId); if (existing != coalesced.end()) { - existing->kind = request.kind; + complete_operation( + existing->operation, false, "Superseded by a newer lifecycle request"); + *existing = lifecycle; } else { - coalesced.push_back(request); + coalesced.push_back(lifecycle); } } @@ -1469,19 +1183,36 @@ void ModLoader::apply_pending_requests() { auto* mod = find_mod(request.modId); if (mod == nullptr) { Log.warn("lifecycle request for unknown mod '{}'", request.modId); + complete_operation(request.operation, false, "The mod is no longer installed"); continue; } - if (request.kind == RequestKind::Reload && mod->inPlace) { - log::write(mod->metadata.id, LOG_LEVEL_WARN, "is a built-in mod and can't be reloaded"); + if (request.action == LifecycleAction::Enable && mod->enabledApplied) { continue; } - if (request.kind == RequestKind::Enable && mod->enabledApplied) { + if (request.action == LifecycleAction::Disable && !mod->enabledApplied && !mod->active) { continue; } - if (request.kind == RequestKind::Disable && !mod->enabledApplied && !mod->active) { - continue; + + if (request.action == LifecycleAction::Reactivate) { + mod->loadFailed = false; + mod->failureReason.clear(); + mod->suspendedByProvider = false; + if (!mod->cvarIsEnabled->getValue()) { + mod->cvarIsEnabled->setValue(true); + } + } + apply_lifecycle_change(*mod, false); + + if (request.action == LifecycleAction::Reactivate) { + std::string error; + if (mod->loadFailed) { + error = + mod->failureReason.empty() ? "The mod failed to activate" : mod->failureReason; + } else if (mod->cvarIsEnabled->getValue() && !mod->active) { + error = "A required provider is unavailable"; + } + complete_operation(request.operation, error.empty(), std::move(error)); } - apply_lifecycle_change(*mod, request.kind == RequestKind::Reload); } svc::modules_lifecycle_applied(); diff --git a/src/dusk/mods/loader/loader.hpp b/src/dusk/mods/loader/loader.hpp index 6b9fd39232..d5708c9be1 100644 --- a/src/dusk/mods/loader/loader.hpp +++ b/src/dusk/mods/loader/loader.hpp @@ -1,10 +1,9 @@ #pragma once #include -#include #include -#include "miniz.h" +#include "dusk/archive.hpp" #include "dusk/mod_loader.hpp" namespace dusk::mods { @@ -28,17 +27,14 @@ public: class ModBundleZip final : public ModBundle { public: - explicit ModBundleZip(std::vector&& data); - ~ModBundleZip() override; + explicit ModBundleZip(const std::filesystem::path& path); + ~ModBundleZip() override = default; std::vector readFile(const std::string& fileName) override; std::vector getFileNames() override; size_t getFileSize(const std::string& fileName) override; private: - std::vector zip_data; - mz_zip_archive res_zip{}; - bool res_zip_open = false; - std::mutex m_mutex; + archive::ZipArchive m_archive; }; class ModBundleDisk final : public ModBundle { diff --git a/src/dusk/mods/loader/manifest.cpp b/src/dusk/mods/loader/manifest.cpp new file mode 100644 index 0000000000..8c8243bd0a --- /dev/null +++ b/src/dusk/mods/loader/manifest.cpp @@ -0,0 +1,203 @@ +#include "manifest.hpp" + +#include "loader.hpp" +#include "natives.hpp" +#include "packages.hpp" + +#include "dusk/mods/log_buffer.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +using namespace std::string_literals; +namespace fs = std::filesystem; + +namespace dusk::mods { +namespace { + +class InvalidModDataException : public std::runtime_error { +public: + explicit InvalidModDataException(const std::string& msg) : runtime_error(msg) {} + explicit InvalidModDataException(const char* msg) : runtime_error(msg) {} +}; + +void validate_mod_id(std::string_view const str) { + if (str.empty()) { + throw InvalidModDataException("Missing ID value in mod metadata"); + } + + bool lastWasPeriod = false; + for (auto const chr : str) { + if (chr == '.') { + if (lastWasPeriod) { + throw InvalidModDataException("Cannot have two consecutive periods in mod ID"); + } + lastWasPeriod = true; + continue; + } + + lastWasPeriod = false; + + if (chr == '_') + continue; + + if (chr >= '0' && chr <= '9') + continue; + + if (chr >= 'a' && chr <= 'z') + continue; + + if (chr >= 'A' && chr <= 'Z') + continue; + + throw InvalidModDataException( + fmt::format("Invalid character '{}' in mod ID. Valid characters are period, " + "underscore, and alphanumerics.", + chr)); + } +} + +bool bundle_has_file(ModBundle& bundle, const std::string& path) { + try { + bundle.getFileSize(path); + return true; + } catch (const std::runtime_error&) { + return false; + } +} + +std::string resolve_image_path(ModBundle& bundle, const std::string& modId, std::string_view key, + const std::string& manifestPath, const std::string& defaultPath) { + if (!manifestPath.empty()) { + if (!is_safe_resource_path(manifestPath)) { + log::write( + modId, LOG_LEVEL_WARN, "invalid {} path '{}' in mod.json", key, manifestPath); + } else if (!bundle_has_file(bundle, manifestPath)) { + log::write( + modId, LOG_LEVEL_WARN, "{} path '{}' not found in bundle", key, manifestPath); + } else { + return manifestPath; + } + } + if (bundle_has_file(bundle, defaultPath)) { + return defaultPath; + } + return {}; +} + +uint16_t parse_runtime_version_component(std::string_view text, std::string_view fieldName) { + uint32_t value = 0; + const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value); + if (text.empty() || error != std::errc{} || end != text.data() + text.size() || + value > UINT16_MAX) + { + throw InvalidModDataException(fmt::format("Invalid {} in runtime version pin", fieldName)); + } + return static_cast(value); +} + +std::optional parse_runtime(const nlohmann::json& manifest) { + const auto field = manifest.find("runtime"); + if (field == manifest.end()) { + return std::nullopt; + } + if (!field->is_string()) { + throw InvalidModDataException("runtime must be a string"); + } + + const std::string pin = field->get(); + const auto at = pin.rfind('@'); + if (at == std::string::npos || at == 0 || at + 1 == pin.size() || pin.find('@') != at || + at >= MOD_META_SERVICE_ID_SIZE) + { + throw InvalidModDataException( + "runtime must be a service id followed by @major or @major.minor"); + } + + const std::string_view version{pin.data() + at + 1, pin.size() - at - 1}; + const auto dot = version.find('.'); + if (dot != std::string_view::npos && version.find('.', dot + 1) != std::string_view::npos) { + throw InvalidModDataException("runtime version pin has too many components"); + } + + DelegatedModRuntime result; + result.id = pin.substr(0, at); + result.major = parse_runtime_version_component( + dot == std::string_view::npos ? version : version.substr(0, dot), "major version"); + if (dot != std::string_view::npos) { + result.minMinor = parse_runtime_version_component(version.substr(dot + 1), "minor version"); + } + return result; +} + +} // namespace + +LoadedManifest load_manifest(const std::filesystem::path& modPath, ModBundle& bundle) { + const auto metaJson = bundle.readFile("mod.json"); + auto j = nlohmann::json::parse(metaJson); + + std::string metaId = j.value("id", ""); + std::string metaName = j.value("name", ""); + std::string metaVersion = j.value("version", ""); + std::string metaAuthor = j.value("author", ""); + std::string metaDescription = j.value("description", ""); + std::string metaIcon = j.value("icon", ""); + std::string metaBanner = j.value("banner", ""); + + validate_mod_id(metaId); + + if (metaName.empty()) { + metaName = borealis::io::fs_path_to_string(modPath.stem()); + } + if (metaVersion.empty()) { + metaVersion = "?"s; + } + if (metaAuthor.empty()) { + metaAuthor = "unknown"s; + } + + std::string iconPath = resolve_image_path(bundle, metaId, "icon", metaIcon, "res/icon.png"s); + std::string bannerPath = + resolve_image_path(bundle, metaId, "banner", metaBanner, "res/banner.png"s); + + return LoadedManifest{ + .metadata = + { + std::move(metaId), + std::move(metaName), + std::move(metaVersion), + std::move(metaAuthor), + std::move(metaDescription), + std::move(iconPath), + std::move(bannerPath), + }, + .runtime = parse_runtime(j), + }; +} + +bool inspect_mod_bundle( + const fs::path& path, ModMetadata& metadata, std::string& error, bool* hasNative) noexcept { + try { + auto bundle = load_bundle(path, false); + metadata = load_manifest(path, *bundle).metadata; + if (hasNative != nullptr) { + *hasNative = std::ranges::any_of(bundle->getFileNames(), + [](const auto& name) { return has_native_library_extension(name); }); + } + error.clear(); + return true; + } catch (const std::exception& exception) { + error = exception.what(); + } catch (...) { + error = "Unknown bundle validation error"; + } + return false; +} + +} // namespace dusk::mods diff --git a/src/dusk/mods/loader/manifest.hpp b/src/dusk/mods/loader/manifest.hpp new file mode 100644 index 0000000000..e3eea70298 --- /dev/null +++ b/src/dusk/mods/loader/manifest.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "dusk/mod_loader.hpp" + +namespace dusk::mods { + +struct LoadedManifest { + ModMetadata metadata; + std::optional runtime; +}; + +LoadedManifest load_manifest(const std::filesystem::path& modPath, ModBundle& bundle); + +} // namespace dusk::mods diff --git a/src/dusk/mods/loader/natives.cpp b/src/dusk/mods/loader/natives.cpp new file mode 100644 index 0000000000..e3802f4b52 --- /dev/null +++ b/src/dusk/mods/loader/natives.cpp @@ -0,0 +1,620 @@ +#include "natives.hpp" + +#include "loader.hpp" +#include "native_module.hpp" + +#include "dusk/data.hpp" +#include "dusk/mods/log_buffer.hpp" +#include "dusk/mods/svc/registry.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace std::string_view_literals; +namespace fs = std::filesystem; + +#if defined(_WIN32) +#if defined(_M_ARM64) +static constexpr std::string_view k_nativePlatform = "windows-arm64"sv; +#elif defined(_M_X64) +static constexpr std::string_view k_nativePlatform = "windows-amd64"sv; +#elif defined(_M_IX86) +static constexpr std::string_view k_nativePlatform = "windows-x86"sv; +#else +static constexpr std::string_view k_nativePlatform = ""sv; +#endif +static constexpr std::string_view k_nativeLibName = "mod.dll"sv; +#elif defined(__ANDROID__) +#if defined(__aarch64__) +static constexpr std::string_view k_nativePlatform = "android-aarch64"sv; +#elif defined(__x86_64__) +static constexpr std::string_view k_nativePlatform = "android-x86_64"sv; +#else +static constexpr std::string_view k_nativePlatform = ""sv; +#endif +static constexpr std::string_view k_nativeLibName = "mod.so"sv; +#elif defined(__APPLE__) +#include +#if TARGET_OS_IOS +static constexpr std::string_view k_nativePlatform = "ios-arm64"sv; +#elif TARGET_OS_TV +static constexpr std::string_view k_nativePlatform = "tvos-arm64"sv; +#elif defined(__aarch64__) +static constexpr std::string_view k_nativePlatform = "macos-arm64"sv; +#elif defined(__x86_64__) +static constexpr std::string_view k_nativePlatform = "macos-x86_64"sv; +#else +static constexpr std::string_view k_nativePlatform = ""sv; +#endif +static constexpr std::string_view k_nativeLibName = "mod.so"sv; +#elif defined(__linux__) +#if defined(__aarch64__) +static constexpr std::string_view k_nativePlatform = "linux-aarch64"sv; +#elif defined(__x86_64__) +static constexpr std::string_view k_nativePlatform = "linux-x86_64"sv; +#elif defined(__i386__) +static constexpr std::string_view k_nativePlatform = "linux-x86"sv; +#else +static constexpr std::string_view k_nativePlatform = ""sv; +#endif +static constexpr std::string_view k_nativeLibName = "mod.so"sv; +#else +static constexpr std::string_view k_nativePlatform = ""sv; +static constexpr std::string_view k_nativeLibName = ""sv; +#endif + +namespace dusk::mods { + +bool has_native_library_extension(std::string_view name) { + const auto endsWith = [name](std::string_view extension) { + if (name.size() < extension.size()) { + return false; + } + const auto suffix = name.substr(name.size() - extension.size()); + return std::ranges::equal(suffix, extension, [](char lhs, char rhs) { + const auto lower = [](char value) { + return value >= 'A' && value <= 'Z' ? static_cast(value + ('a' - 'A')) : + value; + }; + return lower(lhs) == lower(rhs); + }); + }; + return endsWith(".dll"sv) || endsWith(".so"sv) || endsWith(".dylib"sv); +} + +namespace { + +constexpr std::string_view k_nativeLibDir = "lib/"sv; + +class DirectoryRollback { +public: + ~DirectoryRollback() { + if (!mPath.empty()) { + std::error_code ec; + fs::remove_all(mPath, ec); + } + } + + void set_path(fs::path path) { mPath = std::move(path); } + void release() { mPath.clear(); } + +private: + fs::path mPath; +}; + +struct NativeRuntimeLocation { + std::string entry; + std::vector runtimeEntries; + bool anyLibs = false; +}; + +struct NativeLocateFailure { + NativeModStatus status; + std::string logMessage; +}; + +using NativeLocateResult = std::variant; + +NativeLocateResult locate_native_runtime(ModBundle& bundle) { + NativeRuntimeLocation result; + const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform); + const std::string nativeEntry = platformPrefix + std::string{k_nativeLibName}; + for (const auto& name : bundle.getFileNames()) { + if (name.find('/') == std::string::npos && has_native_library_extension(name)) { + return NativeLocateFailure{ + NativeModStatus::InvalidBundle, + fmt::format( + "native library '{}' found at the root (natives go in /lib/{{platform}})", + name), + }; + } + if (!name.starts_with(k_nativeLibDir)) { + continue; + } + + const std::string_view libPath{ + name.data() + k_nativeLibDir.size(), name.size() - k_nativeLibDir.size()}; + const auto platformEnd = libPath.find('/'); + if (platformEnd != std::string_view::npos) { + const auto entryName = libPath.substr(platformEnd + 1); + if (entryName.find('/') == std::string_view::npos && + (entryName == "mod.dll"sv || entryName == "mod.so"sv)) + { + result.anyLibs = true; + } + } + + if (!k_nativePlatform.empty() && name.starts_with(platformPrefix)) { + const std::string_view relativeName{ + name.data() + platformPrefix.size(), name.size() - platformPrefix.size()}; + if (!is_safe_resource_path(relativeName)) { + continue; + } + result.runtimeEntries.push_back(name); + } + if (name == nativeEntry) { + result.entry = name; + } + } + std::ranges::sort(result.runtimeEntries); + result.runtimeEntries.erase( + std::unique(result.runtimeEntries.begin(), result.runtimeEntries.end()), + result.runtimeEntries.end()); + return result; +} + +// True if the first `capacity` bytes of `str` contain a NUL. +bool terminated_within(const char* str, size_t capacity) { + return std::memchr(str, '\0', capacity) != nullptr; +} + +bool parse_meta(NativeMod& native, LoadedMod& mod) { + const ModMeta* meta = native.meta; + if (meta->struct_size < sizeof(ModMeta)) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta descriptor has invalid size {}", + meta->struct_size); + mod.nativeStatus = NativeModStatus::InvalidMetadata; + return false; + } + const auto* cursor = static_cast(meta->records_begin); + const auto* end = static_cast(meta->records_end); + if (cursor == nullptr || end == nullptr || cursor > end || + (reinterpret_cast(cursor) & 7) != 0) + { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta section bounds are invalid"); + mod.nativeStatus = NativeModStatus::InvalidMetadata; + return false; + } + + ModMetaParsed parsed; + size_t headerCount = 0; + const auto invalid = [&](std::string_view why) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "invalid metadata record at offset {}: {}", + cursor - static_cast(meta->records_begin), why); + mod.nativeStatus = NativeModStatus::InvalidMetadata; + return false; + }; + + while (cursor < end) { + if (end - cursor < 8) { + return invalid("trailing bytes"); + } + uint64_t first = 0; + std::memcpy(&first, cursor, sizeof(first)); + if (first == 0) { // linker padding / bounds sentinel + cursor += 8; + continue; + } + + const auto* rec = reinterpret_cast(cursor); + const size_t size = rec->size; + if (size < 8 || size % 8 != 0 || size > static_cast(end - cursor)) { + return invalid("bad record size"); + } + + switch (rec->kind) { + case MOD_META_PAD: + break; + case MOD_META_HEADER: { + if (size < sizeof(ModMetaHeader)) { + return invalid("truncated header record"); + } + const auto* header = reinterpret_cast(rec); + ++headerCount; + parsed.abiVersion = header->abi_version; + break; + } + case MOD_META_IMPORT: { + if (size < sizeof(ModMetaImport)) { + return invalid("truncated import record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) { + return invalid("unterminated import service id"); + } + parsed.imports.push_back(record); + break; + } + case MOD_META_EXPORT: { + if (size < sizeof(ModMetaExport)) { + return invalid("truncated export record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) { + return invalid("unterminated export service id"); + } + parsed.exports.push_back(record); + break; + } + case MOD_META_HOOK_FN: { + if (size < sizeof(ModMetaHookFn)) { + return invalid("truncated hook record"); + } + parsed.hookFns.push_back( + reinterpret_cast(const_cast(cursor))); + break; + } + case MOD_META_HOOK_MEM: { + if (size <= sizeof(ModMetaHookMem)) { + return invalid("truncated hook record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + const char* strings = reinterpret_cast(cursor) + sizeof(ModMetaHookMem); + const size_t capacity = size - sizeof(ModMetaHookMem); + if (!terminated_within(strings, capacity)) { + return invalid("unterminated hook vtable symbol"); + } + const size_t vtableLen = std::char_traits::length(strings); + if (!terminated_within(strings + vtableLen + 1, capacity - vtableLen - 1)) { + return invalid("unterminated hook display name"); + } + parsed.hookMems.push_back(record); + break; + } + case MOD_META_HOOK_MEM_EXT: { + if (size <= sizeof(ModMetaHookMemExt)) { + return invalid("truncated extended hook record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + if (record->pmf_size <= MOD_META_HOOK_MEM_CAPACITY || + record->pmf_size > MOD_META_HOOK_MEM_EXT_CAPACITY || record->materialize == nullptr) + { + return invalid("bad extended hook member-pointer size"); + } + const char* strings = reinterpret_cast(cursor) + sizeof(ModMetaHookMemExt); + const size_t capacity = size - sizeof(ModMetaHookMemExt); + if (!terminated_within(strings, capacity)) { + return invalid("unterminated extended hook vtable symbol"); + } + const size_t vtableLen = std::char_traits::length(strings); + if (!terminated_within(strings + vtableLen + 1, capacity - vtableLen - 1)) { + return invalid("unterminated extended hook display name"); + } + parsed.hookMemExts.push_back(record); + break; + } + case MOD_META_HOOK_NAME: { + if (size <= sizeof(ModMetaHookName)) { + return invalid("truncated hook record"); + } + auto* record = reinterpret_cast(const_cast(cursor)); + const char* name = reinterpret_cast(cursor) + sizeof(ModMetaHookName); + if (!terminated_within(name, size - sizeof(ModMetaHookName))) { + return invalid("unterminated hook symbol name"); + } + parsed.hookNames.push_back(record); + break; + } + default: + // Additive record kinds may appear within a format version; skip them. + log::write(mod.metadata.id, LOG_LEVEL_DEBUG, "skipping unknown metadata record kind {}", + rec->kind); + break; + } + cursor += size; + } + + if (headerCount != 1) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expected 1 metadata header record, found {}", + headerCount); + mod.nativeStatus = NativeModStatus::InvalidMetadata; + return false; + } + if (parsed.abiVersion != MOD_ABI_VERSION) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expects ABI v{} but engine is v{}, skipping", + parsed.abiVersion, MOD_ABI_VERSION); + mod.nativeStatus = NativeModStatus::ApiVersionMismatch; + return false; + } + + native.parsed = std::move(parsed); + return true; +} + +std::string native_status_message(const NativeModStatus status) { + switch (status) { + case NativeModStatus::BuildDisabled: + return "Code mods are disabled on this Dusklight build"; + case NativeModStatus::ModMissingPlatform: + return fmt::format("Mod not supported on this platform ({})", k_nativePlatform); + case NativeModStatus::ApiVersionMismatch: + // TODO: differentiate whether mod or Dusklight is out of date + return "Mod ABI version mismatch"; + case NativeModStatus::MissingExport: + return "Missing required mod API exports"; + case NativeModStatus::InvalidMetadata: + return "Invalid mod metadata records"; + case NativeModStatus::InvalidBundle: + return "Invalid mod bundle layout (old mod?)"; + case NativeModStatus::Unknown: + return "Unknown mod load failure"; + case NativeModStatus::None: + case NativeModStatus::Loaded: + break; + } + return "native mod failed to load"; +} + +} // namespace + +fs::path ModLoader::external_native_lib_path(const LoadedMod& mod) const { + if (k_nativeLibName.empty()) { + return {}; + } + const auto& libDir = m_searchDirs[mod.searchDirIndex].nativeLibDir; + if (libDir.empty()) { + return {}; + } + const auto filename = fmt::format("{}{}", mod.metadata.id, + borealis::io::fs_path_to_string(fs::path{k_nativeLibName}.extension())); + fs::path path = libDir / fs::path{filename}; + std::error_code ec; + if (!fs::is_regular_file(path, ec)) { + return {}; + } + return path; +} + +void ModLoader::load_native( + LoadedMod& mod, const std::string& dllEntry, const std::vector& runtimeEntries) { + if (!EnableCodeMods) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "Code mods are not available in this build"); + mod.nativeStatus = NativeModStatus::BuildDisabled; + return; + } + + const fs::path cacheDir = m_cacheDir / mod.metadata.id; + const fs::path scratchDir = cacheDir / "data"; + std::error_code ec; + fs::create_directories(scratchDir, ec); + if (ec) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to create mod directory {}: {}", + data::abbreviated_path_string(scratchDir), ec.message()); + return; + } + mod.dir = fs::absolute(scratchDir); + mod.dirUtf8 = borealis::io::fs_path_to_string(mod.dir); + + fs::path libPath; + fs::path runtimeDir; + DirectoryRollback runtimeDirRollback; + if (mod.nativeInPlace) { + if (!dllEntry.empty()) { + libPath = mod.modPath / dllEntry; + } else if (auto external = external_native_lib_path(mod); !external.empty()) { + libPath = std::move(external); + } else { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "no native library named {} found; skipping", k_nativeLibName); + mod.nativeStatus = NativeModStatus::ModMissingPlatform; + return; + } + runtimeDir = libPath.parent_path(); + } else { + if (dllEntry.empty()) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "no native library named {} found; skipping", k_nativeLibName); + mod.nativeStatus = NativeModStatus::ModMissingPlatform; + return; + } + + // Every generation gets a new directory. The main module and all of its runtime + // libraries therefore have fresh paths and can coexist with a previous generation + // that is still unwinding after a reload. + runtimeDir = cacheDir / fmt::format("g{}", ++mod.cacheGeneration); + runtimeDirRollback.set_path(runtimeDir); + fs::create_directories(runtimeDir, ec); + if (ec) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "failed to create native runtime directory {}: {}", + data::abbreviated_path_string(runtimeDir), ec.message()); + return; + } + + const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform); + for (const auto& entry : runtimeEntries) { + if (!entry.starts_with(platformPrefix)) { + continue; + } + const std::string_view relativeName{ + entry.data() + platformPrefix.size(), entry.size() - platformPrefix.size()}; + if (!is_safe_resource_path(relativeName)) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "unsafe native runtime path '{}'; skipping", entry); + return; + } + + const fs::path outputPath = runtimeDir / fs::path{relativeName}; + fs::create_directories(outputPath.parent_path(), ec); + if (ec) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "failed to create directory for {}: {}", entry, ec.message()); + return; + } + + std::vector data; + try { + data = mod.bundle->readFile(entry); + } catch (const std::exception& e) { + log::write( + mod.metadata.id, LOG_LEVEL_ERROR, "failed to extract {}: {}", entry, e.what()); + return; + } + + std::ofstream out(outputPath, std::ios::binary | std::ios::out); + if (!out) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry); + return; + } + out.write(reinterpret_cast(data.data()), + static_cast(data.size())); + if (!out) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry); + return; + } + } + + libPath = runtimeDir / fs::path{dllEntry}.filename(); + } + + auto nativeMod = std::make_unique(); + try { + nativeMod->handle = std::make_unique(libPath); + } catch (const std::runtime_error& e) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to open {}: {}", + data::abbreviated_path_string(libPath), e.what()); + return; + } + + nativeMod->meta = nativeMod->handle->LookupSymbol("mod_meta"); + nativeMod->contextSymbol = nativeMod->handle->LookupSymbol("mod_ctx"); + nativeMod->fn_initialize = nativeMod->handle->LookupSymbol("mod_initialize"); + nativeMod->fn_update = nativeMod->handle->LookupSymbol("mod_update"); + nativeMod->fn_shutdown = nativeMod->handle->LookupSymbol("mod_shutdown"); + + if (!nativeMod->meta || !nativeMod->contextSymbol || !nativeMod->fn_initialize || + !nativeMod->fn_update || !nativeMod->fn_shutdown) + { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, + "{} missing required mod API exports; skipping", + data::abbreviated_path_string(libPath)); + mod.nativeStatus = NativeModStatus::MissingExport; + return; + } + + if (!parse_meta(*nativeMod, mod)) { + return; + } + + if (nativeMod->contextSymbol == nullptr) { + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "missing required mod_ctx export"); + mod.nativeStatus = NativeModStatus::MissingExport; + return; + } + *nativeMod->contextSymbol = mod.context.get(); + + mod.nativePath = fs::absolute(libPath); + mod.nativeDir = fs::absolute(runtimeDir); + mod.nativeDirUtf8 = borealis::io::fs_path_to_string(mod.nativeDir); + mod.native = std::move(nativeMod); + mod.nativeStatus = NativeModStatus::Loaded; + runtimeDirRollback.release(); +} + +bool ModLoader::load_native_if_present(LoadedMod& mod) { + const auto result = locate_native_runtime(*mod.bundle); + if (const auto* failure = std::get_if(&result)) { + mod.nativeStatus = failure->status; + log::write(mod.metadata.id, LOG_LEVEL_ERROR, "{}", failure->logMessage); + fail_mod(mod, MOD_ERROR, native_status_message(failure->status)); + return false; + } + + const auto& native = std::get(result); + if (mod.runtime.has_value() && + (native.anyLibs || (mod.nativeInPlace && !external_native_lib_path(mod).empty()))) + { + mod.nativeStatus = NativeModStatus::InvalidBundle; + fail_mod(mod, MOD_CONFLICT, "A mod cannot declare both runtime and native code"); + return false; + } + if (!native.anyLibs && !(mod.nativeInPlace && !external_native_lib_path(mod).empty())) { + mod.nativeStatus = NativeModStatus::None; + return true; + } + + mod.nativeStatus = NativeModStatus::Unknown; + load_native(mod, native.entry, native.runtimeEntries); + if (mod.nativeStatus != NativeModStatus::Loaded) { + fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus)); + return false; + } + return true; +} + +void ModLoader::unload_native(LoadedMod& mod) { + if (!mod.native) { + return; + } + // Deferred dlclose: this mod's code may still be on the stack below the current tick + m_retiredNatives.push_back( + {std::move(mod.native), mod.nativeInPlace ? fs::path{} : std::move(mod.nativeDir)}); + mod.nativePath.clear(); + mod.nativeDir.clear(); + mod.nativeDirUtf8.clear(); +} + +void ModLoader::drain_retired_natives() { + for (auto& retired : m_retiredNatives) { + retired.native.reset(); + if (!retired.directory.empty()) { + std::error_code ec; + fs::remove_all(retired.directory, ec); + } + } + m_retiredNatives.clear(); +} + +bool ModLoader::ensure_native_loaded(LoadedMod& mod) { + if (mod.native || mod.nativeStatus == NativeModStatus::None) { + return true; + } + return load_native_if_present(mod); +} + +ModManifestInfo build_manifest_info(const ModMetaParsed& parsed) { + ModManifestInfo info; + info.imports.reserve(parsed.imports.size()); + for (const auto* record : parsed.imports) { + if (!svc::valid_service_id(record->service_id.chars)) { + continue; + } + info.imports.push_back({ + .id = record->service_id.chars, + .major = record->major_version, + .minMinor = record->min_minor_version, + .required = (record->rec.flags & SERVICE_IMPORT_OPTIONAL) == 0, + }); + } + info.exports.reserve(parsed.exports.size()); + for (const auto* record : parsed.exports) { + if (!svc::valid_service_id(record->service_id.chars)) { + continue; + } + info.exports.push_back({ + .id = record->service_id.chars, + .major = record->major_version, + }); + } + return info; +} + +} // namespace dusk::mods diff --git a/src/dusk/mods/loader/natives.hpp b/src/dusk/mods/loader/natives.hpp new file mode 100644 index 0000000000..657f3d011a --- /dev/null +++ b/src/dusk/mods/loader/natives.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace dusk::mods { + +struct ModManifestInfo; +struct ModMetaParsed; + +bool has_native_library_extension(std::string_view name); +ModManifestInfo build_manifest_info(const ModMetaParsed& parsed); + +} // namespace dusk::mods diff --git a/src/dusk/mods/loader/packages.cpp b/src/dusk/mods/loader/packages.cpp new file mode 100644 index 0000000000..071f5b276e --- /dev/null +++ b/src/dusk/mods/loader/packages.cpp @@ -0,0 +1,201 @@ +#include "packages.hpp" + +#include "loader.hpp" +#include "manifest.hpp" + +#include "dusk/data.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +namespace fs = std::filesystem; + +namespace dusk::mods { +namespace { + +constexpr borealis::Log Log{"dusk::mods::loader"}; + +} // namespace + +std::unique_ptr load_bundle(const fs::path& modPath, bool fromDir) { + if (fromDir) { + return std::make_unique(modPath); + } else { + return std::make_unique(modPath); + } +} + +int compare_package_versions(std::string_view lhs, std::string_view rhs) { + const auto left = borealis::update::parse_version(lhs); + const auto right = borealis::update::parse_version(rhs); + if (left && right) { + return borealis::update::compare_version(*left, *right); + } + return static_cast(left.has_value()) - static_cast(right.has_value()); +} + +std::vector scan_packages(std::span searchDirs) { + std::vector packages; + for (size_t dirIndex = 0; dirIndex < searchDirs.size(); ++dirIndex) { + const auto& searchDir = searchDirs[dirIndex]; + std::error_code error; + bool alreadyScanned = false; + for (size_t earlier = 0; earlier < dirIndex && !alreadyScanned; ++earlier) { + alreadyScanned = fs::equivalent(searchDirs[earlier].path, searchDir.path, error); + } + if (alreadyScanned || !fs::is_directory(searchDir.path)) { + continue; + } + std::vector entries; + for (const auto& entry : fs::directory_iterator{searchDir.path}) { + if ((entry.is_directory() && fs::exists(entry.path() / "mod.json")) || + (entry.is_regular_file() && entry.path().extension() == ".dusk")) + { + entries.push_back(entry); + } + } + std::ranges::sort(entries, {}, &fs::directory_entry::path); + for (const auto& entry : entries) { + try { + const bool fromDirectory = entry.is_directory(); + auto bundle = load_bundle(entry.path(), fromDirectory); + packages.push_back({ + .path = fs::absolute(entry.path()), + .metadata = load_manifest(entry.path(), *bundle).metadata, + .searchDirIndex = static_cast(dirIndex), + .fromDirectory = fromDirectory, + .symlink = entry.is_symlink(), + }); + } catch (const std::exception& exception) { + Log.error("bad mod package {}: {}", + data::abbreviated_path_string(entry.path()), exception.what()); + } + } + } + return packages; +} + +const PackageCandidate* select_package(std::span packages, + std::string_view modId) { + const PackageCandidate* selected = nullptr; + for (const auto& package : packages) { + if (package.metadata.id != modId) { + continue; + } + const int order = selected ? + compare_package_versions(package.metadata.version, selected->metadata.version) : 1; + if (order > 0 || (order == 0 && + (package.searchDirIndex < selected->searchDirIndex || + (package.searchDirIndex == selected->searchDirIndex && package.path < selected->path)))) + { + selected = &package; + } + } + return selected; +} + +void record_package_sources(LoadedMod& mod, std::span packages) { + mod.hasUserPackage = false; + mod.hasBundledCopy = false; + for (const auto& package : packages) { + if (package.metadata.id != mod.metadata.id) { + continue; + } + if (package.searchDirIndex != 0) { + mod.hasBundledCopy = true; + } else if (!package.fromDirectory && !package.symlink) { + mod.hasUserPackage = true; + } + } +} + +PackageInstallResult install_package(const fs::path& path, const fs::path& destination, + std::string_view modId) { + const auto userDir = destination.parent_path(); + std::error_code error; + std::string validationError; + const auto destinationStatus = fs::symlink_status(destination, error); + if (error == std::errc::no_such_file_or_directory) { + error.clear(); + } + if (error) { + return {.replaced = false, + .error = fmt::format("Could not inspect the destination: {}", error.message())}; + } + if (fs::exists(destinationStatus)) { + ModMetadata existingMetadata; + if (!fs::is_regular_file(destinationStatus) || + !inspect_mod_bundle(destination, existingMetadata, validationError) || + existingMetadata.id != modId) + { + return { + .replaced = false, + .error = "The destination filename is already used by another package", + }; + } + } + std::vector duplicates; + for (fs::directory_iterator entry{userDir, error}, end; !error && entry != end; + entry.increment(error)) + { + const auto& candidate = entry->path(); + if (candidate.extension() != ".dusk") { + continue; + } + const bool regularFile = entry->is_regular_file(error); + if (error) { + break; + } + if (!regularFile) { + continue; + } + ModMetadata candidateMetadata; + if (inspect_mod_bundle(candidate, candidateMetadata, validationError) && + candidateMetadata.id == modId) + { + duplicates.push_back(candidate); + } + } + if (error) { + return {.replaced = false, + .error = fmt::format("Could not scan the mods directory: {}", error.message())}; + } + + std::string replaceError; + if (!borealis::io::atomic_replace(path, destination, replaceError)) { + return { + .replaced = false, + .error = std::move(replaceError), + }; + } + + std::string cleanupError; + for (const auto& duplicate : duplicates) { + // Replacing a file on a case-insensitive filesystem can preserve its old spelling. + const bool destinationAlias = + fs::equivalent(duplicate, destination, error) && !fs::is_symlink(duplicate, error); + error.clear(); + if (destinationAlias) { + if (duplicate.filename() == destination.filename()) { + continue; + } + fs::rename(duplicate, destination, error); + } else { + fs::remove(duplicate, error); + } + if (error) { + cleanupError = fmt::format("Installed, but could not consolidate '{}': {}", + data::abbreviated_path_string(duplicate), error.message()); + Log.warn("{}", cleanupError); + } + } + return {.replaced = true, .error = std::move(cleanupError)}; +} + +} // namespace dusk::mods diff --git a/src/dusk/mods/loader/packages.hpp b/src/dusk/mods/loader/packages.hpp new file mode 100644 index 0000000000..0271dca5fe --- /dev/null +++ b/src/dusk/mods/loader/packages.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "dusk/mod_loader.hpp" + +#include +#include +#include +#include +#include +#include + +namespace dusk::mods { + +struct PackageCandidate { + std::filesystem::path path; + ModMetadata metadata; + uint32_t searchDirIndex = 0; + bool fromDirectory = false; + bool symlink = false; +}; + +std::vector scan_packages(std::span searchDirs); +int compare_package_versions(std::string_view lhs, std::string_view rhs); +const PackageCandidate* select_package( + std::span packages, std::string_view modId); +void record_package_sources(LoadedMod& mod, std::span packages); + +std::unique_ptr load_bundle(const std::filesystem::path& modPath, bool fromDir); + +struct PackageInstallResult { + bool replaced = false; + std::string error; +}; + +PackageInstallResult install_package(const std::filesystem::path& path, + const std::filesystem::path& destination, std::string_view modId); + +} // namespace dusk::mods diff --git a/src/dusk/mods/path.hpp b/src/dusk/mods/path.hpp new file mode 100644 index 0000000000..4213c3fbf6 --- /dev/null +++ b/src/dusk/mods/path.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +namespace dusk::mods { + +inline std::string safe_filename(std::string_view value) { + std::string result{value}; + std::ranges::replace_if( + result, + [](char character) { + return !((character >= 'a' && character <= 'z') || + (character >= 'A' && character <= 'Z') || + (character >= '0' && character <= '9') || character == '.' || + character == '_' || character == '-'); + }, + '_'); + return result; +} + +} // namespace dusk::mods diff --git a/src/dusk/mods/queue.cpp b/src/dusk/mods/queue.cpp new file mode 100644 index 0000000000..4be07cb2a3 --- /dev/null +++ b/src/dusk/mods/queue.cpp @@ -0,0 +1,769 @@ +#include "queue.hpp" + +#include "dusk/hash.hpp" +#include "dusk/mod_loader.hpp" +#include "dusk/mods/path.hpp" +#include "dusk/ui/ui.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace dusk::mods::queue { +namespace { + +using clock = std::chrono::steady_clock; + +struct VerifyResult { + std::string error; + ModMetadata metadata; + std::filesystem::path stagedPath; + bool canceled = false; +}; + +enum class PendingIntent { None, Pause, Cancel }; + +struct QueueItem { + std::string key; + Request request; + State state = State::Queued; + std::filesystem::path partialPath; + uint64_t completed = 0; + uint64_t total = 0; + std::string message; + int retryCount = 0; + clock::time_point retryAt{}; + borealis::Task task; + borealis::Task verification; + ModOperationHandle operation; + PendingIntent pendingIntent = PendingIntent::None; +}; + +std::vector queueItems; +uint64_t nextQueueKey = 1; + +QueueItem* find_queue_item(std::string_view key) { + const auto item = std::ranges::find(queueItems, key, + [](const QueueItem& candidate) { return std::string_view{candidate.key}; }); + return item == queueItems.end() ? nullptr : &*item; +} + +QueueItem* find_queue_item_by_mod_id(std::string_view id) { + const auto item = std::ranges::find(queueItems, id, + [](const QueueItem& candidate) { return std::string_view{candidate.request.id}; }); + return item == queueItems.end() ? nullptr : &*item; +} + +const Url* url_source(const QueueItem& item) { + return std::get_if(&item.request.source); +} + +const LocalFile* local_source(const QueueItem& item) { + return std::get_if(&item.request.source); +} + +std::string lowercase(std::string value) { + std::ranges::transform(value, value.begin(), [](char character) { + return character >= 'A' && character <= 'Z' ? static_cast(character + ('a' - 'A')) : + character; + }); + return value; +} + +bool valid_sha256(std::string_view value) { + return value.size() == 64 && std::ranges::all_of(value, [](char character) { + return (character >= '0' && character <= '9') || (character >= 'a' && character <= 'f') || + (character >= 'A' && character <= 'F'); + }); +} + +std::string sha256_file( + const std::filesystem::path& path, borealis::TaskContext& context, std::string& error) { + std::ifstream input{path, std::ios::binary}; + if (!input) { + error = "Could not open the downloaded package"; + return {}; + } + + hash::Sha256 hash; + std::array buffer{}; + uint64_t completed = 0; + while (input) { + if (context.cancel_requested()) { + error = "Canceled"; + return {}; + } + input.read(reinterpret_cast(buffer.data()), buffer.size()); + const auto count = input.gcount(); + if (count > 0) { + hash.update(std::span{buffer.data(), static_cast(count)}); + completed += static_cast(count); + context.report_progress(completed); + } + } + if (!input.eof()) { + error = "Could not read the downloaded package"; + return {}; + } + + return hash.finish(); +} + +std::filesystem::path staging_path( + const std::filesystem::path& stagingDir, std::string_view modId, std::string_view key) { + return stagingDir / fmt::format("{}-{}.dusk.part", safe_filename(modId), safe_filename(key)); +} + +bool copy_to_staging(const std::filesystem::path& source, const std::filesystem::path& destination, + uint64_t total, borealis::TaskContext& context, std::string& error) { + std::error_code filesystemError; + std::filesystem::create_directories(destination.parent_path(), filesystemError); + if (filesystemError) { + error = + fmt::format("Could not create the staging directory: {}", filesystemError.message()); + return false; + } + std::ifstream input{source, std::ios::binary}; + std::ofstream output{destination, std::ios::binary | std::ios::trunc}; + if (!input || !output) { + error = "Could not stage the local package"; + return false; + } + std::array buffer{}; + uint64_t completed = 0; + while (input) { + if (context.cancel_requested()) { + error = "Canceled"; + output.close(); + std::filesystem::remove(destination, filesystemError); + return false; + } + input.read(buffer.data(), buffer.size()); + const auto count = input.gcount(); + if (count > 0) { + output.write(buffer.data(), count); + completed += static_cast(count); + context.report_progress(completed, total); + } + } + output.close(); + if (!input.eof() || !output) { + error = "Could not copy the local package"; + std::filesystem::remove(destination, filesystemError); + return false; + } + return true; +} + +VerifyResult verify_url_package(const std::filesystem::path& path, const Request& request, + const Url& source, const std::filesystem::path& stagingDir, std::string key, + borealis::TaskContext& context) { + std::error_code ec; + const auto actualSize = std::filesystem::file_size(path, ec); + if (ec) { + return {.error = fmt::format("Could not read the downloaded package: {}", ec.message())}; + } + if (actualSize != source.size) { + return {.error = "Package size mismatch"}; + } + + std::string error; + const auto actualHash = sha256_file(path, context, error); + if (!error.empty()) { + return {.error = std::move(error)}; + } + if (context.cancel_requested()) { + return {.canceled = true}; + } + if (actualHash != lowercase(source.sha256)) { + return {.error = "Package checksum mismatch"}; + } + + ModMetadata metadata; + if (!inspect_mod_bundle(path, metadata, error)) { + return {.error = fmt::format("Invalid mod package: {}", error)}; + } + if (metadata.id != request.id) { + return {.error = "Package ID does not match the catalog entry"}; + } + if (metadata.version != request.version) { + return {.error = "Package version does not match the catalog entry"}; + } + if (context.cancel_requested()) { + return {.canceled = true}; + } + const auto stagedPath = staging_path(stagingDir, metadata.id, key); + std::filesystem::create_directories(stagedPath.parent_path(), ec); + if (ec) { + return {.error = fmt::format("Could not create the staging directory: {}", ec.message())}; + } + std::string replaceError; + if (!borealis::io::atomic_replace(path, stagedPath, replaceError)) { + return {.error = std::move(replaceError)}; + } + return {.metadata = std::move(metadata), .stagedPath = stagedPath}; +} + +VerifyResult verify_local_package(const LocalFile& source, const std::filesystem::path& stagingDir, + std::string key, borealis::TaskContext& context) { + std::error_code ec; + const auto size = std::filesystem::file_size(source.path, ec); + if (ec) { + return {.error = fmt::format("Could not read the local package: {}", ec.message())}; + } + context.report_progress(0, size); + const auto stagedPath = stagingDir / fmt::format("{}.dusk.part", key); + std::string error; + if (!copy_to_staging(source.path, stagedPath, size, context, error)) { + return {.error = std::move(error), .canceled = context.cancel_requested()}; + } + // Validate the bytes handed to the loader; the source may change during copying. + ModMetadata metadata; + if (!inspect_mod_bundle(stagedPath, metadata, error)) { + std::filesystem::remove(stagedPath, ec); + return {.error = fmt::format("Invalid mod package: {}", error)}; + } + return {.metadata = std::move(metadata), .stagedPath = stagedPath}; +} + +void remove_partial(const QueueItem& item) { + std::error_code ec; + if (!item.partialPath.empty()) { + std::filesystem::remove(item.partialPath, ec); + auto metadataPath = item.partialPath; + metadataPath += ".borealis-resume.json"; + std::filesystem::remove(metadataPath, ec); + } +} + +void fail(QueueItem& item, std::string message, bool discardPartial) { + item.task = {}; + item.verification = {}; + item.state = State::Failed; + item.message = std::move(message); + item.pendingIntent = PendingIntent::None; + if (discardPartial) { + remove_partial(item); + item.completed = 0; + } + const char* title = + local_source(item) != nullptr ? "Mod package failed" : "Mod download failed"; + ui::push_toast({ + .type = "warning", + .title = title, + .content = fmt::format("{}: {}", item.request.name, item.message), + .duration = std::chrono::seconds{6}, + }); +} + +bool retryable(const borealis::http::Result& result) { + if (result.error == borealis::http::Error::Network || + result.error == borealis::http::Error::Timeout) + { + return true; + } + const int status = result.response.statusCode; + return result.error == borealis::http::Error::None && + (status == 408 || status == 425 || status == 429 || status >= 500); +} + +void schedule_retry(QueueItem& item, std::string message) { + ++item.retryCount; + const int delaySeconds = std::min(30, 1 << std::min(item.retryCount, 4)); + item.retryAt = clock::now() + std::chrono::seconds{delaySeconds}; + item.state = State::Retrying; + item.message = std::move(message); + item.task = {}; +} + +void start_download(QueueItem& item) { + const auto* source = url_source(item); + if (source == nullptr) { + fail(item, "The install source is not a URL", false); + return; + } + const auto userDir = ModLoader::instance().user_mods_dir(); + if (userDir.empty()) { + fail(item, "No writable mods directory is configured", false); + return; + } + + item.partialPath = + userDir / ".downloads" / fmt::format("{}.dusk.part", safe_filename(item.request.id)); + std::error_code ec; + std::filesystem::create_directories(item.partialPath.parent_path(), ec); + if (ec) { + fail(item, fmt::format("Could not create the download directory: {}", ec.message()), false); + return; + } + + item.pendingIntent = PendingIntent::None; + item.message.clear(); + item.total = source->size; + item.state = State::Downloading; + item.task = borealis::http::start({ + .url = source->url, + .downloadTo = item.partialPath, + .connectTimeout = std::chrono::seconds{10}, + .idleTimeout = std::chrono::seconds{15}, + .totalTimeout = std::nullopt, + }); +} + +void start_local_verification(QueueItem& item) { + const auto* source = local_source(item); + if (source == nullptr) { + fail(item, "The install source is not a local file", false); + return; + } + const auto userDir = ModLoader::instance().user_mods_dir(); + if (userDir.empty()) { + fail(item, "No writable mods directory is configured", false); + return; + } + item.state = State::Verifying; + item.message.clear(); + item.completed = 0; + std::error_code error; + item.total = std::filesystem::file_size(source->path, error); + const auto stagingDir = userDir / ".staging"; + const auto local = *source; + item.verification = + borealis::spawn([local, stagingDir, key = item.key](borealis::TaskContext& context) { + return verify_local_package(local, stagingDir, key, context); + }); +} + +void finish_download(QueueItem& item) { + const auto progress = item.task.progress(); + item.completed = std::max(item.completed, progress.completed); + + std::optional completed; + std::string taskError; + bool taskFailed = false; + try { + completed = item.task.try_take(); + } catch (const std::exception& exception) { + taskError = exception.what(); + taskFailed = true; + } catch (...) { + taskError = "The download failed"; + taskFailed = true; + } + if (!completed && !taskFailed) { + return; + } + item.task = {}; + + switch (std::exchange(item.pendingIntent, PendingIntent::None)) { + case PendingIntent::Cancel: + remove_partial(item); + item.completed = 0; + item.state = State::Canceled; + return; + case PendingIntent::Pause: + return; + case PendingIntent::None: + break; + } + + if (taskFailed) { + schedule_retry(item, std::move(taskError)); + return; + } + + if (completed->error != borealis::http::Error::None || completed->response.statusCode < 200 || + completed->response.statusCode >= 300) + { + const auto message = !completed->message.empty() ? completed->message : + completed->response.statusCode != 0 ? + fmt::format("Server returned HTTP {}", + completed->response.statusCode) : + "The download failed"; + if (retryable(*completed)) { + schedule_retry(item, message); + } else { + fail(item, message, true); + } + return; + } + + const auto* source = url_source(item); + if (source == nullptr) { + fail(item, "The install source changed", true); + return; + } + item.completed = source->size; + item.state = State::Verifying; + item.message.clear(); + const auto stagingDir = ModLoader::instance().user_mods_dir() / ".staging"; + item.verification = + borealis::spawn([path = item.partialPath, request = item.request, source = *source, + stagingDir, key = item.key](borealis::TaskContext& context) { + return verify_url_package(path, request, source, stagingDir, key, context); + }); +} + +void finish_verification(QueueItem& item) { + VerifyResult result; + try { + auto completed = item.verification.try_take(); + if (!completed) { + return; + } + result = std::move(*completed); + } catch (const std::exception& exception) { + result.error = exception.what(); + } catch (...) { + result.error = "Package verification failed"; + } + item.verification = {}; + if (result.canceled || item.pendingIntent == PendingIntent::Cancel) { + item.pendingIntent = PendingIntent::None; + if (!result.stagedPath.empty()) { + std::error_code error; + std::filesystem::remove(result.stagedPath, error); + } + remove_partial(item); + item.state = State::Canceled; + item.completed = 0; + return; + } + if (!result.error.empty()) { + fail(item, std::move(result.error), true); + return; + } + remove_partial(item); + item.partialPath = result.stagedPath; + if (const auto duplicate = find_queue_item_by_mod_id(result.metadata.id); + duplicate != nullptr && duplicate != &item && !is_terminal(duplicate->state)) + { + fail(item, "This mod already has an active install", true); + return; + } + if (local_source(item) != nullptr && !item.request.id.empty() && + (item.request.id != result.metadata.id || item.request.version != result.metadata.version)) + { + fail(item, "The local package changed after confirmation", true); + return; + } + item.request.id = result.metadata.id; + item.request.name = result.metadata.name; + item.request.version = result.metadata.version; + item.completed = item.total; + item.state = State::Handoff; + item.operation = ModLoader::instance().request_install(std::move(result.stagedPath)); +} + +Item snapshot(const QueueItem& item) { + Item result{ + .id = item.key, + .modId = item.request.id, + .name = item.request.name, + .version = item.request.version, + .state = item.state, + .completed = item.completed, + .total = item.total, + .message = item.message, + .local = local_source(item) != nullptr, + .icon = item.request.icon, + }; + if (item.task) { + result.completed = std::max(result.completed, item.task.progress().completed); + } + if (item.verification) { + const auto progress = item.verification.progress(); + result.completed = progress.completed; + if (progress.total) { + result.total = *progress.total; + } + } + if (item.state == State::Retrying) { + const auto remaining = item.retryAt - clock::now(); + result.retrySeconds = std::max( + 0, static_cast(std::chrono::ceil(remaining).count())); + } + return result; +} + +} // namespace + +bool enqueue(Request request, std::string* keyOut) { + const auto* source = std::get_if(&request.source); + const auto* local = std::get_if(&request.source); + if (source != nullptr) { + if (request.id.empty() || request.version.empty() || source->size == 0 || + !source->url.starts_with("https://") || !valid_sha256(source->sha256)) + { + return false; + } + } else if (local == nullptr || request.id.empty() || request.version.empty()) { + return false; + } + const auto total = source == nullptr ? 0 : source->size; + if (request.name.empty()) { + request.name = request.id; + } + + if (auto* existing = find_queue_item_by_mod_id(request.id)) { + if (!is_terminal(existing->state)) { + return false; + } + existing->request = std::move(request); + existing->state = State::Queued; + existing->completed = 0; + existing->total = total; + existing->partialPath.clear(); + existing->message.clear(); + existing->retryCount = 0; + existing->operation.reset(); + existing->pendingIntent = PendingIntent::None; + if (keyOut != nullptr) { + *keyOut = existing->key; + } + return true; + } + + const auto key = fmt::format("queue-{}", nextQueueKey++); + if (keyOut != nullptr) { + *keyOut = key; + } + queueItems.push_back({.key = key, .request = std::move(request), .total = total}); + return true; +} + +void update() { + for (auto item = queueItems.begin(); item != queueItems.end();) { + if (item->task && item->task.ready()) { + finish_download(*item); + } + if (item->state == State::Verifying && item->verification && item->verification.ready()) { + finish_verification(*item); + } + if (item->state == State::Handoff && item->operation && + item->operation->state != ModOperation::State::Pending) + { + item->message = item->operation->message; + item->state = item->operation->state == ModOperation::State::Succeeded ? + State::Installed : + State::InstallFailed; + item->operation.reset(); + } + ++item; + } + + for (auto& item : queueItems) { + if (item.task || item.verification || item.operation) { + return; + } + if (is_terminal(item.state) || item.state == State::Paused) { + continue; + } + if (item.state == State::Downloading || item.state == State::Verifying) { + return; + } + if (item.state == State::Retrying && clock::now() < item.retryAt) { + return; + } + if (item.state == State::Queued || item.state == State::Retrying) { + if (local_source(item) != nullptr) { + start_local_verification(item); + } else { + start_download(item); + } + } + return; + } +} + +void shutdown() noexcept { + for (auto& item : queueItems) { + if (item.task) { + item.task.cancel(); + } + if (item.verification) { + item.verification.cancel(); + } + } + queueItems.clear(); +} + +std::vector items() { + std::vector result; + result.reserve(queueItems.size()); + for (const auto& item : queueItems) { + result.push_back(snapshot(item)); + } + return result; +} + +std::optional find(std::string_view id) { + const auto* item = find_queue_item(id); + return item == nullptr ? std::nullopt : std::optional{snapshot(*item)}; +} + +std::optional find_by_mod_id(std::string_view id) { + const auto* item = find_queue_item_by_mod_id(id); + return item == nullptr ? std::nullopt : std::optional{snapshot(*item)}; +} + +bool has_active_items() { + return std::ranges::any_of( + queueItems, [](const QueueItem& item) { return !is_terminal(item.state); }); +} + +size_t item_count() noexcept { + return queueItems.size(); +} + +size_t active_count() noexcept { + return static_cast(std::ranges::count_if( + queueItems, [](const QueueItem& item) { return !is_terminal(item.state); })); +} + +std::optional first_active() { + const auto item = std::ranges::find_if( + queueItems, [](const QueueItem& candidate) { return !is_terminal(candidate.state); }); + return item == queueItems.end() ? std::nullopt : std::optional{snapshot(*item)}; +} + +size_t active_items_ahead(std::string_view id) noexcept { + size_t result = 0; + for (const auto& item : queueItems) { + if (item.request.id == id) { + break; + } + if (!is_terminal(item.state)) { + ++result; + } + } + return result; +} + +void pause(std::string_view id) { + auto* item = find_queue_item(id); + if (item == nullptr || local_source(*item) != nullptr || + item->pendingIntent == PendingIntent::Cancel) + { + return; + } + if (item->state == State::Queued || item->state == State::Retrying) { + item->state = State::Paused; + return; + } + if (item->state == State::Downloading && item->task) { + item->completed = std::max(item->completed, item->task.progress().completed); + item->pendingIntent = PendingIntent::Pause; + item->state = State::Paused; + item->task.cancel(); + } +} + +void resume(std::string_view id) { + auto* item = find_queue_item(id); + if (item == nullptr || item->state != State::Paused || + item->pendingIntent == PendingIntent::Cancel) + { + return; + } + item->state = State::Queued; +} + +void retry(std::string_view id) { + auto* item = find_queue_item(id); + if (item == nullptr) { + return; + } + if (item->state == State::InstallFailed) { + auto* mod = ModLoader::instance().find_mod(item->request.id); + if (mod != nullptr) { + if (mod->activation_failed()) { + item->message.clear(); + item->state = State::Handoff; + item->operation = ModLoader::instance().request_reactivate(item->request.id); + } else { + item->message.clear(); + item->state = State::Installed; + } + return; + } + } else if (item->state != State::Failed) { + return; + } + remove_partial(*item); + item->completed = 0; + item->retryCount = 0; + item->message.clear(); + item->state = State::Queued; +} + +void cancel(std::string_view id) { + auto* item = find_queue_item(id); + if (item == nullptr || item->state == State::Handoff || is_terminal(item->state)) { + return; + } + if (item->task) { + item->pendingIntent = PendingIntent::Cancel; + item->message = "Canceling..."; + item->task.cancel(); + return; + } + if (item->verification) { + item->pendingIntent = PendingIntent::Cancel; + item->message = "Canceling..."; + item->verification.cancel(); + return; + } + remove_partial(*item); + item->pendingIntent = PendingIntent::None; + item->completed = 0; + item->state = State::Canceled; +} + +void clear(std::string_view id) { + const auto item = std::ranges::find( + queueItems, id, [](const QueueItem& candidate) { return std::string_view{candidate.key}; }); + if (item != queueItems.end() && is_terminal(item->state)) { + queueItems.erase(item); + } +} + +void remove_by_mod_id(std::string_view id) { + std::erase_if(queueItems, + [id](const QueueItem& item) { return std::string_view{item.request.id} == id; }); +} + +void pause_all() { + std::vector ids; + for (const auto& item : queueItems) { + if (item.state == State::Queued || item.state == State::Retrying || + item.state == State::Downloading) + { + ids.push_back(item.key); + } + } + for (const auto& id : ids) { + pause(id); + } +} + +void clear_finished() { + std::erase_if(queueItems, [](const QueueItem& item) { return is_terminal(item.state); }); +} + +} // namespace dusk::mods::queue diff --git a/src/dusk/mods/queue.hpp b/src/dusk/mods/queue.hpp new file mode 100644 index 0000000000..85481d6b4a --- /dev/null +++ b/src/dusk/mods/queue.hpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace dusk::mods::queue { + +enum class State { + Queued, + Downloading, + Paused, + Retrying, + Verifying, + Handoff, + Installed, + InstallFailed, + Failed, + Canceled, +}; + +[[nodiscard]] constexpr bool is_terminal(State state) noexcept { + return state == State::Installed || state == State::InstallFailed || state == State::Failed || + state == State::Canceled; +} + +[[nodiscard]] constexpr bool is_install_result(State state) noexcept { + return state == State::Installed || state == State::InstallFailed; +} + +struct Url { + std::string url; + std::string sha256; + uint64_t size = 0; +}; + +struct LocalFile { + std::filesystem::path path; +}; + +using Source = std::variant; + +struct Icon { + std::string url; + uint32_t width = 0; + uint32_t height = 0; +}; + +struct Request { + std::string id; + std::string name; + std::string version; + Source source; + std::optional icon; +}; + +struct Item { + // Queue key, independent of the mod ID. + std::string id; + std::string modId; + std::string name; + std::string version; + State state = State::Queued; + uint64_t completed = 0; + uint64_t total = 0; + std::string message; + int retrySeconds = 0; + bool local = false; + std::optional icon; +}; + +/** Adds an install, replacing failed or canceled work for the same package ID. */ +bool enqueue(Request request, std::string* key = nullptr); + +void update(); +void shutdown() noexcept; + +[[nodiscard]] std::vector items(); +[[nodiscard]] std::optional find(std::string_view key); +[[nodiscard]] std::optional find_by_mod_id(std::string_view id); +[[nodiscard]] bool has_active_items(); +[[nodiscard]] size_t item_count() noexcept; +[[nodiscard]] size_t active_count() noexcept; +[[nodiscard]] std::optional first_active(); +[[nodiscard]] size_t active_items_ahead(std::string_view id) noexcept; + +void pause(std::string_view id); +void resume(std::string_view id); +void retry(std::string_view id); +void cancel(std::string_view id); +void clear(std::string_view id); +void remove_by_mod_id(std::string_view id); +void pause_all(); +void clear_finished(); + +} // namespace dusk::mods::queue diff --git a/src/dusk/mods/svc/hook.cpp b/src/dusk/mods/svc/hook.cpp index d8a9005deb..725e83ab47 100644 --- a/src/dusk/mods/svc/hook.cpp +++ b/src/dusk/mods/svc/hook.cpp @@ -297,27 +297,32 @@ bool install_backend( #endif } -void deactivate_backend(void* target, InstalledBackend& backend) { +bool deactivate_backend(void* target, InstalledBackend& backend) { #if DUSK_HAS_PREPATCH if (backend.kind == BackendKind::Prepatch) { prepatch::publish(backend.prepatchSite, nullptr); backend = {}; - return; + return true; } #endif #if DUSK_HAS_FUNCHOOK if (backend.kind == BackendKind::Funchook) { const int uninst = funchook_uninstall(backend.handle, 0); + if (uninst != 0) { + DuskLog.warn("HookSystem: funchook uninstall for {:p} failed: {}", target, + funchook_error_message(backend.handle)); + return false; + } const int destr = funchook_destroy(backend.handle); - if (uninst != 0 || destr != 0) { - DuskLog.warn("HookSystem: funchook uninstall/destroy for {:p} returned {}/{}", target, - uninst, destr); + if (destr != 0) { + DuskLog.warn("HookSystem: funchook destroy for {:p} returned {}", target, destr); } } #else (void)target; #endif backend = {}; + return true; } bool handoff_backend( @@ -352,8 +357,8 @@ bool handoff_hook(void* target, InstalledHook& entry) { #else constexpr bool prepatched = false; #endif - if (!prepatched) { - deactivate_backend(target, entry.backend); + if (!prepatched && !deactivate_backend(target, entry.backend)) { + DuskLog.fatal("HookSystem: cannot hand off a hook that remains installed at {:p}", target); } entry.active = nullptr; @@ -535,13 +540,20 @@ ModResult hook_uninstall(ModContext* context, void* fnAddr, void** originalFnSlo return MOD_INVALID_ARGUMENT; } + const bool removedActive = entry.activeStore == originalFnSlot; +#if DUSK_HAS_FUNCHOOK + if (removedActive && entry.backend.kind == BackendKind::Funchook && + !deactivate_backend(fnAddr, entry.backend)) { + return MOD_ERROR; + } +#endif + if (const auto registryIt = s_registry.find(key); registryIt != s_registry.end() && erase_callbacks(registryIt->second, context)) { s_registry.erase(registryIt); } - const bool removedActive = entry.activeStore == originalFnSlot; entry.candidates.erase(candidateIt); *originalFnSlot = nullptr; if (!removedActive) { @@ -897,7 +909,9 @@ void hook_remove_mod(LoadedMod& mod) { auto* target = reinterpret_cast(it->first); if (entry.candidates.empty()) { - deactivate_backend(target, entry.backend); + if (!deactivate_backend(target, entry.backend)) { + DuskLog.fatal("HookSystem: cannot detach a mod with a live hook at {:p}", target); + } it = s_installed.erase(it); continue; } diff --git a/src/dusk/mods/svc/http.cpp b/src/dusk/mods/svc/http.cpp index 2277e66e01..423bd50b70 100644 --- a/src/dusk/mods/svc/http.cpp +++ b/src/dusk/mods/svc/http.cpp @@ -179,7 +179,7 @@ borealis::http::Result publish_download(borealis::http::Result result, } std::filesystem::path temporary = destination; - temporary += "." + borealis::io::fs_path_to_string(staging.filename()) + ".part"; + temporary += fmt::format(".{}.part", borealis::io::fs_path_to_string(staging.filename())); std::error_code ec; std::filesystem::copy_file( staging, temporary, std::filesystem::copy_options::overwrite_existing, ec); @@ -188,7 +188,7 @@ borealis::http::Result publish_download(borealis::http::Result result, std::error_code ignored; std::filesystem::remove(temporary, ignored); result.error = borealis::http::Error::Io; - result.message = "Failed to publish download: " + copyError; + result.message = fmt::format("Failed to publish download: {}", copyError); return result; } @@ -196,14 +196,14 @@ borealis::http::Result publish_download(borealis::http::Result result, if (!borealis::io::atomic_replace(temporary, destination, replaceError)) { std::filesystem::remove(temporary, ec); result.error = borealis::http::Error::Io; - result.message = "Failed to publish download: " + replaceError; + result.message = fmt::format("Failed to publish download: {}", replaceError); return result; } std::filesystem::remove(staging, ec); return result; } catch (const std::exception& exception) { result.error = borealis::http::Error::Io; - result.message = std::string{"Failed to publish download: "} + exception.what(); + result.message = fmt::format("Failed to publish download: {}", exception.what()); return result; } catch (...) { result.error = borealis::http::Error::Io; diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index a101c67660..8b7a778789 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -4,22 +4,25 @@ #include "dusk/logging.h" #include "dusk/mods/loader/loader.hpp" +#include + +#include +#include #include #include #include +#include #include namespace dusk::mods::svc { namespace { std::unordered_map s_services; +std::unordered_set s_unavailableServices; std::vector s_modules; std::string service_key(std::string_view id, const uint16_t majorVersion) { - std::string key{id}; - key.push_back('\x1f'); - key += std::to_string(majorVersion); - return key; + return fmt::format("{}\x1f{}", id, majorVersion); } const char* mod_id(const LoadedMod* mod) { @@ -48,6 +51,7 @@ bool validate_service_header(const ServiceHeader* header, const char* serviceId, void clear_services() { s_services.clear(); + s_unavailableServices.clear(); s_modules.clear(); } @@ -137,8 +141,38 @@ const ServiceRecord* find_service_record(const char* serviceId, const uint16_t m return it != s_services.end() ? &it->second : nullptr; } +std::string describe_missing_service(const char* serviceId, const uint16_t majorVersion, + const uint16_t minMinorVersion) { + const char* message = "Mod requires a service that is unavailable"; + if (std::string_view{serviceId}.starts_with(DUSKLIGHT_SERVICE_ID_PREFIX) && + !s_unavailableServices.contains(service_key(serviceId, majorVersion))) + { + if (const auto* record = find_service_record(serviceId, majorVersion)) { + if (record->provider == nullptr && record->service != nullptr && + record->minorVersion < minMinorVersion) + { + message = "Mod requires a newer Dusklight version"; + } + } else { + std::optional highestMajor; + for (const auto& [key, record] : s_services) { + if (record.provider == nullptr && record.service != nullptr && record.id == serviceId) { + highestMajor = std::max(highestMajor.value_or(0), record.majorVersion); + } + } + if (highestMajor) { + message = majorVersion > *highestMajor ? + "Mod requires a newer Dusklight version" : + "Mod must be updated for the current Dusklight version"; + } + } + } + return fmt::format("{} (missing: {})", message, serviceId); +} + ModResult register_module(const ServiceModule& module) { if (module.available != nullptr && !module.available()) { + s_unavailableServices.insert(service_key(module.id, module.majorVersion)); return MOD_UNAVAILABLE; } const auto result = register_service( @@ -146,6 +180,7 @@ ModResult register_module(const ServiceModule& module) { if (result != MOD_OK) { return result; } + s_unavailableServices.erase(service_key(module.id, module.majorVersion)); s_modules.push_back(&module); if (module.initialize != nullptr) { module.initialize(); @@ -272,6 +307,9 @@ bool ModLoader::register_static_service_exports(LoadedMod& mod) { std::string ModLoader::describe_missing_import( const char* serviceId, const uint16_t majorVersion, const uint16_t minMinorVersion) const { + if (std::string_view{serviceId}.starts_with(DUSKLIGHT_SERVICE_ID_PREFIX)) { + return svc::describe_missing_service(serviceId, majorVersion, minMinorVersion); + } if (const auto* record = svc::find_service_record(serviceId, majorVersion)) { if (record->service == nullptr) { return fmt::format("Required service {}@{} was never published by provider '{}'", @@ -298,7 +336,7 @@ std::string ModLoader::describe_missing_import( } } - return fmt::format("Required service unavailable: {}@{}", serviceId, majorVersion); + return svc::describe_missing_service(serviceId, majorVersion, minMinorVersion); } bool ModLoader::resolve_service_imports(LoadedMod& mod) { diff --git a/src/dusk/mods/svc/registry.hpp b/src/dusk/mods/svc/registry.hpp index 7d8de4b506..ae6476d385 100644 --- a/src/dusk/mods/svc/registry.hpp +++ b/src/dusk/mods/svc/registry.hpp @@ -58,6 +58,8 @@ const ServiceRecord* find_service( const char* serviceId, uint16_t majorVersion, uint16_t minMinorVersion); // Unlike find_service, also returns deferred records that have not been published yet. const ServiceRecord* find_service_record(const char* serviceId, uint16_t majorVersion); +std::string describe_missing_service(const char* serviceId, uint16_t majorVersion, + uint16_t minMinorVersion); ModResult register_module(const ServiceModule& module); void modules_mod_deactivating(LoadedMod& mod); diff --git a/src/dusk/mods/svc/ui.cpp b/src/dusk/mods/svc/ui.cpp index f10794d1ab..77f792b73c 100644 --- a/src/dusk/mods/svc/ui.cpp +++ b/src/dusk/mods/svc/ui.cpp @@ -111,9 +111,10 @@ struct UiSlot { std::string styleId; // Cached rendered values for element setters. These make the natural "set every update" // style cheap when the displayed value has not changed. - std::string elementRml; + std::string elementValue; float elementFloat = 0.0f; bool hasElementValue = false; + bool elementValueIsRml = false; }; SlotMap s_slots; @@ -547,7 +548,7 @@ ModResult ui_pane_add_text(LoadedMod& mod, uint64_t pane, const char* text, uint auto* elem = slot->pane->add_text(text); if (outElem != nullptr) { auto& elemSlot = alloc_slot(mod, UiSlotKind::Text, *outElem); - elemSlot.elementRml = ui::escape(text); + elemSlot.elementValue = text; elemSlot.hasElementValue = true; track_element(*outElem, elemSlot, *elem); } @@ -562,8 +563,9 @@ ModResult ui_pane_add_rml(LoadedMod& mod, uint64_t pane, const char* rml, uint64 auto* elem = slot->pane->add_rml(rml); if (outElem != nullptr) { auto& elemSlot = alloc_slot(mod, UiSlotKind::Text, *outElem); - elemSlot.elementRml = rml; + elemSlot.elementValue = rml; elemSlot.hasElementValue = true; + elemSlot.elementValueIsRml = true; track_element(*outElem, elemSlot, *elem); } return MOD_OK; @@ -793,13 +795,13 @@ ModResult ui_elem_set_text(LoadedMod& mod, uint64_t elem, const char* text) { if (slot == nullptr) { return MOD_INVALID_ARGUMENT; } - const std::string rml = ui::escape(text); - if (slot->hasElementValue && slot->elementRml == rml) { + if (slot->hasElementValue && !slot->elementValueIsRml && slot->elementValue == text) { return MOD_OK; } - slot->elementRml = rml; + slot->elementValue = text; slot->hasElementValue = true; - slot->element->SetInnerRML(slot->elementRml); + slot->elementValueIsRml = false; + ui::set_text_content(slot->element, slot->elementValue); return MOD_OK; } @@ -808,11 +810,12 @@ ModResult ui_elem_set_rml(LoadedMod& mod, uint64_t elem, const char* rml) { if (slot == nullptr) { return MOD_INVALID_ARGUMENT; } - if (slot->hasElementValue && slot->elementRml == rml) { + if (slot->hasElementValue && slot->elementValueIsRml && slot->elementValue == rml) { return MOD_OK; } - slot->elementRml = rml; + slot->elementValue = rml; slot->hasElementValue = true; + slot->elementValueIsRml = true; slot->element->SetInnerRML(rml); return MOD_OK; } @@ -936,7 +939,7 @@ ModResult ui_dialog_push(LoadedMod& mod, const UiDialogDesc& desc, uint64_t& out default: break; } - props.title = ui::escape(desc.title); + props.title = desc.title; props.bodyRml = desc.body_rml; props.icon = desc.icon != nullptr ? desc.icon : defaultIcon; props.onDismiss = [modPtr = &mod, handle, fn = desc.on_dismiss, userData = desc.user_data]( diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index a1a0f9ce54..d43530ad73 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -25,13 +25,13 @@ UserSettings g_userSettings = { }, .audio = { + .outputMode {"audio.outputMode", AudioOutputMode::StereoSpeakers}, .masterVolume {"audio.masterVolume", 60}, .mainMusicVolume {"audio.mainMusicVolume", 100}, .subMusicVolume {"audio.subMusicVolume", 100}, .soundEffectsVolume {"audio.soundEffectsVolume", 100}, .fanfareVolume {"audio.fanfareVolume", 100}, .enableReverb {"audio.enableReverb", true}, - .enableHrtf {"audio.enableHrtf", false}, .menuSounds {"audio.menuSounds", true}, }, @@ -256,13 +256,13 @@ void registerSettings() { [](const int&, const int&) { dusk::ui::apply_scale(); }); // Audio + Register(g_userSettings.audio.outputMode); Register(g_userSettings.audio.masterVolume); Register(g_userSettings.audio.mainMusicVolume); Register(g_userSettings.audio.subMusicVolume); Register(g_userSettings.audio.soundEffectsVolume); Register(g_userSettings.audio.fanfareVolume); Register(g_userSettings.audio.enableReverb); - Register(g_userSettings.audio.enableHrtf); Register(g_userSettings.audio.menuSounds); // Game diff --git a/src/dusk/settings.h b/src/dusk/settings.h index 83c2bfc2fa..0b4ce76838 100644 --- a/src/dusk/settings.h +++ b/src/dusk/settings.h @@ -68,6 +68,13 @@ enum class MagicArmorMode : u8 { COSMETIC = 4, }; +enum class AudioOutputMode : u8 { + StereoSpeakers = 0, + StereoHeadphones = 1, // spatial audio + Surround6ch = 2, // discrete 5.1 + Surround8ch = 3, // discrete 7.1 +}; + namespace config { template <> struct ConfigEnumRange { @@ -123,6 +130,12 @@ struct ConfigEnumRange { static constexpr auto max = MagicArmorMode::COSMETIC; }; +template <> +struct ConfigEnumRange { + static constexpr auto min = AudioOutputMode::StereoSpeakers; + static constexpr auto max = AudioOutputMode::Surround8ch; +}; + template <> struct ConfigValueTraits { static constexpr bool enabled = true; @@ -150,13 +163,13 @@ struct UserSettings { struct { // Audio + ConfigVar outputMode; ConfigVar masterVolume; ConfigVar mainMusicVolume; ConfigVar subMusicVolume; ConfigVar soundEffectsVolume; ConfigVar fanfareVolume; ConfigVar enableReverb; - ConfigVar enableHrtf; ConfigVar menuSounds; } audio; diff --git a/src/dusk/ui/achievements.cpp b/src/dusk/ui/achievements.cpp index 3fe713520e..29676f3a66 100644 --- a/src/dusk/ui/achievements.cpp +++ b/src/dusk/ui/achievements.cpp @@ -18,47 +18,39 @@ struct CategoryInfo { }; constexpr CategoryInfo kCategories[] = { - {AchievementCategory::Challenge, "Challenge"}, + {AchievementCategory::Challenge, "Challenge"}, {AchievementCategory::Collection, "Collection"}, - {AchievementCategory::Minigame, "Minigame"}, - {AchievementCategory::Misc, "Misc"}, - {AchievementCategory::Glitched, "Glitched"}, + {AchievementCategory::Minigame, "Minigame"}, + {AchievementCategory::Misc, "Misc"}, + {AchievementCategory::Glitched, "Glitched"}, }; -Rml::String build_achievement_info_rml(const Achievement& a) { - Rml::String s = fmt::format( - R"(
)" - R"({})" - R"({})" - R"(
)" - R"(

{}

)", - a.unlocked ? " unlocked" : "", - a.name, - a.unlocked ? " unlocked" : " locked", - a.unlocked ? "Unlocked" : "Locked", - a.description - ); +void append_achievement_info(Rml::Element* parent, const Achievement& a) { + auto* header = append(parent, "achievement-header"); + auto* name = append(header, "achievement-name"); + name->SetClass("unlocked", a.unlocked); + append_text(name, a.name); + auto* badge = append(header, "achievement-badge"); + badge->SetClass(a.unlocked ? "unlocked" : "locked", true); + append_text(badge, a.unlocked ? "Unlocked" : "Locked"); + auto* description = append(parent, "p"); + description->SetClass("achievement-desc", true); + append_text(description, a.description); if (a.isCounter) { - float fraction = a.goal > 0 ? float(a.progress) / float(a.goal) : 1.0f; - s += fmt::format( - R"()" - R"({} / {})", - fraction, - a.unlocked ? "progress-done" : "progress-ongoing", - a.progress, - a.goal - ); + const float fraction = a.goal > 0 ? float(a.progress) / float(a.goal) : 1.0f; + auto* progress = append(parent, "progress"); + progress->SetAttribute("value", fraction); + progress->SetClass(a.unlocked ? "progress-done" : "progress-ongoing", true); + append_text( + append(parent, "achievement-progress"), fmt::format("{} / {}", a.progress, a.goal)); } - - return s; } class AchievementRow : public FluentComponent { public: AchievementRow(Rml::Element* parent, const Achievement& a) - : FluentComponent(createRowRoot(parent)) - { + : FluentComponent(createRowRoot(parent)) { auto& btn = add_child - + @@ -162,22 +165,6 @@ DiscVerificationState verification_to_config(iso::ValidationError validation) { } } -std::string format_bytes(std::size_t bytes) { - constexpr double KiB = 1024.0; - constexpr double MiB = KiB * 1024.0; - constexpr double GiB = MiB * 1024.0; - if (bytes >= static_cast(GiB)) { - return fmt::format("{:.2f} GiB", static_cast(bytes) / GiB); - } - if (bytes >= static_cast(MiB)) { - return fmt::format("{:.0f} MiB", static_cast(bytes) / MiB); - } - if (bytes >= static_cast(KiB)) { - return fmt::format("{:.0f} KiB", static_cast(bytes) / KiB); - } - return fmt::format("{} B", bytes); -} - void begin_disc_verification(std::string path) noexcept { if (path.empty()) { return; @@ -329,7 +316,7 @@ void apply_disc_verification_result(const DiscVerificationResult& result) { state.pendingDiscPath = result.path; state.pendingDiscInfo = result.info; state.pendingDiscValidation = result.validation; - state.errorString = escape(get_error_msg(result.validation)); + state.errorString = get_error_msg(result.validation); return; } @@ -345,41 +332,34 @@ void apply_disc_verification_result(const DiscVerificationResult& result) { state.pendingDiscPath.clear(); state.pendingDiscInfo = {}; state.pendingDiscValidation = iso::ValidationError::Unknown; - state.errorString = escape(get_error_msg(result.validation)); + state.errorString = get_error_msg(result.validation); } class DiscVerificationModal : public WindowSmall { public: - DiscVerificationModal() : WindowSmall("modal", "modal-dialog") { - auto* header = append(mDialog, "div"); - header->SetClass("modal-header", true); + DiscVerificationModal() : WindowSmall("modal") { + auto* header = append(mDialog, "modal-header"); - auto* title = append(header, "div"); - title->SetClass("modal-title", true); - title->SetInnerRML("Verifying disc image"); + auto* title = append(header, "modal-title"); + append_text(title, "Verifying disc image"); auto* icon = append(header, "icon"); icon->SetClass("verifying", true); - auto* body = append(mDialog, "div"); - body->SetClass("modal-body", true); + auto* body = append(mDialog, "modal-body"); - auto* content = append(body, "div"); - content->SetClass("verification-progress", true); + auto* content = append(body, "verification-progress"); - mFileName = append(content, "div"); - mFileName->SetClass("verification-file", true); + mFileName = append(content, "verification-file"); mProgress = append(content, "progress"); mProgress->SetClass("progress-ongoing", true); mProgress->SetClass("verification-progress-bar", true); mProgress->SetAttribute("value", 0.f); - mDetail = append(content, "div"); - mDetail->SetClass("verification-detail", true); + mDetail = append(content, "verification-detail"); - auto* actions = append(mDialog, "div"); - actions->SetClass("modal-actions", true); + auto* actions = append(mDialog, "modal-actions"); mCancelButton = std::make_unique