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; +}