From 569c8b467bf954f0cb3bdefe4b435d48b594751f Mon Sep 17 00:00:00 2001 From: foxtacles Date: Fri, 13 Mar 2026 15:12:07 -0700 Subject: [PATCH] Separate extensions (#18) * WIP * WIP * Make camera the single source of truth for broadcast state Remove redundant local copies of walkAnimId, idleAnimId, and displayActorIndex from NetworkManager. BroadcastLocalState now reads these from the camera's Controller, eliminating dual-copy sync issues. Additional cleanup: - Early-return on null cam in SendEmote/HandleCustomize for clarity - Only consume camera-dependent pending requests when cam is available - Move local name bubble creation from BroadcastLocalState to Tickle - Remove dead NetworkManager::SetDisplayActorIndex method Co-Authored-By: Claude Opus 4.6 * Fix clang format --------- Co-authored-by: Claude Opus 4.6 --- CMakeLists.txt | 24 +- ISLE/emscripten/filesystem.cpp | 12 +- ISLE/isleapp.cpp | 3 +- .../legoomni/include/legocharactermanager.h | 7 +- .../lego/legoomni/include/legonavcontroller.h | 9 +- .../legoomni/src/actors/islepathactor.cpp | 6 +- .../src/common/legoanimmmpresenter.cpp | 4 +- .../src/common/legocharactermanager.cpp | 4 +- .../legoomni/src/common/legogamestate.cpp | 4 +- .../legoomni/src/common/legotextureinfo.cpp | 2 +- LEGO1/lego/legoomni/src/common/legoutils.cpp | 9 +- LEGO1/lego/legoomni/src/entity/legoentity.cpp | 2 +- .../legoomni/src/entity/legonavcontroller.cpp | 5 +- LEGO1/lego/legoomni/src/entity/legoworld.cpp | 11 +- .../legoomni/src/input/legoinputmanager.cpp | 8 +- LEGO1/lego/legoomni/src/main/legomain.cpp | 12 +- LEGO1/lego/legoomni/src/worlds/infocenter.cpp | 5 +- LEGO1/lego/legoomni/src/worlds/isle.cpp | 5 +- .../omni/src/notify/mxnotificationmanager.cpp | 4 +- .../{multiplayer => common}/animdata.h | 9 +- .../{multiplayer => common}/animutils.h | 19 +- .../characteranimator.h | 32 +- .../{multiplayer => common}/charactercloner.h | 21 +- .../charactercustomizer.h | 12 +- .../include/extensions/common/constants.h | 42 + .../{multiplayer => common}/customizestate.h | 7 +- extensions/include/extensions/extensions.h | 2 +- extensions/include/extensions/multiplayer.h | 64 +- .../extensions/multiplayer/networkmanager.h | 19 +- .../include/extensions/multiplayer/protocol.h | 49 +- .../extensions/multiplayer/remoteplayer.h | 14 +- .../multiplayer/thirdpersoncamera.h | 151 --- extensions/include/extensions/siloader.h | 50 +- extensions/include/extensions/textureloader.h | 13 +- .../include/extensions/thirdpersoncamera.h | 91 ++ .../extensions/thirdpersoncamera/controller.h | 109 ++ .../thirdpersoncamera/displayactor.h | 47 + .../thirdpersoncamera/inputhandler.h | 42 + .../thirdpersoncamera/orbitcamera.h | 69 ++ .../src/{multiplayer => common}/animdata.cpp | 9 +- .../src/{multiplayer => common}/animutils.cpp | 4 +- .../characteranimator.cpp | 43 +- .../charactercloner.cpp | 4 +- .../charactercustomizer.cpp | 51 +- .../customizestate.cpp | 4 +- extensions/src/extensions.cpp | 18 +- extensions/src/multiplayer.cpp | 209 +--- extensions/src/multiplayer/networkmanager.cpp | 235 ++-- extensions/src/multiplayer/remoteplayer.cpp | 44 +- extensions/src/multiplayer/server/gameroom.ts | 10 - .../src/multiplayer/thirdpersoncamera.cpp | 1052 ----------------- extensions/src/multiplayer/worldstatesync.cpp | 6 - extensions/src/siloader.cpp | 53 +- extensions/src/textureloader.cpp | 18 +- extensions/src/thirdpersoncamera.cpp | 241 ++++ .../src/thirdpersoncamera/controller.cpp | 429 +++++++ .../src/thirdpersoncamera/displayactor.cpp | 90 ++ .../src/thirdpersoncamera/inputhandler.cpp | 210 ++++ .../src/thirdpersoncamera/orbitcamera.cpp | 287 +++++ 59 files changed, 2202 insertions(+), 1813 deletions(-) rename extensions/include/extensions/{multiplayer => common}/animdata.h (91%) rename extensions/include/extensions/{multiplayer => common}/animutils.h (78%) rename extensions/include/extensions/{multiplayer => common}/characteranimator.h (87%) rename extensions/include/extensions/{multiplayer => common}/charactercloner.h (54%) rename extensions/include/extensions/{multiplayer => common}/charactercustomizer.h (78%) create mode 100644 extensions/include/extensions/common/constants.h rename extensions/include/extensions/{multiplayer => common}/customizestate.h (87%) delete mode 100644 extensions/include/extensions/multiplayer/thirdpersoncamera.h create mode 100644 extensions/include/extensions/thirdpersoncamera.h create mode 100644 extensions/include/extensions/thirdpersoncamera/controller.h create mode 100644 extensions/include/extensions/thirdpersoncamera/displayactor.h create mode 100644 extensions/include/extensions/thirdpersoncamera/inputhandler.h create mode 100644 extensions/include/extensions/thirdpersoncamera/orbitcamera.h rename extensions/src/{multiplayer => common}/animdata.cpp (95%) rename extensions/src/{multiplayer => common}/animutils.cpp (97%) rename extensions/src/{multiplayer => common}/characteranimator.cpp (93%) rename extensions/src/{multiplayer => common}/charactercloner.cpp (98%) rename extensions/src/{multiplayer => common}/charactercustomizer.cpp (87%) rename extensions/src/{multiplayer => common}/customizestate.cpp (97%) delete mode 100644 extensions/src/multiplayer/thirdpersoncamera.cpp create mode 100644 extensions/src/thirdpersoncamera.cpp create mode 100644 extensions/src/thirdpersoncamera/controller.cpp create mode 100644 extensions/src/thirdpersoncamera/displayactor.cpp create mode 100644 extensions/src/thirdpersoncamera/inputhandler.cpp create mode 100644 extensions/src/thirdpersoncamera/orbitcamera.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a4b62a77..05dbacf7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -531,18 +531,28 @@ if (ISLE_EXTENSIONS) extensions/src/extensions.cpp extensions/src/siloader.cpp extensions/src/textureloader.cpp + + # Common shared code + extensions/src/common/animdata.cpp + extensions/src/common/animutils.cpp + extensions/src/common/characteranimator.cpp + extensions/src/common/charactercloner.cpp + extensions/src/common/charactercustomizer.cpp + extensions/src/common/customizestate.cpp + + # Third person camera extension + extensions/src/thirdpersoncamera.cpp + extensions/src/thirdpersoncamera/controller.cpp + extensions/src/thirdpersoncamera/orbitcamera.cpp + extensions/src/thirdpersoncamera/inputhandler.cpp + extensions/src/thirdpersoncamera/displayactor.cpp + + # Multiplayer extension extensions/src/multiplayer.cpp - extensions/src/multiplayer/animdata.cpp - extensions/src/multiplayer/animutils.cpp - extensions/src/multiplayer/characteranimator.cpp - extensions/src/multiplayer/charactercloner.cpp - extensions/src/multiplayer/charactercustomizer.cpp - extensions/src/multiplayer/customizestate.cpp extensions/src/multiplayer/namebubblerenderer.cpp extensions/src/multiplayer/networkmanager.cpp extensions/src/multiplayer/protocol.cpp extensions/src/multiplayer/remoteplayer.cpp - extensions/src/multiplayer/thirdpersoncamera.cpp extensions/src/multiplayer/worldstatesync.cpp ) if(EMSCRIPTEN) diff --git a/ISLE/emscripten/filesystem.cpp b/ISLE/emscripten/filesystem.cpp index e5644608..e643ac48 100644 --- a/ISLE/emscripten/filesystem.cpp +++ b/ISLE/emscripten/filesystem.cpp @@ -89,10 +89,10 @@ void Emscripten_SetupFilesystem() } #ifdef EXTENSIONS - if (Extensions::TextureLoader::enabled) { + if (Extensions::TextureLoaderExt::enabled) { MxString directory = - MxString("/LEGO") + Extensions::TextureLoader::options["texture loader:texture path"].c_str(); - Extensions::TextureLoader::options["texture loader:texture path"] = directory.GetData(); + MxString("/LEGO") + Extensions::TextureLoaderExt::options["texture loader:texture path"].c_str(); + Extensions::TextureLoaderExt::options["texture loader:texture path"] = directory.GetData(); wasmfs_create_directory(directory.GetData(), 0644, fetchfs); MxU32 i = 0; @@ -102,17 +102,17 @@ void Emscripten_SetupFilesystem() registerFile(path.GetData()); if (!preloadFile(path.GetData())) { - Extensions::TextureLoader::excludedFiles.emplace_back(file); + Extensions::TextureLoaderExt::AddExcludedFile(file); } Emscripten_SendExtensionProgress("HD Textures", (++i * 100) / sizeOfArray(g_textures)); } } - if (Extensions::SiLoader::enabled) { + if (Extensions::SiLoaderExt::enabled) { wasmfs_create_directory("/LEGO/extra", 0644, fetchfs); - for (const auto& file : Extensions::SiLoader::files) { + for (const auto& file : Extensions::SiLoaderExt::GetFiles()) { registerFile(file.c_str()); } } diff --git a/ISLE/isleapp.cpp b/ISLE/isleapp.cpp index 4b3c0aed..d8239451 100644 --- a/ISLE/isleapp.cpp +++ b/ISLE/isleapp.cpp @@ -38,6 +38,7 @@ #include #include +#include #include #include #include @@ -878,7 +879,7 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) } #ifdef EXTENSIONS - Extensions::HandleMultiplayerSDLEvent(event); + Extensions::ThirdPersonCameraExt::HandleSDLEvent(event); #endif return SDL_APP_CONTINUE; diff --git a/LEGO1/lego/legoomni/include/legocharactermanager.h b/LEGO1/lego/legoomni/include/legocharactermanager.h index 174a7747..7f247fc0 100644 --- a/LEGO1/lego/legoomni/include/legocharactermanager.h +++ b/LEGO1/lego/legoomni/include/legocharactermanager.h @@ -13,10 +13,13 @@ class LegoActor; class LegoExtraActor; class LegoStorage; class LegoROI; -namespace Multiplayer +namespace Extensions +{ +namespace Common { class CharacterCloner; } +} // namespace Extensions #pragma warning(disable : 4237) @@ -102,7 +105,7 @@ class LegoCharacterManager { static const char* GetCustomizeAnimFile() { return g_customizeAnimFile; } private: - friend class Multiplayer::CharacterCloner; + friend class Extensions::Common::CharacterCloner; LegoROI* CreateActorROI(const char* p_key); void RemoveROI(LegoROI* p_roi); diff --git a/LEGO1/lego/legoomni/include/legonavcontroller.h b/LEGO1/lego/legoomni/include/legonavcontroller.h index 42a5ff0c..ab87ac6f 100644 --- a/LEGO1/lego/legoomni/include/legonavcontroller.h +++ b/LEGO1/lego/legoomni/include/legonavcontroller.h @@ -7,10 +7,13 @@ struct LegoLocation; class Vector3; -namespace Multiplayer +namespace Extensions { -class ThirdPersonCamera; +namespace ThirdPersonCamera +{ +class OrbitCamera; } +} // namespace Extensions ////////////////////////////////////////////////////////////////////////////// // @@ -126,7 +129,7 @@ class LegoNavController : public MxCore { // LegoNavController::`scalar deleting destructor' protected: - friend class Multiplayer::ThirdPersonCamera; + friend class Extensions::ThirdPersonCamera::OrbitCamera; 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); diff --git a/LEGO1/lego/legoomni/src/actors/islepathactor.cpp b/LEGO1/lego/legoomni/src/actors/islepathactor.cpp index 2d200ee4..9b3b890b 100644 --- a/LEGO1/lego/legoomni/src/actors/islepathactor.cpp +++ b/LEGO1/lego/legoomni/src/actors/islepathactor.cpp @@ -1,7 +1,7 @@ #include "islepathactor.h" #include "3dmanager/lego3dmanager.h" -#include "extensions/multiplayer.h" +#include "extensions/thirdpersoncamera.h" #include "isle_actions.h" #include "jukebox_actions.h" #include "legoanimationmanager.h" @@ -99,7 +99,7 @@ void IslePathActor::Enter() TransformPointOfView(); } - Extension::Call(HandleActorEnter, this); + Extension::Call(TP::HandleActorEnter, this); } // FUNCTION: LEGO1 0x1001a3f0 @@ -160,7 +160,7 @@ void IslePathActor::Exit() TransformPointOfView(); ResetViewVelocity(); - Extension::Call(HandleActorExit, this); + Extension::Call(TP::HandleActorExit, this); } // GLOBAL: LEGO1 0x10102b28 diff --git a/LEGO1/lego/legoomni/src/common/legoanimmmpresenter.cpp b/LEGO1/lego/legoomni/src/common/legoanimmmpresenter.cpp index b3bf1aa7..b4397aaf 100644 --- a/LEGO1/lego/legoomni/src/common/legoanimmmpresenter.cpp +++ b/LEGO1/lego/legoomni/src/common/legoanimmmpresenter.cpp @@ -3,7 +3,7 @@ #include "3dmanager/lego3dmanager.h" #include "decomp.h" #include "define.h" -#include "extensions/multiplayer.h" +#include "extensions/thirdpersoncamera.h" #include "islepathactor.h" #include "legoanimationmanager.h" #include "legoanimpresenter.h" @@ -485,7 +485,7 @@ MxBool LegoAnimMMPresenter::FUN_1004b6d0(MxLong p_time) actor->SetActorState(LegoPathActor::c_initial); if (m_tranInfo->m_unk0x29) { - Extension::Call(HandleCamAnimEnd, actor); + Extension::Call(TP::HandleCamAnimEnd, actor); } } diff --git a/LEGO1/lego/legoomni/src/common/legocharactermanager.cpp b/LEGO1/lego/legoomni/src/common/legocharactermanager.cpp index 93effa24..aa9facbe 100644 --- a/LEGO1/lego/legoomni/src/common/legocharactermanager.cpp +++ b/LEGO1/lego/legoomni/src/common/legocharactermanager.cpp @@ -2,6 +2,7 @@ #include "3dmanager/lego3dmanager.h" #include "extensions/multiplayer.h" +#include "extensions/thirdpersoncamera.h" #include "legoactors.h" #include "legoanimactor.h" #include "legobuildingmanager.h" @@ -283,7 +284,8 @@ LegoROI* LegoCharacterManager::GetActorROI(const char* p_name, MxBool p_createEn if (character != NULL) { if (p_createEntity && character->m_roi->GetEntity() == NULL && - !Extension::Call(IsClonedCharacter, p_name).value_or(FALSE)) { + !Extension::Call(TP::IsClonedCharacter, p_name).value_or(FALSE) && + !Extension::Call(MP::IsClonedCharacter, p_name).value_or(FALSE)) { LegoExtraActor* actor = new LegoExtraActor(); actor->SetROI(character->m_roi, FALSE, FALSE); diff --git a/LEGO1/lego/legoomni/src/common/legogamestate.cpp b/LEGO1/lego/legoomni/src/common/legogamestate.cpp index fca681cc..13f43356 100644 --- a/LEGO1/lego/legoomni/src/common/legogamestate.cpp +++ b/LEGO1/lego/legoomni/src/common/legogamestate.cpp @@ -367,7 +367,7 @@ MxResult LegoGameState::DeleteState() // FUNCTION: BETA10 0x10084329 MxResult LegoGameState::Load(MxULong p_slot) { - Extension::Call(HandleBeforeSaveLoad); + Extension::Call(MP::HandleBeforeSaveLoad); MxResult result = FAILURE; LegoFile storage; @@ -461,7 +461,7 @@ MxResult LegoGameState::Load(MxULong p_slot) result = SUCCESS; m_isDirty = FALSE; - Extension::Call(HandleSaveLoaded); + Extension::Call(MP::HandleSaveLoaded); done: if (result != SUCCESS) { diff --git a/LEGO1/lego/legoomni/src/common/legotextureinfo.cpp b/LEGO1/lego/legoomni/src/common/legotextureinfo.cpp index a0be25b7..d273b2d2 100644 --- a/LEGO1/lego/legoomni/src/common/legotextureinfo.cpp +++ b/LEGO1/lego/legoomni/src/common/legotextureinfo.cpp @@ -59,7 +59,7 @@ LegoTextureInfo* LegoTextureInfo::Create(const char* p_name, LegoTexture* p_text strcpy(textureInfo->m_name, p_name); } - if (Extension::Call(PatchTexture, textureInfo).value_or(false)) { + if (Extension::Call(TL::PatchTexture, textureInfo).value_or(false)) { return textureInfo; } diff --git a/LEGO1/lego/legoomni/src/common/legoutils.cpp b/LEGO1/lego/legoomni/src/common/legoutils.cpp index f8ef1bab..9d765e46 100644 --- a/LEGO1/lego/legoomni/src/common/legoutils.cpp +++ b/LEGO1/lego/legoomni/src/common/legoutils.cpp @@ -503,8 +503,8 @@ MxBool RemoveFromCurrentWorld(const MxAtomId& p_atomId, MxS32 p_id) { LegoWorld* world = CurrentWorld(); - auto result = - Extension::Call(HandleRemove, SiLoader::StreamObject{p_atomId, p_id}, world).value_or(std::nullopt); + auto result = Extension::Call(SI::HandleRemove, SiLoaderExt::StreamObject{p_atomId, p_id}, world) + .value_or(std::nullopt); if (result) { return result.value(); } @@ -543,8 +543,9 @@ MxBool RemoveFromWorld( { LegoWorld* world = FindWorld(p_worldAtom, p_worldEntityId); - auto result = Extension::Call(HandleRemove, SiLoader::StreamObject{p_entityAtom, p_entityId}, world) - .value_or(std::nullopt); + auto result = + Extension::Call(SI::HandleRemove, SiLoaderExt::StreamObject{p_entityAtom, p_entityId}, world) + .value_or(std::nullopt); if (result) { return result.value(); } diff --git a/LEGO1/lego/legoomni/src/entity/legoentity.cpp b/LEGO1/lego/legoomni/src/entity/legoentity.cpp index f1f06034..0d1ea295 100644 --- a/LEGO1/lego/legoomni/src/entity/legoentity.cpp +++ b/LEGO1/lego/legoomni/src/entity/legoentity.cpp @@ -486,7 +486,7 @@ MxLong LegoEntity::Notify(MxParam& p_param) InvokeAction(m_actionType, MxAtomId(m_siFile, e_lowerCase2), m_targetEntityId, this); } else { - if (Extension::Call(HandleEntityNotify, this).value_or(FALSE)) { + if (Extension::Call(MP::HandleEntityNotify, this).value_or(FALSE)) { return 1; } diff --git a/LEGO1/lego/legoomni/src/entity/legonavcontroller.cpp b/LEGO1/lego/legoomni/src/entity/legonavcontroller.cpp index 4c82d541..50c8ac4b 100644 --- a/LEGO1/lego/legoomni/src/entity/legonavcontroller.cpp +++ b/LEGO1/lego/legoomni/src/entity/legonavcontroller.cpp @@ -2,7 +2,7 @@ #include "3dmanager/lego3dmanager.h" #include "act3.h" -#include "extensions/multiplayer.h" +#include "extensions/thirdpersoncamera.h" #include "infocenter.h" #include "legoanimationmanager.h" #include "legocameracontroller.h" @@ -351,7 +351,8 @@ MxBool LegoNavController::CalculateNewPosDir( ProcessJoystickInput(rotatedY); } - if (Extension::Call(HandleNavOverride, this, p_curPos, p_curDir, p_newPos, p_newDir, deltaTime) + if (Extension< + ThirdPersonCameraExt>::Call(TP::HandleNavOverride, this, p_curPos, p_curDir, p_newPos, p_newDir, deltaTime) .value_or(FALSE)) { return TRUE; } diff --git a/LEGO1/lego/legoomni/src/entity/legoworld.cpp b/LEGO1/lego/legoomni/src/entity/legoworld.cpp index 4c756466..ab141f38 100644 --- a/LEGO1/lego/legoomni/src/entity/legoworld.cpp +++ b/LEGO1/lego/legoomni/src/entity/legoworld.cpp @@ -3,6 +3,7 @@ #include "anim/legoanim.h" #include "extensions/multiplayer.h" #include "extensions/siloader.h" +#include "extensions/thirdpersoncamera.h" #include "legoanimationmanager.h" #include "legoanimpresenter.h" #include "legobuildingmanager.h" @@ -640,8 +641,8 @@ MxCore* LegoWorld::Find(const char* p_class, const char* p_name) // FUNCTION: BETA10 0x100db3de MxCore* LegoWorld::Find(const MxAtomId& p_atom, MxS32 p_entityId) { - auto result = - Extension::Call(HandleFind, SiLoader::StreamObject{p_atom, p_entityId}, this).value_or(std::nullopt); + auto result = Extension::Call(SI::HandleFind, SiLoaderExt::StreamObject{p_atom, p_entityId}, this) + .value_or(std::nullopt); if (result) { return result.value(); } @@ -755,7 +756,8 @@ void LegoWorld::Enable(MxBool p_enable) SetIsWorldActive(TRUE); #endif - Extension::Call(HandleWorldEnable, this, TRUE); + Extension::Call(TP::HandleWorldEnable, this, TRUE); + Extension::Call(MP::HandleWorldEnable, this, TRUE); } else if (!p_enable && m_disabledObjects.size() == 0) { MxPresenter* presenter; @@ -819,7 +821,8 @@ void LegoWorld::Enable(MxBool p_enable) GetViewManager()->RemoveAll(NULL); - Extension::Call(HandleWorldEnable, this, FALSE); + Extension::Call(TP::HandleWorldEnable, this, FALSE); + Extension::Call(MP::HandleWorldEnable, this, FALSE); } } diff --git a/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp b/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp index 1a1dfc8f..a3b3f076 100644 --- a/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp +++ b/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp @@ -1,6 +1,7 @@ #include "legoinputmanager.h" #include "extensions/multiplayer.h" +#include "extensions/thirdpersoncamera.h" #include "legocameracontroller.h" #include "legocontrolmanager.h" #include "legomain.h" @@ -325,7 +326,7 @@ MxBool LegoInputManager::ProcessOneEvent(LegoEventNotificationParam& p_param) if (!Lego()->IsPaused()) { if ((p_param.GetModifier() & LegoEventNotificationParam::c_rButtonState) && !(p_param.GetModifier() & LegoEventNotificationParam::c_lButtonState) && - Extension::Call(IsThirdPersonCameraActive).value_or(FALSE)) { + Extension::Call(TP::IsThirdPersonCameraActive).value_or(FALSE)) { return FALSE; } @@ -402,7 +403,8 @@ MxBool LegoInputManager::ProcessOneEvent(LegoEventNotificationParam& p_param) if (entity && entity->Notify(p_param) != 0) { return TRUE; } - if (Extension::Call(HandleROIClick, roi, p_param).value_or(FALSE)) { + if (Extension::Call(MP::HandleROIClick, roi, p_param).value_or(FALSE) || + Extension::Call(TP::HandleROIClick, roi, p_param).value_or(FALSE)) { return TRUE; } } @@ -638,7 +640,7 @@ void LegoInputManager::RemoveJoystick(SDL_JoystickID p_joystickID) MxBool LegoInputManager::HandleTouchEvent(SDL_Event* p_event, TouchScheme p_touchScheme) { - if (Extension::Call(HandleTouchInput, p_event).value_or(FALSE)) { + if (Extension::Call(TP::HandleTouchInput, p_event).value_or(FALSE)) { return FALSE; } diff --git a/LEGO1/lego/legoomni/src/main/legomain.cpp b/LEGO1/lego/legoomni/src/main/legomain.cpp index ad5eca5f..14c3b5be 100644 --- a/LEGO1/lego/legoomni/src/main/legomain.cpp +++ b/LEGO1/lego/legoomni/src/main/legomain.cpp @@ -3,6 +3,7 @@ #include "3dmanager/lego3dmanager.h" #include "extensions/multiplayer.h" #include "extensions/siloader.h" +#include "extensions/thirdpersoncamera.h" #include "islepathactor.h" #include "legoanimationmanager.h" #include "legobuildingmanager.h" @@ -356,7 +357,8 @@ MxResult LegoOmni::Create(MxOmniCreateParam& p_param) m_gameState->SetCurrentAct(LegoGameState::e_act1); #endif - Extension::Call(HandleCreate); + Extension::Call(TP::HandleCreate); + Extension::Call(MP::HandleCreate); result = SUCCESS; } else { @@ -416,7 +418,7 @@ void LegoOmni::AddWorld(LegoWorld* p_world) { m_worldList->Append(p_world); - Extension::Call(HandleWorld, p_world); + Extension::Call(SI::HandleWorld, p_world); } // FUNCTION: LEGO1 0x1005adb0 @@ -484,7 +486,7 @@ LegoWorld* LegoOmni::FindWorld(const MxAtomId& p_atom, MxS32 p_entityid) // STUB: BETA10 0x1008e93e void LegoOmni::DeleteObject(MxDSAction& p_dsAction) { - auto result = Extension::Call(HandleDelete, p_dsAction).value_or(std::nullopt); + auto result = Extension::Call(SI::HandleDelete, p_dsAction).value_or(std::nullopt); if (result && result.value()) { return; } @@ -679,7 +681,7 @@ void LegoOmni::CreateBackgroundAudio() MxResult LegoOmni::Start(MxDSAction* p_dsAction) { { - auto result = Extension::Call(HandleStart, *p_dsAction).value_or(std::nullopt); + auto result = Extension::Call(SI::HandleStart, *p_dsAction).value_or(std::nullopt); if (result) { return result.value(); } @@ -742,5 +744,5 @@ void LegoOmni::Resume() void LegoOmni::LoadSiLoader() { - Extension::Call(Load); + Extension::Call(SI::Load); } diff --git a/LEGO1/lego/legoomni/src/worlds/infocenter.cpp b/LEGO1/lego/legoomni/src/worlds/infocenter.cpp index 8884551b..bebf667d 100644 --- a/LEGO1/lego/legoomni/src/worlds/infocenter.cpp +++ b/LEGO1/lego/legoomni/src/worlds/infocenter.cpp @@ -342,8 +342,9 @@ MxLong Infocenter::HandleEndAction(MxEndActionNotificationParam& p_param) MxLong result = m_radio.Notify(p_param); - if (result || (action->GetAtomId() != m_atomId && action->GetAtomId() != *g_introScript && - !Extension::Call(ReplacedIn, *action, m_atomId, *g_introScript).value_or(std::nullopt))) { + if (result || + (action->GetAtomId() != m_atomId && action->GetAtomId() != *g_introScript && + !Extension::Call(SI::ReplacedIn, *action, m_atomId, *g_introScript).value_or(std::nullopt))) { return result; } diff --git a/LEGO1/lego/legoomni/src/worlds/isle.cpp b/LEGO1/lego/legoomni/src/worlds/isle.cpp index 3165f594..1c4fb2a7 100644 --- a/LEGO1/lego/legoomni/src/worlds/isle.cpp +++ b/LEGO1/lego/legoomni/src/worlds/isle.cpp @@ -217,7 +217,7 @@ MxLong Isle::HandleEndAction(MxEndActionNotificationParam& p_param) result = 1; } } - else if (auto replacedObject = Extension::Call(ReplacedIn, *p_param.GetAction(), *g_jukeboxScript).value_or(std::nullopt)) { + else if (auto replacedObject = Extension::Call(SI::ReplacedIn, *p_param.GetAction(), *g_jukeboxScript).value_or(std::nullopt)) { MxS32 script = replacedObject->second; if (script >= JukeboxScript::c_JBMusic1 && script <= JukeboxScript::c_JBMusic6) { @@ -297,7 +297,8 @@ void Isle::ReadyWorld() MxLong Isle::HandleControl(LegoControlManagerNotificationParam& p_param) { if (p_param.m_enabledChild == 1) { - if (Extension::Call(HandleSkyLightControl, (MxU32) p_param.m_clickedObjectId).value_or(FALSE)) { + if (Extension::Call(MP::HandleSkyLightControl, (MxU32) p_param.m_clickedObjectId) + .value_or(FALSE)) { return 1; } diff --git a/LEGO1/omni/src/notify/mxnotificationmanager.cpp b/LEGO1/omni/src/notify/mxnotificationmanager.cpp index 373c8aef..e6117788 100644 --- a/LEGO1/omni/src/notify/mxnotificationmanager.cpp +++ b/LEGO1/omni/src/notify/mxnotificationmanager.cpp @@ -110,7 +110,7 @@ MxResult MxNotificationManager::Tickle() m_sendList->pop_front(); if (notif->GetParam()->GetNotification() == c_notificationEndAction) { - Extension::Call(HandleEndAction, (MxEndActionNotificationParam&) *notif->GetParam()); + Extension::Call(SI::HandleEndAction, (MxEndActionNotificationParam&) *notif->GetParam()); } notif->GetTarget()->Notify(*notif->GetParam()); @@ -169,7 +169,7 @@ void MxNotificationManager::FlushPending(MxCore* p_listener) pending.pop_front(); if (notif->GetParam()->GetNotification() == c_notificationEndAction) { - Extension::Call(HandleEndAction, (MxEndActionNotificationParam&) *notif->GetParam()); + Extension::Call(SI::HandleEndAction, (MxEndActionNotificationParam&) *notif->GetParam()); } notif->GetTarget()->Notify(*notif->GetParam()); diff --git a/extensions/include/extensions/multiplayer/animdata.h b/extensions/include/extensions/common/animdata.h similarity index 91% rename from extensions/include/extensions/multiplayer/animdata.h rename to extensions/include/extensions/common/animdata.h index 5dd82a2c..803640d2 100644 --- a/extensions/include/extensions/multiplayer/animdata.h +++ b/extensions/include/extensions/common/animdata.h @@ -1,12 +1,14 @@ #pragma once -#include "extensions/multiplayer/protocol.h" +#include "extensions/common/constants.h" #include class LegoPathActor; -namespace Multiplayer +namespace Extensions +{ +namespace Common { // Animation and vehicle tables (defined in animdata.cpp) @@ -46,4 +48,5 @@ bool IsLargeVehicle(int8_t p_vehicleType); // Detect the vehicle type of a given actor, or VEHICLE_NONE if not a vehicle int8_t DetectVehicleType(LegoPathActor* p_actor); -} // namespace Multiplayer +} // namespace Common +} // namespace Extensions diff --git a/extensions/include/extensions/multiplayer/animutils.h b/extensions/include/extensions/common/animutils.h similarity index 78% rename from extensions/include/extensions/multiplayer/animutils.h rename to extensions/include/extensions/common/animutils.h index d0b335a0..2c815a5a 100644 --- a/extensions/include/extensions/multiplayer/animutils.h +++ b/extensions/include/extensions/common/animutils.h @@ -1,6 +1,8 @@ #pragma once #include "mxtypes.h" +#include "mxgeometry/mxmatrix.h" +#include "realtime/vector.h" #include "roi/legoroi.h" #include @@ -8,7 +10,9 @@ class LegoAnim; -namespace Multiplayer +namespace Extensions +{ +namespace Common { namespace AnimUtils @@ -77,6 +81,17 @@ inline void EnsureROIMapVisibility(LegoROI** p_roiMap, MxU32 p_roiMapSize) } } +// Flip a matrix from forward-z to backward-z (or vice versa) in place. +inline void FlipMatrixDirection(MxMatrix& p_mat) +{ + Vector3 right(p_mat[0]); + Vector3 up(p_mat[1]); + Vector3 direction(p_mat[2]); + direction *= -1.0f; + right.EqualsCross(up, direction); +} + } // namespace AnimUtils -} // namespace Multiplayer +} // namespace Common +} // namespace Extensions diff --git a/extensions/include/extensions/multiplayer/characteranimator.h b/extensions/include/extensions/common/characteranimator.h similarity index 87% rename from extensions/include/extensions/multiplayer/characteranimator.h rename to extensions/include/extensions/common/characteranimator.h index 97c7f56b..a07f8778 100644 --- a/extensions/include/extensions/multiplayer/characteranimator.h +++ b/extensions/include/extensions/common/characteranimator.h @@ -1,7 +1,7 @@ #pragma once -#include "extensions/multiplayer/animdata.h" -#include "extensions/multiplayer/animutils.h" +#include "extensions/common/animdata.h" +#include "extensions/common/animutils.h" #include "mxgeometry/mxmatrix.h" #include "mxtypes.h" @@ -14,21 +14,21 @@ class LegoCacheSound; class LegoROI; class LegoAnim; -namespace Multiplayer +namespace Extensions +{ +namespace Common { - -class NameBubbleRenderer; // Configuration for CharacterAnimator behavior that differs between consumers. struct CharacterAnimatorConfig { // When true, save/restore the parent ROI transform during emote playback - // to prevent scale accumulation (needed for ThirdPersonCamera's display clone). + // to prevent scale accumulation (needed for ThirdPersonCameraExt's display clone). bool saveEmoteTransform; }; -// Unified character animation component used by both RemotePlayer and ThirdPersonCamera. -// Handles walk/idle/emote animation playback, vehicle ride animations, click animation -// tracking, and name bubble management. +// Unified character animation component used by both RemotePlayer and ThirdPersonCameraExt. +// Handles walk/idle/emote animation playback, vehicle ride animations, and click animation +// tracking. class CharacterAnimator { public: explicit CharacterAnimator(const CharacterAnimatorConfig& p_config); @@ -72,13 +72,6 @@ class CharacterAnimator { void ClearAll(); void ApplyIdleFrame0(LegoROI* p_roi); - // Name bubble management - void CreateNameBubble(const char* p_name); - void DestroyNameBubble(); - void SetNameBubbleVisible(bool p_visible); - void UpdateNameBubble(LegoROI* p_roi); - NameBubbleRenderer* GetNameBubble() const { return m_nameBubble; } - // Emote state accessors bool IsEmoteActive() const { return m_emoteActive; } @@ -91,7 +84,7 @@ class CharacterAnimator { int8_t GetFrozenEmoteId() const { return m_frozenEmoteId; } void SetFrozenEmoteId(int8_t p_emoteId, LegoROI* p_roi); - // Animation time (needed for vehicle ride tick in ThirdPersonCamera) + // Animation time (needed for vehicle ride tick in ThirdPersonCameraExt) float GetAnimTime() const { return m_animTime; } void SetAnimTime(float p_time) { m_animTime = p_time; } void ResetAnimState(); @@ -146,8 +139,7 @@ class CharacterAnimator { LegoROI* m_rideVehicleROI; int8_t m_currentVehicleType; - - NameBubbleRenderer* m_nameBubble; }; -} // namespace Multiplayer +} // namespace Common +} // namespace Extensions diff --git a/extensions/include/extensions/multiplayer/charactercloner.h b/extensions/include/extensions/common/charactercloner.h similarity index 54% rename from extensions/include/extensions/multiplayer/charactercloner.h rename to extensions/include/extensions/common/charactercloner.h index 2d091980..88170b0f 100644 --- a/extensions/include/extensions/multiplayer/charactercloner.h +++ b/extensions/include/extensions/common/charactercloner.h @@ -1,12 +1,18 @@ #pragma once +#include "extensions/common/constants.h" #include "legoactors.h" #include "misc.h" +#include +#include + class LegoCharacterManager; class LegoROI; -namespace Multiplayer +namespace Extensions +{ +namespace Common { inline bool IsValidDisplayActorIndex(uint8_t p_index) @@ -14,6 +20,16 @@ inline bool IsValidDisplayActorIndex(uint8_t p_index) return p_index < sizeOfArray(g_actorInfoInit); } +inline uint8_t ResolveDisplayActorIndex(const char* p_name) +{ + for (int i = 0; i < static_cast(sizeOfArray(g_actorInfoInit)); i++) { + if (!SDL_strcasecmp(g_actorInfoInit[i].m_name, p_name)) { + return static_cast(i); + } + } + return DISPLAY_ACTOR_NONE; +} + class CharacterCloner { public: // Creates an independent multi-part character ROI clone. @@ -22,4 +38,5 @@ class CharacterCloner { static LegoROI* Clone(LegoCharacterManager* p_charMgr, const char* p_uniqueName, const char* p_characterType); }; -} // namespace Multiplayer +} // namespace Common +} // namespace Extensions diff --git a/extensions/include/extensions/multiplayer/charactercustomizer.h b/extensions/include/extensions/common/charactercustomizer.h similarity index 78% rename from extensions/include/extensions/multiplayer/charactercustomizer.h rename to extensions/include/extensions/common/charactercustomizer.h index ac4e0eb4..b8d0fa5d 100644 --- a/extensions/include/extensions/multiplayer/charactercustomizer.h +++ b/extensions/include/extensions/common/charactercustomizer.h @@ -6,7 +6,9 @@ class LegoROI; -namespace Multiplayer +namespace Extensions +{ +namespace Common { struct CustomizeState; @@ -31,10 +33,16 @@ class CharacterCustomizer { static MxU32 PlayClickAnimation(LegoROI* p_roi, const CustomizeState& p_state); static void StopClickAnimation(MxU32 p_objectId); + // Resolves the current actor's click to a change type and optional part index. + // Returns false if the click should be consumed with no effect (Pepper in act2/3, Brickster) + // or if the actor is unknown. + static bool ResolveClickChangeType(uint8_t& p_changeType, int& p_partIndex, LegoROI* p_clickedROI); + private: static LegoROI* FindChildROI(LegoROI* p_rootROI, const char* p_name); static void ApplyHatVariant(LegoROI* p_rootROI, uint8_t p_actorInfoIndex, const CustomizeState& p_state); }; -} // namespace Multiplayer +} // namespace Common +} // namespace Extensions diff --git a/extensions/include/extensions/common/constants.h b/extensions/include/extensions/common/constants.h new file mode 100644 index 00000000..b4a1b952 --- /dev/null +++ b/extensions/include/extensions/common/constants.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +namespace Extensions +{ +namespace Common +{ + +enum VehicleType : int8_t { + VEHICLE_NONE = -1, + VEHICLE_HELICOPTER = 0, + VEHICLE_JETSKI = 1, + VEHICLE_DUNEBUGGY = 2, + VEHICLE_BIKE = 3, + VEHICLE_SKATEBOARD = 4, + VEHICLE_MOTOCYCLE = 5, + VEHICLE_TOWTRACK = 6, + VEHICLE_AMBULANCE = 7, + VEHICLE_COUNT = 8 +}; + +// Change types for world events (maps to Switch* methods on LegoEntity) +enum WorldChangeType : uint8_t { + CHANGE_VARIANT = 0, + CHANGE_SOUND = 1, + CHANGE_MOVE = 2, + CHANGE_COLOR = 3, + CHANGE_MOOD = 4, + CHANGE_DECREMENT = 5 +}; + +static const uint8_t DISPLAY_ACTOR_NONE = 0xFF; + +// Validate actorId is a playable character (1-5, not brickster) +inline bool IsValidActorId(uint8_t p_actorId) +{ + return p_actorId >= 1 && p_actorId <= 5; +} + +} // namespace Common +} // namespace Extensions diff --git a/extensions/include/extensions/multiplayer/customizestate.h b/extensions/include/extensions/common/customizestate.h similarity index 87% rename from extensions/include/extensions/multiplayer/customizestate.h rename to extensions/include/extensions/common/customizestate.h index 0cc8c1e0..71f5d15b 100644 --- a/extensions/include/extensions/multiplayer/customizestate.h +++ b/extensions/include/extensions/common/customizestate.h @@ -2,7 +2,9 @@ #include -namespace Multiplayer +namespace Extensions +{ +namespace Common { struct CustomizeState { @@ -19,4 +21,5 @@ struct CustomizeState { bool operator!=(const CustomizeState& p_other) const { return !(*this == p_other); } }; -} // namespace Multiplayer +} // namespace Common +} // namespace Extensions diff --git a/extensions/include/extensions/extensions.h b/extensions/include/extensions/extensions.h index b46fe2a0..98b23af3 100644 --- a/extensions/include/extensions/extensions.h +++ b/extensions/include/extensions/extensions.h @@ -11,7 +11,7 @@ namespace Extensions { constexpr const char* availableExtensions[] = - {"extensions:texture loader", "extensions:si loader", "extensions:multiplayer"}; + {"extensions:texture loader", "extensions:si loader", "extensions:third person camera", "extensions:multiplayer"}; LEGO1_EXPORT void Enable(const char* p_key, std::map p_options); diff --git a/extensions/include/extensions/multiplayer.h b/extensions/include/extensions/multiplayer.h index ea1e0891..0d3078f5 100644 --- a/extensions/include/extensions/multiplayer.h +++ b/extensions/include/extensions/multiplayer.h @@ -3,18 +3,13 @@ #include "extensions/extensions.h" #include "mxtypes.h" -#include #include #include -class IslePathActor; class LegoEntity; class LegoEventNotificationParam; -class LegoNavController; -class LegoPathActor; class LegoROI; class LegoWorld; -class Vector3; namespace Multiplayer { @@ -41,55 +36,22 @@ class MultiplayerExt { static MxBool HandleSkyLightControl(MxU32 p_controlId); // Handles clicks on entity-less ROIs (remote players, display actor overrides). + // When multiplayer is enabled, all customization goes through the network. static MxBool HandleROIClick(LegoROI* p_rootROI, LegoEventNotificationParam& p_param); static std::map options; static bool enabled; - static std::string relayUrl; - static std::string room; - - static void HandleActorEnter(IslePathActor* p_actor); - static void HandleActorExit(IslePathActor* p_actor); - static void HandleCamAnimEnd(LegoPathActor* p_actor); - - // Returns TRUE if the name belongs to a multiplayer clone (entity-less ROI). static MxBool IsClonedCharacter(const char* p_name); - - // Called before a save file is loaded. Captures current sky/light state. static void HandleBeforeSaveLoad(); - - // Called after a save file is loaded. Re-syncs world state with multiplayer peers. static void HandleSaveLoaded(); - - // Returns true if the multiplayer connection was rejected (e.g. room full). static MxBool CheckRejected(); - // Forwards SDL events to the third-person camera for orbit controls. - static void HandleSDLEvent(SDL_Event* p_event); - - // Returns TRUE when the third-person camera is active. - static MxBool IsThirdPersonCameraActive(); - - // Routes touch events by screen zone: right half → camera, left half → movement. - // Returns TRUE if the event was consumed by the camera (caller should skip movement). - static MxBool HandleTouchInput(SDL_Event* p_event); - - // 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(); private: + static std::string s_relayUrl; + static std::string s_room; static Multiplayer::NetworkManager* s_networkManager; static Multiplayer::NetworkTransport* s_transport; static Multiplayer::PlatformCallbacks* s_callbacks; @@ -97,41 +59,31 @@ class MultiplayerExt { #ifdef EXTENSIONS LEGO1_EXPORT bool IsMultiplayerRejected(); -LEGO1_EXPORT void HandleMultiplayerSDLEvent(SDL_Event* p_event); +#endif +namespace MP +{ +#ifdef EXTENSIONS constexpr auto HandleCreate = &MultiplayerExt::HandleCreate; constexpr auto HandleWorldEnable = &MultiplayerExt::HandleWorldEnable; constexpr auto HandleEntityNotify = &MultiplayerExt::HandleEntityNotify; constexpr auto HandleSkyLightControl = &MultiplayerExt::HandleSkyLightControl; constexpr auto HandleROIClick = &MultiplayerExt::HandleROIClick; -constexpr auto HandleActorEnter = &MultiplayerExt::HandleActorEnter; -constexpr auto HandleActorExit = &MultiplayerExt::HandleActorExit; -constexpr auto HandleCamAnimEnd = &MultiplayerExt::HandleCamAnimEnd; constexpr auto IsClonedCharacter = &MultiplayerExt::IsClonedCharacter; constexpr auto HandleBeforeSaveLoad = &MultiplayerExt::HandleBeforeSaveLoad; constexpr auto HandleSaveLoaded = &MultiplayerExt::HandleSaveLoaded; 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; constexpr decltype(&MultiplayerExt::HandleEntityNotify) HandleEntityNotify = nullptr; constexpr decltype(&MultiplayerExt::HandleSkyLightControl) HandleSkyLightControl = nullptr; constexpr decltype(&MultiplayerExt::HandleROIClick) HandleROIClick = nullptr; -constexpr decltype(&MultiplayerExt::HandleActorEnter) HandleActorEnter = nullptr; -constexpr decltype(&MultiplayerExt::HandleActorExit) HandleActorExit = nullptr; -constexpr decltype(&MultiplayerExt::HandleCamAnimEnd) HandleCamAnimEnd = nullptr; constexpr decltype(&MultiplayerExt::IsClonedCharacter) IsClonedCharacter = nullptr; constexpr decltype(&MultiplayerExt::HandleBeforeSaveLoad) HandleBeforeSaveLoad = nullptr; constexpr decltype(&MultiplayerExt::HandleSaveLoaded) HandleSaveLoaded = nullptr; 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 MP }; // namespace Extensions diff --git a/extensions/include/extensions/multiplayer/networkmanager.h b/extensions/include/extensions/multiplayer/networkmanager.h index 42bf1b5e..c44a7382 100644 --- a/extensions/include/extensions/multiplayer/networkmanager.h +++ b/extensions/include/extensions/multiplayer/networkmanager.h @@ -4,7 +4,6 @@ #include "extensions/multiplayer/platformcallbacks.h" #include "extensions/multiplayer/protocol.h" #include "extensions/multiplayer/remoteplayer.h" -#include "extensions/multiplayer/thirdpersoncamera.h" #include "extensions/multiplayer/worldstatesync.h" #include "mxcore.h" #include "mxtypes.h" @@ -19,9 +18,16 @@ class LegoEntity; class LegoWorld; +namespace Extensions +{ +class ThirdPersonCameraExt; +} + namespace Multiplayer { +class NameBubbleRenderer; + class NetworkManager : public MxCore { public: NetworkManager(); @@ -48,7 +54,6 @@ class NetworkManager : public MxCore { void SetWalkAnimation(uint8_t p_walkAnimId); void SetIdleAnimation(uint8_t p_idleAnimId); void SendEmote(uint8_t p_emoteId); - void SetDisplayActorIndex(uint8_t p_displayActorIndex); // Thread-safe request methods for cross-thread callers (e.g. WASM exports // running on the browser main thread). Deferred to the game thread in Tickle(). @@ -77,8 +82,6 @@ class NetworkManager : public MxCore { void OnBeforeSaveLoad(); void OnSaveLoaded(); - ThirdPersonCamera& GetThirdPersonCamera() { return m_thirdPersonCamera; } - void NotifyThirdPersonChanged(bool p_enabled); void NotifyNameBubblesChanged(bool p_enabled); void NotifyAllowCustomizeChanged(bool p_enabled); @@ -107,7 +110,6 @@ class NetworkManager : public MxCore { void HandleEmote(const EmoteMsg& p_msg); void HandleCustomize(const CustomizeMsg& p_msg); - void DeriveDisplayActorIndex(uint8_t p_actorId); void ProcessPendingRequests(); void RemoveRemotePlayer(uint32_t p_peerId); void RemoveAllRemotePlayers(); @@ -121,7 +123,7 @@ class NetworkManager : public MxCore { NetworkTransport* m_transport; PlatformCallbacks* m_callbacks; WorldStateSync m_worldSync; - ThirdPersonCamera m_thirdPersonCamera; + NameBubbleRenderer* m_localNameBubble; std::map> m_remotePlayers; std::map m_roiToPlayer; @@ -130,10 +132,6 @@ class NetworkManager : public MxCore { uint32_t m_sequence; uint32_t m_lastBroadcastTime; uint8_t m_lastValidActorId; - uint8_t m_localWalkAnimId; - uint8_t m_localIdleAnimId; - uint8_t m_localDisplayActorIndex; - bool m_displayActorFrozen; bool m_localAllowRemoteCustomize; bool m_inIsleWorld; bool m_registered; @@ -146,6 +144,7 @@ class NetworkManager : public MxCore { std::atomic m_pendingToggleAllowCustomize; bool m_showNameBubbles; + bool m_lastCameraEnabled; static const uint32_t BROADCAST_INTERVAL_MS = 66; // ~15Hz static const uint32_t TIMEOUT_MS = 5000; // 5 second timeout diff --git a/extensions/include/extensions/multiplayer/protocol.h b/extensions/include/extensions/multiplayer/protocol.h index 4038e779..056e40f2 100644 --- a/extensions/include/extensions/multiplayer/protocol.h +++ b/extensions/include/extensions/multiplayer/protocol.h @@ -5,6 +5,8 @@ #include #include +#include "extensions/common/constants.h" + namespace Multiplayer { @@ -26,18 +28,17 @@ enum MessageType : uint8_t { MSG_ASSIGN_ID = 0xFF }; -enum VehicleType : int8_t { - VEHICLE_NONE = -1, - VEHICLE_HELICOPTER = 0, - VEHICLE_JETSKI = 1, - VEHICLE_DUNEBUGGY = 2, - VEHICLE_BIKE = 3, - VEHICLE_SKATEBOARD = 4, - VEHICLE_MOTOCYCLE = 5, - VEHICLE_TOWTRACK = 6, - VEHICLE_AMBULANCE = 7, - VEHICLE_COUNT = 8 -}; +using Extensions::Common::VehicleType; +using Extensions::Common::VEHICLE_NONE; +using Extensions::Common::VEHICLE_HELICOPTER; +using Extensions::Common::VEHICLE_JETSKI; +using Extensions::Common::VEHICLE_DUNEBUGGY; +using Extensions::Common::VEHICLE_BIKE; +using Extensions::Common::VEHICLE_SKATEBOARD; +using Extensions::Common::VEHICLE_MOTOCYCLE; +using Extensions::Common::VEHICLE_TOWTRACK; +using Extensions::Common::VEHICLE_AMBULANCE; +using Extensions::Common::VEHICLE_COUNT; // Entity types for world events enum WorldEntityType : uint8_t { @@ -47,15 +48,13 @@ enum WorldEntityType : uint8_t { ENTITY_LIGHT = 3 }; -// Change types for world events (maps to Switch* methods on LegoEntity) -enum WorldChangeType : uint8_t { - CHANGE_VARIANT = 0, - CHANGE_SOUND = 1, - CHANGE_MOVE = 2, - CHANGE_COLOR = 3, - CHANGE_MOOD = 4, - CHANGE_DECREMENT = 5 -}; +using Extensions::Common::WorldChangeType; +using Extensions::Common::CHANGE_VARIANT; +using Extensions::Common::CHANGE_SOUND; +using Extensions::Common::CHANGE_MOVE; +using Extensions::Common::CHANGE_COLOR; +using Extensions::Common::CHANGE_MOOD; +using Extensions::Common::CHANGE_DECREMENT; // Change types for ENTITY_SKY enum SkyChangeType : uint8_t { @@ -153,17 +152,13 @@ struct CustomizeMsg { #pragma pack(pop) -// Validate actorId is a playable character (1-5, not brickster) -inline bool IsValidActorId(uint8_t p_actorId) -{ - return p_actorId >= 1 && p_actorId <= 5; -} +using Extensions::Common::IsValidActorId; // Convert LegoGameState::Username letter indices (0-25 = A-Z) to ASCII. // Writes up to 7 characters + null terminator into p_out (must be at least 8 bytes). void EncodeUsername(char p_out[8]); -static const uint8_t DISPLAY_ACTOR_NONE = 0xFF; +using Extensions::Common::DISPLAY_ACTOR_NONE; // Parse the message type from a buffer. Returns MSG type or 0 on error. inline uint8_t ParseMessageType(const uint8_t* p_data, size_t p_length) diff --git a/extensions/include/extensions/multiplayer/remoteplayer.h b/extensions/include/extensions/multiplayer/remoteplayer.h index 8ceb7b40..d201a896 100644 --- a/extensions/include/extensions/multiplayer/remoteplayer.h +++ b/extensions/include/extensions/multiplayer/remoteplayer.h @@ -1,7 +1,7 @@ #pragma once -#include "extensions/multiplayer/characteranimator.h" -#include "extensions/multiplayer/customizestate.h" +#include "extensions/common/characteranimator.h" +#include "extensions/common/customizestate.h" #include "extensions/multiplayer/protocol.h" #include "mxtypes.h" @@ -14,6 +14,8 @@ class LegoWorld; namespace Multiplayer { +class NameBubbleRenderer; + class RemotePlayer { public: RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex); @@ -41,7 +43,7 @@ class RemotePlayer { void CreateNameBubble(); void DestroyNameBubble(); - const CustomizeState& GetCustomizeState() const { return m_customizeState; } + const Extensions::Common::CustomizeState& GetCustomizeState() const { return m_customizeState; } bool GetAllowRemoteCustomize() const { return m_allowRemoteCustomize; } void SetClickAnimObjectId(MxU32 p_clickAnimObjectId) { m_animator.SetClickAnimObjectId(p_clickAnimObjectId); } void StopClickAnimation(); @@ -79,11 +81,13 @@ class RemotePlayer { float m_currentDirection[3]; float m_currentUp[3]; - CharacterAnimator m_animator; + Extensions::Common::CharacterAnimator m_animator; LegoROI* m_vehicleROI; - CustomizeState m_customizeState; + NameBubbleRenderer* m_nameBubble; + + Extensions::Common::CustomizeState m_customizeState; bool m_allowRemoteCustomize; }; diff --git a/extensions/include/extensions/multiplayer/thirdpersoncamera.h b/extensions/include/extensions/multiplayer/thirdpersoncamera.h deleted file mode 100644 index 5f8fa21d..00000000 --- a/extensions/include/extensions/multiplayer/thirdpersoncamera.h +++ /dev/null @@ -1,151 +0,0 @@ -#pragma once - -#include "extensions/multiplayer/characteranimator.h" -#include "extensions/multiplayer/customizestate.h" -#include "mxgeometry/mxgeometry3d.h" -#include "mxtypes.h" - -#include -#include - -class IslePathActor; -class LegoNavController; -class LegoPathActor; -class LegoROI; -class LegoWorld; -class Vector3; - -namespace Multiplayer -{ - -class ThirdPersonCamera { -public: - ThirdPersonCamera(); - - void Enable(); - void Disable(); - bool IsEnabled() const { return m_enabled; } - bool IsActive() const { return m_active; } - - // Core hooks - void OnActorEnter(IslePathActor* p_actor); - void OnActorExit(IslePathActor* p_actor); - void OnCamAnimEnd(LegoPathActor* p_actor); - - // Called every frame from NetworkManager::Tickle() - void Tick(float p_deltaTime); - - // Animation selection (forwarded from NetworkManager) - void SetWalkAnimId(uint8_t p_walkAnimId); - void SetIdleAnimId(uint8_t p_idleAnimId); - void TriggerEmote(uint8_t p_emoteId); - bool IsInMultiPartEmote() const; - int8_t GetFrozenEmoteId() const; - void SetDisplayActorIndex(uint8_t p_displayActorIndex); - uint8_t GetDisplayActorIndex() const { return m_displayActorIndex; } - LegoROI* GetDisplayROI() const { return m_displayROI; } - CustomizeState& GetCustomizeState() { return m_customizeState; } - - void ApplyCustomizeChange(uint8_t p_changeType, uint8_t p_partIndex); - void SetClickAnimObjectId(MxU32 p_clickAnimObjectId) { m_animator.SetClickAnimObjectId(p_clickAnimObjectId); } - void StopClickAnimation(); - bool IsInVehicle() const { return m_animator.IsInVehicle(); } - - void SetNameBubbleVisible(bool p_visible); - - 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); - - // Auto-switch flags (set by HandleSDLEvent, consumed by caller) - bool ConsumeAutoDisable(); - bool ConsumeAutoEnable(); - - float GetOrbitDistance() const { return m_orbitDistance; } - void SetOrbitDistance(float p_distance) { m_orbitDistance = p_distance; } - void ResetTouchState() { m_touch = {}; } - - // Finger-claiming API for split-screen touch zones (left=movement, right=camera) - bool TryClaimFinger(const SDL_TouchFingerEvent& event); - bool TryReleaseFinger(SDL_FingerID id); - bool IsFingerTracked(SDL_FingerID id) const; - - static constexpr float CAMERA_ZONE_X = 0.5f; - static constexpr float MIN_DISTANCE = 1.5f; - -private: - // Orbit camera helpers - 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(); - - void CreateNameBubble(); - void DestroyNameBubble(); - - bool EnsureDisplayROI(); - void CreateDisplayClone(); - void DestroyDisplayClone(); - bool HasDisplayOverride() const { return m_displayROI != nullptr; } - - bool m_enabled; - bool m_active; - bool m_pendingWorldTransition; // True between OnWorldEnabled and first Tick; defers camera setup - LegoROI* m_playerROI; // Borrowed, not owned - - // Display actor override - uint8_t m_displayActorIndex; - LegoROI* m_displayROI; // Owned clone; nullptr = use native ROI - char m_displayUniqueName[32]; - CustomizeState m_customizeState; - - CharacterAnimator m_animator; - - bool m_showNameBubble; - - // Orbit camera state - 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 - struct TouchState { - SDL_FingerID id[2]; - float x[2], y[2]; - int count; - float initialPinchDist; - } m_touch; - - static constexpr float DEFAULT_ORBIT_YAW = 0.0f; - static constexpr float DEFAULT_ORBIT_PITCH = 0.3f; - static constexpr float DEFAULT_ORBIT_DISTANCE = 3.5f; - static constexpr float ORBIT_TARGET_HEIGHT = 1.5f; - static constexpr float MIN_PITCH = 0.05f; - static constexpr float MAX_PITCH = 1.4f; - static constexpr float MAX_DISTANCE = 15.0f; - - bool m_wantsAutoDisable; - bool m_wantsAutoEnable; -}; - -} // namespace Multiplayer diff --git a/extensions/include/extensions/siloader.h b/extensions/include/extensions/siloader.h index e581f6b1..3e727ecd 100644 --- a/extensions/include/extensions/siloader.h +++ b/extensions/include/extensions/siloader.h @@ -15,7 +15,7 @@ class Core; namespace Extensions { -class SiLoader { +class SiLoaderExt { public: typedef std::pair StreamObject; @@ -31,12 +31,14 @@ class SiLoader { template static std::optional ReplacedIn(MxDSAction& p_action, Args... p_args); + static const std::vector& GetFiles() { return files; } + static std::map options; - static std::vector files; - static std::vector directives; static bool enabled; private: + static std::vector files; + static std::vector directives; static std::vector> startWith; static std::vector> removeWith; static std::vector> replace; @@ -53,7 +55,7 @@ class SiLoader { #ifdef EXTENSIONS template -std::optional SiLoader::ReplacedIn(MxDSAction& p_action, Args... p_args) +std::optional SiLoaderExt::ReplacedIn(MxDSAction& p_action, Args... p_args) { StreamObject object{p_action.GetAtomId(), p_action.GetObjectId()}; auto checkAtomId = [&p_action, &object](const auto& p_atomId) -> std::optional { @@ -70,26 +72,34 @@ std::optional SiLoader::ReplacedIn(MxDSAction& p_action, ((void) (!result.has_value() && (result = checkAtomId(p_args), true)), ...); return result; } +#endif -constexpr auto Load = &SiLoader::Load; -constexpr auto HandleFind = &SiLoader::HandleFind; -constexpr auto HandleStart = &SiLoader::HandleStart; -constexpr auto HandleWorld = &SiLoader::HandleWorld; -constexpr auto HandleRemove = &SiLoader::HandleRemove; -constexpr auto HandleDelete = &SiLoader::HandleDelete; -constexpr auto HandleEndAction = &SiLoader::HandleEndAction; -constexpr auto ReplacedIn = [](auto&&... args) { return SiLoader::ReplacedIn(std::forward(args)...); }; +namespace SI +{ +#ifdef EXTENSIONS +constexpr auto Load = &SiLoaderExt::Load; +constexpr auto HandleFind = &SiLoaderExt::HandleFind; +constexpr auto HandleStart = &SiLoaderExt::HandleStart; +constexpr auto HandleWorld = &SiLoaderExt::HandleWorld; +constexpr auto HandleRemove = &SiLoaderExt::HandleRemove; +constexpr auto HandleDelete = &SiLoaderExt::HandleDelete; +constexpr auto HandleEndAction = &SiLoaderExt::HandleEndAction; +constexpr auto ReplacedIn = [](auto&&... args) { + return SiLoaderExt::ReplacedIn(std::forward(args)...); +}; #else -constexpr decltype(&SiLoader::Load) Load = nullptr; -constexpr decltype(&SiLoader::HandleFind) HandleFind = nullptr; -constexpr decltype(&SiLoader::HandleStart) HandleStart = nullptr; -constexpr decltype(&SiLoader::HandleWorld) HandleWorld = nullptr; -constexpr decltype(&SiLoader::HandleRemove) HandleRemove = nullptr; -constexpr decltype(&SiLoader::HandleDelete) HandleDelete = nullptr; -constexpr decltype(&SiLoader::HandleEndAction) HandleEndAction = nullptr; -constexpr auto ReplacedIn = [](auto&&... args) -> std::optional { +constexpr decltype(&SiLoaderExt::Load) Load = nullptr; +constexpr decltype(&SiLoaderExt::HandleFind) HandleFind = nullptr; +constexpr decltype(&SiLoaderExt::HandleStart) HandleStart = nullptr; +constexpr decltype(&SiLoaderExt::HandleWorld) HandleWorld = nullptr; +constexpr decltype(&SiLoaderExt::HandleRemove) HandleRemove = nullptr; +constexpr decltype(&SiLoaderExt::HandleDelete) HandleDelete = nullptr; +constexpr decltype(&SiLoaderExt::HandleEndAction) HandleEndAction = nullptr; +constexpr auto ReplacedIn = [](auto&&... args) -> std::optional { ((void) args, ...); return std::nullopt; }; #endif +} // namespace SI + }; // namespace Extensions diff --git a/extensions/include/extensions/textureloader.h b/extensions/include/extensions/textureloader.h index a318c9b8..234f4cdb 100644 --- a/extensions/include/extensions/textureloader.h +++ b/extensions/include/extensions/textureloader.h @@ -9,13 +9,13 @@ namespace Extensions { -class TextureLoader { +class TextureLoaderExt { public: static void Initialize(); static bool PatchTexture(LegoTextureInfo* p_textureInfo); + static void AddExcludedFile(const std::string& p_file); static std::map options; - static std::vector excludedFiles; static bool enabled; static constexpr std::array, 1> defaults = { @@ -23,12 +23,17 @@ class TextureLoader { }; private: + static std::vector excludedFiles; static SDL_Surface* FindTexture(const char* p_name); }; +namespace TL +{ #ifdef EXTENSIONS -constexpr auto PatchTexture = &TextureLoader::PatchTexture; +constexpr auto PatchTexture = &TextureLoaderExt::PatchTexture; #else -constexpr decltype(&TextureLoader::PatchTexture) PatchTexture = nullptr; +constexpr decltype(&TextureLoaderExt::PatchTexture) PatchTexture = nullptr; #endif +} // namespace TL + }; // namespace Extensions diff --git a/extensions/include/extensions/thirdpersoncamera.h b/extensions/include/extensions/thirdpersoncamera.h new file mode 100644 index 00000000..958f3b82 --- /dev/null +++ b/extensions/include/extensions/thirdpersoncamera.h @@ -0,0 +1,91 @@ +#pragma once + +#include "extensions/extensions.h" +#include "mxtypes.h" + +#include +#include +#include + +class IslePathActor; +class LegoEventNotificationParam; +class LegoNavController; +class LegoPathActor; +class LegoROI; +class LegoWorld; +class Vector3; + +namespace Extensions +{ + +namespace ThirdPersonCamera +{ +class Controller; +} + +class ThirdPersonCameraExt { +public: + static void Initialize(); + + static void HandleActorEnter(IslePathActor* p_actor); + static void HandleActorExit(IslePathActor* p_actor); + static void HandleCamAnimEnd(LegoPathActor* p_actor); + static void OnSDLEvent(SDL_Event* p_event); + static MxBool IsThirdPersonCameraActive(); + static MxBool HandleTouchInput(SDL_Event* p_event); + static MxBool HandleNavOverride( + LegoNavController* p_nav, + const Vector3& p_curPos, + const Vector3& p_curDir, + Vector3& p_newPos, + Vector3& p_newDir, + float p_deltaTime + ); + static MxBool HandleWorldEnable(LegoWorld* p_world, MxBool p_enable); + + static MxBool HandleROIClick(LegoROI* p_rootROI, LegoEventNotificationParam& p_param); + static MxBool IsClonedCharacter(const char* p_name); + static void HandleCreate(); + LEGO1_EXPORT static void HandleSDLEvent(SDL_Event* p_event); + + static ThirdPersonCamera::Controller* GetCamera(); + + static std::map options; + static bool enabled; + +private: + static ThirdPersonCamera::Controller* s_camera; + static bool s_registered; + static bool s_inIsleWorld; +}; + +namespace TP +{ +#ifdef EXTENSIONS +constexpr auto HandleCreate = &ThirdPersonCameraExt::HandleCreate; +constexpr auto HandleWorldEnable = &ThirdPersonCameraExt::HandleWorldEnable; +constexpr auto HandleActorEnter = &ThirdPersonCameraExt::HandleActorEnter; +constexpr auto HandleActorExit = &ThirdPersonCameraExt::HandleActorExit; +constexpr auto HandleCamAnimEnd = &ThirdPersonCameraExt::HandleCamAnimEnd; +constexpr auto HandleSDLEvent = &ThirdPersonCameraExt::OnSDLEvent; +constexpr auto IsThirdPersonCameraActive = &ThirdPersonCameraExt::IsThirdPersonCameraActive; +constexpr auto HandleTouchInput = &ThirdPersonCameraExt::HandleTouchInput; +constexpr auto HandleNavOverride = &ThirdPersonCameraExt::HandleNavOverride; +constexpr auto HandleROIClick = &ThirdPersonCameraExt::HandleROIClick; +constexpr auto IsClonedCharacter = &ThirdPersonCameraExt::IsClonedCharacter; +#else +constexpr decltype(&ThirdPersonCameraExt::HandleCreate) HandleCreate = nullptr; +constexpr decltype(&ThirdPersonCameraExt::HandleWorldEnable) HandleWorldEnable = nullptr; +constexpr decltype(&ThirdPersonCameraExt::HandleActorEnter) HandleActorEnter = nullptr; +constexpr decltype(&ThirdPersonCameraExt::HandleActorExit) HandleActorExit = nullptr; +constexpr decltype(&ThirdPersonCameraExt::HandleCamAnimEnd) HandleCamAnimEnd = nullptr; +constexpr decltype(&ThirdPersonCameraExt::OnSDLEvent) HandleSDLEvent = nullptr; +constexpr decltype(&ThirdPersonCameraExt::IsThirdPersonCameraActive) IsThirdPersonCameraActive = nullptr; +constexpr decltype(&ThirdPersonCameraExt::HandleTouchInput) HandleTouchInput = nullptr; +constexpr decltype(&ThirdPersonCameraExt::HandleNavOverride) HandleNavOverride = nullptr; +constexpr decltype(&ThirdPersonCameraExt::HandleROIClick) HandleROIClick = nullptr; +constexpr decltype(&ThirdPersonCameraExt::IsClonedCharacter) IsClonedCharacter = nullptr; +#endif +} // namespace TP + +}; // namespace Extensions diff --git a/extensions/include/extensions/thirdpersoncamera/controller.h b/extensions/include/extensions/thirdpersoncamera/controller.h new file mode 100644 index 00000000..136f752b --- /dev/null +++ b/extensions/include/extensions/thirdpersoncamera/controller.h @@ -0,0 +1,109 @@ +#pragma once + +#include "extensions/common/characteranimator.h" +#include "extensions/thirdpersoncamera/displayactor.h" +#include "extensions/thirdpersoncamera/inputhandler.h" +#include "extensions/thirdpersoncamera/orbitcamera.h" +#include "mxtypes.h" + +#include +#include + +class IslePathActor; +class LegoNavController; +class LegoPathActor; +class LegoROI; +class LegoWorld; +class Vector3; + +namespace Extensions +{ +namespace ThirdPersonCamera +{ + +class Controller { +public: + Controller(); + + void Enable(); + void Disable(); + bool IsEnabled() const { return m_enabled; } + bool IsActive() const { return m_active; } + + void OnActorEnter(IslePathActor* p_actor); + void OnActorExit(IslePathActor* p_actor); + void OnCamAnimEnd(LegoPathActor* p_actor); + + void Tick(float p_deltaTime); + + void SetWalkAnimId(uint8_t p_walkAnimId); + uint8_t GetWalkAnimId() const { return m_animator.GetWalkAnimId(); } + void SetIdleAnimId(uint8_t p_idleAnimId); + uint8_t GetIdleAnimId() const { return m_animator.GetIdleAnimId(); } + void TriggerEmote(uint8_t p_emoteId); + bool IsInMultiPartEmote() const; + int8_t GetFrozenEmoteId() const; + + void SetDisplayActorIndex(uint8_t p_displayActorIndex) { m_display.SetDisplayActorIndex(p_displayActorIndex); } + uint8_t GetDisplayActorIndex() const { return m_display.GetDisplayActorIndex(); } + LegoROI* GetDisplayROI() const { return m_display.GetDisplayROI(); } + Common::CustomizeState& GetCustomizeState() { return m_display.GetCustomizeState(); } + + void ApplyCustomizeChange(uint8_t p_changeType, uint8_t p_partIndex) + { + m_display.ApplyCustomizeChange(p_changeType, p_partIndex); + } + void SetClickAnimObjectId(MxU32 p_clickAnimObjectId) { m_animator.SetClickAnimObjectId(p_clickAnimObjectId); } + void StopClickAnimation(); + bool IsInVehicle() const { return m_animator.IsInVehicle(); } + + void OnWorldEnabled(LegoWorld* p_world); + void OnWorldDisabled(LegoWorld* p_world); + + MxBool HandleCameraRelativeMovement( + LegoNavController* p_nav, + const Vector3& p_curPos, + const Vector3& p_curDir, + Vector3& p_newPos, + Vector3& p_newDir, + float p_deltaTime + ); + + void HandleSDLEventImpl(SDL_Event* p_event); + + bool ConsumeAutoDisable() { return m_input.ConsumeAutoDisable(); } + bool ConsumeAutoEnable() { return m_input.ConsumeAutoEnable(); } + + float GetOrbitDistance() const { return m_orbit.GetOrbitDistance(); } + void SetOrbitDistance(float p_distance) { m_orbit.SetOrbitDistance(p_distance); } + void ResetTouchState() { m_input.ResetTouchState(); } + + bool TryClaimFinger(const SDL_TouchFingerEvent& event) { return m_input.TryClaimFinger(event, m_active); } + bool TryReleaseFinger(SDL_FingerID id) { return m_input.TryReleaseFinger(id); } + bool IsFingerTracked(SDL_FingerID id) const { return m_input.IsFingerTracked(id); } + + void FreezeDisplayActor() { m_display.FreezeDisplayActor(); } + void UnfreezeDisplayActor() { m_display.UnfreezeDisplayActor(); } + bool IsDisplayActorFrozen() const { return m_display.IsDisplayActorFrozen(); } + + LegoROI* GetPlayerROI() const { return m_playerROI; } + + static constexpr float CAMERA_ZONE_X = InputHandler::CAMERA_ZONE_X; + static constexpr float MIN_DISTANCE = OrbitCamera::MIN_DISTANCE; + +private: + void ReinitForCharacter(); + + OrbitCamera m_orbit; + InputHandler m_input; + DisplayActor m_display; + Common::CharacterAnimator m_animator; + + bool m_enabled; + bool m_active; + bool m_pendingWorldTransition; + LegoROI* m_playerROI; +}; + +} // namespace ThirdPersonCamera +} // namespace Extensions diff --git a/extensions/include/extensions/thirdpersoncamera/displayactor.h b/extensions/include/extensions/thirdpersoncamera/displayactor.h new file mode 100644 index 00000000..820ac464 --- /dev/null +++ b/extensions/include/extensions/thirdpersoncamera/displayactor.h @@ -0,0 +1,47 @@ +#pragma once + +#include "extensions/common/customizestate.h" + +#include + +class LegoROI; + +namespace Extensions +{ +namespace ThirdPersonCamera +{ + +class DisplayActor { +public: + DisplayActor(); + + void SetDisplayActorIndex(uint8_t p_displayActorIndex); + uint8_t GetDisplayActorIndex() const { return m_displayActorIndex; } + + bool EnsureDisplayROI(); + void CreateDisplayClone(); + void DestroyDisplayClone(); + + bool HasDisplayOverride() const { return m_displayROI != nullptr; } + LegoROI* GetDisplayROI() const { return m_displayROI; } + + Common::CustomizeState& GetCustomizeState() { return m_customizeState; } + + void ApplyCustomizeChange(uint8_t p_changeType, uint8_t p_partIndex); + + void SyncTransformFromNative(LegoROI* p_nativeROI); + + void FreezeDisplayActor() { m_displayActorFrozen = true; } + void UnfreezeDisplayActor() { m_displayActorFrozen = false; } + bool IsDisplayActorFrozen() const { return m_displayActorFrozen; } + +private: + uint8_t m_displayActorIndex; + bool m_displayActorFrozen; + LegoROI* m_displayROI; + char m_displayUniqueName[32]; + Common::CustomizeState m_customizeState; +}; + +} // namespace ThirdPersonCamera +} // namespace Extensions diff --git a/extensions/include/extensions/thirdpersoncamera/inputhandler.h b/extensions/include/extensions/thirdpersoncamera/inputhandler.h new file mode 100644 index 00000000..ae493e0d --- /dev/null +++ b/extensions/include/extensions/thirdpersoncamera/inputhandler.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +namespace Extensions +{ +namespace ThirdPersonCamera +{ + +class OrbitCamera; + +class InputHandler { +public: + InputHandler(); + + void HandleSDLEvent(SDL_Event* p_event, OrbitCamera& p_orbit, bool p_active); + + bool TryClaimFinger(const SDL_TouchFingerEvent& p_event, bool p_active); + bool TryReleaseFinger(SDL_FingerID p_id); + bool IsFingerTracked(SDL_FingerID p_id) const; + + bool ConsumeAutoDisable(); + bool ConsumeAutoEnable(); + + void ResetTouchState() { m_touch = {}; } + + static constexpr float CAMERA_ZONE_X = 0.5f; + +private: + struct TouchState { + SDL_FingerID id[2]; + float x[2], y[2]; + int count; + float initialPinchDist; + } m_touch; + + bool m_wantsAutoDisable; + bool m_wantsAutoEnable; +}; + +} // namespace ThirdPersonCamera +} // namespace Extensions diff --git a/extensions/include/extensions/thirdpersoncamera/orbitcamera.h b/extensions/include/extensions/thirdpersoncamera/orbitcamera.h new file mode 100644 index 00000000..e0285d48 --- /dev/null +++ b/extensions/include/extensions/thirdpersoncamera/orbitcamera.h @@ -0,0 +1,69 @@ +#pragma once + +#include "mxgeometry/mxgeometry3d.h" + +#include + +class LegoNavController; +class LegoPathActor; +class LegoROI; +class LegoWorld; +class Vector3; + +namespace Extensions +{ +namespace ThirdPersonCamera +{ + +class OrbitCamera { +public: + OrbitCamera(); + + void SetupCamera(LegoPathActor* p_actor); + void ApplyOrbitCamera(); + void ResetOrbitState(); + void ClampPitch(); + void ClampDistance(); + void InitAbsoluteYaw(LegoROI* p_roi); + + void RestoreFirstPersonCamera(); + + MxBool HandleCameraRelativeMovement( + LegoNavController* p_nav, + const Vector3& p_curPos, + const Vector3& p_curDir, + Vector3& p_newPos, + Vector3& p_newDir, + float p_deltaTime, + bool p_isInMultiPartEmote + ); + + void AdjustYaw(float p_delta) { m_absoluteYaw += p_delta; } + void AdjustPitch(float p_delta) { m_orbitPitch += p_delta; } + void AdjustDistance(float p_delta) { m_orbitDistance += p_delta; } + + float GetOrbitDistance() const { return m_orbitDistance; } + void SetOrbitDistance(float p_distance) { m_orbitDistance = p_distance; } + float GetSmoothedSpeed() const { return m_smoothedSpeed; } + + static constexpr float DEFAULT_ORBIT_YAW = 0.0f; + static constexpr float DEFAULT_ORBIT_PITCH = 0.3f; + static constexpr float DEFAULT_ORBIT_DISTANCE = 3.5f; + static constexpr float ORBIT_TARGET_HEIGHT = 1.5f; + static constexpr float MIN_PITCH = 0.05f; + static constexpr float MAX_PITCH = 1.4f; + static constexpr float MIN_DISTANCE = 1.5f; + static constexpr float MAX_DISTANCE = 15.0f; + +private: + void ComputeOrbitVectors(float p_yaw, Mx3DPointFloat& p_at, Mx3DPointFloat& p_dir, Mx3DPointFloat& p_up) const; + float GetLocalYaw(LegoROI* p_roi) const; + + float m_orbitPitch; + float m_orbitDistance; + float m_absoluteYaw; + float m_smoothedSpeed; +}; + +} // namespace ThirdPersonCamera +} // namespace Extensions diff --git a/extensions/src/multiplayer/animdata.cpp b/extensions/src/common/animdata.cpp similarity index 95% rename from extensions/src/multiplayer/animdata.cpp rename to extensions/src/common/animdata.cpp index af2e37e6..49efa514 100644 --- a/extensions/src/multiplayer/animdata.cpp +++ b/extensions/src/common/animdata.cpp @@ -1,8 +1,10 @@ -#include "extensions/multiplayer/animdata.h" +#include "extensions/common/animdata.h" #include "legopathactor.h" -namespace Multiplayer +namespace Extensions +{ +namespace Common { const char* const g_walkAnimNames[] = { @@ -75,4 +77,5 @@ int8_t DetectVehicleType(LegoPathActor* p_actor) return VEHICLE_NONE; } -} // namespace Multiplayer +} // namespace Common +} // namespace Extensions diff --git a/extensions/src/multiplayer/animutils.cpp b/extensions/src/common/animutils.cpp similarity index 97% rename from extensions/src/multiplayer/animutils.cpp rename to extensions/src/common/animutils.cpp index 38198520..398b6439 100644 --- a/extensions/src/multiplayer/animutils.cpp +++ b/extensions/src/common/animutils.cpp @@ -1,4 +1,4 @@ -#include "extensions/multiplayer/animutils.h" +#include "extensions/common/animutils.h" #include "anim/legoanim.h" #include "legoanimpresenter.h" @@ -9,7 +9,7 @@ #include -using namespace Multiplayer; +using namespace Extensions::Common; // Mirrors the game's UpdateStructMapAndROIIndex: assigns ROI indices at runtime // via SetROIIndex() since m_roiIndex starts at 0 for all animation nodes. diff --git a/extensions/src/multiplayer/characteranimator.cpp b/extensions/src/common/characteranimator.cpp similarity index 93% rename from extensions/src/multiplayer/characteranimator.cpp rename to extensions/src/common/characteranimator.cpp index 203b1e45..d54bd035 100644 --- a/extensions/src/multiplayer/characteranimator.cpp +++ b/extensions/src/common/characteranimator.cpp @@ -1,9 +1,8 @@ -#include "extensions/multiplayer/characteranimator.h" +#include "extensions/common/characteranimator.h" #include "3dmanager/lego3dmanager.h" #include "anim/legoanim.h" -#include "extensions/multiplayer/charactercustomizer.h" -#include "extensions/multiplayer/namebubblerenderer.h" +#include "extensions/common/charactercustomizer.h" #include "legoanimpresenter.h" #include "legocachesoundmanager.h" #include "legocachsound.h" @@ -18,21 +17,19 @@ #include -using namespace Multiplayer; +using namespace Extensions::Common; CharacterAnimator::CharacterAnimator(const CharacterAnimatorConfig& p_config) : m_config(p_config), m_walkAnimId(0), m_idleAnimId(0), m_walkAnimCache(nullptr), m_idleAnimCache(nullptr), m_animTime(0.0f), m_idleTime(0.0f), m_idleAnimTime(0.0f), m_wasMoving(false), m_emoteAnimCache(nullptr), m_emoteTime(0.0f), m_emoteDuration(0.0f), m_emoteActive(false), m_currentEmoteId(0), m_frozenEmoteId(-1), m_frozenAnimCache(nullptr), m_frozenAnimDuration(0.0f), m_clickAnimObjectId(0), m_rideAnim(nullptr), - m_rideRoiMap(nullptr), m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_currentVehicleType(VEHICLE_NONE), - m_nameBubble(nullptr) + m_rideRoiMap(nullptr), m_rideRoiMapSize(0), m_rideVehicleROI(nullptr), m_currentVehicleType(VEHICLE_NONE) { } CharacterAnimator::~CharacterAnimator() { - DestroyNameBubble(); ClearRideAnimation(); } @@ -460,35 +457,3 @@ void CharacterAnimator::ApplyIdleFrame0(LegoROI* p_roi) LegoROI::ApplyAnimationTransformation(root->GetChild(i), transform, (LegoTime) 0.0f, m_idleAnimCache->roiMap); } } - -void CharacterAnimator::CreateNameBubble(const char* p_name) -{ - if (m_nameBubble || !p_name || p_name[0] == '\0') { - return; - } - - m_nameBubble = new NameBubbleRenderer(); - m_nameBubble->Create(p_name); -} - -void CharacterAnimator::DestroyNameBubble() -{ - if (m_nameBubble) { - delete m_nameBubble; - m_nameBubble = nullptr; - } -} - -void CharacterAnimator::SetNameBubbleVisible(bool p_visible) -{ - if (m_nameBubble) { - m_nameBubble->SetVisible(p_visible); - } -} - -void CharacterAnimator::UpdateNameBubble(LegoROI* p_roi) -{ - if (m_nameBubble) { - m_nameBubble->Update(p_roi); - } -} diff --git a/extensions/src/multiplayer/charactercloner.cpp b/extensions/src/common/charactercloner.cpp similarity index 98% rename from extensions/src/multiplayer/charactercloner.cpp rename to extensions/src/common/charactercloner.cpp index b0920f8f..a75036b2 100644 --- a/extensions/src/multiplayer/charactercloner.cpp +++ b/extensions/src/common/charactercloner.cpp @@ -1,4 +1,4 @@ -#include "extensions/multiplayer/charactercloner.h" +#include "extensions/common/charactercloner.h" #include "legoactors.h" #include "legocharactermanager.h" @@ -13,7 +13,7 @@ #include #include -using namespace Multiplayer; +using namespace Extensions::Common; LegoROI* CharacterCloner::Clone(LegoCharacterManager* p_charMgr, const char* p_uniqueName, const char* p_characterType) { diff --git a/extensions/src/multiplayer/charactercustomizer.cpp b/extensions/src/common/charactercustomizer.cpp similarity index 87% rename from extensions/src/multiplayer/charactercustomizer.cpp rename to extensions/src/common/charactercustomizer.cpp index 578e2d40..c46d6cf9 100644 --- a/extensions/src/multiplayer/charactercustomizer.cpp +++ b/extensions/src/common/charactercustomizer.cpp @@ -1,12 +1,14 @@ -#include "extensions/multiplayer/charactercustomizer.h" +#include "extensions/common/charactercustomizer.h" #include "3dmanager/lego3dmanager.h" #include "3dmanager/lego3dview.h" -#include "extensions/multiplayer/charactercloner.h" -#include "extensions/multiplayer/customizestate.h" -#include "extensions/multiplayer/protocol.h" +#include "extensions/common/charactercloner.h" +#include "extensions/common/customizestate.h" +#include "extensions/common/constants.h" +#include "legoactor.h" #include "legoactors.h" #include "legocharactermanager.h" +#include "legogamestate.h" #include "legovideomanager.h" #include "misc.h" #include "mxatom.h" @@ -19,7 +21,7 @@ #include -using namespace Multiplayer; +using namespace Extensions::Common; static const MxU32 g_characterSoundIdOffset = 50; static const MxU32 g_characterSoundIdMoodOffset = 66; @@ -326,3 +328,42 @@ void CharacterCustomizer::StopClickAnimation(MxU32 p_objectId) action.SetObjectId(p_objectId); DeleteObject(action); } + +bool CharacterCustomizer::ResolveClickChangeType(uint8_t& p_changeType, int& p_partIndex, LegoROI* p_clickedROI) +{ + p_partIndex = -1; + + switch (GameState()->GetActorId()) { + case LegoActor::c_pepper: + if (GameState()->GetCurrentAct() == LegoGameState::e_act2 || + GameState()->GetCurrentAct() == LegoGameState::e_act3) { + return false; + } + p_changeType = CHANGE_VARIANT; + break; + case LegoActor::c_mama: + p_changeType = CHANGE_SOUND; + break; + case LegoActor::c_papa: + p_changeType = CHANGE_MOVE; + break; + case LegoActor::c_nick: + p_changeType = CHANGE_COLOR; + if (p_clickedROI) { + p_partIndex = MapClickedPartIndex(p_clickedROI->GetName()); + } + if (p_partIndex < 0) { + return false; + } + break; + case LegoActor::c_laura: + p_changeType = CHANGE_MOOD; + break; + case LegoActor::c_brickster: + return false; + default: + return false; + } + + return true; +} diff --git a/extensions/src/multiplayer/customizestate.cpp b/extensions/src/common/customizestate.cpp similarity index 97% rename from extensions/src/multiplayer/customizestate.cpp rename to extensions/src/common/customizestate.cpp index e0e51b9e..b19c104c 100644 --- a/extensions/src/multiplayer/customizestate.cpp +++ b/extensions/src/common/customizestate.cpp @@ -1,11 +1,11 @@ -#include "extensions/multiplayer/customizestate.h" +#include "extensions/common/customizestate.h" #include "legoactors.h" #include "misc.h" #include -using namespace Multiplayer; +using namespace Extensions::Common; void CustomizeState::InitFromActorInfo(uint8_t p_actorInfoIndex) { diff --git a/extensions/src/extensions.cpp b/extensions/src/extensions.cpp index 95b57764..51e880fb 100644 --- a/extensions/src/extensions.cpp +++ b/extensions/src/extensions.cpp @@ -3,6 +3,7 @@ #include "extensions/multiplayer.h" #include "extensions/siloader.h" #include "extensions/textureloader.h" +#include "extensions/thirdpersoncamera.h" #include @@ -11,14 +12,19 @@ void Extensions::Enable(const char* p_key, std::map p_ for (const char* key : availableExtensions) { if (!SDL_strcasecmp(p_key, key)) { if (!SDL_strcasecmp(p_key, "extensions:texture loader")) { - TextureLoader::options = std::move(p_options); - TextureLoader::enabled = true; - TextureLoader::Initialize(); + TextureLoaderExt::options = std::move(p_options); + TextureLoaderExt::enabled = true; + TextureLoaderExt::Initialize(); } else if (!SDL_strcasecmp(p_key, "extensions:si loader")) { - SiLoader::options = std::move(p_options); - SiLoader::enabled = true; - SiLoader::Initialize(); + SiLoaderExt::options = std::move(p_options); + SiLoaderExt::enabled = true; + SiLoaderExt::Initialize(); + } + else if (!SDL_strcasecmp(p_key, "extensions:third person camera")) { + ThirdPersonCameraExt::options = std::move(p_options); + ThirdPersonCameraExt::enabled = true; + ThirdPersonCameraExt::Initialize(); } else if (!SDL_strcasecmp(p_key, "extensions:multiplayer")) { MultiplayerExt::options = std::move(p_options); diff --git a/extensions/src/multiplayer.cpp b/extensions/src/multiplayer.cpp index 1cbe2e4b..18c49ec0 100644 --- a/extensions/src/multiplayer.cpp +++ b/extensions/src/multiplayer.cpp @@ -1,17 +1,19 @@ #include "extensions/multiplayer.h" -#include "extensions/multiplayer/charactercustomizer.h" +#include "extensions/common/charactercloner.h" +#include "extensions/common/charactercustomizer.h" +#include "extensions/common/constants.h" #include "extensions/multiplayer/networkmanager.h" #include "extensions/multiplayer/networktransport.h" #include "extensions/multiplayer/protocol.h" +#include "extensions/thirdpersoncamera.h" +#include "extensions/thirdpersoncamera/controller.h" #include "isle_actions.h" -#include "islepathactor.h" #include "legoactor.h" #include "legoactors.h" #include "legoentity.h" #include "legoeventnotificationparam.h" #include "legogamestate.h" -#include "legonavcontroller.h" #include "legopathactor.h" #include "misc.h" #include "roi/legoroi.h" @@ -27,49 +29,48 @@ using namespace Extensions; -static uint8_t ResolveDisplayActorIndex(const char* p_name) -{ - for (int i = 0; i < static_cast(sizeOfArray(g_actorInfoInit)); i++) { - if (!SDL_strcasecmp(g_actorInfoInit[i].m_name, p_name)) { - return static_cast(i); - } - } - return Multiplayer::DISPLAY_ACTOR_NONE; -} - std::map MultiplayerExt::options; bool MultiplayerExt::enabled = false; -std::string MultiplayerExt::relayUrl; -std::string MultiplayerExt::room; +std::string MultiplayerExt::s_relayUrl; +std::string MultiplayerExt::s_room; Multiplayer::NetworkManager* MultiplayerExt::s_networkManager = nullptr; Multiplayer::NetworkTransport* MultiplayerExt::s_transport = nullptr; Multiplayer::PlatformCallbacks* MultiplayerExt::s_callbacks = nullptr; void MultiplayerExt::Initialize() { - relayUrl = options["multiplayer:relay url"]; - room = options["multiplayer:room"]; + // Multiplayer depends on camera - ensure it's enabled + if (!ThirdPersonCameraExt::enabled) { + ThirdPersonCameraExt::enabled = true; + ThirdPersonCameraExt::Initialize(); + } + + s_relayUrl = options["multiplayer:relay url"]; + s_room = options["multiplayer:room"]; #ifdef __EMSCRIPTEN__ - s_transport = new Multiplayer::WebSocketTransport(relayUrl); + s_transport = new Multiplayer::WebSocketTransport(s_relayUrl); s_callbacks = new Multiplayer::EmscriptenCallbacks(); s_networkManager = new Multiplayer::NetworkManager(); s_networkManager->Initialize(s_transport, s_callbacks); - // Third-person camera enabled by default, toggled via WASM export - s_networkManager->GetThirdPersonCamera().Enable(); + ThirdPersonCamera::Controller* cam = ThirdPersonCameraExt::GetCamera(); + if (cam) { + cam->Enable(); + } std::string actor = options["multiplayer:actor"]; if (!actor.empty()) { - uint8_t displayIndex = ResolveDisplayActorIndex(actor.c_str()); - if (displayIndex != Multiplayer::DISPLAY_ACTOR_NONE) { - s_networkManager->SetDisplayActorIndex(displayIndex); + uint8_t displayIndex = Common::ResolveDisplayActorIndex(actor.c_str()); + if (displayIndex != Common::DISPLAY_ACTOR_NONE && cam) { + cam->SetDisplayActorIndex(displayIndex); + cam->FreezeDisplayActor(); } } - if (!relayUrl.empty() && !room.empty()) { - s_networkManager->Connect(room.c_str()); + if (!s_relayUrl.empty() && !s_room.empty()) { + s_networkManager->Connect(s_room.c_str()); } #endif } @@ -108,10 +109,8 @@ MxBool MultiplayerExt::HandleROIClick(LegoROI* p_rootROI, LegoEventNotificationP // Check if it's a remote player Multiplayer::RemotePlayer* remote = mgr->FindPlayerByROI(p_rootROI); - // Check if it's our own 3rd-person display actor override - bool isSelf = - (mgr->GetThirdPersonCamera().GetDisplayROI() != nullptr && - mgr->GetThirdPersonCamera().GetDisplayROI() == p_rootROI); + ThirdPersonCamera::Controller* cam = ThirdPersonCameraExt::GetCamera(); + bool isSelf = (cam && cam->GetDisplayROI() != nullptr && cam->GetDisplayROI() == p_rootROI); if (!remote && !isSelf) { return FALSE; @@ -124,37 +123,9 @@ MxBool MultiplayerExt::HandleROIClick(LegoROI* p_rootROI, LegoEventNotificationP // Determine change type from clicker's actor ID uint8_t changeType; - int partIndex = -1; - switch (GameState()->GetActorId()) { - case LegoActor::c_pepper: - if (GameState()->GetCurrentAct() == LegoGameState::e_act2 || - GameState()->GetCurrentAct() == LegoGameState::e_act3) { - return TRUE; - } - changeType = Multiplayer::CHANGE_VARIANT; - break; - case LegoActor::c_mama: - changeType = Multiplayer::CHANGE_SOUND; - break; - case LegoActor::c_papa: - changeType = Multiplayer::CHANGE_MOVE; - break; - case LegoActor::c_nick: - changeType = Multiplayer::CHANGE_COLOR; - if (p_param.GetROI()) { - partIndex = Multiplayer::CharacterCustomizer::MapClickedPartIndex(p_param.GetROI()->GetName()); - } - if (partIndex < 0) { - return TRUE; - } - break; - case LegoActor::c_laura: - changeType = Multiplayer::CHANGE_MOOD; - break; - case LegoActor::c_brickster: + int partIndex; + if (!Common::CharacterCustomizer::ResolveClickChangeType(changeType, partIndex, p_param.GetROI())) { return TRUE; - default: - return FALSE; } // Send a customize request to the server. The server echoes it back to all peers @@ -263,27 +234,6 @@ void MultiplayerExt::HandleSaveLoaded() } } -void MultiplayerExt::HandleActorEnter(IslePathActor* p_actor) -{ - if (s_networkManager) { - s_networkManager->GetThirdPersonCamera().OnActorEnter(p_actor); - } -} - -void MultiplayerExt::HandleActorExit(IslePathActor* p_actor) -{ - if (s_networkManager) { - s_networkManager->GetThirdPersonCamera().OnActorExit(p_actor); - } -} - -void MultiplayerExt::HandleCamAnimEnd(LegoPathActor* p_actor) -{ - if (s_networkManager) { - s_networkManager->GetThirdPersonCamera().OnCamAnimEnd(p_actor); - } -} - MxBool MultiplayerExt::IsClonedCharacter(const char* p_name) { if (!s_networkManager) { @@ -293,93 +243,6 @@ MxBool MultiplayerExt::IsClonedCharacter(const char* p_name) return s_networkManager->IsClonedCharacter(p_name) ? TRUE : FALSE; } -void MultiplayerExt::HandleSDLEvent(SDL_Event* p_event) -{ - if (!s_networkManager || !s_networkManager->IsInIsleWorld()) { - return; - } - - Multiplayer::ThirdPersonCamera& camera = s_networkManager->GetThirdPersonCamera(); - - camera.HandleSDLEvent(p_event); - - // Auto-switch 3rd → 1st: zoom-in past minimum distance - if (camera.ConsumeAutoDisable()) { - camera.Disable(); - s_networkManager->NotifyThirdPersonChanged(false); - } - // Auto-switch 1st → 3rd: zoom-out from 1st person - else if (camera.ConsumeAutoEnable()) { - camera.ResetTouchState(); - camera.SetOrbitDistance(Multiplayer::ThirdPersonCamera::MIN_DISTANCE); - camera.Enable(); - s_networkManager->NotifyThirdPersonChanged(true); - } -} - -MxBool MultiplayerExt::IsThirdPersonCameraActive() -{ - if (s_networkManager && s_networkManager->GetThirdPersonCamera().IsActive()) { - return TRUE; - } - - return FALSE; -} - -MxBool MultiplayerExt::HandleTouchInput(SDL_Event* p_event) -{ - if (!s_networkManager || !s_networkManager->GetThirdPersonCamera().IsActive()) { - return FALSE; - } - - Multiplayer::ThirdPersonCamera& cam = s_networkManager->GetThirdPersonCamera(); - - switch (p_event->type) { - case SDL_EVENT_FINGER_DOWN: - if (cam.TryClaimFinger(p_event->tfinger)) { - return TRUE; - } - return FALSE; - - case SDL_EVENT_FINGER_MOTION: - if (cam.IsFingerTracked(p_event->tfinger.fingerID)) { - return TRUE; - } - return FALSE; - - case SDL_EVENT_FINGER_UP: - case SDL_EVENT_FINGER_CANCELED: - if (cam.TryReleaseFinger(p_event->tfinger.fingerID)) { - return TRUE; - } - return FALSE; - - default: - 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()) { @@ -389,11 +252,6 @@ MxBool MultiplayerExt::CheckRejected() return FALSE; } -void MultiplayerExt::SetNetworkManager(Multiplayer::NetworkManager* p_networkManager) -{ - s_networkManager = p_networkManager; -} - Multiplayer::NetworkManager* MultiplayerExt::GetNetworkManager() { return s_networkManager; @@ -401,10 +259,5 @@ Multiplayer::NetworkManager* MultiplayerExt::GetNetworkManager() bool Extensions::IsMultiplayerRejected() { - return Extension::Call(CheckRejected).value_or(FALSE); -} - -void Extensions::HandleMultiplayerSDLEvent(SDL_Event* p_event) -{ - Extension::Call(HandleSDLEvent, p_event); + return Extension::Call(MP::CheckRejected).value_or(FALSE); } diff --git a/extensions/src/multiplayer/networkmanager.cpp b/extensions/src/multiplayer/networkmanager.cpp index 7a364635..24b43b8a 100644 --- a/extensions/src/multiplayer/networkmanager.cpp +++ b/extensions/src/multiplayer/networkmanager.cpp @@ -1,8 +1,10 @@ #include "extensions/multiplayer/networkmanager.h" -#include "extensions/multiplayer/animdata.h" -#include "extensions/multiplayer/charactercloner.h" -#include "extensions/multiplayer/charactercustomizer.h" +#include "extensions/common/animdata.h" +#include "extensions/common/charactercustomizer.h" +#include "extensions/multiplayer/namebubblerenderer.h" +#include "extensions/thirdpersoncamera.h" +#include "extensions/thirdpersoncamera/controller.h" #include "legoanimationmanager.h" #include "legogamestate.h" #include "legomain.h" @@ -17,7 +19,10 @@ #include #include +using namespace Extensions; using namespace Multiplayer; +using Common::DetectVehicleType; +using Common::IsMultiPartEmote; template void NetworkManager::SendMessage(const T& p_msg) @@ -34,12 +39,11 @@ void NetworkManager::SendMessage(const T& p_msg) } NetworkManager::NetworkManager() - : m_transport(nullptr), m_callbacks(nullptr), m_localPeerId(0), m_hostPeerId(0), m_sequence(0), - m_lastBroadcastTime(0), m_lastValidActorId(0), m_localWalkAnimId(0), m_localIdleAnimId(0), - m_localDisplayActorIndex(DISPLAY_ACTOR_NONE), m_displayActorFrozen(false), m_localAllowRemoteCustomize(true), - 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_showNameBubbles(true) + : m_transport(nullptr), m_callbacks(nullptr), m_localNameBubble(nullptr), m_localPeerId(0), m_hostPeerId(0), + m_sequence(0), m_lastBroadcastTime(0), m_lastValidActorId(0), m_localAllowRemoteCustomize(true), + 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_showNameBubbles(true), m_lastCameraEnabled(false) { } @@ -48,20 +52,38 @@ NetworkManager::~NetworkManager() Shutdown(); } +static ThirdPersonCamera::Controller* GetCamera() +{ + return ThirdPersonCameraExt::GetCamera(); +} + MxResult NetworkManager::Tickle() { - // Derive display actor early so it is valid before ProcessPendingRequests - // may toggle the 3rd-person camera (which needs a valid display actor index). - { - LegoPathActor* userActor = UserActor(); - if (userActor) { - DeriveDisplayActorIndex(static_cast(userActor)->GetActorId()); + ProcessPendingRequests(); + + // Detect camera state changes for platform notification + ThirdPersonCamera::Controller* cam = GetCamera(); + if (cam) { + bool cameraEnabled = cam->IsEnabled(); + if (cameraEnabled != m_lastCameraEnabled) { + m_lastCameraEnabled = cameraEnabled; + NotifyThirdPersonChanged(cameraEnabled); + } + + // Create local name bubble when display ROI becomes available + if (m_showNameBubbles && !m_localNameBubble && cam->GetDisplayROI()) { + char name[8]; + EncodeUsername(name); + m_localNameBubble = new NameBubbleRenderer(); + m_localNameBubble->Create(name); + } + + // Update local name bubble position + if (m_localNameBubble && cam->GetDisplayROI()) { + m_localNameBubble->Update(cam->GetDisplayROI()); } } - ProcessPendingRequests(); - m_thirdPersonCamera.Tick(0.016f); - if (!m_transport) { return SUCCESS; } @@ -121,6 +143,9 @@ void NetworkManager::Shutdown() m_worldSync.SetTransport(nullptr); } + delete m_localNameBubble; + m_localNameBubble = nullptr; + RemoveAllRemotePlayers(); } @@ -156,7 +181,6 @@ void NetworkManager::OnWorldEnabled(LegoWorld* p_world) } if (p_world->GetWorldId() == LegoOmni::e_act1) { - m_thirdPersonCamera.OnWorldEnabled(p_world); m_inIsleWorld = true; m_worldSync.SetInIsleWorld(true); @@ -188,9 +212,16 @@ void NetworkManager::OnWorldDisabled(LegoWorld* p_world) } if (p_world->GetWorldId() == LegoOmni::e_act1) { - m_thirdPersonCamera.OnWorldDisabled(p_world); m_inIsleWorld = false; m_worldSync.SetInIsleWorld(false); + + // Destroy local name bubble (ROI is about to be destroyed) + if (m_localNameBubble) { + m_localNameBubble->Destroy(); + delete m_localNameBubble; + m_localNameBubble = nullptr; + } + for (auto& [peerId, player] : m_remotePlayers) { player->SetVisible(false); player->SetNameBubbleVisible(false); @@ -238,29 +269,35 @@ MxBool NetworkManager::HandleSkyLightMutation(uint8_t p_entityType, uint8_t p_ch void NetworkManager::ProcessPendingRequests() { - if (m_pendingToggleThirdPerson.exchange(false, std::memory_order_relaxed)) { - if (m_thirdPersonCamera.IsEnabled()) { - m_thirdPersonCamera.Disable(); + ThirdPersonCamera::Controller* cam = GetCamera(); + + // Camera-dependent requests: only consume when cam is available so + // the request survives until the camera exists. + if (cam) { + if (m_pendingToggleThirdPerson.exchange(false, std::memory_order_relaxed)) { + if (cam->IsEnabled()) { + cam->Disable(); + } + else { + cam->Enable(); + } + NotifyThirdPersonChanged(cam->IsEnabled()); } - else { - m_thirdPersonCamera.Enable(); + + int walkAnim = m_pendingWalkAnim.exchange(-1, std::memory_order_relaxed); + if (walkAnim >= 0) { + SetWalkAnimation(static_cast(walkAnim)); } - NotifyThirdPersonChanged(m_thirdPersonCamera.IsEnabled()); - } - int walkAnim = m_pendingWalkAnim.exchange(-1, std::memory_order_relaxed); - if (walkAnim >= 0) { - SetWalkAnimation(static_cast(walkAnim)); - } + int idleAnim = m_pendingIdleAnim.exchange(-1, std::memory_order_relaxed); + if (idleAnim >= 0) { + SetIdleAnimation(static_cast(idleAnim)); + } - int idleAnim = m_pendingIdleAnim.exchange(-1, std::memory_order_relaxed); - if (idleAnim >= 0) { - SetIdleAnimation(static_cast(idleAnim)); - } - - int emote = m_pendingEmote.exchange(-1, std::memory_order_relaxed); - if (emote >= 0) { - SendEmote(static_cast(emote)); + int emote = m_pendingEmote.exchange(-1, std::memory_order_relaxed); + if (emote >= 0) { + SendEmote(static_cast(emote)); + } } if (m_pendingToggleAllowCustomize.exchange(false, std::memory_order_relaxed)) { @@ -273,7 +310,9 @@ void NetworkManager::ProcessPendingRequests() for (auto& [peerId, player] : m_remotePlayers) { player->SetNameBubbleVisible(m_showNameBubbles); } - m_thirdPersonCamera.SetNameBubbleVisible(m_showNameBubbles); + if (m_localNameBubble) { + m_localNameBubble->SetVisible(m_showNameBubbles); + } NotifyNameBubblesChanged(m_showNameBubbles); } } @@ -313,6 +352,8 @@ void NetworkManager::BroadcastLocalState() return; } + ThirdPersonCamera::Controller* cam = GetCamera(); + PlayerStateMsg msg{}; msg.header = {MSG_STATE, m_localPeerId, m_sequence++, TARGET_BROADCAST}; msg.actorId = actorId; @@ -322,27 +363,29 @@ void NetworkManager::BroadcastLocalState() SDL_memcpy(msg.direction, dir, sizeof(msg.direction)); SDL_memcpy(msg.up, up, sizeof(msg.up)); msg.speed = speed; - msg.walkAnimId = m_localWalkAnimId; - msg.idleAnimId = m_localIdleAnimId; EncodeUsername(msg.name); - msg.displayActorIndex = m_localDisplayActorIndex; + if (cam) { + msg.walkAnimId = cam->GetWalkAnimId(); + msg.idleAnimId = cam->GetIdleAnimId(); + msg.displayActorIndex = cam->GetDisplayActorIndex(); + cam->GetCustomizeState().Pack(msg.customizeData); - m_thirdPersonCamera.GetCustomizeState().Pack(msg.customizeData); - msg.customizeFlags = m_localAllowRemoteCustomize ? 0x01 : 0x00; + // Encode multi-part emote frozen state (0x02 = frozen, emote ID in bits 2-4, max 8 emotes) + int8_t frozenId = cam->GetFrozenEmoteId(); + if (frozenId >= 0) { + msg.customizeFlags |= 0x02; + msg.customizeFlags |= (frozenId & 0x07) << 2; + } - // Encode multi-part emote frozen state (0x02 = frozen, emote ID in bits 2-4, max 8 emotes) - int8_t frozenId = m_thirdPersonCamera.GetFrozenEmoteId(); - if (frozenId >= 0) { - msg.customizeFlags |= 0x02; - msg.customizeFlags |= (frozenId & 0x07) << 2; + // Zero speed when in any phase of a multi-part emote + if (cam->IsInMultiPartEmote()) { + msg.speed = 0.0f; + } } - // Zero speed when in any phase of a multi-part emote - if (m_thirdPersonCamera.IsInMultiPartEmote()) { - msg.speed = 0.0f; - } + msg.customizeFlags |= m_localAllowRemoteCustomize ? 0x01 : 0x00; SendMessage(msg); } @@ -534,33 +577,38 @@ void NetworkManager::HandleHostAssign(const HostAssignMsg& p_msg) void NetworkManager::SetWalkAnimation(uint8_t p_walkAnimId) { - if (p_walkAnimId < g_walkAnimCount) { - m_localWalkAnimId = p_walkAnimId; - m_thirdPersonCamera.SetWalkAnimId(p_walkAnimId); + ThirdPersonCamera::Controller* cam = GetCamera(); + if (cam && p_walkAnimId < Common::g_walkAnimCount) { + cam->SetWalkAnimId(p_walkAnimId); } } void NetworkManager::SetIdleAnimation(uint8_t p_idleAnimId) { - if (p_idleAnimId < g_idleAnimCount) { - m_localIdleAnimId = p_idleAnimId; - m_thirdPersonCamera.SetIdleAnimId(p_idleAnimId); + ThirdPersonCamera::Controller* cam = GetCamera(); + if (cam && p_idleAnimId < Common::g_idleAnimCount) { + cam->SetIdleAnimId(p_idleAnimId); } } void NetworkManager::SendEmote(uint8_t p_emoteId) { - if (p_emoteId >= g_emoteAnimCount) { + if (p_emoteId >= Common::g_emoteAnimCount) { + return; + } + + ThirdPersonCamera::Controller* cam = GetCamera(); + if (!cam) { return; } // Multi-part emotes require 3rd person camera to be active (they need the display clone). // In 1st person mode, skip them entirely to avoid broadcasting an emote the local player can't play. - if (!m_thirdPersonCamera.IsActive() && IsMultiPartEmote(p_emoteId)) { + if (!cam->IsActive() && IsMultiPartEmote(p_emoteId)) { return; } - m_thirdPersonCamera.TriggerEmote(p_emoteId); + cam->TriggerEmote(p_emoteId); EmoteMsg msg{}; msg.header = {MSG_EMOTE, m_localPeerId, m_sequence++, TARGET_BROADCAST}; @@ -568,25 +616,6 @@ void NetworkManager::SendEmote(uint8_t p_emoteId) SendMessage(msg); } -void NetworkManager::SetDisplayActorIndex(uint8_t p_displayActorIndex) -{ - m_localDisplayActorIndex = p_displayActorIndex; - m_displayActorFrozen = true; - m_thirdPersonCamera.SetDisplayActorIndex(p_displayActorIndex); -} - -void NetworkManager::DeriveDisplayActorIndex(uint8_t p_actorId) -{ - if (m_displayActorFrozen || !IsValidActorId(p_actorId)) { - return; - } - uint8_t derived = p_actorId - 1; - if (derived != m_localDisplayActorIndex) { - m_localDisplayActorIndex = derived; - m_thirdPersonCamera.SetDisplayActorIndex(derived); - } -} - void NetworkManager::HandleEmote(const EmoteMsg& p_msg) { uint32_t peerId = p_msg.header.peerId; @@ -697,12 +726,6 @@ bool NetworkManager::IsClonedCharacter(const char* p_name) const } } - // Check local 3rd-person display actor clone - if (m_thirdPersonCamera.GetDisplayROI() != nullptr && - !SDL_strcasecmp(m_thirdPersonCamera.GetDisplayROI()->GetName(), p_name)) { - return true; - } - return false; } @@ -721,7 +744,7 @@ void NetworkManager::HandleCustomize(const CustomizeMsg& p_msg) uint32_t targetPeerId = p_msg.targetPeerId; // Check if the target is a remote player on this client. - // Only play effects here — do NOT modify the remote player's customize state. + // Only play effects here -- do NOT modify the remote player's customize state. // State changes come exclusively through UpdateFromNetwork (from the target's // authoritative PlayerStateMsg), which prevents flip-flop from stale state messages. // Note: sound/mood feedback uses the old state (before the authoritative update arrives), @@ -729,15 +752,17 @@ void NetworkManager::HandleCustomize(const CustomizeMsg& p_msg) auto it = m_remotePlayers.find(targetPeerId); if (it != m_remotePlayers.end()) { if (it->second->GetROI()) { - CharacterCustomizer::PlayClickSound( + Common::CharacterCustomizer::PlayClickSound( it->second->GetROI(), it->second->GetCustomizeState(), p_msg.changeType == CHANGE_MOOD ); if (!it->second->IsMoving() && !it->second->IsInMultiPartEmote()) { it->second->StopClickAnimation(); - MxU32 clickAnimId = - CharacterCustomizer::PlayClickAnimation(it->second->GetROI(), it->second->GetCustomizeState()); + MxU32 clickAnimId = Common::CharacterCustomizer::PlayClickAnimation( + it->second->GetROI(), + it->second->GetCustomizeState() + ); it->second->SetClickAnimObjectId(clickAnimId); } } @@ -751,31 +776,33 @@ void NetworkManager::HandleCustomize(const CustomizeMsg& p_msg) return; } + ThirdPersonCamera::Controller* cam = GetCamera(); + if (!cam) { + return; + } + // ApplyCustomizeChange handles null display ROI (advances state without visual) - m_thirdPersonCamera.ApplyCustomizeChange(p_msg.changeType, p_msg.partIndex); + cam->ApplyCustomizeChange(p_msg.changeType, p_msg.partIndex); // Use display ROI for effects in 3rd person, native ROI in 1st person - LegoROI* effectROI = m_thirdPersonCamera.GetDisplayROI(); + LegoROI* effectROI = cam->GetDisplayROI(); if (!effectROI && UserActor()) { effectROI = UserActor()->GetROI(); } if (effectROI) { - CharacterCustomizer::PlayClickSound( + Common::CharacterCustomizer::PlayClickSound( effectROI, - m_thirdPersonCamera.GetCustomizeState(), + cam->GetCustomizeState(), p_msg.changeType == CHANGE_MOOD ); // Only play click animation in 3rd person (not visible in 1st person or multi-part emote) - if (m_thirdPersonCamera.GetDisplayROI() && !m_thirdPersonCamera.IsInVehicle() && - !m_thirdPersonCamera.IsInMultiPartEmote()) { - m_thirdPersonCamera.StopClickAnimation(); - MxU32 clickAnimId = CharacterCustomizer::PlayClickAnimation( - m_thirdPersonCamera.GetDisplayROI(), - m_thirdPersonCamera.GetCustomizeState() - ); - m_thirdPersonCamera.SetClickAnimObjectId(clickAnimId); + if (cam->GetDisplayROI() && !cam->IsInVehicle() && !cam->IsInMultiPartEmote()) { + cam->StopClickAnimation(); + MxU32 clickAnimId = + Common::CharacterCustomizer::PlayClickAnimation(cam->GetDisplayROI(), cam->GetCustomizeState()); + cam->SetClickAnimObjectId(clickAnimId); } } } diff --git a/extensions/src/multiplayer/remoteplayer.cpp b/extensions/src/multiplayer/remoteplayer.cpp index 2798932e..3d379c4b 100644 --- a/extensions/src/multiplayer/remoteplayer.cpp +++ b/extensions/src/multiplayer/remoteplayer.cpp @@ -1,8 +1,9 @@ #include "extensions/multiplayer/remoteplayer.h" #include "3dmanager/lego3dmanager.h" -#include "extensions/multiplayer/charactercloner.h" -#include "extensions/multiplayer/charactercustomizer.h" +#include "extensions/common/charactercloner.h" +#include "extensions/common/charactercustomizer.h" +#include "extensions/multiplayer/namebubblerenderer.h" #include "legocharactermanager.h" #include "legovideomanager.h" #include "legoworld.h" @@ -15,14 +16,20 @@ #include #include +using namespace Extensions; using namespace Multiplayer; +using Common::DetectVehicleType; +using Common::g_idleAnimCount; +using Common::g_vehicleROINames; +using Common::g_walkAnimCount; +using Common::IsLargeVehicle; RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displayActorIndex) : m_peerId(p_peerId), m_actorId(p_actorId), m_displayActorIndex(p_displayActorIndex), m_roi(nullptr), m_spawned(false), m_visible(false), m_targetSpeed(0.0f), m_targetVehicleType(VEHICLE_NONE), m_targetWorldId(-1), m_lastUpdateTime(SDL_GetTicks()), m_hasReceivedUpdate(false), - m_animator(CharacterAnimatorConfig{/*.saveEmoteTransform=*/false}), m_vehicleROI(nullptr), - m_allowRemoteCustomize(true) + m_animator(Common::CharacterAnimatorConfig{/*.saveEmoteTransform=*/false}), m_vehicleROI(nullptr), + m_nameBubble(nullptr), m_allowRemoteCustomize(true) { m_displayName[0] = '\0'; const char* displayName = GetDisplayActorName(); @@ -62,7 +69,7 @@ void RemotePlayer::Spawn(LegoWorld* p_isleWorld) return; } - m_roi = CharacterCloner::Clone(charMgr, m_uniqueName, actorName); + m_roi = Common::CharacterCloner::Clone(charMgr, m_uniqueName, actorName); if (!m_roi) { return; } @@ -75,7 +82,7 @@ void RemotePlayer::Spawn(LegoWorld* p_isleWorld) m_visible = false; // Initialize customize state from the display actor's info - uint8_t actorInfoIndex = CharacterCustomizer::ResolveActorInfoIndex(m_displayActorIndex); + uint8_t actorInfoIndex = Common::CharacterCustomizer::ResolveActorInfoIndex(m_displayActorIndex); m_customizeState.InitFromActorInfo(actorInfoIndex); // Build initial walk and idle animation caches @@ -150,14 +157,14 @@ void RemotePlayer::UpdateFromNetwork(const PlayerStateMsg& p_msg) } // Update customize state from packed data - CustomizeState newState; + Common::CustomizeState newState; newState.Unpack(p_msg.customizeData); if (newState != m_customizeState) { - uint8_t actorInfoIndex = CharacterCustomizer::ResolveActorInfoIndex(m_displayActorIndex); + uint8_t actorInfoIndex = Common::CharacterCustomizer::ResolveActorInfoIndex(m_displayActorIndex); m_customizeState = newState; if (m_spawned && m_roi) { - CharacterCustomizer::ApplyFullState(m_roi, actorInfoIndex, m_customizeState); + Common::CharacterCustomizer::ApplyFullState(m_roi, actorInfoIndex, m_customizeState); } } @@ -198,7 +205,9 @@ void RemotePlayer::Tick(float p_deltaTime) m_animator.Tick(p_deltaTime, m_roi, isMoving); // Update name bubble position and billboard orientation - m_animator.UpdateNameBubble(m_roi); + if (m_nameBubble) { + m_nameBubble->Update(m_roi); + } } void RemotePlayer::ReAddToScene() @@ -346,17 +355,26 @@ void RemotePlayer::ExitVehicle() void RemotePlayer::CreateNameBubble() { - m_animator.CreateNameBubble(m_displayName); + if (!m_nameBubble) { + m_nameBubble = new NameBubbleRenderer(); + } + m_nameBubble->Create(m_displayName); } void RemotePlayer::DestroyNameBubble() { - m_animator.DestroyNameBubble(); + if (m_nameBubble) { + m_nameBubble->Destroy(); + delete m_nameBubble; + m_nameBubble = nullptr; + } } void RemotePlayer::SetNameBubbleVisible(bool p_visible) { - m_animator.SetNameBubbleVisible(p_visible); + if (m_nameBubble) { + m_nameBubble->SetVisible(p_visible); + } } void RemotePlayer::StopClickAnimation() diff --git a/extensions/src/multiplayer/server/gameroom.ts b/extensions/src/multiplayer/server/gameroom.ts index a78b5701..94829bf0 100644 --- a/extensions/src/multiplayer/server/gameroom.ts +++ b/extensions/src/multiplayer/server/gameroom.ts @@ -66,8 +66,6 @@ export class GameRoom implements DurableObject { return new Response(null, { status: 101, webSocket: client }); } - // ---- HTTP API ---- - private async handleHttpRequest(request: Request): Promise { const method = request.method.toUpperCase(); @@ -132,8 +130,6 @@ export class GameRoom implements DurableObject { }); } - // ---- Connection lifecycle ---- - private assignHostIfNeeded(peerId: number, ws: WebSocket): void { if (this.hostPeerId === 0 || !this.connections.has(this.hostPeerId)) { this.hostPeerId = peerId; @@ -152,8 +148,6 @@ export class GameRoom implements DurableObject { } } - // ---- Message routing ---- - private handleMessage(event: MessageEvent, peerId: number): void { if (!(event.data instanceof ArrayBuffer)) { return; @@ -194,8 +188,6 @@ export class GameRoom implements DurableObject { } } - // ---- Broadcasting ---- - private broadcast(msg: ArrayBuffer): void { for (const ws of this.connections.values()) { this.trySend(ws, msg); @@ -221,8 +213,6 @@ export class GameRoom implements DurableObject { } } - // ---- Host election ---- - private electNewHost(): void { this.hostPeerId = 0; diff --git a/extensions/src/multiplayer/thirdpersoncamera.cpp b/extensions/src/multiplayer/thirdpersoncamera.cpp deleted file mode 100644 index 2bc2acc8..00000000 --- a/extensions/src/multiplayer/thirdpersoncamera.cpp +++ /dev/null @@ -1,1052 +0,0 @@ -#include "extensions/multiplayer/thirdpersoncamera.h" - -#include "3dmanager/lego3dmanager.h" -#include "anim/legoanim.h" -#include "extensions/multiplayer/charactercloner.h" -#include "extensions/multiplayer/charactercustomizer.h" -#include "islepathactor.h" -#include "legocameracontroller.h" -#include "legocharactermanager.h" -#include "legoinputmanager.h" -#include "legonavcontroller.h" -#include "legopathactor.h" -#include "legovideomanager.h" -#include "legoworld.h" -#include "misc.h" -#include "misc/legotree.h" -#include "mxgeometry/mxgeometry3d.h" -#include "mxgeometry/mxmatrix.h" -#include "realtime/realtime.h" -#include "realtime/vector.h" -#include "roi/legoroi.h" - -#include -#include - -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) -{ - Vector3 right(p_mat[0]); - Vector3 up(p_mat[1]); - Vector3 direction(p_mat[2]); - direction *= -1.0f; - right.EqualsCross(up, direction); -} - -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_orbitPitch(DEFAULT_ORBIT_PITCH), m_orbitDistance(DEFAULT_ORBIT_DISTANCE), m_absoluteYaw(DEFAULT_ORBIT_YAW), - m_smoothedSpeed(0.0f), m_touch{}, m_wantsAutoDisable(false), m_wantsAutoEnable(false) -{ - SDL_memset(m_displayUniqueName, 0, sizeof(m_displayUniqueName)); -} - -void ThirdPersonCamera::Enable() -{ - m_enabled = true; - ReinitForCharacter(); -} - -void ThirdPersonCamera::Disable() -{ - m_enabled = false; - - if (m_active && m_playerROI) { - LegoPathActor* userActor = UserActor(); - LegoWorld* world = CurrentWorld(); - - m_playerROI->SetVisibility(FALSE); - VideoManager()->Get3DManager()->Remove(*m_playerROI); - - // Restore vanilla 1st-person camera (eye-height offset, same as ResetWorldTransform). - if (userActor && world && world->GetCameraController()) { - world->GetCameraController()->SetWorldTransform( - Mx3DPointFloat(0.0F, 1.25F, 0.0F), - Mx3DPointFloat(0.0F, 0.0F, 1.0F), - Mx3DPointFloat(0.0F, 1.0F, 0.0F) - ); - userActor->TransformPointOfView(); - } - } - - m_active = false; - m_pendingWorldTransition = false; - DestroyNameBubble(); - m_animator.StopROISounds(); - DestroyDisplayClone(); - m_animator.ClearRideAnimation(); - m_animator.ClearAll(); - - ResetOrbitState(); -} - -void ThirdPersonCamera::OnActorEnter(IslePathActor* p_actor) -{ - LegoPathActor* userActor = UserActor(); - if (static_cast(p_actor) != userActor) { - return; - } - - // Always track vehicle type so OnActorExit can handle exits - // even if Enable() was called after entering the vehicle. - m_animator.SetCurrentVehicleType(DetectVehicleType(userActor)); - - if (!m_enabled) { - return; - } - - // During a world transition, the ROI position is stale (PlaceActor hasn't - // run yet). Skip camera setup — the stale orbit view would freeze on - // screen during the ~500ms world load. ApplyOrbitCamera in the first - // Tick after PlaceActor handles camera setup naturally. - if (m_pendingWorldTransition && m_active) { - return; - } - - LegoROI* newROI = userActor->GetROI(); - if (!newROI) { - return; - } - - if (m_animator.GetCurrentVehicleType() != VEHICLE_NONE) { - // Large vehicles and helicopter: stay first-person. - if (IsLargeVehicle(m_animator.GetCurrentVehicleType()) || - m_animator.GetCurrentVehicleType() == VEHICLE_HELICOPTER) { - // Hide walking character ROI (Enter doesn't call Exit on it). - if (m_playerROI) { - m_playerROI->SetVisibility(FALSE); - VideoManager()->Get3DManager()->Remove(*m_playerROI); - } - DestroyNameBubble(); - m_active = false; - return; - } - - // Small vehicle: need the character ROI for ride animations. - if (!m_playerROI) { - return; - } - - m_active = true; - SetupCamera(userActor); - m_animator.BuildRideAnimation(m_animator.GetCurrentVehicleType(), m_playerROI, 0); - CreateNameBubble(); - return; - } - - // Walking character entry. - newROI->SetVisibility(FALSE); - if (!EnsureDisplayROI()) { - return; - } - m_active = true; - - m_playerROI->SetVisibility(TRUE); - - // Re-add ROI so it renders in third-person (SpawnPlayer removes it). - VideoManager()->Get3DManager()->Remove(*m_playerROI); - VideoManager()->Get3DManager()->Add(*m_playerROI); - - // Build animation caches and reset state - m_animator.InitAnimCaches(m_playerROI); - m_animator.ResetAnimState(); - - m_animator.ApplyIdleFrame0(m_playerROI); - - SetupCamera(userActor); - CreateNameBubble(); -} - -void ThirdPersonCamera::OnActorExit(IslePathActor* p_actor) -{ - if (!m_enabled) { - return; - } - - // For vehicle exit, p_actor is the vehicle, not UserActor — - // check m_currentVehicleType instead. - if (m_animator.GetCurrentVehicleType() != VEHICLE_NONE) { - // Exiting a vehicle: reinitialize for the walking character. - m_animator.ClearRideAnimation(); - m_animator.ClearAll(); - ReinitForCharacter(); - } - else if (m_active && static_cast(p_actor) == UserActor()) { - // Exiting on foot: full teardown. - DestroyNameBubble(); - if (m_playerROI) { - m_playerROI->SetVisibility(FALSE); - VideoManager()->Get3DManager()->Remove(*m_playerROI); - } - m_animator.ClearRideAnimation(); - m_animator.ClearAll(); - m_playerROI = nullptr; - m_active = false; - } -} - -void ThirdPersonCamera::OnCamAnimEnd(LegoPathActor* p_actor) -{ - m_pendingWorldTransition = false; - - if (!m_active) { - return; - } - - // Cam anim end placed the actor via PlaceActor (forward-z). - // Restore the orbit camera. - SetupCamera(p_actor); -} - -void ThirdPersonCamera::Tick(float p_deltaTime) -{ - if (!m_active) { - return; - } - - if (!m_playerROI) { - return; - } - - // 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. 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 - // ApplyOrbitCamera would fight the cam anim each frame and, critically, - // if the cam anim is interrupted (space bar), its end handler reads - // ViewROI position to place the actor. Our orbit camera position - // (elevated, behind player) would cause the actor to be placed in the - // air. Once the player is released (first interruption resets actor - // state to c_initial), the orbit camera resumes immediately. - if (!UserActor() || UserActor()->GetActorState() != LegoPathActor::c_disabled) { - ApplyOrbitCamera(); - } - - m_animator.UpdateNameBubble(m_playerROI); - - // Small vehicle with ride animation - if (m_animator.GetCurrentVehicleType() != VEHICLE_NONE) { - m_animator.StopClickAnimation(); - if (m_animator.GetRideAnim() && m_animator.GetRideRoiMap()) { - LegoPathActor* actor = UserActor(); - if (!actor || !actor->GetROI()) { - return; - } - - // Force visibility of ride ROI map entries - AnimUtils::EnsureROIMapVisibility(m_animator.GetRideRoiMap(), m_animator.GetRideRoiMapSize()); - - // Only advance animation time when actually moving - float speed = actor->GetWorldSpeed(); - if (SDL_fabsf(speed) > 0.01f) { - m_animator.SetAnimTime(m_animator.GetAnimTime() + p_deltaTime * 2000.0f); - } - - // Use vehicle actor's transform as base, flipped to backward-z - // so the character mesh (which faces -z) renders correctly. - MxMatrix transform(actor->GetROI()->GetLocal2World()); - FlipMatrixDirection(transform); - - // Position character ROI at the vehicle for bone rendering. - m_playerROI->WrappedSetLocal2WorldWithWorldDataUpdate(transform); - m_playerROI->SetVisibility(TRUE); - - float duration = (float) m_animator.GetRideAnim()->GetDuration(); - if (duration > 0.0f) { - float timeInCycle = - m_animator.GetAnimTime() - duration * SDL_floorf(m_animator.GetAnimTime() / duration); - - LegoTreeNode* root = m_animator.GetRideAnim()->GetRoot(); - for (LegoU32 i = 0; i < root->GetNumChildren(); i++) { - LegoROI::ApplyAnimationTransformation( - root->GetChild(i), - transform, - (LegoTime) timeInCycle, - m_animator.GetRideRoiMap() - ); - } - } - } - return; - } - - LegoPathActor* userActor = UserActor(); - if (!userActor) { - return; - } - - // Sync display clone position from native ROI - if (m_displayROI && m_displayROI == m_playerROI) { - LegoROI* nativeROI = userActor->GetROI(); - if (nativeROI) { - MxMatrix mat(nativeROI->GetLocal2World()); - // Native ROI uses forward-z; flip to backward-z for the mesh. - FlipMatrixDirection(mat); - m_displayROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat); - VideoManager()->Get3DManager()->Moved(*m_displayROI); - } - } - - float speed = userActor->GetWorldSpeed(); - bool isMoving = SDL_fabsf(speed) > 0.01f; - if (m_animator.IsInMultiPartEmote()) { - isMoving = false; - userActor->SetWorldSpeed(0.0f); - NavController()->SetLinearVel(0.0f); - } - - m_animator.Tick(p_deltaTime, m_playerROI, isMoving); -} - -void ThirdPersonCamera::SetWalkAnimId(uint8_t p_walkAnimId) -{ - m_animator.SetWalkAnimId(p_walkAnimId, m_active ? m_playerROI : nullptr); -} - -void ThirdPersonCamera::SetIdleAnimId(uint8_t p_idleAnimId) -{ - m_animator.SetIdleAnimId(p_idleAnimId, m_active ? m_playerROI : nullptr); -} - -bool ThirdPersonCamera::IsInMultiPartEmote() const -{ - return m_animator.IsInMultiPartEmote(); -} - -int8_t ThirdPersonCamera::GetFrozenEmoteId() const -{ - return m_animator.GetFrozenEmoteId(); -} - -void ThirdPersonCamera::TriggerEmote(uint8_t p_emoteId) -{ - if (!m_active) { - return; - } - - LegoPathActor* userActor = UserActor(); - if (!userActor) { - return; - } - - bool isMoving = SDL_fabsf(userActor->GetWorldSpeed()) > 0.01f; - if (m_animator.IsInMultiPartEmote()) { - isMoving = false; - } - m_animator.TriggerEmote(p_emoteId, m_playerROI, isMoving); -} - -void ThirdPersonCamera::ApplyCustomizeChange(uint8_t p_changeType, uint8_t p_partIndex) -{ - uint8_t actorInfoIndex = CharacterCustomizer::ResolveActorInfoIndex(m_displayActorIndex); - - CharacterCustomizer::ApplyChange(m_displayROI, actorInfoIndex, m_customizeState, p_changeType, p_partIndex); -} - -void ThirdPersonCamera::StopClickAnimation() -{ - m_animator.StopClickAnimation(); -} - -void ThirdPersonCamera::OnWorldEnabled(LegoWorld* p_world) -{ - if (!p_world) { - return; - } - - if (!m_enabled) { - return; - } - - // Animation presenters may have been recreated. - m_animator.ClearAll(); - - // Reset orbit to default position behind the character. - ResetOrbitState(); - - // ReinitForCharacter runs BEFORE SpawnPlayer/PlaceActor, so the ROI - // position is stale. Set the flag so OnActorEnter and ReinitForCharacter - // defer camera setup. The first Tick after PlaceActor clears it, and - // ApplyOrbitCamera handles the camera naturally. - m_pendingWorldTransition = true; - - ReinitForCharacter(); -} - -void ThirdPersonCamera::OnWorldDisabled(LegoWorld* p_world) -{ - if (!p_world) { - return; - } - m_active = false; - m_pendingWorldTransition = false; - m_playerROI = nullptr; - DestroyNameBubble(); - m_animator.StopROISounds(); - DestroyDisplayClone(); - m_animator.ClearRideAnimation(); - 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(); - if (!world || !world->GetCameraController()) { - return; - } - - 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(); -} - -void ThirdPersonCamera::SetDisplayActorIndex(uint8_t p_displayActorIndex) -{ - if (m_displayActorIndex != p_displayActorIndex) { - m_customizeState.InitFromActorInfo(p_displayActorIndex); - } - m_displayActorIndex = p_displayActorIndex; -} - -bool ThirdPersonCamera::EnsureDisplayROI() -{ - if (!IsValidDisplayActorIndex(m_displayActorIndex)) { - return false; - } - if (!m_displayROI) { - CreateDisplayClone(); - } - if (!m_displayROI) { - return false; - } - m_playerROI = m_displayROI; - return true; -} - -void ThirdPersonCamera::CreateDisplayClone() -{ - if (!IsValidDisplayActorIndex(m_displayActorIndex)) { - return; - } - LegoCharacterManager* charMgr = CharacterManager(); - const char* actorName = charMgr->GetActorName(m_displayActorIndex); - if (!actorName) { - return; - } - SDL_snprintf(m_displayUniqueName, sizeof(m_displayUniqueName), "tp_display"); - m_displayROI = CharacterCloner::Clone(charMgr, m_displayUniqueName, actorName); - - if (m_displayROI) { - // Reapply existing customize state to the new clone (preserves state across world transitions). - // The state is only reset to defaults when the display actor index changes (SetDisplayActorIndex). - CharacterCustomizer::ApplyFullState(m_displayROI, m_displayActorIndex, m_customizeState); - } -} - -void ThirdPersonCamera::DestroyDisplayClone() -{ - m_animator.StopClickAnimation(); - if (m_displayROI) { - if (m_playerROI == m_displayROI) { - m_playerROI = nullptr; - } - VideoManager()->Get3DManager()->Remove(*m_displayROI); - CharacterManager()->ReleaseActor(m_displayUniqueName); - m_displayROI = nullptr; - } -} - -void ThirdPersonCamera::CreateNameBubble() -{ - char name[8] = {}; - EncodeUsername(name); - - if (name[0] == '\0') { - return; - } - - m_animator.CreateNameBubble(name); - - if (!m_showNameBubble) { - m_animator.SetNameBubbleVisible(false); - } -} - -void ThirdPersonCamera::DestroyNameBubble() -{ - m_animator.DestroyNameBubble(); -} - -void ThirdPersonCamera::SetNameBubbleVisible(bool p_visible) -{ - m_showNameBubble = p_visible; - m_animator.SetNameBubbleVisible(p_visible); -} - -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(p_yaw); - float cosY = SDL_cosf(p_yaw); - - p_at = Mx3DPointFloat( - m_orbitDistance * sinY * cosP, - ORBIT_TARGET_HEIGHT + m_orbitDistance * sinP, - -m_orbitDistance * cosY * cosP - ); - - // Direction points from camera toward the pivot (the character). - p_dir = Mx3DPointFloat(-sinY * cosP, -sinP, cosY * cosP); - - p_up = Mx3DPointFloat(0.0f, 1.0f, 0.0f); -} - -void ThirdPersonCamera::ApplyOrbitCamera() -{ - LegoPathActor* actor = UserActor(); - LegoWorld* world = CurrentWorld(); - if (!actor || !world || !world->GetCameraController()) { - return; - } - - // 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_orbitPitch = DEFAULT_ORBIT_PITCH; - m_orbitDistance = DEFAULT_ORBIT_DISTANCE; - m_absoluteYaw = DEFAULT_ORBIT_YAW; - m_smoothedSpeed = 0.0f; - m_touch = {}; -} - -void ThirdPersonCamera::ClampPitch() -{ - if (m_orbitPitch < MIN_PITCH) { - m_orbitPitch = MIN_PITCH; - } - if (m_orbitPitch > MAX_PITCH) { - m_orbitPitch = MAX_PITCH; - } -} - -void ThirdPersonCamera::ClampDistance() -{ - if (m_orbitDistance < MIN_DISTANCE) { - m_orbitDistance = MIN_DISTANCE; - } - if (m_orbitDistance > MAX_DISTANCE) { - m_orbitDistance = MAX_DISTANCE; - } -} - -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 and touch/joystick 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; - } - - // Mirror CalculateNewPosDir priority: only read joystick/virtual thumbstick - // when no keyboard or e_arrowKeys touch input is active. - if (keyFlags == 0 && inputManager) { - MxU32 joystickX, joystickY, povPosition; - if (inputManager->GetJoystickState(&joystickX, &joystickY, &povPosition) == SUCCESS) { - // Convert 0-100 range (center=50) to -1..1, applying deadzone - float jx = (joystickX - 50.0f) / 50.0f; - float jy = -(joystickY - 50.0f) / 50.0f; // negate: low Y = forward - - if (SDL_fabsf(jx) < 0.1f) { - jx = 0.0f; - } - if (SDL_fabsf(jy) < 0.1f) { - jy = 0.0f; - } - - moveDirX += camForwardX * jy + camRightX * jx; - moveDirZ += camForwardZ * jy + camRightZ * jx; - } - } - - // Normalize movement direction - float moveDirLen = SDL_sqrtf(moveDirX * moveDirX + moveDirZ * moveDirZ); - bool hasInput = moveDirLen > 0.001f; - - // Block translation during multi-part emotes (rotation/pan/zoom handled separately) - if (m_animator.IsInMultiPartEmote()) { - hasInput = false; - m_smoothedSpeed = 0.0f; - } - - 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; -} - -bool ThirdPersonCamera::TryClaimFinger(const SDL_TouchFingerEvent& event) -{ - if (!m_active || m_touch.count >= 2 || event.x < CAMERA_ZONE_X) { - return false; - } - - int idx = m_touch.count; - m_touch.id[idx] = event.fingerID; - m_touch.x[idx] = event.x; - m_touch.y[idx] = event.y; - m_touch.count++; - - if (m_touch.count == 2) { - float dx = m_touch.x[1] - m_touch.x[0]; - float dy = m_touch.y[1] - m_touch.y[0]; - m_touch.initialPinchDist = SDL_sqrtf(dx * dx + dy * dy); - } - - return true; -} - -bool ThirdPersonCamera::TryReleaseFinger(SDL_FingerID id) -{ - for (int i = 0; i < m_touch.count; i++) { - if (m_touch.id[i] == id) { - // Shift remaining finger down - if (i == 0 && m_touch.count == 2) { - m_touch.id[0] = m_touch.id[1]; - m_touch.x[0] = m_touch.x[1]; - m_touch.y[0] = m_touch.y[1]; - } - m_touch.count--; - m_touch.initialPinchDist = 0.0f; - return true; - } - } - return false; -} - -bool ThirdPersonCamera::IsFingerTracked(SDL_FingerID id) const -{ - for (int i = 0; i < m_touch.count; i++) { - if (m_touch.id[i] == id) { - return true; - } - } - return false; -} - -bool ThirdPersonCamera::ConsumeAutoDisable() -{ - return std::exchange(m_wantsAutoDisable, false); -} - -bool ThirdPersonCamera::ConsumeAutoEnable() -{ - return std::exchange(m_wantsAutoEnable, false); -} - -void ThirdPersonCamera::HandleSDLEvent(SDL_Event* p_event) -{ - switch (p_event->type) { - case SDL_EVENT_MOUSE_WHEEL: - if (!m_active) { - if (p_event->wheel.y < 0) { - m_wantsAutoEnable = true; - } - break; - } - if (m_orbitDistance <= MIN_DISTANCE && p_event->wheel.y > 0) { - m_wantsAutoDisable = true; - break; - } - m_orbitDistance -= p_event->wheel.y * 0.5f; - ClampDistance(); - break; - - case SDL_EVENT_MOUSE_MOTION: - if (!m_active) { - break; - } - if (p_event->motion.state & SDL_BUTTON_RMASK) { - 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: { - if (!m_active) { - break; - } - 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: { - // Finger may already be claimed via TryClaimFinger (called from HandleTouchInput). - // Only register if not already tracked and in the camera zone. - if (!IsFingerTracked(p_event->tfinger.fingerID) && m_touch.count < 2 && p_event->tfinger.x >= CAMERA_ZONE_X) { - int idx = m_touch.count; - m_touch.id[idx] = p_event->tfinger.fingerID; - m_touch.x[idx] = p_event->tfinger.x; - m_touch.y[idx] = p_event->tfinger.y; - m_touch.count++; - - if (m_touch.count == 2) { - float dx = m_touch.x[1] - m_touch.x[0]; - float dy = m_touch.y[1] - m_touch.y[0]; - m_touch.initialPinchDist = SDL_sqrtf(dx * dx + dy * dy); - } - } - break; - } - - case SDL_EVENT_FINGER_MOTION: { - if (m_touch.count == 1) { - if (!m_active) { - break; - } - // Single-finger drag: apply yaw/pitch rotation - if (m_touch.id[0] == p_event->tfinger.fingerID) { - float oldX = m_touch.x[0]; - float oldY = m_touch.y[0]; - m_touch.x[0] = p_event->tfinger.x; - m_touch.y[0] = p_event->tfinger.y; - - float moveX = m_touch.x[0] - oldX; - float moveY = m_touch.y[0] - oldY; - m_absoluteYaw -= moveX * 2.0f; - m_orbitPitch += moveY * 2.0f; - ClampPitch(); - } - } - else if (m_touch.count == 2) { - // Find which finger moved - int idx = -1; - for (int i = 0; i < 2; i++) { - if (m_touch.id[i] == p_event->tfinger.fingerID) { - idx = i; - break; - } - } - if (idx < 0) { - break; - } - - float oldX = m_touch.x[idx]; - float oldY = m_touch.y[idx]; - m_touch.x[idx] = p_event->tfinger.x; - m_touch.y[idx] = p_event->tfinger.y; - - // Pinch zoom - float dx = m_touch.x[1] - m_touch.x[0]; - float dy = m_touch.y[1] - m_touch.y[0]; - float newDist = SDL_sqrtf(dx * dx + dy * dy); - - if (m_touch.initialPinchDist > 0.001f) { - float pinchDelta = m_touch.initialPinchDist - newDist; - - if (!m_active) { - // Pinch together (zoom out) from 1st person → auto-enable 3rd person - if (pinchDelta > 0) { - m_wantsAutoEnable = true; - } - m_touch.initialPinchDist = newDist; - break; - } - - // Spread apart (zoom in) past min distance → auto-disable to 1st person - if (m_orbitDistance <= MIN_DISTANCE && pinchDelta < 0) { - m_wantsAutoDisable = true; - m_touch.initialPinchDist = newDist; - break; - } - - m_orbitDistance += pinchDelta * 15.0f; - ClampDistance(); - m_touch.initialPinchDist = newDist; - } - - // Two-finger drag for orbit (only when active) - float moveX = m_touch.x[idx] - oldX; - float moveY = m_touch.y[idx] - oldY; - m_absoluteYaw -= moveX * 2.0f; - m_orbitPitch += moveY * 2.0f; - ClampPitch(); - } - break; - } - - case SDL_EVENT_FINGER_UP: - case SDL_EVENT_FINGER_CANCELED: { - TryReleaseFinger(p_event->tfinger.fingerID); - break; - } - - default: - break; - } -} - -void ThirdPersonCamera::ReinitForCharacter() -{ - DestroyNameBubble(); - - LegoPathActor* userActor = UserActor(); - if (!userActor) { - m_active = false; - return; - } - - LegoROI* roi = userActor->GetROI(); - if (!roi) { - m_active = false; - return; - } - - int8_t vehicleType = DetectVehicleType(userActor); - - // Large vehicles and helicopter: stay first-person - if (vehicleType == VEHICLE_HELICOPTER || (vehicleType != VEHICLE_NONE && IsLargeVehicle(vehicleType))) { - m_active = false; - m_pendingWorldTransition = false; - return; - } - - m_animator.SetCurrentVehicleType(vehicleType); - - if (vehicleType != VEHICLE_NONE) { - if (!EnsureDisplayROI()) { - m_active = false; - return; - } - - if (!m_playerROI) { - m_active = false; - return; - } - - m_pendingWorldTransition = false; - - VideoManager()->Get3DManager()->Remove(*m_playerROI); - VideoManager()->Get3DManager()->Add(*m_playerROI); - m_active = true; - SetupCamera(userActor); - m_animator.BuildRideAnimation(vehicleType, m_playerROI, 0); - CreateNameBubble(); - return; - } - - // Reinitializing for walking character - roi->SetVisibility(FALSE); - if (!EnsureDisplayROI()) { - m_active = false; - return; - } - - m_playerROI->SetVisibility(TRUE); - - // Ensure the ROI is in the 3D manager. - VideoManager()->Get3DManager()->Remove(*m_playerROI); - VideoManager()->Get3DManager()->Add(*m_playerROI); - - m_animator.InitAnimCaches(m_playerROI); - m_animator.ResetAnimState(); - m_active = true; - - m_animator.ApplyIdleFrame0(m_playerROI); - - // During a world transition, PlaceActor hasn't run yet — the ROI is at - // a stale position. Defer camera setup; ApplyOrbitCamera in the first - // Tick after PlaceActor handles it. - if (!m_pendingWorldTransition) { - SetupCamera(userActor); - } - CreateNameBubble(); -} diff --git a/extensions/src/multiplayer/worldstatesync.cpp b/extensions/src/multiplayer/worldstatesync.cpp index 21e7ca8f..9eae76c8 100644 --- a/extensions/src/multiplayer/worldstatesync.cpp +++ b/extensions/src/multiplayer/worldstatesync.cpp @@ -159,8 +159,6 @@ void WorldStateSync::HandleWorldEventRequest(const WorldEventRequestMsg& p_msg) BroadcastWorldEvent(p_msg.entityType, p_msg.changeType, p_msg.entityIndex); } -// ---- Entity mutation routing ---- - template static int FindEntityIndex(TInfo* p_infoArray, MxS32 p_count, LegoEntity* p_entity) { @@ -225,8 +223,6 @@ MxBool WorldStateSync::HandleSkyLightMutation(uint8_t p_entityType, uint8_t p_ch } } -// ---- Send helpers ---- - void WorldStateSync::SendSnapshotRequest() { RequestSnapshotMsg msg{}; @@ -298,8 +294,6 @@ void WorldStateSync::SendWorldEventRequest(uint8_t p_entityType, uint8_t p_chang SendMessage(msg); } -// ---- Apply world events ---- - // Dispatch Switch*() calls shared by all entity types. // Returns true if the change was handled, false for type-specific changes. static bool DispatchEntitySwitch(LegoEntity* p_entity, uint8_t p_changeType) diff --git a/extensions/src/siloader.cpp b/extensions/src/siloader.cpp index 0768bf1a..4ed087c8 100644 --- a/extensions/src/siloader.cpp +++ b/extensions/src/siloader.cpp @@ -13,38 +13,38 @@ using namespace Extensions; const char prependedMarker[] = ";;prepended;;"; -std::map SiLoader::options; -std::vector SiLoader::files; -std::vector SiLoader::directives; -std::vector> SiLoader::startWith; -std::vector> SiLoader::removeWith; -std::vector> SiLoader::replace; -std::vector> SiLoader::prepend; -std::vector SiLoader::fullScreenMovie; -std::vector SiLoader::disable3d; -bool SiLoader::enabled = false; +std::map SiLoaderExt::options; +std::vector SiLoaderExt::files; +std::vector SiLoaderExt::directives; +std::vector> SiLoaderExt::startWith; +std::vector> SiLoaderExt::removeWith; +std::vector> SiLoaderExt::replace; +std::vector> SiLoaderExt::prepend; +std::vector SiLoaderExt::fullScreenMovie; +std::vector SiLoaderExt::disable3d; +bool SiLoaderExt::enabled = false; -void SiLoader::Initialize() +void SiLoaderExt::Initialize() { char* files = SDL_strdup(options["si loader:files"].c_str()); char* saveptr; for (char* file = SDL_strtok_r(files, ",\n\r ", &saveptr); file; file = SDL_strtok_r(NULL, ",\n\r ", &saveptr)) { - SiLoader::files.emplace_back(file); + SiLoaderExt::files.emplace_back(file); } char* directives = SDL_strdup(options["si loader:directives"].c_str()); for (char* directive = SDL_strtok_r(directives, ",\n\r ", &saveptr); directive; directive = SDL_strtok_r(NULL, ",\n\r ", &saveptr)) { - SiLoader::directives.emplace_back(directive); + SiLoaderExt::directives.emplace_back(directive); } SDL_free(files); SDL_free(directives); } -bool SiLoader::Load() +bool SiLoaderExt::Load() { for (const auto& file : files) { LoadFile(file.c_str()); @@ -57,7 +57,7 @@ bool SiLoader::Load() return true; } -std::optional SiLoader::HandleFind(StreamObject p_object, LegoWorld* world) +std::optional SiLoaderExt::HandleFind(StreamObject p_object, LegoWorld* world) { for (const auto& key : replace) { if (key.first == p_object) { @@ -68,7 +68,7 @@ std::optional SiLoader::HandleFind(StreamObject p_object, LegoWorld* wo return std::nullopt; } -std::optional SiLoader::HandleStart(MxDSAction& p_action) +std::optional SiLoaderExt::HandleStart(MxDSAction& p_action) { StreamObject object{p_action.GetAtomId(), p_action.GetObjectId()}; auto start = [](const StreamObject& p_object, MxDSAction& p_in, MxDSAction& p_out) -> MxResult { @@ -130,7 +130,7 @@ std::optional SiLoader::HandleStart(MxDSAction& p_action) return std::nullopt; } -MxBool SiLoader::HandleWorld(LegoWorld* p_world) +MxBool SiLoaderExt::HandleWorld(LegoWorld* p_world) { StreamObject object{p_world->GetAtomId(), p_world->GetEntityId()}; auto start = [](const StreamObject& p_object, MxDSAction& p_out) { @@ -154,7 +154,7 @@ MxBool SiLoader::HandleWorld(LegoWorld* p_world) return TRUE; } -std::optional SiLoader::HandleRemove(StreamObject p_object, LegoWorld* world) +std::optional SiLoaderExt::HandleRemove(StreamObject p_object, LegoWorld* world) { for (const auto& key : removeWith) { if (key.first == p_object) { @@ -171,7 +171,7 @@ std::optional SiLoader::HandleRemove(StreamObject p_object, LegoWorld* w return std::nullopt; } -std::optional SiLoader::HandleDelete(MxDSAction& p_action) +std::optional SiLoaderExt::HandleDelete(MxDSAction& p_action) { StreamObject object{p_action.GetAtomId(), p_action.GetObjectId()}; auto deleteObject = [](const StreamObject& p_object, MxDSAction& p_in, MxDSAction& p_out) { @@ -202,7 +202,7 @@ std::optional SiLoader::HandleDelete(MxDSAction& p_action) return std::nullopt; } -MxBool SiLoader::HandleEndAction(MxEndActionNotificationParam& p_param) +MxBool SiLoaderExt::HandleEndAction(MxEndActionNotificationParam& p_param) { StreamObject object{p_param.GetAction()->GetAtomId(), p_param.GetAction()->GetObjectId()}; auto start = [](const StreamObject& p_object, MxDSAction& p_in, MxDSAction& p_out) -> MxResult { @@ -235,7 +235,7 @@ MxBool SiLoader::HandleEndAction(MxEndActionNotificationParam& p_param) return TRUE; } -bool SiLoader::LoadFile(const char* p_file) +bool SiLoaderExt::LoadFile(const char* p_file) { si::Interleaf si; MxStreamController* controller; @@ -245,8 +245,7 @@ bool SiLoader::LoadFile(const char* p_file) if (si.Read(path.GetData(), si::Interleaf::ObjectsOnly) != si::Interleaf::ERROR_SUCCESS) { path = MxString(MxOmni::GetCD()) + p_file; path.MapPathToFilesystem(); - if (si.Read(path.GetData(), si::Interleaf::ObjectsOnly) != - si::Interleaf::ERROR_SUCCESS) { + if (si.Read(path.GetData(), si::Interleaf::ObjectsOnly) != si::Interleaf::ERROR_SUCCESS) { SDL_Log("Could not parse SI file %s", p_file); return false; } @@ -260,7 +259,7 @@ bool SiLoader::LoadFile(const char* p_file) return true; } -bool SiLoader::LoadDirective(const char* p_directive) +bool SiLoaderExt::LoadDirective(const char* p_directive) { char originAtom[256], targetAtom[256]; uint32_t originId, targetId; @@ -306,7 +305,7 @@ bool SiLoader::LoadDirective(const char* p_directive) return true; } -MxStreamController* SiLoader::OpenStream(const char* p_file) +MxStreamController* SiLoaderExt::OpenStream(const char* p_file) { MxStreamController* controller; @@ -318,7 +317,7 @@ MxStreamController* SiLoader::OpenStream(const char* p_file) return controller; } -void SiLoader::ParseExtra(const MxAtomId& p_atom, si::Core* p_core) +void SiLoaderExt::ParseExtra(const MxAtomId& p_atom, si::Core* p_core) { for (si::Core* child : p_core->GetChildren()) { if (si::Object* object = dynamic_cast(child)) { @@ -378,7 +377,7 @@ void SiLoader::ParseExtra(const MxAtomId& p_atom, si::Core* p_core) } } -bool SiLoader::IsWorld(const StreamObject& p_object) +bool SiLoaderExt::IsWorld(const StreamObject& p_object) { // The convention in LEGO Island is that world objects are always at ID 0 if (p_object.second == 0) { diff --git a/extensions/src/textureloader.cpp b/extensions/src/textureloader.cpp index d46485e1..7542db33 100644 --- a/extensions/src/textureloader.cpp +++ b/extensions/src/textureloader.cpp @@ -1,4 +1,5 @@ #include "extensions/textureloader.h" + #include "legovideomanager.h" #include "misc.h" #include "mxdirectx/mxdirect3d.h" @@ -7,11 +8,11 @@ using namespace Extensions; -std::map TextureLoader::options; -std::vector TextureLoader::excludedFiles; -bool TextureLoader::enabled = false; +std::map TextureLoaderExt::options; +std::vector TextureLoaderExt::excludedFiles; +bool TextureLoaderExt::enabled = false; -void TextureLoader::Initialize() +void TextureLoaderExt::Initialize() { for (const auto& option : defaults) { if (!options.count(option.first.data())) { @@ -20,7 +21,12 @@ void TextureLoader::Initialize() } } -bool TextureLoader::PatchTexture(LegoTextureInfo* p_textureInfo) +void TextureLoaderExt::AddExcludedFile(const std::string& p_file) +{ + excludedFiles.emplace_back(p_file); +} + +bool TextureLoaderExt::PatchTexture(LegoTextureInfo* p_textureInfo) { SDL_Surface* surface = FindTexture(p_textureInfo->m_name); if (!surface) { @@ -103,7 +109,7 @@ bool TextureLoader::PatchTexture(LegoTextureInfo* p_textureInfo) return true; } -SDL_Surface* TextureLoader::FindTexture(const char* p_name) +SDL_Surface* TextureLoaderExt::FindTexture(const char* p_name) { if (std::find(excludedFiles.begin(), excludedFiles.end(), p_name) != excludedFiles.end()) { return nullptr; diff --git a/extensions/src/thirdpersoncamera.cpp b/extensions/src/thirdpersoncamera.cpp new file mode 100644 index 00000000..754aefa0 --- /dev/null +++ b/extensions/src/thirdpersoncamera.cpp @@ -0,0 +1,241 @@ +#include "extensions/thirdpersoncamera.h" + +#include "extensions/common/charactercustomizer.h" +#include "extensions/thirdpersoncamera/controller.h" +#include "islepathactor.h" +#include "legoeventnotificationparam.h" +#include "legonavcontroller.h" +#include "legopathactor.h" +#include "legovideomanager.h" +#include "misc.h" +#include "mxcore.h" +#include "mxmisc.h" +#include "mxticklemanager.h" +#include "realtime/vector.h" +#include "roi/legoroi.h" + +#include + +using namespace Extensions; +using namespace Extensions::Common; + +std::map ThirdPersonCameraExt::options; +bool ThirdPersonCameraExt::enabled = false; +ThirdPersonCamera::Controller* ThirdPersonCameraExt::s_camera = nullptr; +bool ThirdPersonCameraExt::s_registered = false; +bool ThirdPersonCameraExt::s_inIsleWorld = false; + +namespace Extensions +{ +namespace ThirdPersonCamera +{ + +class TickleAdapter : public MxCore { +public: + TickleAdapter(Controller* p_camera) : m_camera(p_camera) {} + + MxResult Tickle() override + { + if (m_camera) { + m_camera->Tick(0.016f); + } + return SUCCESS; + } + + const char* ClassName() const override { return "ThirdPersonCamera::TickleAdapter"; } + +private: + Controller* m_camera; +}; + +} // namespace ThirdPersonCamera +} // namespace Extensions + +static Extensions::ThirdPersonCamera::TickleAdapter* s_tickleAdapter = nullptr; + +void ThirdPersonCameraExt::Initialize() +{ + if (!s_camera) { + s_camera = new ThirdPersonCamera::Controller(); + } + s_camera->Enable(); +} + +void ThirdPersonCameraExt::HandleCreate() +{ + if (!s_registered && s_camera) { + s_tickleAdapter = new Extensions::ThirdPersonCamera::TickleAdapter(s_camera); + TickleManager()->RegisterClient(s_tickleAdapter, 10); + s_registered = true; + } +} + +MxBool ThirdPersonCameraExt::HandleWorldEnable(LegoWorld* p_world, MxBool p_enable) +{ + if (!s_camera) { + return FALSE; + } + + if (p_enable) { + s_camera->OnWorldEnabled(p_world); + s_inIsleWorld = true; + } + else { + s_camera->OnWorldDisabled(p_world); + s_inIsleWorld = false; + } + + return TRUE; +} + +void ThirdPersonCameraExt::HandleActorEnter(IslePathActor* p_actor) +{ + if (s_camera) { + s_camera->OnActorEnter(p_actor); + } +} + +void ThirdPersonCameraExt::HandleActorExit(IslePathActor* p_actor) +{ + if (s_camera) { + s_camera->OnActorExit(p_actor); + } +} + +void ThirdPersonCameraExt::HandleCamAnimEnd(LegoPathActor* p_actor) +{ + if (s_camera) { + s_camera->OnCamAnimEnd(p_actor); + } +} + +void ThirdPersonCameraExt::OnSDLEvent(SDL_Event* p_event) +{ + if (!s_camera || !s_inIsleWorld) { + return; + } + + s_camera->HandleSDLEventImpl(p_event); + + if (s_camera->ConsumeAutoDisable()) { + s_camera->Disable(); + } + else if (s_camera->ConsumeAutoEnable()) { + s_camera->ResetTouchState(); + s_camera->SetOrbitDistance(ThirdPersonCamera::Controller::MIN_DISTANCE); + s_camera->Enable(); + } +} + +MxBool ThirdPersonCameraExt::IsThirdPersonCameraActive() +{ + if (s_camera && s_camera->IsActive()) { + return TRUE; + } + + return FALSE; +} + +MxBool ThirdPersonCameraExt::HandleTouchInput(SDL_Event* p_event) +{ + if (!s_camera || !s_camera->IsActive()) { + return FALSE; + } + + switch (p_event->type) { + case SDL_EVENT_FINGER_DOWN: + if (s_camera->TryClaimFinger(p_event->tfinger)) { + return TRUE; + } + return FALSE; + + case SDL_EVENT_FINGER_MOTION: + if (s_camera->IsFingerTracked(p_event->tfinger.fingerID)) { + return TRUE; + } + return FALSE; + + case SDL_EVENT_FINGER_UP: + case SDL_EVENT_FINGER_CANCELED: + if (s_camera->TryReleaseFinger(p_event->tfinger.fingerID)) { + return TRUE; + } + return FALSE; + + default: + return FALSE; + } +} + +MxBool ThirdPersonCameraExt::HandleNavOverride( + LegoNavController* p_nav, + const Vector3& p_curPos, + const Vector3& p_curDir, + Vector3& p_newPos, + Vector3& p_newDir, + float p_deltaTime +) +{ + if (!s_camera) { + return FALSE; + } + + if (!s_camera->IsActive()) { + return FALSE; + } + + return s_camera->HandleCameraRelativeMovement(p_nav, p_curPos, p_curDir, p_newPos, p_newDir, p_deltaTime); +} + +MxBool ThirdPersonCameraExt::HandleROIClick(LegoROI* p_rootROI, LegoEventNotificationParam& p_param) +{ + if (!s_camera) { + return FALSE; + } + + if (!s_camera->GetDisplayROI() || s_camera->GetDisplayROI() != p_rootROI) { + return FALSE; + } + + uint8_t changeType; + int partIndex; + if (!CharacterCustomizer::ResolveClickChangeType(changeType, partIndex, p_param.GetROI())) { + return TRUE; + } + + s_camera->ApplyCustomizeChange(changeType, static_cast(partIndex >= 0 ? partIndex : 0xFF)); + + LegoROI* effectROI = s_camera->GetDisplayROI(); + if (effectROI) { + CharacterCustomizer::PlayClickSound(effectROI, s_camera->GetCustomizeState(), changeType == CHANGE_MOOD); + if (!s_camera->IsInVehicle() && !s_camera->IsInMultiPartEmote()) { + s_camera->StopClickAnimation(); + MxU32 clickAnimId = CharacterCustomizer::PlayClickAnimation(effectROI, s_camera->GetCustomizeState()); + s_camera->SetClickAnimObjectId(clickAnimId); + } + } + return TRUE; +} + +MxBool ThirdPersonCameraExt::IsClonedCharacter(const char* p_name) +{ + if (!s_camera) { + return FALSE; + } + + if (s_camera->GetDisplayROI() != nullptr && !SDL_strcasecmp(s_camera->GetDisplayROI()->GetName(), p_name)) { + return TRUE; + } + + return FALSE; +} + +ThirdPersonCamera::Controller* ThirdPersonCameraExt::GetCamera() +{ + return s_camera; +} + +void ThirdPersonCameraExt::HandleSDLEvent(SDL_Event* p_event) +{ + Extension::Call(TP::HandleSDLEvent, p_event); +} diff --git a/extensions/src/thirdpersoncamera/controller.cpp b/extensions/src/thirdpersoncamera/controller.cpp new file mode 100644 index 00000000..c6d77a32 --- /dev/null +++ b/extensions/src/thirdpersoncamera/controller.cpp @@ -0,0 +1,429 @@ +#include "extensions/thirdpersoncamera/controller.h" + +#include "3dmanager/lego3dmanager.h" +#include "anim/legoanim.h" +#include "extensions/common/animutils.h" +#include "extensions/common/charactercustomizer.h" +#include "extensions/common/constants.h" +#include "islepathactor.h" +#include "legoactor.h" +#include "legoactors.h" +#include "legocameracontroller.h" +#include "legonavcontroller.h" +#include "legopathactor.h" +#include "legovideomanager.h" +#include "legoworld.h" +#include "misc.h" +#include "misc/legotree.h" +#include "mxgeometry/mxgeometry3d.h" +#include "mxgeometry/mxmatrix.h" +#include "mxmisc.h" +#include "realtime/vector.h" +#include "roi/legoroi.h" + +#include + +using namespace Extensions; +using namespace Extensions::Common; +using namespace Extensions::ThirdPersonCamera; + +Controller::Controller() + : m_animator(CharacterAnimatorConfig{/*.saveEmoteTransform=*/true}), m_enabled(false), m_active(false), + m_pendingWorldTransition(false), m_playerROI(nullptr) +{ +} + +void Controller::Enable() +{ + m_enabled = true; + ReinitForCharacter(); +} + +void Controller::Disable() +{ + m_enabled = false; + + if (m_active && m_playerROI) { + m_playerROI->SetVisibility(FALSE); + VideoManager()->Get3DManager()->Remove(*m_playerROI); + + m_orbit.RestoreFirstPersonCamera(); + } + + m_active = false; + m_pendingWorldTransition = false; + m_animator.StopROISounds(); + m_animator.StopClickAnimation(); + m_display.DestroyDisplayClone(); + m_playerROI = nullptr; + m_animator.ClearRideAnimation(); + m_animator.ClearAll(); + + m_orbit.ResetOrbitState(); + m_input.ResetTouchState(); +} + +void Controller::OnActorEnter(IslePathActor* p_actor) +{ + LegoPathActor* userActor = UserActor(); + if (static_cast(p_actor) != userActor) { + return; + } + + m_animator.SetCurrentVehicleType(DetectVehicleType(userActor)); + + if (!m_enabled) { + return; + } + + if (m_pendingWorldTransition && m_active) { + return; + } + + LegoROI* newROI = userActor->GetROI(); + if (!newROI) { + return; + } + + if (m_animator.GetCurrentVehicleType() != VEHICLE_NONE) { + if (IsLargeVehicle(m_animator.GetCurrentVehicleType()) || + m_animator.GetCurrentVehicleType() == VEHICLE_HELICOPTER) { + if (m_playerROI) { + m_playerROI->SetVisibility(FALSE); + VideoManager()->Get3DManager()->Remove(*m_playerROI); + } + m_active = false; + return; + } + + if (!m_playerROI) { + return; + } + + m_active = true; + m_orbit.SetupCamera(userActor); + m_animator.BuildRideAnimation(m_animator.GetCurrentVehicleType(), m_playerROI, 0); + return; + } + + newROI->SetVisibility(FALSE); + if (!m_display.EnsureDisplayROI()) { + return; + } + m_playerROI = m_display.GetDisplayROI(); + m_active = true; + + m_playerROI->SetVisibility(TRUE); + + VideoManager()->Get3DManager()->Remove(*m_playerROI); + VideoManager()->Get3DManager()->Add(*m_playerROI); + + m_animator.InitAnimCaches(m_playerROI); + m_animator.ResetAnimState(); + + m_animator.ApplyIdleFrame0(m_playerROI); + + m_orbit.SetupCamera(userActor); +} + +void Controller::OnActorExit(IslePathActor* p_actor) +{ + if (!m_enabled) { + return; + } + + if (m_animator.GetCurrentVehicleType() != VEHICLE_NONE) { + m_animator.ClearRideAnimation(); + m_animator.ClearAll(); + ReinitForCharacter(); + } + else if (m_active && static_cast(p_actor) == UserActor()) { + if (m_playerROI) { + m_playerROI->SetVisibility(FALSE); + VideoManager()->Get3DManager()->Remove(*m_playerROI); + } + m_animator.ClearRideAnimation(); + m_animator.ClearAll(); + m_playerROI = nullptr; + m_active = false; + } +} + +void Controller::OnCamAnimEnd(LegoPathActor* p_actor) +{ + m_pendingWorldTransition = false; + + if (!m_active) { + return; + } + + m_orbit.SetupCamera(p_actor); +} + +void Controller::Tick(float p_deltaTime) +{ + if (!m_display.IsDisplayActorFrozen()) { + LegoPathActor* userActor = UserActor(); + if (userActor) { + uint8_t actorId = static_cast(userActor)->GetActorId(); + if (IsValidActorId(actorId)) { + uint8_t derived = actorId - 1; + if (derived != m_display.GetDisplayActorIndex()) { + m_display.SetDisplayActorIndex(derived); + } + } + } + } + + if (!m_active) { + return; + } + + if (!m_playerROI) { + return; + } + + if (m_pendingWorldTransition) { + m_pendingWorldTransition = false; + LegoPathActor* actor = UserActor(); + if (actor && actor->GetROI()) { + m_orbit.InitAbsoluteYaw(actor->GetROI()); + } + } + + if (!UserActor() || UserActor()->GetActorState() != LegoPathActor::c_disabled) { + m_orbit.ApplyOrbitCamera(); + } + + // Small vehicle with ride animation + if (m_animator.GetCurrentVehicleType() != VEHICLE_NONE) { + m_animator.StopClickAnimation(); + if (m_animator.GetRideAnim() && m_animator.GetRideRoiMap()) { + LegoPathActor* actor = UserActor(); + if (!actor || !actor->GetROI()) { + return; + } + + AnimUtils::EnsureROIMapVisibility(m_animator.GetRideRoiMap(), m_animator.GetRideRoiMapSize()); + + float speed = actor->GetWorldSpeed(); + if (SDL_fabsf(speed) > 0.01f) { + m_animator.SetAnimTime(m_animator.GetAnimTime() + p_deltaTime * 2000.0f); + } + + MxMatrix transform(actor->GetROI()->GetLocal2World()); + AnimUtils::FlipMatrixDirection(transform); + + m_playerROI->WrappedSetLocal2WorldWithWorldDataUpdate(transform); + m_playerROI->SetVisibility(TRUE); + + float duration = (float) m_animator.GetRideAnim()->GetDuration(); + if (duration > 0.0f) { + float timeInCycle = + m_animator.GetAnimTime() - duration * SDL_floorf(m_animator.GetAnimTime() / duration); + + LegoTreeNode* root = m_animator.GetRideAnim()->GetRoot(); + for (LegoU32 i = 0; i < root->GetNumChildren(); i++) { + LegoROI::ApplyAnimationTransformation( + root->GetChild(i), + transform, + (LegoTime) timeInCycle, + m_animator.GetRideRoiMap() + ); + } + } + } + return; + } + + LegoPathActor* userActor = UserActor(); + if (!userActor) { + return; + } + + // Sync display clone position from native ROI + if (m_display.GetDisplayROI() && m_display.GetDisplayROI() == m_playerROI) { + m_display.SyncTransformFromNative(userActor->GetROI()); + } + + float speed = userActor->GetWorldSpeed(); + bool isMoving = SDL_fabsf(speed) > 0.01f; + if (m_animator.IsInMultiPartEmote()) { + isMoving = false; + userActor->SetWorldSpeed(0.0f); + NavController()->SetLinearVel(0.0f); + } + + m_animator.Tick(p_deltaTime, m_playerROI, isMoving); +} + +void Controller::SetWalkAnimId(uint8_t p_walkAnimId) +{ + m_animator.SetWalkAnimId(p_walkAnimId, m_active ? m_playerROI : nullptr); +} + +void Controller::SetIdleAnimId(uint8_t p_idleAnimId) +{ + m_animator.SetIdleAnimId(p_idleAnimId, m_active ? m_playerROI : nullptr); +} + +bool Controller::IsInMultiPartEmote() const +{ + return m_animator.IsInMultiPartEmote(); +} + +int8_t Controller::GetFrozenEmoteId() const +{ + return m_animator.GetFrozenEmoteId(); +} + +void Controller::TriggerEmote(uint8_t p_emoteId) +{ + if (!m_active) { + return; + } + + LegoPathActor* userActor = UserActor(); + if (!userActor) { + return; + } + + bool isMoving = SDL_fabsf(userActor->GetWorldSpeed()) > 0.01f; + if (m_animator.IsInMultiPartEmote()) { + isMoving = false; + } + m_animator.TriggerEmote(p_emoteId, m_playerROI, isMoving); +} + +void Controller::StopClickAnimation() +{ + m_animator.StopClickAnimation(); +} + +void Controller::OnWorldEnabled(LegoWorld* p_world) +{ + if (!p_world) { + return; + } + + if (!m_enabled) { + return; + } + + m_animator.ClearAll(); + + m_orbit.ResetOrbitState(); + + m_pendingWorldTransition = true; + + ReinitForCharacter(); +} + +void Controller::OnWorldDisabled(LegoWorld* p_world) +{ + if (!p_world) { + return; + } + m_active = false; + m_pendingWorldTransition = false; + m_playerROI = nullptr; + m_animator.StopROISounds(); + m_animator.StopClickAnimation(); + m_display.DestroyDisplayClone(); + m_animator.ClearRideAnimation(); + m_animator.ClearAll(); +} + +MxBool Controller::HandleCameraRelativeMovement( + LegoNavController* p_nav, + const Vector3& p_curPos, + const Vector3& p_curDir, + Vector3& p_newPos, + Vector3& p_newDir, + float p_deltaTime +) +{ + return m_orbit.HandleCameraRelativeMovement( + p_nav, + p_curPos, + p_curDir, + p_newPos, + p_newDir, + p_deltaTime, + m_animator.IsInMultiPartEmote() + ); +} + +void Controller::HandleSDLEventImpl(SDL_Event* p_event) +{ + m_input.HandleSDLEvent(p_event, m_orbit, m_active); +} + +void Controller::ReinitForCharacter() +{ + LegoPathActor* userActor = UserActor(); + if (!userActor) { + m_active = false; + return; + } + + LegoROI* roi = userActor->GetROI(); + if (!roi) { + m_active = false; + return; + } + + int8_t vehicleType = DetectVehicleType(userActor); + + if (vehicleType == VEHICLE_HELICOPTER || (vehicleType != VEHICLE_NONE && IsLargeVehicle(vehicleType))) { + m_active = false; + m_pendingWorldTransition = false; + return; + } + + m_animator.SetCurrentVehicleType(vehicleType); + + if (vehicleType != VEHICLE_NONE) { + if (!m_display.EnsureDisplayROI()) { + m_active = false; + return; + } + m_playerROI = m_display.GetDisplayROI(); + + if (!m_playerROI) { + m_active = false; + return; + } + + m_pendingWorldTransition = false; + + VideoManager()->Get3DManager()->Remove(*m_playerROI); + VideoManager()->Get3DManager()->Add(*m_playerROI); + m_active = true; + m_orbit.SetupCamera(userActor); + m_animator.BuildRideAnimation(vehicleType, m_playerROI, 0); + return; + } + + roi->SetVisibility(FALSE); + if (!m_display.EnsureDisplayROI()) { + m_active = false; + return; + } + m_playerROI = m_display.GetDisplayROI(); + + m_playerROI->SetVisibility(TRUE); + + VideoManager()->Get3DManager()->Remove(*m_playerROI); + VideoManager()->Get3DManager()->Add(*m_playerROI); + + m_animator.InitAnimCaches(m_playerROI); + m_animator.ResetAnimState(); + m_active = true; + + m_animator.ApplyIdleFrame0(m_playerROI); + + if (!m_pendingWorldTransition) { + m_orbit.SetupCamera(userActor); + } +} diff --git a/extensions/src/thirdpersoncamera/displayactor.cpp b/extensions/src/thirdpersoncamera/displayactor.cpp new file mode 100644 index 00000000..63c2449f --- /dev/null +++ b/extensions/src/thirdpersoncamera/displayactor.cpp @@ -0,0 +1,90 @@ +#include "extensions/thirdpersoncamera/displayactor.h" + +#include "3dmanager/lego3dmanager.h" +#include "extensions/common/animutils.h" +#include "extensions/common/charactercloner.h" +#include "extensions/common/charactercustomizer.h" +#include "extensions/common/constants.h" +#include "legocharactermanager.h" +#include "legovideomanager.h" +#include "misc.h" +#include "mxgeometry/mxmatrix.h" +#include "realtime/vector.h" +#include "roi/legoroi.h" + +#include + +using namespace Extensions::ThirdPersonCamera; +using namespace Extensions::Common; + +DisplayActor::DisplayActor() + : m_displayActorIndex(DISPLAY_ACTOR_NONE), m_displayActorFrozen(false), m_displayROI(nullptr) +{ + SDL_memset(m_displayUniqueName, 0, sizeof(m_displayUniqueName)); +} + +void DisplayActor::SetDisplayActorIndex(uint8_t p_displayActorIndex) +{ + if (m_displayActorIndex != p_displayActorIndex) { + m_customizeState.InitFromActorInfo(p_displayActorIndex); + } + m_displayActorIndex = p_displayActorIndex; +} + +bool DisplayActor::EnsureDisplayROI() +{ + if (!IsValidDisplayActorIndex(m_displayActorIndex)) { + return false; + } + if (!m_displayROI) { + CreateDisplayClone(); + } + if (!m_displayROI) { + return false; + } + return true; +} + +void DisplayActor::CreateDisplayClone() +{ + if (!IsValidDisplayActorIndex(m_displayActorIndex)) { + return; + } + LegoCharacterManager* charMgr = CharacterManager(); + const char* actorName = charMgr->GetActorName(m_displayActorIndex); + if (!actorName) { + return; + } + SDL_snprintf(m_displayUniqueName, sizeof(m_displayUniqueName), "tp_display"); + m_displayROI = CharacterCloner::Clone(charMgr, m_displayUniqueName, actorName); + + if (m_displayROI) { + CharacterCustomizer::ApplyFullState(m_displayROI, m_displayActorIndex, m_customizeState); + } +} + +void DisplayActor::DestroyDisplayClone() +{ + if (m_displayROI) { + VideoManager()->Get3DManager()->Remove(*m_displayROI); + CharacterManager()->ReleaseActor(m_displayUniqueName); + m_displayROI = nullptr; + } +} + +void DisplayActor::ApplyCustomizeChange(uint8_t p_changeType, uint8_t p_partIndex) +{ + uint8_t actorInfoIndex = CharacterCustomizer::ResolveActorInfoIndex(m_displayActorIndex); + + CharacterCustomizer::ApplyChange(m_displayROI, actorInfoIndex, m_customizeState, p_changeType, p_partIndex); +} + +void DisplayActor::SyncTransformFromNative(LegoROI* p_nativeROI) +{ + if (m_displayROI && p_nativeROI) { + MxMatrix mat(p_nativeROI->GetLocal2World()); + AnimUtils::FlipMatrixDirection(mat); + m_displayROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat); + VideoManager()->Get3DManager()->Moved(*m_displayROI); + } +} diff --git a/extensions/src/thirdpersoncamera/inputhandler.cpp b/extensions/src/thirdpersoncamera/inputhandler.cpp new file mode 100644 index 00000000..70e93f09 --- /dev/null +++ b/extensions/src/thirdpersoncamera/inputhandler.cpp @@ -0,0 +1,210 @@ +#include "extensions/thirdpersoncamera/inputhandler.h" + +#include "extensions/thirdpersoncamera/orbitcamera.h" + +#include +#include + +using namespace Extensions::ThirdPersonCamera; + +InputHandler::InputHandler() : m_touch{}, m_wantsAutoDisable(false), m_wantsAutoEnable(false) +{ +} + +bool InputHandler::TryClaimFinger(const SDL_TouchFingerEvent& p_event, bool p_active) +{ + if (!p_active || m_touch.count >= 2 || p_event.x < CAMERA_ZONE_X) { + return false; + } + + int idx = m_touch.count; + m_touch.id[idx] = p_event.fingerID; + m_touch.x[idx] = p_event.x; + m_touch.y[idx] = p_event.y; + m_touch.count++; + + if (m_touch.count == 2) { + float dx = m_touch.x[1] - m_touch.x[0]; + float dy = m_touch.y[1] - m_touch.y[0]; + m_touch.initialPinchDist = SDL_sqrtf(dx * dx + dy * dy); + } + + return true; +} + +bool InputHandler::TryReleaseFinger(SDL_FingerID p_id) +{ + for (int i = 0; i < m_touch.count; i++) { + if (m_touch.id[i] == p_id) { + if (i == 0 && m_touch.count == 2) { + m_touch.id[0] = m_touch.id[1]; + m_touch.x[0] = m_touch.x[1]; + m_touch.y[0] = m_touch.y[1]; + } + m_touch.count--; + m_touch.initialPinchDist = 0.0f; + return true; + } + } + return false; +} + +bool InputHandler::IsFingerTracked(SDL_FingerID p_id) const +{ + for (int i = 0; i < m_touch.count; i++) { + if (m_touch.id[i] == p_id) { + return true; + } + } + return false; +} + +bool InputHandler::ConsumeAutoDisable() +{ + return std::exchange(m_wantsAutoDisable, false); +} + +bool InputHandler::ConsumeAutoEnable() +{ + return std::exchange(m_wantsAutoEnable, false); +} + +void InputHandler::HandleSDLEvent(SDL_Event* p_event, OrbitCamera& p_orbit, bool p_active) +{ + switch (p_event->type) { + case SDL_EVENT_MOUSE_WHEEL: + if (!p_active) { + if (p_event->wheel.y < 0) { + m_wantsAutoEnable = true; + } + break; + } + if (p_orbit.GetOrbitDistance() <= OrbitCamera::MIN_DISTANCE && p_event->wheel.y > 0) { + m_wantsAutoDisable = true; + break; + } + p_orbit.AdjustDistance(-p_event->wheel.y * 0.5f); + p_orbit.ClampDistance(); + break; + + case SDL_EVENT_MOUSE_MOTION: + if (!p_active) { + break; + } + if (p_event->motion.state & SDL_BUTTON_RMASK) { + p_orbit.AdjustYaw(-p_event->motion.xrel * 0.005f); + p_orbit.AdjustPitch(p_event->motion.yrel * 0.005f); + p_orbit.ClampPitch(); + } + break; + + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: { + if (!p_active) { + break; + } + 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 (!IsFingerTracked(p_event->tfinger.fingerID) && m_touch.count < 2 && + p_event->tfinger.x >= CAMERA_ZONE_X) { + int idx = m_touch.count; + m_touch.id[idx] = p_event->tfinger.fingerID; + m_touch.x[idx] = p_event->tfinger.x; + m_touch.y[idx] = p_event->tfinger.y; + m_touch.count++; + + if (m_touch.count == 2) { + float dx = m_touch.x[1] - m_touch.x[0]; + float dy = m_touch.y[1] - m_touch.y[0]; + m_touch.initialPinchDist = SDL_sqrtf(dx * dx + dy * dy); + } + } + break; + } + + case SDL_EVENT_FINGER_MOTION: { + if (m_touch.count == 1) { + if (!p_active) { + break; + } + if (m_touch.id[0] == p_event->tfinger.fingerID) { + float oldX = m_touch.x[0]; + float oldY = m_touch.y[0]; + m_touch.x[0] = p_event->tfinger.x; + m_touch.y[0] = p_event->tfinger.y; + + float moveX = m_touch.x[0] - oldX; + float moveY = m_touch.y[0] - oldY; + p_orbit.AdjustYaw(-moveX * 2.0f); + p_orbit.AdjustPitch(moveY * 2.0f); + p_orbit.ClampPitch(); + } + } + else if (m_touch.count == 2) { + int idx = -1; + for (int i = 0; i < 2; i++) { + if (m_touch.id[i] == p_event->tfinger.fingerID) { + idx = i; + break; + } + } + if (idx < 0) { + break; + } + + float oldX = m_touch.x[idx]; + float oldY = m_touch.y[idx]; + m_touch.x[idx] = p_event->tfinger.x; + m_touch.y[idx] = p_event->tfinger.y; + + float dx = m_touch.x[1] - m_touch.x[0]; + float dy = m_touch.y[1] - m_touch.y[0]; + float newDist = SDL_sqrtf(dx * dx + dy * dy); + + if (m_touch.initialPinchDist > 0.001f) { + float pinchDelta = m_touch.initialPinchDist - newDist; + + if (!p_active) { + if (pinchDelta > 0) { + m_wantsAutoEnable = true; + } + m_touch.initialPinchDist = newDist; + break; + } + + if (p_orbit.GetOrbitDistance() <= OrbitCamera::MIN_DISTANCE && pinchDelta < 0) { + m_wantsAutoDisable = true; + m_touch.initialPinchDist = newDist; + break; + } + + p_orbit.AdjustDistance(pinchDelta * 15.0f); + p_orbit.ClampDistance(); + m_touch.initialPinchDist = newDist; + } + + float moveX = m_touch.x[idx] - oldX; + float moveY = m_touch.y[idx] - oldY; + p_orbit.AdjustYaw(-moveX * 2.0f); + p_orbit.AdjustPitch(moveY * 2.0f); + p_orbit.ClampPitch(); + } + break; + } + + case SDL_EVENT_FINGER_UP: + case SDL_EVENT_FINGER_CANCELED: { + TryReleaseFinger(p_event->tfinger.fingerID); + break; + } + + default: + break; + } +} diff --git a/extensions/src/thirdpersoncamera/orbitcamera.cpp b/extensions/src/thirdpersoncamera/orbitcamera.cpp new file mode 100644 index 00000000..bc85815b --- /dev/null +++ b/extensions/src/thirdpersoncamera/orbitcamera.cpp @@ -0,0 +1,287 @@ +#include "extensions/thirdpersoncamera/orbitcamera.h" + +#include "extensions/common/characteranimator.h" +#include "legocameracontroller.h" +#include "legoinputmanager.h" +#include "legonavcontroller.h" +#include "legopathactor.h" +#include "legoworld.h" +#include "misc.h" +#include "mxgeometry/mxmatrix.h" +#include "realtime/vector.h" +#include "roi/legoroi.h" + +using namespace Extensions::ThirdPersonCamera; + +static constexpr float TURN_RATE = 10.0f; + +OrbitCamera::OrbitCamera() + : m_orbitPitch(DEFAULT_ORBIT_PITCH), m_orbitDistance(DEFAULT_ORBIT_DISTANCE), + m_absoluteYaw(DEFAULT_ORBIT_YAW), m_smoothedSpeed(0.0f) +{ +} + +void OrbitCamera::ComputeOrbitVectors( + float p_yaw, + Mx3DPointFloat& p_at, + Mx3DPointFloat& p_dir, + Mx3DPointFloat& p_up +) const +{ + float cosP = SDL_cosf(m_orbitPitch); + float sinP = SDL_sinf(m_orbitPitch); + float sinY = SDL_sinf(p_yaw); + float cosY = SDL_cosf(p_yaw); + + p_at = Mx3DPointFloat( + m_orbitDistance * sinY * cosP, + ORBIT_TARGET_HEIGHT + m_orbitDistance * sinP, + -m_orbitDistance * cosY * cosP + ); + + p_dir = Mx3DPointFloat(-sinY * cosP, -sinP, cosY * cosP); + + p_up = Mx3DPointFloat(0.0f, 1.0f, 0.0f); +} + +float OrbitCamera::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 OrbitCamera::InitAbsoluteYaw(LegoROI* p_roi) +{ + const float* dir = p_roi->GetWorldDirection(); + m_absoluteYaw = SDL_atan2f(-dir[0], dir[2]) + DEFAULT_ORBIT_YAW; +} + +void OrbitCamera::SetupCamera(LegoPathActor* p_actor) +{ + LegoWorld* world = CurrentWorld(); + if (!world || !world->GetCameraController()) { + return; + } + + LegoROI* roi = p_actor->GetROI(); + if (roi) { + InitAbsoluteYaw(roi); + } + m_smoothedSpeed = 0.0f; + + Mx3DPointFloat at, camDir, up; + ComputeOrbitVectors(DEFAULT_ORBIT_YAW, at, camDir, up); + + world->GetCameraController()->SetWorldTransform(at, camDir, up); + p_actor->TransformPointOfView(); +} + +void OrbitCamera::ApplyOrbitCamera() +{ + LegoPathActor* actor = UserActor(); + LegoWorld* world = CurrentWorld(); + if (!actor || !world || !world->GetCameraController()) { + return; + } + + float localYaw = GetLocalYaw(actor->GetROI()); + + Mx3DPointFloat at, camDir, up; + ComputeOrbitVectors(localYaw, at, camDir, up); + + world->GetCameraController()->SetWorldTransform(at, camDir, up); + actor->TransformPointOfView(); +} + +void OrbitCamera::ResetOrbitState() +{ + m_orbitPitch = DEFAULT_ORBIT_PITCH; + m_orbitDistance = DEFAULT_ORBIT_DISTANCE; + m_absoluteYaw = DEFAULT_ORBIT_YAW; + m_smoothedSpeed = 0.0f; +} + +void OrbitCamera::ClampPitch() +{ + if (m_orbitPitch < MIN_PITCH) { + m_orbitPitch = MIN_PITCH; + } + if (m_orbitPitch > MAX_PITCH) { + m_orbitPitch = MAX_PITCH; + } +} + +void OrbitCamera::ClampDistance() +{ + if (m_orbitDistance < MIN_DISTANCE) { + m_orbitDistance = MIN_DISTANCE; + } + if (m_orbitDistance > MAX_DISTANCE) { + m_orbitDistance = MAX_DISTANCE; + } +} + +void OrbitCamera::RestoreFirstPersonCamera() +{ + LegoPathActor* userActor = UserActor(); + LegoWorld* world = CurrentWorld(); + + if (userActor && world && world->GetCameraController()) { + world->GetCameraController()->SetWorldTransform( + Mx3DPointFloat(0.0F, 1.25F, 0.0F), + Mx3DPointFloat(0.0F, 0.0F, 1.0F), + Mx3DPointFloat(0.0F, 1.0F, 0.0F) + ); + userActor->TransformPointOfView(); + } +} + +MxBool OrbitCamera::HandleCameraRelativeMovement( + LegoNavController* p_nav, + const Vector3& p_curPos, + const Vector3& p_curDir, + Vector3& p_newPos, + Vector3& p_newDir, + float p_deltaTime, + bool p_isInMultiPartEmote +) +{ + LegoInputManager* inputManager = InputManager(); + MxU32 keyFlags = 0; + if (!inputManager || inputManager->GetNavigationKeyStates(keyFlags) == FAILURE) { + keyFlags = 0; + } + + float camForwardX = -SDL_sinf(m_absoluteYaw); + float camForwardZ = SDL_cosf(m_absoluteYaw); + float camRightX = SDL_cosf(m_absoluteYaw); + float camRightZ = SDL_sinf(m_absoluteYaw); + + 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; + } + + if (keyFlags == 0 && inputManager) { + MxU32 joystickX, joystickY, povPosition; + if (inputManager->GetJoystickState(&joystickX, &joystickY, &povPosition) == SUCCESS) { + float jx = (joystickX - 50.0f) / 50.0f; + float jy = -(joystickY - 50.0f) / 50.0f; + + if (SDL_fabsf(jx) < 0.1f) { + jx = 0.0f; + } + if (SDL_fabsf(jy) < 0.1f) { + jy = 0.0f; + } + + moveDirX += camForwardX * jy + camRightX * jx; + moveDirZ += camForwardZ * jy + camRightZ * jx; + } + } + + float moveDirLen = SDL_sqrtf(moveDirX * moveDirX + moveDirZ * moveDirZ); + bool hasInput = moveDirLen > 0.001f; + + if (p_isInMultiPartEmote) { + hasInput = false; + m_smoothedSpeed = 0.0f; + } + + if (hasInput) { + moveDirX /= moveDirLen; + moveDirZ /= moveDirLen; + } + + 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; + p_newPos = p_curPos; + p_newDir = p_curDir; + } + else { + 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; + + float targetYaw = SDL_atan2f(-moveDirX, moveDirZ); + float currentYaw = SDL_atan2f(-p_curDir[0], p_curDir[2]); + float angleDiff = targetYaw - currentYaw; + + 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 { + 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; + } + } + + p_nav->m_linearVel = m_smoothedSpeed; + p_nav->m_rotationalVel = 0.0f; + + 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; +}