diff --git a/BG3Extender/BG3Extender.vcxproj b/BG3Extender/BG3Extender.vcxproj index 5c516dfa8..3c6e92f07 100644 --- a/BG3Extender/BG3Extender.vcxproj +++ b/BG3Extender/BG3Extender.vcxproj @@ -282,6 +282,7 @@ $(SolutionDir)x64\Release\ResourceBundler.exe "$(ProjectDir)LuaScripts" "$(Proje + @@ -398,6 +399,7 @@ $(SolutionDir)x64\Release\ResourceBundler.exe "$(ProjectDir)LuaScripts" "$(Proje + NotUsing @@ -500,6 +502,7 @@ $(SolutionDir)x64\Release\ResourceBundler.exe "$(ProjectDir)LuaScripts" "$(Proje + diff --git a/BG3Extender/BG3Extender.vcxproj.filters b/BG3Extender/BG3Extender.vcxproj.filters index c5aa785d8..817c391f7 100644 --- a/BG3Extender/BG3Extender.vcxproj.filters +++ b/BG3Extender/BG3Extender.vcxproj.filters @@ -1,6 +1,9 @@  + + Lua\Client + Lua\Shared @@ -448,6 +451,7 @@ + @@ -809,6 +813,7 @@ GameDefinitions\Base + GameDefinitions diff --git a/BG3Extender/GameDefinitions/Components/Camera.h b/BG3Extender/GameDefinitions/Components/Camera.h index 0955018a4..4741c1651 100644 --- a/BG3Extender/GameDefinitions/Components/Camera.h +++ b/BG3Extender/GameDefinitions/Components/Camera.h @@ -218,6 +218,11 @@ END_SE() BEGIN_NS(ecl) +struct CameraSystem : public BaseSystem +{ + DEFINE_SYSTEM(ClientCamera, "ecl::CameraSystem") +}; + struct GameCameraBehavior : public BaseComponent { DEFINE_COMPONENT(GameCameraBehavior, "ecl::GameCameraBehavior") @@ -300,6 +305,43 @@ struct GameCameraBehavior : public BaseComponent HashSet field_208; std::optional field_238; std::optional field_248; + + //# P_GETTER(CameraDefinition, GetCameraDefinition) + CameraGlobalSwitches* GetCameraDefinition() const; +}; + +// Partial native argument layouts used by camera update hooks. Only the +// accessed fields are identified; these are not complete engine structures. +struct [[bg3::hidden]] CameraUpdateTime +{ + uint8_t field_0[0x0c]; + float DeltaTime; +}; + +struct [[bg3::hidden]] CameraUpdateComponentView +{ + uint8_t field_0[0x30]; + GameCameraBehavior* Camera; + uint8_t field_38[0x38]; + EntityHandle Entity; +}; + +struct [[bg3::hidden]] CameraZoomUpdateComponentView +{ + uint8_t field_0[0x10]; + GameCameraBehavior* Camera; +}; + +struct [[bg3::hidden]] CameraInputComponentView +{ + uint8_t field_0[0x40]; + GameCameraBehavior* Camera; +}; + +struct [[bg3::hidden]] CameraDefaultZoomComponentView +{ + uint8_t field_0[0x20]; + GameCameraBehavior* Camera; }; END_NS() diff --git a/BG3Extender/GameDefinitions/Components/Camera.inl b/BG3Extender/GameDefinitions/Components/Camera.inl new file mode 100644 index 000000000..89b6dae15 --- /dev/null +++ b/BG3Extender/GameDefinitions/Components/Camera.inl @@ -0,0 +1,11 @@ +#include + +BEGIN_NS(ecl) + +CameraGlobalSwitches* GameCameraBehavior::GetCameraDefinition() const +{ + auto getter = GetStaticSymbols().ecl__GameCameraBehavior__GetCameraDefinition; + return getter ? getter(this) : nullptr; +} + +END_NS() diff --git a/BG3Extender/GameDefinitions/Components/ExposedSystemTypes.inl b/BG3Extender/GameDefinitions/Components/ExposedSystemTypes.inl index 85c411d39..5a0a28e01 100644 --- a/BG3Extender/GameDefinitions/Components/ExposedSystemTypes.inl +++ b/BG3Extender/GameDefinitions/Components/ExposedSystemTypes.inl @@ -78,6 +78,7 @@ T(ecl::EquipmentVisualsSystem) T(ecl::VisualSystem) T(ecl::CharacterIconRenderSystem) T(ecl::CharacterManager) +T(ecl::CameraSystem) T(ecl::equipment::VisualsVisibilityStateSystem) T(ecl::effect::HandlerSystem) T(ecl::TimelineSystem) diff --git a/BG3Extender/GameDefinitions/Enumerations/ECS.inl b/BG3Extender/GameDefinitions/Enumerations/ECS.inl index 6d9e4984c..7f89b7923 100644 --- a/BG3Extender/GameDefinitions/Enumerations/ECS.inl +++ b/BG3Extender/GameDefinitions/Enumerations/ECS.inl @@ -81,6 +81,7 @@ BEGIN_ENUM(ExtSystemType, uint32_t) E(ClientVisual) E(ClientCharacterIconRender) E(ClientCharacterManager) + E(ClientCamera) E(ClientVisualsVisibilityState) E(ClientEffectHandler) E(ClientTimeline) diff --git a/BG3Extender/GameDefinitions/GameDefinitionHelpers.cpp b/BG3Extender/GameDefinitions/GameDefinitionHelpers.cpp index b136ddd50..845cd1224 100644 --- a/BG3Extender/GameDefinitions/GameDefinitionHelpers.cpp +++ b/BG3Extender/GameDefinitions/GameDefinitionHelpers.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include diff --git a/BG3Extender/GameDefinitions/Symbols.h b/BG3Extender/GameDefinitions/Symbols.h index 21217671e..2c9f33bd5 100644 --- a/BG3Extender/GameDefinitions/Symbols.h +++ b/BG3Extender/GameDefinitions/Symbols.h @@ -56,6 +56,13 @@ struct StaticSymbols : Noncopyable esv::GameStateThreaded__GameStateWorker__DoWorkProc* esv__GameStateThreaded__GameStateWorker__DoWork{ nullptr }; ecl::GameStateMachine__UpdateProc* ecl__GameStateMachine__Update{ nullptr }; esv::GameStateMachine__UpdateProc* esv__GameStateMachine__Update{ nullptr }; + ecl::CameraSystem__UpdateCameraProc* ecl__CameraSystem__UpdateCamera{ nullptr }; + ecl::CameraSystem__UpdateCameraPitchProc* ecl__CameraSystem__UpdateCameraPitch{ nullptr }; + ecl::CameraSystem__UpdateCameraZoomProc* ecl__CameraSystem__UpdateCameraZoom{ nullptr }; + ecl::GameCameraBehavior__GetCameraDefinitionProc* ecl__GameCameraBehavior__GetCameraDefinition{ nullptr }; + ecl::GameCameraBehavior__CalculatePitchProc* ecl__GameCameraBehavior__CalculatePitch{ nullptr }; + ecl::CameraSystem__HandleInputProc* ecl__CameraSystem__HandleInput{ nullptr }; + ecl::CameraSystem__SetDefaultZoomProc* ecl__CameraSystem__SetDefaultZoom{ nullptr }; App__LoadGraphicSettingsProc* App__LoadGraphicSettings{ nullptr }; input::InputManager** ls__gInputManager{ nullptr }; diff --git a/BG3Extender/GameHooks/BinaryMappings.xml b/BG3Extender/GameHooks/BinaryMappings.xml index 3c863f048..e7a5ab109 100644 --- a/BG3Extender/GameHooks/BinaryMappings.xml +++ b/BG3Extender/GameHooks/BinaryMappings.xml @@ -179,6 +179,65 @@ + + @ref1 e8 ?? ?? ?? ?? // call ecl__CameraSystem__UpdateCamera + 48 8d 8d f8 04 00 00 // lea rcx, [rbp+4F8h] + e8 ?? ?? ?? ?? // call cleanup + e9 af fd ff ff // jmp camera loop + + + + + f3 0f 11 6d f0 // movss [rbp-10h], xmm5 + e8 ?? ?? ?? ?? // call Normalize + f2 0f 10 45 e8 // movsd xmm0, [rbp-18h] + 8b 45 f0 // mov eax, [rbp-10h] + f2 0f 11 47 70 // movsd [rdi+70h], xmm0 + 89 47 78 // mov [rdi+78h], eax + 4c 8b cb // mov r9, rbx + 4c 8b c7 // mov r8, rdi + 49 8b d6 // mov rdx, r14 + 49 8b cf // mov rcx, r15 + @ref1 e8 ?? ?? ?? ?? // call ecl__CameraSystem__UpdateCameraPitch + 48 8b 46 70 // mov rax, [rsi+70h] + + + + + @ref1 e8 ?? ?? ?? ?? // call ecl__CameraSystem__UpdateCameraZoom + 80 bf 54 02 00 00 00 // cmp byte ptr [rdi+254h], 0 + + + + + 48 8b 05 ?? ?? ?? ?? // mov rax, cs:ls_GlobalSwitches + 80 b8 32 13 00 00 00 // cmp byte ptr [rax+1332h], 0 + 74 07 // jz normal camera definition + + + + + @ref1 e8 ?? ?? ?? ?? // call ecl__GameCameraBehavior__CalculatePitch + 80 bf 4c 01 00 00 00 // cmp byte ptr [rdi+14Ch], 0 + + + + + @ref1 e8 ?? ?? ?? ?? // call ecl__CameraSystem__HandleInput + 0f b7 08 // movzx ecx, word ptr [rax] + 66 89 0b // mov [rbx], cx + 80 3b 00 // cmp byte ptr [rbx], 0 + + + + + @ref1 e8 ?? ?? ?? ?? // call ecl__CameraSystem__SetDefaultZoom + 48 8b cf // mov rcx, rdi + e8 ?? ?? ?? ?? // call camera mode update + e9 81 02 00 00 // jmp input handling continuation + + + 48 83 be 00 03 00 00 00 // cmp qword ptr [rsi+300h], 0 74 08 // jz short loc_143693C93 diff --git a/BG3Extender/GameHooks/DataLibrariesBG3Game.cpp b/BG3Extender/GameHooks/DataLibrariesBG3Game.cpp index 3af379dd1..584e180e4 100644 --- a/BG3Extender/GameHooks/DataLibrariesBG3Game.cpp +++ b/BG3Extender/GameHooks/DataLibrariesBG3Game.cpp @@ -149,6 +149,13 @@ namespace bg3se SYM_OFF(esv__GameStateThreaded__GameStateWorker__DoWork); SYM_OFF(ecl__GameStateMachine__Update); SYM_OFF(esv__GameStateMachine__Update); + SYM_OFF(ecl__CameraSystem__UpdateCamera); + SYM_OFF(ecl__CameraSystem__UpdateCameraPitch); + SYM_OFF(ecl__CameraSystem__UpdateCameraZoom); + SYM_OFF(ecl__GameCameraBehavior__GetCameraDefinition); + SYM_OFF(ecl__GameCameraBehavior__CalculatePitch); + SYM_OFF(ecl__CameraSystem__HandleInput); + SYM_OFF(ecl__CameraSystem__SetDefaultZoom); SYM_OFF(App__LoadGraphicSettings); SYM_OFF(ls__gInputManager); diff --git a/BG3Extender/GameHooks/EngineHooks.inl b/BG3Extender/GameHooks/EngineHooks.inl index 00539c919..f9007ec83 100644 --- a/BG3Extender/GameHooks/EngineHooks.inl +++ b/BG3Extender/GameHooks/EngineHooks.inl @@ -14,6 +14,12 @@ HOOK_DEFN(esv__StatsSystem__ThrowDamageEvent, esv__StatsSystem__ThrowDamageEvent HOOK_DEFN(eoc__AiGrid__FindPath, eoc__AiGrid__FindPath, AiGrid__FindPathProc) HOOK_DEFN(ls__Visual__UpdateBlendshapeWeightsFromSkeleton, ls__Visual__UpdateBlendshapeWeightsFromSkeleton, Visual__UpdateBlendshapeWeightsFromSkeletonProc) HOOK_DEFN(ui__DataContextProvider__ExecuteCommandQueues, ui__DataContextProvider__ExecuteCommandQueues, ui::DataContextProvider__ExecuteCommandQueuesProc) +HOOK_DEFN(ecl__CameraSystem__UpdateCamera, ecl__CameraSystem__UpdateCamera, ecl::CameraSystem__UpdateCameraProc) +HOOK_DEFN(ecl__CameraSystem__UpdateCameraPitch, ecl__CameraSystem__UpdateCameraPitch, ecl::CameraSystem__UpdateCameraPitchProc) +HOOK_DEFN(ecl__CameraSystem__UpdateCameraZoom, ecl__CameraSystem__UpdateCameraZoom, ecl::CameraSystem__UpdateCameraZoomProc) +HOOK_DEFN(ecl__GameCameraBehavior__CalculatePitch, ecl__GameCameraBehavior__CalculatePitch, ecl::GameCameraBehavior__CalculatePitchProc) +HOOK_DEFN(ecl__CameraSystem__HandleInput, ecl__CameraSystem__HandleInput, ecl::CameraSystem__HandleInputProc) +HOOK_DEFN(ecl__CameraSystem__SetDefaultZoom, ecl__CameraSystem__SetDefaultZoom, ecl::CameraSystem__SetDefaultZoomProc) HOOK_DEFN(esv__ExecuteStatsFunctor_AttackTargetContext, esv__ExecuteStatsFunctor_AttackTargetContext, stats::ExecuteFunctorProc) HOOK_DEFN(esv__ExecuteStatsFunctor_AttackPositionContext, esv__ExecuteStatsFunctor_AttackPositionContext, stats::ExecuteFunctorProc) diff --git a/BG3Extender/GameHooks/EngineHooksFwdDecl.h b/BG3Extender/GameHooks/EngineHooksFwdDecl.h index 7701fa24c..8e4d13455 100644 --- a/BG3Extender/GameHooks/EngineHooksFwdDecl.h +++ b/BG3Extender/GameHooks/EngineHooksFwdDecl.h @@ -16,6 +16,7 @@ struct AttackDesc; struct FileReader; struct AiGrid; struct Visual; +struct CameraGlobalSwitches; using AiPathId = int32_t; @@ -39,6 +40,12 @@ using TextureManager__UnloadTextureProc = bool (TextureManager* self, FixedStrin END_SE() +BEGIN_NS(input) + +struct InputEvent; + +END_NS() + BEGIN_NS(stats) struct DealDamageFunctor; @@ -112,11 +119,25 @@ END_NS() BEGIN_NS(ecl) +struct GameCameraBehavior; +struct CameraUpdateTime; +struct CameraUpdateComponentView; +struct CameraZoomUpdateComponentView; +struct CameraInputComponentView; +struct CameraDefaultZoomComponentView; + using EoCClient__HandleErrorProc = void (EoCClient* self, TranslatedString const& message, bool exitGame, TranslatedString const& a4, uint8_t messageBoxLocalId); using GameStateEventManager__ExecuteGameStateChangedEventProc = void (void* self, GameState fromState, GameState toState); using GameStateThreaded__GameStateWorker__DoWorkProc = void (void* self); using GameStateMachine__UpdateProc = void (void* self, GameTime* time); +using CameraSystem__UpdateCameraProc = void (void* self, void* systemContext, CameraUpdateTime const* time, CameraUpdateComponentView* view); +using CameraSystem__UpdateCameraPitchProc = void (void* self, void* entity, GameCameraBehavior* camera, CameraUpdateTime const* time); +using CameraSystem__UpdateCameraZoomProc = void (void* self, void* entity, CameraZoomUpdateComponentView* view, CameraUpdateTime const* time); +using GameCameraBehavior__GetCameraDefinitionProc = CameraGlobalSwitches* (GameCameraBehavior const* camera); +using GameCameraBehavior__CalculatePitchProc = float (GameCameraBehavior* camera, bool ignoreAvoidance, bool selectMode); +using CameraSystem__HandleInputProc = uint16_t* (void* self, uint16_t* result, CameraInputComponentView* view, input::InputEvent* inputEvent); +using CameraSystem__SetDefaultZoomProc = void (CameraDefaultZoomComponentView* view); END_NS() diff --git a/BG3Extender/Lua/Client/CameraEvents.cpp b/BG3Extender/Lua/Client/CameraEvents.cpp new file mode 100644 index 000000000..e4614820e --- /dev/null +++ b/BG3Extender/Lua/Client/CameraEvents.cpp @@ -0,0 +1,182 @@ +#include +#include +#include +#include +#include +#include +#include + +BEGIN_NS(ecl::lua) + +CameraEventHooks::CameraEventHooks() +{ + auto& hooks = gExtender->GetEngineHooks(); + hooks.ecl__CameraSystem__UpdateCamera.SetPrePostHook(&CameraEventHooks::OnCameraUpdating, &CameraEventHooks::OnCameraUpdated, this); + hooks.ecl__CameraSystem__UpdateCameraPitch.SetPrePostHook(&CameraEventHooks::OnCameraPitchUpdating, &CameraEventHooks::OnCameraPitchUpdated, this); + hooks.ecl__CameraSystem__UpdateCameraZoom.SetPostHook(&CameraEventHooks::OnCameraZoomUpdated, this); + hooks.ecl__GameCameraBehavior__CalculatePitch.SetWrapper(&CameraEventHooks::OnCameraPitchCalculated, this); + hooks.ecl__CameraSystem__HandleInput.SetWrapper(&CameraEventHooks::OnCameraInput, this); + hooks.ecl__CameraSystem__SetDefaultZoom.SetPrePostHook(&CameraEventHooks::OnCameraZoomResetting, &CameraEventHooks::OnCameraZoomReset, this); +} + +CameraEventHooks::~CameraEventHooks() +{ + auto& hooks = gExtender->GetEngineHooks(); + hooks.ecl__CameraSystem__UpdateCamera.ClearHook(); + hooks.ecl__CameraSystem__UpdateCameraPitch.ClearHook(); + hooks.ecl__CameraSystem__UpdateCameraZoom.ClearHook(); + hooks.ecl__GameCameraBehavior__CalculatePitch.ClearHook(); + hooks.ecl__CameraSystem__HandleInput.ClearHook(); + hooks.ecl__CameraSystem__SetDefaultZoom.ClearHook(); +} + +void CameraEventHooks::OnCameraUpdating(void* self, void* systemContext, ecl::CameraUpdateTime const* time, ecl::CameraUpdateComponentView* view) +{ + ContextGuardAnyThread context(ContextType::Client); + LuaClientPin lua(gExtender->GetClient().GetExtensionState()); + if (lua) { + CameraUpdatingEvent evt{ + .Entity = view->Entity, + .Camera = view->Camera, + .DeltaTime = time->DeltaTime + }; + lua->ThrowEvent("CameraUpdating", evt, false, 0); + } +} + +void CameraEventHooks::OnCameraUpdated(void* self, void* systemContext, ecl::CameraUpdateTime const* time, ecl::CameraUpdateComponentView* view) +{ + ContextGuardAnyThread context(ContextType::Client); + LuaClientPin lua(gExtender->GetClient().GetExtensionState()); + if (lua) { + CameraUpdatedEvent evt{ + .Entity = view->Entity, + .Camera = view->Camera, + .DeltaTime = time->DeltaTime + }; + lua->ThrowEvent("CameraUpdated", evt, false, 0); + } +} + +void CameraEventHooks::OnCameraPitchUpdating(void* self, void* entity, ecl::GameCameraBehavior* camera, ecl::CameraUpdateTime const* time) +{ + ContextGuardAnyThread context(ContextType::Client); + LuaClientPin lua(gExtender->GetClient().GetExtensionState()); + if (lua) { + CameraPitchUpdatingEvent evt{ + .Camera = camera, + .DeltaTime = time->DeltaTime + }; + lua->ThrowEvent("CameraPitchUpdating", evt, false, 0); + } +} + +void CameraEventHooks::OnCameraPitchUpdated(void* self, void* entity, ecl::GameCameraBehavior* camera, ecl::CameraUpdateTime const* time) +{ + ContextGuardAnyThread context(ContextType::Client); + LuaClientPin lua(gExtender->GetClient().GetExtensionState()); + if (lua) { + CameraPitchUpdatedEvent evt{ + .Camera = camera, + .DeltaTime = time->DeltaTime + }; + lua->ThrowEvent("CameraPitchUpdated", evt, false, 0); + } +} + +void CameraEventHooks::OnCameraZoomUpdated(void* self, void* entity, ecl::CameraZoomUpdateComponentView* view, ecl::CameraUpdateTime const* time) +{ + ContextGuardAnyThread context(ContextType::Client); + LuaClientPin lua(gExtender->GetClient().GetExtensionState()); + if (lua) { + CameraZoomUpdatedEvent evt{ + .Camera = view->Camera, + .DeltaTime = time->DeltaTime + }; + lua->ThrowEvent("CameraZoomUpdated", evt, false, 0); + } +} + +float CameraEventHooks::OnCameraPitchCalculated(ecl::GameCameraBehavior__CalculatePitchProc* next, ecl::GameCameraBehavior* camera, bool ignoreAvoidance, bool selectMode) +{ + auto pitch = next(camera, ignoreAvoidance, selectMode); + ContextGuardAnyThread context(ContextType::Client); + LuaClientPin lua(gExtender->GetClient().GetExtensionState()); + if (lua) { + CameraPitchCalculatedEvent evt{ + .Camera = camera, + .IgnoreAvoidance = ignoreAvoidance, + .SelectMode = selectMode, + .OriginalPitch = pitch, + .Pitch = pitch + }; + auto result = lua->ThrowEvent("CameraPitchCalculated", evt, false, 0); + // Check for a finite number; otherwise return the original native pitch. + if (result == EventResult::Successful && (std::bit_cast(evt.Pitch) & 0x7f800000u) != 0x7f800000u) { + return evt.Pitch; + } + } + + return pitch; +} + +uint16_t* CameraEventHooks::OnCameraInput(ecl::CameraSystem__HandleInputProc* next, void* self, uint16_t* result, ecl::CameraInputComponentView* view, bg3se::input::InputEvent* input) +{ + { + ContextGuardAnyThread context(ContextType::Client); + LuaClientPin lua(gExtender->GetClient().GetExtensionState()); + if (lua) { + CameraInputEvent evt{ + .Camera = view->Camera, + .Input = input + }; + if (lua->ThrowEvent("CameraInput", evt, true, 0) == EventResult::ActionPrevented) { + *result = evt.Result; + return result; + } + } + } + + auto ret = next(self, result, view, input); + + { + ContextGuardAnyThread context(ContextType::Client); + LuaClientPin lua(gExtender->GetClient().GetExtensionState()); + if (lua) { + CameraInputHandledEvent evt{ + .Camera = view->Camera, + .Input = input, + .Result = *ret + }; + lua->ThrowEvent("CameraInputHandled", evt, false, 0); + } + } + + return ret; +} + +void CameraEventHooks::OnCameraZoomResetting(ecl::CameraDefaultZoomComponentView* view) +{ + ContextGuardAnyThread context(ContextType::Client); + LuaClientPin lua(gExtender->GetClient().GetExtensionState()); + if (lua) { + CameraZoomResettingEvent evt{ + .Camera = view->Camera + }; + lua->ThrowEvent("CameraZoomResetting", evt, false, 0); + } +} + +void CameraEventHooks::OnCameraZoomReset(ecl::CameraDefaultZoomComponentView* view) +{ + ContextGuardAnyThread context(ContextType::Client); + LuaClientPin lua(gExtender->GetClient().GetExtensionState()); + if (lua) { + CameraZoomResetEvent evt{ + .Camera = view->Camera + }; + lua->ThrowEvent("CameraZoomReset", evt, false, 0); + } +} + +END_NS() diff --git a/BG3Extender/Lua/Client/CameraEvents.h b/BG3Extender/Lua/Client/CameraEvents.h new file mode 100644 index 000000000..67032a332 --- /dev/null +++ b/BG3Extender/Lua/Client/CameraEvents.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +BEGIN_NS(ecl::lua) + +class CameraEventHooks +{ +public: + CameraEventHooks(); + ~CameraEventHooks(); + +private: + void OnCameraUpdating(void* self, void* systemContext, ecl::CameraUpdateTime const* time, ecl::CameraUpdateComponentView* view); + void OnCameraUpdated(void* self, void* systemContext, ecl::CameraUpdateTime const* time, ecl::CameraUpdateComponentView* view); + void OnCameraPitchUpdating(void* self, void* entity, ecl::GameCameraBehavior* camera, ecl::CameraUpdateTime const* time); + void OnCameraPitchUpdated(void* self, void* entity, ecl::GameCameraBehavior* camera, ecl::CameraUpdateTime const* time); + void OnCameraZoomUpdated(void* self, void* entity, ecl::CameraZoomUpdateComponentView* view, ecl::CameraUpdateTime const* time); + float OnCameraPitchCalculated(ecl::GameCameraBehavior__CalculatePitchProc* next, ecl::GameCameraBehavior* camera, bool ignoreAvoidance, bool selectMode); + uint16_t* OnCameraInput(ecl::CameraSystem__HandleInputProc* next, void* self, uint16_t* result, ecl::CameraInputComponentView* view, bg3se::input::InputEvent* input); + void OnCameraZoomResetting(ecl::CameraDefaultZoomComponentView* view); + void OnCameraZoomReset(ecl::CameraDefaultZoomComponentView* view); +}; + +END_NS() diff --git a/BG3Extender/Lua/Client/ClientEvents.h b/BG3Extender/Lua/Client/ClientEvents.h index 9d0d7513d..945845db7 100644 --- a/BG3Extender/Lua/Client/ClientEvents.h +++ b/BG3Extender/Lua/Client/ClientEvents.h @@ -1,5 +1,11 @@ #pragma once +BEGIN_NS(ecl) + +struct GameCameraBehavior; + +END_NS() + BEGIN_NS(ecl::lua) using namespace bg3se::lua; @@ -57,4 +63,69 @@ struct ViewportResizedEvent : public lua::EventBase int Height; }; +struct CameraUpdatingEvent : public lua::EventBase +{ + [[bg3::readonly]] EntityHandle Entity; + [[bg3::readonly]] ecl::GameCameraBehavior* Camera; + [[bg3::readonly]] float DeltaTime; +}; + +struct CameraUpdatedEvent : public lua::EventBase +{ + [[bg3::readonly]] EntityHandle Entity; + [[bg3::readonly]] ecl::GameCameraBehavior* Camera; + [[bg3::readonly]] float DeltaTime; +}; + +struct CameraPitchUpdatedEvent : public lua::EventBase +{ + [[bg3::readonly]] ecl::GameCameraBehavior* Camera; + [[bg3::readonly]] float DeltaTime; +}; + +struct CameraZoomUpdatedEvent : public lua::EventBase +{ + [[bg3::readonly]] ecl::GameCameraBehavior* Camera; + [[bg3::readonly]] float DeltaTime; +}; + +struct CameraPitchUpdatingEvent : public lua::EventBase +{ + [[bg3::readonly]] ecl::GameCameraBehavior* Camera; + [[bg3::readonly]] float DeltaTime; +}; + +struct CameraPitchCalculatedEvent : public lua::EventBase +{ + [[bg3::readonly]] ecl::GameCameraBehavior* Camera; + [[bg3::readonly]] bool IgnoreAvoidance; + [[bg3::readonly]] bool SelectMode; + [[bg3::readonly]] float OriginalPitch; + float Pitch; +}; + +struct CameraInputEvent : public lua::EventBase +{ + [[bg3::readonly]] ecl::GameCameraBehavior* Camera; + [[bg3::readonly]] bg3se::input::InputEvent* Input; + uint16_t Result{ 0 }; +}; + +struct CameraInputHandledEvent : public lua::EventBase +{ + [[bg3::readonly]] ecl::GameCameraBehavior* Camera; + [[bg3::readonly]] bg3se::input::InputEvent* Input; + [[bg3::readonly]] uint16_t Result; +}; + +struct CameraZoomResettingEvent : public lua::EventBase +{ + [[bg3::readonly]] ecl::GameCameraBehavior* Camera; +}; + +struct CameraZoomResetEvent : public lua::EventBase +{ + [[bg3::readonly]] ecl::GameCameraBehavior* Camera; +}; + END_NS() diff --git a/BG3Extender/Lua/Client/LuaBindingClient.h b/BG3Extender/Lua/Client/LuaBindingClient.h index 60badfadf..69b6bebd5 100644 --- a/BG3Extender/Lua/Client/LuaBindingClient.h +++ b/BG3Extender/Lua/Client/LuaBindingClient.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -60,6 +61,7 @@ class ClientState : public State private: ExtensionLibraryClient library_; + CameraEventHooks cameraHooks_; ClientEntityReplicationEventHooks replicationHooks_; UIEventHooks uiEvents_; DeferredUIEvents deferredUIEvents_; diff --git a/BG3Extender/LuaScripts/BuiltinLibraryClient.lua b/BG3Extender/LuaScripts/BuiltinLibraryClient.lua index 55262fb37..bb1a3f87f 100644 --- a/BG3Extender/LuaScripts/BuiltinLibraryClient.lua +++ b/BG3Extender/LuaScripts/BuiltinLibraryClient.lua @@ -6,7 +6,17 @@ _I._PublishedEvents = { "MouseWheelInput", "ControllerAxisInput", "ControllerButtonInput", - "ViewportResized" + "ViewportResized", + "CameraUpdating", + "CameraUpdated", + "CameraPitchUpdating", + "CameraPitchCalculated", + "CameraPitchUpdated", + "CameraZoomUpdated", + "CameraInput", + "CameraInputHandled", + "CameraZoomResetting", + "CameraZoomReset" } Ext.IsClient = function () diff --git a/Docs/API.md b/Docs/API.md index 62b40de1a..86c175ddb 100644 --- a/Docs/API.md +++ b/Docs/API.md @@ -54,6 +54,7 @@ - [Level](#level) - [Math Library](#math) - [Engine Events](#engine-events) +- [Camera](#camera) ## Getting Started @@ -1934,3 +1935,47 @@ You can listen to SE and engine events with `Ext.Events.:Subscribe(f | `ResetCompleted` | Thrown when `Ext.Debug.Reset()` or `reset` console command completes on the client or server.
Indicates that the Lua state was reloaded. | | `GameStateChanged` | Indicates that the server/client game state changed (e.g., loading save, paused, main menu, etc.). | | `Tick` | Thrown after each game engine tick on both the client and the server.
Server logic runs at ~30hz, so this event is thrown roughly every 33ms.
Helper: `Ext.OnNextTick(fun)` registers a handler that is only called on the next tick and is unregistered afterwards. | + +## Camera + +Client-only. Camera events use the standard [SE event system](#lua-events). `Ext.Events.CameraUpdated:Subscribe(callback, options)` returns a handler ID; call `Ext.Events.CameraUpdated:Unsubscribe(id)` on the same event to remove it. The standard `Priority` and `Once` options apply. Subscriptions are cleared when the client Lua state is destroyed. + +Callbacks run synchronously in priority order, with registration order preserved for equal priorities. Explicit subscription additions and removals during dispatch take effect after the outermost dispatch of that event finishes. `StopPropagation()` skips later callbacks for that invocation; it does not prevent the native operation. + +```lua +local id = Ext.Events.CameraPitchCalculated:Subscribe(function(e) + e.Pitch = 0.5 +end) +Ext.Events.CameraPitchCalculated:Unsubscribe(id) +``` + +| Event | Description | +|-------|-------------| +| `CameraUpdating` | Client-only. Thrown before a gameplay camera is updated. | +| `CameraUpdated` | Client-only. Thrown after a gameplay camera is updated. | +| `CameraPitchUpdating` | Client-only. Thrown before the camera pitch update. | +| `CameraPitchUpdated` | Client-only. Thrown after the camera pitch update. | +| `CameraPitchCalculated` | Client-only. Allows overriding a calculated camera pitch. | +| `CameraZoomUpdated` | Client-only. Thrown after zoom and collision-distance updates, before final placement. | +| `CameraInput` | Client-only. Allows modifying or preventing camera input handling. | +| `CameraInputHandled` | Client-only. Thrown after camera input handling. | +| `CameraZoomResetting` | Client-only. Thrown before the default zoom is applied. | +| `CameraZoomReset` | Client-only. Thrown after the default zoom is applied. | + +All camera events expose `Camera` (`ecl.GameCameraBehavior`). Its fields can be modified, but the reference cannot be replaced. Event objects and their references are only valid during the callback. + +`CameraUpdating` and `CameraUpdated` also expose read-only `Entity`, the entity that owns the camera component. + +`CameraUpdating`, `CameraUpdated`, `CameraPitchUpdating`, `CameraPitchUpdated`, and `CameraZoomUpdated` also expose read-only `DeltaTime` in seconds. These events cannot be prevented. They apply to individual cameras, not the whole camera system. + +`CameraPitchCalculated` exposes read-only `IgnoreAvoidance`, `SelectMode`, and `OriginalPitch`, and writable `Pitch`. Set `Pitch` to replace the calculated value. Nonfinite values are ignored. This event also runs during target and collision calculations, outside the pitch update. + +`CameraInput` exposes `Input` (`input.InputEvent`) and writable `Result` (unsigned 16-bit integer, initially zero). Changes to `Input` are passed to the camera handler. Calling `PreventAction()` skips the handler and returns `Result`. Zero allows fallback input handling; one suppresses it at the camera dispatch boundary. + +`CameraInputHandled` exposes the same `Camera` and `Input`, plus read-only `Result` returned by the handler. It is not thrown when `CameraInput` prevents handling. + +`CameraZoomResetting` and `CameraZoomReset` cannot be prevented. They run before and after the default-zoom routine, respectively. + +### GameCameraBehavior.CameraDefinition + +Read-only property returning the `CameraGlobalSwitches` selected for this camera on each access. The returned object's fields are writable. Available through `entity.GameCameraBehavior.CameraDefinition` independently of subscriptions, and through `e.Camera.CameraDefinition` in a callback. Returns `nil` if the native getter is unavailable.