diff --git a/ISLE/isleapp.cpp b/ISLE/isleapp.cpp index 87147982..4b3c0aed 100644 --- a/ISLE/isleapp.cpp +++ b/ISLE/isleapp.cpp @@ -877,6 +877,10 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) } } +#ifdef EXTENSIONS + Extensions::HandleMultiplayerSDLEvent(event); +#endif + return SDL_APP_CONTINUE; } diff --git a/LEGO1/lego/legoomni/include/legonavcontroller.h b/LEGO1/lego/legoomni/include/legonavcontroller.h index 974966c6..42a5ff0c 100644 --- a/LEGO1/lego/legoomni/include/legonavcontroller.h +++ b/LEGO1/lego/legoomni/include/legonavcontroller.h @@ -7,6 +7,10 @@ struct LegoLocation; class Vector3; +namespace Multiplayer +{ +class ThirdPersonCamera; +} ////////////////////////////////////////////////////////////////////////////// // @@ -122,6 +126,8 @@ class LegoNavController : public MxCore { // LegoNavController::`scalar deleting destructor' protected: + friend class Multiplayer::ThirdPersonCamera; + float CalculateNewVel(float p_targetVel, float p_currentVel, float p_accel, float p_time); float CalculateNewTargetVel(int p_pos, int p_center, float p_max); float CalculateNewAccel(int p_pos, int p_center, float p_max, int p_min); diff --git a/LEGO1/lego/legoomni/src/entity/legonavcontroller.cpp b/LEGO1/lego/legoomni/src/entity/legonavcontroller.cpp index aea8beb0..4c82d541 100644 --- a/LEGO1/lego/legoomni/src/entity/legonavcontroller.cpp +++ b/LEGO1/lego/legoomni/src/entity/legonavcontroller.cpp @@ -2,6 +2,7 @@ #include "3dmanager/lego3dmanager.h" #include "act3.h" +#include "extensions/multiplayer.h" #include "infocenter.h" #include "legoanimationmanager.h" #include "legocameracontroller.h" @@ -29,6 +30,8 @@ #include #include +using namespace Extensions; + DECOMP_SIZE_ASSERT(LegoNavController, 0x70) // MSVC 4.20 didn't define a macro for this key @@ -348,6 +351,11 @@ MxBool LegoNavController::CalculateNewPosDir( ProcessJoystickInput(rotatedY); } + if (Extension::Call(HandleNavOverride, this, p_curPos, p_curDir, p_newPos, p_newDir, deltaTime) + .value_or(FALSE)) { + return TRUE; + } + if (m_useRotationalVel) { m_rotationalVel = CalculateNewVel(m_targetRotationalVel, m_rotationalVel, m_rotationalAccel * 40.0f, deltaTime); } diff --git a/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp b/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp index d84368c8..a794581b 100644 --- a/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp +++ b/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp @@ -813,8 +813,6 @@ void LegoInputManager::InitializeHaptics() void LegoInputManager::UpdateLastInputMethod(SDL_Event* p_event) { - Extension::Call(HandleSDLEvent, p_event); - switch (p_event->type) { case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: diff --git a/extensions/include/extensions/multiplayer.h b/extensions/include/extensions/multiplayer.h index df5d9389..e8e3f77c 100644 --- a/extensions/include/extensions/multiplayer.h +++ b/extensions/include/extensions/multiplayer.h @@ -10,9 +10,11 @@ class IslePathActor; class LegoEntity; class LegoEventNotificationParam; +class LegoNavController; class LegoPathActor; class LegoROI; class LegoWorld; +class Vector3; namespace Multiplayer { @@ -73,6 +75,17 @@ class MultiplayerExt { // Returns TRUE if the caller should return early. static MxBool HandleTouchInput(); + // Overrides nav controller movement for camera-relative 3rd person controls. + // Returns TRUE if the hook handled movement (caller should return early). + static MxBool HandleNavOverride( + LegoNavController* p_nav, + const Vector3& p_curPos, + const Vector3& p_curDir, + Vector3& p_newPos, + Vector3& p_newDir, + float p_deltaTime + ); + static void SetNetworkManager(Multiplayer::NetworkManager* p_networkManager); static Multiplayer::NetworkManager* GetNetworkManager(); @@ -84,6 +97,7 @@ class MultiplayerExt { #ifdef EXTENSIONS LEGO1_EXPORT bool IsMultiplayerRejected(); +LEGO1_EXPORT void HandleMultiplayerSDLEvent(SDL_Event* p_event); constexpr auto HandleCreate = &MultiplayerExt::HandleCreate; constexpr auto HandleWorldEnable = &MultiplayerExt::HandleWorldEnable; @@ -100,6 +114,7 @@ constexpr auto CheckRejected = &MultiplayerExt::CheckRejected; constexpr auto HandleSDLEvent = &MultiplayerExt::HandleSDLEvent; constexpr auto IsThirdPersonCameraActive = &MultiplayerExt::IsThirdPersonCameraActive; constexpr auto HandleTouchInput = &MultiplayerExt::HandleTouchInput; +constexpr auto HandleNavOverride = &MultiplayerExt::HandleNavOverride; #else constexpr decltype(&MultiplayerExt::HandleCreate) HandleCreate = nullptr; constexpr decltype(&MultiplayerExt::HandleWorldEnable) HandleWorldEnable = nullptr; @@ -116,6 +131,7 @@ constexpr decltype(&MultiplayerExt::CheckRejected) CheckRejected = nullptr; constexpr decltype(&MultiplayerExt::HandleSDLEvent) HandleSDLEvent = nullptr; constexpr decltype(&MultiplayerExt::IsThirdPersonCameraActive) IsThirdPersonCameraActive = nullptr; constexpr decltype(&MultiplayerExt::HandleTouchInput) HandleTouchInput = nullptr; +constexpr decltype(&MultiplayerExt::HandleNavOverride) HandleNavOverride = nullptr; #endif }; // namespace Extensions diff --git a/extensions/include/extensions/multiplayer/thirdpersoncamera.h b/extensions/include/extensions/multiplayer/thirdpersoncamera.h index 795cb034..d9dfeb6f 100644 --- a/extensions/include/extensions/multiplayer/thirdpersoncamera.h +++ b/extensions/include/extensions/multiplayer/thirdpersoncamera.h @@ -9,9 +9,11 @@ #include class IslePathActor; +class LegoNavController; class LegoPathActor; class LegoROI; class LegoWorld; +class Vector3; namespace Multiplayer { @@ -54,18 +56,31 @@ class ThirdPersonCamera { void OnWorldEnabled(LegoWorld* p_world); void OnWorldDisabled(LegoWorld* p_world); + // Camera-relative movement override (called from nav controller hook) + MxBool HandleCameraRelativeMovement( + LegoNavController* p_nav, + const Vector3& p_curPos, + const Vector3& p_curDir, + Vector3& p_newPos, + Vector3& p_newDir, + float p_deltaTime + ); + // Free camera input handling void HandleSDLEvent(SDL_Event* p_event); bool IsTouchGestureActive() const { return m_touchGestureActive; } private: // Orbit camera helpers - void ComputeOrbitVectors(Mx3DPointFloat& p_at, Mx3DPointFloat& p_dir, Mx3DPointFloat& p_up) const; + void ComputeOrbitVectors(float p_yaw, Mx3DPointFloat& p_at, Mx3DPointFloat& p_dir, Mx3DPointFloat& p_up) const; void ApplyOrbitCamera(); void ResetOrbitState(); void ClampPitch(); void ClampDistance(); + float GetLocalYaw(LegoROI* p_roi) const; + void InitAbsoluteYaw(LegoROI* p_roi); + void SetupCamera(LegoPathActor* p_actor); void ReinitForCharacter(); @@ -93,9 +108,10 @@ class ThirdPersonCamera { bool m_showNameBubble; // Orbit camera state - float m_orbitYaw; float m_orbitPitch; float m_orbitDistance; + float m_absoluteYaw; // Camera yaw in world space (decoupled from player facing) + float m_smoothedSpeed; // Extension-managed velocity for smooth acceleration/deceleration // Touch gesture tracking bool m_touchGestureActive = false; diff --git a/extensions/src/multiplayer.cpp b/extensions/src/multiplayer.cpp index bc44558c..bede0661 100644 --- a/extensions/src/multiplayer.cpp +++ b/extensions/src/multiplayer.cpp @@ -12,6 +12,7 @@ #include "legoeventnotificationparam.h" #include "legogamestate.h" #include "legoinputmanager.h" +#include "legonavcontroller.h" #include "legopathactor.h" #include "misc.h" #include "roi/legoroi.h" @@ -323,6 +324,27 @@ MxBool MultiplayerExt::HandleTouchInput() return FALSE; } +MxBool MultiplayerExt::HandleNavOverride( + LegoNavController* p_nav, + const Vector3& p_curPos, + const Vector3& p_curDir, + Vector3& p_newPos, + Vector3& p_newDir, + float p_deltaTime +) +{ + if (!s_networkManager) { + return FALSE; + } + + Multiplayer::ThirdPersonCamera& cam = s_networkManager->GetThirdPersonCamera(); + if (!cam.IsActive()) { + return FALSE; + } + + return cam.HandleCameraRelativeMovement(p_nav, p_curPos, p_curDir, p_newPos, p_newDir, p_deltaTime); +} + MxBool MultiplayerExt::CheckRejected() { if (s_networkManager && s_networkManager->WasRejected()) { @@ -346,3 +368,8 @@ bool Extensions::IsMultiplayerRejected() { return Extension::Call(CheckRejected).value_or(FALSE); } + +void Extensions::HandleMultiplayerSDLEvent(SDL_Event* p_event) +{ + Extension::Call(HandleSDLEvent, p_event); +} diff --git a/extensions/src/multiplayer/thirdpersoncamera.cpp b/extensions/src/multiplayer/thirdpersoncamera.cpp index b6f41d42..1f918877 100644 --- a/extensions/src/multiplayer/thirdpersoncamera.cpp +++ b/extensions/src/multiplayer/thirdpersoncamera.cpp @@ -7,6 +7,7 @@ #include "islepathactor.h" #include "legocameracontroller.h" #include "legocharactermanager.h" +#include "legoinputmanager.h" #include "legonavcontroller.h" #include "legopathactor.h" #include "legovideomanager.h" @@ -23,6 +24,8 @@ using namespace Multiplayer; +static constexpr float TURN_RATE = 10.0f; + // Flip a matrix from forward-z to backward-z (or vice versa) in place. // Same operation as IslePathActor::TurnAround: negate z, recompute right. static void FlipMatrixDirection(MxMatrix& p_mat) @@ -38,8 +41,8 @@ ThirdPersonCamera::ThirdPersonCamera() : m_enabled(false), m_active(false), m_pendingWorldTransition(false), m_playerROI(nullptr), m_displayActorIndex(DISPLAY_ACTOR_NONE), m_displayROI(nullptr), m_animator(CharacterAnimatorConfig{/*.saveEmoteTransform=*/true}), m_showNameBubble(true), - m_orbitYaw(DEFAULT_ORBIT_YAW), m_orbitPitch(DEFAULT_ORBIT_PITCH), m_orbitDistance(DEFAULT_ORBIT_DISTANCE), - m_touch{} + m_orbitPitch(DEFAULT_ORBIT_PITCH), m_orbitDistance(DEFAULT_ORBIT_DISTANCE), + m_absoluteYaw(DEFAULT_ORBIT_YAW), m_smoothedSpeed(0.0f), m_touch{} { SDL_memset(m_displayUniqueName, 0, sizeof(m_displayUniqueName)); } @@ -212,9 +215,14 @@ void ThirdPersonCamera::Tick(float p_deltaTime) // After a world transition, PlaceActor has now run and set the ROI to // the correct position. Clear the flag so subsequent OnActorEnter calls - // work normally. ApplyOrbitCamera below handles the camera setup. + // work normally. Initialize absolute yaw from the player's actual + // direction so the camera starts behind the character. if (m_pendingWorldTransition) { m_pendingWorldTransition = false; + LegoPathActor* actor = UserActor(); + if (actor && actor->GetROI()) { + InitAbsoluteYaw(actor->GetROI()); + } } // While a cam anim locks the player (actor state c_disabled), calling @@ -393,6 +401,22 @@ void ThirdPersonCamera::OnWorldDisabled(LegoWorld* p_world) m_animator.ClearAll(); } +float ThirdPersonCamera::GetLocalYaw(LegoROI* p_roi) const +{ + if (p_roi) { + const float* dir = p_roi->GetWorldDirection(); + float playerWorldYaw = SDL_atan2f(-dir[0], dir[2]); + return m_absoluteYaw - playerWorldYaw; + } + return m_absoluteYaw; +} + +void ThirdPersonCamera::InitAbsoluteYaw(LegoROI* p_roi) +{ + const float* dir = p_roi->GetWorldDirection(); + m_absoluteYaw = SDL_atan2f(-dir[0], dir[2]) + DEFAULT_ORBIT_YAW; +} + void ThirdPersonCamera::SetupCamera(LegoPathActor* p_actor) { LegoWorld* world = CurrentWorld(); @@ -400,9 +424,18 @@ void ThirdPersonCamera::SetupCamera(LegoPathActor* p_actor) return; } - Mx3DPointFloat at, dir, up; - ComputeOrbitVectors(at, dir, up); - world->GetCameraController()->SetWorldTransform(at, dir, up); + LegoROI* roi = p_actor->GetROI(); + if (roi) { + InitAbsoluteYaw(roi); + } + m_smoothedSpeed = 0.0f; + + // InitAbsoluteYaw sets m_absoluteYaw = playerYaw + DEFAULT_ORBIT_YAW, + // so localYaw = m_absoluteYaw - playerYaw = DEFAULT_ORBIT_YAW. + Mx3DPointFloat at, camDir, up; + ComputeOrbitVectors(DEFAULT_ORBIT_YAW, at, camDir, up); + + world->GetCameraController()->SetWorldTransform(at, camDir, up); p_actor->TransformPointOfView(); } @@ -489,15 +522,20 @@ void ThirdPersonCamera::SetNameBubbleVisible(bool p_visible) m_animator.SetNameBubbleVisible(p_visible); } -void ThirdPersonCamera::ComputeOrbitVectors(Mx3DPointFloat& p_at, Mx3DPointFloat& p_dir, Mx3DPointFloat& p_up) const +void ThirdPersonCamera::ComputeOrbitVectors( + float p_yaw, + Mx3DPointFloat& p_at, + Mx3DPointFloat& p_dir, + Mx3DPointFloat& p_up +) const { // Convert spherical coordinates to camera offset in entity-local space. // The ROI uses forward-z (Z+ = visual forward). The camera orbits // behind the character, so at yaw=0 it sits at local -Z. float cosP = SDL_cosf(m_orbitPitch); float sinP = SDL_sinf(m_orbitPitch); - float sinY = SDL_sinf(m_orbitYaw); - float cosY = SDL_cosf(m_orbitYaw); + float sinY = SDL_sinf(p_yaw); + float cosY = SDL_cosf(p_yaw); p_at = Mx3DPointFloat( m_orbitDistance * sinY * cosP, @@ -519,17 +557,23 @@ void ThirdPersonCamera::ApplyOrbitCamera() return; } - Mx3DPointFloat at, dir, up; - ComputeOrbitVectors(at, dir, up); - world->GetCameraController()->SetWorldTransform(at, dir, up); + // Derive entity-local yaw from absolute yaw and player's world facing. + // This prevents the camera from rotating when the player turns. + float localYaw = GetLocalYaw(actor->GetROI()); + + Mx3DPointFloat at, camDir, up; + ComputeOrbitVectors(localYaw, at, camDir, up); + + world->GetCameraController()->SetWorldTransform(at, camDir, up); actor->TransformPointOfView(); } void ThirdPersonCamera::ResetOrbitState() { - m_orbitYaw = DEFAULT_ORBIT_YAW; m_orbitPitch = DEFAULT_ORBIT_PITCH; m_orbitDistance = DEFAULT_ORBIT_DISTANCE; + m_absoluteYaw = DEFAULT_ORBIT_YAW; + m_smoothedSpeed = 0.0f; m_touch = {}; } @@ -553,6 +597,146 @@ void ThirdPersonCamera::ClampDistance() } } +MxBool ThirdPersonCamera::HandleCameraRelativeMovement( + LegoNavController* p_nav, + const Vector3& p_curPos, + const Vector3& p_curDir, + Vector3& p_newPos, + Vector3& p_newDir, + float p_deltaTime +) +{ + // Read keyboard state + LegoInputManager* inputManager = InputManager(); + MxU32 keyFlags = 0; + if (!inputManager || inputManager->GetNavigationKeyStates(keyFlags) == FAILURE) { + keyFlags = 0; + } + + // Compute camera world-forward and right from absolute yaw + float camForwardX = -SDL_sinf(m_absoluteYaw); + float camForwardZ = SDL_cosf(m_absoluteYaw); + float camRightX = SDL_cosf(m_absoluteYaw); + float camRightZ = SDL_sinf(m_absoluteYaw); + + // Map key flags to combined movement direction + float moveDirX = 0.0f; + float moveDirZ = 0.0f; + + if (keyFlags & LegoInputManager::c_up) { + moveDirX += camForwardX; + moveDirZ += camForwardZ; + } + if (keyFlags & LegoInputManager::c_down) { + moveDirX -= camForwardX; + moveDirZ -= camForwardZ; + } + if (keyFlags & LegoInputManager::c_left) { + moveDirX -= camRightX; + moveDirZ -= camRightZ; + } + if (keyFlags & LegoInputManager::c_right) { + moveDirX += camRightX; + moveDirZ += camRightZ; + } + + // Normalize movement direction + float moveDirLen = SDL_sqrtf(moveDirX * moveDirX + moveDirZ * moveDirZ); + bool hasInput = moveDirLen > 0.001f; + if (hasInput) { + moveDirX /= moveDirLen; + moveDirZ /= moveDirLen; + } + + // Smooth speed using acceleration/deceleration (mirroring nav controller's model) + float maxSpeed = p_nav->m_maxLinearVel; + if (hasInput) { + float accel = p_nav->m_maxLinearAccel; + m_smoothedSpeed += accel * p_deltaTime; + if (m_smoothedSpeed > maxSpeed) { + m_smoothedSpeed = maxSpeed; + } + } + else { + float decel = p_nav->m_maxLinearDeccel; + m_smoothedSpeed -= decel * p_deltaTime; + if (m_smoothedSpeed < 0.0f) { + m_smoothedSpeed = 0.0f; + } + } + + if (m_smoothedSpeed < p_nav->m_zeroThreshold && !hasInput) { + m_smoothedSpeed = 0.0f; + // No movement, keep current position and direction + p_newPos = p_curPos; + p_newDir = p_curDir; + } + else { + // Compute new position. Include p_curDir[1] (slope from boundary + // orientation) so the actor follows terrain height changes. + float speed = m_smoothedSpeed * p_deltaTime; + if (hasInput) { + p_newPos[0] = p_curPos[0] + moveDirX * speed; + p_newPos[1] = p_curPos[1] + p_curDir[1] * speed; + p_newPos[2] = p_curPos[2] + moveDirZ * speed; + + // Smooth turn: interpolate facing toward movement direction + float targetYaw = SDL_atan2f(-moveDirX, moveDirZ); + float currentYaw = SDL_atan2f(-p_curDir[0], p_curDir[2]); + float angleDiff = targetYaw - currentYaw; + + // Wrap to [-PI, PI] + while (angleDiff > SDL_PI_F) { + angleDiff -= 2.0f * SDL_PI_F; + } + while (angleDiff < -SDL_PI_F) { + angleDiff += 2.0f * SDL_PI_F; + } + + float maxTurn = TURN_RATE * p_deltaTime; + if (SDL_fabsf(angleDiff) > maxTurn) { + angleDiff = angleDiff > 0 ? maxTurn : -maxTurn; + } + + float newYaw = currentYaw + angleDiff; + p_newDir[0] = -SDL_sinf(newYaw); + p_newDir[1] = p_curDir[1]; + p_newDir[2] = SDL_cosf(newYaw); + } + else { + // Decelerating: continue in current direction + p_newPos[0] = p_curPos[0] + p_curDir[0] * speed; + p_newPos[1] = p_curPos[1] + p_curDir[1] * speed; + p_newPos[2] = p_curPos[2] + p_curDir[2] * speed; + p_newDir = p_curDir; + } + } + + // Set nav controller velocities via friend access so GetWorldSpeed() + // reports correctly for animations/network + p_nav->m_linearVel = m_smoothedSpeed; + // Suppress camera roll in Animate() + p_nav->m_rotationalVel = 0.0f; + + // Pre-set camera controller's local transform for the NEW player direction. + // TransformPointOfView() runs after this hook returns but before Tick()'s + // ApplyOrbitCamera(). Without this, the stale local transform (computed for + // the old facing) composes with the new actor transform, causing a one-frame + // camera flash in the wrong direction. + LegoWorld* world = CurrentWorld(); + if (world && world->GetCameraController()) { + float newPlayerYaw = SDL_atan2f(-p_newDir[0], p_newDir[2]); + float localYaw = m_absoluteYaw - newPlayerYaw; + + Mx3DPointFloat at, camDir, camUp; + ComputeOrbitVectors(localYaw, at, camDir, camUp); + + world->GetCameraController()->SetWorldTransform(at, camDir, camUp); + } + + return TRUE; +} + void ThirdPersonCamera::HandleSDLEvent(SDL_Event* p_event) { switch (p_event->type) { @@ -563,12 +747,21 @@ void ThirdPersonCamera::HandleSDLEvent(SDL_Event* p_event) case SDL_EVENT_MOUSE_MOTION: if (p_event->motion.state & SDL_BUTTON_RMASK) { - m_orbitYaw -= p_event->motion.xrel * 0.005f; + m_absoluteYaw -= p_event->motion.xrel * 0.005f; m_orbitPitch += p_event->motion.yrel * 0.005f; ClampPitch(); } break; + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: { + SDL_Window* window = SDL_GetWindowFromID(p_event->button.windowID); + if (window) { + SDL_SetWindowRelativeMouseMode(window, SDL_GetMouseState(NULL, NULL) & SDL_BUTTON_RMASK); + } + break; + } + case SDL_EVENT_FINGER_DOWN: { if (m_touch.count < 2) { int idx = m_touch.count; @@ -621,7 +814,7 @@ void ThirdPersonCamera::HandleSDLEvent(SDL_Event* p_event) // Two-finger drag for orbit float moveX = m_touch.x[idx] - oldX; float moveY = m_touch.y[idx] - oldY; - m_orbitYaw += moveX * 2.0f; + m_absoluteYaw += moveX * 2.0f; m_orbitPitch += moveY * 2.0f; ClampPitch(); }