diff --git a/CMakeLists.txt b/CMakeLists.txt index 3989a1facb..2674aded19 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -291,13 +291,13 @@ message(STATUS "dusklight: Fetching cxxopts") FetchContent_Declare(cxxopts URL https://github.com/jarro2783/cxxopts/archive/refs/tags/v3.3.1.tar.gz URL_HASH SHA256=3bfc70542c521d4b55a46429d808178916a579b28d048bd8c727ee76c39e2072 - DOWNLOAD_EXTRACT_TIMESTAMP TRUE + DOWNLOAD_EXTRACT_TIMESTAMP FALSE ) message(STATUS "dusklight: Fetching nlohmann/json") FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz URL_HASH SHA256=42f6e95cad6ec532fd372391373363b62a14af6d771056dbfc86160e6dfff7aa - DOWNLOAD_EXTRACT_TIMESTAMP TRUE + DOWNLOAD_EXTRACT_TIMESTAMP FALSE ) FetchContent_MakeAvailable(cxxopts json) diff --git a/CMakePresets.json b/CMakePresets.json index 461e751296..b7cb18c2d1 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -158,7 +158,11 @@ "cacheVariables": { "CMAKE_C_COMPILER": "cl", "CMAKE_CXX_COMPILER": "cl", - "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install" + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install", + "CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": { + "type": "BOOL", + "value": true + } }, "vendor": { "microsoft.com/VisualStudioSettings/CMake/1.0": { diff --git a/extern/aurora b/extern/aurora index da18e28ed4..cc1b2e3e5a 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit da18e28ed4047ebfb62a9aed64b5f12913cc1825 +Subproject commit cc1b2e3e5ac37b1f8df17739edbce8bece0aeda1 diff --git a/files.cmake b/files.cmake index 9704e13c5b..817feb7307 100644 --- a/files.cmake +++ b/files.cmake @@ -1418,7 +1418,11 @@ set(DUSK_FILES include/dusk/scope_guard.hpp src/dusk/dvd_asset.cpp src/d/actor/d_a_alink_dusk.cpp + src/dusk/android_frame_rate.hpp + src/dusk/android_frame_rate.cpp src/dusk/asserts.cpp + src/dusk/batch.cpp + src/dusk/batch.hpp src/dusk/config.cpp src/dusk/crash_handler.cpp src/dusk/crash_reporting.cpp @@ -1432,6 +1436,8 @@ set(DUSK_FILES src/dusk/game_clock.cpp src/dusk/globals.cpp src/dusk/gyro.cpp + include/dusk/menu_pointer.h + src/dusk/menu_pointer.cpp src/dusk/mouse.cpp src/dusk/gamepad_color.cpp src/dusk/autosave.cpp @@ -1445,6 +1451,7 @@ set(DUSK_FILES src/dusk/stubs.cpp include/dusk/texture_replacements.hpp src/dusk/texture_replacements.cpp + src/dusk/touch_camera.cpp src/dusk/update_check.cpp src/dusk/update_check.hpp #src/dusk/m_Do_ext_dusk.cpp @@ -1474,6 +1481,7 @@ set(DUSK_FILES src/dusk/ui/button.hpp src/dusk/ui/component.cpp src/dusk/ui/component.hpp + src/dusk/ui/controls.hpp src/dusk/ui/controller_config.cpp src/dusk/ui/controller_config.hpp src/dusk/ui/document.cpp @@ -1486,6 +1494,8 @@ set(DUSK_FILES src/dusk/ui/graphics_tuner.hpp src/dusk/ui/input.cpp src/dusk/ui/input.hpp + src/dusk/ui/icon_provider.cpp + src/dusk/ui/icon_provider.hpp src/dusk/ui/modal.cpp src/dusk/ui/modal.hpp src/dusk/ui/nav_types.hpp @@ -1511,6 +1521,12 @@ set(DUSK_FILES src/dusk/ui/string_button.hpp src/dusk/ui/tab_bar.cpp src/dusk/ui/tab_bar.hpp + src/dusk/ui/touch_controls_common.cpp + src/dusk/ui/touch_controls_common.hpp + src/dusk/ui/touch_controls.cpp + src/dusk/ui/touch_controls.hpp + src/dusk/ui/touch_controls_editor.cpp + src/dusk/ui/touch_controls_editor.hpp src/dusk/ui/ui.cpp src/dusk/ui/ui.hpp src/dusk/ui/warp.cpp diff --git a/flake.nix b/flake.nix index 99fcd49f94..ec87c5526a 100644 --- a/flake.nix +++ b/flake.nix @@ -138,7 +138,7 @@ NOD_PREBUILT = nod; CXXOPTS = pkgs.cxxopts.src; JSON = pkgs.nlohmann_json.src; - XXHASH = pkgs.xxHash.src; + XXHASH = pkgs.xxhash.src; ZSTD = pkgs.zstd.src; FMT = pkgs.fetchzip { url = "https://github.com/fmtlib/fmt/archive/refs/tags/11.1.4.tar.gz"; @@ -194,7 +194,7 @@ pkgs.zstd pkgs.cxxopts pkgs.nlohmann_json - pkgs.xxHash + pkgs.xxhash pkgs.abseil-cpp pkgs.zlib pkgs.libpng diff --git a/include/d/actor/d_a_alink.h b/include/d/actor/d_a_alink.h index 889bed2a4f..4ba442d755 100644 --- a/include/d/actor/d_a_alink.h +++ b/include/d/actor/d_a_alink.h @@ -4556,6 +4556,7 @@ public: void handleWolfHowl(); void handleQuickTransform(); bool checkAimContext(); + bool checkAimInputContext(); void onIronBallChainInterpCallback(); diff --git a/include/d/actor/d_flower.h b/include/d/actor/d_flower.h index cd505335a8..f63847956a 100644 --- a/include/d/actor/d_flower.h +++ b/include/d/actor/d_flower.h @@ -4,6 +4,10 @@ #include "JSystem/J3DGraphBase/J3DPacket.h" #include "SSystem/SComponent/c_xyz.h" +#if TARGET_PC +#include "dusk/batch.hpp" +#endif + class cCcD_Obj; class dCcMassS_HitInf; class fopAc_ac_c; @@ -107,6 +111,12 @@ public: #if TARGET_PC TGXTexObj mTexObj_l_J_Ohana00_64TEX; TGXTexObj mTexObj_l_J_Ohana01_64128_0419TEX; + + dusk::batch::LeafTemplate mTplHana00; // l_J_hana00DL + dusk::batch::LeafTemplate mTplHana00Cut; // l_J_hana00_cDL + dusk::batch::LeafTemplate mTplHana01; // l_J_hana01DL + dusk::batch::LeafTemplate mTplHana01Cut00; // l_J_hana01_c_00DL + dusk::batch::LeafTemplate mTplHana01Cut; // l_J_hana01_c_01DL #endif }; // Size: 0x12A54 diff --git a/include/d/actor/d_grass.h b/include/d/actor/d_grass.h index 47b948679d..b7cc3d7d95 100644 --- a/include/d/actor/d_grass.h +++ b/include/d/actor/d_grass.h @@ -4,6 +4,10 @@ #include "JSystem/J3DGraphBase/J3DPacket.h" #include "SSystem/SComponent/c_xyz.h" +#if TARGET_PC +#include "../../../src/dusk/batch.hpp" +#endif + class cCcD_Obj; class csXyz; class dCcMassS_HitInf; @@ -110,6 +114,10 @@ public: #if TARGET_PC TGXTexObj mTexObj_l_M_Hijiki00TEX; TGXTexObj mTexObj_l_M_kusa05_RGBATEX; + + dusk::batch::LeafTemplate mTplKusa9q; // l_M_Kusa_9qDL + dusk::batch::LeafTemplate mTplKusa9qCut; // l_M_Kusa_9q_cDL + dusk::batch::LeafTemplate mTplTengusa; // l_M_TenGusaDL #endif }; // Size: 0x1D718 diff --git a/include/d/d_camera.h b/include/d/d_camera.h index 0698bbbe5a..f98667d988 100644 --- a/include/d/d_camera.h +++ b/include/d/d_camera.h @@ -1037,7 +1037,7 @@ public: bool test1Camera(s32); bool test2Camera(s32); #if TARGET_PC - static bool canUseFreeCam(); + static bool isAimActive(); bool freeCamera(); bool executeDebugFlyCam(); void deactivateDebugFlyCam(); diff --git a/include/d/d_file_select.h b/include/d/d_file_select.h index d478126b38..634c0db352 100644 --- a/include/d/d_file_select.h +++ b/include/d/d_file_select.h @@ -287,6 +287,11 @@ public: MEMCARDCHECKPROC_ERR_YESNO_CURSOR_MOVE_ANM, MEMCARDCHECKPROC_SAVEDATA_CLEAR, +#if TARGET_PC + MEMCARDCHECKPROC_AUTO_MAKE_GAMEFILE, + MEMCARDCHECKPROC_AUTO_MAKE_GAMEFILE_ERR_WAIT, +#endif + #if PLATFORM_WII || PLATFORM_SHIELD MEMCARDCHECKPROC_NAND_STAT_CHECK, MEMCARDCHECKPROC_GAMEFILE_INIT_SEL, @@ -411,6 +416,10 @@ public: bool yesnoWakuAlpahAnm(u8); #if TARGET_PC void fileSelectWide(); + bool pointerDataSelect(); + bool pointerMenuSelect(); + bool pointerCopyDataToSelect(); + bool pointerYesNoSelect(bool errorSelect); #endif void _draw(); void errorMoveAnmInitSet(int, int); @@ -445,6 +454,10 @@ public: void MemCardMakeGameFile(); void MemCardMakeGameFileWait(); void MemCardMakeGameFileCheck(); +#if TARGET_PC + void MemCardAutoMakeGameFile(); + void MemCardAutoMakeGameFileErrWait(); +#endif void MemCardMsgWindowInitOpen(); void MemCardMsgWindowOpen(); void MemCardMsgWindowClose(); diff --git a/include/d/d_map.h b/include/d/d_map.h index 1acc7e4519..1cddbf3944 100644 --- a/include/d/d_map.h +++ b/include/d/d_map.h @@ -157,6 +157,9 @@ public: int getDispType() const; void _move(f32, f32, int, f32); void _draw(); +#if TARGET_PC + bool refreshTextureSize(); +#endif virtual ~dMap_c() { #if DEBUG diff --git a/include/d/d_menu_collect.h b/include/d/d_menu_collect.h index 28a095896e..d99b8f17cb 100644 --- a/include/d/d_menu_collect.h +++ b/include/d/d_menu_collect.h @@ -74,6 +74,8 @@ public: #if TARGET_PC void menuCollectWide(); + bool pointerWait(); + void pointerActivateCurrent(); #endif void _create(); diff --git a/include/d/d_menu_insect.h b/include/d/d_menu_insect.h index 6b23a3845d..90cfca7d5a 100644 --- a/include/d/d_menu_insect.h +++ b/include/d/d_menu_insect.h @@ -51,6 +51,10 @@ public: void setBButtonString(u16); void setHIO(bool); +#if TARGET_PC + bool pointerWait(); +#endif + virtual void draw() { _draw(); } virtual ~dMenu_Insect_c(); diff --git a/include/d/d_menu_letter.h b/include/d/d_menu_letter.h index 163c381699..208f8b65a9 100644 --- a/include/d/d_menu_letter.h +++ b/include/d/d_menu_letter.h @@ -55,6 +55,10 @@ public: u8 getLetterNum(); void setHIO(bool); +#if TARGET_PC + bool pointerWait(); +#endif + virtual void draw() { _draw(); } virtual ~dMenu_Letter_c(); diff --git a/include/d/d_menu_option.h b/include/d/d_menu_option.h index 7204d62974..03aca42808 100644 --- a/include/d/d_menu_option.h +++ b/include/d/d_menu_option.h @@ -80,6 +80,9 @@ public: void setBButtonString(u16); bool isRumbleSupported(); bool dpdMenuMove(); +#if TARGET_PC + bool pointerConfirmSelect(); +#endif void paneResize(u64); void initialize(); void yesnoMenuMoveAnmInitSet(int, int); diff --git a/include/d/d_menu_ring.h b/include/d/d_menu_ring.h index 74624eac80..f2137a9748 100644 --- a/include/d/d_menu_ring.h +++ b/include/d/d_menu_ring.h @@ -74,6 +74,9 @@ public: void clacEllipsePlotAverage(int, f32, f32); bool dpdMove(); u8 openExplain(u8); +#if TARGET_PC + bool pointerMove(); +#endif virtual void draw() { _draw(); } virtual ~dMenu_Ring_c(); diff --git a/include/d/d_menu_save.h b/include/d/d_menu_save.h index 116795f7be..d9d052aa33 100644 --- a/include/d/d_menu_save.h +++ b/include/d/d_menu_save.h @@ -266,6 +266,8 @@ public: #if TARGET_PC void menuSaveWide(); + bool pointerSaveSelect(); + bool pointerYesNoSelect(bool errorSelect, u8 errParam = 0, u8 soundParam = 0); #endif void _draw2(); diff --git a/include/d/d_menu_skill.h b/include/d/d_menu_skill.h index 9ea305163e..2f9097b165 100644 --- a/include/d/d_menu_skill.h +++ b/include/d/d_menu_skill.h @@ -49,6 +49,10 @@ public: u8 getSkillNum(); void setHIO(bool); +#if TARGET_PC + bool pointerWait(); +#endif + virtual void draw() { _draw(); } virtual ~dMenu_Skill_c(); diff --git a/include/d/d_msg_scrn_3select.h b/include/d/d_msg_scrn_3select.h index 949db3f130..1cb92e5be6 100644 --- a/include/d/d_msg_scrn_3select.h +++ b/include/d/d_msg_scrn_3select.h @@ -49,6 +49,10 @@ public: void selectScale(); void selectTrans(); void selectAnimeTransform(int); +#if TARGET_PC + bool pointerMove(); + bool consumePointerClick(); +#endif void setOffsetX(f32 i_offsetX) { mOffsetX = i_offsetX; } bool isAnimeUpdate(int param_0) { return (field_0x114 & (u8)(1 << param_0)) ? TRUE : FALSE; } diff --git a/include/dusk/action_bindings.h b/include/dusk/action_bindings.h index 7eba412fe8..a71dac5dfe 100644 --- a/include/dusk/action_bindings.h +++ b/include/dusk/action_bindings.h @@ -9,6 +9,8 @@ namespace dusk { enum class ActionBinds { FIRST_PERSON_CAMERA, CALL_MIDNA, + OPEN_MAP_SCREEN, + TOGGLE_MINIMAP, OPEN_DUSKLIGHT_MENU, TURBO_SPEED_BUTTON, COUNT, @@ -32,6 +34,12 @@ bool isActionBound(ActionBinds action, u32 port); void updateActionBindings(); +void setVirtualActionBind(ActionBinds action, u32 port, bool pressed, bool available = true); + +void clearVirtualActionBind(ActionBinds action, u32 port); + +void clearAllVirtualActionBinds(); + bool getActionBindTrig(ActionBinds action, u32 port); bool getActionBindHold(ActionBinds action, u32 port); diff --git a/include/dusk/config_var.hpp b/include/dusk/config_var.hpp index 0bae27bfd3..258bf4143c 100644 --- a/include/dusk/config_var.hpp +++ b/include/dusk/config_var.hpp @@ -4,6 +4,7 @@ #include "dolphin/types.h" #include #include +#include #include /** @@ -139,11 +140,16 @@ concept ConfigValueInteger = || std::is_same_v || std::is_same_v; +template +struct ConfigValueTraits { + static constexpr bool enabled = false; +}; + /** * \brief Concept that defines the legal set of types that can be used for CVar values. * * Valid types cannot be cv-qualified and must be basic primitive types (int, float, bool), - * strings, or enums of the basic primitives. + * strings, enums of the basic primitives, or explicitly-enabled structured settings. */ template concept ConfigValue = @@ -154,7 +160,8 @@ concept ConfigValue = || std::is_same_v || std::is_same_v || std::is_same_v - || (std::is_enum_v && ConfigValueInteger>)); + || (std::is_enum_v && ConfigValueInteger>) + || ConfigValueTraits::enabled); template const ConfigImplBase* GetConfigImpl(); diff --git a/include/dusk/menu_pointer.h b/include/dusk/menu_pointer.h new file mode 100644 index 0000000000..17546877d6 --- /dev/null +++ b/include/dusk/menu_pointer.h @@ -0,0 +1,60 @@ +#pragma once + +#include "dolphin/types.h" + +class CPaneMgr; + +namespace dusk::menu_pointer { + +enum class Context { + None, + FileSelect, + Save, + ItemWheel, + Collection, + Options, + Dialog, +}; + +enum class Phase { + Move, + Press, + Release, + Cancel, +}; + +struct State { + f32 x = 0.0f; + f32 y = 0.0f; + bool valid = false; + bool down = false; + bool pressed = false; + bool released = false; + bool clicked = false; + bool touch = false; +}; + +void begin_game_frame() noexcept; +void end_game_frame() noexcept; +void begin_context(Context context) noexcept; +bool handle_fallthrough_pointer(f32 x, f32 y, Phase phase, bool touch, s32 mouseButton = -1) noexcept; + +bool active() noexcept; +bool enabled() noexcept; +bool mouse_capture_active() noexcept; +const State& state() noexcept; +bool consume_click() noexcept; +void set_dialog_choice(u8 choice, bool clicked) noexcept; +bool get_dialog_choice(u8& choice) noexcept; +bool consume_dialog_click(u8& choice) noexcept; +void defer_activation(Context context, u8 target) noexcept; +bool consume_deferred_activation(Context context, u8 target) noexcept; +void clear_deferred_activation(Context context) noexcept; +u32 suppressed_pad_buttons(u32 port) noexcept; +void finish_pad_suppression_read(u32 port) noexcept; + +bool hit_rect(f32 left, f32 top, f32 right, f32 bottom, f32 padding = 0.0f) noexcept; +bool hit_pane(CPaneMgr* pane, f32 padding = 0.0f) noexcept; +bool hit_pane(J2DPane* pane, f32 padding = 0.0f) noexcept; + +} // namespace dusk::menu_pointer diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 4137f340ad..2354ee9852 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -1,10 +1,10 @@ -#ifndef DUSK_CONFIG_H -#define DUSK_CONFIG_H +#pragma once #include #include #include "dusk/config_var.hpp" +#include "dusk/ui/controls.hpp" namespace dusk { @@ -41,11 +41,6 @@ enum class DiscVerificationState : u8 { HashMismatch, }; -enum class GyroMode : u8 { - Sensor = 0, - Mouse = 1, -}; - enum class FrameInterpMode : u8 { Off = 0, Capped = 1, @@ -97,12 +92,6 @@ struct ConfigEnumRange { static constexpr auto max = DiscVerificationState::HashMismatch; }; -template <> -struct ConfigEnumRange { - static constexpr auto min = GyroMode::Sensor; - static constexpr auto max = GyroMode::Mouse; -}; - template <> struct ConfigEnumRange { static constexpr auto min = FrameInterpMode::Off; @@ -120,6 +109,11 @@ struct ConfigEnumRange { static constexpr auto min = MagicArmorMode::NORMAL; static constexpr auto max = MagicArmorMode::COSMETIC; }; + +template <> +struct ConfigValueTraits { + static constexpr bool enabled = true; +}; } // namespace config // Persistent user settings @@ -135,6 +129,9 @@ struct UserSettings { ConfigVar enableFpsOverlay; ConfigVar fpsOverlayCorner; ConfigVar maxFrameRate; + ConfigVar rememberWindowSize; + ConfigVar lastWindowWidth; + ConfigVar lastWindowHeight; } video; struct { @@ -219,6 +216,9 @@ struct UserSettings { ConfigVar mouseCameraSensitivity; ConfigVar invertMouseY; ConfigVar freeCamera; + ConfigVar enableTouchControls; + ConfigVar enableMenuPointer; + ConfigVar touchControlsLayout; ConfigVar invertCameraXAxis; ConfigVar invertCameraYAxis; ConfigVar invertFirstPersonXAxis; @@ -227,6 +227,8 @@ struct UserSettings { ConfigVar invertAirSwimY; ConfigVar freeCameraXSensitivity; ConfigVar freeCameraYSensitivity; + ConfigVar touchCameraXSensitivity; + ConfigVar touchCameraYSensitivity; ConfigVar debugFlyCam; ConfigVar debugFlyCamLockEvents; ConfigVar allowBackgroundInput; @@ -288,6 +290,8 @@ struct UserSettings { struct { std::array firstPersonCamera; std::array callMidna; + std::array openMapScreen; + std::array toggleMinimap; std::array openDusklightMenu; std::array turboSpeedButton; } actionBindings; @@ -322,6 +326,4 @@ struct TransientSettings { TransientSettings& getTransientSettings(); -} - -#endif // DUSK_CONFIG_H +} // namespace dusk diff --git a/include/dusk/touch_camera.h b/include/dusk/touch_camera.h new file mode 100644 index 0000000000..d1680afed1 --- /dev/null +++ b/include/dusk/touch_camera.h @@ -0,0 +1,12 @@ +#pragma once + +namespace dusk::touch_camera { + +constexpr float YAW_DEGREES_PER_DP = 0.34f; +constexpr float PITCH_DEGREES_PER_DP = 0.22f; + +void add_delta(float yaw_dp, float pitch_dp) noexcept; +bool consume_delta(float& yaw_dp, float& pitch_dp) noexcept; +void clear() noexcept; + +} // namespace dusk::touch_camera diff --git a/libs/JSystem/include/JSystem/J2DGraph/J2DPicture.h b/libs/JSystem/include/JSystem/J2DGraph/J2DPicture.h index b054c97fae..f738a96dd4 100644 --- a/libs/JSystem/include/JSystem/J2DGraph/J2DPicture.h +++ b/libs/JSystem/include/JSystem/J2DGraph/J2DPicture.h @@ -212,6 +212,9 @@ public: void setCornerColor(JUtility::TColor c0) { setCornerColor(c0, c0, c0, c0); } +#if TARGET_PC + JUtility::TColor corner(size_t index) const { return mCornerColor[index]; } +#endif protected: /* 0x100 */ JUTTexture* mTexture[2]; diff --git a/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h b/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h index 795a5a2628..ec15c0430a 100644 --- a/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h +++ b/libs/JSystem/include/JSystem/JParticle/JPABaseShape.h @@ -3,6 +3,20 @@ #include +#if TARGET_PC +#include + +struct ParticleDrawCtx { + bool batch; // off = immediate mode + bool useTexMtx; // UVs transformed by texMtx + bool useClr0; // prm color in GX_VA_CLR0 + bool useClr1; // env color in GX_VA_CLR1 + Mtx texMtx; + GXColor clr0; + GXColor clr1; +}; +#endif + struct JPAEmitterWorkData; class JPABaseParticle; class JKRHeap; @@ -75,6 +89,9 @@ public: const GXTevColorArg* getTevColorArg() const { return st_ca[(pBsd->mFlags >> 0x0F) & 0x07]; } const GXTevAlphaArg* getTevAlphaArg() const { return st_aa[(pBsd->mFlags >> 0x12) & 0x01]; } +#if TARGET_PC + u32 getTevColorArgSel() const { return (pBsd->mFlags >> 0x0F) & 0x07; } +#endif u32 getType() const { return (pBsd->mFlags >> 0) & 0x0F; } u32 getDirType() const { return (pBsd->mFlags >> 4) & 0x07; } @@ -186,26 +203,34 @@ void JPARegistPrm(JPAEmitterWorkData*); void JPARegistEnv(JPAEmitterWorkData*); void JPARegistPrmEnv(JPAEmitterWorkData*); -void JPADrawPoint(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawLine(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawRotBillboard(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawBillboard(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawRotDirection(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawDirection(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawRotation(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawDBillboard(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawRotYBillboard(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawYBillboard(JPAEmitterWorkData*, JPABaseParticle*); -void JPADrawParticleCallBack(JPAEmitterWorkData*, JPABaseParticle*); -void JPALoadTexAnm(JPAEmitterWorkData*, JPABaseParticle*); -void JPASetPointSize(JPAEmitterWorkData*, JPABaseParticle*); -void JPASetLineWidth(JPAEmitterWorkData*, JPABaseParticle*); -void JPALoadCalcTexCrdMtxAnm(JPAEmitterWorkData*, JPABaseParticle*); -void JPARegistAlpha(JPAEmitterWorkData*, JPABaseParticle*); -void JPARegistEnv(JPAEmitterWorkData*, JPABaseParticle*); -void JPARegistAlphaEnv(JPAEmitterWorkData*, JPABaseParticle*); -void JPARegistPrmAlpha(JPAEmitterWorkData*, JPABaseParticle*); -void JPARegistPrmAlphaEnv(JPAEmitterWorkData*, JPABaseParticle*); +#if TARGET_PC +#define JPA_DRAW_PARTICLE_ARGS JPAEmitterWorkData*, JPABaseParticle*, ParticleDrawCtx* +#else +#define JPA_DRAW_PARTICLE_ARGS JPAEmitterWorkData*, JPABaseParticle* +#endif + +void JPADrawPoint(JPA_DRAW_PARTICLE_ARGS); +void JPADrawLine(JPA_DRAW_PARTICLE_ARGS); +void JPADrawRotBillboard(JPA_DRAW_PARTICLE_ARGS); +void JPADrawBillboard(JPA_DRAW_PARTICLE_ARGS); +void JPADrawRotDirection(JPA_DRAW_PARTICLE_ARGS); +void JPADrawDirection(JPA_DRAW_PARTICLE_ARGS); +void JPADrawRotation(JPA_DRAW_PARTICLE_ARGS); +void JPADrawDBillboard(JPA_DRAW_PARTICLE_ARGS); +void JPADrawRotYBillboard(JPA_DRAW_PARTICLE_ARGS); +void JPADrawYBillboard(JPA_DRAW_PARTICLE_ARGS); +void JPADrawParticleCallBack(JPA_DRAW_PARTICLE_ARGS); +void JPALoadTexAnm(JPA_DRAW_PARTICLE_ARGS); +void JPASetPointSize(JPA_DRAW_PARTICLE_ARGS); +void JPASetLineWidth(JPA_DRAW_PARTICLE_ARGS); +void JPALoadCalcTexCrdMtxAnm(JPA_DRAW_PARTICLE_ARGS); +void JPARegistAlpha(JPA_DRAW_PARTICLE_ARGS); +void JPARegistEnv(JPA_DRAW_PARTICLE_ARGS); +void JPARegistAlphaEnv(JPA_DRAW_PARTICLE_ARGS); +void JPARegistPrmAlpha(JPA_DRAW_PARTICLE_ARGS); +void JPARegistPrmAlphaEnv(JPA_DRAW_PARTICLE_ARGS); + +#undef JPA_DRAW_PARTICLE_ARGS #if TARGET_PC void JPAInterpBillboard(JPAEmitterWorkData*, JPABaseParticle*); diff --git a/libs/JSystem/include/JSystem/JParticle/JPAResource.h b/libs/JSystem/include/JSystem/JParticle/JPAResource.h index ebf2127033..07563fa73d 100644 --- a/libs/JSystem/include/JSystem/JParticle/JPAResource.h +++ b/libs/JSystem/include/JSystem/JParticle/JPAResource.h @@ -17,6 +17,10 @@ class JPADynamicsBlock; class JPAFieldBlock; class JPAKeyBlock; +#if TARGET_PC +struct ParticleDrawCtx; +#endif + /** * @ingroup jsystem-jparticle * @@ -50,13 +54,19 @@ public: public: typedef void (*EmitterFunc)(JPAEmitterWorkData*); typedef void (*ParticleFunc)(JPAEmitterWorkData*, JPABaseParticle*); +#if TARGET_PC + typedef void (*DrawParticleFunc)(JPAEmitterWorkData*, JPABaseParticle*, + ParticleDrawCtx*); +#else + typedef ParticleFunc DrawParticleFunc; +#endif /* 0x00 */ EmitterFunc* mpCalcEmitterFuncList; /* 0x04 */ EmitterFunc* mpDrawEmitterFuncList; /* 0x08 */ EmitterFunc* mpDrawEmitterChildFuncList; /* 0x0C */ ParticleFunc* mpCalcParticleFuncList; - /* 0x10 */ ParticleFunc* mpDrawParticleFuncList; + /* 0x10 */ DrawParticleFunc* mpDrawParticleFuncList; /* 0x14 */ ParticleFunc* mpCalcParticleChildFuncList; - /* 0x18 */ ParticleFunc* mpDrawParticleChildFuncList; + /* 0x18 */ DrawParticleFunc* mpDrawParticleChildFuncList; /* 0x1C */ JPABaseShape* pBsp; /* 0x20 */ JPAExtraShape* pEsp; @@ -77,6 +87,20 @@ public: /* 0x45 */ u8 mpDrawParticleFuncListNum; /* 0x46 */ u8 mpCalcParticleChildFuncListNum; /* 0x47 */ u8 mpDrawParticleChildFuncListNum; + +#if TARGET_PC + struct BatchInfo { + f32 vtxPos[8][3]; + f32 vtxUv[8][2]; + u8 vtxCount; // 4 (quad) or 8 (cross) + bool supported; // draw func list contains only batchable funcs + bool hasPtclColor; // per-particle JPARegist* func is present + bool hasPtclTexMtx; // JPALoadCalcTexCrdMtxAnm is present + }; + BatchInfo mBatchInfo; + + void initBatchInfo(); +#endif }; #endif /* JPARESOURCE_H */ diff --git a/libs/JSystem/src/J3DGraphBase/J3DShape.cpp b/libs/JSystem/src/J3DGraphBase/J3DShape.cpp index fc7ac5b727..4b407d1e05 100644 --- a/libs/JSystem/src/J3DGraphBase/J3DShape.cpp +++ b/libs/JSystem/src/J3DGraphBase/J3DShape.cpp @@ -136,8 +136,8 @@ void J3DLoadCPCmd(u8 addr, u32 val) { #if TARGET_PC static void J3DLoadArrayBasePtr(GXAttr attr, void* data, u32 size, bool le) { u32 idx = (attr == GX_VA_NBT) ? 1 : (attr - GX_VA_POS); - GXCmd1u8(GX_LOAD_AURORA); - GXCmd1u16(GX_LOAD_AURORA_ARRAYBASE | idx); + GXCmd1u8(GX_AURORA); + GXCmd1u16(GX_AURORA_LOAD_ARRAYBASE | idx); GXCmd1u64((u64)data); GXCmd1u32(size); GXCmd1u8(le ? 1 : 0); diff --git a/libs/JSystem/src/J3DGraphBase/J3DShapeDraw.cpp b/libs/JSystem/src/J3DGraphBase/J3DShapeDraw.cpp index b40f577d79..e9abdc85f1 100644 --- a/libs/JSystem/src/J3DGraphBase/J3DShapeDraw.cpp +++ b/libs/JSystem/src/J3DGraphBase/J3DShapeDraw.cpp @@ -7,265 +7,11 @@ #include "JSystem/JKernel/JKRHeap.h" #if TARGET_PC -#include +#include #include -#include -#include "dusk/logging.h" namespace { -u16 read_be16(const u8* data) { - return (u16(data[0]) << 8) | data[1]; -} - -void append_be16(std::vector& out, u16 value) { - out.push_back(value >> 8); - out.push_back(value & 0xFF); -} - -void append_bytes(std::vector& out, const u8* data, u32 size) { - out.insert(out.end(), data, data + size); -} - -bool is_matrix_idx_attr(GXAttr attr) { - return attr >= GX_VA_PNMTXIDX && attr <= GX_VA_TEX7MTXIDX; -} - -bool is_draw_opcode(u8 opcode) { - return opcode == GX_QUADS || opcode == GX_TRIANGLES || opcode == GX_TRIANGLESTRIP || - opcode == GX_TRIANGLEFAN || opcode == GX_LINES || opcode == GX_LINESTRIP || - opcode == GX_POINTS; -} - -bool is_mergeable_draw_opcode(u8 opcode) { - return opcode == GX_QUADS || opcode == GX_TRIANGLES || opcode == GX_TRIANGLESTRIP || - opcode == GX_TRIANGLEFAN; -} - -bool calc_vtx_stride(const GXVtxDescList* vtxDesc, u32& stride) { - stride = 0; - for (; vtxDesc->attr != GX_VA_NULL; vtxDesc++) { - switch (vtxDesc->type) { - case GX_NONE: - break; - case GX_DIRECT: - if (!is_matrix_idx_attr(vtxDesc->attr)) { - return false; - } - stride += 1; - break; - case GX_INDEX8: - stride += 1; - break; - case GX_INDEX16: - stride += 2; - break; - default: - return false; - } - } - return stride != 0; -} - -bool get_command_size(const u8* dlStart, u32 dlSize, u32 offset, u32 stride, u32& cmdSize) { - if (offset >= dlSize) { - return false; - } - - const u8 cmd = dlStart[offset]; - const u8 opcode = cmd & GX_OPCODE_MASK; - switch (opcode) { - case GX_NOP: - case GX_CMD_INVL_VC: - cmdSize = 1; - return true; - case (GX_LOAD_BP_REG & GX_OPCODE_MASK): - cmdSize = 5; - return offset + cmdSize <= dlSize; - case GX_LOAD_CP_REG: - cmdSize = 6; - return offset + cmdSize <= dlSize; - case GX_LOAD_XF_REG: { - if (offset + 5 > dlSize) { - return false; - } - const u16 count = read_be16(dlStart + offset + 1) + 1; - cmdSize = 5 + count * 4; - return offset + cmdSize <= dlSize; - } - case GX_LOAD_INDX_A: - case GX_LOAD_INDX_B: - case GX_LOAD_INDX_C: - case GX_LOAD_INDX_D: - cmdSize = 5; - return offset + cmdSize <= dlSize; - case GX_CMD_CALL_DL: - cmdSize = 9; - return offset + cmdSize <= dlSize; - default: - if (is_draw_opcode(opcode)) { - if (offset + 3 > dlSize) { - return false; - } - const u16 vtxCount = read_be16(dlStart + offset + 1); - cmdSize = 3 + vtxCount * stride; - return offset + cmdSize <= dlSize; - } - return false; - } -} - -struct MergeRun { - u8 cmd = 0; - u16 vtxCount = 0; - std::vector vertices; -}; - -void flush_merge_run(std::vector& out, MergeRun& run) { - if (run.vtxCount == 0) { - return; - } - - out.push_back(run.cmd); - append_be16(out, run.vtxCount); - append_bytes(out, run.vertices.data(), run.vertices.size()); - run.vertices.clear(); - run.vtxCount = 0; -} - -void append_vertex(std::vector& out, const u8* vertices, u32 stride, u16 idx) { - append_bytes(out, vertices + idx * stride, stride); -} - -bool triangulate_draw( - std::vector& out, u8 opcode, const u8* vertices, u32 stride, u16 vtxCount) { - switch (opcode) { - case GX_TRIANGLES: - append_bytes(out, vertices, vtxCount * stride); - return true; - case GX_TRIANGLEFAN: - if (vtxCount < 3) { - return false; - } - for (u16 v = 2; v < vtxCount; v++) { - append_vertex(out, vertices, stride, 0); - append_vertex(out, vertices, stride, v - 1); - append_vertex(out, vertices, stride, v); - } - return true; - case GX_TRIANGLESTRIP: - if (vtxCount < 3) { - return false; - } - for (u16 v = 2; v < vtxCount; v++) { - if ((v & 1) == 0) { - append_vertex(out, vertices, stride, v - 2); - append_vertex(out, vertices, stride, v - 1); - } else { - append_vertex(out, vertices, stride, v - 1); - append_vertex(out, vertices, stride, v - 2); - } - append_vertex(out, vertices, stride, v); - } - return true; - case GX_QUADS: - if ((vtxCount & 3) != 0) { - return false; - } - for (u16 v = 0; v < vtxCount; v += 4) { - append_vertex(out, vertices, stride, v); - append_vertex(out, vertices, stride, v + 1); - append_vertex(out, vertices, stride, v + 2); - append_vertex(out, vertices, stride, v + 2); - append_vertex(out, vertices, stride, v + 3); - append_vertex(out, vertices, stride, v); - } - return true; - default: - return false; - } -} - -void append_triangles_to_run( - std::vector& out, MergeRun& run, u8 cmd, const std::vector& vertices, u32 stride) { - u32 offset = 0; - u32 remaining = vertices.size() / stride; - while (remaining != 0) { - if (run.vtxCount != 0 && run.cmd != cmd) { - flush_merge_run(out, run); - } - - if (run.vtxCount == 0) { - run.cmd = cmd; - } - - u32 available = 0xFFFF - run.vtxCount; - if (available == 0) { - flush_merge_run(out, run); - continue; - } - - u32 toCopy = std::min(remaining, available); - append_bytes(run.vertices, vertices.data() + offset * stride, toCopy * stride); - run.vtxCount += toCopy; - offset += toCopy; - remaining -= toCopy; - - if (run.vtxCount == 0xFFFF) { - flush_merge_run(out, run); - } - } -} - -bool optimize_display_list(const u8* dlStart, u32 dlSize, u32 stride, std::vector& out) { - MergeRun run; - out.reserve(dlSize); - - for (u32 offset = 0; offset < dlSize;) { - u32 cmdSize = 0; - if (!get_command_size(dlStart, dlSize, offset, stride, cmdSize)) { - return false; - } - - const u8 cmd = dlStart[offset]; - const u8 opcode = cmd & GX_OPCODE_MASK; - if (opcode == GX_NOP) { - offset += cmdSize; - continue; - } - - if (!is_draw_opcode(opcode)) { - flush_merge_run(out, run); - append_bytes(out, dlStart + offset, cmdSize); - offset += cmdSize; - continue; - } - - if (!is_mergeable_draw_opcode(opcode)) { - flush_merge_run(out, run); - append_bytes(out, dlStart + offset, cmdSize); - offset += cmdSize; - continue; - } - - const u16 vtxCount = read_be16(dlStart + offset + 1); - const u8* vertices = dlStart + offset + 3; - std::vector triangles; - if (!triangulate_draw(triangles, opcode, vertices, stride, vtxCount)) { - flush_merge_run(out, run); - append_bytes(out, dlStart + offset, cmdSize); - offset += cmdSize; - continue; - } - - append_triangles_to_run(out, run, (GX_TRIANGLES | (cmd & GX_VAT_MASK)), triangles, stride); - offset += cmdSize; - } - - flush_merge_run(out, run); - return true; -} - void set_display_list_copy(void*& displayList, u32& displayListSize, const u8* data, u32 size) { const u32 alignedSize = ALIGN_NEXT(size, 0x20); u8* newDL = JKR_NEW_ARRAY_ARGS(u8, alignedSize, 0x20); @@ -289,20 +35,11 @@ u32 J3DShapeDraw::countVertex(u32 stride) { u8* dlStart = (u8*)getDisplayList(); #if TARGET_PC - for (u32 offset = 0; offset < getDisplayListSize();) { - u8 cmd = dlStart[offset]; - u8 opcode = cmd & GX_OPCODE_MASK; - u32 cmdSize = 0; - if (!get_command_size(dlStart, getDisplayListSize(), offset, stride, cmdSize)) { - break; + aurora::gx::dl::Reader reader{dlStart, getDisplayListSize(), static_cast(stride)}; + while (const auto cmd = reader.next()) { + if (cmd->kind != aurora::gx::dl::Command::Kind::Passthrough) { + count += cmd->draw.vtxCount; } - if (!is_draw_opcode(opcode)) { - offset += cmdSize; - continue; - } - int vtxNum = be16(*reinterpret_cast(dlStart + offset + 1)); - count += vtxNum; - offset += 3 + stride * vtxNum; } #else for (u8* dl = dlStart; (dl - dlStart) < getDisplayListSize();) { @@ -320,6 +57,53 @@ u32 J3DShapeDraw::countVertex(u32 stride) { return count; } +#if TARGET_PC +void J3DShapeDraw::addTexMtxIndexInDL(u32 stride, u32 attrOffs, u32 valueBase) { + u32 byteNum = countVertex(stride); + u32 oldSize = mDisplayListSize; + u32 newSize = ALIGN_NEXT(oldSize + byteNum, 0x20); + u8* newDLStart = JKR_NEW_ARRAY_ARGS(u8, newSize, 0x20); + u8* oldDLStart = (u8*)mDisplayList; + u8* newDL = newDLStart; + + aurora::gx::dl::Reader reader{oldDLStart, mDisplayListSize, static_cast(stride)}; + while (const auto cmd = reader.next()) { + if (cmd->kind == aurora::gx::dl::Command::Kind::Passthrough) { + std::memcpy(newDL, cmd->data, cmd->size); + newDL += cmd->size; + continue; + } + + const auto& draw = cmd->draw; + const u32 headerSize = draw.vertices - cmd->data; + std::memcpy(newDL, cmd->data, headerSize); + newDL += headerSize; + + for (u32 i = 0; i < draw.vtxCount; i++) { + const u8* oldVtx = draw.vertices + stride * i; + u8 pnmtxidx = oldVtx[0]; + std::memcpy(newDL, oldVtx, attrOffs); + newDL += attrOffs; + *newDL++ = valueBase + pnmtxidx; + std::memcpy(newDL, oldVtx + attrOffs, stride - attrOffs); + newDL += stride - attrOffs; + } + } + if (reader.failed()) { + // preserve the remainder untouched + std::memcpy(newDL, oldDLStart + reader.pos(), mDisplayListSize - reader.pos()); + newDL += mDisplayListSize - reader.pos(); + } + + u32 realSize = ALIGN_NEXT((uintptr_t)newDL - (uintptr_t)newDLStart, 0x20); + for (; (newDL - newDLStart) < newSize; newDL++) + *newDL = 0; + + mDisplayListSize = realSize; + mDisplayList = newDLStart; + DCStoreRange(newDLStart, mDisplayListSize); +} +#else void J3DShapeDraw::addTexMtxIndexInDL(u32 stride, u32 attrOffs, u32 valueBase) { u32 byteNum = countVertex(stride); u32 oldSize = mDisplayListSize; @@ -330,32 +114,13 @@ void J3DShapeDraw::addTexMtxIndexInDL(u32 stride, u32 attrOffs, u32 valueBase) { u8* newDL = newDLStart; for (; (oldDL - oldDLStart) < mDisplayListSize;) { -#if TARGET_PC - u32 oldOffset = oldDL - oldDLStart; - u32 cmdSize = 0; - if (!get_command_size(oldDLStart, mDisplayListSize, oldOffset, stride, cmdSize)) { - memcpy(newDL, oldDL, mDisplayListSize - oldOffset); - newDL += mDisplayListSize - oldOffset; - break; - } -#endif // Copy command u8 cmd = *(u8*)oldDL; oldDL++; *newDL++ = cmd; -#if TARGET_PC - u8 opcode = cmd & GX_OPCODE_MASK; - if (!is_draw_opcode(opcode)) { - memcpy(newDL, oldDL, cmdSize - 1); - oldDL += cmdSize - 1; - newDL += cmdSize - 1; - continue; - } -#else if (cmd != GX_TRIANGLEFAN && cmd != GX_TRIANGLESTRIP) break; -#endif // Copy count int vtxNum = *(u16*)oldDL; @@ -384,6 +149,7 @@ void J3DShapeDraw::addTexMtxIndexInDL(u32 stride, u32 attrOffs, u32 valueBase) { mDisplayList = newDLStart; DCStoreRange(newDLStart, mDisplayListSize); } +#endif J3DShapeDraw::J3DShapeDraw(const u8* displayList, u32 displayListSize) { #if TARGET_PC @@ -397,12 +163,8 @@ J3DShapeDraw::J3DShapeDraw(const u8* displayList, u32 displayListSize) { #if TARGET_PC J3DShapeDraw::J3DShapeDraw( const u8* displayList, u32 displayListSize, const GXVtxDescList* vtxDesc) { - u32 stride = 0; - std::vector optimized; - if (calc_vtx_stride(vtxDesc, stride) && - optimize_display_list(displayList, displayListSize, stride, optimized)) - { - set_display_list_copy(mDisplayList, mDisplayListSize, optimized.data(), optimized.size()); + if (const auto optimized = aurora::gx::dl::optimize(displayList, displayListSize, vtxDesc)) { + set_display_list_copy(mDisplayList, mDisplayListSize, optimized->data(), optimized->size()); } else { set_display_list_copy(mDisplayList, mDisplayListSize, displayList, displayListSize); } diff --git a/libs/JSystem/src/JParticle/JPABaseShape.cpp b/libs/JSystem/src/JParticle/JPABaseShape.cpp index add61135fe..a569d96842 100644 --- a/libs/JSystem/src/JParticle/JPABaseShape.cpp +++ b/libs/JSystem/src/JParticle/JPABaseShape.cpp @@ -14,6 +14,33 @@ #endif #include "tracy/Tracy.hpp" +#if TARGET_PC +#define JPA_DRAW_CTX_PARAM , ParticleDrawCtx* ctx + +namespace { +GXColor emitter_prm_color(JPAEmitterWorkData* work) { + JPABaseEmitter* emtr = work->mpEmtr; + GXColor prm = emtr->mPrmClr; + prm.r = COLOR_MULTI(prm.r, emtr->mGlobalPrmClr.r); + prm.g = COLOR_MULTI(prm.g, emtr->mGlobalPrmClr.g); + prm.b = COLOR_MULTI(prm.b, emtr->mGlobalPrmClr.b); + prm.a = COLOR_MULTI(prm.a, emtr->mGlobalPrmClr.a); + return prm; +} + +GXColor emitter_env_color(JPAEmitterWorkData* work) { + JPABaseEmitter* emtr = work->mpEmtr; + GXColor env = emtr->mEnvClr; + env.r = COLOR_MULTI(env.r, emtr->mGlobalEnvClr.r); + env.g = COLOR_MULTI(env.g, emtr->mGlobalEnvClr.g); + env.b = COLOR_MULTI(env.b, emtr->mGlobalEnvClr.b); + return env; +} +} // namespace +#else +#define JPA_DRAW_CTX_PARAM +#endif + void JPASetPointSize(JPAEmitterWorkData* work) { GXSetPointSize((u8)(25.0f * work->mGlobalPtclScl.x), GX_TO_ONE); } @@ -22,15 +49,16 @@ void JPASetLineWidth(JPAEmitterWorkData* work) { GXSetLineWidth((u8)(25.0f * work->mGlobalPtclScl.x), GX_TO_ONE); } -void JPASetPointSize(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPASetPointSize(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { GXSetPointSize((u8)(ptcl->mParticleScaleX * (25.0f * work->mGlobalPtclScl.x)), GX_TO_ONE); } -void JPASetLineWidth(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPASetLineWidth(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { GXSetLineWidth((u8)(ptcl->mParticleScaleX * (25.0f * work->mGlobalPtclScl.x)), GX_TO_ONE); } void JPARegistPrm(JPAEmitterWorkData* work) { + ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor prm = emtr->mPrmClr; prm.r = COLOR_MULTI(prm.r, emtr->mGlobalPrmClr.r); @@ -41,6 +69,7 @@ void JPARegistPrm(JPAEmitterWorkData* work) { } void JPARegistEnv(JPAEmitterWorkData* work) { + ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor env = emtr->mEnvClr; env.r = COLOR_MULTI(env.r, emtr->mGlobalEnvClr.r); @@ -50,6 +79,7 @@ void JPARegistEnv(JPAEmitterWorkData* work) { } void JPARegistPrmEnv(JPAEmitterWorkData* work) { + ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor prm = emtr->mPrmClr; GXColor env = emtr->mEnvClr; @@ -64,7 +94,8 @@ void JPARegistPrmEnv(JPAEmitterWorkData* work) { GXSetTevColor(GX_TEVREG1, env); } -void JPARegistAlpha(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPARegistAlpha(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { + ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor prm = emtr->mPrmClr; prm.r = COLOR_MULTI(prm.r, emtr->mGlobalPrmClr.r); @@ -72,10 +103,19 @@ void JPARegistAlpha(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { prm.b = COLOR_MULTI(prm.b, emtr->mGlobalPrmClr.b); prm.a = COLOR_MULTI(prm.a, emtr->mGlobalPrmClr.a); prm.a = COLOR_MULTI(prm.a, ptcl->mPrmColorAlphaAnm); +#if TARGET_PC + if (ctx->batch) { + ctx->clr0 = prm; + if (ctx->useClr1) { + ctx->clr1 = emitter_env_color(work); + } + return; + } +#endif GXSetTevColor(GX_TEVREG0, prm); } -void JPARegistPrmAlpha(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPARegistPrmAlpha(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor prm = ptcl->mPrmClr; @@ -84,10 +124,19 @@ void JPARegistPrmAlpha(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { prm.b = COLOR_MULTI(prm.b, emtr->mGlobalPrmClr.b); prm.a = COLOR_MULTI(prm.a, emtr->mGlobalPrmClr.a); prm.a = COLOR_MULTI(prm.a, ptcl->mPrmColorAlphaAnm); +#if TARGET_PC + if (ctx->batch) { + ctx->clr0 = prm; + if (ctx->useClr1) { + ctx->clr1 = emitter_env_color(work); + } + return; + } +#endif GXSetTevColor(GX_TEVREG0, prm); } -void JPARegistPrmAlphaEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPARegistPrmAlphaEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor prm = ptcl->mPrmClr; @@ -100,11 +149,19 @@ void JPARegistPrmAlphaEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { env.r = COLOR_MULTI(env.r, emtr->mGlobalEnvClr.r); env.g = COLOR_MULTI(env.g, emtr->mGlobalEnvClr.g); env.b = COLOR_MULTI(env.b, emtr->mGlobalEnvClr.b); +#if TARGET_PC + if (ctx->batch) { + ctx->clr0 = prm; + ctx->clr1 = env; + return; + } +#endif GXSetTevColor(GX_TEVREG0, prm); GXSetTevColor(GX_TEVREG1, env); } -void JPARegistAlphaEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPARegistAlphaEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { + ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor prm = emtr->mPrmClr; GXColor env = ptcl->mEnvClr; @@ -116,16 +173,31 @@ void JPARegistAlphaEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { env.r = COLOR_MULTI(env.r, emtr->mGlobalEnvClr.r); env.g = COLOR_MULTI(env.g, emtr->mGlobalEnvClr.g); env.b = COLOR_MULTI(env.b, emtr->mGlobalEnvClr.b); +#if TARGET_PC + if (ctx->batch) { + ctx->clr0 = prm; + ctx->clr1 = env; + return; + } +#endif GXSetTevColor(GX_TEVREG0, prm); GXSetTevColor(GX_TEVREG1, env); } -void JPARegistEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPARegistEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { + ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor env = ptcl->mEnvClr; env.r = COLOR_MULTI(env.r, emtr->mGlobalEnvClr.r); env.g = COLOR_MULTI(env.g, emtr->mGlobalEnvClr.g); env.b = COLOR_MULTI(env.b, emtr->mGlobalEnvClr.b); +#if TARGET_PC + if (ctx->batch) { + ctx->clr0 = emitter_prm_color(work); + ctx->clr1 = env; + return; + } +#endif GXSetTevColor(GX_TEVREG1, env); } @@ -258,7 +330,7 @@ void JPAGenCalcTexCrdMtxAnm(JPAEmitterWorkData* work) { GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_TEXMTX0); } -void JPALoadCalcTexCrdMtxAnm(JPAEmitterWorkData* work, JPABaseParticle* param_1) { +void JPALoadCalcTexCrdMtxAnm(JPAEmitterWorkData* work, JPABaseParticle* param_1 JPA_DRAW_CTX_PARAM) { ZoneScoped; JPABaseShape* shape = work->mpRes->getBsp(); f32 dVar16 = param_1->mAge; @@ -286,6 +358,12 @@ void JPALoadCalcTexCrdMtxAnm(JPAEmitterWorkData* work, JPABaseParticle* param_1) local_108[2][1] = 0.0f; local_108[2][2] = 1.0f; local_108[2][3] = 0.0f; +#if TARGET_PC + if (ctx->batch) { + MTXCopy(local_108, ctx->texMtx); + return; + } +#endif GXLoadTexMtxImm(local_108, 0x1e, GX_MTX2x4); } @@ -299,7 +377,7 @@ void JPALoadTexAnm(JPAEmitterWorkData* work) { work->mpResMgr->load(work->mpRes->getTexIdx(work->mpEmtr->mTexAnmIdx), GX_TEXMAP0); } -void JPALoadTexAnm(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPALoadTexAnm(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { ZoneScoped; work->mpResMgr->load(work->mpRes->getTexIdx(ptcl->mTexAnmIdx), GX_TEXMAP0); } @@ -429,6 +507,47 @@ static projectionFunc p_prj[3] = { }; #if TARGET_PC +static void emit_batch_quad(JPAEmitterWorkData* work, const ParticleDrawCtx* ctx, + const Mtx posMtx) { + const JPAResource::BatchInfo& info = work->mpRes->mBatchInfo; + + for (int i = 0; i < info.vtxCount; i++) { + Vec localPos = {info.vtxPos[i][0], info.vtxPos[i][1], info.vtxPos[i][2]}; + Vec drawPos; + MTXMultVec(posMtx, &localPos, &drawPos); + + f32 texS = info.vtxUv[i][0]; + f32 texT = info.vtxUv[i][1]; + if (ctx->useTexMtx) { + f32 srcS = texS; + f32 srcT = texT; + texS = ctx->texMtx[0][0] * srcS + ctx->texMtx[0][1] * srcT + ctx->texMtx[0][3]; + texT = ctx->texMtx[1][0] * srcS + ctx->texMtx[1][1] * srcT + ctx->texMtx[1][3]; + } + + GXPosition3f32(drawPos.x, drawPos.y, drawPos.z); + if (ctx->useClr0) { + GXColor4u8(ctx->clr0.r, ctx->clr0.g, ctx->clr0.b, ctx->clr0.a); + } + if (ctx->useClr1) { + GXColor4u8(ctx->clr1.r, ctx->clr1.g, ctx->clr1.b, ctx->clr1.a); + } + GXTexCoord2f32(texS, texT); + } +} + +static void submit_particle_quad( + JPAEmitterWorkData* work, ParticleDrawCtx* ctx, const Mtx posMtx, const u8* dl, u32 dlSize) { + if (ctx->batch) { + emit_batch_quad(work, ctx, posMtx); + return; + } + + GXLoadPosMtxImm(posMtx, GX_PNMTX0); + p_prj[work->mPrjType](work, posMtx); + GXCallDisplayList(dl, dlSize); +} + void JPAInterpBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { Mtx ptclPosMtx; MTXTrans(ptclPosMtx, ptcl->mPosition.x, ptcl->mPosition.y, ptcl->mPosition.z); @@ -448,7 +567,7 @@ void JPAInterpRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { } #endif -void JPADrawBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPADrawBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { if (ptcl->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -473,12 +592,16 @@ void JPADrawBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { posMtx[2][2] = 1.0f; posMtx[2][3] = pos.z; posMtx[0][1] = posMtx[0][2] = posMtx[1][0] = posMtx[1][2] = posMtx[2][0] = posMtx[2][1] = 0.0f; +#if TARGET_PC + submit_particle_quad(work, ctx, posMtx, jpa_dl, sizeof(jpa_dl)); +#else GXLoadPosMtxImm(posMtx, GX_PNMTX0); p_prj[work->mPrjType](work, posMtx); GXCallDisplayList(jpa_dl, sizeof(jpa_dl)); +#endif } -void JPADrawRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPADrawRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { if (ptcl->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -517,12 +640,16 @@ void JPADrawRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { posMtx[2][2] = 1.0f; posMtx[2][3] = pos.z; posMtx[0][2] = posMtx[1][2] = posMtx[2][0] = posMtx[2][1] = 0.0f; +#if TARGET_PC + submit_particle_quad(work, ctx, posMtx, jpa_dl, sizeof(jpa_dl)); +#else GXLoadPosMtxImm(posMtx, GX_PNMTX0); p_prj[work->mPrjType](work, posMtx); GXCallDisplayList(jpa_dl, sizeof(jpa_dl)); +#endif } -void JPADrawYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) { +void JPADrawYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1 JPA_DRAW_CTX_PARAM) { if (param_1->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -542,12 +669,16 @@ void JPADrawYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) { local_38[2][2] = work->mYBBCamMtx[2][2]; local_38[2][3] = local_48.z; local_38[0][1] = local_38[0][2] = local_38[1][0] = local_38[2][0] = 0.0f; +#if TARGET_PC + submit_particle_quad(work, ctx, local_38, jpa_dl, sizeof(jpa_dl)); +#else GXLoadPosMtxImm(local_38, GX_PNMTX0); p_prj[work->mPrjType](work, local_38); GXCallDisplayList(jpa_dl, sizeof(jpa_dl)); +#endif } -void JPADrawRotYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) { +void JPADrawRotYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1 JPA_DRAW_CTX_PARAM) { if (param_1->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -576,9 +707,13 @@ void JPADrawRotYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) { local_38[2][1] = local_94 * fVar1; local_38[2][2] = local_90; local_38[2][3] = local_48.z; +#if TARGET_PC + submit_particle_quad(work, ctx, local_38, jpa_dl, sizeof(jpa_dl)); +#else GXLoadPosMtxImm(local_38, GX_PNMTX0); p_prj[work->mPrjType](work, local_38); GXCallDisplayList(jpa_dl, sizeof(jpa_dl)); +#endif } void dirTypeVel(JPAEmitterWorkData const* work, JPABaseParticle const* param_1, @@ -741,6 +876,88 @@ static u8* p_dl[2] = { }; #if TARGET_PC +static bool make_direction_mtx(JPAEmitterWorkData* work, JPABaseParticle* ptcl, Mtx posMtx) { + JGeometry::TVec3 axisY; + JGeometry::TVec3 axisZ; + JGeometry::TVec3 baseAxis(ptcl->mBaseAxis); + p_direction[work->mDirType](work, ptcl, &axisY); + if (axisY.isZero()) { + return false; + } + + axisY.normalize(); + axisZ.cross(baseAxis, axisY); + if (axisZ.isZero()) { + return false; + } + + axisZ.normalize(); + baseAxis.cross(axisY, axisZ); + baseAxis.normalize(); + ptcl->mBaseAxis.set(baseAxis); + + f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX; + f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY; + posMtx[0][0] = baseAxis.x; + posMtx[0][1] = axisY.x; + posMtx[0][2] = axisZ.x; + posMtx[0][3] = ptcl->mPosition.x; + posMtx[1][0] = baseAxis.y; + posMtx[1][1] = axisY.y; + posMtx[1][2] = axisZ.y; + posMtx[1][3] = ptcl->mPosition.y; + posMtx[2][0] = baseAxis.z; + posMtx[2][1] = axisY.z; + posMtx[2][2] = axisZ.z; + posMtx[2][3] = ptcl->mPosition.z; + p_plane[work->mPlaneType](posMtx, scaleX, scaleY); + return true; +} + +static bool make_rot_direction_mtx(JPAEmitterWorkData* work, JPABaseParticle* ptcl, Mtx posMtx) { + f32 sinRot = JMASSin(ptcl->mRotateAngle); + f32 cosRot = JMASCos(ptcl->mRotateAngle); + JGeometry::TVec3 axisY; + JGeometry::TVec3 axisZ; + JGeometry::TVec3 baseAxis(ptcl->mBaseAxis); + p_direction[work->mDirType](work, ptcl, &axisY); + if (axisY.isZero()) { + return false; + } + + axisY.normalize(); + axisZ.cross(baseAxis, axisY); + if (axisZ.isZero()) { + return false; + } + + axisZ.normalize(); + baseAxis.cross(axisY, axisZ); + baseAxis.normalize(); + ptcl->mBaseAxis.set(baseAxis); + + f32 scaleX = work->mGlobalPtclScl.x * ptcl->mParticleScaleX; + f32 scaleY = work->mGlobalPtclScl.y * ptcl->mParticleScaleY; + Mtx rotMtx; + Mtx dirMtx; + p_rot[work->mRotType](sinRot, cosRot, rotMtx); + p_plane[work->mPlaneType](rotMtx, scaleX, scaleY); + dirMtx[0][0] = baseAxis.x; + dirMtx[0][1] = axisY.x; + dirMtx[0][2] = axisZ.x; + dirMtx[0][3] = ptcl->mPosition.x; + dirMtx[1][0] = baseAxis.y; + dirMtx[1][1] = axisY.y; + dirMtx[1][2] = axisZ.y; + dirMtx[1][3] = ptcl->mPosition.y; + dirMtx[2][0] = baseAxis.z; + dirMtx[2][1] = axisY.z; + dirMtx[2][2] = axisZ.z; + dirMtx[2][3] = ptcl->mPosition.z; + MTXConcat(dirMtx, rotMtx, posMtx); + return true; +} + void JPAInterpDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { JGeometry::TVec3 axisY; JGeometry::TVec3 axisZ; @@ -823,7 +1040,7 @@ void JPAInterpRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { } #endif -void JPADrawDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPADrawDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { if (ptcl->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -832,8 +1049,12 @@ void JPADrawDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { Mtx posMtx; #if TARGET_PC - if (!dusk::frame_interp::lookup_replacement(ptcl, posMtx)) -#endif + if (!dusk::frame_interp::lookup_replacement(ptcl, posMtx) && + !make_direction_mtx(work, ptcl, posMtx)) + { + return; + } +#else { JGeometry::TVec3 axisY; JGeometry::TVec3 axisZ; @@ -869,14 +1090,19 @@ void JPADrawDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { posMtx[2][3] = ptcl->mPosition.z; p_plane[work->mPlaneType](posMtx, scaleX, scaleY); } +#endif MTXConcat(work->mPosCamMtx, posMtx, posMtx); +#if TARGET_PC + submit_particle_quad(work, ctx, posMtx, p_dl[work->mDLType], sizeof(jpa_dl)); +#else GXLoadPosMtxImm(posMtx, GX_PNMTX0); p_prj[work->mPrjType](work, posMtx); GXCallDisplayList(p_dl[work->mDLType], sizeof(jpa_dl)); +#endif } -void JPADrawRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPADrawRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { if (ptcl->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -886,8 +1112,12 @@ void JPADrawRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { Mtx mtx1; Mtx mtx2; #if TARGET_PC - if (!dusk::frame_interp::lookup_replacement(ptcl, mtx1)) -#endif + if (!dusk::frame_interp::lookup_replacement(ptcl, mtx1) && + !make_rot_direction_mtx(work, ptcl, mtx1)) + { + return; + } +#else { f32 sinRot = JMASSin(ptcl->mRotateAngle); f32 cosRot = JMASCos(ptcl->mRotateAngle); @@ -927,13 +1157,18 @@ void JPADrawRotDirection(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { mtx2[2][3] = ptcl->mPosition.z; MTXConcat(mtx2, mtx1, mtx1); } +#endif MTXConcat(work->mPosCamMtx, mtx1, mtx2); +#if TARGET_PC + submit_particle_quad(work, ctx, mtx2, p_dl[work->mDLType], sizeof(jpa_dl)); +#else GXLoadPosMtxImm(mtx2, GX_PNMTX0); p_prj[work->mPrjType](work, mtx2); GXCallDisplayList(p_dl[work->mDLType], sizeof(jpa_dl)); +#endif } -void JPADrawDBillboard(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) { +void JPADrawDBillboard(JPAEmitterWorkData* param_0, JPABaseParticle* param_1 JPA_DRAW_CTX_PARAM) { if (param_1->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -970,7 +1205,7 @@ void JPADrawDBillboard(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) { GXCallDisplayList(jpa_dl, sizeof(jpa_dl)); } -void JPADrawRotation(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) { +void JPADrawRotation(JPAEmitterWorkData* param_0, JPABaseParticle* param_1 JPA_DRAW_CTX_PARAM) { if (param_1->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -988,12 +1223,16 @@ void JPADrawRotation(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) { auStack_88[1][3] = param_1->mPosition.y; auStack_88[2][3] = param_1->mPosition.z; MTXConcat(param_0->mPosCamMtx, auStack_88, auStack_88); +#if TARGET_PC + submit_particle_quad(param_0, ctx, auStack_88, p_dl[param_0->mDLType], sizeof(jpa_dl)); +#else GXLoadPosMtxImm(auStack_88, 0); p_prj[param_0->mPrjType](param_0, auStack_88); GXCallDisplayList(p_dl[param_0->mDLType], sizeof(jpa_dl)); +#endif } -void JPADrawPoint(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPADrawPoint(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { if (ptcl->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -1010,7 +1249,7 @@ void JPADrawPoint(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { GXSetVtxDesc(GX_VA_TEX0, GX_INDEX8); } -void JPADrawLine(JPAEmitterWorkData* param_0, JPABaseParticle* param_1) { +void JPADrawLine(JPAEmitterWorkData* param_0, JPABaseParticle* param_1 JPA_DRAW_CTX_PARAM) { if (param_1->checkStatus(JPAPtclStts_Invisible)) { return; } @@ -1086,7 +1325,7 @@ void JPADrawStripe(JPAEmitterWorkData* param_0) { GXSetVtxDesc(GX_VA_POS, GX_DIRECT); GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT); GXBegin(GX_TRIANGLESTRIP, GX_VTXFMT1, ptcl_num << 1); - for (JPANode* node = startNode; node != param_0->mpAlivePtcl->getEnd(); + for (JPANode* node = startNode; node != param_0->mpAlivePtcl->getEnd(); node = node_func(node), coord += step) { param_0->mpCurNode = node; JPABaseParticle* particle = node->getObject(); @@ -1111,7 +1350,7 @@ void JPADrawStripe(JPAEmitterWorkData* param_0) { } particle->mBaseAxis.cross(local_f8, local_104); particle->mBaseAxis.normalize(); - + local_c8[0][0] = local_104.x; local_c8[0][1] = local_f8.x; local_c8[0][2] = particle->mBaseAxis.x; @@ -1177,7 +1416,7 @@ void JPADrawStripeX(JPAEmitterWorkData* param_0) { GXSetVtxDesc(GX_VA_POS, GX_DIRECT); GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT); GXBegin(GX_TRIANGLESTRIP, GX_VTXFMT1, ptcl_num << 1); - for (JPANode* node = startNode; node != param_0->mpAlivePtcl->getEnd(); + for (JPANode* node = startNode; node != param_0->mpAlivePtcl->getEnd(); node = node_func(node), coord += step) { param_0->mpCurNode = node; JPABaseParticle* particle = node->getObject(); @@ -1202,7 +1441,7 @@ void JPADrawStripeX(JPAEmitterWorkData* param_0) { } particle->mBaseAxis.cross(local_c0, local_cc); particle->mBaseAxis.normalize(); - + local_90[0][0] = local_cc.x; local_90[0][1] = local_c0.x; local_90[0][2] = particle->mBaseAxis.x; @@ -1227,7 +1466,7 @@ void JPADrawStripeX(JPAEmitterWorkData* param_0) { coord = start_coord; GXBegin(GX_TRIANGLESTRIP, GX_VTXFMT1, ptcl_num << 1); - for (JPANode* node = startNode; node != param_0->mpAlivePtcl->getEnd(); + for (JPANode* node = startNode; node != param_0->mpAlivePtcl->getEnd(); node = node_func(node), coord += step) { param_0->mpCurNode = node; JPABaseParticle* particle = node->getObject(); @@ -1252,7 +1491,7 @@ void JPADrawStripeX(JPAEmitterWorkData* param_0) { } particle->mBaseAxis.cross(local_c0, local_cc); particle->mBaseAxis.normalize(); - + local_90[0][0] = local_cc.x; local_90[0][1] = local_c0.x; local_90[0][2] = particle->mBaseAxis.x; @@ -1289,7 +1528,7 @@ void JPADrawEmitterCallBackB(JPAEmitterWorkData* work) { emtr->mpEmtrCallBack->draw(emtr); } -void JPADrawParticleCallBack(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { +void JPADrawParticleCallBack(JPAEmitterWorkData* work, JPABaseParticle* ptcl JPA_DRAW_CTX_PARAM) { JPABaseEmitter* emtr = work->mpEmtr; if (emtr->mpPtclCallBack == NULL) { return; diff --git a/libs/JSystem/src/JParticle/JPAResource.cpp b/libs/JSystem/src/JParticle/JPAResource.cpp index 59e679d449..2d5d872bac 100644 --- a/libs/JSystem/src/JParticle/JPAResource.cpp +++ b/libs/JSystem/src/JParticle/JPAResource.cpp @@ -18,9 +18,21 @@ #include "global.h" #include "tracy/Tracy.hpp" +#if TARGET_PC +#define JPA_DRAW_CTX_ARG , &ctx +#else +#define JPA_DRAW_CTX_ARG +#endif + JPAResource::JPAResource() { mpCalcEmitterFuncList = mpDrawEmitterFuncList = mpDrawEmitterChildFuncList = NULL; +#if TARGET_PC + mpCalcParticleFuncList = mpCalcParticleChildFuncList = NULL; + mpDrawParticleFuncList = mpDrawParticleChildFuncList = NULL; + mBatchInfo = {}; +#else mpCalcParticleFuncList = mpDrawParticleFuncList = mpCalcParticleChildFuncList = mpDrawParticleChildFuncList = NULL; +#endif pBsp = NULL; pEsp = NULL; pCsp = NULL; @@ -61,6 +73,60 @@ static u8 jpa_crd[32] ATTRIBUTE_ALIGN(32) = { 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x02, 0x02, 0x00, 0x02, }; +#if TARGET_PC +void JPAResource::initBatchInfo() { + mBatchInfo = {}; + + bool hasDrawFunc = false; + for (int i = 0; i < mpDrawParticleFuncListNum; i++) { + DrawParticleFunc func = mpDrawParticleFuncList[i]; + if (func == JPADrawBillboard || func == JPADrawRotBillboard || + func == JPADrawYBillboard || func == JPADrawRotYBillboard || + func == JPADrawDirection || func == JPADrawRotDirection || func == JPADrawRotation) + { + hasDrawFunc = true; + } else if (func == JPADrawParticleCallBack) { + // Batchable only for emitters without a particle callback; checked per draw + } else if (func == JPALoadCalcTexCrdMtxAnm) { + mBatchInfo.hasPtclTexMtx = true; + } else if (func == JPARegistAlpha || func == JPARegistPrmAlpha || + func == JPARegistPrmAlphaEnv || func == JPARegistAlphaEnv || + func == static_cast(JPARegistEnv)) // overloaded + { + mBatchInfo.hasPtclColor = true; + } else { + // JPADrawPoint, JPADrawLine, JPADrawDBillboard, JPALoadTexAnm, + // JPASetPointSize, JPASetLineWidth + return; + } + } + if (!hasDrawFunc) { + return; + } + + // Template array offsets, same math as setPTev + int base_plane_type = (pBsp->getType() == 3 || pBsp->getType() == 7) ? + pBsp->getBasePlaneType() : 0; + int center_offset = pEsp != nullptr ? (pEsp->getScaleCenterX() + 3 * pEsp->getScaleCenterY()) * 0xC : 0x30; + const s8* pos = reinterpret_cast(jpa_pos) + center_offset + base_plane_type * 0x6C; + const s8* crd = reinterpret_cast(jpa_crd) + (pBsp->getTilingS() + 2 * pBsp->getTilingT()) * 8; + + bool cross = pBsp->getType() == 4 || pBsp->getType() == 8; + mBatchInfo.vtxCount = cross ? 8 : 4; + for (int i = 0; i < mBatchInfo.vtxCount; i++) { + int posIdx = i < 4 ? i : 72 + (i - 4); + int crdIdx = i & 3; + mBatchInfo.vtxPos[i][0] = pos[posIdx * 3 + 0]; + mBatchInfo.vtxPos[i][1] = pos[posIdx * 3 + 1]; + mBatchInfo.vtxPos[i][2] = pos[posIdx * 3 + 2]; + mBatchInfo.vtxUv[i][0] = crd[crdIdx * 2 + 0]; + mBatchInfo.vtxUv[i][1] = crd[crdIdx * 2 + 1]; + } + + mBatchInfo.supported = true; +} +#endif + void JPAResource::init(JKRHeap* heap) { BOOL is_glbl_clr_anm = pBsp->isGlblClrAnm(); BOOL is_glbl_tex_anm = pBsp->isGlblTexAnm(); @@ -525,7 +591,10 @@ void JPAResource::init(JKRHeap* heap) { if (mpDrawParticleFuncListNum != 0) { mpDrawParticleFuncList = - (ParticleFunc*)JKRAllocFromHeap(heap, mpDrawParticleFuncListNum * sizeof(ParticleFunc), alignof(ParticleFunc)); + (DrawParticleFunc*)JKRAllocFromHeap( + heap, + mpDrawParticleFuncListNum * sizeof(DrawParticleFunc), + alignof(DrawParticleFunc)); } func_no = 0; @@ -635,7 +704,10 @@ void JPAResource::init(JKRHeap* heap) { if (mpDrawParticleChildFuncListNum != 0) { mpDrawParticleChildFuncList = - (ParticleFunc*)JKRAllocFromHeap(heap, mpDrawParticleChildFuncListNum * sizeof(ParticleFunc), sizeof(EmitterFunc)); + (DrawParticleFunc*)JKRAllocFromHeap( + heap, + mpDrawParticleChildFuncListNum * sizeof(DrawParticleFunc), + alignof(DrawParticleFunc)); } func_no = 0; @@ -699,6 +771,10 @@ void JPAResource::init(JKRHeap* heap) { mpDrawParticleChildFuncList[func_no] = &JPARegistPrmAlphaEnv; func_no++; } + +#if TARGET_PC + initBatchInfo(); +#endif } bool JPAResource::calc(JPAEmitterWorkData* work, JPABaseEmitter* emtr) { @@ -808,6 +884,183 @@ void JPAResource::draw(JPAEmitterWorkData* work, JPABaseEmitter* emtr) { } } +#if TARGET_PC +static GXTevAlphaArg to_vtx_alpha_arg(GXTevAlphaArg arg) { + return arg == GX_CA_A0 ? GX_CA_RASA : arg; +} + +static void batch_set_tev_op(GXTevStageID stage) { + GXSetTevColorOp(stage, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXSetTevAlphaOp(stage, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); +} + +static void batch_setup_tev(JPAEmitterWorkData* work, bool useClr1) { + JPABaseShape* shape = work->mpRes->getBsp(); + JPAExTexShape* ets = work->mpRes->getEts(); + bool useIndirect = ets != nullptr && ets->isUseIndirect(); + + // JPAEmitterManager::draw configures both channels to pass vertex color through + GXSetNumChans(useClr1 ? 2 : 1); + + const GXTevAlphaArg* alphaArg = shape->getTevAlphaArg(); + GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR0A0); + GXSetTevAlphaIn(GX_TEVSTAGE0, to_vtx_alpha_arg(alphaArg[0]), to_vtx_alpha_arg(alphaArg[1]), + to_vtx_alpha_arg(alphaArg[2]), to_vtx_alpha_arg(alphaArg[3])); + batch_set_tev_op(GX_TEVSTAGE0); + if (!useIndirect) { + GXSetTevDirect(GX_TEVSTAGE0); + } + GXTevStageID nextStage = GX_TEVSTAGE1; + + switch (shape->getTevColorArgSel()) { + case 0: // TEXC + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_TEXC, GX_CC_ONE, GX_CC_ZERO); + break; + case 1: // C0 * TEXC + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_RASC, GX_CC_TEXC, GX_CC_ZERO); + break; + case 2: // lerp(C0, 1, TEXC) + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_RASC, GX_CC_ONE, GX_CC_TEXC, GX_CC_ZERO); + break; + case 3: // lerp(C1, C0, TEXC) = C0 * TEXC (stage 0) + C1 * (1 - TEXC) (stage 1) + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_RASC, GX_CC_TEXC, GX_CC_ZERO); + GXSetTevOrder(nextStage, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR1A1); + GXSetTevColorIn(nextStage, GX_CC_RASC, GX_CC_ZERO, GX_CC_TEXC, GX_CC_CPREV); + GXSetTevAlphaIn(nextStage, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_APREV); + batch_set_tev_op(nextStage); + GXSetTevDirect(nextStage); + nextStage = static_cast(nextStage + 1); + break; + case 4: // TEXC * C0 + C1: C0 * TEXC (stage 0), + C1 (stage 1) + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_RASC, GX_CC_TEXC, GX_CC_ZERO); + GXSetTevOrder(nextStage, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR1A1); + GXSetTevColorIn(nextStage, GX_CC_CPREV, GX_CC_ZERO, GX_CC_ZERO, GX_CC_RASC); + GXSetTevAlphaIn(nextStage, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_APREV); + batch_set_tev_op(nextStage); + GXSetTevDirect(nextStage); + nextStage = static_cast(nextStage + 1); + break; + case 5: // C0 + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_RASC); + break; + } + + if (ets != nullptr && ets->isUseSecTex()) { + // Mirrors setPTev's secondary texture stage, at the next free stage + GXTexCoordID texCoord = useIndirect ? GX_TEXCOORD2 : GX_TEXCOORD1; + GXSetTevOrder(nextStage, texCoord, GX_TEXMAP3, GX_COLOR_NULL); + GXSetTevColorIn(nextStage, GX_CC_ZERO, GX_CC_TEXC, GX_CC_CPREV, GX_CC_ZERO); + GXSetTevAlphaIn(nextStage, GX_CA_ZERO, GX_CA_TEXA, GX_CA_APREV, GX_CA_ZERO); + batch_set_tev_op(nextStage); + GXSetTevDirect(nextStage); + nextStage = static_cast(nextStage + 1); + } + + GXSetNumTevStages(nextStage); +} + +static void batch_setup_vtx_desc(bool useClr0, bool useClr1) { + static Mtx identityMtx = { + {1.0f, 0.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f, 0.0f}, + }; + + GXLoadPosMtxImm(identityMtx, GX_PNMTX0); + GXSetCurrentMtx(GX_PNMTX0); + GXClearVtxDesc(); + GXSetVtxDesc(GX_VA_POS, GX_DIRECT); + if (useClr0) { + GXSetVtxDesc(GX_VA_CLR0, GX_DIRECT); + } + if (useClr1) { + GXSetVtxDesc(GX_VA_CLR1, GX_DIRECT); + } + GXSetVtxDesc(GX_VA_TEX0, GX_DIRECT); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); + if (useClr0) { + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0); + } + if (useClr1) { + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_CLR1, GX_CLR_RGBA, GX_RGBA8, 0); + } + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0); +} + +static void batch_restore_gx(JPAEmitterWorkData* work, bool changedTev, bool changedTexMtx) { + GXClearVtxDesc(); + GXSetVtxDesc(GX_VA_POS, GX_INDEX8); + GXSetVtxDesc(GX_VA_TEX0, GX_INDEX8); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_S8, 0); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_S8, 0); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0); + GXSetCurrentMtx(GX_PNMTX0); + + if (changedTexMtx) { + GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_TEXMTX0); + } + + if (changedTev) { + GXSetNumChans(0); + work->mpRes->getBsp()->setGX(work); + work->mpRes->setPTev(); + } +} + +static bool draw_particle_batch(JPAEmitterWorkData* work) { + ZoneScoped; + + JPAResource* res = work->mpRes; + const JPAResource::BatchInfo& info = res->mBatchInfo; + if (!info.supported || work->mPrjType != 0 || work->mpEmtr->mpPtclCallBack != nullptr) { + return false; + } + + bool useClr0 = false; + bool useClr1 = false; + if (info.hasPtclColor) { + u32 colorSel = res->getBsp()->getTevColorArgSel(); + if (colorSel >= 6) { + return false; + } + useClr0 = true; + useClr1 = colorSel == 3 || colorSel == 4; + batch_setup_tev(work, useClr1); + } + + if (info.hasPtclTexMtx) { + // UVs are CPU-transformed; drop the texgen + GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY); + } + + batch_setup_vtx_desc(useClr0, useClr1); + + ParticleDrawCtx ctx{}; + ctx.batch = true; + ctx.useTexMtx = info.hasPtclTexMtx; + ctx.useClr0 = useClr0; + ctx.useClr1 = useClr1; + + bool fwdAhead = res->getBsp()->isDrawFwdAhead(); + JPANode* node = fwdAhead ? work->mpEmtr->mAlivePtclBase.getLast() : + work->mpEmtr->mAlivePtclBase.getFirst(); + + GXBegin(GX_QUADS, GX_VTXFMT1, GX_AUTO); + while (node != work->mpEmtr->mAlivePtclBase.getEnd()) { + work->mpCurNode = node; + for (int i = res->mpDrawParticleFuncListNum - 1; i >= 0; i--) { + (*res->mpDrawParticleFuncList[i])(work, node->getObject(), &ctx); + } + node = fwdAhead ? node->getPrev() : node->getNext(); + } + GXEnd(); + + batch_restore_gx(work, useClr0, info.hasPtclTexMtx); + return true; +} +#endif + void JPAResource::drawP(JPAEmitterWorkData* work) { ZoneScoped; work->mpEmtr->clearStatus(0x80); @@ -842,13 +1095,25 @@ void JPAResource::drawP(JPAEmitterWorkData* work) { (*mpDrawEmitterFuncList[i])(work); } +#if TARGET_PC + if (draw_particle_batch(work)) { + GXSetMisc(GX_MT_XF_FLUSH, 0); + if (work->mpEmtr->mpEmtrCallBack != nullptr) { + work->mpEmtr->mpEmtrCallBack->drawAfter(work->mpEmtr); + } + return; + } + + ParticleDrawCtx ctx{}; // immediate mode +#endif + if (pBsp->isDrawFwdAhead()) { JPANode* node = work->mpEmtr->mAlivePtclBase.getLast(); for (; node != work->mpEmtr->mAlivePtclBase.getEnd(); node = node->getPrev()) { work->mpCurNode = node; if (mpDrawParticleFuncList != NULL) { for (int i = mpDrawParticleFuncListNum - 1; i >= 0; i--) { - (*mpDrawParticleFuncList[i])(work, node->getObject()); + (*mpDrawParticleFuncList[i])(work, node->getObject() JPA_DRAW_CTX_ARG); } } } @@ -858,7 +1123,7 @@ void JPAResource::drawP(JPAEmitterWorkData* work) { work->mpCurNode = node; if (mpDrawParticleFuncList != NULL) { for (int i = mpDrawParticleFuncListNum - 1; i >= 0; i--) { - (*mpDrawParticleFuncList[i])(work, node->getObject()); + (*mpDrawParticleFuncList[i])(work, node->getObject() JPA_DRAW_CTX_ARG); } } } @@ -905,13 +1170,17 @@ void JPAResource::drawC(JPAEmitterWorkData* work) { (*mpDrawEmitterChildFuncList[i])(work); } +#if TARGET_PC + ParticleDrawCtx ctx{}; // immediate mode +#endif + if (pBsp->isDrawFwdAhead()) { JPANode* node = work->mpEmtr->mAlivePtclChld.getLast(); for (; node != work->mpEmtr->mAlivePtclChld.getEnd(); node = node->getPrev()) { work->mpCurNode = node; if (mpDrawParticleChildFuncList != NULL) { for (int i = mpDrawParticleChildFuncListNum - 1; i >= 0; i--) { - (*mpDrawParticleChildFuncList[i])(work, node->getObject()); + (*mpDrawParticleChildFuncList[i])(work, node->getObject() JPA_DRAW_CTX_ARG); } } } @@ -921,7 +1190,7 @@ void JPAResource::drawC(JPAEmitterWorkData* work) { work->mpCurNode = node; if (mpDrawParticleChildFuncList != NULL) { for (int i = mpDrawParticleChildFuncListNum - 1; i >= 0; i--) { - (*mpDrawParticleChildFuncList[i])(work, node->getObject()); + (*mpDrawParticleChildFuncList[i])(work, node->getObject() JPA_DRAW_CTX_ARG); } } } diff --git a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java index cc1c985193..96fc302d9e 100644 --- a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java +++ b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskActivity.java @@ -4,6 +4,7 @@ import android.app.ActionBar; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.ClipData; +import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; @@ -14,12 +15,16 @@ import android.provider.DocumentsContract; import android.provider.OpenableColumns; import android.provider.Settings; import android.util.Log; +import android.view.Display; +import android.view.Surface; +import android.view.SurfaceHolder; import android.view.View; import android.view.Window; import android.view.WindowInsets; import android.view.WindowInsetsController; import org.libsdl.app.SDLActivity; +import org.libsdl.app.SDLSurface; import java.io.File; import java.util.ArrayList; @@ -27,6 +32,7 @@ import java.util.List; public class DuskActivity extends SDLActivity { private static final String TAG = "DuskActivity"; + private static final float DEFAULT_SURFACE_FRAME_RATE = 60.0f; private static final int FOLDER_DIALOG_REQUEST_CODE = 0x4455; private static final int MANAGE_STORAGE_REQUEST_CODE = 0x4456; private static final String EXTERNAL_STORAGE_AUTHORITY = @@ -88,6 +94,11 @@ public class DuskActivity extends SDLActivity { hideSystemBars(); } + @Override + protected SDLSurface createSDLSurface(Context context) { + return new DuskSurface(context); + } + @Override protected void onResume() { super.onResume(); @@ -139,6 +150,77 @@ public class DuskActivity extends SDLActivity { }; } + public void setPreferredSurfaceFrameRate(float frameRate) { + runOnUiThread(() -> { + if (mSurface instanceof DuskSurface) { + ((DuskSurface)mSurface).setPreferredFrameRate(frameRate); + } + }); + } + + private static final class DuskSurface extends SDLSurface { + private float preferredFrameRate = DEFAULT_SURFACE_FRAME_RATE; + + DuskSurface(Context context) { + super(context); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + super.surfaceChanged(holder, format, width, height); + setTargetFrameRate(holder); + } + + void setPreferredFrameRate(float frameRate) { + preferredFrameRate = frameRate; + setTargetFrameRate(getHolder()); + } + + private void setTargetFrameRate(SurfaceHolder holder) { + if (!mIsSurfaceReady || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return; + } + + Surface surface = holder != null ? holder.getSurface() : getHolder().getSurface(); + if (surface == null || !surface.isValid()) { + return; + } + + float targetFrameRate = getMaxSupportedFrameRate(); + if (preferredFrameRate > 0.0f) { + targetFrameRate = preferredFrameRate; + } + if (targetFrameRate <= 0.0f) { + return; + } + + try { + surface.setFrameRate( + targetFrameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT); + Log.v(TAG, "Requested surface frame rate " + targetFrameRate + " fps"); + } catch (RuntimeException e) { + Log.w(TAG, "Failed to request surface frame rate", e); + } + } + + private float getMaxSupportedFrameRate() { + if (mDisplay == null) { + return 0.0f; + } + + float maxFrameRate = mDisplay.getRefreshRate(); + Display.Mode[] modes = mDisplay.getSupportedModes(); + if (modes == null) { + return maxFrameRate; + } + + for (Display.Mode mode : modes) { + maxFrameRate = Math.max(maxFrameRate, mode.getRefreshRate()); + } + return maxFrameRate; + } + } + @Override protected String[] getArguments() { Intent intent = getIntent(); diff --git a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java index bf1ca2149d..bcd8806c41 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java @@ -19,9 +19,13 @@ import android.os.*; import java.lang.Runnable; import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedList; import java.util.UUID; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice { private static final String TAG = "hidapi"; @@ -33,10 +37,19 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe private boolean mIsConnected = false; private boolean mIsChromebook = false; private boolean mIsReconnecting = false; + private boolean mHasEnabledNotifications = false; + private boolean mHasSeenInputUpdate = false; private boolean mFrozen = false; private LinkedList mOperations; GattOperation mCurrentOperation = null; private Handler mHandler; + private int mProductId = -1; + private int mReportId = 0; + private UUID mInputCharacteristic; + + private static final int D0G_BLE2_PID = 0x1106; + private static final int TRITON_BLE_PID = 0x1303; + private static final int TRANSPORT_AUTO = 0; private static final int TRANSPORT_BREDR = 1; @@ -45,10 +58,14 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000; static final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3"); - static final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); + static final UUID inputCharacteristicD0G = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); + static final UUID inputCharacteristicTriton_0x45 = UUID.fromString("100F6C7A-1735-4313-B402-38567131E5F3"); + static final UUID inputCharacteristicTriton_0x47 = UUID.fromString("100F6C7C-1735-4313-B402-38567131E5F3"); static final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3"); static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 }; + private HashMap mOutputReportChars = new HashMap(); + static class GattOperation { private enum Operation { CHR_READ, @@ -61,6 +78,7 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe byte[] mValue; BluetoothGatt mGatt; boolean mResult = true; + int mDelayMs = 0; private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) { mGatt = gatt; @@ -68,6 +86,13 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe mUuid = uuid; } + private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, int delayMs) { + mGatt = gatt; + mOp = operation; + mUuid = uuid; + mDelayMs = delayMs; + } + private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) { mGatt = gatt; mOp = operation; @@ -75,6 +100,14 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe mValue = value; } + private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value, int delayMs) { + mGatt = gatt; + mOp = operation; + mUuid = uuid; + mValue = value; + mDelayMs = delayMs; + } + public void run() { // This is executed in main thread BluetoothGattCharacteristic chr; @@ -136,6 +169,8 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe return mResult; } + public int getDelayMs() { return mDelayMs; } + private BluetoothGattCharacteristic getCharacteristic(UUID uuid) { BluetoothGattService valveService = mGatt.getService(steamControllerService); if (valveService == null) @@ -154,6 +189,10 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) { return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid); } + + static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid, int delayMs) { + return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid, delayMs); + } } HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) { @@ -166,6 +205,8 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe mHandler = new Handler(Looper.getMainLooper()); mGatt = connectGatt(); + mHasEnabledNotifications = false; + mHasSeenInputUpdate = false; // final HIDDeviceBLESteamController finalThis = this; // mHandler.postDelayed(new Runnable() { // @Override @@ -314,8 +355,45 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe Log.v(TAG, "Found Valve steam controller service " + service.getUuid()); for (BluetoothGattCharacteristic chr : service.getCharacteristics()) { - if (chr.getUuid().equals(inputCharacteristic)) { - Log.v(TAG, "Found input characteristic"); + if (chr.getUuid().equals(inputCharacteristicTriton_0x45)) { + Log.v(TAG, "Found Triton input characteristic 0x45"); + mProductId = TRITON_BLE_PID; + mReportId = 0x45; + mInputCharacteristic = chr.getUuid(); + } else if (chr.getUuid().equals(inputCharacteristicTriton_0x47)) { + Log.v(TAG, "Found Triton input characteristic 0x47"); + mProductId = TRITON_BLE_PID; + mReportId = 0x47; + mInputCharacteristic = chr.getUuid(); + } else if (chr.getUuid().equals(inputCharacteristicD0G)) { + Log.v(TAG, "Found D0G input characteristic"); + mProductId = D0G_BLE2_PID; + mReportId = 0x03; + mInputCharacteristic = chr.getUuid(); + } else { + Pattern reportPattern = Pattern.compile("100F6C([0-9A-Z]{2})", Pattern.CASE_INSENSITIVE); + Matcher matcher = reportPattern.matcher(chr.getUuid().toString()); + + if (matcher.find()) { + try { + int reportId = Integer.parseInt(matcher.group(1), 16); + + reportId -= 0x35; + if (reportId >= 0x80) { + // This is a Triton output report characteristic that we need to care about. + Log.v(TAG, "Found Triton output report 0x" + Integer.toString(reportId, 16)); + mOutputReportChars.put(reportId, chr); + } + } + catch (NumberFormatException nfe) { + Log.w(TAG, "Could not parse report characteristic " + chr.getUuid().toString() + ": " + nfe.toString()); + } + } + } + } + + for (BluetoothGattCharacteristic chr : service.getCharacteristics()) { + if (chr.getUuid().equals(mInputCharacteristic)) { // Start notifications BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); if (cccd != null) { @@ -372,21 +450,30 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe mCurrentOperation = mOperations.removeFirst(); } - // Run in main thread - mHandler.post(new Runnable() { - @Override - public void run() { - synchronized (mOperations) { - if (mCurrentOperation == null) { - Log.e(TAG, "Current operation null in executor?"); - return; - } + Runnable gattOperationRunnable = new Runnable() { + @Override + public void run() { + synchronized (mOperations) { + if (mCurrentOperation == null) { + Log.e(TAG, "Current operation null in executor?"); + return; + } - mCurrentOperation.run(); - // now wait for the GATT callback and when it comes, finish this operation + mCurrentOperation.run(); + // now wait for the GATT callback and when it comes, finish this operation + } } - } - }); + }; + + if (mCurrentOperation.getDelayMs() == 0) { + // Run in main thread + mHandler.post(gattOperationRunnable); + } + else { + // If we have a delay on this operation, wait before we post it. + mHandler.postDelayed(gattOperationRunnable, mCurrentOperation.getDelayMs()); + } + } private void queueGattOperation(GattOperation op) { @@ -397,8 +484,39 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe } private void enableNotification(UUID chrUuid) { - GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid); + // Add a 500ms delay to notification write for Amazon Fire TV devices, as otherwise if we do this too quickly after connecting + // it will return success and then silently drop the operation on the floor. + GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid, 500); queueGattOperation(op); + + // Amazon Fire devices can also silently timeout on writeDescriptor, so + // set up a little delayed check that will attempt to write a second time. + // + // While this only seems to be needed on Amazon Fire TV devices at present, it + // doesn't hurt to have a retry on other devices as well. + // + final HIDDeviceBLESteamController finalThis = this; + final UUID finalUuid = chrUuid; + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (!finalThis.mHasEnabledNotifications) { + + if (finalThis.mHasSeenInputUpdate) { + // Amazon Five devices may have enabled notifications on the input characteristic and not given us a callback. If we've seen + // input reports, though, somewhat by definition notifications are enabled. + Log.w(TAG, "WriteDescriptor has never returned, but we've seen input reports. Moving on with controller initialization."); + finalThis.mHasEnabledNotifications = true; + finalThis.enableValveMode(); + return; + } + + // Give one more try. + GattOperation retry = HIDDeviceBLESteamController.GattOperation.enableNotification(finalThis.mGatt, finalUuid, 500); + finalThis.queueGattOperation(retry); + } + } + }, 1000); } void writeCharacteristic(UUID uuid, byte[] value) { @@ -448,8 +566,16 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe mIsConnected = false; gatt.disconnect(); mGatt = connectGatt(false); - } - else { + } else { + if (getProductId() == TRITON_BLE_PID) { + // Android will not properly play well with Data Length Extensions without manually requesting a large MTU, + // and Triton controllers require DLE support. + // + // 517 is basically a "magic number" as far as Android's bluetooth code is concerned, so do not change + // this value. It is functionally "please enable data length extensions" on some Android builds. + mGatt.requestMtu(517); + } + probeService(this); } } @@ -474,7 +600,7 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe // Only register controller with the native side once it has been fully configured if (!isRegistered()) { Log.v(TAG, "Registering Steam Controller with ID: " + getId()); - mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true); + mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId); setRegistered(); } } @@ -487,7 +613,8 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe // Enable this for verbose logging of controller input reports //Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue())); - if (characteristic.getUuid().equals(inputCharacteristic) && !mFrozen) { + if (characteristic.getUuid().equals(mInputCharacteristic) && !mFrozen) { + mHasSeenInputUpdate = true; mManager.HIDDeviceInputReport(getId(), characteristic.getValue()); } } @@ -497,19 +624,36 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe //Log.v(TAG, "onDescriptorRead status=" + status); } + private void enableValveMode() + { + BluetoothGattService valveService = mGatt.getService(steamControllerService); + if (valveService == null) + return; + + BluetoothGattCharacteristic reportChr = valveService.getCharacteristic(reportCharacteristic); + if (reportChr != null) { + if (getProductId() == TRITON_BLE_PID) { + // For Triton we just mark things registered. + Log.v(TAG, "Registering Triton Steam Controller with ID: " + getId()); + mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId); + setRegistered(); + } else { + // For the original controller, we need to manually enter Valve mode. + Log.v(TAG, "Writing report characteristic to enter valve mode"); + reportChr.setValue(enterValveMode); + mGatt.writeCharacteristic(reportChr); + } + } + } + @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { BluetoothGattCharacteristic chr = descriptor.getCharacteristic(); //Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid()); - if (chr.getUuid().equals(inputCharacteristic)) { - boolean hasWrittenInputDescriptor = true; - BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic); - if (reportChr != null) { - Log.v(TAG, "Writing report characteristic to enter valve mode"); - reportChr.setValue(enterValveMode); - gatt.writeCharacteristic(reportChr); - } + if (chr.getUuid().equals(mInputCharacteristic)) { + mHasEnabledNotifications = true; + enableValveMode(); } finishCurrentGattOperation(); @@ -548,9 +692,20 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe @Override public int getProductId() { - // We don't have an easy way to query from the Bluetooth device, but we know what it is - final int D0G_BLE2_PID = 0x1106; - return D0G_BLE2_PID; + if (mProductId > 0) { + // We've already set a product ID. + return mProductId; + } + + if (mDevice.getName().startsWith("Steam Ctrl")) { + // We're a newer Triton device + mProductId = TRITON_BLE_PID; + } else { + // We're an OG Steam Controller + mProductId = D0G_BLE2_PID; + } + + return mProductId; } @Override @@ -601,10 +756,29 @@ class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDe writeCharacteristic(reportCharacteristic, actual_report); return report.length; } else { - //Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report)); - writeCharacteristic(reportCharacteristic, report); - return report.length; + // If we're an original-recipe Steam Controller we just write to the characteristic directly. + if (getProductId() == D0G_BLE2_PID) { + //Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report)); + writeCharacteristic(reportCharacteristic, report); + return report.length; + } + + // If we're a Triton, we need to find the correct report characteristic. + if (report.length > 0) { + int reportId = report[0] & 0xFF; + BluetoothGattCharacteristic targetedReportCharacteristic = mOutputReportChars.get(reportId); + if (targetedReportCharacteristic != null) { + byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1); + //Log.v(TAG, "writeOutputReport 0x" + Integer.toString(reportId, 16) + " " + HexDump.dumpHexString(report)); + writeCharacteristic(targetedReportCharacteristic.getUuid(), actual_report); + return report.length; + } else { + Log.w(TAG, "Got report write request for unknown report type 0x" + Integer.toString(reportId, 16)); + } + } } + + return -1; } @Override diff --git a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java index 1fb2bfb4a7..691416c1c9 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java @@ -256,6 +256,7 @@ public class HIDDeviceManager { 0x24c6, // PowerA 0x2c22, // Qanba 0x2dc8, // 8BitDo + 0x3537, // GameSir 0x37d7, // Flydigi 0x9886, // ASTRO Gaming }; @@ -360,7 +361,7 @@ public class HIDDeviceManager { HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index); int id = device.getId(); mDevicesById.put(id, device); - HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol(), false); + HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol(), false, 0); } } } @@ -529,7 +530,13 @@ public class HIDDeviceManager { return false; } - return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0); + // Steam Controllers will always support Bluetooth Low Energy + if ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) == 0) { + return false; + } + + // Match on the name either the original Steam Controller or the new second-generation one advertise with. + return bluetoothDevice.getName().equals("SteamController") || bluetoothDevice.getName().startsWith("Steam Ctrl"); } private void close() { @@ -681,7 +688,7 @@ public class HIDDeviceManager { private native void HIDDeviceRegisterCallback(); private native void HIDDeviceReleaseCallback(); - native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol, boolean bBluetooth); + native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol, boolean bBluetooth, int reportID); native void HIDDeviceOpenPending(int deviceID); native void HIDDeviceOpenResult(int deviceID, boolean opened); native void HIDDeviceDisconnected(int deviceID); diff --git a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java index f9e9389802..8954639733 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java @@ -21,6 +21,7 @@ class HIDDeviceUSB implements HIDDevice { protected InputThread mInputThread; protected boolean mRunning; protected boolean mFrozen; + protected boolean mClaimed; public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) { mManager = manager; @@ -29,6 +30,7 @@ class HIDDeviceUSB implements HIDDevice { mInterface = mDevice.getInterface(mInterfaceIndex).getId(); mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier()); mRunning = false; + mClaimed = false; } String getIdentifier() { @@ -114,6 +116,7 @@ class HIDDeviceUSB implements HIDDevice { close(); return false; } + mClaimed = true; // Find the endpoints for (int j = 0; j < iface.getEndpointCount(); j++) { @@ -132,9 +135,12 @@ class HIDDeviceUSB implements HIDDevice { } } - // Make sure the required endpoints were present - if (mInputEndpoint == null || mOutputEndpoint == null) { + // Make sure the required endpoints were present. The original Steam Controller and the wireless dongle for it do NOT + // actually have -- or require -- output endpoints, so we need to accept only an input one for them or else we'll fall + // back to the Android system gamepad functionality (and lose our paddles et al). + if (mInputEndpoint == null) { Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName()); + mConnection.releaseInterface(iface); close(); return false; } @@ -154,6 +160,11 @@ class HIDDeviceUSB implements HIDDevice { return -1; } + if (!mClaimed) { + Log.w(TAG, "writeReport() called but some other process currently owns the USB device"); + return -1; + } + if (feature) { int res = -1; int offset = 0; @@ -185,6 +196,11 @@ class HIDDeviceUSB implements HIDDevice { } return length; } else { + if (mOutputEndpoint == null) + { + Log.e(TAG, "Tried to write an output report to an interface with no output endpoint!"); + return -1; + } int res = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000); if (res != report.length) { Log.w(TAG, "writeOutputReport() returned " + res + " on device " + getDeviceName()); @@ -205,6 +221,12 @@ class HIDDeviceUSB implements HIDDevice { Log.w(TAG, "readReport() called with no device connection"); return false; } + if (!mClaimed) { + if (feature) { + return false; + } + return true; + } if (report_number == 0x0) { /* Offset the return buffer by 1, so that the report ID @@ -258,10 +280,13 @@ class HIDDeviceUSB implements HIDDevice { mInputThread = null; } if (mConnection != null) { - UsbInterface iface = mDevice.getInterface(mInterfaceIndex); - mConnection.releaseInterface(iface); + if (mClaimed) { + UsbInterface iface = mDevice.getInterface(mInterfaceIndex); + mConnection.releaseInterface(iface); + } mConnection.close(); mConnection = null; + mClaimed = false; } } @@ -274,6 +299,22 @@ class HIDDeviceUSB implements HIDDevice { @Override public void setFrozen(boolean frozen) { mFrozen = frozen; + + /* If we have a valid device connection and the claim state doesn't match what we want, try to correct that. */ + if (mConnection != null && mClaimed == mFrozen) { + UsbInterface iface = mDevice.getInterface(mInterfaceIndex); + if (frozen) { + mClaimed = !mConnection.releaseInterface(iface); + if (mClaimed) { + Log.e(TAG, "Tried to release claim on USB device, but failed!"); + } + } else { + mClaimed = mConnection.claimInterface(iface, true); + if (!mClaimed) { + Log.e(TAG, "Tried to regain claim on USB device, but failed!"); + } + } + } } protected class InputThread extends Thread { diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java index 5c54cde863..dcc49852ac 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -61,7 +61,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh private static final String TAG = "SDL"; private static final int SDL_MAJOR_VERSION = 3; private static final int SDL_MINOR_VERSION = 4; - private static final int SDL_MICRO_VERSION = 8; + private static final int SDL_MICRO_VERSION = 10; /* // Display InputType.SOURCE/CLASS of events and devices // @@ -530,7 +530,8 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh if (mHIDDeviceManager != null) { mHIDDeviceManager.setFrozen(true); - } + } + if (!mHasMultiWindow) { pauseNativeThread(); } @@ -543,7 +544,8 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh if (mHIDDeviceManager != null) { mHIDDeviceManager.setFrozen(false); - } + } + if (!mHasMultiWindow) { resumeNativeThread(); } @@ -616,6 +618,14 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh super.onWindowFocusChanged(hasFocus); Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); + // If we are gaining focus, we can always try to restore our USB devices. If we are losing focus, + // only try to relinquish them if we don't have background events allowed (for multi-window Android setups). + if (hasFocus || !SDLActivity.nativeGetHintBoolean("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", false)) { + if (mHIDDeviceManager != null) { + mHIDDeviceManager.setFrozen(!hasFocus); + } + } + if (SDLActivity.mBrokenLibraries) { return; } @@ -1481,11 +1491,11 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) { // Note that we process events with specific key codes here if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (SDLControllerManager.onNativePadDown(deviceId, keyCode)) { + if (SDLControllerManager.onNativePadDown(deviceId, keyCode, event.getScanCode())) { return true; } } else if (event.getAction() == KeyEvent.ACTION_UP) { - if (SDLControllerManager.onNativePadUp(deviceId, keyCode)) { + if (SDLControllerManager.onNativePadUp(deviceId, keyCode, event.getScanCode())) { return true; } } @@ -1963,7 +1973,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(url)); - int flags = Intent.FLAG_ACTIVITY_NO_HISTORY + int flags = Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT; i.addFlags(flags); @@ -2227,3 +2237,4 @@ class SDLClipboardHandler implements SDLActivity.onNativeClipboardChanged(); } } + diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java index 7655ecfd6f..8681d050b7 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLControllerManager.java @@ -10,6 +10,10 @@ import android.hardware.lights.Light; import android.hardware.lights.LightsRequest; import android.hardware.lights.LightsManager; import android.hardware.lights.LightState; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; import android.graphics.Color; import android.os.Build; import android.os.VibrationEffect; @@ -30,16 +34,18 @@ public class SDLControllerManager static native void nativeAddJoystick(int device_id, String name, String desc, int vendor_id, int product_id, int button_mask, - int naxes, int axis_mask, int nhats, boolean can_rumble, boolean has_rgb_led); + int naxes, int axis_mask, int nhats, boolean can_rumble, boolean has_rgb_led, + boolean has_accelerometer, boolean has_gyroscope); static native void nativeRemoveJoystick(int device_id); static native void nativeAddHaptic(int device_id, String name); static native void nativeRemoveHaptic(int device_id); - static public native boolean onNativePadDown(int device_id, int keycode); - static public native boolean onNativePadUp(int device_id, int keycode); + static public native boolean onNativePadDown(int device_id, int keycode, int scancode); + static public native boolean onNativePadUp(int device_id, int keycode, int scancode); static native void onNativeJoy(int device_id, int axis, float value); static native void onNativeHat(int device_id, int hat_id, int x, int y); + static native void onNativeJoySensor(int device_id, int sensor_type, long sensor_timestamp, float x, float y, float z); protected static SDLJoystickHandler mJoystickHandler; protected static SDLHapticHandler mHapticHandler; @@ -81,6 +87,13 @@ public class SDLControllerManager mJoystickHandler.setLED(device_id, red, green, blue); } + /** + * This method is called by SDL using JNI. + */ + static void joystickSetSensorsEnabled(int device_id, boolean enabled) { + mJoystickHandler.setSensorsEnabled(device_id, enabled); + } + /** * This method is called by SDL using JNI. */ @@ -153,6 +166,10 @@ class SDLJoystickHandler { ArrayList hats; ArrayList lights; LightsManager.LightsSession lightsSession; + SensorManager sensorManager; + SDLJoySensorListener sensorListener; + Sensor accelerometerSensor; + Sensor gyroscopeSensor; } static class RangeComparator implements Comparator { @Override @@ -225,12 +242,13 @@ class SDLJoystickHandler { joystick.desc = getJoystickDescriptor(joystickDevice); joystick.axes = new ArrayList(); joystick.hats = new ArrayList(); + java.util.Set axisStrsSet = new java.util.HashSet(); joystick.lights = new ArrayList(); List ranges = joystickDevice.getMotionRanges(); Collections.sort(ranges, new RangeComparator()); for (InputDevice.MotionRange range : ranges) { - if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { + if (((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) && axisStrsSet.add(range.getAxis())) { if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) { joystick.hats.add(range); } else { @@ -241,6 +259,8 @@ class SDLJoystickHandler { boolean can_rumble = false; boolean has_rgb_led = false; + boolean has_accelerometer = false; + boolean has_gyroscope = false; if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) { VibratorManager vibratorManager = joystickDevice.getVibratorManager(); int[] vibrators = vibratorManager.getVibratorIds(); @@ -258,12 +278,26 @@ class SDLJoystickHandler { joystick.lightsSession = lightsManager.openSession(); has_rgb_led = true; } + SensorManager sensorManager = joystickDevice.getSensorManager(); + if (sensorManager != null) { + joystick.sensorManager = sensorManager; + joystick.sensorListener = new SDLJoySensorListener(joystick.device_id); + joystick.accelerometerSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + if (joystick.accelerometerSensor != null) { + has_accelerometer = true; + } + joystick.gyroscopeSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); + if (joystick.gyroscopeSensor != null) { + has_gyroscope = true; + } + } } mJoysticks.add(joystick); SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc, getVendorId(joystickDevice), getProductId(joystickDevice), - getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, can_rumble, has_rgb_led); + getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, can_rumble, has_rgb_led, + has_accelerometer, has_gyroscope); } } } @@ -508,6 +542,31 @@ class SDLJoystickHandler { } joystick.lightsSession.requestLights(lightsRequest.build()); } + + void setSensorsEnabled(int device_id, boolean enabled) { + if (Build.VERSION.SDK_INT < 31 /* Android 12.0 (S) */) { + return; + } + SDLJoystick joystick = getJoystick(device_id); + if (joystick == null || joystick.sensorManager == null) { + return; + } + if (enabled) { + if (joystick.accelerometerSensor != null) { + SDLSensorManager.registerListener(joystick.sensorManager, joystick.sensorListener, joystick.accelerometerSensor, SensorManager.SENSOR_DELAY_GAME); + } + if (joystick.gyroscopeSensor != null) { + SDLSensorManager.registerListener(joystick.sensorManager, joystick.sensorListener, joystick.gyroscopeSensor, SensorManager.SENSOR_DELAY_GAME); + } + } else { + if (joystick.accelerometerSensor != null) { + SDLSensorManager.unregisterListener(joystick.sensorManager, joystick.sensorListener, joystick.accelerometerSensor); + } + if (joystick.gyroscopeSensor != null) { + SDLSensorManager.unregisterListener(joystick.sensorManager, joystick.sensorListener, joystick.gyroscopeSensor); + } + } + } } class SDLHapticHandler_API31 extends SDLHapticHandler { @@ -933,3 +992,19 @@ class SDLGenericMotionListener_API29 extends SDLGenericMotionListener_API26 { return penDevice.isExternal() ? SDL_PEN_DEVICE_TYPE_INDIRECT : SDL_PEN_DEVICE_TYPE_DIRECT; } } + +class SDLJoySensorListener implements SensorEventListener { + int device_id; + + public SDLJoySensorListener(int device_id) { + this.device_id = device_id; + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) {} + + @Override + public void onSensorChanged(SensorEvent event) { + SDLControllerManager.onNativeJoySensor(device_id, event.sensor.getType(), event.timestamp, event.values[0], event.values[1], event.values[2]); + } +} diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLSensorManager.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLSensorManager.java new file mode 100644 index 0000000000..586e3fab6e --- /dev/null +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLSensorManager.java @@ -0,0 +1,32 @@ +package org.libsdl.app; + +import android.hardware.Sensor; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; + +// This class coordinates synchronized access to sensor manager registration +// +// This prevents a java.util.ConcurrentModificationException exception on +// Android 16, specifically on the Samsung Tab S9 Ultra. + +class SDLSensorManager +{ + static private SDLSensorManager mManager = new SDLSensorManager(); + + public static void registerListener(SensorManager manager, SensorEventListener listener, Sensor sensor, int samplingPeriodUs) { + mManager.RegisterListener(manager, listener, sensor, samplingPeriodUs); + } + + public static void unregisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor) { + mManager.UnregisterListener(manager, listener, sensor); + } + + private synchronized void RegisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor, int samplingPeriodUs) { + manager.registerListener(listener, sensor, samplingPeriodUs, null); + } + + private synchronized void UnregisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor) { + manager.unregisterListener(listener, sensor); + } +} + diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java index 5ed335ac39..8d56658958 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java @@ -47,6 +47,9 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, // Is SurfaceView ready for rendering protected boolean mIsSurfaceReady; + // Is on-screen keyboard visible + protected boolean mKeyboardVisible; + // Pinch events private final ScaleGestureDetector scaleGestureDetector; @@ -213,6 +216,18 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, WindowInsets.Type.displayCutout()); SDLActivity.onNativeInsetsChanged(combined.left, combined.right, combined.top, combined.bottom); + + if (insets.isVisible(WindowInsets.Type.ime())) { + if (!mKeyboardVisible) { + mKeyboardVisible = true; + SDLActivity.onNativeScreenKeyboardShown(); + } + } else { + if (mKeyboardVisible) { + mKeyboardVisible = false; + SDLActivity.onNativeScreenKeyboardHidden(); + } + } } // Pass these to any child views in case they need them @@ -318,11 +333,11 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback, protected void enableSensor(int sensortype, boolean enabled) { // TODO: This uses getDefaultSensor - what if we have >1 accels? if (enabled) { - mSensorManager.registerListener(this, + SDLSensorManager.registerListener(mSensorManager, this, mSensorManager.getDefaultSensor(sensortype), - SensorManager.SENSOR_DELAY_GAME, null); + SensorManager.SENSOR_DELAY_GAME); } else { - mSensorManager.unregisterListener(this, + SDLSensorManager.unregisterListener(mSensorManager, this, mSensorManager.getDefaultSensor(sensortype)); } } diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss index 8927e1a01c..9a86843295 100644 --- a/res/rml/overlay.rcss +++ b/res/rml/overlay.rcss @@ -16,7 +16,7 @@ body { flex-direction: column; justify-content: flex-end; align-items: stretch; - z-index: 1; + z-index: 2; pointer-events: none; } diff --git a/res/rml/touch_controls.rcss b/res/rml/touch_controls.rcss new file mode 100644 index 0000000000..4c8057d6c0 --- /dev/null +++ b/res/rml/touch_controls.rcss @@ -0,0 +1,339 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; + font-family: "Fira Sans Condensed"; + font-weight: bold; + color: rgba(248, 244, 232, 90%); + z-index: 1; + filter: opacity(0); + transition: filter 0.2s linear-in-out; +} + +body[open] { + filter: opacity(1); +} + +body:not([open]) { + pointer-events: none; +} + +button { + display: flex; + align-items: center; + 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%); + 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; +} + +button.pressed, +button.active { + background-color: rgba(63, 78, 90, 68%); + border-color: rgba(255, 255, 255, 48%); + filter: brightness(1.18); +} + +button:hidden { + opacity: 0; + pointer-events: none; +} + +button span { + display: block; + line-height: 1; +} + +button icon { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +button icon glyph { + display: block; + font-family: "Material Symbols Rounded"; + font-weight: normal; + font-size: 24dp; + line-height: 1; +} + +.midna-icon, +.item-icon, +.item-count, +.oil-meter { + display: none; +} + +.midna-icon.visible, +.item-icon.visible, +.item-count.visible, +.oil-meter.visible { + display: block; +} + +.control { + position: absolute; +} + +.trigger-l.active { + background-color: rgba(57, 116, 133, 74%); + border-color: rgba(128, 222, 234, 72%); +} + +.trigger, +.skip { + border-radius: 23dp; +} + +.trigger { + font-size: 22dp; +} + +.button-z { + background-color: rgba(118, 79, 158, 58%); + border-color: rgba(203, 170, 255, 36%); +} + +.midna-icon { + position: absolute; + left: 9dp; + top: -1dp; + height: 48dp; +} + +.button-z.has-icon span, +.face.has-item span { + position: absolute; + font-size: 13dp; + line-height: 1; +} + +.button-z.has-icon span { + right: 9dp; + 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-radius: 23dp; + background-color: rgba(22, 24, 28, 48%); + /* backdrop-filter: blur(7dp); */ + /* box-shadow: 0 -6dp 18dp rgba(0, 0, 0, 28%); */ + overflow: hidden; + opacity: 1; + transform-origin: center; + transition: opacity 0.2s linear-in-out; +} + +action-bar:hidden, +action-bar:hidden button, +action-bar:hidden separator { + opacity: 0; + pointer-events: none; +} + +.utility { + position: relative; + flex: 1 1 auto; + width: 56dp; + height: 44dp; + margin: 0; + border-width: 0dp; + border-radius: 0; + background-color: transparent; + box-shadow: none; +} + +.utility, +.skip { + opacity: 0.55; +} + +.utility.pressed { + background-color: rgba(63, 78, 90, 68%); +} + +.utility.pressed, +.skip.pressed { + opacity: 1; +} + +.skip { + z-index: 1; + border-color: rgba(255, 255, 255, 36%); +} + +separator { + display: block; + flex: 0 0 1dp; + width: 1dp; + height: 24dp; + background-color: rgba(255, 255, 255, 18%); + opacity: 1; + transition: opacity 0.2s linear-in-out; +} + +.face { + position: absolute; + border-radius: 29dp; + font-size: 24dp; + overflow: visible; +} + +.item-icon { + width: auto; + height: auto; + max-width: 76%; + max-height: 76%; +} + +.item-count { + position: absolute; + left: 6dp; + bottom: 5dp; + 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; + line-height: 13dp; + text-align: center; +} + +.oil-meter { + position: absolute; + left: 12dp; + 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%); */ +} + +oil-fill { + display: block; + width: 0%; + height: 100%; + border-radius: 2dp; + background-color: rgb(255, 232, 74); +} + +.face.has-item span { + right: 6dp; + bottom: 6dp; + color: rgba(255, 255, 255, 88%); +} + +.face.a { + border-radius: 37dp; + font-size: 31dp; + background-color: rgba(34, 112, 123, 62%); +} + +.face.b { + background-color: rgba(161, 61, 66, 58%); +} + +.face.x { + background-color: rgba(83, 115, 151, 56%); +} + +.face.y { + background-color: rgba(113, 91, 150, 54%); +} + +button.control.docked-top, +action-bar.docked-top { + border-top-width: 0dp; + border-top-left-radius: 0dp; + border-top-right-radius: 0dp; +} + +button.control.docked-bottom, +action-bar.docked-bottom { + border-bottom-width: 0dp; + border-bottom-left-radius: 0dp; + border-bottom-right-radius: 0dp; +} + +button.control.docked-left, +action-bar.docked-left { + border-left-width: 0dp; + border-top-left-radius: 0dp; + border-bottom-left-radius: 0dp; +} + +button.control.docked-right, +action-bar.docked-right { + border-right-width: 0dp; + border-top-right-radius: 0dp; + border-bottom-right-radius: 0dp; +} + +touch-stick { + display: block; + position: absolute; + 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%); */ + opacity: 0; + pointer-events: none; + transition: opacity 0.18s linear-in-out; +} + +touch-stick.active { + opacity: 1; +} + +stick-ring { + position: absolute; + left: 18dp; + top: 18dp; + width: 88dp; + height: 88dp; + border-radius: 44dp; + border: 1dp rgba(255, 255, 255, 18%); +} + +stick-knob { + position: absolute; + width: 48dp; + height: 48dp; + border-radius: 24dp; + background-color: rgba(238, 236, 226, 55%); + border: 1dp rgba(255, 255, 255, 45%); +} diff --git a/res/rml/touch_controls_editor.rcss b/res/rml/touch_controls_editor.rcss new file mode 100644 index 0000000000..2ca99d935a --- /dev/null +++ b/res/rml/touch_controls_editor.rcss @@ -0,0 +1,138 @@ +body.touch-editor { + background-color: rgba(4, 6, 8, 34%); + z-index: 8; +} + +body.touch-editor .control, +body.touch-editor action-bar { + opacity: 0.88; + cursor: move; + pointer-events: auto; +} + +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%); + filter: brightness(1.15); +} + +body.touch-editor action-bar button, +body.touch-editor action-bar separator { + pointer-events: none; +} + +selection-frame { + display: none; + position: absolute; + z-index: 20; + border: 2dp rgba(255, 232, 128, 88%); + background-color: rgba(255, 232, 128, 7%); + pointer-events: none; +} + +selection-frame.visible { + display: block; +} + +resize-handle { + display: block; + position: absolute; + width: 22dp; + height: 22dp; + border: 2dp rgba(255, 244, 190, 96%); + border-radius: 11dp; + background-color: rgba(34, 37, 42, 86%); + pointer-events: auto; +} + +resize-handle.left { + left: -12dp; +} + +resize-handle.right { + right: -12dp; +} + +resize-handle.top { + top: -12dp; +} + +resize-handle.bottom { + bottom: -12dp; +} + +resize-handle.horizontal { + top: 50%; + margin-top: -11dp; +} + +resize-handle.vertical { + left: 50%; + margin-left: -11dp; +} + +resize-handle.corner.left { + left: -12dp; +} + +resize-handle.corner.right { + right: -12dp; +} + +resize-handle.corner.top { + top: -12dp; +} + +resize-handle.corner.bottom { + bottom: -12dp; +} + +editor-toolbar { + display: flex; + position: absolute; + left: 24dp; + right: 24dp; + top: 50%; + z-index: 30; + height: 48dp; + margin-top: -24dp; + gap: 8dp; + justify-content: center; + pointer-events: auto; +} + +editor-toolbar button.editor-command { + flex: 0 1 150dp; + 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; + line-height: 48dp; + opacity: 1; + cursor: pointer; +} + +editor-toolbar button.editor-command span { + display: block; + width: 100%; + line-height: 48dp; + text-align: center; +} + +editor-toolbar button.editor-command.primary { + border-color: rgba(255, 232, 128, 70%); + background-color: 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%); +} diff --git a/src/d/actor/d_a_alink_dusk.cpp b/src/d/actor/d_a_alink_dusk.cpp index f42f5b4e08..41d365c005 100644 --- a/src/d/actor/d_a_alink_dusk.cpp +++ b/src/d/actor/d_a_alink_dusk.cpp @@ -181,3 +181,13 @@ bool daAlink_c::checkAimContext() { return false; } } + +bool daAlink_c::checkAimInputContext() { + switch (mProcID) { + case PROC_HOOKSHOT_ROOF_WAIT: + case PROC_HOOKSHOT_WALL_WAIT: + return false; + default: + return checkAimContext(); + } +} diff --git a/src/d/actor/d_a_alink_link.inc b/src/d/actor/d_a_alink_link.inc index c2320d3f73..4771a3c4e8 100644 --- a/src/d/actor/d_a_alink_link.inc +++ b/src/d/actor/d_a_alink_link.inc @@ -14,6 +14,7 @@ #include "dusk/action_bindings.h" #include "dusk/gyro.h" #include "dusk/mouse.h" +#include "dusk/touch_camera.h" #endif bool daAlink_c::checkNoSubjectModeCamera() { @@ -122,7 +123,7 @@ BOOL daAlink_c::setBodyAngleToCamera() { } #if TARGET_PC - if (dusk::getSettings().game.enableMouseAim && checkAimContext()) { + if (dusk::getSettings().game.enableMouseAim && checkAimInputContext()) { sp8 = mBodyAngle.x; } else #endif @@ -141,7 +142,7 @@ BOOL daAlink_c::setBodyAngleToCamera() { #if TARGET_PC if ((dusk::getSettings().game.enableGyroAim || dusk::getSettings().game.enableMouseAim) && - checkAimContext()) + checkAimInputContext()) { f32 gyro_scale = 1.0f; if (checkWolfEyeUp()) { @@ -172,6 +173,32 @@ BOOL daAlink_c::setBodyAngleToCamera() { sp8 = mBodyAngle.x; } } + + if (dusk::getSettings().game.enableTouchControls && checkAimInputContext()) { + f32 touchYawDp = 0.0f; + f32 touchPitchDp = 0.0f; + if (dusk::touch_camera::consume_delta(touchYawDp, touchPitchDp)) { + f32 scale = 1.0f; + if (checkWolfEyeUp()) { + scale *= 0.6f; + } + if (dComIfGp_checkPlayerStatus0(0, 0x200000)) { + scale /= dComIfGp_getCameraZoomScale(field_0x317c); + } + + const f32 yawDeg = -touchYawDp * dusk::touch_camera::YAW_DEGREES_PER_DP * scale * + dusk::getSettings().game.touchCameraXSensitivity; + const f32 pitchDeg = touchPitchDp * dusk::touch_camera::PITCH_DEGREES_PER_DP * + scale * dusk::getSettings().game.touchCameraYSensitivity; + + shape_angle.y = shape_angle.y + cM_deg2s(yawDeg); + sp8 = sp8 + cM_deg2s(pitchDeg); + + if (checkNotItemSinkLimit() && sp8 > 0 && sp8 > mBodyAngle.x) { + sp8 = mBodyAngle.x; + } + } + } #endif if (checkNotItemSinkLimit() && sp8 > 0) { diff --git a/src/d/actor/d_a_npc_ks.cpp b/src/d/actor/d_a_npc_ks.cpp index 71eeb16335..222827df63 100644 --- a/src/d/actor/d_a_npc_ks.cpp +++ b/src/d/actor/d_a_npc_ks.cpp @@ -6882,6 +6882,16 @@ static int daNpc_Ks_Delete(npc_ks_class* i_this) { i_this->model->stopZelAnime(); } +#if TARGET_PC + if (leader == i_this) { + leader = NULL; + } + + if (saru_p[i_this->set_id] == i_this) { + saru_p[i_this->set_id] = NULL; + } +#endif + return 1; } diff --git a/src/d/actor/d_flower.inc b/src/d/actor/d_flower.inc index e5f055fb16..54b1e022cc 100644 --- a/src/d/actor/d_flower.inc +++ b/src/d/actor/d_flower.inc @@ -15,6 +15,12 @@ const u16 l_J_Ohana00_64TEX__height = 63; using GameVersion = dusk::version::GameVersion; static u8* l_J_Ohana00_64TEX_get() { static u8 buf[0x800]; static bool _ = (dusk::LoadArchivedRelAsset(buf, 'AMEM', "d_a_grass.rel", {{GameVersion::GcnUsa, 0x9060}, {GameVersion::GcnPal, 0x9060}}, 0x800), true); return buf; } #define l_J_Ohana00_64TEX (l_J_Ohana00_64TEX_get()) + +// from d_grass.inc +static MtxP get_model_mtx(Mtx modelMtx, Mtx storage); +static void transform_positions( + const dusk::batch::LeafTemplate& tpl, const Vec* posArray, const Mtx mtx, Vec* xfPos); +static void split_batch(u32& emitted, u32 vtxCount); #else #include "assets/l_J_Ohana00_64TEX.h" #endif @@ -588,6 +594,12 @@ dFlower_packet_c::dFlower_packet_c() { GXInitTexObj(&mTexObj_l_J_Ohana01_64128_0419TEX, l_J_Ohana01_64128_0419TEX, l_J_Ohana01_64128_0419TEX__width + 1, l_J_Ohana01_64128_0419TEX__height + 1, GX_TF_CMPR, GX_MIRROR, GX_MIRROR, GX_FALSE ); + + dusk::batch::decode_leaf_template(l_J_hana00DL, 0x140, mTplHana00); + dusk::batch::decode_leaf_template(l_J_hana00_cDL, 0xC0, mTplHana00Cut); + dusk::batch::decode_leaf_template(l_J_hana01DL, 0x120, mTplHana01); + dusk::batch::decode_leaf_template(l_J_hana01_c_00DL, 0xC0, mTplHana01Cut00); + dusk::batch::decode_leaf_template(l_J_hana01_c_01DL, 0x120, mTplHana01Cut); #endif m_deleteRoom = &dFlower_packet_c::deleteRoom; @@ -597,6 +609,371 @@ dFlower_packet_c::dFlower_packet_c() { #endif } +#if TARGET_PC +static void batch_setup_tev(u32 lightMask) { + GXSetCullMode(GX_CULL_NONE); + + GXSetNumChans(2); + GXSetChanCtrl(GX_COLOR0, GX_FALSE, GX_SRC_REG, GX_SRC_VTX, 0, GX_DF_NONE, GX_AF_NONE); + GXSetChanCtrl(GX_COLOR1, GX_TRUE, GX_SRC_VTX, GX_SRC_REG, lightMask, GX_DF_CLAMP, GX_AF_SPOT); + + GXSetNumTevStages(3); + + GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR1A1); + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_RASC); + GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO); + GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + + GXSetTevOrder(GX_TEVSTAGE1, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR0A0); + GXSetTevColorIn(GX_TEVSTAGE1, GX_CC_ZERO, GX_CC_CPREV, GX_CC_RASC, GX_CC_ZERO); + GXSetTevColorOp(GX_TEVSTAGE1, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXSetTevAlphaIn(GX_TEVSTAGE1, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO); + GXSetTevAlphaOp(GX_TEVSTAGE1, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + + GXSetTevOrder(GX_TEVSTAGE2, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR_NULL); + GXSetTevColorIn(GX_TEVSTAGE2, GX_CC_ZERO, GX_CC_TEXC, GX_CC_CPREV, GX_CC_C0); + GXSetTevColorOp(GX_TEVSTAGE2, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_4, GX_TRUE, GX_TEVPREV); + GXSetTevAlphaIn(GX_TEVSTAGE2, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_TEXA); + GXSetTevAlphaOp(GX_TEVSTAGE2, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); +} + +static GXColor hana00_amb_color(const dFlower_data_c* flower, const dKy_tevstr_c* tevstr) { + GXColor amb = {0, 0, 0, 0xFF}; + if (DEBUG && g_kankyoHIO.navy.grass_adjust_ON != 0) { + amb.r = g_kankyoHIO.navy.grass_ambcol.r * 2; + amb.g = g_kankyoHIO.navy.grass_ambcol.g * 2; + amb.b = g_kankyoHIO.navy.grass_ambcol.b * 2; + } else { + amb.r = (flower->field_0x04 & 0x1F) * 2; + amb.g = ((flower->field_0x04 >> 5) & 0x1F) * 2; + amb.b = ((flower->field_0x04 >> 0xA) & 0x1F) * 2; + } + + if (daPy_py_c::checkNowWolfPowerUp()) { + f32 ambRate = g_env_light.bg_amb_col[0].r / 255.0f; + f32 col = (((flower->field_0x04 & 0x1F) * 2 + 0x10)); + amb.r = col * (ambRate * 4.0f); + + ambRate = g_env_light.bg_amb_col[0].g / 255.0f; + f32 col2 = (((flower->field_0x04 >> 5) & 0x1F) * 2 + 0x10); + amb.g = col2 * (4.0f * ambRate); + + ambRate = g_env_light.bg_amb_col[0].b / 255.0f; + f32 col3 = (((flower->field_0x04 >> 10) & 0x1F) * 2 + 0x10); + amb.b = col3 * (4.0f * ambRate); + } + + if (amb.r == 0x3E) { + amb.r = tevstr->AmbCol.r; + } + + if (amb.g == 0x3E) { + amb.g = tevstr->AmbCol.g; + } + + if (amb.b == 0x3E) { + amb.b = tevstr->AmbCol.b; + } + + return amb; +} + +static GXColor hana01_amb_color(int idx, const dKy_tevstr_c* tevstr) { + f32 rRate = tevstr->AmbCol.r * 0.03125f; + if (rRate > 1.0f) { + rRate = 1.0f; + } + + f32 gRate = tevstr->AmbCol.g * 0.03125f; + if (gRate > 1.0f) { + gRate = 1.0f; + } + + f32 bRate = tevstr->AmbCol.b * 0.03125f; + if (bRate > 1.0f) { + bRate = 1.0f; + } + + GXColor amb = {1, 1, 1, 1}; + + GXColor sub; + sub.r = -0.4f * tevstr->AmbCol.r * rRate; + sub.g = -0.4f * tevstr->AmbCol.g * gRate; + sub.b = -0.4f * tevstr->AmbCol.b * bRate; + + switch (idx & 7) { + case 0: + amb.r = tevstr->AmbCol.r + sub.r; + amb.g = tevstr->AmbCol.g; + amb.b = tevstr->AmbCol.b; + break; + case 1: + amb.r = tevstr->AmbCol.r; + amb.g = tevstr->AmbCol.g + sub.g; + amb.b = tevstr->AmbCol.b; + break; + case 2: + amb.r = tevstr->AmbCol.r; + amb.g = tevstr->AmbCol.g; + amb.b = tevstr->AmbCol.b + sub.b; + break; + case 3: + amb.r = tevstr->AmbCol.r + sub.r; + amb.g = tevstr->AmbCol.g + sub.g; + amb.b = tevstr->AmbCol.b; + break; + case 4: + amb.r = tevstr->AmbCol.r; + amb.g = tevstr->AmbCol.g + sub.g; + amb.b = tevstr->AmbCol.b + sub.b; + break; + case 5: + amb.r = tevstr->AmbCol.r + sub.r; + amb.g = tevstr->AmbCol.g; + amb.b = tevstr->AmbCol.b + sub.b; + break; + case 6: + amb.r = tevstr->AmbCol.r + sub.r; + amb.g = tevstr->AmbCol.g + sub.g; + amb.b = tevstr->AmbCol.b + sub.b; + break; + case 7: + break; + } + + if (daPy_py_c::checkNowWolfPowerUp()) { + f32 ambRate = g_env_light.bg_amb_col[0].r / 255.0f; + amb.r = (amb.r + 8) * (6.0f * ambRate); + + ambRate = g_env_light.bg_amb_col[0].g / 255.0f; + amb.g = (amb.g + 8) * (6.0f * ambRate); + + ambRate = g_env_light.bg_amb_col[0].b / 255.0f; + amb.b = (amb.b + 8) * (6.0f * ambRate); + } + + amb.a = 0xFF; + return amb; +} + +static void flower_emit(const dusk::batch::LeafTemplate& tpl, const Vec* xformedPos, GXColor amb) { + for (u32 i = 0; i < tpl.vtxCount; i++) { + const dusk::batch::LeafTemplate::Vtx& v = tpl.vtx[i]; + const Vec& p = xformedPos[v.pos]; + GXPosition3f32(p.x, p.y, p.z); + GXNormal1x8(v.nrm); + GXColor1x8(v.clr); + GXColor4u8(amb.r, amb.g, amb.b, amb.a); + GXTexCoord1x8(v.tex); + } +} + +void dFlower_packet_c::draw() { + ZoneScoped; + dScnKy_env_light_c* kankyo = dKy_getEnvlight(); + j3dSys.reinitGX(); + + GXSetNumIndStages(0); + dKy_setLight_again(); + GXClearVtxDesc(); + GXSetVtxDesc(GX_VA_POS, GX_INDEX8); + GXSetVtxDesc(GX_VA_NRM, GX_INDEX8); + GXSetVtxDesc(GX_VA_CLR0, GX_INDEX8); + GXSetVtxDesc(GX_VA_TEX0, GX_INDEX8); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_NRM, GX_NRM_XYZ, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_NRM, GX_NRM_XYZ, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_CLR1, GX_CLR_RGBA, GX_RGBA8, 0); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0); + GXSETARRAY(GX_VA_POS, &l_flowerPos, sizeof(l_flowerPos), sizeof(Vec), true); + GXSETARRAY(GX_VA_NRM, &l_flowerNormal, sizeof(l_flowerNormal), sizeof(Vec), true); + GXSETARRAY(GX_VA_CLR0, &l_flowerColor, sizeof(l_flowerColor), sizeof(GXColor), true); + GXSETARRAY(GX_VA_TEX0, &l_flowerTexCoord, sizeof(l_flowerTexCoord), 8, true); + + static GXVtxDescList vtxDescList[] = { + {GX_VA_POS, GX_DIRECT}, + {GX_VA_NRM, GX_INDEX8}, + {GX_VA_CLR0, GX_INDEX8}, + {GX_VA_CLR1, GX_DIRECT}, + {GX_VA_TEX0, GX_INDEX8}, + {GX_VA_NULL, GX_NONE}, + }; + static Vec xfPos[256]; + Mtx identity; + MTXIdentity(identity); + + // --- hana00 --- + for (int i = 0; i < 64; i++) { + dFlower_data_c* first = m_room[i].getData(); + if (first == nullptr || !dComIfGp_roomControl_checkStatusFlag(i, 0x10)) { + continue; + } + + dKy_tevstr_c* tevstr = dComIfGp_roomControl_getTevStr(i); + int lightCount = 6; + + if (dComIfGp_roomControl_getStatusRoomDt(i) != nullptr) { + lightCount = dComIfGp_roomControl_getStatusRoomDt(i)->getLightVecInfoNum(); + } + + if (dKy_SunMoon_Light_Check() && lightCount < 2) { + lightCount = 2; + } + + for (int j = 0; j < 6; j++) { + if (kankyo->field_0x0c18[j].field_0x26 == 1) { + lightCount++; + } + } + + if (lightCount <= 2) { + GXCallDisplayList(l_matLight4DL, 0x80); + } else { + GXCallDisplayList(l_matDL, 0x80); + } + + GXSetTevColorS10(GX_TEVREG0, {0, 0, 0, 0}); + dKy_Global_amb_set(tevstr); + dKy_GxFog_tevstr_set(tevstr); + dKy_setLight_nowroom_grass(tevstr->room_no, 1.0f); + + GXLoadTexObj(&mTexObj_l_J_Ohana00_64TEX, GX_TEXMAP0); + batch_setup_tev(lightCount <= 2 ? (GX_LIGHT1 | GX_LIGHT2 | GX_LIGHT3 | GX_LIGHT4) : + (GX_LIGHT1 | GX_LIGHT2 | GX_LIGHT3 | GX_LIGHT4 | + GX_LIGHT5 | GX_LIGHT6 | GX_LIGHT7)); + GXSetVtxDescv(vtxDescList); + GXLoadPosMtxImm(identity, GX_PNMTX0); + GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0); + + for (int bucket = 0; bucket < 2; bucket++) { + const bool cut = bucket != 0; + const dusk::batch::LeafTemplate& tpl = cut ? mTplHana00Cut : mTplHana00; + + bool open = false; + u32 emitted = 0; + for (dFlower_data_c* flower = first; flower != nullptr; flower = flower->mp_next) { + if (cLib_checkBit(flower->m_state, 4) || + cLib_checkBit(flower->m_state, 0x40)) + { + continue; + } + if ((cLib_checkBit(flower->m_state, 8) != 0) != cut) { + continue; + } + + if (!open) { + GXBegin(GX_TRIANGLES, GX_VTXFMT1, GX_AUTO); + open = true; + } + split_batch(emitted, tpl.vtxCount); + + Mtx interpMtx; + MtxP mtx = get_model_mtx(flower->m_modelMtx, interpMtx); + transform_positions(tpl, reinterpret_cast(l_flowerPos), mtx, xfPos); + flower_emit(tpl, xfPos, hana00_amb_color(flower, tevstr)); + } + if (open) { + GXEnd(); + } + } + } + + // --- hana01 --- + GXSETARRAY(GX_VA_POS, mp_pos, sizeof(l_flowerPos2), sizeof(Vec), true); + GXSETARRAY(GX_VA_NRM, &l_flowerNormal2, sizeof(l_flowerNormal2), sizeof(Vec), true); + GXSETARRAY(GX_VA_CLR0, mp_colors, sizeof(l_flowerColor2), sizeof(GXColor), true); + GXSETARRAY(GX_VA_TEX0, mp_texCoords, sizeof(l_flowerTexCoord2), 8, true); + + for (int i = 0; i < 64; i++) { + dFlower_data_c* first = m_room[i].getData(); + if (first == NULL) { + continue; + } + + dKy_tevstr_c* tevstr = dComIfGp_roomControl_getTevStr(i); + int lightCount = 6; + + if (dComIfGp_roomControl_getStatusRoomDt(i) != NULL) { + lightCount = dComIfGp_roomControl_getStatusRoomDt(i)->getLightVecInfoNum(); + } + +#if DEBUG + if (g_kankyoHIO.light.m_HOSTIO_setting != 0) { + lightCount = g_kankyoHIO.dungeonLight.usedLights; + } +#endif + + if (dKy_SunMoon_Light_Check() == TRUE && lightCount < 2) { + lightCount = 2; + } + + if (lightCount <= 2) { + GXCallDisplayList(mp_mat2Light4DL, m_mat2Light4DL_size); + } else { + GXCallDisplayList(mp_mat2DL, m_mat2DL_size); + } + + GXSetTevColorS10(GX_TEVREG0, {0, 0, 0, 0}); + dKy_Global_amb_set(tevstr); + dKy_GxFog_tevstr_set(tevstr); + dKy_setLight_nowroom_grass(tevstr->room_no, 1.0f); + + GXLoadTexObj(&mTexObj_l_J_Ohana01_64128_0419TEX, GX_TEXMAP0); + batch_setup_tev(lightCount <= 2 ? (GX_LIGHT1 | GX_LIGHT2 | GX_LIGHT3 | GX_LIGHT4) : + (GX_LIGHT1 | GX_LIGHT2 | GX_LIGHT3 | GX_LIGHT4 | + GX_LIGHT5 | GX_LIGHT6 | GX_LIGHT7)); + GXSetVtxDescv(vtxDescList); + GXLoadPosMtxImm(identity, GX_PNMTX0); + GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0); + + const dusk::batch::LeafTemplate* const buckets[3] = { + &mTplHana01, &mTplHana01Cut00, &mTplHana01Cut}; + for (int bucket = 0; bucket < 3; bucket++) { + const dusk::batch::LeafTemplate& tpl = *buckets[bucket]; + + bool open = false; + u32 emitted = 0; + int idx = 0; + for (dFlower_data_c* flower = first; flower != NULL; flower = flower->mp_next, idx++) { + if (cLib_checkBit(flower->m_state, 4) || + !cLib_checkBit(flower->m_state, 0x40)) + { + continue; + } + const int flowerBucket = cLib_checkBit(flower->m_state, 8) ? 2 : + cLib_checkBit(flower->m_state, 0x10) ? 1 : + 0; + if (flowerBucket != bucket) { + continue; + } + + if (!open) { + GXBegin(GX_TRIANGLES, GX_VTXFMT1, GX_AUTO); + open = true; + } + split_batch(emitted, tpl.vtxCount); + + Mtx interpMtx; + MtxP mtx = get_model_mtx(flower->m_modelMtx, interpMtx); + transform_positions(tpl, mp_pos, mtx, xfPos); + flower_emit(tpl, xfPos, hana01_amb_color(idx, tevstr)); + } + if (open) { + GXEnd(); + } + } + } + + GXSetNumTevStages(1); + GXSetNumChans(1); + J3DShape::resetVcdVatCache(); +} +#else void dFlower_packet_c::draw() { ZoneScoped; dScnKy_env_light_c* kankyo = dKy_getEnvlight(); @@ -886,6 +1263,7 @@ void dFlower_packet_c::draw() { J3DShape::resetVcdVatCache(); } +#endif void dFlower_packet_c::calc() { dFlower_anm_c* anm_p = getAnm(); diff --git a/src/d/actor/d_grass.inc b/src/d/actor/d_grass.inc index 8b94368a34..9e8c360518 100644 --- a/src/d/actor/d_grass.inc +++ b/src/d/actor/d_grass.inc @@ -512,11 +512,366 @@ dGrass_packet_c::dGrass_packet_c() { m_Mkusa_9q_cDL_size = 0xC0; field_0x1d714 = 0; +#if TARGET_PC + dusk::batch::decode_leaf_template(mp_Mkusa_9q_DL, m_Mkusa_9q_DL_size, mTplKusa9q); + dusk::batch::decode_leaf_template(mp_Mkusa_9q_cDL, m_Mkusa_9q_cDL_size, mTplKusa9qCut); + dusk::batch::decode_leaf_template(l_M_TenGusaDL, 0xC0, mTplTengusa); +#endif + OS_REPORT("草群メモリ=%fK\n", 117.7734375f); m_deleteRoom = &dGrass_packet_c::deleteRoom; } +#if TARGET_PC +static MtxP get_model_mtx(Mtx modelMtx, Mtx storage) { + if (dusk::frame_interp::lookup_replacement(modelMtx, storage)) { + cMtx_concat(j3dSys.getViewMtx(), storage, storage); + return storage; + } + return modelMtx; +} + +static void transform_positions( + const dusk::batch::LeafTemplate& tpl, const Vec* posArray, const Mtx mtx, Vec* xfPos) { + for (u32 i = 0; i < tpl.posRefCount; i++) { + const u8 idx = tpl.posRefs[i]; + MTXMultVec(mtx, &posArray[idx], &xfPos[idx]); + } +} + +static void split_batch(u32& emitted, u32 vtxCount) { + if (emitted + vtxCount > 0xFFFF) { + GXEnd(); + GXBegin(GX_TRIANGLES, GX_VTXFMT1, GX_AUTO); + emitted = 0; + } + emitted += vtxCount; +} + +static GXColor blade_amb_color(const dGrass_data_c* blade, const dKy_tevstr_c* tevstr) { + GXColor amb; + amb.a = 0; + +#if DEBUG + if (g_kankyoHIO.navy.grass_adjust_ON) { + amb.r = g_kankyoHIO.navy.grass_ambcol.r * 2; + amb.g = g_kankyoHIO.navy.grass_ambcol.g * 2; + amb.b = g_kankyoHIO.navy.grass_ambcol.b * 2; + return amb; + } +#endif + + amb.r = (blade->m_addCol & 0x1F) * 2; + amb.g = ((blade->m_addCol >> 5) & 0x1F) * 2; + amb.b = ((blade->m_addCol >> 0xA) & 0x1F) * 2; + + if (daPy_py_c::checkNowWolfPowerUp()) { + f32 ambRate = g_env_light.bg_amb_col[0].r / 255.0f; + f32 col = (((blade->m_addCol & 0x1F) * 2 + 0x10)); + amb.r = col * (ambRate * 4.0f); + + ambRate = g_env_light.bg_amb_col[0].g / 255.0f; + f32 col2 = (((blade->m_addCol >> 5) & 0x1F) * 2 + 0x10); + amb.g = col2 * (4.0f * ambRate); + + ambRate = g_env_light.bg_amb_col[0].b / 255.0f; + f32 col3 = (((blade->m_addCol >> 10) & 0x1F) * 2 + 0x10); + amb.b = col3 * (4.0f * ambRate); + } + + f32 roomAmbScale = 1.0f - (static_cast(blade->m_pos.x) & 0xFF) * 0.001953125f; + f32 colScale = 1.1f - (static_cast(static_cast(blade->m_pos.x)) & 0xFF) / 2000.0f; + colScale -= (static_cast(blade->m_pos.z) & 0xFF) / 2000.0f; + + if (colScale > 1.0f) { + colScale = 1.0f; + } + + if (amb.r == 0x3E) { + amb.r = tevstr->AmbCol.r * roomAmbScale; + } else { + amb.r = amb.r * colScale; + } + + if (amb.g == 0x3E) { + amb.g = tevstr->AmbCol.g * roomAmbScale; + } else { + amb.g = amb.g * colScale; + } + + if (amb.b == 0x3E) { + amb.b = tevstr->AmbCol.b * roomAmbScale; + } else { + amb.b = amb.b * colScale; + } + + return amb; +} + +static void blade_emit(const dusk::batch::LeafTemplate& tpl, const Vec* xformedPos, + const GXColor* colors, GXColor amb) { + for (u32 i = 0; i < tpl.vtxCount; i++) { + const dusk::batch::LeafTemplate::Vtx& v = tpl.vtx[i]; + const Vec& p = xformedPos[v.pos]; + GXPosition3f32(p.x, p.y, p.z); + GXNormal1x8(v.nrm); + GXColor4u8(amb.r, amb.g, amb.b, colors[v.clr].a); + GXTexCoord1x8(v.tex); + } +} + +void dGrass_packet_c::draw() { + ZoneScoped; + dScnKy_env_light_c* kankyo = dKy_getEnvlight(); + + j3dSys.reinitGX(); + GXSetNumIndStages(0); + dKy_setLight_again(); + GXClearVtxDesc(); + + static GXVtxDescList l_vtxDescList[] = { + {GX_VA_POS, GX_INDEX8}, + {GX_VA_NRM, GX_INDEX8}, + {GX_VA_CLR0, GX_INDEX8}, + {GX_VA_TEX0, GX_INDEX8}, + {GX_VA_NULL, GX_NONE}, + }; + + static GXVtxDescList l_batchVtxDescList[] = { + {GX_VA_POS, GX_DIRECT}, + {GX_VA_NRM, GX_INDEX8}, + {GX_VA_CLR0, GX_DIRECT}, + {GX_VA_TEX0, GX_INDEX8}, + {GX_VA_NULL, GX_NONE}, + }; + + GXSetVtxDescv(l_vtxDescList); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_NRM, GX_NRM_XYZ, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_NRM, GX_NRM_XYZ, GX_F32, 0); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_CLR0, GX_CLR_RGBA, GX_RGBA8, 0); + GXSetVtxAttrFmt(GX_VTXFMT1, GX_VA_TEX0, GX_TEX_ST, GX_F32, 0); + GXSETARRAY(GX_VA_POS, mp_pos, sizeof(l_pos), sizeof(Vec), true); + GXSETARRAY(GX_VA_NRM, mp_normal, sizeof(l_normal), sizeof(Vec), true); + GXSETARRAY(GX_VA_CLR0, mp_colors, sizeof(l_color), sizeof(GXColor), true); + GXSETARRAY(GX_VA_TEX0, mp_texCoords, sizeof(l_texCoord), 8, true); + + GXColorS10 reg1 = {0, 0, 0, 0}; + + // daytime "shine" alpha curve (TEVREG1 alpha) + f32 daytime = g_env_light.getDaytime(); + f32 ratio; + f32 shine; + if (daytime >= 90.0f && daytime < 135.0f) { + ratio = 1.0f - (0.022222223f * (135.0f - daytime)); + shine = 100.0f - (18.0f * ratio); + } else if (daytime >= 135.0f && daytime < 225.0f) { + ratio = 1.0f - (0.011111111f * (225.0f - daytime)); + shine = 82.0f - (25.0f * ratio); + } else if (daytime >= 225.0f && daytime < 270.0f) { + ratio = 1.0f - (0.022222223f * (270.0f - daytime)); + shine = 57.0f - (-25.0f * ratio); + } else if (daytime >= 270.0f && daytime < 315.0f) { + ratio = (1.0f - (0.022222223f * (315.0f - daytime))); + shine = 82.0f - (-18.0f * ratio); + } else { + shine = 100.0f; + } + +#if DEBUG + if (g_kankyoHIO.navy.grass_shine_value != 0.0f) { + shine = g_kankyoHIO.navy.grass_shine_value; + } +#endif + + static Vec xfPos[256]; + Mtx identity; + PSMTXIdentity(identity); + + for (int i = 0; i < 64; i++) { + dGrass_data_c* first = m_room[i].getData(); + if (first == NULL || !dComIfGp_roomControl_checkStatusFlag(i, 0x10)) { + continue; + } + + int lightCount = 6; + dKy_tevstr_c* tevstr = dComIfGp_roomControl_getTevStr(i); + + f32 lightInf = g_env_light.grass_light_inf_rate * g_env_light.bg_light_influence; + lightInf += 0.5f * (1.0f - lightInf); + + J3DLightInfo* lightInfo = tevstr->mLights[0].getLightInfo(); + reg1.r = lightInfo->mColor.r * lightInf; + reg1.g = lightInfo->mColor.g * lightInf; + reg1.b = lightInfo->mColor.b * lightInf; + reg1.a = shine; + if (memcmp(dComIfGp_getStartStageName(), "D_MN01", 6) == 0) { + reg1.r = 0; + reg1.g = 0x1E; + reg1.b = 5; + reg1.a = 0x50; + } + GFSetTevColorS10(GX_TEVREG1, reg1); + + if (dComIfGp_roomControl_getStatusRoomDt(i) != nullptr) { + lightCount = dComIfGp_roomControl_getStatusRoomDt(i)->getLightVecInfoNum(); + } + +#if DEBUG + if (g_kankyoHIO.light.m_HOSTIO_setting != 0) { + lightCount = g_kankyoHIO.dungeonLight.usedLights; + } +#endif + + if (dKy_SunMoon_Light_Check() == TRUE && lightCount < 2) { + lightCount = 2; + } + + for (int j = 0; j < 6; j++) { + if (kankyo->field_0x0c18[j].field_0x26 == 1) { + lightCount++; + } + } + + // room-level setup + if (first->field_0x05 <= 3 || first->field_0x05 >= 10) { + GXLoadTexObj(&mTexObj_l_M_kusa05_RGBATEX, GX_TEXMAP0); + if (lightCount <= 3) { + GXCallDisplayList(mp_kusa9q_14_DL, m_kusa9q_DL_14_size); + } else { + GXCallDisplayList(mp_kusa9q_DL, m_kusa9q_DL_size); + } + } else { + GXLoadTexObj(&mTexObj_l_M_Hijiki00TEX, GX_TEXMAP0); + GXCallDisplayList(l_Tengusa_matDL, 0xA0); + } + + GFSetTevColorS10(GX_TEVREG2, {0, 0, 0, 0}); + + dKy_Global_amb_set(tevstr); + dKy_GfFog_tevstr_set(tevstr); + dKy_setLight_nowroom_grass(tevstr->room_no, 0.0f); + + GXSetVtxDescv(l_batchVtxDescList); + GXLoadPosMtxImm(identity, GX_PNMTX0); + GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0); + + // buckets: (kusa05 vs tengusa) x (standing vs cut) + bool hasRegrowing = false; + for (int bucket = 0; bucket < 4; bucket++) { + const bool kusaTex = bucket < 2; + const bool cut = (bucket & 1) != 0; + const dusk::batch::LeafTemplate& tpl = + cut ? mTplKusa9qCut : (kusaTex ? mTplKusa9q : mTplTengusa); + + bool open = false; + u32 emitted = 0; + for (dGrass_data_c* blade = first; blade != NULL; blade = blade->mp_next) { + if (cLib_checkBit(blade->field_0x01, 2)) { + continue; // clipped + } + if (blade->field_0x02 < -1) { + hasRegrowing = true; + continue; + } + const bool bladeKusaTex = blade->field_0x05 <= 3 || blade->field_0x05 >= 10; + if (bladeKusaTex != kusaTex || (blade->field_0x02 < 0) != cut) { + continue; + } + + if (!open) { + if (kusaTex) { + GXLoadTexObj(&mTexObj_l_M_kusa05_RGBATEX, GX_TEXMAP0); + if (lightCount <= 2) { + GXCallDisplayList(mp_kusa9q_14_DL, m_kusa9q_DL_14_size); + } else { + GXCallDisplayList(mp_kusa9q_DL, m_kusa9q_DL_size); + } + } else { + GXLoadTexObj(&mTexObj_l_M_Hijiki00TEX, GX_TEXMAP0); + GXCallDisplayList(l_Tengusa_matDL, 0xA0); + } + // change amb_src to GX_SRC_VTX + const u32 lightMask = + (kusaTex && lightCount <= 2) + ? (GX_LIGHT1 | GX_LIGHT2 | GX_LIGHT3 | GX_LIGHT4) + : (GX_LIGHT1 | GX_LIGHT2 | GX_LIGHT3 | GX_LIGHT4 | GX_LIGHT5 | + GX_LIGHT6 | GX_LIGHT7); + GXSetChanCtrl(GX_COLOR0, GX_TRUE, GX_SRC_VTX, GX_SRC_REG, lightMask, + GX_DF_CLAMP, GX_AF_SPOT); + reg1.a = cut ? 0 : shine; + GFSetTevColorS10(GX_TEVREG1, reg1); + GXBegin(GX_TRIANGLES, GX_VTXFMT1, GX_AUTO); + open = true; + } + + split_batch(emitted, tpl.vtxCount); + + Mtx interpMtx; + MtxP mtx = get_model_mtx(blade->m_modelMtx, interpMtx); + transform_positions(tpl, mp_pos, mtx, xfPos); + blade_emit(tpl, xfPos, mp_colors, blade_amb_color(blade, tevstr)); + } + if (open) { + GXEnd(); + } + } + + // regrowing blades have per-blade TEVREG2 alpha + // draw them with the original immediate path + if (hasRegrowing) { + GXSetVtxDescv(l_vtxDescList); + for (dGrass_data_c* blade = first; blade != NULL; blade = blade->mp_next) { + if (blade->field_0x02 >= -1 || cLib_checkBit(blade->field_0x01, 2)) { + continue; + } + + const bool kusaTex = blade->field_0x05 <= 3 || blade->field_0x05 >= 10; + if (kusaTex) { + GXLoadTexObj(&mTexObj_l_M_kusa05_RGBATEX, GX_TEXMAP0); + if (lightCount <= 2) { + GXCallDisplayList(mp_kusa9q_14_DL, m_kusa9q_DL_14_size); + } else { + GXCallDisplayList(mp_kusa9q_DL, m_kusa9q_DL_size); + } + } else { + GXLoadTexObj(&mTexObj_l_M_Hijiki00TEX, GX_TEXMAP0); + GXCallDisplayList(l_Tengusa_matDL, 0xA0); + } + + reg1.a = 0; + GFSetTevColorS10(GX_TEVREG1, reg1); + GXSetChanAmbColor(GX_COLOR0A0, blade_amb_color(blade, tevstr)); + + Mtx modelMtx; + GXLoadPosMtxImm(get_model_mtx(blade->m_modelMtx, modelMtx), GX_PNMTX0); + GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0); + + GFSetTevColorS10(GX_TEVREG2, + {0, 0, 0, static_cast(-0x100 - (blade->field_0x02 << 8) / 40)}); + + if (blade->field_0x02 != -2) { + if (kusaTex) { + GXCallDisplayList(mp_Mkusa_9q_DL, m_Mkusa_9q_DL_size); + } else { + GXCallDisplayList(l_M_TenGusaDL, 0xC0); + } + } else { + GXCallDisplayList(mp_Mkusa_9q_cDL, m_Mkusa_9q_cDL_size); + } + + GFSetTevColorS10(GX_TEVREG2, {0, 0, 0, 0}); + } + } + } + + J3DShape::resetVcdVatCache(); +} +#else void dGrass_packet_c::draw() { ZoneScoped; dScnKy_env_light_c* kankyo = dKy_getEnvlight(); @@ -811,6 +1166,7 @@ void dGrass_packet_c::draw() { J3DShape::resetVcdVatCache(); } +#endif void dGrass_packet_c::calc() { cXyz* temp_r29 = dKyw_get_wind_vec(); diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index 50e9f073b6..cf0f7cbb56 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -34,6 +34,7 @@ #include "dusk/action_bindings.h" #include "dusk/mouse.h" #include "dusk/settings.h" +#include "dusk/touch_camera.h" #include "imgui.h" #endif @@ -7499,6 +7500,15 @@ static constexpr s16 FLYCAM_ROLL_SPEED = 256; static ImVec2 sFlyCamLastMousePos = {-1.f, -1.f}; #if TARGET_PC +static constexpr f32 TOUCH_CAMERA_CSTICK_EXIT_THRESHOLD = 0.05f; +static bool sTouchFreeCameraActive = false; + +bool dCamera_c::isAimActive() { + auto* link = daAlink_getAlinkActorClass(); + return link != nullptr && link->checkAimInputContext() && + dComIfGp_checkCameraAttentionStatus(link->field_0x317c, 0x10); +} + bool dCamera_c::executeDebugFlyCam() { if (!dusk::getSettings().game.debugFlyCam) { if (mDebugFlyCam.initialized) { @@ -7640,16 +7650,30 @@ void dCamera_c::deactivateDebugFlyCam() { mDebugFlyCam.initialized = false; } -bool dCamera_c::canUseFreeCam() { - return dusk::getSettings().game.freeCamera || dusk::getSettings().game.enableMouseCamera; -} - bool dCamera_c::freeCamera() { - if (canUseFreeCam() && mGear == 1) { + f32 touchYawDp = 0.0f; + f32 touchPitchDp = 0.0f; + bool touchCameraMoved = false; + const bool touchControlsEnabled = dusk::getSettings().game.enableTouchControls; + if (touchControlsEnabled && !isAimActive()) { + touchCameraMoved = dusk::touch_camera::consume_delta(touchYawDp, touchPitchDp); + } + if (!touchControlsEnabled || + mPadInfo.mCStick.mLastValue > TOUCH_CAMERA_CSTICK_EXIT_THRESHOLD) + { + sTouchFreeCameraActive = false; + } + if (touchCameraMoved) { + sTouchFreeCameraActive = true; + } + + const bool useFreeCamera = dusk::getSettings().game.freeCamera || + dusk::getSettings().game.enableMouseCamera || sTouchFreeCameraActive; + if (useFreeCamera && mGear == 1) { mGear = 0; } - if (!canUseFreeCam() || mCamStyle == 70) + if (!useFreeCamera || mCamStyle == 70) { mCamParam.mManualMode = 0; return false; @@ -7660,6 +7684,17 @@ bool dCamera_c::freeCamera() { mCamParam.freeYAngle = mViewCache.mDirection.mInclination.Degree(); } + if (touchCameraMoved) { + mCamParam.mManualMode = 1; + const f32 yawInput = dusk::getSettings().game.invertCameraXAxis ? -touchYawDp : touchYawDp; + const f32 pitchInput = + touchPitchDp * (dusk::getSettings().game.invertCameraYAxis ? -1.0f : 1.0f); + mCamParam.freeXAngle += yawInput * dusk::getSettings().game.touchCameraXSensitivity * + dusk::touch_camera::YAW_DEGREES_PER_DP; + mCamParam.freeYAngle += pitchInput * dusk::getSettings().game.touchCameraYSensitivity * + dusk::touch_camera::PITCH_DEGREES_PER_DP; + } + cXyz camMovement = {mPadInfo.mCStick.mLastPosX, mPadInfo.mCStick.mLastPosY, 0.0f}; f32 magnitude = sqrt(mPadInfo.mCStick.mLastPosX * mPadInfo.mCStick.mLastPosX + mPadInfo.mCStick.mLastPosY * mPadInfo.mCStick.mLastPosY); @@ -11359,7 +11394,7 @@ static int camera_execute(camera_process_class* i_this) { const auto target = get_target_trim_height(i_this); const auto step = dusk::frame_interp::get_interpolation_step(); const auto cur = camera->TrimHeight(); - const auto prev = (4.0f * cur - target) / 3.0f; + const auto prev = (4.0f * cur - target) / 3.0f; const auto trim_height = prev + (cur - prev) * step; widezoom_correction(i_this, trim_height); diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index 7ba3fac836..6e5e2e4954 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -23,8 +23,22 @@ #include "m_Do/m_Do_graphic.h" #include +#if TARGET_PC +#include "dusk/menu_pointer.h" #include "dusk/string.hpp" +namespace { +constexpr u8 pointer_target(u8 group, u8 index) noexcept { + return static_cast((group << 4) | (index & 0x0F)); +} + +constexpr u8 s_pointerDataSelectTarget = 0; +constexpr u8 s_pointerMenuSelectTarget = 1; +constexpr u8 s_pointerCopySelectTarget = 2; +constexpr u8 s_pointerYesNoSelectTarget = 3; +} // namespace +#endif + static s32 SelStartFrameTbl[3] = { 59, 99, @@ -756,8 +770,143 @@ void dFile_select_c::dataSelectInit() { } } +#if TARGET_PC +bool dFile_select_c::pointerDataSelect() { + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect); + for (u8 i = 0; i < 3; ++i) { + if (!dusk::menu_pointer::hit_pane(mSelFilePanes[i], 8.0f)) { + continue; + } + const bool clicked = dusk::menu_pointer::consume_click(); + if (mSelectNum != i) { + mDoAud_seStart(Z2SE_FILE_SELECT_CURSOR, NULL, 0, 0); + mLastSelectNum = mSelectNum; + mSelectNum = i; + if (clicked) { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerDataSelectTarget, i)); + } + dataSelectAnmSet(); + mDataSelProc = DATASELPROC_DATA_SELECT_MOVE_ANIME; + return true; + } + if (clicked) { + dataSelectStart(); + return true; + } + } + return false; +} + +bool dFile_select_c::pointerMenuSelect() { + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect); + for (u8 i = 0; i < 3; ++i) { + if (!dusk::menu_pointer::hit_pane(m3mSelPane[i], 8.0f)) { + continue; + } + const bool clicked = dusk::menu_pointer::consume_click(); + if (!mIsDataNew[mSelectNum] && mSelectMenuNum != i) { + mDoAud_seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0); + mLastSelectMenuNum = mSelectMenuNum; + mSelectMenuNum = i; + if (clicked) { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerMenuSelectTarget, i)); + } + menuSelectAnmSet(); + mDataSelProc = DATASELPROC_MENU_SELECT_MOVE_ANM; + return true; + } + if (clicked) { + menuSelectStart(); + return true; + } + } + return false; +} + +bool dFile_select_c::pointerCopyDataToSelect() { + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect); + for (u8 i = 0; i < 2; ++i) { + if (!dusk::menu_pointer::hit_pane(mCpSelPane[i], 8.0f)) { + continue; + } + const bool clicked = dusk::menu_pointer::consume_click(); + if (field_0x026b != i) { + mDoAud_seStart(Z2SE_FILE_SELECT_CURSOR, NULL, 0, 0); + field_0x026c = field_0x026b; + field_0x026b = i; + if (clicked) { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerCopySelectTarget, i)); + } + copyDataToSelectMoveAnmSet(); + mDataSelProc = DATASELPROC_COPY_DATA_TO_SELECT_MOVE_ANM; + return true; + } + if (clicked) { + copyDataToSelectStart(); + return true; + } + } + return false; +} + +bool dFile_select_c::pointerYesNoSelect(bool errorSelect) { + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect); + for (u8 i = 0; i < 2; ++i) { + if (!dusk::menu_pointer::hit_pane(mYnSelPane[i], 8.0f)) { + continue; + } + const bool clicked = + (!errorSelect || field_0x0268 == i) && dusk::menu_pointer::consume_click(); + if (field_0x0268 != i) { + field_0x0269 = field_0x0268; + field_0x0268 = i; + if (errorSelect) { + errCurMove(0); + return false; + } else { + mDoAud_seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0); + if (clicked) { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerYesNoSelectTarget, i)); + } + yesnoSelectAnmSet(); + mDataSelProc = DATASELPROC_YES_NO_CURSOR_MOVE_ANM; + return true; + } + } + if (clicked) { + if (errorSelect) { + if (field_0x0268 != 0) { + mDoAud_seStart(Z2SE_SY_CURSOR_OK, 0, 0, 0); + } else { + mDoAud_seStart(Z2SE_SY_CURSOR_CANCEL, 0, 0, 0); + } + mSelIcon->setAlphaRate(0.0f); + } else { + yesNoSelectStart(); + } + return true; + } + } + return false; +} +#endif + // handles switching between quest logs void dFile_select_c::dataSelect() { +#if TARGET_PC + if (pointerDataSelect()) { + return; + } +#endif + stick->checkTrigger(); // If A or Start was pressed @@ -801,6 +950,9 @@ static u16 msgTbl[3] = { }; void dFile_select_c::dataSelectStart() { +#if TARGET_PC + dusk::menu_pointer::clear_deferred_activation(dusk::menu_pointer::Context::FileSelect); +#endif mSelIcon->setAlphaRate(0.0f); if (mIsNoData[mSelectNum]) { @@ -949,6 +1101,16 @@ void dFile_select_c::dataSelectAnmSet() { } void dFile_select_c::dataSelectMoveAnime() { +#if TARGET_PC + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect); + if (mSelectNum != 0xFF && dusk::menu_pointer::hit_pane(mSelFilePanes[mSelectNum], 8.0f) && + dusk::menu_pointer::consume_click()) + { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerDataSelectTarget, mSelectNum)); + } +#endif bool iVar7 = true; bool iVar6 = true; bool bVar1 = true; @@ -997,6 +1159,14 @@ void dFile_select_c::dataSelectMoveAnime() { mSelFilePanes[mLastSelectNum]->getPanePtr()->setAnimation((J2DAnmTransform*)NULL); } +#if TARGET_PC + if (dusk::menu_pointer::consume_deferred_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerDataSelectTarget, mSelectNum))) { + dataSelectStart(); + return; + } +#endif mDataSelProc = DATASELPROC_DATA_SELECT; } } @@ -1161,6 +1331,12 @@ void dFile_select_c::selectDataOpenEraseMove() { // Handles selecting between copy / start / delete menus in quest log void dFile_select_c::menuSelect() { +#if TARGET_PC + if (pointerMenuSelect()) { + return; + } +#endif + stick->checkTrigger(); // if a was pressed, do the menu selection process @@ -1191,6 +1367,9 @@ void dFile_select_c::menuSelect() { // Handles copy / start / delete actions depending on which menu is selected from menuSelect void dFile_select_c::menuSelectStart() { +#if TARGET_PC + dusk::menu_pointer::clear_deferred_activation(dusk::menu_pointer::Context::FileSelect); +#endif #if TARGET_PC if (!dusk::getSettings().game.hideTvSettingsScreen || mSelectMenuNum != 1) { mDoAud_seStart(Z2SE_SY_CURSOR_OK, NULL, 0, 0); @@ -1312,6 +1491,17 @@ void dFile_select_c::menuSelectAnmSet() { } void dFile_select_c::menuSelectMoveAnm() { +#if TARGET_PC + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect); + if (mSelectMenuNum != 0xFF && + dusk::menu_pointer::hit_pane(m3mSelPane[mSelectMenuNum], 8.0f) && + dusk::menu_pointer::consume_click()) + { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerMenuSelectTarget, mSelectMenuNum)); + } +#endif bool tmp1 = true; if (mSelectMenuNum != 0xFF && @@ -1369,6 +1559,14 @@ void dFile_select_c::menuSelectMoveAnm() { m3mSelPane[mLastSelectMenuNum]->getPanePtr()->setAnimation((J2DAnmTransform*)NULL); } +#if TARGET_PC + if (dusk::menu_pointer::consume_deferred_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerMenuSelectTarget, mSelectMenuNum))) { + menuSelectStart(); + return; + } +#endif mDataSelProc = DATASELPROC_MENU_SELECT; } } @@ -1698,6 +1896,12 @@ void dFile_select_c::setSaveDataForCopySel() { } void dFile_select_c::copyDataToSelect() { +#if TARGET_PC + if (pointerCopyDataToSelect()) { + return; + } +#endif + stick->checkTrigger(); if (mDoCPd_c::getTrigA(PAD_1)) { @@ -1722,6 +1926,9 @@ void dFile_select_c::copyDataToSelect() { } void dFile_select_c::copyDataToSelectStart() { +#if TARGET_PC + dusk::menu_pointer::clear_deferred_activation(dusk::menu_pointer::Context::FileSelect); +#endif mDoAud_seStart(Z2SE_SY_CURSOR_OK, NULL, 0, 0); mCpDataToNum = getCptoNum(field_0x026b); @@ -1787,6 +1994,17 @@ void dFile_select_c::copyDataToSelectMoveAnmSet() { } void dFile_select_c::copyDataToSelectMoveAnm() { +#if TARGET_PC + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect); + if (field_0x026b != 0xFF && + dusk::menu_pointer::hit_pane(mCpSelPane[field_0x026b], 8.0f) && + dusk::menu_pointer::consume_click()) + { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerCopySelectTarget, field_0x026b)); + } +#endif bool iVar7 = true; bool iVar6 = true; bool bVar1 = true; @@ -1836,6 +2054,14 @@ void dFile_select_c::copyDataToSelectMoveAnm() { mSelIcon2->setAlphaRate(1.0f); } +#if TARGET_PC + if (dusk::menu_pointer::consume_deferred_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerCopySelectTarget, field_0x026b))) { + copyDataToSelectStart(); + return; + } +#endif mDataSelProc = DATASELPROC_COPY_DATA_TO_SELECT; } } @@ -2105,6 +2331,12 @@ void dFile_select_c::yesnoCursorShow() { } void dFile_select_c::YesNoSelect() { +#if TARGET_PC + if (pointerYesNoSelect(false)) { + return; + } +#endif + stick->checkTrigger(); if (mDoCPd_c::getTrigA(PAD_1)) { @@ -2129,6 +2361,9 @@ void dFile_select_c::YesNoSelect() { } void dFile_select_c::yesNoSelectStart() { +#if TARGET_PC + dusk::menu_pointer::clear_deferred_activation(dusk::menu_pointer::Context::FileSelect); +#endif if (field_0x0268 != 0) { mDoAud_seStart(Z2SE_SY_CURSOR_OK, NULL, 0, 0); field_0x03b1 = 1; @@ -2284,10 +2519,29 @@ void dFile_select_c::YesNoCancelMove() { } void dFile_select_c::yesNoCursorMoveAnm() { +#if TARGET_PC + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::FileSelect); + if (field_0x0268 != 0xFF && + dusk::menu_pointer::hit_pane(mYnSelPane[field_0x0268], 8.0f) && + dusk::menu_pointer::consume_click()) + { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerYesNoSelectTarget, field_0x0268)); + } +#endif bool isYnSelMove = yesnoSelectMoveAnm(); bool isYnWakuAlpha = yesnoWakuAlpahAnm(field_0x0269); if (isYnSelMove == true && isYnWakuAlpha == true) { yesnoCursorShow(); +#if TARGET_PC + if (dusk::menu_pointer::consume_deferred_activation( + dusk::menu_pointer::Context::FileSelect, + pointer_target(s_pointerYesNoSelectTarget, field_0x0268))) { + yesNoSelectStart(); + return; + } +#endif mDataSelProc = DATASELPROC_YES_NO_SELECT; } } @@ -4238,6 +4492,11 @@ static MemCardCheckFuncT MemCardCheckProc[] = { &dFile_select_c::MemCardErrYesNoCursorMoveAnm, &dFile_select_c::MemCardSaveDataClear, +#if TARGET_PC + &dFile_select_c::MemCardAutoMakeGameFile, + &dFile_select_c::MemCardAutoMakeGameFileErrWait, +#endif + #if PLATFORM_WII || PLATFORM_SHIELD &dFile_select_c::nandStatCheck, &dFile_select_c::gameFileInitSel, @@ -4321,11 +4580,33 @@ void dFile_select_c::MemCardStatCheck() { mDoMemCd_Load(); mCardCheckProc = MEMCARDCHECKPROC_LOAD_WAIT; break; +#if TARGET_PC + case 1: { // no save file + if (dusk::getSettings().game.instantSaves) { + field_0x03b1 = 1; + setInitSaveData(); + dataSave(); + mCardCheckProc = MEMCARDCHECKPROC_AUTO_MAKE_GAMEFILE; + } else { + errDispInitSet(22, 0); + field_0x0280 = true; + mNextCardCheckProc = MEMCARDCHECKPROC_MAKE_GAMEFILE_SEL; + } + break; + } + case 4: // card is writing + if (dusk::getSettings().game.instantSaves) { + field_0x03b1 = 1; + mCardCheckProc = MEMCARDCHECKPROC_AUTO_MAKE_GAMEFILE; + } + break; +#else case 1: errDispInitSet(22, 0); field_0x0280 = true; mNextCardCheckProc = MEMCARDCHECKPROC_MAKE_GAMEFILE_SEL; break; +#endif } #else switch (status) { @@ -5031,6 +5312,33 @@ void dFile_select_c::MemCardMakeGameFileCheck() { } } +#if TARGET_PC +void dFile_select_c::MemCardAutoMakeGameFile() { + field_0x03b4 = mDoMemCd_SaveSync(); + if (field_0x03b4 == 0) { + return; + } + + field_0x03b1 = 0; + if (field_0x03b4 == 1) { + mDoMemCd_Load(); + mCardCheckProc = MEMCARDCHECKPROC_LOAD_WAIT; + } else { + errDispInitSet(0x1A, 0); + field_0x0280 = false; + mWindowCloseMsgDispCb = NULL; + mKeyWaitMsgDispCb = NULL; + mNextCardCheckProc = MEMCARDCHECKPROC_AUTO_MAKE_GAMEFILE_ERR_WAIT; + } +} + +void dFile_select_c::MemCardAutoMakeGameFileErrWait() { + mNextCardCheckProc = MEMCARDCHECKPROC_STAT_CHECK; + mKeyWaitCardCheckProc = MEMCARDCHECKPROC_MSG_WINDOW_CLOSE; + mCardCheckProc = MEMCARDCHECKPROC_ERRMSG_WAIT_KEY; +} +#endif + #if PLATFORM_WII || PLATFORM_SHIELD void dFile_select_c::gameFileInitSel() { if (errYesNoSelect() != 0) { @@ -5184,6 +5492,12 @@ void dFile_select_c::MemCardMsgWindowClose() { bool dFile_select_c::errYesNoSelect() { bool rv = false; +#if TARGET_PC + if (pointerYesNoSelect(true)) { + return true; + } +#endif + stick->checkTrigger(); if (mDoCPd_c::getTrigA(PAD_1)) { diff --git a/src/d/d_map.cpp b/src/d/d_map.cpp index 8cecead15a..765facc335 100644 --- a/src/d/d_map.cpp +++ b/src/d/d_map.cpp @@ -1213,6 +1213,10 @@ void dMap_c::changeTextureSize(int param_1, int param_2, int param_3) { JUT_ASSERT(2672, mImage_p != NULL); JUT_ASSERT(2673, mResTIMG != NULL); +#if TARGET_PC + GXDestroyCopyTex(mImage_p); +#endif + mTexSizeX = param_1 >> param_3; mTexSizeY = param_2 >> param_3; @@ -1226,6 +1230,24 @@ void dMap_c::changeTextureSize(int param_1, int param_2, int param_3) { } #endif +#if TARGET_PC +bool dMap_c::refreshTextureSize() { + JUT_ASSERT(2688, mImage_p != NULL); + JUT_ASSERT(2689, mResTIMG != NULL); + + const u16 oldWidth = mResTIMG->width; + const u16 oldHeight = mResTIMG->height; + makeResTIMG(mResTIMG, mTexSizeX, mTexSizeY, mImage_p, (u8*)m_res, 0x33); + + if (mResTIMG->width == oldWidth && mResTIMG->height == oldHeight) { + return false; + } + + GXDestroyCopyTex(mImage_p); + return true; +} +#endif + void dMap_c::_remove() { if (mImage_p != NULL) { #if TARGET_PC diff --git a/src/d/d_map_path.cpp b/src/d/d_map_path.cpp index a7579ee92d..cafbb93e38 100644 --- a/src/d/d_map_path.cpp +++ b/src/d/d_map_path.cpp @@ -15,32 +15,49 @@ #include #ifdef TARGET_PC -#include -#include -#include +#include "dusk/settings.h" +#include "m_Do/m_Do_graphic.h" +#include +#include -constexpr u16 kPreferredMapResolutionMultiplier = 4; -constexpr u32 kMaxMapRenderPixels = 4096 * 4096; -constexpr u16 kMapImageSide = 16 * kPreferredMapResolutionMultiplier; +#include +#include +#include +#include +#include +#include +#include + +constexpr u16 kMapIconResolutionMultiplier = 4; +constexpr u16 kMapImageSide = 16 * kMapIconResolutionMultiplier; constexpr u32 kMapImageTotalPixels = kMapImageSide * kMapImageSide; typedef std::function PaintI8Fn; -u16 map_resolution_multiplier(u16 width, u16 height) { - const u32 basePixels = static_cast(width) * height; - if (basePixels == 0) { - return 1; +u16 scaled_map_axis(u16 value, f32 scale) { + const auto scaledValue = + static_cast(std::max(1.0f, std::round(static_cast(value) * scale))); + return static_cast(std::min(scaledValue, std::numeric_limits::max())); +} + +aurora::Vec2 map_render_size_for(u16 width, u16 height) { + if (width == 0 || height == 0) { + return {width, height}; } - u16 scale = kPreferredMapResolutionMultiplier; - while (scale > 1) { - const u32 scalePixels = static_cast(scale) * scale; - if (basePixels <= kMaxMapRenderPixels / scalePixels) { - break; - } - scale--; - } - return scale; + u32 renderWidth = 0; + u32 renderHeight = 0; + AuroraGetRenderSize(&renderWidth, &renderHeight); + + const f32 logicalWidth = std::max(mDoGph_gInf_c::getWidthF(), 1.0f); + const f32 logicalHeight = std::max(mDoGph_gInf_c::getHeightF(), 1.0f); + const f32 irScaleX = renderWidth > 0 ? static_cast(renderWidth) / logicalWidth : 1.0f; + const f32 irScaleY = renderHeight > 0 ? static_cast(renderHeight) / logicalHeight : 1.0f; + const f32 hudScale = std::clamp(dusk::getSettings().game.hudScale.getValue(), 0.5f, 2.0f); + return { + scaled_map_axis(width, irScaleX * hudScale), + scaled_map_axis(height, irScaleY * hudScale), + }; } void paint_i8(std::span dst, size_t width, PaintI8Fn paint) { @@ -496,9 +513,9 @@ void dRenderingMap_c::makeResTIMG(ResTIMG* p_image, u16 width, u16 height, u8* p p_image->format = GX_TF_C8; p_image->alphaEnabled = 2; #ifdef TARGET_PC - const u16 scale = map_resolution_multiplier(width, height); - p_image->width = width * scale; - p_image->height = height * scale; + const auto [rw, rh] = map_render_size_for(width, height); + p_image->width = rw; + p_image->height = rh; #else p_image->width = width; p_image->height = height; @@ -581,16 +598,14 @@ void dRenderingFDAmap_c::drawBack() const { void dRenderingFDAmap_c::preRenderingMap() { #ifdef TARGET_PC - const u16 scale = map_resolution_multiplier(mTexWidth, mTexHeight); - const u16 w = mTexWidth * scale; - const u16 h = mTexHeight * scale; - GXCreateFrameBuffer(w, h); + const auto [rw, rh] = map_render_size_for(mTexWidth, mTexHeight); + GXCreateFrameBuffer(rw, rh); // Set logical viewport dimensions GXSetViewport(0.0f, 0.0f, mTexWidth, mTexHeight, 0.0f, 1.0f); GXSetScissor(0, 0, mTexWidth, mTexHeight); // Set render viewport dimensions - GXSetViewportRender(0.0f, 0.0f, w, h, 0.0f, 1.0f); - GXSetScissorRender(0, 0, w, h); + GXSetViewportRender(0.0f, 0.0f, rw, rh, 0.0f, 1.0f); + GXSetScissorRender(0, 0, rw, rh); #else GXSetViewport(0.0f, 0.0f, mTexWidth, mTexHeight, 0.0f, 1.0f); GXSetScissor(0, 0, mTexWidth, mTexHeight); @@ -628,11 +643,9 @@ void dRenderingFDAmap_c::preRenderingMap() { void dRenderingFDAmap_c::postRenderingMap() { GXSetCopyFilter(GX_FALSE, NULL, GX_FALSE, NULL); #ifdef TARGET_PC - const u16 scale = map_resolution_multiplier(mTexWidth, mTexHeight); - const u16 w = mTexWidth * scale; - const u16 h = mTexHeight * scale; - GXSetTexCopySrc(0, 0, w, h); - GXSetTexCopyDst(w, h, GX_CTF_R8, GX_FALSE); + const auto [rw, rh] = map_render_size_for(mTexWidth, mTexHeight); + GXSetTexCopySrc(0, 0, rw, rh); + GXSetTexCopyDst(rw, rh, GX_CTF_R8, GX_FALSE); GXCopyTex(field_0x4, GX_TRUE); GXRestoreFrameBuffer(); #else diff --git a/src/d/d_menu_collect.cpp b/src/d/d_menu_collect.cpp index 770610bd0d..2b4a54d57a 100644 --- a/src/d/d_menu_collect.cpp +++ b/src/d/d_menu_collect.cpp @@ -36,6 +36,10 @@ #include "d/d_menu_window.h" #include "JSystem/J3DGraphBase/J3DMaterial.h" +#if TARGET_PC +#include "dusk/menu_pointer.h" +#endif + typedef void (dMenu_Collect2D_c::*initFunc)(); static DUSK_CONSTEXPR initFunc init[] = { &dMenu_Collect2D_c::wait_init, &dMenu_Collect2D_c::save_open_init, @@ -1788,6 +1792,12 @@ void dMenu_Collect2D_c::wait_init() { } void dMenu_Collect2D_c::wait_proc() { +#if TARGET_PC + if (pointerWait()) { + return; + } +#endif + if (dMw_A_TRIGGER()) { if (mCursorX == 0 && mCursorY == 5) { if (mDoGph_gInf_c::getFader()->mStatus == 1) { @@ -1889,6 +1899,87 @@ void dMenu_Collect2D_c::wait_proc() { } } +#if TARGET_PC +void dMenu_Collect2D_c::pointerActivateCurrent() { + if (mCursorX == 0 && mCursorY == 5) { + if (mDoGph_gInf_c::getFader()->mStatus == 1) { + mSubWindowOpenCheck = 1; + Z2GetAudioMgr()->seStart(Z2SE_SY_MENU_CHANGE_WINDOW, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + dMeter2Info_set2DVibrationM(); + } + } else if (mCursorX == 1 && mCursorY == 5) { + if (mDoGph_gInf_c::getFader()->mStatus == 1) { + mSubWindowOpenCheck = 2; + Z2GetAudioMgr()->seStart(Z2SE_SY_MENU_CHANGE_WINDOW, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + dMeter2Info_set2DVibrationM(); + } + } else if (mCursorX == 3 && mCursorY == 4) { + if (field_0x22d[3][4] != 0 && mDoGph_gInf_c::getFader()->mStatus == 1) { + mSubWindowOpenCheck = 3; + dMeter2Info_set2DVibration(); + } + } else if (mCursorX == 2 && mCursorY == 4) { + if (isFishIconVisible() && mDoGph_gInf_c::getFader()->mStatus == 1) { + mSubWindowOpenCheck = 4; + dMeter2Info_set2DVibration(); + } + } else if (mCursorX == 3 && mCursorY == 3) { + if (isSkillIconVisible() && mDoGph_gInf_c::getFader()->mStatus == 1) { + mSubWindowOpenCheck = 5; + dMeter2Info_set2DVibration(); + } + } else if (mCursorX == 2 && mCursorY == 3) { + if (isInsectIconVisible() && mDoGph_gInf_c::getFader()->mStatus == 1) { + mSubWindowOpenCheck = 6; + dMeter2Info_set2DVibration(); + } + } else if (field_0x22d[mCursorX][mCursorY] != 0 && !mIsWolf) { + if ((mCursorX >= 3 && mCursorX <= 4) || (mCursorX == 5 && mCursorY == 2)) { + u8 cursorY = mCursorY; + if (cursorY == 0) { + if (daPy_getPlayerActorClass()->getSwordChangeWaitTimer() == 0) { + changeSword(); + } + } else if (cursorY == 1) { + if (daPy_getPlayerActorClass()->getShieldChangeWaitTimer() == 0) { + changeShield(); + } + } else if (cursorY == 2 && + daPy_getPlayerActorClass()->getClothesChangeWaitTimer() == 0) + { + changeClothe(); + } + } + } +} + +bool dMenu_Collect2D_c::pointerWait() { + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Collection); + for (u8 y = 0; y < 6; ++y) { + for (u8 x = 0; x < 7; ++x) { + if (getItemTag(x, y, true) == 0 || !dusk::menu_pointer::hit_pane(mpSelPm[x][y], 8.0f)) { + continue; + } + if (mCursorX != x || mCursorY != y) { + mDoAud_seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0); + mCursorX = x; + mCursorY = y; + cursorPosSet(); + setItemNameString(mCursorX, mCursorY); + } + if (dusk::menu_pointer::consume_click()) { + pointerActivateCurrent(); + return true; + } + return false; + } + } + return false; +} +#endif + void dMenu_Collect2D_c::save_open_init() { JKRHeap* heap = mDoExt_setCurrentHeap(mpSubHeap); diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index 960131a7a2..795e3a5f64 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -17,7 +17,10 @@ #include "d/d_msg_scrn_explain.h" #include "m_Do/m_Do_graphic.h" #include "d/actor/d_a_midna.h" +#if TARGET_PC #include "dusk/frame_interpolation.h" +#include "dusk/ui/touch_controls.hpp" +#endif #include #if TARGET_PC @@ -2509,6 +2512,10 @@ dMenu_Fmap2DTop_c::dMenu_Fmap2DTop_c(JKRExpHeap* i_heap, STControl* i_stick) { } dMenu_Fmap2DTop_c::~dMenu_Fmap2DTop_c() { +#if TARGET_PC + dusk::ui::set_control_override(dusk::ui::Control::Z, dusk::ui::ControlOverride::Default); +#endif + deleteExplain(); JKR_DELETE(mpTitleScreen); mpTitleScreen = NULL; @@ -2782,6 +2789,12 @@ void dMenu_Fmap2DTop_c::setZButtonString(u32 param_0, u8 i_alpha) { param_0 = 0x533; } +#if TARGET_PC + dusk::ui::set_control_override(dusk::ui::Control::Z, + param_0 != 0 && isWarpAccept() ? dusk::ui::ControlOverride::Action : + dusk::ui::ControlOverride::Default); +#endif + #if VERSION == VERSION_GCN_JPN static const u64 cont_zt[5] = {MULTI_CHAR('cont_zt'), MULTI_CHAR('cont_zt1'), MULTI_CHAR('cont_zt2'), MULTI_CHAR('cont_zt3'), MULTI_CHAR('cont_zt4')}; #define setZButtonString_font_zt cont_zt diff --git a/src/d/d_menu_insect.cpp b/src/d/d_menu_insect.cpp index 8e6ad49b5b..7967ca6cdf 100644 --- a/src/d/d_menu_insect.cpp +++ b/src/d/d_menu_insect.cpp @@ -22,6 +22,10 @@ #include #include +#if TARGET_PC +#include "dusk/menu_pointer.h" +#endif + typedef void (dMenu_Insect_c::*initFunc)(); static initFunc map_init_process[] = { &dMenu_Insect_c::wait_init, &dMenu_Insect_c::explain_open_init, @@ -280,6 +284,12 @@ void dMenu_Insect_c::wait_init() { void dMenu_Insect_c::wait_move() { if (mDoGph_gInf_c::getFader()->getStatus() == 1) { +#if TARGET_PC + if (pointerWait()) { + return; + } +#endif + if (mDoCPd_c::getTrigB(PAD_1) || field_0xf7 == 0) { if (mDoCPd_c::getTrigB(PAD_1) && field_0xf6 == 1) { dMeter2Info_setInsectSelectType(0); @@ -301,6 +311,39 @@ void dMenu_Insect_c::wait_move() { } } +#if TARGET_PC +bool dMenu_Insect_c::pointerWait() { + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Collection); + for (u8 y = 0; y < 4; ++y) { + for (u8 x = 0; x < 6; ++x) { + const int index = x + y * 6; + if (!isGetInsect(x, y) || !dusk::menu_pointer::hit_pane(mpINSParent[index], 8.0f)) { + continue; + } + + if (field_0xf4 != x || field_0xf5 != y) { + field_0xf4 = x; + field_0xf5 = y; + setCursorPos(); + setAButtonString(0x368); + Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_ITEM, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + } + if (dusk::menu_pointer::consume_click()) { + field_0xf3 = 1; + Z2GetAudioMgr()->seStart(Z2SE_SY_EXP_WIN_OPEN, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + dMeter2Info_set2DVibration(); + return true; + } + return false; + } + } + + return false; +} +#endif + void dMenu_Insect_c::explain_open_init() { char local_78[32]; char local_98[32]; diff --git a/src/d/d_menu_letter.cpp b/src/d/d_menu_letter.cpp index cd7da6a6f4..0d7efd3adb 100644 --- a/src/d/d_menu_letter.cpp +++ b/src/d/d_menu_letter.cpp @@ -19,6 +19,15 @@ #ifdef TARGET_PC #include "dusk/achievements.h" +#include "dusk/menu_pointer.h" +#include "dusk/ui/touch_controls.hpp" + +static void enable_turn_page_controls(bool enabled) { + const auto controlOverride = + enabled ? dusk::ui::ControlOverride::Action : dusk::ui::ControlOverride::Default; + dusk::ui::set_control_override(dusk::ui::Control::L, controlOverride); + dusk::ui::set_control_override(dusk::ui::Control::R, controlOverride); +} #endif #if VERSION == VERSION_GCN_JPN @@ -82,6 +91,10 @@ dMenu_Letter_c::dMenu_Letter_c(JKRExpHeap* i_heap, STControl* i_stick, CSTContro dMenu_Letter_c::~dMenu_Letter_c() { +#if TARGET_PC + enable_turn_page_controls(false); +#endif + JKR_DELETE(mpDrawCursor); mpDrawCursor = NULL; @@ -357,6 +370,10 @@ int dMenu_Letter_c::_open() { } int dMenu_Letter_c::_close() { +#if TARGET_PC + enable_turn_page_controls(false); +#endif + s16 closeWindowFrame = g_drawHIO.mLetterSelectScreen.mCloseFrame[dMeter_drawLetterHIO_c::WINDOW_FRAME]; field_0x368 = 0; @@ -386,6 +403,10 @@ int dMenu_Letter_c::_close() { } void dMenu_Letter_c::wait_init() { +#if TARGET_PC + enable_turn_page_controls(field_0x374 > 1); +#endif + setAButtonString(0x40c); setBButtonString(0x3f9); } @@ -393,6 +414,12 @@ void dMenu_Letter_c::wait_init() { void dMenu_Letter_c::wait_move() { u8 oldIndex = mIndex; if (mDoGph_gInf_c::getFader()->getStatus() == 1) { +#if TARGET_PC + if (pointerWait()) { + return; + } +#endif + if (mDoCPd_c::getTrigB(PAD_1) != 0) { mpDrawCursor->offPlayAnime(0); mStatus = 3; @@ -448,8 +475,40 @@ void dMenu_Letter_c::wait_move() { } } +#if TARGET_PC +bool dMenu_Letter_c::pointerWait() { + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Collection); + for (u8 i = 0; i < field_0x373; ++i) { + if (!dusk::menu_pointer::hit_pane(mpLetterParent[i], 8.0f)) { + continue; + } + + if (mIndex != i) { + mIndex = i; + changeActiveColor(); + Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_ITEM, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + } + if (dusk::menu_pointer::consume_click()) { + mProcess = 3; + Z2GetAudioMgr()->seStart(Z2SE_SY_LETTER_OPEN, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + dMeter2Info_set2DVibration(); + return true; + } + return false; + } + + return false; +} +#endif + void dMenu_Letter_c::slide_right_init() { +#if TARGET_PC + enable_turn_page_controls(false); +#endif + field_0x358 = -field_0x1ec->getWidth() * mDoGph_gInf_c::getInvScale(); field_0x35c = field_0x1ec->getWidth() IF_NOT_DUSK(* mDoGph_gInf_c::getInvScale()); changePageLight(); @@ -467,6 +526,10 @@ void dMenu_Letter_c::slide_right_move() { } void dMenu_Letter_c::slide_left_init() { +#if TARGET_PC + enable_turn_page_controls(false); +#endif + field_0x358 = field_0x1ec->getWidth() * mDoGph_gInf_c::getInvScale(); field_0x35c = -field_0x1ec->getWidth() IF_NOT_DUSK(* mDoGph_gInf_c::getInvScale()); changePageLight(); @@ -484,6 +547,10 @@ void dMenu_Letter_c::slide_left_move() { } void dMenu_Letter_c::read_open_init() { +#if TARGET_PC + enable_turn_page_controls(false); +#endif + field_0x36a = 0; u8 idx = field_0x3ac[field_0x36f * 6 + mIndex] - 1; field_0x3e3 = 1; diff --git a/src/d/d_menu_option.cpp b/src/d/d_menu_option.cpp index 62f30d2d99..cc4bb01f2a 100644 --- a/src/d/d_menu_option.cpp +++ b/src/d/d_menu_option.cpp @@ -26,6 +26,11 @@ #include "JSystem/JAudio2/JASDriverIF.h" +#if TARGET_PC +#include "dusk/menu_pointer.h" +#include "dusk/ui/touch_controls.hpp" +#endif + typedef void (dMenu_Option_c::*initFunc)(); static initFunc init[] = { &dMenu_Option_c::atten_init, @@ -293,6 +298,10 @@ void dMenu_Option_c::_create() { } void dMenu_Option_c::_delete() { +#if TARGET_PC + dusk::ui::set_control_override(dusk::ui::Control::Z, dusk::ui::ControlOverride::Default); +#endif + JKR_DELETE(mpString); mpString = NULL; @@ -518,6 +527,15 @@ void dMenu_Option_c::_move() { (this->*init[field_0x3ef])(); } } + +#if TARGET_PC + if (field_0x3f4 == 5 && field_0x3ef != SelectType3 && field_0x3f3 == 5 && + field_0x3ef != SelectType4 && field_0x3ef != SelectType5 && field_0x3ef != SelectType6 && + field_0x3ef != SelectType7 && pointerConfirmSelect()) + { + goto skip; + } +#endif } skip: u8 oldValue = field_0x3ef; @@ -1074,6 +1092,34 @@ void dMenu_Option_c::confirm_move_move() { bool leftTrigger = checkLeftTrigger(); bool rightTrigger = checkRightTrigger(); +#if TARGET_PC + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options); + for (u8 i = 0; i < 2; ++i) { + if (!dusk::menu_pointer::hit_pane(mpYesNoSelBase_c[i], 8.0f)) { + continue; + } + if (field_0x3f9 != i) { + Z2GetAudioMgr()->seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + field_0x3fa = field_0x3f9; + field_0x3f9 = i; + yesnoSelectAnmSet(); + field_0x3ef = SelectType6; + mpWarning->_move(); + setAnimation(); + return; + } + if (dusk::menu_pointer::consume_click()) { + yesNoSelectStart(); + field_0x3ef = SelectType7; + dMeter2Info_set2DVibrationM(); + mpWarning->_move(); + setAnimation(); + return; + } + } +#endif + if (mDoCPd_c::getTrigA(PAD_1) != 0) { yesNoSelectStart(); field_0x3ef = SelectType7; @@ -2063,6 +2109,11 @@ void dMenu_Option_c::cursorAnime(f32 i_cursorValue) { } void dMenu_Option_c::setZButtonString(u16 i_stringID) { +#if TARGET_PC + dusk::ui::set_control_override(dusk::ui::Control::Z, + i_stringID != 0 ? dusk::ui::ControlOverride::Action : dusk::ui::ControlOverride::Default); +#endif + if (i_stringID == 0) { for (int i = 0; i < 3; i++) { if (mpZButtonText[i] != NULL) { @@ -2142,7 +2193,88 @@ bool dMenu_Option_c::isRumbleSupported() { return JUTGamePad::sRumbleSupported >> 0x1f; } +#if TARGET_PC +bool dMenu_Option_c::pointerConfirmSelect() { + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options); + if (!dusk::menu_pointer::state().clicked) { + return false; + } + + for (u8 i = 0; i < SelectType3; ++i) { + if (dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) { + return false; + } + } + + if (!dusk::menu_pointer::consume_click()) { + return false; + } + + field_0x3f7 = 1; + field_0x3f5 = field_0x3ef; + field_0x3ef = SelectType4; + dMeter2Info_set2DVibration(); + (this->*init[field_0x3ef])(); + return true; +} +#endif + bool dMenu_Option_c::dpdMenuMove() { +#if TARGET_PC + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Options); + for (u8 i = 0; i < SelectType3; ++i) { + if (!dusk::menu_pointer::hit_pane(mpMenuPane[i], 8.0f)) { + continue; + } + if (getSelectType() != i) { + field_0x3ef = i; + setCursorPos(i); + Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_OPTION, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + } + if (!dusk::menu_pointer::consume_click()) { + return true; + } + + switch (i) { + case SelectType0: + field_0x3e4 ^= 1; + field_0x3da = 5; + field_0x3ef = SelectType3; + field_0x3f5 = SelectType0; + Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + return true; + case SelectType1: + if (isRumbleSupported()) { + field_0x3ea ^= 1; + if (field_0x3ea != 0) { + mDoCPd_c::startMotorWave(0, &field_0x3e0, JUTGamePad::CRumble::VAL_0, 0x3c); + } + field_0x3da = 5; + field_0x3ef = SelectType3; + field_0x3f5 = SelectType1; + Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + } + return true; + case SelectType2: + 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 = SelectType3; + field_0x3f5 = SelectType2; + Z2GetAudioMgr()->seStart(Z2SE_SY_OPTION_SWITCH, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + return true; + } + } +#endif return false; } diff --git a/src/d/d_menu_ring.cpp b/src/d/d_menu_ring.cpp index a468ba889e..fde1f35af8 100644 --- a/src/d/d_menu_ring.cpp +++ b/src/d/d_menu_ring.cpp @@ -31,7 +31,9 @@ #if TARGET_PC #include "dusk/game_clock.h" +#include "dusk/menu_pointer.h" #include "dusk/settings.h" +#include "dusk/ui/touch_controls.hpp" #endif typedef void (dMenu_Ring_c::*initFunc)(); @@ -614,6 +616,9 @@ void dMenu_Ring_c::_delete() { * initializes a new process if mStatus changes */ void dMenu_Ring_c::_move() { +#if TARGET_PC + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::ItemWheel); +#endif mRingRadiusH = g_ringHIO.mRingRadiusH; mRingRadiusV = g_ringHIO.mRingRadiusV; mOldStatus = mStatus; // Save current status for check @@ -1517,6 +1522,11 @@ void dMenu_Ring_c::stick_wait_proc() { setDoStatus(0); return; } +#if TARGET_PC + if (pointerMove()) { + return; + } +#endif if (dMw_A_TRIGGER() && !dMeter2Info_isTouchKeyCheck(0xe)) { Z2GetAudioMgr()->seStart(Z2SE_SYS_ERROR, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0); } @@ -1528,6 +1538,49 @@ void dMenu_Ring_c::stick_wait_proc() { } } +#if TARGET_PC +bool dMenu_Ring_c::pointerMove() { + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::ItemWheel); + const auto& pointer = dusk::menu_pointer::state(); + if (!pointer.valid || mItemsTotal == 0) { + return false; + } + + int hoveredSlot = -1; + f32 bestDistance = 42.0f; + for (u8 i = 0; i < mItemsTotal; ++i) { + const f32 x = mItemSlotPosX[i] + mCenterPosX; + const f32 y = mItemSlotPosY[i] + mCenterPosY; + const f32 distance = calcDistance(pointer.x, pointer.y, x, y); + if (distance < bestDistance) { + bestDistance = distance; + hoveredSlot = i; + } + } + + if (hoveredSlot < 0) { + return false; + } + + if (mCurrentSlot != hoveredSlot) { + mDirectSelectCursorPos.x = mItemSlotPosX[mCurrentSlot]; + mDirectSelectCursorPos.z = mItemSlotPosY[mCurrentSlot]; + mCurrentSlot = hoveredSlot; + mDirectSelectActive = true; + field_0x670 = field_0x63e[mCurrentSlot]; + setStatus(STATUS_MOVE); + field_0x6b2 = 0; + return true; + } + + if (dusk::menu_pointer::consume_click()) { + return true; + } + + return false; +} +#endif + void dMenu_Ring_c::stick_move_init() { if (mCursorSpeed == 0) { mCursorSpeed = g_ringHIO.mCursorInitSpeed; @@ -1672,12 +1725,40 @@ void dMenu_Ring_c::drawSelectItem() { #else if (field_0x674[i] < 10) { #endif +#if TARGET_PC + f32 initSizeX; + f32 initSizeY; + f32 initScaleX; + f32 initScaleY; + Vec pos; + dusk::ui::EquipTarget touchTarget; + if (dusk::ui::get_equip_target(i, touchTarget)) { + initSizeX = touchTarget.width; + initSizeY = touchTarget.height; + initScaleX = 1.0f; + initScaleY = 1.0f; + pos.x = touchTarget.left; + pos.y = touchTarget.top; + pos.z = 0.0f; + } else { + CPaneMgr* meterItemPane = dMeter2Info_getMeterItemPanePtr(i); + if (meterItemPane == NULL) { + continue; + } + initSizeX = meterItemPane->getInitSizeX() * 1.7f; + initSizeY = meterItemPane->getInitSizeY() * 1.7f; + initScaleX = meterItemPane->getInitScaleX(); + initScaleY = meterItemPane->getInitScaleY(); + pos = meterItemPane->getGlobalVtxCenter(meterItemPane->mPane, true, 0); + } +#else f32 initSizeX = dMeter2Info_getMeterItemPanePtr(i)->getInitSizeX() * 1.7f; f32 initSizeY = dMeter2Info_getMeterItemPanePtr(i)->getInitSizeY() * 1.7f; f32 initScaleX = dMeter2Info_getMeterItemPanePtr(i)->getInitScaleX(); f32 initScaleY = dMeter2Info_getMeterItemPanePtr(i)->getInitScaleY(); Vec pos = dMeter2Info_getMeterItemPanePtr(i)->getGlobalVtxCenter( dMeter2Info_getMeterItemPanePtr(i)->mPane, true, 0); +#endif #if TARGET_PC f32 fVar14 = 0.1f + 0.8f * u; diff --git a/src/d/d_menu_save.cpp b/src/d/d_menu_save.cpp index d73e4ff5e8..d272c57500 100644 --- a/src/d/d_menu_save.cpp +++ b/src/d/d_menu_save.cpp @@ -18,11 +18,15 @@ #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_graphic.h" #include "d/d_msg_scrn_explain.h" -#include "dusk/frame_interpolation.h" -#include "dusk/settings.h" #include "JSystem/J2DGraph/J2DAnmLoader.h" #include "f_op/f_op_msg_mng.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#include "dusk/menu_pointer.h" +#include "dusk/settings.h" +#endif + static int SelStartFrameTbl[3] = { 59, 99, @@ -54,6 +58,17 @@ static int YnSelStartFrameTbl[2][2] = { static int YnSelEndFrameTbl[2][2] = {{2138, 3171}, {2150, 3181}}; +#if TARGET_PC +namespace { +constexpr u8 pointer_target(u8 group, u8 index) noexcept { + return static_cast((group << 4) | (index & 0x0F)); +} + +constexpr u8 s_pointerSaveSelectTarget = 0; +constexpr u8 s_pointerYesNoSelectTarget = 1; +} // namespace +#endif + static dMs_HIO_c g_msHIO; dMs_HIO_c::dMs_HIO_c() { @@ -1766,6 +1781,12 @@ void dMenu_save_c::openSaveSelect3() { void dMenu_save_c::saveSelect() { if (!mDoRst::isReset()) { +#if TARGET_PC + if (pointerSaveSelect()) { + return; + } +#endif + stick->checkTrigger(); if (mDoCPd_c::getTrigA(PAD_1)) { @@ -1792,7 +1813,84 @@ void dMenu_save_c::saveSelect() { } } +#if TARGET_PC +bool dMenu_save_c::pointerSaveSelect() { + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Save); + for (u8 i = 0; i < 3; ++i) { + if (!dusk::menu_pointer::hit_pane(mpSelData[i], 8.0f)) { + continue; + } + const bool clicked = dusk::menu_pointer::consume_click(); + if (mSelectedFile != i) { + mDoAud_seStart(Z2SE_FILE_SELECT_CURSOR, NULL, 0, 0); + mLastSelFile = mSelectedFile; + mSelectedFile = i; + if (clicked) { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::Save, + pointer_target(s_pointerSaveSelectTarget, i)); + } + dataSelectAnmSet(); + mMenuProc = PROC_SAVE_SELECT_MOVE_ANM; + return true; + } + if (clicked) { + saveSelectStart(); + return true; + } + } + return false; +} + +bool dMenu_save_c::pointerYesNoSelect(bool errorSelect, u8 errParam, u8 soundParam) { + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Save); + for (u8 i = 0; i < 2; ++i) { + if (!dusk::menu_pointer::hit_pane(mpNoYes[i], 8.0f)) { + continue; + } + const bool clicked = + (!errorSelect || mYesNoCursor == i) && dusk::menu_pointer::consume_click(); + if (mYesNoCursor != i) { + if (errorSelect) { + errCurMove(errParam, soundParam); + return false; + } + mDoAud_seStart(Z2SE_SY_MENU_CURSOR_COMMON, NULL, 0, 0); + mYesNoPrevCursor = mYesNoCursor; + mYesNoCursor = i; + if (clicked) { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::Save, + pointer_target(s_pointerYesNoSelectTarget, i)); + } + yesnoSelectAnmSet(0); + mMenuProc = PROC_YES_NO_CURSOR_MOVE_ANM; + return true; + } + if (clicked) { + if (errorSelect) { + if (mYesNoCursor != CURSOR_NO) { + if (soundParam == 0) { + mDoAud_seStart(Z2SE_SY_CURSOR_OK, NULL, 0, 0); + } + } else if (soundParam == 0) { + mDoAud_seStart(Z2SE_SY_CURSOR_CANCEL, NULL, 0, 0); + } + mSelIcon->setAlphaRate(0.0f); + } else { + yesnoSelectStart(); + } + return true; + } + } + return false; +} +#endif + void dMenu_save_c::saveSelectStart() { +#if TARGET_PC + dusk::menu_pointer::clear_deferred_activation(dusk::menu_pointer::Context::Save); +#endif mDoAud_seStart(Z2SE_SY_CURSOR_OK, NULL, 0, 0); selectDataMoveAnmInitSet(SelOpenStartFrameTbl[mSelectedFile], SelOpenEndFrameTbl[mSelectedFile]); @@ -1851,6 +1949,17 @@ void dMenu_save_c::dataSelectAnmSet() { } void dMenu_save_c::saveSelectMoveAnime() { +#if TARGET_PC + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Save); + if (mSelectedFile != 0xFF && + dusk::menu_pointer::hit_pane(mpSelData[mSelectedFile], 8.0f) && + dusk::menu_pointer::consume_click()) + { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::Save, + pointer_target(s_pointerSaveSelectTarget, mSelectedFile)); + } +#endif bool bookWakuAnmComplete = true; bool selWakuAnmComplete = true; bool var_r29 = true; @@ -1900,12 +2009,26 @@ void dMenu_save_c::saveSelectMoveAnime() { if (mLastSelFile != 0xFF) { mpSelData[mLastSelFile]->getPanePtr()->setAnimation((J2DAnmTransformKey*)NULL); } +#if TARGET_PC + if (dusk::menu_pointer::consume_deferred_activation( + dusk::menu_pointer::Context::Save, + pointer_target(s_pointerSaveSelectTarget, mSelectedFile))) { + saveSelectStart(); + return; + } +#endif mMenuProc = PROC_SAVE_SELECT; } } void dMenu_save_c::saveYesNoSelect() { if (!mDoRst::isReset()) { +#if TARGET_PC + if (pointerYesNoSelect(false)) { + return; + } +#endif + stick->checkTrigger(); if (mDoCPd_c::getTrigA(PAD_1)) { @@ -1933,6 +2056,9 @@ void dMenu_save_c::saveYesNoSelect() { } void dMenu_save_c::yesnoSelectStart() { +#if TARGET_PC + dusk::menu_pointer::clear_deferred_activation(dusk::menu_pointer::Context::Save); +#endif if (mYesNoCursor != CURSOR_NO) { mDoAud_seStart(Z2SE_SY_CURSOR_OK, NULL, 0, 0); mSelIcon->setAlphaRate(0.0f); @@ -2001,11 +2127,30 @@ void dMenu_save_c::yesnoSelectAnmSet(u8 param_0) { } void dMenu_save_c::yesNoCursorMoveAnm() { +#if TARGET_PC + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Save); + if (mYesNoCursor != 0xFF && + dusk::menu_pointer::hit_pane(mpNoYes[mYesNoCursor], 8.0f) && + dusk::menu_pointer::consume_click()) + { + dusk::menu_pointer::defer_activation( + dusk::menu_pointer::Context::Save, + pointer_target(s_pointerYesNoSelectTarget, mYesNoCursor)); + } +#endif bool selAnmComplete = yesnoSelectMoveAnm(0); bool wakuAnmComplete = yesnoWakuAlpahAnm(mYesNoPrevCursor); if (selAnmComplete == true && wakuAnmComplete == true) { yesnoCursorShow(); +#if TARGET_PC + if (dusk::menu_pointer::consume_deferred_activation( + dusk::menu_pointer::Context::Save, + pointer_target(s_pointerYesNoSelectTarget, mYesNoCursor))) { + yesnoSelectStart(); + return; + } +#endif mMenuProc = PROC_SAVE_YES_NO_SELECT; } } @@ -2181,6 +2326,12 @@ bool dMenu_save_c::errYesNoSelect(u8 param_0, u8 param_1) { return false; } +#if TARGET_PC + if (pointerYesNoSelect(true, param_0, param_1)) { + return true; + } +#endif + stick->checkTrigger(); if (mDoCPd_c::getTrigA(PAD_1)) { diff --git a/src/d/d_menu_skill.cpp b/src/d/d_menu_skill.cpp index 2692d33c3f..601bae1eb6 100644 --- a/src/d/d_menu_skill.cpp +++ b/src/d/d_menu_skill.cpp @@ -18,6 +18,10 @@ #include "m_Do/m_Do_graphic.h" #include +#if TARGET_PC +#include "dusk/menu_pointer.h" +#endif + typedef void (dMenu_Skill_c::*initFunc)(); static initFunc map_init_process[] = { &dMenu_Skill_c::wait_init, @@ -275,6 +279,12 @@ void dMenu_Skill_c::wait_init() { void dMenu_Skill_c::wait_move() { u8 oldIndex = mIndex; if (mDoGph_gInf_c::getFader()->getStatus() == 1) { +#if TARGET_PC + if (pointerWait()) { + return; + } +#endif + if (mDoCPd_c::getTrigB(PAD_1) != 0) { mpDrawCursor->offPlayAnime(0); mStatus = 3; @@ -299,6 +309,34 @@ void dMenu_Skill_c::wait_move() { } } +#if TARGET_PC +bool dMenu_Skill_c::pointerWait() { + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Collection); + for (u8 i = 0; i < mSkillNum; ++i) { + if (!dusk::menu_pointer::hit_pane(mpLetterParent[i], 8.0f)) { + continue; + } + + if (mIndex != i) { + mIndex = i; + changeActiveColor(); + Z2GetAudioMgr()->seStart(Z2SE_SY_CURSOR_ITEM, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + } + if (dusk::menu_pointer::consume_click()) { + mProcess = PROC_WAIT_MOVE; + Z2GetAudioMgr()->seStart(Z2SE_SY_EXP_WIN_OPEN, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + dMeter2Info_set2DVibration(); + return true; + } + return false; + } + + return false; +} +#endif + void dMenu_Skill_c::read_open_init() { static const u32 i_id[7] = { 1716, 1715, 1717, 1718, 1719, 1720, 1721, diff --git a/src/d/d_meter2.cpp b/src/d/d_meter2.cpp index 7675bbfb0a..f628466919 100644 --- a/src/d/d_meter2.cpp +++ b/src/d/d_meter2.cpp @@ -663,8 +663,15 @@ void dMeter2_c::moveLife() { draw_life = true; } - if (mLifeGaugeScale != g_drawHIO.mLifeParentScale) { - mLifeGaugeScale = g_drawHIO.mLifeParentScale; +#if TARGET_PC + const f32 lifeGaugeScale = + g_drawHIO.mLifeParentScale * + std::clamp(dusk::getSettings().game.hudScale.getValue(), 0.5f, 2.0f); +#else + const f32 lifeGaugeScale = g_drawHIO.mLifeParentScale; +#endif + if (mLifeGaugeScale != lifeGaugeScale) { + mLifeGaugeScale = lifeGaugeScale; draw_life = true; } @@ -2966,7 +2973,15 @@ void dMeter2_c::alphaAnimeButtonCross() { field_0x190++; } } else { +#if TARGET_PC + if (dusk::getSettings().game.enableTouchControls) { + mpMeterDraw->setAlphaButtonCrossAnimeMin(); + } else { + mpMeterDraw->setAlphaButtonCrossAnimeMax(); + } +#else mpMeterDraw->setAlphaButtonCrossAnimeMax(); +#endif if (field_0x190 < 5) { field_0x190++; diff --git a/src/d/d_meter2_draw.cpp b/src/d/d_meter2_draw.cpp index f4f044d445..86779d4448 100644 --- a/src/d/d_meter2_draw.cpp +++ b/src/d/d_meter2_draw.cpp @@ -25,6 +25,7 @@ #if TARGET_PC #include "dusk/settings.h" +#include "dusk/ui/icon_provider.hpp" #include namespace { @@ -653,10 +654,22 @@ void dMeter2Draw_c::draw() { J2DGrafContext* graf_ctx = dComIfGp_getCurrentGrafPort(); graf_ctx->setup2D(); +#if TARGET_PC + const bool touchControlsEnabled = dusk::getSettings().game.enableTouchControls; + if (touchControlsEnabled) { + mpButtonParent->hide(); + } else { + mpButtonParent->show(); + } +#endif + mpScreen->draw(0.0f, 0.0f, graf_ctx); drawKanteraScreen(1); drawKanteraScreen(2); +#if TARGET_PC + if (!touchControlsEnabled) { +#endif for (int i = 0; i < 2; i++) { if (mpItemXY[i] != NULL) { for (int j = 0; j < 3; j++) { @@ -705,6 +718,9 @@ void dMeter2Draw_c::draw() { } } } +#if TARGET_PC + } +#endif if (mpLightDropParent->getAlphaRate() != 0.0f) { f32 var_f28 = g_drawHIO.mLightDrop.mPikariScaleNormal; @@ -788,7 +804,11 @@ void dMeter2Draw_c::draw() { } } +#if TARGET_PC + if (!touchControlsEnabled && field_0x738 > 0.0f) { +#else if (field_0x738 > 0.0f) { +#endif drawPikari(mpButtonMidona, &field_0x738, g_drawHIO.mMidnaIconPikariScale, g_drawHIO.mMidnaIconPikariFrontOuter, g_drawHIO.mMidnaIconPikariFrontInner, g_drawHIO.mMidnaIconPikariBackOuter, g_drawHIO.mMidnaIconPikariBackInner, @@ -2480,6 +2500,11 @@ void dMeter2Draw_c::drawButtonB(u8 i_action, bool param_1, f32 i_posX, f32 i_pos SAFE_STRCPY(static_cast(mpBText[i]->getPanePtr())->getStringPtr(), mp_string); } +#if TARGET_PC + if (dusk::getSettings().game.enableTouchControls) { + mpScreen->search(MULTI_CHAR('item_b_n'))->hide(); + } else +#endif if (i_action == 0x26 || i_action == 0x2E) { mpScreen->search(MULTI_CHAR('item_b_n'))->show(); var_r31 = 1; @@ -2757,6 +2782,12 @@ void dMeter2Draw_c::drawButtonXY(int i_no, u8 i_itemNo, u8 i_action, bool param_ mpTextXY[i_no]->scale(g_drawHIO.mButtonXYTextScale, g_drawHIO.mButtonXYTextScale); mpTextXY[i_no]->paneTrans(g_drawHIO.mButtonXYTextPosX, g_drawHIO.mButtonXYTextPosY); } + +#if TARGET_PC + if (dusk::getSettings().game.enableTouchControls) { + mpScreen->search(tag[i_no])->hide(); + } +#endif } } @@ -3322,6 +3353,10 @@ void dMeter2Draw_c::setButtonIconMidonaAlpha(u32 param_0) { } mpButtonXY[2]->setAlpha(255.0f * field_0x724 * temp_f30_2); + +#if TARGET_PC + dusk::ui::update_midna_icon_texture(mpButtonMidona != NULL ? mpButtonMidona->getPanePtr() : NULL); +#endif } void dMeter2Draw_c::setButtonIconAlpha(int i_no, u8 unused0, u32 unused1, bool unused2) { diff --git a/src/d/d_meter_map.cpp b/src/d/d_meter_map.cpp index c03214b963..bffdbb8931 100644 --- a/src/d/d_meter_map.cpp +++ b/src/d/d_meter_map.cpp @@ -22,6 +22,10 @@ #endif #include +#if TARGET_PC +#include "dusk/action_bindings.h" +#endif + #if (PLATFORM_WII || PLATFORM_SHIELD) dMeter_map_HIO_c g_meter_mapHIO; #endif @@ -539,6 +543,12 @@ void dMeterMap_c::_move(u32 param_0) { } #endif +#if TARGET_PC + if (mMap->refreshTextureSize()) { + mMapJ2DPicture->changeTexture(mMap->getResTIMGPointer(), 0); + } +#endif + int stayNo = dComIfGp_roomControl_getStayNo(); field_0x14 = param_0; @@ -732,7 +742,38 @@ void dMeterMap_c::ctrlShowMap() { } } - } else if (!mDoCPd_c::getTrigUp(PAD_1) && !mDoCPd_c::getTrigDown(PAD_1)) { + } +#if TARGET_PC + else if (!isEventRunCheck() && + (dMeter2Info_getMapStatus() == 0 || dMeter2Info_getMapStatus() == 1) && + !dMeter2Info_isSub2DStatus(1) && (isFmapScreen() || isDmapScreen()) && + dusk::getActionBindTrig(dusk::ActionBinds::OPEN_MAP_SCREEN, PAD_1)) + { + dMeter2Info_setMapStatus(2); + dMeter2Info_setMapKeyDirection(0x400); + Z2GetAudioMgr()->seStart(Z2SE_SY_MAP_OPEN_S, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + dMeter2Info_set2DVibration(); + } else if (!isEventRunCheck() && + (dMeter2Info_getMapStatus() == 0 || dMeter2Info_getMapStatus() == 1) && + isEnableDispMapAndMapDispSizeTypeNo() && + dusk::getActionBindTrig(dusk::ActionBinds::TOGGLE_MINIMAP, PAD_1)) + { + if (isDispPosInsideFlg()) { + setDispPosOutsideFlg_SE_On(); + Z2GetAudioMgr()->seStart(Z2SE_SY_MAP_CLOSE_S, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + dMeter2Info_setMapStatus(0); + } else { + setDispPosInsideFlg_SE_On(); + Z2GetAudioMgr()->seStart(Z2SE_SY_MAP_OPEN_S, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + dMeter2Info_set2DVibration(); + dMeter2Info_setMapStatus(1); + } + } +#endif + else if (!mDoCPd_c::getTrigUp(PAD_1) && !mDoCPd_c::getTrigDown(PAD_1)) { keyCheck(); } @@ -827,7 +868,21 @@ void dMeterMap_c::meter_map_move(u32 param_0) { dMeter2Info_set2DVibration(); } dMeter2Info_resetPauseStatus(); - } else if ( + } +#if TARGET_PC + else if (!dComIfGp_event_runCheck() && !dMsgObject_isTalkNowCheck() && + (dMeter2Info_getMapStatus() == 0 || dMeter2Info_getMapStatus() == 1) && + !dMeter2Info_isSub2DStatus(1) && (isFmapScreen() || isDmapScreen()) && + dusk::getActionBindTrig(dusk::ActionBinds::OPEN_MAP_SCREEN, PAD_1)) + { + dMeter2Info_setMapStatus(2); + dMeter2Info_setMapKeyDirection(0x400); + Z2GetAudioMgr()->seStart(Z2SE_SY_MAP_OPEN_S, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + dMeter2Info_set2DVibration(); + } +#endif + else if ( #if DEBUG dMw_RIGHT_TRIGGER() && #else diff --git a/src/d/d_msg_class.cpp b/src/d/d_msg_class.cpp index 01d2ab48e6..8dc2a82c01 100644 --- a/src/d/d_msg_class.cpp +++ b/src/d/d_msg_class.cpp @@ -13,6 +13,7 @@ #include "JSystem/JUtility/JUTFont.h" #if TARGET_PC +#include "dusk/menu_pointer.h" #include "dusk/scope_guard.hpp" #endif @@ -575,6 +576,20 @@ void jmessage_tReference::pageSend() { void jmessage_tReference::selectMessage() { if (mSelectNum != 0) { +#if TARGET_PC + u8 pointerChoice = 0xFF; + if (dusk::menu_pointer::get_dialog_choice(pointerChoice) && pointerChoice < mSelectNum && + pointerChoice != mSelectPos) + { + mSelectPos = pointerChoice; + if (mSelectType != 0) { + getObjectPtr()->getSequenceProcessor()->calcStringLength(); + } + Z2GetAudioMgr()->seStart(Z2SE_SY_TALK_CURSOR, NULL, 0, 0, 1.0f, 1.0f, -1.0f, + -1.0f, 0); + } +#endif + mpStick->checkTrigger(); if (mSelectType == 0) { diff --git a/src/d/d_msg_object.cpp b/src/d/d_msg_object.cpp index 7f4ad951b3..39f6d64d16 100644 --- a/src/d/d_msg_object.cpp +++ b/src/d/d_msg_object.cpp @@ -26,12 +26,13 @@ #include #include "JSystem/JKernel/JKRExpHeap.h" -#include "dusk/version.hpp" #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_lib.h" #if TARGET_PC +#include "dusk/menu_pointer.h" #include "dusk/settings.h" +#include "dusk/version.hpp" #include #include #include @@ -1130,7 +1131,20 @@ void dMsgObject_c::selectProc() { dComIfGp_setAStatusForce(0x2a, 0); } } - if (mDoCPd_c::getTrigA(0)) { +#if TARGET_PC + jmessage_tReference* pRef = (jmessage_tReference*)mpRenProc->getReference(); + u8 pointerChoice = 0xFF; + bool pointerConfirm = dusk::menu_pointer::consume_dialog_click(pointerChoice) && + pointerChoice < pRef->getSelectNum(); + if (pointerConfirm) { + pRef->setSelectPos(pointerChoice); + } +#endif + if (mDoCPd_c::getTrigA(0) +#if TARGET_PC + || pointerConfirm +#endif + ) { if (getSelectCursorPosLocal() != 0xff) { field_0x1a3 = 1; } @@ -1152,7 +1166,9 @@ void dMsgObject_c::selectProc() { } field_0x1a3 = 2; } +#ifndef TARGET_PC jmessage_tReference* pRef = (jmessage_tReference*)mpRenProc->getReference(); +#endif if (getStatusLocal() == 8) { if (isMidonaMessage() && field_0x1a3 != 0) { if (field_0x1a3 == 2 && getSelectCancelPos() == 3) { diff --git a/src/d/d_msg_scrn_3select.cpp b/src/d/d_msg_scrn_3select.cpp index 68d0732481..b021b52dec 100644 --- a/src/d/d_msg_scrn_3select.cpp +++ b/src/d/d_msg_scrn_3select.cpp @@ -16,6 +16,17 @@ #include "d/d_msg_object.h" #include "d/d_pane_class.h" +#if TARGET_PC +#include "dusk/menu_pointer.h" + +namespace { +bool hit_choice_pane(CPaneMgr* pane, f32 padding) { + return pane != NULL && pane->getPanePtr() != NULL && pane->getPanePtr()->isVisible() && + dusk::menu_pointer::hit_pane(pane, padding); +} +} // namespace +#endif + typedef void (dMsgScrn3Select_c::*processFn)(); processFn process[] = { &dMsgScrn3Select_c::open1Proc, &dMsgScrn3Select_c::open2Proc, &dMsgScrn3Select_c::waitProc, @@ -470,6 +481,9 @@ bool dMsgScrn3Select_c::selAnimeMove(u8 i_selNum, u8 param_1, bool param_2) { mSelNum = i_selNum; field_0x114 = 0; field_0x108 = param_2; +#if TARGET_PC + pointerMove(); +#endif (this->*process[mProcess])(); @@ -518,6 +532,47 @@ bool dMsgScrn3Select_c::selAnimeMove(u8 i_selNum, u8 param_1, bool param_2) { return mProcess == PROC_SELECT_e ? TRUE : FALSE; } +#if TARGET_PC +bool dMsgScrn3Select_c::pointerMove() { + dusk::menu_pointer::begin_context(dusk::menu_pointer::Context::Dialog); + mDPDPoint = 0xFF; + + const u8 firstPane = mSelNum == 2 ? 1 : 0; + for (u8 choice = 0; choice < mSelNum; ++choice) { + const u8 paneIndex = firstPane + choice; + if (paneIndex >= 3) { + continue; + } + + // TODO: this sucks and should be replaced with Wii mpTouchArea + bool hit = hit_choice_pane(mpSel_c[paneIndex], 8.0f) || + hit_choice_pane(mpTmSel_c[paneIndex], 24.0f) || + hit_choice_pane(mpTmrSel_c[paneIndex], 24.0f) || + hit_choice_pane(mpKahen_c[paneIndex], 8.0f) || + hit_choice_pane(mpCursor_c[paneIndex], 8.0f); + for (int i = 0; i < 5 && !hit; ++i) { + hit = hit_choice_pane(mpSelCldw_c[i][paneIndex], 8.0f); + } + + if (!hit) { + continue; + } + + mDPDPoint = choice; + field_0x110 = paneIndex; + dusk::menu_pointer::set_dialog_choice(choice, dusk::menu_pointer::state().clicked); + return true; + } + + return false; +} + +bool dMsgScrn3Select_c::consumePointerClick() { + u8 choice = 0xFF; + return dusk::menu_pointer::consume_dialog_click(choice); +} +#endif + bool dMsgScrn3Select_c::selAnimeEnd() { if (mProcess == PROC_MAX_e) { return true; diff --git a/src/d/d_msg_scrn_explain.cpp b/src/d/d_msg_scrn_explain.cpp index bc6275e10c..29a88590ee 100644 --- a/src/d/d_msg_scrn_explain.cpp +++ b/src/d/d_msg_scrn_explain.cpp @@ -643,6 +643,10 @@ f32 dMsgScrnExplain_c::getAlphaRatio() { bool dMsgScrnExplain_c::checkTriggerA() { if (mDoCPd_c::getTrigA(PAD_1)) { return true; +#if TARGET_PC + } else if (mpSelect_c != NULL && mpSelect_c->consumePointerClick()) { + return true; +#endif } else { return false; } diff --git a/src/dusk/OSMutex.cpp b/src/dusk/OSMutex.cpp index ef1a528f1b..b3127eb6fc 100644 --- a/src/dusk/OSMutex.cpp +++ b/src/dusk/OSMutex.cpp @@ -181,20 +181,22 @@ void OSWaitCond(OSCond* cond, OSMutex* mutex) { mutex->count = 0; mutex->thread = nullptr; - // Unlock the recursive mutex the same number of times it was locked - for (s32 i = 0; i < savedCount; i++) { - mutexData.nativeMutex.unlock(); - } - - // Wait on the condition variable - { - std::unique_lock lock(mutexData.nativeMutex); + // Keep one recursion level held so cv.wait() is what releases the mutex; + // fully unlocking before the wait opens a window where a signal is lost. + if (savedCount >= 1) { + for (s32 i = 1; i < savedCount; i++) { + mutexData.nativeMutex.unlock(); + } + std::unique_lock lock(mutexData.nativeMutex, std::adopt_lock); + condData.cv.wait(lock); + lock.release(); + for (s32 i = 1; i < savedCount; i++) { + mutexData.nativeMutex.lock(); + } + } else { + // Mutex wasn't held on entry (contract violation); wait anyway. + std::unique_lock lock(mutexData.nativeMutex); condData.cv.wait(lock); - } - - // Re-lock the recursive mutex the same number of times - for (s32 i = 0; i < savedCount; i++) { - mutexData.nativeMutex.lock(); } // Restore GC mutex state diff --git a/src/dusk/action_bindings.cpp b/src/dusk/action_bindings.cpp index 204f219558..68b07d6c0a 100644 --- a/src/dusk/action_bindings.cpp +++ b/src/dusk/action_bindings.cpp @@ -8,10 +8,19 @@ namespace dusk { static std::array(ActionBinds::COUNT)>, PAD_CHANMAX> actionPressData{}; +struct VirtualActionBindData { + bool pressed = false; + bool available = false; +}; + +static std::array(ActionBinds::COUNT)>, PAD_CHANMAX> virtualActionData{}; + ActionBindsMap& getActionBinds() { static ActionBindsMap actionBinds = { {ActionBinds::FIRST_PERSON_CAMERA, {&getSettings().actionBindings.firstPersonCamera, "First Person Camera"}}, {ActionBinds::CALL_MIDNA, {&getSettings().actionBindings.callMidna, "Call Midna"}}, + {ActionBinds::OPEN_MAP_SCREEN, {&getSettings().actionBindings.openMapScreen, "Open Map Screen"}}, + {ActionBinds::TOGGLE_MINIMAP, {&getSettings().actionBindings.toggleMinimap, "Toggle Minimap"}}, {ActionBinds::OPEN_DUSKLIGHT_MENU, {&getSettings().actionBindings.openDusklightMenu, "Open Dusklight Menu"}}, {ActionBinds::TURBO_SPEED_BUTTON, {&getSettings().actionBindings.turboSpeedButton, "Turbo Speed Button"}}, }; @@ -25,6 +34,10 @@ bool isActionBound(ActionBinds action, u32 port) { return false; } + if (port < PAD_CHANMAX && virtualActionData[port][static_cast(action)].available) { + return true; + } + return getActionBindButton(action, port) != PAD_NATIVE_BUTTON_INVALID; } @@ -41,43 +54,71 @@ void updateActionBindings() { // If the action isn't bound, or if documents are visible and the action isn't // opening the dusklight menu, don't update. Otherwise, we may accidentally // perform actions while the dusklight menu is open. - if (!isActionBound(action, port) || + const int button = boundAction.configVars->at(port); + const bool virtualAvailable = virtualActionData[port][static_cast(action)].available; + if ((button == PAD_NATIVE_BUTTON_INVALID && !virtualAvailable) || (ui::any_document_visible() && action != ActionBinds::OPEN_DUSKLIGHT_MENU)) { continue; } - int button = boundAction.configVars->at(port); - - // If keyboard is active for this port - u32 count = 0; - if (PADGetKeyButtonBindings(port, &count) != nullptr) { - int numKeys = 0; - const bool* kbState = SDL_GetKeyboardState(&numKeys); - if (kbState[button]) { - actionPressData[port][static_cast(action)].pressedCurFrame = true; - } - } else { - // If controller is active - auto controller = aurora::input::get_controller_for_player(port); - if (controller) { - if (SDL_GetGamepadButton(controller->m_controller, static_cast(button))) { + if (button != PAD_NATIVE_BUTTON_INVALID) { + // If keyboard is active for this port + u32 count = 0; + if (PADGetKeyButtonBindings(port, &count) != nullptr) { + int numKeys = 0; + const bool* kbState = SDL_GetKeyboardState(&numKeys); + if (kbState[button]) { actionPressData[port][static_cast(action)].pressedCurFrame = true; } + } else { + // If controller is active + auto controller = aurora::input::get_controller_for_player(port); + if (controller) { + if (SDL_GetGamepadButton(controller->m_controller, static_cast(button))) { + actionPressData[port][static_cast(action)].pressedCurFrame = true; + } + } } } } + + for (auto& [action, _] : getActionBinds()) { + const auto& virtualAction = virtualActionData[port][static_cast(action)]; + if (virtualAction.available && virtualAction.pressed && !ui::any_document_visible()) { + actionPressData[port][static_cast(action)].pressedCurFrame = true; + } + } } } +void setVirtualActionBind(ActionBinds action, u32 port, bool pressed, bool available) { + if (port >= PAD_CHANMAX) { + return; + } + virtualActionData[port][static_cast(action)] = { + .pressed = pressed, + .available = available, + }; +} + +void clearVirtualActionBind(ActionBinds action, u32 port) { + if (port >= PAD_CHANMAX) { + return; + } + virtualActionData[port][static_cast(action)] = {}; +} + +void clearAllVirtualActionBinds() { + virtualActionData = {}; +} + bool getActionBindTrig(ActionBinds action, u32 port) { - return isActionBound(action, port) && - actionPressData[port][static_cast(action)].pressedCurFrame && + return actionPressData[port][static_cast(action)].pressedCurFrame && !actionPressData[port][static_cast(action)].pressedPrevFrame; } bool getActionBindHold(ActionBinds action, u32 port) { - return isActionBound(action, port) && - actionPressData[port][static_cast(action)].pressedCurFrame && + return actionPressData[port][static_cast(action)].pressedCurFrame && actionPressData[port][static_cast(action)].pressedPrevFrame; } diff --git a/src/dusk/android_frame_rate.cpp b/src/dusk/android_frame_rate.cpp new file mode 100644 index 0000000000..bf889a9482 --- /dev/null +++ b/src/dusk/android_frame_rate.cpp @@ -0,0 +1,74 @@ +#include "dusk/android_frame_rate.hpp" + +#if defined(TARGET_ANDROID) || defined(__ANDROID__) || defined(ANDROID) +#include "dusk/settings.h" + +#include +#include + +namespace dusk::android { +namespace { + +float preferred_surface_frame_rate() { + switch (getSettings().game.enableFrameInterpolation.getValue()) { + case FrameInterpMode::Off: + return 30.0f; + case FrameInterpMode::Unlimited: + default: + return 0.0f; + case FrameInterpMode::Capped: + return static_cast(getSettings().video.maxFrameRate.getValue()); + } +} + +bool clear_pending_exception(JNIEnv* env) { + if (env == nullptr || !env->ExceptionCheck()) { + return false; + } + env->ExceptionClear(); + return true; +} + +} // namespace + +void update_surface_frame_rate() { + auto* env = static_cast(SDL_GetAndroidJNIEnv()); + if (env == nullptr) { + return; + } + + jobject activity = static_cast(SDL_GetAndroidActivity()); + if (activity == nullptr || clear_pending_exception(env)) { + if (activity != nullptr) { + env->DeleteLocalRef(activity); + } + return; + } + + jclass activityClass = env->GetObjectClass(activity); + if (activityClass == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(activity); + return; + } + + jmethodID setPreferredFrameRate = + env->GetMethodID(activityClass, "setPreferredSurfaceFrameRate", "(F)V"); + env->DeleteLocalRef(activityClass); + if (setPreferredFrameRate == nullptr || clear_pending_exception(env)) { + env->DeleteLocalRef(activity); + return; + } + + jvalue args[1]{}; + args[0].f = preferred_surface_frame_rate(); + env->CallVoidMethodA(activity, setPreferredFrameRate, args); + env->DeleteLocalRef(activity); + clear_pending_exception(env); +} + +} // namespace dusk::android +#else +namespace dusk::android { +void update_surface_frame_rate() {} +} // namespace dusk::android +#endif diff --git a/src/dusk/android_frame_rate.hpp b/src/dusk/android_frame_rate.hpp new file mode 100644 index 0000000000..03a7876633 --- /dev/null +++ b/src/dusk/android_frame_rate.hpp @@ -0,0 +1,7 @@ +#pragma once + +namespace dusk::android { + +void update_surface_frame_rate(); + +} // namespace dusk::android diff --git a/src/dusk/batch.cpp b/src/dusk/batch.cpp new file mode 100644 index 0000000000..04cba52b0d --- /dev/null +++ b/src/dusk/batch.cpp @@ -0,0 +1,72 @@ +#include "dusk/batch.hpp" +#include "dusk/logging.h" + +#include +#include + +namespace dusk::batch { + +void decode_leaf_template(const u8* dl, u32 size, LeafTemplate& out) { + out.vtxCount = 0; + out.posRefCount = 0; + bool posSeen[256] = {}; + + static constexpr GXVtxDescList kLeafDesc[] = { + {GX_VA_POS, GX_INDEX8}, + {GX_VA_NRM, GX_INDEX8}, + {GX_VA_CLR0, GX_INDEX8}, + {GX_VA_TEX0, GX_INDEX8}, + {GX_VA_NULL, GX_NONE}, + }; + + aurora::gx::dl::Reader reader{dl, size, kLeafDesc}; + while (const auto cmd = reader.next()) { + if (cmd->kind == aurora::gx::dl::Command::Kind::Passthrough) { + if (cmd->data[0] != GX_NOP) { + DuskLog.fatal("decode_leaf_template: unexpected opcode {:#x}", cmd->data[0]); + } + continue; + } + if (cmd->kind != aurora::gx::dl::Command::Kind::Draw) { + DuskLog.fatal("decode_leaf_template: unexpected pre-optimized draw"); + } + + const auto& draw = cmd->draw; + bool overflow = false; + const bool expanded = + aurora::gx::dl::expand_triangles(draw.prim, draw.vtxCount, [&](u16 i0, u16 i1, u16 i2) { + if (overflow || out.vtxCount + 3 > LeafTemplate::kMaxVtx) { + overflow = true; + return; + } + for (const u16 elem : {i0, i1, i2}) { + LeafTemplate::Vtx& v = out.vtx[out.vtxCount++]; + v.pos = draw.attr_idx(elem, GX_VA_POS); + v.nrm = draw.attr_idx(elem, GX_VA_NRM); + v.clr = draw.attr_idx(elem, GX_VA_CLR0); + v.tex = draw.attr_idx(elem, GX_VA_TEX0); + if (!posSeen[v.pos]) { + posSeen[v.pos] = true; + if (out.posRefCount >= LeafTemplate::kMaxPosRefs) { + overflow = true; + return; + } + out.posRefs[out.posRefCount++] = v.pos; + } + } + }); + if (!expanded) { + DuskLog.fatal("decode_leaf_template: untriangulable draw (prim {:#x}, {} verts)", + static_cast(draw.prim), draw.vtxCount); + } + if (overflow) { + DuskLog.fatal("decode_leaf_template: template overflow ({} verts, {} positions)", + out.vtxCount, out.posRefCount); + } + } + if (reader.failed()) { + DuskLog.fatal("decode_leaf_template: failed to walk display list"); + } +} + +} // namespace dusk::batch diff --git a/src/dusk/batch.hpp b/src/dusk/batch.hpp new file mode 100644 index 0000000000..2569f27761 --- /dev/null +++ b/src/dusk/batch.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace dusk::batch { + +struct LeafTemplate { + static constexpr u32 kMaxVtx = 192; + static constexpr u32 kMaxPosRefs = 64; + + struct Vtx { + u8 pos; + u8 nrm; + u8 clr; + u8 tex; + }; + Vtx vtx[kMaxVtx]; + u16 vtxCount = 0; + u8 posRefs[kMaxPosRefs]; + u8 posRefCount = 0; +}; + +void decode_leaf_template(const u8* dl, u32 size, LeafTemplate& out); + +} // namespace dusk diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index 31e4a80b05..fa331eb518 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -1,19 +1,23 @@ #include "dusk/config.hpp" +#include "absl/container/flat_hash_map.h" #include "fmt/format.h" #include "nlohmann/json.hpp" -#include "absl/container/flat_hash_map.h" #include "aurora/lib/logging.hpp" #include "dusk/io.hpp" #include "dusk/settings.h" -#include +#include #include -#include +#include +#include #include +#include +#include +#include -#include "dusk/main.h" #include "dusk/action_bindings.h" +#include "dusk/main.h" using namespace dusk::config; @@ -26,6 +30,104 @@ aurora::Module DuskConfigLog("dusk::config"); static absl::flat_hash_map RegisteredConfigVars; static bool RegistrationDone = false; +static std::optional parse_control_anchor(std::string_view value) { + if (value == "none") { + return dusk::ui::ControlAnchor::None; + } + if (value == "top") { + return dusk::ui::ControlAnchor::Top; + } + if (value == "left") { + return dusk::ui::ControlAnchor::Left; + } + if (value == "bottom") { + return dusk::ui::ControlAnchor::Bottom; + } + if (value == "right") { + return dusk::ui::ControlAnchor::Right; + } + if (value == "topLeft") { + return dusk::ui::ControlAnchor::TopLeft; + } + if (value == "topRight") { + return dusk::ui::ControlAnchor::TopRight; + } + if (value == "bottomLeft") { + return dusk::ui::ControlAnchor::BottomLeft; + } + if (value == "bottomRight") { + return dusk::ui::ControlAnchor::BottomRight; + } + return std::nullopt; +} + +static const char* control_anchor_value(dusk::ui::ControlAnchor anchor) { + switch (anchor) { + case dusk::ui::ControlAnchor::None: + return "none"; + case dusk::ui::ControlAnchor::Top: + return "top"; + case dusk::ui::ControlAnchor::Left: + return "left"; + case dusk::ui::ControlAnchor::Bottom: + return "bottom"; + case dusk::ui::ControlAnchor::Right: + return "right"; + case dusk::ui::ControlAnchor::TopLeft: + return "topLeft"; + case dusk::ui::ControlAnchor::TopRight: + return "topRight"; + case dusk::ui::ControlAnchor::BottomLeft: + return "bottomLeft"; + case dusk::ui::ControlAnchor::BottomRight: + return "bottomRight"; + } + return "none"; +} + +static std::optional json_finite_float(const json& object, const char* key) { + const auto iter = object.find(key); + if (iter == object.end() || !iter->is_number()) { + return std::nullopt; + } + + const float value = iter->get(); + if (!std::isfinite(value)) { + return std::nullopt; + } + + return value; +} + +static std::optional parse_control_props(const json& value) { + if (!value.is_object()) { + return std::nullopt; + } + + const auto x = json_finite_float(value, "x"); + const auto y = json_finite_float(value, "y"); + const auto w = json_finite_float(value, "w"); + const auto h = json_finite_float(value, "h"); + const auto scale = json_finite_float(value, "scale"); + const auto anchorIter = value.find("anchor"); + if (!x || !y || !w || !h || !scale || anchorIter == value.end() || !anchorIter->is_string()) { + return std::nullopt; + } + + const auto anchor = parse_control_anchor(anchorIter->get()); + if (!anchor || *w <= 0.0f || *h <= 0.0f || *scale <= 0.0f) { + return std::nullopt; + } + return dusk::ui::ControlProps{ + .x = *x, + .y = *y, + .w = *w, + .h = *h, + .scale = *scale, + .anchor = *anchor, + }; +} + static std::filesystem::path GetConfigJsonPath() { return dusk::ConfigPath / ConfigFileName; } @@ -46,8 +148,8 @@ static void ReplaceFile(const std::filesystem::path& source, const std::filesyst } } -ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl) : name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) { -} +ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl) + : name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) {} const char* ConfigVarBase::getName() const noexcept { return name; @@ -72,11 +174,13 @@ static T sanitizeEnumValue(const ConfigVar& cVar, T value) { return value; } -template +template void ConfigImpl::loadFromJson(ConfigVar& cVar, const json& jsonValue) { if constexpr (std::is_enum_v) { if (jsonValue.is_boolean()) { - DuskConfigLog.error("Doing default migration of CVar {} from bool, enum values may not be what is expected!", cVar.getName()); + DuskConfigLog.error("Doing default migration of CVar {} from bool, enum values may not " + "be what is expected!", + cVar.getName()); using Underlying = std::underlying_type_t; const bool b = jsonValue.get(); @@ -91,13 +195,14 @@ void ConfigImpl::loadFromJson(ConfigVar& cVar, const json& jsonValue) { cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get()), false); } -template +template nlohmann::json ConfigImpl::dumpToJson(const ConfigVar& cVar) { return cVar.getValueForSave(); } -template requires std::is_integral_v && std::is_signed_v -static void loadFromArgImpl(ConfigVar& cVar, const std::string_view stringValue) { +template +requires std::is_integral_v&& std::is_signed_v static void loadFromArgImpl( + ConfigVar& cVar, const std::string_view stringValue) { const std::string str(stringValue); const auto result = std::stoll(str); if (result >= std::numeric_limits::min() && result <= std::numeric_limits::max()) { @@ -107,8 +212,9 @@ static void loadFromArgImpl(ConfigVar& cVar, const std::string_view stringVal } } -template requires std::is_integral_v && std::is_unsigned_v -static void loadFromArgImpl(ConfigVar& cVar, const std::string_view stringValue) { +template +requires std::is_integral_v&& std::is_unsigned_v static void loadFromArgImpl( + ConfigVar& cVar, const std::string_view stringValue) { const std::string str(stringValue); const auto result = std::stoull(str); if (result <= std::numeric_limits::max()) { @@ -134,14 +240,17 @@ static void loadFromArgImpl(ConfigVar& cVar, const std::string_view cVar.setOverrideValue(std::string(stringValue)); } -template requires std::is_enum_v -static void loadFromArgImpl(ConfigVar& cVar, const std::string_view stringValue) { +template +requires std::is_enum_v static void loadFromArgImpl( + ConfigVar& cVar, const std::string_view stringValue) { using Underlying = std::underlying_type_t; const std::string str(stringValue); if constexpr (std::is_signed_v) { const auto result = std::stoll(str); - if (result >= std::numeric_limits::min() && result <= std::numeric_limits::max()) { + if (result >= std::numeric_limits::min() && + result <= std::numeric_limits::max()) + { cVar.setOverrideValue(sanitizeEnumValue(cVar, static_cast(result))); } else { throw std::out_of_range("Value is too large"); @@ -156,16 +265,20 @@ static void loadFromArgImpl(ConfigVar& cVar, const std::string_view stringVal } } -template +template void ConfigImpl::loadFromArg(ConfigVar& cVar, const std::string_view stringValue) { loadFromArgImpl(cVar, stringValue); } -template<> +template <> void ConfigImpl::loadFromArg(ConfigVar& cVar, const std::string_view stringValue) { - if (stringValue == "1" || stringValue == "TRUE" || stringValue == "true" || stringValue == "True") { + if (stringValue == "1" || stringValue == "TRUE" || stringValue == "true" || + stringValue == "True") + { cVar.setOverrideValue(true); - } else if (stringValue == "0" || stringValue == "FALSE" || stringValue == "false" || stringValue == "False") { + } else if (stringValue == "0" || stringValue == "FALSE" || stringValue == "false" || + stringValue == "False") + { cVar.setOverrideValue(false); } else { throw InvalidConfigError("Value cannot be parsed as boolean"); @@ -174,42 +287,103 @@ void ConfigImpl::loadFromArg(ConfigVar& cVar, const std::string_view // My IDE is convinced this namespace is necessary. It shouldn't be AFAICT? namespace dusk::config { - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; - template<> void ConfigImpl::loadFromJson(ConfigVar& cVar, const json& jsonValue) { - if (jsonValue.is_boolean()) { - const bool b = jsonValue.get(); +template <> +void ConfigImpl::loadFromJson( + ConfigVar& cVar, const json& jsonValue) { + if (jsonValue.is_boolean()) { + const bool b = jsonValue.get(); - const FrameInterpMode mode = b ? FrameInterpMode::Unlimited : FrameInterpMode::Off; + const FrameInterpMode mode = b ? FrameInterpMode::Unlimited : FrameInterpMode::Off; - cVar.setValue(sanitizeEnumValue(cVar, mode), false); - return; + cVar.setValue(sanitizeEnumValue(cVar, mode), false); + return; + } + + cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get()), false); +} + +template <> +void ConfigImpl::loadFromJson( + ConfigVar& cVar, const json& jsonValue) { + if (!jsonValue.is_object()) { + return; + } + + const int version = jsonValue.value("version", 0); + if (version != ui::ControlLayout::Version) { + return; + } + + const auto controlsIter = jsonValue.find("controls"); + if (controlsIter == jsonValue.end() || !controlsIter->is_object()) { + return; + } + + ui::ControlLayout layout{.version = version}; + for (const auto& control : controlsIter->items()) { + if (!ui::is_control_layout_id(control.key())) { + continue; } - cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get()), false); + if (const auto props = parse_control_props(control.value())) { + layout.controls[control.key()] = *props; + } } - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; - template class ConfigImpl; + + cVar.setValue(std::move(layout), false); } +template <> +void ConfigImpl::loadFromArg( + ConfigVar&, const std::string_view) { + throw InvalidConfigError("Touch control layout cannot be parsed from launch arguments"); +} + +template <> +nlohmann::json ConfigImpl::dumpToJson(const ConfigVar& cVar) { + const auto& layout = cVar.getValueForSave(); + json controls = json::object(); + for (const auto& [id, props] : layout.controls) { + controls[id] = { + {"x", props.x}, + {"y", props.y}, + {"w", props.w}, + {"h", props.h}, + {"scale", props.scale}, + {"anchor", control_anchor_value(props.anchor)}, + }; + } + + return { + {"version", ui::ControlLayout::Version}, + {"controls", std::move(controls)}, + }; +} + +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +template class ConfigImpl; +} // namespace dusk::config + void dusk::config::Register(ConfigVarBase& configVar) { const auto& name = configVar.getName(); if (RegistrationDone) { @@ -298,9 +472,7 @@ void dusk::config::Save() { } const auto configPathString = io::fs_path_to_string(configJsonPath); - DuskConfigLog.info( - "Saving config to '{}'", - configPathString); + DuskConfigLog.info("Saving config to '{}'", configPathString); json j; diff --git a/src/dusk/gyro.cpp b/src/dusk/gyro.cpp index 1e2e379960..31b32bd412 100644 --- a/src/dusk/gyro.cpp +++ b/src/dusk/gyro.cpp @@ -70,16 +70,7 @@ bool rollgoal_gyro_enabled() { } bool queryGyroAimContext() { - if (!static_cast(getSettings().game.enableGyroAim)) { - return false; - } - - daAlink_c* link = daAlink_getAlinkActorClass(); - if (link == nullptr) { - return false; - } - - return link->checkAimContext() && dComIfGp_checkCameraAttentionStatus(link->field_0x317c, 0x10); + return getSettings().game.enableGyroAim.getValue() && dCamera_c::isAimActive(); } void read(float dt) { diff --git a/src/dusk/menu_pointer.cpp b/src/dusk/menu_pointer.cpp new file mode 100644 index 0000000000..91b66cce55 --- /dev/null +++ b/src/dusk/menu_pointer.cpp @@ -0,0 +1,386 @@ +#include "dusk/menu_pointer.h" + +#include "m_Do/m_Do_graphic.h" +#include "d/d_pane_class.h" +#include "dusk/settings.h" + +#include +#include + +#include + +namespace dusk::menu_pointer { +namespace { +State s_state; +bool s_clickConsumed = false; +Context s_lastContext = Context::None; +Context s_currentContext = Context::None; +u8 s_lastDialogChoice = 0xFF; +u8 s_currentDialogChoice = 0xFF; +bool s_lastDialogChoiceValid = false; +bool s_currentDialogChoiceValid = false; +bool s_lastDialogClicked = false; +bool s_currentDialogClicked = false; +bool s_mouseActive = false; +bool s_mouseButtonCaptured = false; +s32 s_mouseButton = -1; +u32 s_suppressedPadHoldMask = 0; +u32 s_suppressedPadNextReadMask = 0; +Context s_deferredActivationContext = Context::None; +u8 s_deferredActivationTarget = 0xFF; + +s32 scancode_from_rml_button(s32 button) noexcept { + switch (button) { + case 0: + return PAD_KEY_MOUSE_LEFT; + case 1: + return PAD_KEY_MOUSE_RIGHT; + case 2: + return PAD_KEY_MOUSE_MIDDLE; + default: + return PAD_KEY_INVALID; + } +} + +bool is_mouse_scancode(s32 scancode) noexcept { + return scancode >= PAD_KEY_MOUSE_X2 && scancode <= PAD_KEY_MOUSE_LEFT; +} + +PADButton pad_button_for_scancode(u32 port, s32 scancode) noexcept { + u32 count = 0; + PADKeyButtonBinding* bindings = PADGetKeyButtonBindings(port, &count); + if (bindings == nullptr) { + return 0; + } + + for (u32 i = 0; i < count; ++i) { + if (bindings[i].scancode == scancode) { + return bindings[i].padButton; + } + } + + return 0; +} + +s32 menu_confirm_mouse_scancode() noexcept { + constexpr u32 port = PAD_CHAN0; + u32 count = 0; + PADKeyButtonBinding* bindings = PADGetKeyButtonBindings(port, &count); + if (bindings == nullptr) { + return PAD_KEY_MOUSE_LEFT; + } + + for (u32 i = 0; i < count; ++i) { + if (bindings[i].padButton == PAD_BUTTON_A && is_mouse_scancode(bindings[i].scancode)) { + return bindings[i].scancode; + } + } + + return pad_button_for_scancode(port, PAD_KEY_MOUSE_LEFT) != 0 ? PAD_KEY_INVALID : + PAD_KEY_MOUSE_LEFT; +} + +bool mouse_button_is_menu_confirm(s32 button) noexcept { + const s32 scancode = scancode_from_rml_button(button); + return scancode != PAD_KEY_INVALID && scancode == menu_confirm_mouse_scancode(); +} + +void suppress_pad_for_mouse_button(s32 button, bool held) noexcept { + const s32 scancode = scancode_from_rml_button(button); + if (scancode == PAD_KEY_INVALID) { + return; + } + + const PADButton padButton = pad_button_for_scancode(PAD_CHAN0, scancode); + if (padButton == 0) { + return; + } + + s_suppressedPadNextReadMask |= padButton; + if (held) { + s_suppressedPadHoldMask |= padButton; + } else { + s_suppressedPadHoldMask &= ~padButton; + } +} + +void set_position_from_rml(f32 x, f32 y) noexcept { + auto* context = aurora::rmlui::get_context(); + if (context == nullptr) { + return; + } + + const auto dimensions = context->GetDimensions(); + const f32 width = std::max(static_cast(dimensions.x), 1.0f); + const f32 height = std::max(static_cast(dimensions.y), 1.0f); + + s_state.x = mDoGph_gInf_c::getMinXF() + x / width * mDoGph_gInf_c::getWidthF(); + s_state.y = mDoGph_gInf_c::getMinYF() + y / height * mDoGph_gInf_c::getHeightF(); + s_state.valid = true; +} + +void clear_input_state() noexcept { + s_state = {}; + s_clickConsumed = false; + s_lastDialogChoice = 0xFF; + s_currentDialogChoice = 0xFF; + s_lastDialogChoiceValid = false; + s_currentDialogChoiceValid = false; + s_lastDialogClicked = false; + s_currentDialogClicked = false; + s_mouseActive = false; + s_mouseButtonCaptured = false; + s_mouseButton = -1; + s_suppressedPadHoldMask = 0; + s_suppressedPadNextReadMask = 0; + s_deferredActivationContext = Context::None; + s_deferredActivationTarget = 0xFF; +} + +} // namespace + +bool handle_fallthrough_pointer(f32 x, f32 y, Phase phase, bool touch, s32 mouseButton) noexcept { + if (!enabled()) { + return false; + } + + s_clickConsumed = false; + + if (!touch) { + if (phase == Phase::Press) { + if (!mouse_button_is_menu_confirm(mouseButton)) { + return false; + } + s_mouseButtonCaptured = true; + s_mouseButton = mouseButton; + suppress_pad_for_mouse_button(mouseButton, true); + } else if (phase == Phase::Release) { + if (!s_mouseButtonCaptured || s_mouseButton != mouseButton) { + return false; + } + suppress_pad_for_mouse_button(mouseButton, false); + s_mouseButtonCaptured = false; + s_mouseButton = -1; + } else if (phase == Phase::Cancel) { + if (s_mouseButtonCaptured) { + suppress_pad_for_mouse_button(s_mouseButton, false); + s_mouseButtonCaptured = false; + s_mouseButton = -1; + } else if (!s_mouseActive) { + return false; + } + } + s_mouseActive = true; + } + + if (phase != Phase::Cancel) { + set_position_from_rml(x, y); + } + s_state.touch = touch; + + switch (phase) { + case Phase::Press: + s_state.down = true; + s_state.pressed = true; + break; + case Phase::Release: + s_state.down = false; + s_state.released = true; + s_state.clicked = true; + break; + case Phase::Cancel: + s_state.down = false; + break; + case Phase::Move: + default: + break; + } + + return true; +} + +void begin_game_frame() noexcept { + s_currentContext = Context::None; + s_currentDialogChoice = 0xFF; + s_currentDialogChoiceValid = false; + s_currentDialogClicked = false; + s_clickConsumed = false; + if (!enabled()) { + clear_input_state(); + } +} + +void end_game_frame() noexcept { + s_lastContext = s_currentContext; + s_lastDialogChoice = s_currentDialogChoice; + s_lastDialogChoiceValid = s_currentDialogChoiceValid; + s_lastDialogClicked = s_currentDialogClicked; + s_state.pressed = false; + s_state.released = false; + s_state.clicked = false; + if (!s_state.down) { + s_state.valid = false; + } + s_clickConsumed = false; +} + +void begin_context(Context context) noexcept { + if (context == Context::None) { + return; + } + + if (s_lastContext == Context::None && s_currentContext == Context::None) { + s_state = {}; + s_mouseActive = false; + s_mouseButtonCaptured = false; + s_mouseButton = -1; + s_suppressedPadHoldMask = 0; + s_suppressedPadNextReadMask = 0; + s_deferredActivationContext = Context::None; + s_deferredActivationTarget = 0xFF; + } + + s_currentContext = context; +} + +bool active() noexcept { + return s_currentContext != Context::None || s_lastContext != Context::None; +} + +bool enabled() noexcept { + return getSettings().game.enableMenuPointer.getValue(); +} + +bool mouse_capture_active() noexcept { + return enabled() && s_mouseButtonCaptured; +} + +const State& state() noexcept { + return s_state; +} + +bool consume_click() noexcept { + if (!s_state.clicked || s_clickConsumed) { + return false; + } + + s_clickConsumed = true; + return true; +} + +void set_dialog_choice(u8 choice, bool clicked) noexcept { + s_currentDialogChoice = choice; + s_currentDialogChoiceValid = true; + s_currentDialogClicked = clicked; +} + +bool get_dialog_choice(u8& choice) noexcept { + if (s_currentDialogChoiceValid) { + choice = s_currentDialogChoice; + return true; + } + if (s_lastDialogChoiceValid) { + choice = s_lastDialogChoice; + return true; + } + return false; +} + +bool consume_dialog_click(u8& choice) noexcept { + if (s_currentDialogChoiceValid && s_currentDialogClicked) { + choice = s_currentDialogChoice; + s_currentDialogClicked = false; + return true; + } + if (s_lastDialogChoiceValid && s_lastDialogClicked) { + choice = s_lastDialogChoice; + s_lastDialogClicked = false; + return true; + } + return false; +} + +void defer_activation(Context context, u8 target) noexcept { + s_deferredActivationContext = context; + s_deferredActivationTarget = target; +} + +bool consume_deferred_activation(Context context, u8 target) noexcept { + if (s_deferredActivationContext != context || s_deferredActivationTarget != target) { + return false; + } + + s_deferredActivationContext = Context::None; + s_deferredActivationTarget = 0xFF; + return true; +} + +void clear_deferred_activation(Context context) noexcept { + if (s_deferredActivationContext != context) { + return; + } + + s_deferredActivationContext = Context::None; + s_deferredActivationTarget = 0xFF; +} + +u32 suppressed_pad_buttons(u32 port) noexcept { + if (port != PAD_CHAN0) { + return 0; + } + + return s_suppressedPadHoldMask | s_suppressedPadNextReadMask; +} + +void finish_pad_suppression_read(u32 port) noexcept { + if (port != PAD_CHAN0) { + return; + } + + s_suppressedPadNextReadMask = 0; +} + +bool hit_rect(f32 left, f32 top, f32 right, f32 bottom, f32 padding) noexcept { + const auto& state = menu_pointer::state(); + if (!state.valid) { + return false; + } + + if (left > right) { + std::swap(left, right); + } + if (top > bottom) { + std::swap(top, bottom); + } + + return state.x >= left - padding && state.x <= right + padding && state.y >= top - padding && + state.y <= bottom + padding; +} + +bool hit_pane(CPaneMgr* pane, f32 padding) noexcept { + if (pane == nullptr || pane->getPanePtr() == nullptr) { + return false; + } + + Mtx mtx; + Vec v0 = pane->getGlobalVtx(&mtx, 0, false, 0); + Vec v1 = pane->getGlobalVtx(&mtx, 1, false, 0); + Vec v2 = pane->getGlobalVtx(&mtx, 2, false, 0); + Vec v3 = pane->getGlobalVtx(&mtx, 3, false, 0); + const f32 left = std::min({v0.x, v1.x, v2.x, v3.x}); + const f32 right = std::max({v0.x, v1.x, v2.x, v3.x}); + const f32 top = std::min({v0.y, v1.y, v2.y, v3.y}); + const f32 bottom = std::max({v0.y, v1.y, v2.y, v3.y}); + return hit_rect(left, top, right, bottom, padding); +} + +bool hit_pane(J2DPane* pane, f32 padding) noexcept { + if (pane == nullptr || !pane->isVisible()) { + return false; + } + + const JGeometry::TBox2& bounds = pane->getBounds(); + return hit_rect(bounds.i.x, bounds.i.y, bounds.f.x, bounds.f.y, padding); +} + +} // namespace dusk::menu_pointer diff --git a/src/dusk/mouse.cpp b/src/dusk/mouse.cpp index 8ebfaf60cb..f1aa5672fd 100644 --- a/src/dusk/mouse.cpp +++ b/src/dusk/mouse.cpp @@ -1,4 +1,5 @@ #include "dusk/mouse.h" +#include "dusk/menu_pointer.h" #include "dusk/settings.h" #include "dusk/ui/ui.hpp" #include "d/actor/d_a_alink.h" @@ -26,16 +27,7 @@ void reset_deltas() { } bool queryMouseAimContext() { - if (!getSettings().game.enableMouseAim) { - return false; - } - - daAlink_c* link = daAlink_getAlinkActorClass(); - if (link == nullptr) { - return false; - } - - return link->checkAimContext() && dComIfGp_checkCameraAttentionStatus(link->field_0x317c, 0x10); + return getSettings().game.enableMouseAim.getValue() && dCamera_c::isAimActive(); } bool wantMouseCapture() { @@ -50,7 +42,7 @@ bool isWindowFocused(SDL_Window* window) { } bool shouldCaptureMouse(SDL_Window* window) { - if (window == nullptr || ui::any_document_visible()) { + if (window == nullptr || ui::any_document_visible() || menu_pointer::active()) { return false; } return wantMouseCapture() && isWindowFocused(window); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 51718734ec..fc178458e2 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -14,6 +14,9 @@ UserSettings g_userSettings = { .enableFpsOverlay {"game.enableFpsOverlay", false}, .fpsOverlayCorner {"game.fpsOverlayCorner", 0}, .maxFrameRate {"video.maxFrameRate", 240}, + .rememberWindowSize {"video.rememberWindowSize", false}, + .lastWindowWidth {"video.lastWindowWidth", 0}, + .lastWindowHeight {"video.lastWindowHeight", 0}, }, .audio = { @@ -95,6 +98,9 @@ UserSettings g_userSettings = { .mouseCameraSensitivity {"game.mouseCameraSensitivity", 1.0f}, .invertMouseY {"game.invertMouseY", false}, .freeCamera {"game.freeCamera", false}, + .enableTouchControls {"game.enableTouchControls", false}, + .enableMenuPointer {"game.enableMenuPointer", true}, + .touchControlsLayout {"game.touchControlsLayout", ui::ControlLayout{}}, .invertCameraXAxis {"game.invertCameraXAxis", false}, .invertCameraYAxis {"game.invertCameraYAxis", false}, .invertFirstPersonXAxis {"game.invertFirstPersonXAxis", false}, @@ -103,6 +109,8 @@ UserSettings g_userSettings = { .invertAirSwimY {"game.invertAirSwimY", false}, .freeCameraXSensitivity {"game.freeCameraXSensitivity", 1.0f}, .freeCameraYSensitivity {"game.freeCameraYSensitivity", 1.0f}, + .touchCameraXSensitivity {"game.touchCameraXSensitivity", 1.0f}, + .touchCameraYSensitivity {"game.touchCameraYSensitivity", 1.0f}, .debugFlyCam {"game.debugFlyCam", false}, .debugFlyCamLockEvents {"game.debugFlyCamLockEvents", true}, .allowBackgroundInput {"game.allowBackgroundInput", true}, @@ -179,6 +187,18 @@ UserSettings g_userSettings = { ActionBindConfigVar{"actionBindings.callMidna_port2", PAD_NATIVE_BUTTON_INVALID}, ActionBindConfigVar{"actionBindings.callMidna_port3", PAD_NATIVE_BUTTON_INVALID}, }, + .openMapScreen { + ActionBindConfigVar{"actionBindings.openMapScreen_port0", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.openMapScreen_port1", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.openMapScreen_port2", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.openMapScreen_port3", PAD_NATIVE_BUTTON_INVALID}, + }, + .toggleMinimap { + ActionBindConfigVar{"actionBindings.toggleMinimap_port0", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.toggleMinimap_port1", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.toggleMinimap_port2", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.toggleMinimap_port3", PAD_NATIVE_BUTTON_INVALID}, + }, .openDusklightMenu { ActionBindConfigVar{"actionBindings.openDusklightMenu_port0", PAD_NATIVE_BUTTON_INVALID}, ActionBindConfigVar{"actionBindings.openDusklightMenu_port1", PAD_NATIVE_BUTTON_INVALID}, @@ -228,6 +248,9 @@ void registerSettings() { Register(g_userSettings.video.enableFpsOverlay); Register(g_userSettings.video.fpsOverlayCorner); Register(g_userSettings.video.maxFrameRate); + Register(g_userSettings.video.rememberWindowSize); + Register(g_userSettings.video.lastWindowWidth); + Register(g_userSettings.video.lastWindowHeight); // Audio Register(g_userSettings.audio.masterVolume); @@ -268,6 +291,8 @@ void registerSettings() { Register(g_userSettings.game.invertAirSwimY); Register(g_userSettings.game.freeCameraXSensitivity); Register(g_userSettings.game.freeCameraYSensitivity); + Register(g_userSettings.game.touchCameraXSensitivity); + Register(g_userSettings.game.touchCameraYSensitivity); Register(g_userSettings.game.minimalHUD); Register(g_userSettings.game.hudScale); Register(g_userSettings.game.pauseOnFocusLost); @@ -332,6 +357,9 @@ void registerSettings() { Register(g_userSettings.game.mouseCameraSensitivity); Register(g_userSettings.game.invertMouseY); Register(g_userSettings.game.freeCamera); + Register(g_userSettings.game.enableTouchControls); + Register(g_userSettings.game.enableMenuPointer); + Register(g_userSettings.game.touchControlsLayout); Register(g_userSettings.game.debugFlyCam); Register(g_userSettings.game.debugFlyCamLockEvents); Register(g_userSettings.game.allowBackgroundInput); @@ -362,6 +390,14 @@ void registerSettings() { Register(g_userSettings.actionBindings.callMidna[1]); Register(g_userSettings.actionBindings.callMidna[2]); Register(g_userSettings.actionBindings.callMidna[3]); + Register(g_userSettings.actionBindings.openMapScreen[0]); + Register(g_userSettings.actionBindings.openMapScreen[1]); + Register(g_userSettings.actionBindings.openMapScreen[2]); + Register(g_userSettings.actionBindings.openMapScreen[3]); + Register(g_userSettings.actionBindings.toggleMinimap[0]); + Register(g_userSettings.actionBindings.toggleMinimap[1]); + Register(g_userSettings.actionBindings.toggleMinimap[2]); + Register(g_userSettings.actionBindings.toggleMinimap[3]); Register(g_userSettings.actionBindings.openDusklightMenu[0]); Register(g_userSettings.actionBindings.openDusklightMenu[1]); Register(g_userSettings.actionBindings.openDusklightMenu[2]); diff --git a/src/dusk/touch_camera.cpp b/src/dusk/touch_camera.cpp new file mode 100644 index 0000000000..6f48d22c38 --- /dev/null +++ b/src/dusk/touch_camera.cpp @@ -0,0 +1,26 @@ +#include "dusk/touch_camera.h" + +namespace dusk::touch_camera { +namespace { +float s_yaw_dp = 0.0f; +float s_pitch_dp = 0.0f; +} // namespace + +void add_delta(float yaw_dp, float pitch_dp) noexcept { + s_yaw_dp += yaw_dp; + s_pitch_dp += pitch_dp; +} + +bool consume_delta(float& yaw_dp, float& pitch_dp) noexcept { + yaw_dp = s_yaw_dp; + pitch_dp = s_pitch_dp; + clear(); + return yaw_dp != 0.0f || pitch_dp != 0.0f; +} + +void clear() noexcept { + s_yaw_dp = 0.0f; + s_pitch_dp = 0.0f; +} + +} // namespace dusk::touch_camera diff --git a/src/dusk/ui/button.hpp b/src/dusk/ui/button.hpp index bf97a66e6e..f2c9756c98 100644 --- a/src/dusk/ui/button.hpp +++ b/src/dusk/ui/button.hpp @@ -19,8 +19,6 @@ public: void set_text(const Rml::String& text); Button& on_pressed(ButtonCallback callback); - const Rml::String& get_text() const { return mProps.text; } - private: void update_props(Props props); diff --git a/src/dusk/ui/controller_config.cpp b/src/dusk/ui/controller_config.cpp index 20e9ef62d5..a506a593a6 100644 --- a/src/dusk/ui/controller_config.cpp +++ b/src/dusk/ui/controller_config.cpp @@ -861,6 +861,20 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { break; } case Page::Rumble: { + if (PADCanForceDeviceRumble(static_cast(port))) { + pane.add_child(BoolButton::Props{ + .key = "Use Device Haptics", + .getValue = [port] { return PADGetForceDeviceRumble(static_cast(port)); }, + .setValue = + [port](bool value) { + PADSetForceDeviceRumble(static_cast(port), value ? TRUE : FALSE); + PADSerializeMappings(); + }, + .isDisabled = [this] { return mRumbleTestActive; }, + }); + pane.add_text("Use native device haptics instead of controller rumble. " + "Useful for devices with built-in gamepads."); + } auto& rumbleTest = pane.add_select_button({ .key = "Test Rumble", .getValue = diff --git a/src/dusk/ui/controls.hpp b/src/dusk/ui/controls.hpp new file mode 100644 index 0000000000..2a83d6127f --- /dev/null +++ b/src/dusk/ui/controls.hpp @@ -0,0 +1,192 @@ +#pragma once + +#include + +namespace dusk::ui { + +struct EquipTarget { + float left = 0.0f; + float top = 0.0f; + float width = 0.0f; + float height = 0.0f; + bool valid = false; +}; + +enum class Control { + A, + B, + X, + Y, + Z, + L, + R, + FIRST_PERSON, + ITEMS, + COLLECTIONS, + MAP, + SKIP, + DPAD_UP, + DPAD_DOWN, + DPAD_LEFT, + DPAD_RIGHT, + COUNT, +}; + +enum class ControlAnchor : u8 { + None, + Top, + Left, + Bottom, + Right, + TopLeft, + TopRight, + BottomLeft, + BottomRight, +}; + +struct ControlProps { + float x = 0.0f; + float y = 0.0f; + float w = 0.0f; + float h = 0.0f; + float scale = 1.0f; + ControlAnchor anchor = ControlAnchor::None; +}; + +struct ControlRect { + float l = 0.0f; + float t = 0.0f; + float w = 0.0f; + float h = 0.0f; +}; + +struct ResolvedControlLayout { + ControlRect visual; + ControlRect box; + float scale = 1.0f; +}; + +struct ControlLayoutSize { + float w = 0.0f; + float h = 0.0f; +}; + +struct ControlLayout { + static constexpr int Version = 1; + + int version = Version; + std::map > controls; +}; + +constexpr std::array kControlLayoutIds = { + "actionBar", + "buttonA", + "buttonB", + "buttonX", + "buttonY", + "buttonZ", + "skip", + "triggerL", + "triggerR", +}; + +constexpr bool is_control_layout_id(std::string_view id) noexcept { + for (const auto knownId : kControlLayoutIds) { + if (id == knownId) { + return true; + } + } + return false; +} + +constexpr ControlRect resolve_anchored_rect( + ControlAnchor anchor, float x, float y, float w, float h, ControlLayoutSize docSize) noexcept { + switch (anchor) { + case ControlAnchor::None: + return {x * docSize.w - w * 0.5f, y * docSize.h - h * 0.5f, w, h}; + case ControlAnchor::Top: + return {x * docSize.w - w * 0.5f, y, w, h}; + case ControlAnchor::Bottom: + return {x * docSize.w - w * 0.5f, docSize.h - y - h, w, h}; + case ControlAnchor::Left: + return {x, y * docSize.h - h * 0.5f, w, h}; + case ControlAnchor::Right: + return {docSize.w - x - w, y * docSize.h - h * 0.5f, w, h}; + case ControlAnchor::TopLeft: + return {x, y, w, h}; + case ControlAnchor::TopRight: + return {docSize.w - x - w, y, w, h}; + case ControlAnchor::BottomLeft: + return {x, docSize.h - y - h, w, h}; + case ControlAnchor::BottomRight: + return {docSize.w - x - w, docSize.h - y - h, w, h}; + } + return {}; +} + +constexpr ResolvedControlLayout resolve_control_layout( + ControlProps props, ControlLayoutSize docSize) noexcept { + const float visualW = props.w * props.scale; + const float visualH = props.h * props.scale; + const ControlRect visual = + resolve_anchored_rect(props.anchor, props.x, props.y, visualW, visualH, docSize); + const ControlRect box = { + visual.l + (visual.w - props.w) * 0.5f, + visual.t + (visual.h - props.h) * 0.5f, + props.w, + props.h, + }; + return { + .visual = visual, + .box = box, + .scale = props.scale, + }; +} + +constexpr ControlProps encode_control_props(ControlRect visual, ControlLayoutSize docSize, + ControlProps props, ControlAnchor anchor) noexcept { + props.anchor = anchor; + + switch (anchor) { + case ControlAnchor::None: + props.x = (visual.l + visual.w * 0.5f) / docSize.w; + props.y = (visual.t + visual.h * 0.5f) / docSize.h; + break; + case ControlAnchor::Top: + props.x = (visual.l + visual.w * 0.5f) / docSize.w; + props.y = visual.t; + break; + case ControlAnchor::Bottom: + props.x = (visual.l + visual.w * 0.5f) / docSize.w; + props.y = docSize.h - visual.t - visual.h; + break; + case ControlAnchor::Left: + props.x = visual.l; + props.y = (visual.t + visual.h * 0.5f) / docSize.h; + break; + case ControlAnchor::Right: + props.x = docSize.w - visual.l - visual.w; + props.y = (visual.t + visual.h * 0.5f) / docSize.h; + break; + case ControlAnchor::TopLeft: + props.x = visual.l; + props.y = visual.t; + break; + case ControlAnchor::TopRight: + props.x = docSize.w - visual.l - visual.w; + props.y = visual.t; + break; + case ControlAnchor::BottomLeft: + props.x = visual.l; + props.y = docSize.h - visual.t - visual.h; + break; + case ControlAnchor::BottomRight: + props.x = docSize.w - visual.l - visual.w; + props.y = docSize.h - visual.t - visual.h; + break; + } + + return props; +} + +} // namespace dusk::ui diff --git a/src/dusk/ui/document.cpp b/src/dusk/ui/document.cpp index a7bcc3f9ed..1ad03bc1ec 100644 --- a/src/dusk/ui/document.cpp +++ b/src/dusk/ui/document.cpp @@ -3,7 +3,6 @@ #include "aurora/rmlui.hpp" #include "ui.hpp" -#include "Z2AudioLib/Z2SeMgr.h" #include "m_Do/m_Do_audio.h" namespace dusk::ui { @@ -19,32 +18,39 @@ Rml::ElementDocument* load_document(const Rml::String& source) { } // namespace -Document::Document(const Rml::String& source) : mDocument(load_document(source)) { +Document::Document(const Rml::String& source, bool passive) + : mDocument(load_document(source)), mPassive(passive) { // Block events while hidden (except for Menu command); play nav sounds when visible listen( Rml::EventId::Keydown, [this](Rml::Event& event) { + if (mPassive) { + return; + } const auto cmd = map_nav_event(event); - if (cmd != NavCommand::Menu && !visible()) { + if (cmd != NavCommand::Menu && (!visible() || !active())) { event.StopImmediatePropagation(); } }, true); - const auto blockUnlessVisible = [this](Rml::Event& event) { - if (!visible()) { + const auto blockUnlessActive = [this](Rml::Event& event) { + if (!visible() || !active()) { event.StopImmediatePropagation(); } }; - listen(Rml::EventId::Mouseover, blockUnlessVisible, true); - listen(Rml::EventId::Click, blockUnlessVisible, true); - listen(Rml::EventId::Scroll, blockUnlessVisible, true); + listen(Rml::EventId::Mouseover, blockUnlessActive, true); + listen(Rml::EventId::Click, blockUnlessActive, true); + listen(Rml::EventId::Scroll, blockUnlessActive, true); listen(Rml::EventId::Keydown, [this](Rml::Event& event) { - const auto cmd = map_nav_event(event); - if (cmd == NavCommand::None) { + if (mPassive) { + auto* doc = top_document(); + if (doc != nullptr && doc->handle_nav_event(event)) { + event.StopPropagation(); + } return; } - if (handle_nav_command(event, cmd)) { + if (handle_nav_event(event)) { event.StopPropagation(); } }); @@ -97,6 +103,18 @@ void Document::listen(Rml::Element* element, Rml::EventId event, std::make_unique(element, event, std::move(callback), capture)); } +void Document::listen(Rml::Element* element, const Rml::String& event, + ScopedEventListener::Callback callback, bool capture) { + if (element == nullptr) { + element = mDocument; + } + if (element == nullptr || event.empty() || !callback) { + return; + } + mListeners.emplace_back( + std::make_unique(element, event, std::move(callback), capture)); +} + bool Document::visible() const { if (mDocument == nullptr) { return false; @@ -104,6 +122,21 @@ bool Document::visible() const { return *mDocument->GetProperty(Rml::PropertyId::Visibility) == Rml::Style::Visibility::Visible; } +bool Document::active() const { + return !mClosed && !mPendingClose; +} + +bool Document::handle_nav_event(Rml::Event& event) { + if (!active()) { + return false; + } + const auto cmd = map_nav_event(event); + if (cmd == NavCommand::None || (cmd != NavCommand::Menu && !visible())) { + return false; + } + return handle_nav_command(event, cmd); +} + bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) { if (cmd == NavCommand::Menu) { mDoAud_seStartMenu(visible() ? kSoundMenuClose : kSoundMenuOpen); diff --git a/src/dusk/ui/document.hpp b/src/dusk/ui/document.hpp index d0f4cae841..c3428d9fea 100644 --- a/src/dusk/ui/document.hpp +++ b/src/dusk/ui/document.hpp @@ -7,7 +7,7 @@ namespace dusk::ui { class Document { public: - Document(const Rml::String& source); + explicit Document(const Rml::String& source, bool passive = false); virtual ~Document(); Document(const Document&) = delete; @@ -18,12 +18,19 @@ public: virtual void update(); virtual bool focus(); virtual bool visible() const; + virtual bool active() const; void listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback, bool capture = false); + void listen(Rml::Element* element, const Rml::String& event, + ScopedEventListener::Callback callback, bool capture = false); void listen(Rml::EventId event, ScopedEventListener::Callback callback, bool capture = false) { listen(mDocument, event, std::move(callback), capture); } + void listen( + const Rml::String& event, ScopedEventListener::Callback callback, bool capture = false) { + listen(mDocument, event, std::move(callback), capture); + } void toggle() { if (visible()) { hide(false); @@ -35,14 +42,15 @@ public: push_document(std::move(document)); hide(false); } - void pop() { + void pop(bool show = true) { hide(true); - show_top_document(); + focus_top_document(show); } - bool pending_close() const { return mPendingClose; } bool closed() const { return mClosed; } + bool handle_nav_event(Rml::Event& event); + protected: virtual bool handle_nav_command(Rml::Event& event, NavCommand cmd); @@ -50,6 +58,7 @@ protected: std::vector > mListeners; bool mPendingClose = false; bool mClosed = false; + bool mPassive = false; }; } // namespace dusk::ui diff --git a/src/dusk/ui/icon_provider.cpp b/src/dusk/ui/icon_provider.cpp new file mode 100644 index 0000000000..b8307b92f7 --- /dev/null +++ b/src/dusk/ui/icon_provider.cpp @@ -0,0 +1,899 @@ +#include "icon_provider.hpp" + +#include "d/dolzel.h" // IWYU pragma: keep + +#ifdef AURORA_ENABLE_RMLUI + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "JSystem/J2DGraph/J2DPicture.h" +#include "JSystem/JUtility/JUTTexture.h" +#include "d/actor/d_a_alink.h" +#include "d/d_com_inf_game.h" +#include "d/d_item_data.h" +#include "d/d_meter2_info.h" +#include "d/d_pane_class.h" + +namespace dusk::ui { +namespace { + +constexpr std::string_view kScheme = "item"; +constexpr std::string_view kSourcePrefix = "item://"; +constexpr std::string_view kMeterScheme = "meter"; +constexpr std::string_view kMeterSourcePrefix = "meter://"; +constexpr size_t kItemTextureBufferSize = 0xC00; +constexpr size_t kMaxCachedIcons = 128; +constexpr uint64_t kMeterTextureSourceSlots = 8; +constexpr uint32_t kMinRenderedPaneIconSize = 128; +constexpr uint32_t kMaxRenderedPaneIconSize = 1024; + +struct alignas(32) ItemTextureBuffer { + std::array bytes{}; + + std::byte* data() noexcept { return bytes.data(); } + const std::byte* data() const noexcept { return bytes.data(); } +}; + +struct CachedIcon { + std::vector pixels; + uint32_t width = 0; + uint32_t height = 0; +}; + +struct RuntimeIconState { + CachedIcon icon; + uint64_t revision = 0; + bool valid = false; +}; + +struct LayerColors { + JUtility::TColor black; + JUtility::TColor white; + std::array corner; +}; + +struct RectF { + float left = std::numeric_limits::max(); + float top = std::numeric_limits::max(); + float right = std::numeric_limits::lowest(); + float bottom = std::numeric_limits::lowest(); + + bool valid() const noexcept { return left < right && top < bottom; } + float width() const noexcept { return right - left; } + float height() const noexcept { return bottom - top; } + + void include(float x, float y) noexcept { + if (!std::isfinite(x) || !std::isfinite(y)) { + return; + } + left = std::min(left, x); + top = std::min(top, y); + right = std::max(right, x); + bottom = std::max(bottom, y); + } + + void include(const RectF& rect) noexcept { + if (!rect.valid()) { + return; + } + include(rect.left, rect.top); + include(rect.right, rect.bottom); + } +}; + +struct PictureLayer { + J2DPicture* picture = nullptr; + RectF rect; + uint8_t alpha = 0; +}; + +struct SurfaceDeleter { + void operator()(SDL_Surface* surface) const noexcept { SDL_DestroySurface(surface); } +}; + +using SurfacePtr = std::unique_ptr; + +std::unordered_map& icon_cache() { + static auto* cache = new std::unordered_map(); + return *cache; +} + +RuntimeIconState& midna_icon_state() { + static auto* state = new RuntimeIconState(); + return *state; +} + +std::string_view strip_query(std::string_view path) noexcept { + const auto queryPos = path.find_first_of("?#"); + if (queryPos != std::string_view::npos) { + path = path.substr(0, queryPos); + } + return path; +} + +std::optional parse_item_no(std::string_view text) noexcept { + if (text.starts_with("0x") || text.starts_with("0X")) { + text.remove_prefix(2); + } + unsigned value = 0; + const auto* first = text.data(); + const auto* last = text.data() + text.size(); + const auto [ptr, ec] = std::from_chars(first, last, value, 16); + if (ec != std::errc() || ptr != last || value > 0xFF) { + return std::nullopt; + } + return static_cast(value); +} + +bool is_valid_icon_item(u8 itemNo) noexcept { + return itemNo != 0 && itemNo != dItemNo_NONE_e; +} + +u8 item_icon_texture_item(u8 itemNo) noexcept { + if (itemNo == dItemNo_LIGHT_ARROW_e) { + return dItemNo_BOW_e; + } + return itemNo; +} + +std::optional selected_slot_item(int slot) noexcept { + const u8 itemNo = dComIfGp_getSelectItem(slot); + if (!is_valid_icon_item(itemNo)) { + return std::nullopt; + } + return item_icon_texture_item(itemNo); +} + +bool is_sword_item(u8 itemNo) noexcept { + switch (itemNo) { + case dItemNo_WOOD_STICK_e: + case dItemNo_SWORD_e: + case dItemNo_MASTER_SWORD_e: + case dItemNo_LIGHT_SWORD_e: + return true; + default: + return false; + } +} + +std::optional b_button_item() noexcept { + const u8 action = dComIfGp_getAStatus(); + if (action == 0x26 || action == 0x2E) { + const u8 sword = dComIfGs_getSelectEquipSword(); + if (is_sword_item(sword)) { + return sword; + } + return std::nullopt; + } + if (action == 0x4F) { + return dItemNo_LURE_ROD_e; + } + return std::nullopt; +} + +std::optional item_for_source(std::string_view source) noexcept { + if (!source.starts_with(kSourcePrefix)) { + return std::nullopt; + } + + std::string_view path = strip_query(source.substr(kSourcePrefix.size())); + if (path.starts_with("item/")) { + path.remove_prefix(5); + const auto itemNo = parse_item_no(path); + if (itemNo && is_valid_icon_item(*itemNo)) { + return item_icon_texture_item(*itemNo); + } + return std::nullopt; + } + if (path == "slot/x") { + return selected_slot_item(0); + } + if (path == "slot/y") { + return selected_slot_item(1); + } + if (path == "button/b") { + return b_button_item(); + } + return std::nullopt; +} + +uint32_t item_revision(u8 itemNo) noexcept { + uint32_t revision = itemNo; + revision = revision * 131u + g_meter2_info.getItemType(itemNo); + + if (itemNo == dItemNo_KANTERA_e || itemNo == dItemNo_KANTERA2_e) { + revision = revision * 131u + (dComIfGs_getOil() == 0 ? 0u : 1u); + } + if (itemNo == dItemNo_COPY_ROD_e) { + auto* player = daPy_getPlayerActorClass(); + revision = revision * 131u + (player != nullptr && player->checkCopyRodTopUse() ? 1u : 0u); + } + return revision; +} + +std::string item_source_for_item(u8 itemNo) { + itemNo = item_icon_texture_item(itemNo); + return fmt::format("{}://item/{:02x}?rev={:08x}", kScheme, itemNo, item_revision(itemNo)); +} + +std::optional selected_slot_count(int slot) noexcept { + const u8 itemNo = dComIfGp_getSelectItem(slot); + if (!is_valid_icon_item(itemNo)) { + return std::nullopt; + } + if (item_icon_texture_item(itemNo) == dItemNo_KANTERA_e || + item_icon_texture_item(itemNo) == dItemNo_KANTERA2_e) + { + return std::nullopt; + } + + int count = 0; + int max = 0; + switch (itemNo) { + case dItemNo_BOW_e: + case dItemNo_LIGHT_ARROW_e: + case dItemNo_ARROW_LV1_e: + case dItemNo_ARROW_LV2_e: + case dItemNo_ARROW_LV3_e: + case dItemNo_HAWK_ARROW_e: + count = dComIfGs_getArrowNum(); + max = dComIfGs_getArrowMax(); + break; + case dItemNo_BOMB_ARROW_e: + count = std::min(dComIfGp_getSelectItemNum(slot), dComIfGs_getArrowNum()); + max = std::max(dComIfGp_getSelectItemMaxNum(slot), dComIfGs_getArrowMax()); + break; + default: + count = dComIfGp_getSelectItemNum(slot); + max = dComIfGp_getSelectItemMaxNum(slot); + break; + } + if (max <= 0) { + return std::nullopt; + } + return std::clamp(count, 0, max); +} + +aurora::gfx::ConvertedTexture decode_timg(const ResTIMG* image) { + if (image == nullptr || image->width.host() == 0 || image->height.host() == 0) { + return {}; + } + + const auto* base = reinterpret_cast(image); + const auto width = image->width.host(); + const auto height = image->height.host(); + const uint32_t textureSize = GXGetTexBufferSize(width, height, image->format, GX_FALSE, 0); + const auto* textureData = base + static_cast(image->imageOffset); + + if (image->indexTexture != 0) { + const auto* paletteData = base + static_cast(image->paletteOffset); + return aurora::gfx::convert_texture_palette(image->format, width, height, 1, + aurora::ArrayRef{textureData, textureSize}, static_cast(image->colorFormat), + image->numColors, + aurora::ArrayRef{paletteData, static_cast(image->numColors) * 2}); + } + + return aurora::gfx::convert_texture( + image->format, width, height, 1, aurora::ArrayRef{textureData, textureSize}); +} + +uint8_t lerp_u8(uint8_t a, uint8_t b, uint32_t t) noexcept { + return static_cast( + (static_cast(a) * (255u - t) + static_cast(b) * t) / 255u); +} + +JUtility::TColor lerp_color( + const JUtility::TColor& a, const JUtility::TColor& b, uint32_t t) noexcept { + return { + lerp_u8(a.r, b.r, t), + lerp_u8(a.g, b.g, t), + lerp_u8(a.b, b.b, t), + lerp_u8(a.a, b.a, t), + }; +} + +JUtility::TColor bilerp_corner( + const LayerColors& colors, uint32_t x, uint32_t y, uint32_t width, uint32_t height) noexcept { + const uint32_t u = width > 1 ? (x * 255u) / (width - 1u) : 0u; + const uint32_t v = height > 1 ? (y * 255u) / (height - 1u) : 0u; + const JUtility::TColor top = lerp_color(colors.corner[0], colors.corner[1], u); + const JUtility::TColor bottom = lerp_color(colors.corner[2], colors.corner[3], u); + return lerp_color(top, bottom, v); +} + +std::array apply_layer_colors(std::span src, + const LayerColors& colors, uint32_t x, uint32_t y, uint32_t width, uint32_t height) noexcept { + std::array out{ + lerp_u8(colors.black.r, colors.white.r, src[0]), + lerp_u8(colors.black.g, colors.white.g, src[1]), + lerp_u8(colors.black.b, colors.white.b, src[2]), + src[3], + }; + + const auto corner = bilerp_corner(colors, x, y, width, height); + out[0] = static_cast((static_cast(out[0]) * corner.r) / 255u); + out[1] = static_cast((static_cast(out[1]) * corner.g) / 255u); + out[2] = static_cast((static_cast(out[2]) * corner.b) / 255u); + out[3] = static_cast((static_cast(out[3]) * corner.a) / 255u); + return out; +} + +void blend_premultiplied(uint8_t* dst, const std::array& src) noexcept { + const uint32_t srcAlpha = src[3]; + const uint32_t invAlpha = 255u - srcAlpha; + const uint32_t srcR = (static_cast(src[0]) * srcAlpha) / 255u; + const uint32_t srcG = (static_cast(src[1]) * srcAlpha) / 255u; + const uint32_t srcB = (static_cast(src[2]) * srcAlpha) / 255u; + + dst[0] = static_cast( + std::min(255u, srcR + (static_cast(dst[0]) * invAlpha) / 255u)); + dst[1] = static_cast( + std::min(255u, srcG + (static_cast(dst[1]) * invAlpha) / 255u)); + dst[2] = static_cast( + std::min(255u, srcB + (static_cast(dst[2]) * invAlpha) / 255u)); + dst[3] = static_cast( + std::min(255u, srcAlpha + (static_cast(dst[3]) * invAlpha) / 255u)); +} + +LayerColors layer_colors(const J2DPicture& picture) noexcept { + return { + .black = picture.getBlack(), + .white = picture.getWhite(), + .corner = {picture.corner(0), picture.corner(1), picture.corner(2), picture.corner(3)}, + }; +} + +LayerColors layer_colors(J2DPicture& picture, uint8_t alpha) noexcept { + std::array corners{}; + picture.getNewColor(corners.data()); + for (auto& corner : corners) { + corner.a = static_cast((static_cast(corner.a) * alpha) / 255u); + } + return { + .black = picture.getBlack(), + .white = picture.getWhite(), + .corner = corners, + }; +} + +std::optional render_item_icon(u8 itemNo) { + std::array buffers{}; + std::array pictures{}; + + const int textureCount = + dMeter2Info_readItemTexture(itemNo, buffers[0].data(), &pictures[0], buffers[1].data(), + &pictures[1], buffers[2].data(), &pictures[2], buffers[3].data(), &pictures[3], -1); + if (textureCount <= 0) { + return std::nullopt; + } + + std::array decodedLayers{}; + std::array colors{}; + int decodedCount = 0; + for (int i = 0; i < textureCount && i < static_cast(decodedLayers.size()); ++i) { + auto decoded = decode_timg(reinterpret_cast(buffers[i].data())); + if (decoded.data.empty()) { + continue; + } + colors[decodedCount] = layer_colors(pictures[i]); + decodedLayers[decodedCount] = std::move(decoded); + ++decodedCount; + } + if (decodedCount == 0) { + return std::nullopt; + } + + CachedIcon icon{ + .width = decodedLayers[0].width, + .height = decodedLayers[0].height, + }; + icon.pixels.assign(static_cast(icon.width) * static_cast(icon.height) * 4, 0); + + for (int layer = 0; layer < decodedCount; ++layer) { + const auto& decoded = decodedLayers[layer]; + for (uint32_t y = 0; y < icon.height; ++y) { + const uint32_t sourceY = decoded.height > 0 ? (y * decoded.height) / icon.height : 0; + for (uint32_t x = 0; x < icon.width; ++x) { + const uint32_t sourceX = decoded.width > 0 ? (x * decoded.width) / icon.width : 0; + const size_t sourceOffset = + (static_cast(sourceY) * decoded.width + static_cast(sourceX)) * + 4; + if (sourceOffset + 3 >= decoded.data.size()) { + continue; + } + + const std::span sourcePixel( + decoded.data.data() + sourceOffset, 4); + const auto pixel = + apply_layer_colors(sourcePixel, colors[layer], x, y, icon.width, icon.height); + uint8_t* destination = + icon.pixels.data() + + (static_cast(y) * icon.width + static_cast(x)) * 4; + blend_premultiplied(destination, pixel); + } + } + } + + return icon; +} + +SurfacePtr create_rgba_surface(uint32_t width, uint32_t height) { + if (width == 0 || height == 0 || + width > static_cast(std::numeric_limits::max()) || + height > static_cast(std::numeric_limits::max())) + { + return {}; + } + + return SurfacePtr{SDL_CreateSurface( + static_cast(width), static_cast(height), SDL_PIXELFORMAT_RGBA32)}; +} + +bool lock_surface(SDL_Surface* surface) noexcept { + return surface != nullptr && (!SDL_MUSTLOCK(surface) || SDL_LockSurface(surface)); +} + +void unlock_surface(SDL_Surface* surface) noexcept { + if (surface != nullptr && SDL_MUSTLOCK(surface)) { + SDL_UnlockSurface(surface); + } +} + +SurfacePtr create_layer_surface( + const aurora::gfx::ConvertedTexture& decoded, const LayerColors& colors) { + if (decoded.width == 0 || decoded.height == 0 || decoded.data.empty()) { + return {}; + } + + auto surface = create_rgba_surface(decoded.width, decoded.height); + if (!surface || !lock_surface(surface.get())) { + return {}; + } + + for (uint32_t y = 0; y < decoded.height; ++y) { + auto* destination = static_cast(surface->pixels) + + static_cast(y) * static_cast(surface->pitch); + for (uint32_t x = 0; x < decoded.width; ++x) { + const size_t sourceOffset = + (static_cast(y) * decoded.width + static_cast(x)) * 4; + if (sourceOffset + 3 >= decoded.data.size()) { + continue; + } + + const std::span sourcePixel(decoded.data.data() + sourceOffset, 4); + const auto pixel = + apply_layer_colors(sourcePixel, colors, x, y, decoded.width, decoded.height); + std::memcpy(destination + static_cast(x) * 4, pixel.data(), pixel.size()); + } + } + + unlock_surface(surface.get()); + SDL_SetSurfaceBlendMode(surface.get(), SDL_BLENDMODE_BLEND); + return surface; +} + +std::optional icon_from_surface(SDL_Surface* surface) { + if (surface == nullptr || surface->w <= 0 || surface->h <= 0) { + return std::nullopt; + } + + CachedIcon icon{ + .width = static_cast(surface->w), + .height = static_cast(surface->h), + }; + const size_t rowSize = static_cast(icon.width) * 4u; + icon.pixels.resize(rowSize * static_cast(icon.height)); + + if (!lock_surface(surface)) { + return std::nullopt; + } + + for (uint32_t y = 0; y < icon.height; ++y) { + const auto* source = static_cast(surface->pixels) + + static_cast(y) * static_cast(surface->pitch); + auto* destination = icon.pixels.data() + static_cast(y) * rowSize; + std::memcpy(destination, source, rowSize); + } + + unlock_surface(surface); + return icon; +} + +RectF pane_global_rect(J2DPane* pane) noexcept { + RectF rect; + CPaneMgr paneMgr; + Mtx m; + for (u8 i = 0; i < 4; ++i) { + const Vec vertex = paneMgr.getGlobalVtx(pane, &m, i, false, 0); + rect.include(vertex.x, vertex.y); + } + return rect; +} + +uint8_t effective_pane_alpha(J2DPane& pane, uint8_t parentAlpha) noexcept { + uint32_t alpha = pane.getAlpha(); + if (pane.isInfluencedAlpha()) { + alpha = alpha * parentAlpha / 255u; + } + return static_cast(alpha); +} + +void collect_picture_layers( + J2DPane* pane, std::vector& layers, uint8_t parentAlpha = 255) noexcept { + if (pane == nullptr || !pane->isVisible()) { + return; + } + + const uint8_t paneAlpha = effective_pane_alpha(*pane, parentAlpha); + if (paneAlpha == 0) { + return; + } + + if (pane->getKind() == MULTI_CHAR('PIC1') || pane->getKind() == MULTI_CHAR('PIC2')) { + auto* picture = static_cast(pane); + if (picture->getTexture(0) != nullptr) { + RectF rect = pane_global_rect(pane); + if (rect.valid()) { + layers.push_back({ + .picture = picture, + .rect = rect, + .alpha = paneAlpha, + }); + } + } + } + + for (J2DPane* child = pane->getFirstChildPane(); child != nullptr; + child = child->getNextChildPane()) + { + collect_picture_layers(child, layers, paneAlpha); + } +} + +std::optional icon_dimension(float value) noexcept { + if (!std::isfinite(value) || value <= 0.0f) { + return std::nullopt; + } + + const auto dimension = static_cast(std::ceil(value)); + if (dimension == 0 || dimension > kMaxRenderedPaneIconSize) { + return std::nullopt; + } + return dimension; +} + +float pane_icon_render_scale(const std::vector& layers, const RectF& canvas) { + float scale = 1.0f; + for (const auto& layer : layers) { + if (layer.picture == nullptr || !layer.rect.valid() || layer.rect.width() <= 0.0f || + layer.rect.height() <= 0.0f) + { + continue; + } + + auto* texture = layer.picture->getTexture(0); + const ResTIMG* image = texture != nullptr ? texture->getTexInfo() : nullptr; + if (image == nullptr || image->width.host() == 0 || image->height.host() == 0) { + continue; + } + + scale = std::max(scale, static_cast(image->width) / layer.rect.width()); + scale = std::max(scale, static_cast(image->height) / layer.rect.height()); + } + + const float canvasMax = std::max(canvas.width(), canvas.height()); + if (canvasMax <= 0.0f) { + return scale; + } + + const float minScale = static_cast(kMinRenderedPaneIconSize) / canvasMax; + const float maxScale = static_cast(kMaxRenderedPaneIconSize) / canvasMax; + return std::clamp(std::max(scale, minScale), 1.0f, maxScale); +} + +void composite_picture_layer( + SDL_Surface& icon, const RectF& canvas, const PictureLayer& layer, float renderScale) { + if (layer.picture == nullptr || !layer.rect.valid()) { + return; + } + + auto* texture = layer.picture->getTexture(0); + if (texture == nullptr) { + return; + } + + auto decoded = decode_timg(texture->getTexInfo()); + if (decoded.data.empty() || decoded.width == 0 || decoded.height == 0) { + return; + } + + const auto colors = layer_colors(*layer.picture, layer.alpha); + auto layerSurface = create_layer_surface(decoded, colors); + if (!layerSurface) { + return; + } + + const float dstLeft = (layer.rect.left - canvas.left) * renderScale; + const float dstTop = (layer.rect.top - canvas.top) * renderScale; + const float dstRight = (layer.rect.right - canvas.left) * renderScale; + const float dstBottom = (layer.rect.bottom - canvas.top) * renderScale; + const float dstWidth = dstRight - dstLeft; + const float dstHeight = dstBottom - dstTop; + if (dstWidth <= 0.0f || dstHeight <= 0.0f) { + return; + } + + const int x0 = std::clamp(static_cast(std::floor(dstLeft)), 0, icon.w); + const int y0 = std::clamp(static_cast(std::floor(dstTop)), 0, icon.h); + const int x1 = std::clamp(static_cast(std::ceil(dstRight)), 0, icon.w); + const int y1 = std::clamp(static_cast(std::ceil(dstBottom)), 0, icon.h); + if (x0 >= x1 || y0 >= y1) { + return; + } + + SDL_Rect destinationRect{ + .x = x0, + .y = y0, + .w = x1 - x0, + .h = y1 - y0, + }; + SDL_BlitSurfaceScaled( + layerSurface.get(), nullptr, &icon, &destinationRect, SDL_SCALEMODE_LINEAR); +} + +std::optional render_j2d_pane_icon(J2DPane* pane) { + std::vector layers; + collect_picture_layers(pane, layers); + if (layers.empty()) { + return std::nullopt; + } + + RectF canvas; + for (const auto& layer : layers) { + canvas.include(layer.rect); + } + if (!canvas.valid()) { + return std::nullopt; + } + + const float renderScale = pane_icon_render_scale(layers, canvas); + auto width = icon_dimension(canvas.width() * renderScale); + auto height = icon_dimension(canvas.height() * renderScale); + if (!width || !height) { + return std::nullopt; + } + + auto surface = create_rgba_surface(*width, *height); + if (!surface) { + return std::nullopt; + } + + for (const auto& layer : layers) { + composite_picture_layer(*surface, canvas, layer, renderScale); + } + + return icon_from_surface(surface.get()); +} + +std::optional icon_provider(std::string_view source) { + const auto itemNo = item_for_source(source); + if (!itemNo) { + return std::nullopt; + } + + auto& cache = icon_cache(); + const std::string key(source); + auto it = cache.find(key); + if (it == cache.end()) { + auto icon = render_item_icon(*itemNo); + if (!icon) { + return std::nullopt; + } + if (cache.size() >= kMaxCachedIcons) { + cache.erase(cache.begin()); + } + it = cache.emplace(key, std::move(*icon)).first; + } + + const auto& icon = it->second; + return aurora::rmlui::RuntimeTexture{ + .width = icon.width, + .height = icon.height, + .rgba8 = + std::span(reinterpret_cast(icon.pixels.data()), icon.pixels.size()), + .premultipliedAlpha = true, + }; +} + +std::optional meter_texture_provider(std::string_view source) { + if (!source.starts_with(kMeterSourcePrefix)) { + return std::nullopt; + } + + const std::string name(strip_query(source.substr(kMeterSourcePrefix.size()))); + if (name != "midna") { + return std::nullopt; + } + + const auto& state = midna_icon_state(); + if (!state.valid) { + return std::nullopt; + } + + return aurora::rmlui::RuntimeTexture{ + .width = state.icon.width, + .height = state.icon.height, + .rgba8 = std::span( + reinterpret_cast(state.icon.pixels.data()), state.icon.pixels.size()), + .premultipliedAlpha = true, + }; +} + +} // namespace + +void register_icon_texture_provider() noexcept { + aurora::rmlui::register_texture_provider(std::string(kScheme), icon_provider); + aurora::rmlui::register_texture_provider(std::string(kMeterScheme), meter_texture_provider); +} + +void unregister_icon_texture_provider() noexcept { + aurora::rmlui::unregister_texture_provider(kScheme); + aurora::rmlui::unregister_texture_provider(kMeterScheme); + icon_cache().clear(); + midna_icon_state() = {}; +} + +void update_midna_icon_texture(J2DPane* pane) noexcept { + auto& state = midna_icon_state(); + if (pane == nullptr || !pane->isVisible()) { + if (state.valid) { + state.valid = false; + state.icon = {}; + state.revision++; + } + return; + } + + auto icon = render_j2d_pane_icon(pane); + if (!icon) { + if (state.valid) { + state.valid = false; + state.icon = {}; + state.revision++; + } + return; + } + + if (!state.valid || state.icon.width != icon->width || state.icon.height != icon->height || + state.icon.pixels != icon->pixels) + { + state.icon = std::move(*icon); + state.valid = true; + state.revision++; + } +} + +std::string midna_icon_source() { + const auto& state = midna_icon_state(); + if (!state.valid) { + return ""; + } + return fmt::format( + "{}://midna?slot={}", kMeterScheme, state.revision % kMeterTextureSourceSlots); +} + +uint64_t midna_icon_revision() noexcept { + const auto& state = midna_icon_state(); + return state.valid ? state.revision : 0; +} + +std::string item_icon_source_for_button(Control control) { + std::optional itemNo; + switch (control) { + case Control::X: + itemNo = selected_slot_item(0); + break; + case Control::Y: + itemNo = selected_slot_item(1); + break; + case Control::B: + itemNo = b_button_item(); + break; + default: + break; + } + if (!itemNo) { + return {}; + } + return item_source_for_item(*itemNo); +} + +std::string item_count_label_for_button(Control control) { + std::optional count; + switch (control) { + case Control::X: + count = selected_slot_count(0); + break; + case Control::Y: + count = selected_slot_count(1); + break; + default: + break; + } + if (!count) { + return {}; + } + return fmt::format("{}", *count); +} + +std::optional item_oil_fill_for_button(Control control) noexcept { + std::optional itemNo; + switch (control) { + case Control::X: + itemNo = selected_slot_item(0); + break; + case Control::Y: + itemNo = selected_slot_item(1); + break; + default: + break; + } + if (!itemNo || (*itemNo != dItemNo_KANTERA_e && *itemNo != dItemNo_KANTERA2_e)) { + return std::nullopt; + } + + const int maxOil = dComIfGs_getMaxOil(); + if (maxOil <= 0) { + return std::nullopt; + } + return std::clamp( + static_cast(dComIfGs_getOil()) / static_cast(maxOil), 0.0f, 1.0f); +} + +} // namespace dusk::ui + +#else + +namespace dusk::ui { + +void register_icon_texture_provider() noexcept {} +void unregister_icon_texture_provider() noexcept {} +void update_midna_icon_texture(J2DPane*) noexcept {} +std::string midna_icon_source() { + return {}; +} +uint64_t midna_icon_revision() noexcept { + return 0; +} +std::string item_icon_source_for_button(Control) { + return {}; +} +std::string item_count_label_for_button(Control) { + return {}; +} +std::optional item_oil_fill_for_button(Control) noexcept { + return std::nullopt; +} + +} // namespace dusk::ui + +#endif diff --git a/src/dusk/ui/icon_provider.hpp b/src/dusk/ui/icon_provider.hpp new file mode 100644 index 0000000000..a77c518992 --- /dev/null +++ b/src/dusk/ui/icon_provider.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "controls.hpp" + +#include +#include +#include + +class J2DPane; + +namespace dusk::ui { + +void register_icon_texture_provider() noexcept; +void unregister_icon_texture_provider() noexcept; + +void update_midna_icon_texture(J2DPane* pane) noexcept; +std::string midna_icon_source(); +uint64_t midna_icon_revision() noexcept; +std::string item_icon_source_for_button(Control control); +std::string item_count_label_for_button(Control control); +std::optional item_oil_fill_for_button(Control control) noexcept; + +} // namespace dusk::ui diff --git a/src/dusk/ui/overlay.cpp b/src/dusk/ui/overlay.cpp index 69c262720e..192b0ebf6a 100644 --- a/src/dusk/ui/overlay.cpp +++ b/src/dusk/ui/overlay.cpp @@ -5,6 +5,7 @@ #include "dusk/action_bindings.h" #include "controller_config.hpp" #include "dusk/livesplit.h" +#include "dusk/settings.h" #include "dusk/speedrun.h" #include "fmt/format.h" #include "magic_enum.hpp" @@ -13,6 +14,7 @@ #include #include #include +#include #include #include @@ -187,50 +189,13 @@ void remove_element(Rml::Element*& elem) noexcept { } // namespace -// https://vplesko.com/posts/how_to_implement_an_fps_counter.html -void Overlay::advance_fps_counter(float& outFps, Uint64 perfFreq) { - if (perfFreq == 0) { - outFps = 0.f; - return; - } - - const Uint64 curr = SDL_GetPerformanceCounter(); - if (!mFpsHavePrevCounter) { - mFpsPrevCounter = curr; - mFpsHavePrevCounter = true; - outFps = 0.f; - return; - } - - const Uint64 processingTicks = curr - mFpsPrevCounter; - mFpsPrevCounter = curr; - - mFpsFrameEvents.push_back({curr, processingTicks}); - mFpsSumTicks += processingTicks; - - while (!mFpsFrameEvents.empty() && mFpsFrameEvents.front().endCounter + perfFreq < curr) { - mFpsSumTicks -= mFpsFrameEvents.front().processingTicks; - mFpsFrameEvents.pop_front(); - } - - const auto n = mFpsFrameEvents.size(); - if (n == 0 || mFpsSumTicks == 0) { - outFps = 0.f; - return; - } - - const double avgSeconds = - static_cast(mFpsSumTicks) / static_cast(n) / static_cast(perfFreq); - outFps = static_cast(1.0 / avgSeconds); -} - static std::string FormatTime(OSTime ticks) { OSCalendarTime t; OSTicksToCalendarTime(ticks, &t); return fmt::format("{0:02}:{1:02}:{2:02}.{3:03}", t.hour, t.min, t.sec, t.msec); } -Overlay::Overlay() : Document(kDocumentSource) { +Overlay::Overlay() : Document(kDocumentSource, true) { mFpsCounter = mDocument->GetElementById("fps"); mSpeedrunTimer = mDocument->GetElementById("speedrun-timer"); mSpeedrunRta = mDocument->GetElementById("speedrun-rta"); @@ -276,8 +241,7 @@ void Overlay::update() { mFpsCounter->SetAttribute("corner", kFpsCorners[idx]); const Uint64 perfFreq = SDL_GetPerformanceFrequency(); - float fps = 0.f; - advance_fps_counter(fps, perfFreq); + float fps = aurora_get_fps(); const Uint64 now = SDL_GetPerformanceCounter(); // Limit updates to twice per second @@ -290,9 +254,6 @@ void Overlay::update() { } } else { mFpsCounter->RemoveAttribute("open"); - mFpsFrameEvents.clear(); - mFpsSumTicks = 0; - mFpsHavePrevCounter = false; mFpsLastUpdate = 0; } } @@ -357,6 +318,7 @@ void Overlay::update() { u32 count = 0; const bool showControllerWarning = PADGetIndexForPort(PAD_CHAN0) < 0 && PADGetKeyButtonBindings(PAD_CHAN0, &count) == nullptr && + !getSettings().game.enableTouchControls && dynamic_cast(top_document()) == nullptr && dynamic_cast(top_document()) == nullptr; if (showControllerWarning && mControllerWarning == nullptr) { diff --git a/src/dusk/ui/overlay.hpp b/src/dusk/ui/overlay.hpp index 8a2edd4a26..a8ccde5b9d 100644 --- a/src/dusk/ui/overlay.hpp +++ b/src/dusk/ui/overlay.hpp @@ -3,7 +3,6 @@ #include "document.hpp" #include -#include namespace dusk::ui { @@ -26,19 +25,7 @@ protected: Rml::Element* mSpeedrunIgt = nullptr; clock::time_point mCurrentToastStartTime; clock::time_point mMenuNotificationStartTime; - - struct FpsFrameEvent { - Uint64 endCounter; - Uint64 processingTicks; - }; - - std::deque mFpsFrameEvents; - Uint64 mFpsSumTicks = 0; - bool mFpsHavePrevCounter = false; - Uint64 mFpsPrevCounter = 0; Uint64 mFpsLastUpdate = 0; - - void advance_fps_counter(float& outFps, Uint64 perfFreq); }; } // namespace dusk::ui diff --git a/src/dusk/ui/prelaunch.cpp b/src/dusk/ui/prelaunch.cpp index 23de46155e..6fefe8839a 100644 --- a/src/dusk/ui/prelaunch.cpp +++ b/src/dusk/ui/prelaunch.cpp @@ -739,7 +739,7 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB } IsGameLaunched = true; - hide(true); + pop(false); }); apply_intro_animation(mMenuButtons.back()->root(), "delay-1"); diff --git a/src/dusk/ui/preset.cpp b/src/dusk/ui/preset.cpp index 5d1bbcdd28..1a75a27e6b 100644 --- a/src/dusk/ui/preset.cpp +++ b/src/dusk/ui/preset.cpp @@ -21,6 +21,7 @@ void applyPresetClassic() { s.game.shadowResolutionMultiplier.setValue(1); s.game.hideTvSettingsScreen.setValue(false); s.game.menuScalingMode.setValue(MenuScaling::GameCube); + s.game.enableMenuPointer.setValue(false); AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT); } @@ -53,6 +54,7 @@ void applyPresetDusk() { s.game.autoSave.setValue(true); s.game.menuScalingMode.setValue(MenuScaling::Dusklight); s.game.enhancedMapMenus.setValue(true); + s.game.enableMenuPointer.setValue(true); } } // namespace diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index df75293cbe..cf0d561a11 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -6,6 +6,7 @@ #include "dusk/app_info.hpp" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/audio/DuskDsp.hpp" +#include "dusk/android_frame_rate.hpp" #include "dusk/config.hpp" #include "dusk/hotkeys.h" #include "dusk/data.hpp" @@ -22,6 +23,7 @@ #include "menu_bar.hpp" #include "pane.hpp" #include "prelaunch.hpp" +#include "touch_controls_editor.hpp" #include "ui.hpp" #include @@ -35,6 +37,17 @@ #include #include +#if defined(__APPLE__) +#include +#endif + +#if defined(TARGET_ANDROID) || defined(__ANDROID__) || \ + (defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST) +#define TOUCH_CONTROLS_AVAILABLE true +#else +#define TOUCH_CONTROLS_AVAILABLE false +#endif + namespace dusk::ui { namespace { @@ -482,14 +495,19 @@ SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar& var, Rml::String key, Rml::String helpText, int min, int max, int step = 5, - std::function isDisabled = {}, std::string suffix = "") { + std::function isDisabled = {}, std::function onChange = {}, + std::string suffix = "") { auto& button = leftPane.add_child(NumberButton::Props{ .key = std::move(key), .getValue = [&var] { return var; }, .setValue = - [&var, min, max](int value) { - var.setValue(std::clamp(value, min, max)); + [&var, min, max, callback = std::move(onChange)](int value) { + const int clampedValue = std::clamp(value, min, max); + var.setValue(clampedValue); config::Save(); + if (callback) { + callback(clampedValue); + } }, .isDisabled = std::move(isDisabled), .isModified = [&var] { return var.getValue() != var.getDefaultValue(); }, @@ -871,6 +889,21 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { pane.add_rml( "
Display the current framerate in a corner of the screen while playing."); }); + config_bool_select(leftPane, rightPane, getSettings().video.rememberWindowSize, + { + .key = "Remember Window Size", + .helpText = "Save and restore the previous session's window size when opening Dusklight.", + .onChange = + [](bool value) { + if (value && !dusk::getSettings().video.enableFullscreen) { + const auto windowSize = aurora::window::get_window_size(); + dusk::getSettings().video.lastWindowWidth.setValue(windowSize.width); + dusk::getSettings().video.lastWindowHeight.setValue(windowSize.height); + dusk::config::Save(); + } + }, + .isDisabled = [] { return IsMobile; }, + }); leftPane.add_section("Resolution"); graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.internalResolutionScale, @@ -971,6 +1004,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .on_pressed([i] { mDoAud_seStartMenu(kSoundItemChange); getSettings().game.enableFrameInterpolation.setValue(static_cast(i)); + android::update_surface_frame_rate(); config::Save(); }); } @@ -978,7 +1012,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { }); config_int_select(leftPane, rightPane, getSettings().video.maxFrameRate, "Framerate Cap", "Limit the framerate to the specified value.", 30, 540, 1, - [] { return getSettings().game.enableFrameInterpolation.getValue() != FrameInterpMode::Capped; }); + [] { return getSettings().game.enableFrameInterpolation.getValue() != FrameInterpMode::Capped; }, + [](int) { android::update_surface_frame_rate(); }); config_bool_select(leftPane, rightPane, getSettings().game.enableMapBackground, { .key = "Enable Mini-Map Shadows", @@ -1022,6 +1057,31 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .onChange = [](bool value) { aurora_set_background_input(value); }, }); +#if TOUCH_CONTROLS_AVAILABLE + leftPane.add_section("Touch"); + addOption("Touch Controls", getSettings().game.enableTouchControls, + "Enables controls overlay for touch screens.

Press and drag on the left side " + "of the screen to move, and on the right side of the screen to control the camera."); + auto& customizeTouchLayout = leftPane.add_button(ControlledButton::Props{ + .text = "Customize Layout", + .isDisabled = [] { return !getSettings().game.enableTouchControls; }, + }); + leftPane.register_control(customizeTouchLayout.on_pressed( + [this] { push(std::make_unique()); }), + rightPane, [](Pane& pane) { + pane.clear(); + pane.add_text("Open the touch controls layout editor."); + }); + config_percent_select(leftPane, rightPane, getSettings().game.touchCameraXSensitivity, + "Touch Camera X Sensitivity", + "Adjusts touch camera horizontal sensitivity.

Applies to touch input only.", + 25, 400, 5, [] { return !getSettings().game.enableTouchControls; }); + config_percent_select(leftPane, rightPane, getSettings().game.touchCameraYSensitivity, + "Touch Camera Y Sensitivity", + "Adjusts touch camera vertical sensitivity.

Applies to touch input only.", 25, + 400, 5, [] { return !getSettings().game.enableTouchControls; }); +#endif + leftPane.add_section("Camera"); addOption("Free Camera", getSettings().game.freeCamera, "Enables free camera control, letting you control the camera fully with the C-Stick."); @@ -1089,6 +1149,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { [] { return !getSettings().game.enableMouseAim || !getSettings().game.enableMouseCamera; }); leftPane.add_section("Gameplay"); + addOption("Mouse/Touch in Menus", getSettings().game.enableMenuPointer, + "Enables mouse and touch input for supported in-game menus."); addOption("Invert Air/Swim X Axis", getSettings().game.invertAirSwimX, "Invert horizontal movement while flying or swimming."); addOption("Invert Air/Swim Y Axis", getSettings().game.invertAirSwimY, diff --git a/src/dusk/ui/touch_controls.cpp b/src/dusk/ui/touch_controls.cpp new file mode 100644 index 0000000000..6056f0d11d --- /dev/null +++ b/src/dusk/ui/touch_controls.cpp @@ -0,0 +1,1390 @@ +#include "touch_controls.hpp" +#include "touch_controls_common.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "d/actor/d_a_alink.h" +#include "d/actor/d_a_player.h" +#include "d/d_com_inf_game.h" +#include "d/d_meter2_info.h" +#include "d/d_msg_object.h" +#include "dusk/action_bindings.h" +#include "dusk/menu_pointer.h" +#include "dusk/settings.h" +#include "dusk/touch_camera.h" +#include "f_op/f_op_overlap_mng.h" +#include "icon_provider.hpp" +#include "m_Do/m_Do_graphic.h" +#include "ui.hpp" + +namespace dusk::ui { +namespace { + +constexpr u32 kPort = PAD_CHAN0; +constexpr float kStickRadiusDp = 62.f; +constexpr float kStickKnobRadiusDp = 24.f; +constexpr float kAnalogZoneTopDp = 92.f; +constexpr float kAnalogZoneBottomDp = 30.f; +constexpr float kLeftZoneWidth = 0.46f; +constexpr float kRightZoneStart = 0.52f; +constexpr u8 kTriggerAnalog = 180; +constexpr auto kLDoubleTapWindow = std::chrono::milliseconds(300); +constexpr auto kHoldActionDuration = std::chrono::milliseconds(450); +constexpr float kFaceIconTargetRatio = 0.76f; +constexpr float kPressedScale = 0.94f; +constexpr size_t kEquipTargetCount = 4; + +std::array sEquipTargets{}; +std::array(Control::COUNT)> sControlOverrides{}; +TouchControls* sTouchControls = nullptr; + +struct ControlInfo { + const char* id = nullptr; + const char* iconId = nullptr; + const char* oilId = nullptr; + const char* oilFillId = nullptr; + const char* countId = nullptr; + u16 padButton = 0; + std::optional tapAction; + std::optional holdAction; +}; + +constexpr std::array(Control::COUNT)> kControls = {{ + { + .id = "button-a", + .padButton = PAD_BUTTON_A, + }, + { + .id = "button-b", + .iconId = "button-b-icon", + .padButton = PAD_BUTTON_B, + }, + { + .id = "button-x", + .iconId = "button-x-icon", + .oilId = "button-x-oil", + .oilFillId = "button-x-oil-fill", + .countId = "button-x-count", + .padButton = PAD_BUTTON_X, + }, + { + .id = "button-y", + .iconId = "button-y-icon", + .oilId = "button-y-oil", + .oilFillId = "button-y-oil-fill", + .countId = "button-y-count", + .padButton = PAD_BUTTON_Y, + }, + { + .id = "button-z", + .iconId = "z-midna-icon", + .padButton = PAD_TRIGGER_Z, + }, + { + .id = "trigger-l", + }, + { + .id = "trigger-r", + }, + { + .id = "first-person", + .tapAction = ActionBinds::FIRST_PERSON_CAMERA, + }, + { + .id = "items", + .padButton = PAD_BUTTON_UP, + }, + { + .id = "collections", + .padButton = PAD_BUTTON_START, + }, + { + .id = "map", + .tapAction = ActionBinds::OPEN_MAP_SCREEN, + .holdAction = ActionBinds::TOGGLE_MINIMAP, + }, + { + .id = "skip", + .padButton = PAD_BUTTON_START, + }, +}}; + +constexpr const ControlInfo* control_info(Control control) noexcept { + const auto index = static_cast(control); + return index < kControls.size() ? &kControls[index] : nullptr; +} + +bool control_override_active(Control control) noexcept { + const auto index = static_cast(control); + return index < sControlOverrides.size() && sControlOverrides[index] != ControlOverride::Default; +} + +Rml::String touch_controls_document_source() { + const auto fragment = touch_controls_rml_fragment(); + return Rml::String{R"RML( + + + + + + + + + +)RML"} + Rml::String{fragment.data(), fragment.size()} + + Rml::String{R"RML( + + +)RML"}; +} + +s8 stick_value(float value) noexcept { + return static_cast(std::clamp(std::lround(value * 127.f), -127l, 127l)); +} + +bool player_attention_locked() noexcept { + const auto* player = daPy_getPlayerActorClass(); + return player != nullptr && (player->checkAttentionLock() || player->checkEnemyAttentionLock()); +} + +bool hawkeye_active() noexcept { + return dCamera_c::isAimActive() && dComIfGp_checkPlayerStatus0(0, 0x200000); +} + +bool item_wheel_active() noexcept { + return dMeter2Info_getWindowStatus() == 2; +} + +bool fishing_controls_active() noexcept { + const auto* player = daAlink_getAlinkActorClass(); + if (player == nullptr) { + return false; + } + return player->checkCanoeFishingWaitAnime(); +} + +enum class StickOutput { + MainStick, + CStick, +}; + +StickOutput stick_output_mode() noexcept { + if (fishing_controls_active() || hawkeye_active()) { + return StickOutput::CStick; + } + return StickOutput::MainStick; +} + +bool controls_available(bool allowItemWheel) noexcept { + if (dComIfGp_getLinkPlayer() == nullptr) { + return false; + } + + const auto* fader = mDoGph_gInf_c::getFader(); + if (fader == nullptr || fader->getStatus() != JUTFader::Wait || mDoGph_gInf_c::isFade()) { + return false; + } + + const bool itemWheelActive = allowItemWheel && item_wheel_active(); + const auto heapLock = dComIfGp_isHeapLockFlag(); + if ((heapLock != 0 && heapLock != 5 && !(itemWheelActive && heapLock == 1)) || + (dComIfGp_isPauseFlag() && !itemWheelActive) || dComIfGp_getMesgStatus() != 0 || + dComIfGp_isEnableNextStage() || fopOvlpM_IsDoingReq()) + { + return false; + } + + return true; +} + +Rml::Vector2f clamped_stick_delta( + Rml::Vector2f start, Rml::Vector2f current, float stickRadius) noexcept { + Rml::Vector2f delta = current - start; + const float length = std::sqrt(delta.x * delta.x + delta.y * delta.y); + delta *= length > stickRadius ? stickRadius / length : 1.f; + return delta; +} + +struct FaceButtonState { + std::string iconSource; + uint64_t iconRevision = 0; + bool visible = false; + bool showIcon = false; +}; + +void release_rml_texture(const std::string& source) noexcept { + if (!source.empty()) { + Rml::ReleaseTexture(source); + } +} + +FaceButtonState override_button_state(Control control) { + if (control_override_active(control)) { + return { + .iconSource = "", + .visible = true, + .showIcon = false, + }; + } + + return {}; +} + +bool game_controls_suppressed() noexcept { + return !controls_available(true) || dComIfGp_event_runCheck() || + (dComIfGp_getMsgObjectClass() != nullptr && dMsgObject_isTalkNowCheck()); +} + +FaceButtonState xy_button_state(Control control) { + if (sControlOverrides[static_cast(control)] != ControlOverride::Default) { + return override_button_state(control); + } + if (game_controls_suppressed()) { + return {}; + } + + const bool itemMode = dComIfGp_getLinkPlayer() != nullptr && daPy_py_c::checkNowWolf() == 0; + const auto source = itemMode ? item_icon_source_for_button(control) : std::string(); + return { + .iconSource = source, + .visible = true, + .showIcon = itemMode && !source.empty(), + }; +} + +FaceButtonState z_button_state() { + if (sControlOverrides[static_cast(Control::Z)] != ControlOverride::Default) { + return override_button_state(Control::Z); + } + if (game_controls_suppressed()) { + return {}; + } + + const auto source = midna_icon_source(); + return { + .iconSource = source, + .iconRevision = midna_icon_revision(), + .visible = true, + .showIcon = !source.empty(), + }; +} + +FaceButtonState b_button_state() { + if (game_controls_suppressed()) { + return { + .iconSource = "", + .visible = true, + .showIcon = false, + }; + } + + const auto source = item_icon_source_for_button(Control::B); + return { + .iconSource = source, + .visible = true, + .showIcon = dMeter2Info_isUseButton(METER2_USEBUTTON_B) && !source.empty(), + }; +} + +void clear_equip_targets() noexcept { + for (auto& target : sEquipTargets) { + target.valid = false; + } +} + +void sync_equip_target(int slot, ControlRect rectDp, float widthRatio, float heightRatio, + bool square = false) noexcept { + if (slot < 0 || static_cast(slot) >= sEquipTargets.size()) { + return; + } + + auto& target = sEquipTargets[slot]; + target.valid = false; + + auto* context = aurora::rmlui::get_context(); + if (context == nullptr) { + return; + } + + const auto dimensions = context->GetDimensions(); + if (dimensions.x <= 0 || dimensions.y <= 0) { + return; + } + + const float scale = std::max(context->GetDensityIndependentPixelRatio(), 1.f); + const Rml::Vector2f buttonPosition{rectDp.l * scale, rectDp.t * scale}; + const Rml::Vector2f buttonSize{rectDp.w * scale, rectDp.h * scale}; + if (buttonSize.x <= 0.f || buttonSize.y <= 0.f) { + return; + } + + Rml::Vector2f targetSize{buttonSize.x * widthRatio, buttonSize.y * heightRatio}; + if (square) { + const float side = std::min(buttonSize.x, buttonSize.y) * widthRatio; + targetSize = Rml::Vector2f{side, side}; + } + const auto targetPosition = buttonPosition + (buttonSize - targetSize) * 0.5f; + const float scaleX = mDoGph_gInf_c::getWidthF() / static_cast(dimensions.x); + const float scaleY = mDoGph_gInf_c::getHeightF() / static_cast(dimensions.y); + + target.left = mDoGph_gInf_c::getMinXF() + targetPosition.x * scaleX; + target.top = mDoGph_gInf_c::getMinYF() + targetPosition.y * scaleY; + target.width = targetSize.x * scaleX; + target.height = targetSize.y * scaleY; + target.valid = true; +} + +} // namespace + +bool get_equip_target(int slot, EquipTarget& target) noexcept { + if (slot < 0 || static_cast(slot) >= sEquipTargets.size()) { + return false; + } + + const auto& stored = sEquipTargets[slot]; + if (!stored.valid) { + return false; + } + + target = stored; + return true; +} + +void set_control_override(Control control, ControlOverride override) noexcept { + const auto index = static_cast(control); + if (index >= sControlOverrides.size()) { + return; + } + sControlOverrides[index] = override; +} + +void sync_virtual_input() noexcept { + if (sTouchControls != nullptr) { + sTouchControls->sync_virtual_input(); + } +} + +TouchControls::TouchControls() + : Document(touch_controls_document_source(), true), + mRoot(mDocument != nullptr ? mDocument->GetElementById("root") : nullptr), + mControlStick(mDocument != nullptr ? mDocument->GetElementById("control-stick") : nullptr), + mControlKnob(mDocument != nullptr ? mDocument->GetElementById("control-knob") : nullptr), + mActionBar(mDocument != nullptr ? mDocument->GetElementById("action-bar") : nullptr) { + sTouchControls = this; + if (mDocument != nullptr) { + for (std::size_t i = 0; i < kControls.size(); ++i) { + const auto& info = kControls[i]; + auto& elements = mControlElements[i]; + elements.root = info.id != nullptr ? mDocument->GetElementById(info.id) : nullptr; + elements.icon = + info.iconId != nullptr ? mDocument->GetElementById(info.iconId) : nullptr; + elements.oil = info.oilId != nullptr ? mDocument->GetElementById(info.oilId) : nullptr; + elements.oilFill = + info.oilFillId != nullptr ? mDocument->GetElementById(info.oilFillId) : nullptr; + elements.count = + info.countId != nullptr ? mDocument->GetElementById(info.countId) : nullptr; + } + } + + listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) { + if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") && + Document::visible()) + { + Document::hide(mPendingClose); + } + }); + + auto listenControl = [this](Control control) { + const auto index = static_cast(control); + auto* element = index < mControlElements.size() ? mControlElements[index].root : nullptr; + if (element == nullptr) { + return; + } + listen(element, aurora::rmlui::TouchStartEvent, [this, control](Rml::Event& event) { + if (!visible() || mWasSuppressed || !getSettings().game.enableTouchControls) { + return; + } + if (start_control_touch(touch_event_id(event), control)) { + event.StopPropagation(); + } + }); + listen(element, aurora::rmlui::TouchEndEvent, [this](Rml::Event& event) { + if (release_control_touch(touch_event_id(event), false)) { + event.StopPropagation(); + } + }); + listen(element, aurora::rmlui::TouchCancelEvent, [this](Rml::Event& event) { + if (release_control_touch(touch_event_id(event), true)) { + event.StopPropagation(); + } + }); + }; + for (std::size_t i = 0; i < kControls.size(); ++i) { + listenControl(static_cast(i)); + } + + listen(mRoot, aurora::rmlui::TouchStartEvent, + [this](Rml::Event& event) { handle_touch_down(event); }); + listen(mRoot, aurora::rmlui::TouchMoveEvent, + [this](Rml::Event& event) { handle_touch_motion(event); }); + listen( + mRoot, aurora::rmlui::TouchEndEvent, [this](Rml::Event& event) { handle_touch_up(event); }); + listen(mRoot, aurora::rmlui::TouchCancelEvent, + [this](Rml::Event& event) { handle_touch_cancel(event); }); + listen(mRoot, Rml::EventId::Mousemove, [this](Rml::Event& event) { handle_mouse_move(event); }); + listen(mRoot, Rml::EventId::Mousedown, [this](Rml::Event& event) { handle_mouse_down(event); }); + listen(mRoot, Rml::EventId::Mouseup, [this](Rml::Event& event) { handle_mouse_up(event); }); +} + +TouchControls::~TouchControls() { + clear_virtual_input(); + clearAllVirtualActionBinds(); + if (sTouchControls == this) { + sTouchControls = nullptr; + } +} + +void TouchControls::show() { + Document::show(); + if (mRoot != nullptr) { + mRoot->SetAttribute("open", ""); + } +} + +void TouchControls::hide(bool close) { + clear_virtual_input(); + if (mRoot != nullptr) { + mRoot->RemoveAttribute("open"); + mPendingClose = close; + } else { + Document::hide(close); + } +} + +void TouchControls::set_control_pressed(Control control, bool pressed) { + set_control_visual(control, pressed); + sync_control_button_mask(); + + switch (control) { + case Control::L: + if (control_override_active(control)) { + mLPressed = pressed; + mLLatched = false; + mManualLLatched = false; + mLReleasePending = false; + mLPressStartTime = {}; + mLastLTapTime = {}; + break; + } + if (pressed && (mLLatched || mManualLLatched)) { + mLLatched = false; + mManualLLatched = false; + mLPressed = false; + mLReleasePending = true; + mLPressStartTime = {}; + mLastLTapTime = {}; + set_control_visual(control, false); + } else if (pressed) { + const auto now = clock::now(); + if (!player_attention_locked() && mLastLTapTime != clock::time_point{} && + now - mLastLTapTime <= kLDoubleTapWindow) + { + mManualLLatched = true; + mLPressed = false; + mLReleasePending = true; + mLPressStartTime = {}; + mLastLTapTime = {}; + } else if (!mLReleasePending) { + mLPressed = true; + mLPressStartTime = now; + } + } else if (!mLReleasePending) { + mLPressed = false; + } + if (!pressed) { + const auto now = clock::now(); + if (!mLReleasePending) { + const bool wasQuickTap = mLPressStartTime != clock::time_point{} && + now - mLPressStartTime <= kLDoubleTapWindow; + mLastLTapTime = wasQuickTap ? now : clock::time_point{}; + } + mLPressStartTime = {}; + mLReleasePending = false; + } + if (!pressed && !player_attention_locked()) { + mLLatched = false; + } + break; + case Control::R: + mRTriggerHeld = pressed; + break; + default: + break; + } +} + +bool TouchControls::fire_control_action(Control control, ControlAction action) noexcept { + const auto* info = control_info(control); + if (info == nullptr) { + return false; + } + + const auto actionBind = action == ControlAction::Tap ? info->tapAction : info->holdAction; + if (!actionBind) { + return false; + } + + const auto actionIndex = static_cast(*actionBind); + if (actionIndex >= mQueuedActions.size()) { + return false; + } + + mQueuedActions.set(actionIndex); + return true; +} + +bool TouchControls::start_control_touch(SDL_FingerID id, Control control) noexcept { + const auto index = static_cast(control); + if (index >= mControlTouches.size()) { + return false; + } + + auto& touch = mControlTouches[index]; + if (touch.active) { + return false; + } + + touch = { + .id = id, + .startTime = clock::now(), + .active = true, + .longPressFired = false, + }; + set_control_pressed(control, true); + return true; +} + +void TouchControls::release_control(Control control) noexcept { + const auto index = static_cast(control); + if (index < mControlTouches.size()) { + mControlTouches[index] = {}; + } + + sync_control_button_mask(); + switch (control) { + case Control::L: + mLPressed = false; + mLLatched = false; + mManualLLatched = false; + mLReleasePending = false; + mLPressStartTime = {}; + mLastLTapTime = {}; + break; + case Control::R: + mRTriggerHeld = false; + break; + default: + break; + } + set_control_visual(control, false); +} + +void TouchControls::sync_control_button_mask() noexcept { + u16 buttonMask = 0; + for (std::size_t i = 0; i < mControlTouches.size() && i < kControls.size(); ++i) { + if (mControlTouches[i].active) { + buttonMask |= kControls[i].padButton; + } + } + mButtonMask = buttonMask; +} + +void TouchControls::set_control_visual(Control control, bool pressed) noexcept { + const auto index = static_cast(control); + auto* element = index < mControlElements.size() ? mControlElements[index].root : nullptr; + if (index >= mControlVisualPressed.size()) { + return; + } + mControlVisualPressed[index] = pressed; + if (element != nullptr) { + element->SetClass("pressed", pressed); + } + apply_control_transform(control); +} + +void TouchControls::apply_control_transform(Control control) noexcept { + const auto index = static_cast(control); + if (index >= mControlElements.size()) { + return; + } + auto& elements = mControlElements[index]; + auto& layout = elements.layout; + const float pressedScale = + index < mControlVisualPressed.size() && mControlVisualPressed[index] ? kPressedScale : 1.f; + apply_control_transform_if_changed( + elements.root, layout.appliedTransform, layout.layoutScale * pressedScale); +} + +void TouchControls::sync_l_lock_state() noexcept { + if (player_attention_locked()) { + if (mLPressed) { + mLLatched = true; + } + } else { + mLLatched = false; + } +} + +void TouchControls::clear_motion_touch_input() noexcept { + mMoveTouch = {}; + mCameraTouch = {}; + touch_camera::clear(); + if (mControlStick != nullptr) { + mControlStick->SetClass("active", false); + } +} + +void TouchControls::clear_control_input() noexcept { + for (std::size_t i = 0; i < mControlTouches.size(); ++i) { + release_control(static_cast(i)); + } + mQueuedActions.reset(); + mButtonMask = 0; + mWantsVirtualPad = false; + PADClearVirtualStatus(kPort); + for (const auto& control : kControls) { + if (control.tapAction) { + setVirtualActionBind(*control.tapAction, kPort, false, false); + } + if (control.holdAction) { + setVirtualActionBind(*control.holdAction, kPort, false, false); + } + } +} + +void TouchControls::clear_virtual_input() noexcept { + clear_motion_touch_input(); + mMenuPointerTouch = 0; + mMenuPointerMouseSuppressions = 0; + mMenuPointerTouchActive = false; + clear_control_input(); +} + +void TouchControls::sync_touch_state() noexcept { + const bool controlsEnabled = getSettings().game.enableTouchControls; + const bool pointerMenuActive = menu_pointer::active(); + if (mWasSuppressed || (!controlsEnabled && !pointerMenuActive)) { + clear_virtual_input(); + return; + } + + if (pointerMenuActive) { + clear_motion_touch_input(); + if (!controlsEnabled) { + clear_control_input(); + return; + } + } + + sync_l_lock_state(); + const bool aimActive = dCamera_c::isAimActive(); + if (aimActive && !hawkeye_active() && mMoveTouch.active) { + if (!mCameraTouch.active) { + mCameraTouch = mMoveTouch; + mCameraTouch.start = mMoveTouch.current; + } + mMoveTouch = {}; + } + + const float stickRadius = kStickRadiusDp * touch_dp_scale(); + if (mControlStick != nullptr) { + if (mMoveTouch.active && stickRadius > 0.f) { + const auto delta = + clamped_stick_delta(mMoveTouch.start, mMoveTouch.current, stickRadius); + const float knobRadius = kStickKnobRadiusDp * touch_dp_scale(); + mControlStick->SetClass("active", true); + mControlStick->SetProperty(Rml::PropertyId::Left, + Rml::Property(mMoveTouch.start.x - stickRadius, Rml::Unit::PX)); + mControlStick->SetProperty(Rml::PropertyId::Top, + Rml::Property(mMoveTouch.start.y - stickRadius, Rml::Unit::PX)); + mControlKnob->SetProperty(Rml::PropertyId::Left, + Rml::Property(stickRadius + delta.x - knobRadius, Rml::Unit::PX)); + mControlKnob->SetProperty(Rml::PropertyId::Top, + Rml::Property(stickRadius + delta.y - knobRadius, Rml::Unit::PX)); + } else { + mControlStick->SetClass("active", false); + } + } + + sync_visual_state(); +} + +void TouchControls::sync_virtual_input() noexcept { + sync_touch_state(); + if (mWasSuppressed || !getSettings().game.enableTouchControls) { + return; + } + + PADStatus status{}; + status.err = PAD_ERR_NONE; + status.button = mButtonMask; + + if (mLPressed || mLLatched || mManualLLatched) { + status.button |= PAD_TRIGGER_L; + status.triggerLeft = kTriggerAnalog; + } + if (mRTriggerHeld) { + status.button |= PAD_TRIGGER_R; + status.triggerRight = kTriggerAnalog; + } + + const float stickRadius = kStickRadiusDp * touch_dp_scale(); + if (mMoveTouch.active && stickRadius > 0.f) { + const auto delta = clamped_stick_delta(mMoveTouch.start, mMoveTouch.current, stickRadius); + const float stickX = stick_value(delta.x / stickRadius); + const float stickY = stick_value(-delta.y / stickRadius); + switch (stick_output_mode()) { + case StickOutput::CStick: + status.substickX = stickX; + status.substickY = stickY; + break; + case StickOutput::MainStick: + default: + status.stickX = stickX; + status.stickY = stickY; + break; + } + } + + mWantsVirtualPad = status.button != 0 || status.stickX != 0 || status.stickY != 0 || + status.substickX != 0 || status.substickY != 0 || status.triggerLeft != 0 || + status.triggerRight != 0; + if (mWantsVirtualPad) { + PADSetVirtualStatus(kPort, &status); + } else { + PADClearVirtualStatus(kPort); + } + + for (const auto& control : kControls) { + if (control.tapAction) { + const bool queued = mQueuedActions.test(static_cast(*control.tapAction)); + setVirtualActionBind( + *control.tapAction, kPort, queued, queued && visible() && !mWasSuppressed); + } + if (control.holdAction) { + const bool queued = mQueuedActions.test(static_cast(*control.holdAction)); + setVirtualActionBind( + *control.holdAction, kPort, queued, queued && visible() && !mWasSuppressed); + } + } + mQueuedActions.reset(); +} + +void TouchControls::sync_visibility() noexcept { + mWasSuppressed = any_document_visible(); + if ((getSettings().game.enableTouchControls || + (menu_pointer::enabled() && menu_pointer::active())) && + !mWasSuppressed) + { + show(); + } else if (visible()) { + hide(false); + } else { + clear_virtual_input(); + } +} + +void TouchControls::sync_safe_area() noexcept { + if (mDocument == nullptr) { + return; + } + const auto insets = safe_area_insets(mDocument->GetContext()); + if (insets == mSafeInsets) { + return; + } + mSafeInsets = insets; +} + +void TouchControls::sync_control_layouts() noexcept { + auto* context = mDocument != nullptr ? mDocument->GetContext() : aurora::rmlui::get_context(); + const auto docSize = touch_document_size_dp(context); + if (docSize.w <= 0.f || docSize.h <= 0.f || context == nullptr) { + return; + } + + const auto& customControls = getSettings().game.touchControlsLayout.getValue().controls; + for (const auto& info : touch_layout_controls()) { + auto props = info.props; + if (const auto iter = customControls.find(info.layoutId); iter != customControls.end()) { + props = iter->second; + } + + const auto layout = resolve_control_layout(props, docSize); + if (info.hasControl) { + const auto index = static_cast(info.control); + if (index >= mControlElements.size()) { + continue; + } + + auto& elements = mControlElements[index]; + auto& state = elements.layout; + state.visualRect = layout.visual; + state.layoutScale = layout.scale; + apply_control_box_if_changed(elements.root, state.appliedBox, layout.box); + apply_control_dock_classes( + elements.root, touch_control_dock_anchor(layout.visual, docSize)); + apply_control_transform(info.control); + continue; + } + + mActionBarLayout.visualRect = layout.visual; + mActionBarLayout.layoutScale = layout.scale; + apply_control_box_if_changed(mActionBar, mActionBarLayout.appliedBox, layout.box); + apply_control_dock_classes(mActionBar, touch_control_dock_anchor(layout.visual, docSize)); + apply_control_transform_if_changed( + mActionBar, mActionBarLayout.appliedTransform, mActionBarLayout.layoutScale); + } +} + +void TouchControls::sync_visual_state() noexcept { + if (mWasSuppressed || !getSettings().game.enableTouchControls) { + clear_motion_touch_input(); + for (const auto control : {Control::L, Control::R}) { + const auto& elements = mControlElements[static_cast(control)]; + if (elements.root != nullptr) { + elements.root->SetPseudoClass("hidden", true); + } + release_control(control); + } + return; + } + + const bool hideGameplayControls = game_controls_suppressed(); + const auto& lTrigger = mControlElements[static_cast(Control::L)]; + const auto& rTrigger = mControlElements[static_cast(Control::R)]; + const bool lHidden = hideGameplayControls && !control_override_active(Control::L); + const bool rHidden = hideGameplayControls && !control_override_active(Control::R); + + if (lTrigger.root != nullptr) { + lTrigger.root->SetPseudoClass("hidden", lHidden); + lTrigger.root->SetClass("active", + !lHidden && (mLPressed || mLLatched || mManualLLatched || + (!control_override_active(Control::L) && player_attention_locked()))); + } + if (rTrigger.root != nullptr) { + rTrigger.root->SetPseudoClass("hidden", rHidden); + } + + if (lHidden) { + release_control(Control::L); + } + if (rHidden) { + release_control(Control::R); + } +} + +void TouchControls::sync_action_bar_state() noexcept { + if (mWasSuppressed || !getSettings().game.enableTouchControls) { + if (mActionBar != nullptr) { + mActionBar->SetPseudoClass("hidden", true); + } + const auto& skip = mControlElements[static_cast(Control::SKIP)]; + if (skip.root != nullptr) { + skip.root->SetPseudoClass("hidden", true); + } + for (const auto control : {Control::FIRST_PERSON, Control::ITEMS, Control::COLLECTIONS, + Control::MAP, Control::SKIP}) + { + release_control(control); + } + return; + } + + auto* event = dComIfGp_getEvent(); + const bool skipVisible = event != nullptr && event->mEventStatus == 1 && + event->mSkipFunc != nullptr && !event->chkFlag2(2); + const bool hidden = + !skipVisible && + (!controls_available(false) || dComIfGp_event_runCheck() || + (dComIfGp_getMsgObjectClass() != nullptr && dMsgObject_isTalkNowCheck())); + const auto& skip = mControlElements[static_cast(Control::SKIP)]; + if (mActionBar != nullptr) { + mActionBar->SetPseudoClass("hidden", hidden || skipVisible); + } + if (skip.root != nullptr) { + skip.root->SetPseudoClass("hidden", !skipVisible); + } + if (skipVisible) { + for (const auto control : + {Control::FIRST_PERSON, Control::ITEMS, Control::COLLECTIONS, Control::MAP}) + { + release_control(control); + } + return; + } + + release_control(Control::SKIP); + if (!hidden) { + return; + } + + for (const auto control : + {Control::FIRST_PERSON, Control::ITEMS, Control::COLLECTIONS, Control::MAP}) + { + release_control(control); + } +} + +void TouchControls::sync_control_displays() noexcept { + if (mWasSuppressed || !getSettings().game.enableTouchControls) { + for (const auto control : {Control::A, Control::B, Control::X, Control::Y, Control::Z}) { + const auto& elements = mControlElements[static_cast(control)]; + if (elements.root != nullptr) { + elements.root->SetPseudoClass("hidden", true); + } + release_control(control); + } + clear_equip_targets(); + return; + } + + const auto bState = b_button_state(); + const auto xState = xy_button_state(Control::X); + const auto yState = xy_button_state(Control::Y); + const auto zState = z_button_state(); + + const auto& a = mControlElements[static_cast(Control::A)]; + const auto& b = mControlElements[static_cast(Control::B)]; + const auto& x = mControlElements[static_cast(Control::X)]; + const auto& y = mControlElements[static_cast(Control::Y)]; + const auto& z = mControlElements[static_cast(Control::Z)]; + + if (a.root != nullptr) { + a.root->SetPseudoClass("hidden", false); + } + if (z.root != nullptr) { + z.root->SetPseudoClass("hidden", !zState.visible); + z.root->SetClass("has-icon", zState.showIcon); + } + if (!zState.visible) { + release_control(Control::Z); + } + if (z.icon != nullptr) { + z.icon->SetClass("visible", zState.showIcon); + } + + const bool zSourceChanged = zState.iconSource != mZTriggerIconSource; + const bool zRevisionChanged = zState.iconRevision != mZTriggerIconRevision; + if (zSourceChanged || zRevisionChanged) { + const std::string previousSource = mZTriggerIconSource; + mZTriggerIconSource = zState.iconSource; + mZTriggerIconRevision = zState.iconRevision; + if (z.icon == nullptr) { + release_rml_texture(previousSource); + } else if (zState.iconSource.empty()) { + z.icon->RemoveAttribute("src"); + } else { + release_rml_texture(zState.iconSource); + if (!zSourceChanged) { + z.icon->RemoveAttribute("src"); + } + z.icon->SetAttribute("src", zState.iconSource); + } + if (zSourceChanged) { + release_rml_texture(previousSource); + } + } + + const auto syncIcon = [this](Rml::Element* button, Rml::Element* icon, std::string& lastSource, + Control control, const FaceButtonState& state) { + if (button != nullptr) { + button->SetPseudoClass("hidden", !state.visible); + button->SetClass("has-item", state.showIcon); + } + if (!state.visible) { + release_control(control); + } + + if (icon != nullptr) { + icon->SetClass("visible", state.showIcon); + } + + if (state.iconSource == lastSource) { + return; + } + + const std::string previousSource = lastSource; + lastSource = state.iconSource; + if (icon == nullptr) { + release_rml_texture(previousSource); + return; + } + if (state.iconSource.empty()) { + icon->RemoveAttribute("src"); + } else { + icon->SetAttribute("src", state.iconSource); + } + release_rml_texture(previousSource); + }; + + syncIcon(b.root, b.icon, mButtonBIconSource, Control::B, bState); + syncIcon(x.root, x.icon, mButtonXIconSource, Control::X, xState); + syncIcon(y.root, y.icon, mButtonYIconSource, Control::Y, yState); + + const auto syncCount = [](Rml::Element* countElement, std::string& lastLabel, Control control, + const FaceButtonState& state) { + const std::string label = state.showIcon ? item_count_label_for_button(control) : ""; + if (label == lastLabel) { + return; + } + + lastLabel = label; + if (countElement == nullptr) { + return; + } + countElement->SetClass("visible", !label.empty()); + countElement->SetInnerRML(label); + }; + + syncCount(x.count, mButtonXCountLabel, Control::X, xState); + syncCount(y.count, mButtonYCountLabel, Control::Y, yState); + + const auto syncOil = [](Rml::Element* meter, Rml::Element* fill, Control control, + const FaceButtonState& state) { + const auto oilFill = state.showIcon ? item_oil_fill_for_button(control) : std::nullopt; + if (meter != nullptr) { + meter->SetClass("visible", oilFill.has_value()); + } + if (fill != nullptr) { + const float percent = oilFill ? *oilFill * 100.f : 0.f; + fill->SetProperty(Rml::PropertyId::Width, Rml::Property(percent, Rml::Unit::PERCENT)); + } + }; + + syncOil(b.oil, b.oilFill, Control::B, bState); + syncOil(x.oil, x.oilFill, Control::X, xState); + syncOil(y.oil, y.oilFill, Control::Y, yState); + + clear_equip_targets(); + if (!visible() || mWasSuppressed || !getSettings().game.enableTouchControls) { + return; + } + + if (xState.showIcon && x.layout.visualRect) { + sync_equip_target(0, *x.layout.visualRect, kFaceIconTargetRatio, kFaceIconTargetRatio); + } + if (yState.showIcon && y.layout.visualRect) { + sync_equip_target(1, *y.layout.visualRect, kFaceIconTargetRatio, kFaceIconTargetRatio); + } + if (zState.showIcon && z.layout.visualRect) { + sync_equip_target(2, *z.layout.visualRect, 1.f, 1.f, true); + } + if (bState.showIcon && b.layout.visualRect) { + sync_equip_target(3, *b.layout.visualRect, kFaceIconTargetRatio, kFaceIconTargetRatio); + } +} + +void TouchControls::update() { + sync_visibility(); + sync_control_long_presses(); + sync_safe_area(); + sync_control_layouts(); + sync_visual_state(); + sync_action_bar_state(); + sync_control_displays(); + sync_touch_state(); +} + +bool TouchControls::release_control_touch(SDL_FingerID id, bool cancelled) noexcept { + for (std::size_t i = 0; i < mControlTouches.size(); ++i) { + auto& touch = mControlTouches[i]; + if (!touch.active || touch.id != id) { + continue; + } + + const auto control = static_cast(i); + const bool shouldFireTapAction = !cancelled && !touch.longPressFired; + touch = {}; + set_control_pressed(control, false); + if (shouldFireTapAction) { + fire_control_action(control, ControlAction::Tap); + } + return true; + } + + return false; +} + +void TouchControls::sync_control_long_presses() noexcept { + const auto now = clock::now(); + for (std::size_t i = 0; i < mControlTouches.size(); ++i) { + auto& touch = mControlTouches[i]; + if (!touch.active || touch.longPressFired || now - touch.startTime < kHoldActionDuration) { + continue; + } + + if (!fire_control_action(static_cast(i), ControlAction::Hold)) { + continue; + } + + touch.longPressFired = true; + } +} + +bool TouchControls::handle_menu_event(Rml::Event& event, menu_pointer::Phase phase) noexcept { + if (!menu_pointer::active() || event.GetTargetElement() != mRoot) { + return false; + } + if (!menu_pointer::enabled()) { + mMenuPointerTouch = 0; + mMenuPointerMouseSuppressions = 0; + mMenuPointerTouchActive = false; + event.StopPropagation(); + return true; + } + + const auto id = touch_event_id(event); + switch (phase) { + case menu_pointer::Phase::Press: + if (mMenuPointerTouchActive) { + event.StopPropagation(); + return true; + } + mMenuPointerTouch = id; + mMenuPointerTouchActive = true; + break; + case menu_pointer::Phase::Move: + if (!mMenuPointerTouchActive || mMenuPointerTouch != id) { + event.StopPropagation(); + return true; + } + break; + case menu_pointer::Phase::Release: + case menu_pointer::Phase::Cancel: + if (!mMenuPointerTouchActive || mMenuPointerTouch != id) { + event.StopPropagation(); + return true; + } + mMenuPointerTouchActive = false; + break; + } + + const auto position = touch_event_position(event); + menu_pointer::handle_fallthrough_pointer(position.x, position.y, phase, true); + switch (phase) { + case menu_pointer::Phase::Press: + case menu_pointer::Phase::Release: + mMenuPointerMouseSuppressions = 2; + break; + case menu_pointer::Phase::Move: + case menu_pointer::Phase::Cancel: + mMenuPointerMouseSuppressions = 1; + break; + } + event.StopPropagation(); + return true; +} + +void TouchControls::handle_touch_down(Rml::Event& event) noexcept { + if (!visible() || mWasSuppressed) { + return; + } + if (handle_menu_event(event, menu_pointer::Phase::Press)) { + return; + } + if (!getSettings().game.enableTouchControls) { + return; + } + + const auto position = touch_event_position(event); + auto* context = aurora::rmlui::get_context(); + if (context == nullptr) { + return; + } + + const auto id = touch_event_id(event); + const auto dimensions = context->GetDimensions(); + const float top = mSafeInsets.top + kAnalogZoneTopDp * touch_dp_scale(); + const float bottom = static_cast(dimensions.y) - mSafeInsets.bottom - + kAnalogZoneBottomDp * touch_dp_scale(); + const auto width = static_cast(dimensions.x); + const bool inAnalogZone = position.y >= top && position.y <= bottom; + const bool inLeftZone = position.x < width * kLeftZoneWidth; + if (dCamera_c::isAimActive()) { + if (hawkeye_active() && inAnalogZone && inLeftZone) { + if (!mMoveTouch.active) { + mMoveTouch = { + .id = id, + .start = position, + .current = position, + .active = true, + }; + } + return; + } + + if (!mCameraTouch.active) { + mCameraTouch = { + .id = id, + .start = position, + .current = position, + .active = true, + }; + } + return; + } + + if (!inAnalogZone) { + return; + } + + if (!mMoveTouch.active && inLeftZone) { + mMoveTouch = { + .id = id, + .start = position, + .current = position, + .active = true, + }; + } else if (!mCameraTouch.active && position.x > width * kRightZoneStart) { + mCameraTouch = { + .id = id, + .start = position, + .current = position, + .active = true, + }; + } +} + +void TouchControls::handle_touch_motion(Rml::Event& event) noexcept { + if (!visible() || mWasSuppressed) { + return; + } + if (handle_menu_event(event, menu_pointer::Phase::Move)) { + return; + } + if (!getSettings().game.enableTouchControls) { + return; + } + + const auto id = touch_event_id(event); + const auto position = touch_event_position(event); + if (mMoveTouch.active && mMoveTouch.id == id) { + mMoveTouch.current = position; + } + if (mCameraTouch.active && mCameraTouch.id == id) { + const auto delta = position - mCameraTouch.current; + mCameraTouch.current = position; + const float scale = touch_dp_scale(); + touch_camera::add_delta(delta.x / scale, delta.y / scale); + } +} + +void TouchControls::handle_touch_up(Rml::Event& event) noexcept { + if (!visible() || mWasSuppressed) { + return; + } + const auto id = touch_event_id(event); + if (release_control_touch(id, false)) { + return; + } + if (handle_menu_event(event, menu_pointer::Phase::Release)) { + return; + } + if (mMoveTouch.active && mMoveTouch.id == id) { + mMoveTouch = {}; + } + if (mCameraTouch.active && mCameraTouch.id == id) { + mCameraTouch = {}; + } +} + +void TouchControls::handle_touch_cancel(Rml::Event& event) noexcept { + if (!visible() || mWasSuppressed) { + return; + } + const auto id = touch_event_id(event); + if (release_control_touch(id, true)) { + return; + } + if (handle_menu_event(event, menu_pointer::Phase::Cancel)) { + return; + } + if (mMoveTouch.active && mMoveTouch.id == id) { + mMoveTouch = {}; + } + if (mCameraTouch.active && mCameraTouch.id == id) { + mCameraTouch = {}; + } +} + +void TouchControls::handle_mouse_move(Rml::Event& event) noexcept { + if (mMenuPointerMouseSuppressions > 0) { + --mMenuPointerMouseSuppressions; + return; + } + if (!visible() || mWasSuppressed || !menu_pointer::active() || + !menu_pointer::enabled() || event.GetTargetElement() != mRoot) + { + return; + } + + const auto position = mouse_event_position(event); + menu_pointer::handle_fallthrough_pointer( + position.x, position.y, menu_pointer::Phase::Move, false); + event.StopPropagation(); +} + +void TouchControls::handle_mouse_down(Rml::Event& event) noexcept { + if (mMenuPointerMouseSuppressions > 0) { + --mMenuPointerMouseSuppressions; + return; + } + if (!visible() || mWasSuppressed || !menu_pointer::active() || + !menu_pointer::enabled() || event.GetTargetElement() != mRoot) + { + return; + } + + const auto position = mouse_event_position(event); + const s32 button = event.GetParameter("button", -1); + if (!menu_pointer::handle_fallthrough_pointer( + position.x, position.y, menu_pointer::Phase::Press, false, button)) + { + return; + } + event.StopPropagation(); +} + +void TouchControls::handle_mouse_up(Rml::Event& event) noexcept { + if (mMenuPointerMouseSuppressions > 0) { + --mMenuPointerMouseSuppressions; + return; + } + if (!visible() || mWasSuppressed || + !menu_pointer::enabled() || + (!menu_pointer::active() && !menu_pointer::mouse_capture_active()) || + event.GetTargetElement() != mRoot) + { + return; + } + + const auto position = mouse_event_position(event); + const s32 button = event.GetParameter("button", -1); + if (!menu_pointer::handle_fallthrough_pointer( + position.x, position.y, menu_pointer::Phase::Release, false, button)) + { + return; + } + event.StopPropagation(); +} + +} // namespace dusk::ui diff --git a/src/dusk/ui/touch_controls.hpp b/src/dusk/ui/touch_controls.hpp new file mode 100644 index 0000000000..752afd58b2 --- /dev/null +++ b/src/dusk/ui/touch_controls.hpp @@ -0,0 +1,133 @@ +#pragma once + +#include "controls.hpp" +#include "document.hpp" + +#include "dusk/action_bindings.h" +#include "dusk/menu_pointer.h" +#include "dusk/ui/controls.hpp" + +#include +#include +#include +#include +#include +#include + +namespace dusk::ui { + +enum class ControlOverride { + Default, + Action, +}; + +bool get_equip_target(int slot, EquipTarget& target) noexcept; +void set_control_override(Control control, ControlOverride override) noexcept; +void sync_virtual_input() noexcept; + +class TouchControls final : public Document { +public: + TouchControls(); + ~TouchControls() override; + + void show() override; + void hide(bool close) override; + void update() override; + void sync_virtual_input() noexcept; + +private: + struct StickTouch { + SDL_FingerID id = 0; + Rml::Vector2f start; + Rml::Vector2f current; + bool active = false; + }; + struct ControlTouch { + SDL_FingerID id = 0; + clock::time_point startTime{}; + bool active = false; + bool longPressFired = false; + }; + struct LayoutState { + std::optional visualRect; + std::optional appliedBox; + float layoutScale = 1.0f; + std::optional appliedTransform; + }; + struct ControlElements { + Rml::Element* root = nullptr; + Rml::Element* icon = nullptr; + Rml::Element* oil = nullptr; + Rml::Element* oilFill = nullptr; + Rml::Element* count = nullptr; + LayoutState layout; + }; + enum class ControlAction { + Tap, + Hold, + }; + + void set_control_pressed(Control control, bool pressed); + void release_control(Control control) noexcept; + void sync_control_button_mask() noexcept; + bool fire_control_action(Control control, ControlAction action) noexcept; + bool start_control_touch(SDL_FingerID id, Control control) noexcept; + void set_control_visual(Control control, bool pressed) noexcept; + void sync_l_lock_state() noexcept; + void clear_motion_touch_input() noexcept; + void clear_control_input() noexcept; + void clear_virtual_input() noexcept; + void sync_touch_state() noexcept; + void sync_visibility() noexcept; + void sync_safe_area() noexcept; + void sync_control_layouts() noexcept; + void sync_visual_state() noexcept; + void sync_action_bar_state() noexcept; + void sync_control_displays() noexcept; + void apply_control_transform(Control control) noexcept; + void handle_touch_down(Rml::Event& event) noexcept; + void handle_touch_motion(Rml::Event& event) noexcept; + void handle_touch_up(Rml::Event& event) noexcept; + void handle_touch_cancel(Rml::Event& event) noexcept; + void handle_mouse_move(Rml::Event& event) noexcept; + void handle_mouse_down(Rml::Event& event) noexcept; + void handle_mouse_up(Rml::Event& event) noexcept; + void sync_control_long_presses() noexcept; + bool release_control_touch(SDL_FingerID id, bool cancelled) noexcept; + bool handle_menu_event(Rml::Event& event, menu_pointer::Phase phase) noexcept; + + Rml::Element* mRoot = nullptr; + Rml::Element* mControlStick = nullptr; + Rml::Element* mControlKnob = nullptr; + Rml::Element* mActionBar = nullptr; + std::array(Control::COUNT)> mControlElements{}; + std::string mButtonBIconSource; + std::string mButtonXIconSource; + std::string mButtonYIconSource; + std::string mZTriggerIconSource; + uint64_t mZTriggerIconRevision = 0; + std::string mButtonXCountLabel; + std::string mButtonYCountLabel; + StickTouch mMoveTouch; + StickTouch mCameraTouch; + SDL_FingerID mMenuPointerTouch = 0; + int mMenuPointerMouseSuppressions = 0; + std::array(Control::COUNT)> mControlTouches{}; + std::array(Control::COUNT)> mControlVisualPressed{}; + std::bitset(ActionBinds::COUNT)> mQueuedActions; + LayoutState mActionBarLayout; + Insets mSafeInsets; + u16 mButtonMask = 0; + bool mLPressed = false; + bool mLLatched = false; + bool mManualLLatched = false; + bool mLReleasePending = false; + bool mRTriggerHeld = false; + bool mWantsVirtualPad = false; + bool mWasSuppressed = true; + bool mMenuPointerTouchActive = false; + clock::time_point mLPressStartTime{}; + clock::time_point mLastLTapTime{}; +}; + +} // namespace dusk::ui diff --git a/src/dusk/ui/touch_controls_common.cpp b/src/dusk/ui/touch_controls_common.cpp new file mode 100644 index 0000000000..ef46d49335 --- /dev/null +++ b/src/dusk/ui/touch_controls_common.cpp @@ -0,0 +1,359 @@ +#include "touch_controls_common.hpp" + +#include + +#include +#include +#include + +namespace dusk::ui { +namespace { + +constexpr std::array kLayoutControls = {{ + { + .layoutId = "triggerL", + .elementId = "trigger-l", + .props = + { + .x = 24.f, + .y = 18.f, + .w = 78.f, + .h = 46.f, + .scale = 1.f, + .anchor = ControlAnchor::TopLeft, + }, + .control = Control::L, + .hasControl = true, + }, + { + .layoutId = "triggerR", + .elementId = "trigger-r", + .props = + { + .x = 24.f, + .y = 18.f, + .w = 78.f, + .h = 46.f, + .scale = 1.f, + .anchor = ControlAnchor::TopRight, + }, + .control = Control::R, + .hasControl = true, + }, + { + .layoutId = "buttonZ", + .elementId = "button-z", + .props = + { + .x = 24.f, + .y = 72.f, + .w = 78.f, + .h = 46.f, + .scale = 1.f, + .anchor = ControlAnchor::TopRight, + }, + .control = Control::Z, + .hasControl = true, + }, + { + .layoutId = "actionBar", + .elementId = "action-bar", + .props = + { + .x = 56.f, + .y = 0.f, + .w = 230.f, + .h = 46.f, + .scale = 1.f, + .anchor = ControlAnchor::BottomLeft, + }, + }, + { + .layoutId = "skip", + .elementId = "skip", + .props = + { + .x = 24.f, + .y = 18.f, + .w = 64.f, + .h = 46.f, + .scale = 1.f, + .anchor = ControlAnchor::TopRight, + }, + .control = Control::SKIP, + .hasControl = true, + }, + { + .layoutId = "buttonY", + .elementId = "button-y", + .props = + { + .x = 124.f, + .y = 138.f, + .w = 58.f, + .h = 58.f, + .scale = 1.f, + .anchor = ControlAnchor::BottomRight, + }, + .control = Control::Y, + .hasControl = true, + }, + { + .layoutId = "buttonX", + .elementId = "button-x", + .props = + { + .x = 28.f, + .y = 144.f, + .w = 58.f, + .h = 58.f, + .scale = 1.f, + .anchor = ControlAnchor::BottomRight, + }, + .control = Control::X, + .hasControl = true, + }, + { + .layoutId = "buttonB", + .elementId = "button-b", + .props = + { + .x = 158.f, + .y = 48.f, + .w = 58.f, + .h = 58.f, + .scale = 1.f, + .anchor = ControlAnchor::BottomRight, + }, + .control = Control::B, + .hasControl = true, + }, + { + .layoutId = "buttonA", + .elementId = "button-a", + .props = + { + .x = 62.f, + .y = 64.f, + .w = 74.f, + .h = 74.f, + .scale = 1.f, + .anchor = ControlAnchor::BottomRight, + }, + .control = Control::A, + .hasControl = true, + }, +}}; + +constexpr std::string_view kTouchControlsRmlFragment = R"RML( + + + + + + + + + + + + + + + + + + + +)RML"; + +} // namespace + +std::string_view touch_controls_rml_fragment() noexcept { + return kTouchControlsRmlFragment; +} + +std::span touch_layout_controls() noexcept { + return kLayoutControls; +} + +const TouchLayoutControlInfo* find_touch_layout_control(std::string_view layoutId) noexcept { + for (const auto& info : kLayoutControls) { + if (info.layoutId == layoutId) { + return &info; + } + } + return nullptr; +} + +const TouchLayoutControlInfo* find_touch_layout_control(Control control) noexcept { + for (const auto& info : kLayoutControls) { + if (info.hasControl && info.control == control) { + return &info; + } + } + return nullptr; +} + +SDL_FingerID touch_event_id(const Rml::Event& event) noexcept { + return event.GetParameter("finger_id", 0); +} + +Rml::Vector2f touch_event_position(const Rml::Event& event) noexcept { + return { + event.GetParameter("x", 0.f), + event.GetParameter("y", 0.f), + }; +} + +Rml::Vector2f mouse_event_position(const Rml::Event& event) noexcept { + return { + event.GetParameter("mouse_x", 0.f), + event.GetParameter("mouse_y", 0.f), + }; +} + +float touch_dp_scale(Rml::Context* context) noexcept { + if (context == nullptr) { + context = aurora::rmlui::get_context(); + } + if (context == nullptr) { + return 1.f; + } + return std::max(context->GetDensityIndependentPixelRatio(), 1.f); +} + +ControlLayoutSize touch_document_size_dp(Rml::Context* context) noexcept { + if (context == nullptr) { + return {}; + } + + const auto dimensions = context->GetDimensions(); + const float scale = touch_dp_scale(context); + return { + .w = static_cast(dimensions.x) / scale, + .h = static_cast(dimensions.y) / scale, + }; +} + +ControlAnchor touch_control_dock_anchor(ControlRect visual, ControlLayoutSize docSize) noexcept { + if (docSize.w <= 0.f || docSize.h <= 0.f || visual.w <= 0.f || visual.h <= 0.f) { + return ControlAnchor::None; + } + + const bool top = control_float_near(visual.t, 0.f); + const bool bottom = control_float_near(visual.t + visual.h, docSize.h); + const bool left = control_float_near(visual.l, 0.f); + const bool right = control_float_near(visual.l + visual.w, docSize.w); + + if (top && left && !right) { + return ControlAnchor::TopLeft; + } + if (top && right && !left) { + return ControlAnchor::TopRight; + } + if (bottom && left && !right) { + return ControlAnchor::BottomLeft; + } + if (bottom && right && !left) { + return ControlAnchor::BottomRight; + } + if (top) { + return ControlAnchor::Top; + } + if (bottom) { + return ControlAnchor::Bottom; + } + if (left) { + return ControlAnchor::Left; + } + if (right) { + return ControlAnchor::Right; + } + return ControlAnchor::None; +} + +bool control_float_near(float a, float b) noexcept { + return std::abs(a - b) <= 0.01f; +} + +bool control_rect_near(ControlRect a, ControlRect b) noexcept { + return control_float_near(a.l, b.l) && control_float_near(a.t, b.t) && + control_float_near(a.w, b.w) && control_float_near(a.h, b.h); +} + +void apply_control_box_if_changed( + Rml::Element* element, std::optional& appliedBox, ControlRect box) noexcept { + if (element == nullptr || (appliedBox && control_rect_near(*appliedBox, box))) { + return; + } + + element->SetProperty(Rml::PropertyId::Left, Rml::Property(box.l, Rml::Unit::DP)); + element->SetProperty(Rml::PropertyId::Top, Rml::Property(box.t, Rml::Unit::DP)); + element->SetProperty(Rml::PropertyId::Width, Rml::Property(box.w, Rml::Unit::DP)); + element->SetProperty(Rml::PropertyId::Height, Rml::Property(box.h, Rml::Unit::DP)); + appliedBox = box; +} + +void apply_control_transform_if_changed( + Rml::Element* element, std::optional& appliedTransform, float scale) noexcept { + if (element == nullptr || (appliedTransform && control_float_near(*appliedTransform, scale))) { + return; + } + + element->SetProperty(Rml::PropertyId::Transform, + Rml::Transform::MakeProperty({Rml::Transforms::Scale2D{scale}})); + appliedTransform = scale; +} + +void apply_control_dock_classes(Rml::Element* element, ControlAnchor anchor) noexcept { + if (element == nullptr) { + return; + } + + bool top = false; + bool bottom = false; + bool left = false; + bool right = false; + + switch (anchor) { + case ControlAnchor::Top: + top = true; + break; + case ControlAnchor::Bottom: + bottom = true; + break; + case ControlAnchor::Left: + left = true; + break; + case ControlAnchor::Right: + right = true; + break; + case ControlAnchor::TopLeft: + top = true; + left = true; + break; + case ControlAnchor::TopRight: + top = true; + right = true; + break; + case ControlAnchor::BottomLeft: + bottom = true; + left = true; + break; + case ControlAnchor::BottomRight: + bottom = true; + right = true; + break; + case ControlAnchor::None: + break; + } + + element->SetClass("docked", top || bottom || left || right); + element->SetClass("docked-top", top); + element->SetClass("docked-bottom", bottom); + element->SetClass("docked-left", left); + element->SetClass("docked-right", right); +} + +} // namespace dusk::ui diff --git a/src/dusk/ui/touch_controls_common.hpp b/src/dusk/ui/touch_controls_common.hpp new file mode 100644 index 0000000000..30445479ff --- /dev/null +++ b/src/dusk/ui/touch_controls_common.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "controls.hpp" + +#include +#include + +#include +#include +#include +#include + +namespace dusk::ui { + +constexpr std::size_t kTouchLayoutControlCount = 9; + +struct TouchLayoutControlInfo { + std::string_view layoutId; + const char* elementId = nullptr; + ControlProps props; + Control control = Control::COUNT; + bool hasControl = false; +}; + +std::string_view touch_controls_rml_fragment() noexcept; +std::span touch_layout_controls() noexcept; +const TouchLayoutControlInfo* find_touch_layout_control(std::string_view layoutId) noexcept; +const TouchLayoutControlInfo* find_touch_layout_control(Control control) noexcept; + +SDL_FingerID touch_event_id(const Rml::Event& event) noexcept; +Rml::Vector2f touch_event_position(const Rml::Event& event) noexcept; +Rml::Vector2f mouse_event_position(const Rml::Event& event) noexcept; +float touch_dp_scale(Rml::Context* context = nullptr) noexcept; +ControlLayoutSize touch_document_size_dp(Rml::Context* context) noexcept; +ControlAnchor touch_control_dock_anchor(ControlRect visual, ControlLayoutSize docSize) noexcept; + +bool control_float_near(float a, float b) noexcept; +bool control_rect_near(ControlRect a, ControlRect b) noexcept; +void apply_control_box_if_changed( + Rml::Element* element, std::optional& appliedBox, ControlRect box) noexcept; +void apply_control_transform_if_changed( + Rml::Element* element, std::optional& appliedTransform, float scale) noexcept; +void apply_control_dock_classes(Rml::Element* element, ControlAnchor anchor) noexcept; + +} // namespace dusk::ui diff --git a/src/dusk/ui/touch_controls_editor.cpp b/src/dusk/ui/touch_controls_editor.cpp new file mode 100644 index 0000000000..539494d42f --- /dev/null +++ b/src/dusk/ui/touch_controls_editor.cpp @@ -0,0 +1,630 @@ +#include "touch_controls_editor.hpp" + +#include "modal.hpp" + +#include "Z2AudioLib/Z2SeMgr.h" +#include "dusk/config.hpp" +#include "dusk/settings.h" +#include "m_Do/m_Do_audio.h" + +#include + +#include +#include +#include +#include + +namespace dusk::ui { +namespace { + +constexpr float kDragThresholdDp = 6.f; +constexpr float kMinControlDp = 36.f; +constexpr float kMinTriggerWidthDp = 44.f; +constexpr float kMinTriggerHeightDp = 32.f; +constexpr float kMinActionBarWidthDp = 112.f; +constexpr float kMinActionBarHeightDp = 36.f; +constexpr float kMinScale = 0.25f; + +struct HandleBinding { + const char* id = nullptr; + TouchControlsEditor::EditHandle handle = TouchControlsEditor::EditHandle::Move; +}; + +constexpr std::array kHandleBindings = { + HandleBinding{"editor-handle-left", TouchControlsEditor::EditHandle::Left}, + HandleBinding{"editor-handle-right", TouchControlsEditor::EditHandle::Right}, + HandleBinding{"editor-handle-top", TouchControlsEditor::EditHandle::Top}, + HandleBinding{"editor-handle-bottom", TouchControlsEditor::EditHandle::Bottom}, + HandleBinding{"editor-handle-top-left", TouchControlsEditor::EditHandle::TopLeft}, + HandleBinding{"editor-handle-top-right", TouchControlsEditor::EditHandle::TopRight}, + HandleBinding{"editor-handle-bottom-left", TouchControlsEditor::EditHandle::BottomLeft}, + HandleBinding{"editor-handle-bottom-right", TouchControlsEditor::EditHandle::BottomRight}, +}; + +Rml::String touch_controls_editor_document_source() { + const auto fragment = touch_controls_rml_fragment(); + return Rml::String{R"RML( + + + + + + +)RML"} + Rml::String{fragment.data(), fragment.size()} + Rml::String{R"RML( + + + + + + + + + + + + + + + + + +)RML"}; +} + +bool is_corner(TouchControlsEditor::EditHandle handle) noexcept { + using EditHandle = TouchControlsEditor::EditHandle; + return handle == EditHandle::TopLeft || handle == EditHandle::TopRight || + handle == EditHandle::BottomLeft || handle == EditHandle::BottomRight; +} + +bool is_horizontal_edge(TouchControlsEditor::EditHandle handle) noexcept { + using EditHandle = TouchControlsEditor::EditHandle; + return handle == EditHandle::Left || handle == EditHandle::Right; +} + +bool is_vertical_edge(TouchControlsEditor::EditHandle handle) noexcept { + using EditHandle = TouchControlsEditor::EditHandle; + return handle == EditHandle::Top || handle == EditHandle::Bottom; +} + +bool control_valid(std::size_t index) noexcept { + return index < touch_layout_controls().size(); +} + +float squared_distance(Rml::Vector2f a, Rml::Vector2f b) noexcept { + const auto delta = a - b; + return delta.x * delta.x + delta.y * delta.y; +} + +} // namespace + +TouchControlsEditor::TouchControlsEditor() + : Document(touch_controls_editor_document_source()), + mRoot(mDocument != nullptr ? mDocument->GetElementById("root") : nullptr), + mSelectionFrame( + mDocument != nullptr ? mDocument->GetElementById("editor-selection-frame") : nullptr), + mSaveButton(mDocument != nullptr ? mDocument->GetElementById("editor-save") : nullptr), + mResetButton(mDocument != nullptr ? mDocument->GetElementById("editor-reset") : nullptr), + mCancelButton(mDocument != nullptr ? mDocument->GetElementById("editor-cancel") : nullptr), + mWorkingLayout(getSettings().game.touchControlsLayout.getValue()) { + mWorkingLayout.version = ControlLayout::Version; + + const auto controls = touch_layout_controls(); + for (std::size_t i = 0; i < controls.size() && i < mElements.size(); ++i) { + mElements[i].root = + mDocument != nullptr ? mDocument->GetElementById(controls[i].elementId) : nullptr; + } + + bind_control_events(); + bind_handle_events(); + bind_toolbar_events(); + + listen(mRoot, aurora::rmlui::TouchStartEvent, [this](Rml::Event& event) { + if (event.GetTargetElement() != mRoot) { + return; + } + clear_selected_control(); + event.StopPropagation(); + }); + listen(mRoot, Rml::EventId::Mousedown, [this](Rml::Event& event) { + const s32 button = event.GetParameter("button", -1); + if (button != 0 || event.GetTargetElement() != mRoot) { + return; + } + clear_selected_control(); + event.StopPropagation(); + }); + listen(mRoot, aurora::rmlui::TouchMoveEvent, [this](Rml::Event& event) { + if (continue_edit(touch_event_position(event))) { + event.StopPropagation(); + } + }); + listen(mRoot, aurora::rmlui::TouchEndEvent, [this](Rml::Event& event) { + if (end_edit(true, touch_event_id(event), false)) { + event.StopPropagation(); + } + }); + listen(mRoot, aurora::rmlui::TouchCancelEvent, [this](Rml::Event& event) { + if (end_edit(true, touch_event_id(event), true)) { + event.StopPropagation(); + } + }); + listen(mRoot, Rml::EventId::Mousemove, [this](Rml::Event& event) { + if (continue_edit(mouse_event_position(event))) { + event.StopPropagation(); + } + }); + listen(mRoot, Rml::EventId::Mouseup, [this](Rml::Event& event) { + if (end_edit(false, 0, false)) { + event.StopPropagation(); + } + }); + listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) { + if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") && + Document::visible()) + { + Document::hide(mPendingClose); + } + }); +} + +void TouchControlsEditor::show() { + Document::show(); + if (mRoot != nullptr) { + mRoot->SetAttribute("open", ""); + } +} + +void TouchControlsEditor::hide(bool close) { + if (mRoot != nullptr) { + mRoot->RemoveAttribute("open"); + mPendingClose = close; + } else { + Document::hide(close); + } +} + +void TouchControlsEditor::update() { + sync_control_layouts(); + sync_selection_frame(); + Document::update(); +} + +bool TouchControlsEditor::focus() { + return mSaveButton != nullptr && mSaveButton->Focus(true); +} + +void TouchControlsEditor::bind_control_events() noexcept { + const auto controls = touch_layout_controls(); + for (std::size_t i = 0; i < controls.size() && i < mElements.size(); ++i) { + auto* element = mElements[i].root; + if (element == nullptr) { + continue; + } + + listen(element, aurora::rmlui::TouchStartEvent, [this, i](Rml::Event& event) { + if (begin_edit(i, EditHandle::Move, touch_event_position(event), true, + touch_event_id(event))) + { + event.StopPropagation(); + } + }); + listen(element, Rml::EventId::Mousedown, [this, i](Rml::Event& event) { + const s32 button = event.GetParameter("button", -1); + if (button != 0) { + return; + } + if (begin_edit(i, EditHandle::Move, mouse_event_position(event), false)) { + event.StopPropagation(); + } + }); + } +} + +void TouchControlsEditor::bind_handle_events() noexcept { + for (const auto& binding : kHandleBindings) { + auto* element = mDocument != nullptr ? mDocument->GetElementById(binding.id) : nullptr; + if (element == nullptr) { + continue; + } + + listen(element, aurora::rmlui::TouchStartEvent, [this, handle = binding.handle]( + Rml::Event& event) { + if (!control_valid(mSelectedIndex)) { + return; + } + if (begin_edit(mSelectedIndex, handle, touch_event_position(event), true, + touch_event_id(event))) + { + event.StopPropagation(); + } + }); + listen(element, Rml::EventId::Mousedown, [this, handle = binding.handle](Rml::Event& event) { + const s32 button = event.GetParameter("button", -1); + if (button != 0 || !control_valid(mSelectedIndex)) { + return; + } + if (begin_edit(mSelectedIndex, handle, mouse_event_position(event), false)) { + event.StopPropagation(); + } + }); + } +} + +void TouchControlsEditor::bind_toolbar_events() noexcept { + bind_button_command(mSaveButton, &TouchControlsEditor::save_layout); + bind_button_command(mResetButton, &TouchControlsEditor::request_reset); + bind_button_command(mCancelButton, &TouchControlsEditor::cancel_edit); +} + +void TouchControlsEditor::bind_button_command( + Rml::Element* element, void (TouchControlsEditor::*callback)()) noexcept { + if (element == nullptr) { + return; + } + + listen(element, Rml::EventId::Click, [this, callback](Rml::Event& event) { + (this->*callback)(); + event.StopPropagation(); + }); + listen(element, Rml::EventId::Keydown, [this, callback](Rml::Event& event) { + if (map_nav_event(event) != NavCommand::Confirm) { + return; + } + (this->*callback)(); + event.StopPropagation(); + }); +} + +void TouchControlsEditor::sync_control_layouts() noexcept { + auto* context = mDocument != nullptr ? mDocument->GetContext() : nullptr; + const auto docSize = touch_document_size_dp(context); + if (docSize.w <= 0.f || docSize.h <= 0.f || context == nullptr) { + return; + } + + const auto controls = touch_layout_controls(); + for (std::size_t i = 0; i < controls.size() && i < mElements.size(); ++i) { + const auto layout = resolve_control_layout(props_for(i), docSize); + auto& element = mElements[i]; + element.layout.visualRect = layout.visual; + element.layout.layoutScale = layout.scale; + if (element.root != nullptr) { + element.root->SetPseudoClass("hidden", false); + } + apply_control_box_if_changed(element.root, element.layout.appliedBox, layout.box); + apply_control_dock_classes( + element.root, touch_control_dock_anchor(layout.visual, docSize)); + apply_control_transform_if_changed( + element.root, element.layout.appliedTransform, element.layout.layoutScale); + } +} + +void TouchControlsEditor::sync_selection_frame() noexcept { + const bool hasSelection = + control_valid(mSelectedIndex) && mElements[mSelectedIndex].layout.visualRect; + if (mSelectionFrame == nullptr) { + return; + } + + mSelectionFrame->SetClass("visible", hasSelection); + for (std::size_t i = 0; i < mElements.size(); ++i) { + if (mElements[i].root != nullptr) { + mElements[i].root->SetClass("editor-selected", hasSelection && i == mSelectedIndex); + } + } + if (!hasSelection) { + mAppliedSelectionFrame = std::nullopt; + return; + } + + apply_control_box_if_changed( + mSelectionFrame, mAppliedSelectionFrame, *mElements[mSelectedIndex].layout.visualRect); +} + +void TouchControlsEditor::set_selected_control(std::size_t index) noexcept { + if (!control_valid(index)) { + clear_selected_control(); + return; + } + mSelectedIndex = index; + sync_selection_frame(); +} + +void TouchControlsEditor::clear_selected_control() noexcept { + mSelectedIndex = kTouchLayoutControlCount; + sync_selection_frame(); +} + +ControlProps TouchControlsEditor::props_for(std::size_t index) const { + const auto controls = touch_layout_controls(); + if (!control_valid(index)) { + return {}; + } + + const auto& info = controls[index]; + if (const auto iter = mWorkingLayout.controls.find(info.layoutId); + iter != mWorkingLayout.controls.end()) + { + return iter->second; + } + return info.props; +} + +void TouchControlsEditor::store_props( + std::size_t index, ControlRect visual, ControlProps props) noexcept { + if (!control_valid(index)) { + return; + } + + auto* context = mDocument != nullptr ? mDocument->GetContext() : nullptr; + const auto docSize = touch_document_size_dp(context); + if (docSize.w <= 0.f || docSize.h <= 0.f) { + return; + } + + props.w = std::max(props.w, 1.f); + props.h = std::max(props.h, 1.f); + props.scale = std::max(props.scale, kMinScale); + props = encode_control_props(visual, docSize, props, touch_control_dock_anchor(visual, docSize)); + mWorkingLayout.version = ControlLayout::Version; + mWorkingLayout.controls[std::string{touch_layout_controls()[index].layoutId}] = props; + sync_control_layouts(); + sync_selection_frame(); +} + +void TouchControlsEditor::restore_active_control() noexcept { + if (!control_valid(mPointerEdit.index)) { + return; + } + + auto& controls = mWorkingLayout.controls; + const auto key = std::string{touch_layout_controls()[mPointerEdit.index].layoutId}; + if (mPointerEdit.storedProps) { + controls[key] = *mPointerEdit.storedProps; + } else { + controls.erase(key); + } + sync_control_layouts(); + sync_selection_frame(); +} + +bool TouchControlsEditor::begin_edit( + std::size_t index, EditHandle handle, Rml::Vector2f positionPx, bool touch, + SDL_FingerID touchId) noexcept { + if (!control_valid(index) || mPointerEdit.active) { + return false; + } + + auto* context = mDocument != nullptr ? mDocument->GetContext() : nullptr; + const auto docSize = touch_document_size_dp(context); + if (docSize.w <= 0.f || docSize.h <= 0.f) { + return false; + } + + const auto props = props_for(index); + const auto layout = resolve_control_layout(props, docSize); + std::optional storedProps; + if (const auto iter = mWorkingLayout.controls.find(touch_layout_controls()[index].layoutId); + iter != mWorkingLayout.controls.end()) + { + storedProps = iter->second; + } + + mPointerEdit = { + .index = index, + .touchId = touchId, + .startPointerDp = pointer_position_dp(positionPx), + .startVisual = layout.visual, + .startProps = props, + .storedProps = storedProps, + .handle = handle, + .active = true, + .touch = touch, + }; + set_selected_control(index); + return true; +} + +bool TouchControlsEditor::continue_edit(Rml::Vector2f positionPx) noexcept { + if (!mPointerEdit.active) { + return false; + } + + const auto pointerDp = pointer_position_dp(positionPx); + if (!mPointerEdit.dragging) { + if (squared_distance(pointerDp, mPointerEdit.startPointerDp) < + kDragThresholdDp * kDragThresholdDp) + { + return true; + } + mPointerEdit.dragging = true; + } + + auto props = mPointerEdit.startProps; + auto rect = rect_for_edit(pointerDp, props); + rect = clamp_visual_rect(mPointerEdit.index, rect); + if (is_corner(mPointerEdit.handle)) { + props.scale = std::max(rect.w / std::max(mPointerEdit.startProps.w, 1.f), kMinScale); + } else if (is_horizontal_edge(mPointerEdit.handle)) { + props.w = rect.w / std::max(props.scale, kMinScale); + } else if (is_vertical_edge(mPointerEdit.handle)) { + props.h = rect.h / std::max(props.scale, kMinScale); + } + store_props(mPointerEdit.index, rect, props); + return true; +} + +bool TouchControlsEditor::end_edit(bool touch, SDL_FingerID touchId, bool cancelled) noexcept { + if (!mPointerEdit.active || mPointerEdit.touch != touch || + (touch && mPointerEdit.touchId != touchId)) + { + return false; + } + + if (cancelled && mPointerEdit.dragging) { + restore_active_control(); + } + mPointerEdit = {}; + return true; +} + +Rml::Vector2f TouchControlsEditor::pointer_position_dp(Rml::Vector2f positionPx) const noexcept { + auto* context = mDocument != nullptr ? mDocument->GetContext() : nullptr; + return positionPx / touch_dp_scale(context); +} + +ControlRect TouchControlsEditor::rect_for_edit( + Rml::Vector2f pointerDp, ControlProps& props) const noexcept { + const auto& edit = mPointerEdit; + auto rect = edit.startVisual; + const auto delta = pointerDp - edit.startPointerDp; + + switch (edit.handle) { + case EditHandle::Move: + rect.l += delta.x; + rect.t += delta.y; + return rect; + case EditHandle::Left: { + const float right = edit.startVisual.l + edit.startVisual.w; + rect.l = pointerDp.x; + rect.w = right - rect.l; + return rect; + } + case EditHandle::Right: + rect.w = pointerDp.x - edit.startVisual.l; + return rect; + case EditHandle::Top: { + const float bottom = edit.startVisual.t + edit.startVisual.h; + rect.t = pointerDp.y; + rect.h = bottom - rect.t; + return rect; + } + case EditHandle::Bottom: + rect.h = pointerDp.y - edit.startVisual.t; + return rect; + case EditHandle::TopLeft: + case EditHandle::TopRight: + case EditHandle::BottomLeft: + case EditHandle::BottomRight: + break; + } + + auto* context = mDocument != nullptr ? mDocument->GetContext() : nullptr; + const auto docSize = touch_document_size_dp(context); + const bool left = edit.handle == EditHandle::TopLeft || edit.handle == EditHandle::BottomLeft; + const bool top = edit.handle == EditHandle::TopLeft || edit.handle == EditHandle::TopRight; + const Rml::Vector2f fixed{ + left ? edit.startVisual.l + edit.startVisual.w : edit.startVisual.l, + top ? edit.startVisual.t + edit.startVisual.h : edit.startVisual.t, + }; + const float desiredW = left ? fixed.x - pointerDp.x : pointerDp.x - fixed.x; + const float desiredH = top ? fixed.y - pointerDp.y : pointerDp.y - fixed.y; + const auto minSize = min_visual_size(edit.index); + const float minRatio = + std::max(minSize.x / std::max(edit.startVisual.w, 1.f), + minSize.y / std::max(edit.startVisual.h, 1.f)); + const float maxW = left ? fixed.x : docSize.w - fixed.x; + const float maxH = top ? fixed.y : docSize.h - fixed.y; + const float maxRatio = + std::max(minRatio, std::min(maxW / std::max(edit.startVisual.w, 1.f), + maxH / std::max(edit.startVisual.h, 1.f))); + const float ratio = + std::clamp(std::max(desiredW / std::max(edit.startVisual.w, 1.f), + desiredH / std::max(edit.startVisual.h, 1.f)), + minRatio, maxRatio); + + rect.w = edit.startVisual.w * ratio; + rect.h = edit.startVisual.h * ratio; + rect.l = left ? fixed.x - rect.w : fixed.x; + rect.t = top ? fixed.y - rect.h : fixed.y; + props.scale = std::max(edit.startProps.scale * ratio, kMinScale); + return rect; +} + +ControlRect TouchControlsEditor::clamp_visual_rect(std::size_t index, ControlRect rect) const noexcept { + auto* context = mDocument != nullptr ? mDocument->GetContext() : nullptr; + const auto docSize = touch_document_size_dp(context); + if (docSize.w <= 0.f || docSize.h <= 0.f || !control_valid(index)) { + return rect; + } + + const auto minSize = min_visual_size(index); + const float minW = std::min(minSize.x, docSize.w); + const float minH = std::min(minSize.y, docSize.h); + rect.w = std::clamp(rect.w, minW, docSize.w); + rect.h = std::clamp(rect.h, minH, docSize.h); + rect.l = std::clamp(rect.l, 0.f, std::max(0.f, docSize.w - rect.w)); + rect.t = std::clamp(rect.t, 0.f, std::max(0.f, docSize.h - rect.h)); + return rect; +} + +Rml::Vector2f TouchControlsEditor::min_visual_size(std::size_t index) const noexcept { + if (!control_valid(index)) { + return {kMinControlDp, kMinControlDp}; + } + + const auto id = touch_layout_controls()[index].layoutId; + if (id == "actionBar") { + return {kMinActionBarWidthDp, kMinActionBarHeightDp}; + } + if (id == "triggerL" || id == "triggerR" || id == "buttonZ" || id == "skip") { + return {kMinTriggerWidthDp, kMinTriggerHeightDp}; + } + return {kMinControlDp, kMinControlDp}; +} + +bool TouchControlsEditor::handle_nav_command(Rml::Event& event, NavCommand cmd) { + if (cmd == NavCommand::Cancel || cmd == NavCommand::Menu) { + cancel_edit(); + return true; + } + return Document::handle_nav_command(event, cmd); +} + +void TouchControlsEditor::save_layout() { + mWorkingLayout.version = ControlLayout::Version; + getSettings().game.touchControlsLayout.setValue(mWorkingLayout); + config::Save(); + mDoAud_seStartMenu(kSoundItemChange); + pop(); +} + +void TouchControlsEditor::request_reset() { + auto dismiss = [](Modal& modal) { modal.pop(); }; + push(std::make_unique(Modal::Props{ + .title = "Reset Touch Layout?", + .bodyRml = "Reset controls to their default layout. This will not be saved until you press Save.", + .actions = + { + ModalAction{ + .label = "Reset", + .onPressed = + [this, dismiss](Modal& modal) { + reset_working_layout(); + mDoAud_seStartMenu(kSoundItemChange); + dismiss(modal); + }, + }, + ModalAction{ + .label = "Cancel", + .onPressed = dismiss, + }, + }, + })); +} + +void TouchControlsEditor::reset_working_layout() noexcept { + mWorkingLayout = ControlLayout{}; + mWorkingLayout.version = ControlLayout::Version; + mPointerEdit = {}; + sync_control_layouts(); + sync_selection_frame(); +} + +void TouchControlsEditor::cancel_edit() { + mDoAud_seStartMenu(kSoundWindowClose); + pop(); +} + +} // namespace dusk::ui diff --git a/src/dusk/ui/touch_controls_editor.hpp b/src/dusk/ui/touch_controls_editor.hpp new file mode 100644 index 0000000000..4af33d8fbc --- /dev/null +++ b/src/dusk/ui/touch_controls_editor.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include "controls.hpp" +#include "document.hpp" +#include "touch_controls_common.hpp" + +#include +#include +#include + +namespace dusk::ui { + +class TouchControlsEditor final : public Document { +public: + TouchControlsEditor(); + + void show() override; + void hide(bool close) override; + void update() override; + bool focus() override; + + enum class EditHandle { + Move, + Left, + Right, + Top, + Bottom, + TopLeft, + TopRight, + BottomLeft, + BottomRight, + }; + +private: + struct LayoutState { + std::optional visualRect; + std::optional appliedBox; + float layoutScale = 1.0f; + std::optional appliedTransform; + }; + + struct EditElement { + Rml::Element* root = nullptr; + LayoutState layout; + }; + + struct PointerEdit { + std::size_t index = kTouchLayoutControlCount; + SDL_FingerID touchId = 0; + Rml::Vector2f startPointerDp; + ControlRect startVisual; + ControlProps startProps; + std::optional storedProps; + EditHandle handle = EditHandle::Move; + bool active = false; + bool touch = false; + bool dragging = false; + }; + + void bind_control_events() noexcept; + void bind_handle_events() noexcept; + void bind_toolbar_events() noexcept; + void bind_button_command( + Rml::Element* element, void (TouchControlsEditor::*callback)()) noexcept; + void sync_control_layouts() noexcept; + void sync_selection_frame() noexcept; + void set_selected_control(std::size_t index) noexcept; + void clear_selected_control() noexcept; + ControlProps props_for(std::size_t index) const; + void store_props(std::size_t index, ControlRect visual, ControlProps props) noexcept; + void restore_active_control() noexcept; + bool begin_edit(std::size_t index, EditHandle handle, Rml::Vector2f positionPx, bool touch, + SDL_FingerID touchId = 0) noexcept; + bool continue_edit(Rml::Vector2f positionPx) noexcept; + bool end_edit(bool touch, SDL_FingerID touchId, bool cancelled) noexcept; + Rml::Vector2f pointer_position_dp(Rml::Vector2f positionPx) const noexcept; + ControlRect rect_for_edit(Rml::Vector2f pointerDp, ControlProps& props) const noexcept; + ControlRect clamp_visual_rect(std::size_t index, ControlRect rect) const noexcept; + Rml::Vector2f min_visual_size(std::size_t index) const noexcept; + bool handle_nav_command(Rml::Event& event, NavCommand cmd) override; + void save_layout(); + void request_reset(); + void reset_working_layout() noexcept; + void cancel_edit(); + + Rml::Element* mRoot = nullptr; + Rml::Element* mSelectionFrame = nullptr; + Rml::Element* mSaveButton = nullptr; + Rml::Element* mResetButton = nullptr; + Rml::Element* mCancelButton = nullptr; + std::array mElements{}; + ControlLayout mWorkingLayout; + PointerEdit mPointerEdit; + std::optional mAppliedSelectionFrame; + std::size_t mSelectedIndex = kTouchLayoutControlCount; +}; + +} // namespace dusk::ui diff --git a/src/dusk/ui/ui.cpp b/src/dusk/ui/ui.cpp index af05d0f476..ecff09f6aa 100644 --- a/src/dusk/ui/ui.cpp +++ b/src/dusk/ui/ui.cpp @@ -1,7 +1,11 @@ #include "ui.hpp" #include -#include +#include +#include +#include +#include +#include #include #include #include @@ -11,11 +15,12 @@ #include #include "aurora/lib/window.hpp" +#include "dusk/config.hpp" #include "dusk/io.hpp" #include "input.hpp" +#include "icon_provider.hpp" #include "prelaunch.hpp" #include "window.hpp" -#include "dusk/config.hpp" namespace dusk::ui { namespace { @@ -56,11 +61,13 @@ bool initialize() noexcept { load_font("MaterialSymbolsRounded-Regular.ttf"); load_font("NotoMono-Regular.ttf"); + register_icon_texture_provider(); sInitialized = true; return true; } void shutdown() noexcept { + unregister_icon_texture_provider(); sDocumentStack.clear(); sPassiveDocuments.clear(); sConnectedGamepads.clear(); @@ -188,9 +195,13 @@ Document& push_document(std::unique_ptr doc, bool show, bool passive) return ret; } -void show_top_document() noexcept { +void focus_top_document(bool show) noexcept { if (auto* doc = top_document()) { - doc->show(); + if (show) { + doc->show(); + } else { + doc->focus(); + } } input::sync_input_block(); } @@ -203,13 +214,13 @@ bool any_document_visible() noexcept { bool is_prelaunch_open() noexcept { return std::any_of(sDocumentStack.begin(), sDocumentStack.end(), [](const auto& doc) { const auto* prelaunch = dynamic_cast(doc.get()); - return prelaunch != nullptr && !prelaunch->pending_close() && !prelaunch->closed(); + return prelaunch != nullptr && prelaunch->active(); }); } Document* top_document() noexcept { for (auto& doc : std::views::reverse(sDocumentStack)) { - if (!doc->closed() && !doc->pending_close()) { + if (doc->active()) { return doc.get(); } } @@ -252,7 +263,7 @@ void update() noexcept { context->GetFocusElement() == context->GetRootElement())) { for (auto& doc : std::views::reverse(sDocumentStack)) { - if (!doc->closed() && !doc->pending_close() && doc->focus()) { + if (doc->active() && doc->focus()) { break; } } diff --git a/src/dusk/ui/ui.hpp b/src/dusk/ui/ui.hpp index cbfe3dcc9d..a32bfbbcbc 100644 --- a/src/dusk/ui/ui.hpp +++ b/src/dusk/ui/ui.hpp @@ -74,7 +74,7 @@ void update() noexcept; Document& push_document( std::unique_ptr doc, bool show = true, bool passive = false) noexcept; -void show_top_document() noexcept; +void focus_top_document(bool show) noexcept; bool any_document_visible() noexcept; bool is_prelaunch_open() noexcept; Document* top_document() noexcept; diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp index e2e8a61168..ca11ccbe64 100644 --- a/src/f_ap/f_ap_game.cpp +++ b/src/f_ap/f_ap_game.cpp @@ -29,6 +29,7 @@ #include "tracy/Tracy.hpp" #include #include +#include "dusk/menu_pointer.h" #endif fapGm_HIO_c::fapGm_HIO_c() { @@ -743,6 +744,7 @@ static void fapGm_AfterRecord() { BOOL isRecording = false; static void duskExecute() { + dusk::menu_pointer::begin_game_frame(); dusk::input::handleGamepadColor(); updateAutoSave(); @@ -842,6 +844,7 @@ void fapGm_Execute() { #ifdef TARGET_PC dusk::speedrun::onGameFrame(); dusk::AchievementSystem::get().tick(); + dusk::menu_pointer::end_game_frame(); #endif } diff --git a/src/m_Do/m_Do_controller_pad.cpp b/src/m_Do/m_Do_controller_pad.cpp index 8ba03fc63b..f7eaa92ca2 100644 --- a/src/m_Do/m_Do_controller_pad.cpp +++ b/src/m_Do/m_Do_controller_pad.cpp @@ -12,6 +12,11 @@ #include "m_Do/m_Do_main.h" #include "tracy/Tracy.hpp" +#if TARGET_PC +#include "dusk/menu_pointer.h" +#include "dusk/ui/touch_controls.hpp" +#endif + JUTGamePad* mDoCPd_c::m_gamePad[4]; interface_of_controller_pad mDoCPd_c::m_cpadInfo[4]; @@ -58,6 +63,9 @@ void mDoCPd_c::create() { void mDoCPd_c::read() { ZoneScoped; +#if TARGET_PC + dusk::ui::sync_virtual_input(); +#endif JUTGamePad::read(); if (!mDoRst::isReset() && mDoRst::is3ButtonReset()) { @@ -88,6 +96,12 @@ void mDoCPd_c::read() { cLib_memSet(interface, 0, sizeof(interface_of_controller_pad)); } else { convert(interface, *pad); +#if TARGET_PC + const u32 suppressedButtons = dusk::menu_pointer::suppressed_pad_buttons(i); + interface->mButtonFlags &= ~suppressedButtons; + interface->mPressedButtonFlags &= ~suppressedButtons; + dusk::menu_pointer::finish_pad_suppression_read(i); +#endif LRlockCheck(interface); } #if DEBUG diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index ed3d083b27..18ce31cedd 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -47,6 +47,7 @@ #include #include #include "SSystem/SComponent/c_API.h" +#include "dusk/android_frame_rate.hpp" #include "dusk/app_info.hpp" #include "dusk/crash_handler.h" #include "dusk/crash_reporting.h" @@ -65,6 +66,7 @@ #include "dusk/ui/overlay.hpp" #include "dusk/ui/prelaunch.hpp" #include "dusk/ui/preset.hpp" +#include "dusk/ui/touch_controls.hpp" #include "dusk/ui/ui.hpp" #include "version.h" @@ -260,6 +262,13 @@ void main01(void) { dusk::ui::handle_event(event->sdl); dusk::g_imguiConsole.HandleSDLEvent(event->sdl); break; + case AURORA_WINDOW_RESIZED: + if (dusk::getSettings().video.rememberWindowSize && !dusk::getSettings().video.enableFullscreen) { + dusk::getSettings().video.lastWindowWidth.setValue(event->windowSize.width); + dusk::getSettings().video.lastWindowHeight.setValue(event->windowSize.height); + dusk::config::Save(); + } + break; case AURORA_DISPLAY_SCALE_CHANGED: dusk::ImGuiEngine_Initialize(event->windowSize.scale); break; @@ -326,11 +335,21 @@ void main01(void) { mDoAud_Execute(); } + aurora_end_frame(); + + FrameMark; + +#ifdef DUSK_DISCORD + dusk::discord::run_callbacks(); + dusk::discord::update_presence(); +#endif + static Limiter main_loop_limiter; static double last_fps_setting = 0.0; static Limiter::duration_t target_ns = 0; if (dusk::getSettings().game.enableFrameInterpolation.getValue() == dusk::FrameInterpMode::Capped && !dusk::getTransientSettings().skipFrameRateLimit) { + ZoneScopedN("Frame limiter"); double current_fps = dusk::getSettings().video.maxFrameRate.getValue(); if (current_fps != last_fps_setting) { last_fps_setting = current_fps; @@ -342,16 +361,6 @@ void main01(void) { } else { main_loop_limiter.Reset(); } - - aurora_end_frame(); - - - FrameMark; - -#ifdef DUSK_DISCORD - dusk::discord::run_callbacks(); - dusk::discord::update_presence(); -#endif } while (dusk::IsRunning); exit:; @@ -548,6 +557,7 @@ int game_main(int argc, char* argv[]) { dusk::resetForSpeedrunMode(); } ApplyCVarOverrides(parsed_arg_options["cvar"]); + dusk::android::update_surface_frame_rate(); dusk::crash_reporting::initialize(); dusk::crash_handler::install(); // TODO: How to handle this? @@ -585,8 +595,18 @@ int game_main(int argc, char* argv[]) { config.startFullscreen = dusk::getSettings().video.enableFullscreen; config.windowPosX = -1; config.windowPosY = -1; - config.windowWidth = defaultWindowWidth * 2; - config.windowHeight = defaultWindowHeight * 2; + + const int lastWindowWidth = dusk::getSettings().video.lastWindowWidth.getValue(); + const int lastWindowHeight = dusk::getSettings().video.lastWindowHeight.getValue(); + + if (dusk::getSettings().video.rememberWindowSize && lastWindowWidth > 0 && lastWindowHeight > 0) { + config.windowWidth = lastWindowWidth; + config.windowHeight = lastWindowHeight; + } else { + config.windowWidth = defaultWindowWidth * 2; + config.windowHeight = defaultWindowHeight * 2; + } + config.desiredBackend = ResolveDesiredBackend(parsed_arg_options); config.logCallback = &aurora_log_callback; config.logLevel = startupLogLevel; @@ -651,6 +671,7 @@ int game_main(int argc, char* argv[]) { dusk::texture_replacements::reload(); dusk::ui::initialize(); dusk::ui::push_document(std::make_unique(), true, true); + dusk::ui::push_document(std::make_unique(), false, true); dusk::ui::push_document(std::make_unique(), false); // Invalidate a bad saved isoPath so that Dusklight can't get blocked from starting up.