From 99c871ab1620c73fd91fb1dbe52371a2eec26281 Mon Sep 17 00:00:00 2001 From: foxtacles Date: Mon, 23 Mar 2026 15:46:16 -0700 Subject: [PATCH] Nick bricks memories (#21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP: Add animation completion protocol message for Nick Brick's Memories Add MSG_ANIM_COMPLETE (15) protocol message broadcast by the host on natural animation completion. Contains a 64-bit random event ID, the SI object ID, and per-participant data (charIndex, displayName). - BroadcastAnimComplete: gathers participants from session slots, resolves spectator characters from display actors, generates event ID - HandleAnimComplete: filters observers (only participants get callback), builds JSON with eventId/objectId/participants for frontend - OnAnimationCompleted callback in PlatformCallbacks, implemented for Emscripten (CustomEvent dispatch) and Native (SDL_Log) - GetDisplayName() getter on RemotePlayer Co-Authored-By: Claude Opus 4.6 (1M context) * Fix participant ordering in animation completion message Emit the local player first in the JSON participants array so the frontend can rely on participants[0] being self when reporting to the server. Extract participant JSON-building into a lambda to avoid duplication. Retain null-termination safety for displayName. Co-Authored-By: Claude Opus 4.6 (1M context) * Disable workers.dev and preview URLs for relay server Co-Authored-By: Claude Opus 4.6 (1M context) * Fix path actor assertion failure / freeze on repeated vehicle enter/exit When the third-person camera is active, LMB triggers both forward movement and vehicle interaction. This leaves the previous actor with m_worldSpeed > 0 when entering a vehicle, causing it to wander on the path system in non-user-nav mode. On exit, SetBoundary() overwrites m_boundary without updating m_destEdge, creating a boundary/edge mismatch. On the next vehicle enter, the stale spline finishes and SwitchBoundary asserts (debug) or loops infinitely (release). Stop the previous actor from wandering by zeroing its world speed on enter. Co-Authored-By: Claude Opus 4.6 (1M context) * Add hold threshold to disambiguate LMB click from hold-to-walk A 300ms time threshold prevents brief clicks (for interacting with world objects) from also triggering forward movement in third-person camera mode. Co-Authored-By: Claude Opus 4.6 (1M context) * Add missing SDL_timer.h include for SDL_GetTicks Co-Authored-By: Claude Opus 4.6 (1M context) * Remove maxActors setting from multiplayer — NPCs are always disabled The maxActors room setting added unnecessary complexity for a feature that should always be off in multiplayer. Remove it from the relay server protocol, room configuration API, C++ client, and simplify NPC enforcement to be unconditional. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- LEGO1/lego/legoomni/include/islepathactor.h | 3 + .../extensions/multiplayer/networkmanager.h | 3 +- .../multiplayer/platformcallbacks.h | 5 + .../platforms/emscripten/callbacks.h | 1 + .../platforms/native/nativecallbacks.h | 1 + .../include/extensions/multiplayer/protocol.h | 17 ++ .../extensions/multiplayer/remoteplayer.h | 2 + .../thirdpersoncamera/inputhandler.h | 3 + extensions/src/multiplayer/networkmanager.cpp | 175 +++++++++++++++--- .../platforms/emscripten/callbacks.cpp | 14 ++ .../platforms/native/nativecallbacks.cpp | 5 + extensions/src/multiplayer/server/gameroom.ts | 13 +- extensions/src/multiplayer/server/protocol.ts | 7 +- .../src/multiplayer/server/wrangler.toml | 2 + .../src/thirdpersoncamera/controller.cpp | 10 +- .../src/thirdpersoncamera/inputhandler.cpp | 10 +- 16 files changed, 228 insertions(+), 43 deletions(-) diff --git a/LEGO1/lego/legoomni/include/islepathactor.h b/LEGO1/lego/legoomni/include/islepathactor.h index d1966b9c..df747516 100644 --- a/LEGO1/lego/legoomni/include/islepathactor.h +++ b/LEGO1/lego/legoomni/include/islepathactor.h @@ -1,6 +1,7 @@ #ifndef ISLEPATHACTOR_H #define ISLEPATHACTOR_H +#include "extensions/fwd.h" #include "legogamestate.h" #include "legopathactor.h" #include "mxtypes.h" @@ -139,6 +140,8 @@ class IslePathActor : public LegoPathActor { // IslePathActor::`scalar deleting destructor' protected: + friend class Extensions::ThirdPersonCamera::Controller; + LegoWorld* m_world; // 0x154 LegoPathActor* m_previousActor; // 0x158 MxFloat m_previousVel; // 0x15c diff --git a/extensions/include/extensions/multiplayer/networkmanager.h b/extensions/include/extensions/multiplayer/networkmanager.h index 3d3ad76f..523a065a 100644 --- a/extensions/include/extensions/multiplayer/networkmanager.h +++ b/extensions/include/extensions/multiplayer/networkmanager.h @@ -140,6 +140,8 @@ class NetworkManager : public MxCore { void BroadcastAnimUpdate(uint16_t p_animIndex); void SendAnimUpdateToPlayer(uint16_t p_animIndex, uint32_t p_targetPeerId); void BroadcastAnimStart(uint16_t p_animIndex); + void BroadcastAnimComplete(uint16_t p_animIndex); + void HandleAnimComplete(const AnimCompleteMsg& p_msg); int16_t GetPeerLocation(uint32_t p_peerId) const; bool GetPeerPosition(uint32_t p_peerId, float& p_x, float& p_z) const; bool IsPeerNearby(uint32_t p_peerId, float p_refX, float p_refZ) const; @@ -191,7 +193,6 @@ class NetworkManager : public MxCore { std::atomic m_pendingAnimInterest; std::atomic m_pendingAnimCancel; - bool m_disableAllNPCs; bool m_showNameBubbles; bool m_lastCameraEnabled; bool m_wasInRestrictedArea; diff --git a/extensions/include/extensions/multiplayer/platformcallbacks.h b/extensions/include/extensions/multiplayer/platformcallbacks.h index da14359e..18cf610c 100644 --- a/extensions/include/extensions/multiplayer/platformcallbacks.h +++ b/extensions/include/extensions/multiplayer/platformcallbacks.h @@ -33,6 +33,11 @@ class PlatformCallbacks { // Called when animation eligibility state changes (location change, player join/leave, etc.). // p_json = JSON payload with location, coordinator state, and per-animation slot fill status. virtual void OnAnimationsAvailable(const char* p_json) = 0; + + // Called when an animation completes successfully (natural completion, not cancellation). + // Only fired for actual participants, not observers. + // p_json = JSON with eventId, animIndex, and participant details (charIndex, displayName). + virtual void OnAnimationCompleted(const char* p_json) = 0; }; } // namespace Multiplayer diff --git a/extensions/include/extensions/multiplayer/platforms/emscripten/callbacks.h b/extensions/include/extensions/multiplayer/platforms/emscripten/callbacks.h index 4e95ecd0..cb387f13 100644 --- a/extensions/include/extensions/multiplayer/platforms/emscripten/callbacks.h +++ b/extensions/include/extensions/multiplayer/platforms/emscripten/callbacks.h @@ -15,6 +15,7 @@ class EmscriptenCallbacks : public PlatformCallbacks { void OnAllowCustomizeChanged(bool p_enabled) override; void OnConnectionStatusChanged(int p_status) override; void OnAnimationsAvailable(const char* p_json) override; + void OnAnimationCompleted(const char* p_json) override; }; } // namespace Multiplayer diff --git a/extensions/include/extensions/multiplayer/platforms/native/nativecallbacks.h b/extensions/include/extensions/multiplayer/platforms/native/nativecallbacks.h index 7049ac02..1a429a9a 100644 --- a/extensions/include/extensions/multiplayer/platforms/native/nativecallbacks.h +++ b/extensions/include/extensions/multiplayer/platforms/native/nativecallbacks.h @@ -15,6 +15,7 @@ class NativeCallbacks : public PlatformCallbacks { void OnAllowCustomizeChanged(bool p_enabled) override; void OnConnectionStatusChanged(int p_status) override; void OnAnimationsAvailable(const char* p_json) override; + void OnAnimationCompleted(const char* p_json) override; }; } // namespace Multiplayer diff --git a/extensions/include/extensions/multiplayer/protocol.h b/extensions/include/extensions/multiplayer/protocol.h index 737057a5..a6e8403c 100644 --- a/extensions/include/extensions/multiplayer/protocol.h +++ b/extensions/include/extensions/multiplayer/protocol.h @@ -29,6 +29,7 @@ enum MessageType : uint8_t { MSG_ANIM_CANCEL = 12, MSG_ANIM_UPDATE = 13, MSG_ANIM_START = 14, + MSG_ANIM_COMPLETE = 15, MSG_ASSIGN_ID = 0xFF }; @@ -187,6 +188,22 @@ struct AnimStartMsg { uint16_t animIndex; }; +// Per-participant data in AnimCompleteMsg +struct AnimCompletionParticipant { + uint32_t peerId; + int8_t charIndex; // Participant's character (g_characters index) + char displayName[8]; // 7 chars + null +}; + +// Host -> All: animation completed successfully (natural completion only, not cancellation) +struct AnimCompleteMsg { + MessageHeader header; + uint64_t eventId; // Random 64-bit ID unique to this completion event + uint32_t objectId; // SI file object ID (stable, used as frontend key) + uint8_t participantCount; + AnimCompletionParticipant participants[8]; +}; + #pragma pack(pop) using Extensions::Common::IsValidActorId; diff --git a/extensions/include/extensions/multiplayer/remoteplayer.h b/extensions/include/extensions/multiplayer/remoteplayer.h index 4c0fe269..d364072a 100644 --- a/extensions/include/extensions/multiplayer/remoteplayer.h +++ b/extensions/include/extensions/multiplayer/remoteplayer.h @@ -54,6 +54,8 @@ class RemotePlayer { bool IsMoving() const { return m_animator.IsInVehicle() || m_targetSpeed > 0.01f; } bool IsInMultiPartEmote() const { return m_animator.IsInMultiPartEmote(); } + const char* GetDisplayName() const { return m_displayName; } + void SetAnimationLocked(bool p_locked) { m_animationLocked = p_locked; } bool IsAnimationLocked() const { return m_animationLocked; } diff --git a/extensions/include/extensions/thirdpersoncamera/inputhandler.h b/extensions/include/extensions/thirdpersoncamera/inputhandler.h index 7b4e07d7..36784ebb 100644 --- a/extensions/include/extensions/thirdpersoncamera/inputhandler.h +++ b/extensions/include/extensions/thirdpersoncamera/inputhandler.h @@ -22,6 +22,7 @@ class InputHandler { SDL_FingerID GetFingerID(int p_idx) const { return m_touch.id[p_idx]; } bool IsLeftButtonHeld() const { return m_leftButtonHeld; } + bool IsLmbHeldForMovement() const; bool ConsumeAutoDisable(); bool ConsumeAutoEnable(); @@ -31,6 +32,7 @@ class InputHandler { static constexpr float CAMERA_ZONE_X = 0.5f; static constexpr float PINCH_TRANSITION_THRESHOLD = 0.03f; + static constexpr Uint64 LMB_HOLD_THRESHOLD_MS = 300; private: struct TouchState { @@ -46,6 +48,7 @@ class InputHandler { bool m_wantsAutoEnable; bool m_rightButtonHeld; bool m_leftButtonHeld; + Uint64 m_leftButtonDownTime; float m_savedMouseX; float m_savedMouseY; }; diff --git a/extensions/src/multiplayer/networkmanager.cpp b/extensions/src/multiplayer/networkmanager.cpp index e1b9f4d1..34c5774b 100644 --- a/extensions/src/multiplayer/networkmanager.cpp +++ b/extensions/src/multiplayer/networkmanager.cpp @@ -68,10 +68,10 @@ NetworkManager::NetworkManager() m_inIsleWorld(false), m_registered(false), m_pendingToggleThirdPerson(false), m_pendingToggleNameBubbles(false), m_pendingWalkAnim(-1), m_pendingIdleAnim(-1), m_pendingEmote(-1), m_pendingToggleAllowCustomize(false), m_pendingAnimInterest(-1), m_pendingAnimCancel(false), m_localPendingAnimInterest(-1), - m_playingAnimIndex(Animation::ANIM_INDEX_NONE), m_disableAllNPCs(false), m_showNameBubbles(true), - m_lastCameraEnabled(false), m_wasInRestrictedArea(false), m_animStateDirty(false), m_animInterestDirty(false), - m_lastAnimPushTime(0), m_connectionState(STATE_DISCONNECTED), m_wasRejected(false), m_reconnectAttempt(0), - m_reconnectDelay(0), m_nextReconnectTime(0) + m_playingAnimIndex(Animation::ANIM_INDEX_NONE), m_showNameBubbles(true), m_lastCameraEnabled(false), + m_wasInRestrictedArea(false), m_animStateDirty(false), m_animInterestDirty(false), m_lastAnimPushTime(0), + m_connectionState(STATE_DISCONNECTED), m_wasRejected(false), m_reconnectAttempt(0), m_reconnectDelay(0), + m_nextReconnectTime(0) { } @@ -90,9 +90,7 @@ MxResult NetworkManager::Tickle() ProcessPendingRequests(); CheckConnectionState(); - if (m_disableAllNPCs) { - EnforceDisableNPCs(); - } + EnforceDisableNPCs(); // Detect camera state changes for platform notification ThirdPersonCamera::Controller* cam = GetCamera(); @@ -354,10 +352,7 @@ void NetworkManager::OnWorldEnabled(LegoWorld* p_world) } NotifyPlayerCountChanged(); - - if (m_disableAllNPCs) { - EnforceDisableNPCs(); - } + EnforceDisableNPCs(); // Refresh animation catalog from the animation manager if (AnimationManager()) { @@ -752,21 +747,13 @@ void NetworkManager::ProcessIncomingPackets() m_localPeerId = assignedId; m_worldSync.SetLocalPeerId(assignedId); m_animCoordinator.SetLocalPeerId(assignedId); - } - if (length >= 6) { - uint8_t maxActors = data[5]; - if (maxActors <= 40) { - LegoAnimationManager::configureLegoAnimationManager(maxActors); - if (AnimationManager()) { - AnimationManager()->m_maxAllowedExtras = maxActors; - AnimationManager()->m_numAllowedExtras = - SDL_min(AnimationManager()->m_numAllowedExtras, (MxU32) maxActors); - } - m_disableAllNPCs = (maxActors == 0); - if (m_disableAllNPCs) { - EnforceDisableNPCs(); - } + + LegoAnimationManager::configureLegoAnimationManager(0); + if (AnimationManager()) { + AnimationManager()->m_maxAllowedExtras = 0; + AnimationManager()->m_numAllowedExtras = 0; } + EnforceDisableNPCs(); } break; } @@ -858,6 +845,13 @@ void NetworkManager::ProcessIncomingPackets() } break; } + case MSG_ANIM_COMPLETE: { + AnimCompleteMsg msg; + if (DeserializeMsg(data, length, msg) && msg.header.type == MSG_ANIM_COMPLETE) { + HandleAnimComplete(msg); + } + break; + } default: break; } @@ -1224,6 +1218,7 @@ void NetworkManager::TickAnimation() } if (IsHost() && m_playingAnimIndex != Animation::ANIM_INDEX_NONE) { + BroadcastAnimComplete(m_playingAnimIndex); // Must fire before EraseSession destroys participant data m_animSessionHost.EraseSession(m_playingAnimIndex); BroadcastAnimUpdate(m_playingAnimIndex); // Broadcast cleared state } @@ -1579,6 +1574,136 @@ void NetworkManager::BroadcastAnimStart(uint16_t p_animIndex) m_animCoordinator.ApplyAnimStart(p_animIndex); } +void NetworkManager::BroadcastAnimComplete(uint16_t p_animIndex) +{ + const Animation::AnimSession* session = m_animSessionHost.FindSession(p_animIndex); + if (!session) { + return; + } + + const AnimInfo* animInfo = m_animCatalog.GetAnimInfo(p_animIndex); + if (!animInfo) { + return; + } + + AnimCompleteMsg msg{}; + msg.header = {MSG_ANIM_COMPLETE, m_localPeerId, m_sequence++, TARGET_BROADCAST}; + msg.eventId = (static_cast(SDL_rand_bits()) << 32) | static_cast(SDL_rand_bits()); + msg.objectId = animInfo->m_objectId; + msg.participantCount = 0; + + char localName[8]; + EncodeUsername(localName); + + for (const auto& slot : session->slots) { + if (slot.peerId == 0 || msg.participantCount >= 8) { + continue; + } + + AnimCompletionParticipant& p = msg.participants[msg.participantCount]; + p.peerId = slot.peerId; + + if (slot.IsSpectator()) { + // Resolve spectator's actual character from their display actor + if (slot.peerId == m_localPeerId) { + ThirdPersonCamera::Controller* cam = GetCamera(); + p.charIndex = cam ? Animation::Catalog::DisplayActorToCharacterIndex(cam->GetDisplayActorIndex()) : -1; + } + else { + auto it = m_remotePlayers.find(slot.peerId); + p.charIndex = it != m_remotePlayers.end() + ? Animation::Catalog::DisplayActorToCharacterIndex(it->second->GetDisplayActorIndex()) + : -1; + } + } + else { + p.charIndex = slot.charIndex; + } + + if (slot.peerId == m_localPeerId) { + SDL_memcpy(p.displayName, localName, sizeof(p.displayName)); + } + else { + auto it = m_remotePlayers.find(slot.peerId); + if (it != m_remotePlayers.end()) { + SDL_memcpy(p.displayName, it->second->GetDisplayName(), sizeof(p.displayName)); + } + else { + p.displayName[0] = '\0'; + } + } + + msg.participantCount++; + } + + SendMessage(msg); + + // Also handle locally on the host (message sent to TARGET_BROADCAST excludes sender) + HandleAnimComplete(msg); +} + +void NetworkManager::HandleAnimComplete(const AnimCompleteMsg& p_msg) +{ + // Only fire callback for actual participants, not observers + int localIdx = -1; + for (uint8_t i = 0; i < p_msg.participantCount; i++) { + if (p_msg.participants[i].peerId == m_localPeerId) { + localIdx = i; + break; + } + } + + if (localIdx < 0 || !m_callbacks) { + return; + } + + // Build JSON for frontend + char eventIdHex[17]; + SDL_snprintf( + eventIdHex, + sizeof(eventIdHex), + "%08x%08x", + static_cast(p_msg.eventId >> 32), + static_cast(p_msg.eventId & 0xFFFFFFFF) + ); + + std::string json = "{\"eventId\":\""; + json += eventIdHex; + json += "\",\"objectId\":"; + json += std::to_string(p_msg.objectId); + json += ",\"participants\":["; + + // Emit local player first so frontend can rely on participants[0] being self + bool first = true; + auto appendParticipant = [&](uint8_t i) { + if (!first) { + json += ','; + } + first = false; + const AnimCompletionParticipant& p = p_msg.participants[i]; + // Ensure null-termination safety for displayName (protocol uses fixed char[8]) + char name[8]; + SDL_memcpy(name, p.displayName, sizeof(name)); + name[7] = '\0'; + json += "{\"charIndex\":"; + json += std::to_string(static_cast(p.charIndex)); + json += ",\"displayName\":\""; + json += name; + json += "\"}"; + }; + + appendParticipant(static_cast(localIdx)); + for (uint8_t i = 0; i < p_msg.participantCount; i++) { + if (i != static_cast(localIdx)) { + appendParticipant(i); + } + } + + json += "]}"; + + m_callbacks->OnAnimationCompleted(json.c_str()); +} + int16_t NetworkManager::GetPeerLocation(uint32_t p_peerId) const { if (p_peerId == m_localPeerId) { diff --git a/extensions/src/multiplayer/platforms/emscripten/callbacks.cpp b/extensions/src/multiplayer/platforms/emscripten/callbacks.cpp index 6ef1d5f6..c6ffb182 100644 --- a/extensions/src/multiplayer/platforms/emscripten/callbacks.cpp +++ b/extensions/src/multiplayer/platforms/emscripten/callbacks.cpp @@ -78,6 +78,20 @@ void EmscriptenCallbacks::OnAnimationsAvailable(const char* p_json) // clang-format on } +void EmscriptenCallbacks::OnAnimationCompleted(const char* p_json) +{ + // clang-format off + MAIN_THREAD_EM_ASM({ + var canvas = Module.canvas; + if (canvas) { + canvas.dispatchEvent(new CustomEvent('animationCompleted', { + detail: { json: UTF8ToString($0) } + })); + } + }, p_json); + // clang-format on +} + } // namespace Multiplayer #endif // __EMSCRIPTEN__ diff --git a/extensions/src/multiplayer/platforms/native/nativecallbacks.cpp b/extensions/src/multiplayer/platforms/native/nativecallbacks.cpp index 6f834b46..c1e35627 100644 --- a/extensions/src/multiplayer/platforms/native/nativecallbacks.cpp +++ b/extensions/src/multiplayer/platforms/native/nativecallbacks.cpp @@ -57,6 +57,11 @@ void NativeCallbacks::OnAnimationsAvailable(const char* p_json) (void) p_json; } +void NativeCallbacks::OnAnimationCompleted(const char* p_json) +{ + SDL_Log("[Multiplayer] Animation completed: %s", p_json); +} + } // namespace Multiplayer #endif // !__EMSCRIPTEN__ diff --git a/extensions/src/multiplayer/server/gameroom.ts b/extensions/src/multiplayer/server/gameroom.ts index 1ef2e50d..c0f0d01f 100644 --- a/extensions/src/multiplayer/server/gameroom.ts +++ b/extensions/src/multiplayer/server/gameroom.ts @@ -23,7 +23,6 @@ export class GameRoom implements DurableObject { private nextPeerId = 1; private hostPeerId = 0; private maxPlayers = 5; - private maxActors = 0; constructor( private state: DurableObjectState, @@ -52,7 +51,7 @@ export class GameRoom implements DurableObject { server.accept(); this.connections.set(peerId, server); - server.send(createAssignIdMsg(peerId, this.maxActors)); + server.send(createAssignIdMsg(peerId)); this.assignHostIfNeeded(peerId, server); server.addEventListener("message", (event) => @@ -77,7 +76,6 @@ export class GameRoom implements DurableObject { try { const body = (await request.json()) as { maxPlayers?: number; - maxActors?: number; }; const ceiling = this.env.MAX_PLAYERS_CEILING ? Number(this.env.MAX_PLAYERS_CEILING) @@ -88,17 +86,11 @@ export class GameRoom implements DurableObject { Math.min(body.maxPlayers, ceiling) ); } - if (body.maxActors !== undefined) { - this.maxActors = Math.max( - 0, - Math.min(body.maxActors, 40) - ); - } } catch { // Ignore parse errors, keep defaults } return new Response( - JSON.stringify({ maxPlayers: this.maxPlayers, maxActors: this.maxActors }), + JSON.stringify({ maxPlayers: this.maxPlayers }), { headers: { "Content-Type": "application/json", @@ -113,7 +105,6 @@ export class GameRoom implements DurableObject { JSON.stringify({ players: this.connections.size, maxPlayers: this.maxPlayers, - maxActors: this.maxActors, }), { headers: { diff --git a/extensions/src/multiplayer/server/protocol.ts b/extensions/src/multiplayer/server/protocol.ts index ba87dcac..33c89638 100644 --- a/extensions/src/multiplayer/server/protocol.ts +++ b/extensions/src/multiplayer/server/protocol.ts @@ -16,18 +16,17 @@ export const MSG_LEAVE = 2; export const MSG_HOST_ASSIGN = 4; export const MSG_ASSIGN_ID = 0xff; -// AssignIdMsg: compact server-only message — type(1) + peerId(4) + maxActors(1) -const ASSIGN_ID_SIZE = 1 + 4 + 1; +// AssignIdMsg: compact server-only message — type(1) + peerId(4) +const ASSIGN_ID_SIZE = 1 + 4; // HostAssignMsg: header(13) + hostPeerId(4) const HOST_ASSIGN_SIZE = HEADER_SIZE + 4; -export function createAssignIdMsg(peerId: number, maxActors: number): ArrayBuffer { +export function createAssignIdMsg(peerId: number): ArrayBuffer { const buf = new ArrayBuffer(ASSIGN_ID_SIZE); const view = new DataView(buf); view.setUint8(0, MSG_ASSIGN_ID); view.setUint32(1, peerId, true); - view.setUint8(5, maxActors); return buf; } diff --git a/extensions/src/multiplayer/server/wrangler.toml b/extensions/src/multiplayer/server/wrangler.toml index 7ed1a726..f421e611 100644 --- a/extensions/src/multiplayer/server/wrangler.toml +++ b/extensions/src/multiplayer/server/wrangler.toml @@ -1,6 +1,8 @@ name = "isle-relay" main = "relay.ts" compatibility_date = "2024-01-01" +workers_dev = false +preview_urls = false [durable_objects] bindings = [ diff --git a/extensions/src/thirdpersoncamera/controller.cpp b/extensions/src/thirdpersoncamera/controller.cpp index ad44a164..1e715c95 100644 --- a/extensions/src/thirdpersoncamera/controller.cpp +++ b/extensions/src/thirdpersoncamera/controller.cpp @@ -87,6 +87,14 @@ void Controller::OnActorEnter(IslePathActor* p_actor) return; } + // Prevent the previous actor from wandering on the path system with stale + // spline state while the player is in a vehicle. Exit() will later call + // SetBoundary() without updating m_destEdge, so any non-user-nav animation + // with the old spline would use a mismatched boundary/edge pair. + if (p_actor->m_previousActor) { + p_actor->m_previousActor->SetWorldSpeed(0); + } + m_animator.SetCurrentVehicleType(DetectVehicleType(userActor)); if (!m_enabled || IsRestrictedArea(GameState()->m_currentArea)) { @@ -392,7 +400,7 @@ MxBool Controller::HandleCameraRelativeMovement( p_newDir, p_deltaTime, m_animator.IsInMultiPartEmote() || m_animPlaying, - m_input.IsLeftButtonHeld() + m_input.IsLmbHeldForMovement() ); } diff --git a/extensions/src/thirdpersoncamera/inputhandler.cpp b/extensions/src/thirdpersoncamera/inputhandler.cpp index 376c04bc..be179620 100644 --- a/extensions/src/thirdpersoncamera/inputhandler.cpp +++ b/extensions/src/thirdpersoncamera/inputhandler.cpp @@ -3,13 +3,14 @@ #include "extensions/thirdpersoncamera/orbitcamera.h" #include +#include #include using namespace Extensions::ThirdPersonCamera; InputHandler::InputHandler() : m_touch{}, m_wantsAutoDisable(false), m_wantsAutoEnable(false), m_rightButtonHeld(false), - m_leftButtonHeld(false), m_savedMouseX(0.0f), m_savedMouseY(0.0f) + m_leftButtonHeld(false), m_leftButtonDownTime(0), m_savedMouseX(0.0f), m_savedMouseY(0.0f) { } @@ -75,6 +76,12 @@ bool InputHandler::ConsumeAutoEnable() return std::exchange(m_wantsAutoEnable, false); } +bool InputHandler::IsLmbHeldForMovement() const +{ + return m_leftButtonHeld && m_leftButtonDownTime > 0 && + (SDL_GetTicks() - m_leftButtonDownTime) >= LMB_HOLD_THRESHOLD_MS; +} + void InputHandler::SuppressGestures() { m_touch.synced[0] = false; @@ -132,6 +139,7 @@ void InputHandler::HandleSDLEvent(SDL_Event* p_event, OrbitCamera& p_orbit, bool } else if (p_event->button.button == SDL_BUTTON_LEFT) { m_leftButtonHeld = p_event->button.down; + m_leftButtonDownTime = p_event->button.down ? SDL_GetTicks() : 0; } break; }