From 3d7bbdf0aed735058547db0ace7f0efa44f49214 Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Sun, 29 Mar 2026 08:03:41 -0700 Subject: [PATCH] Add third person camera extension Introduces a third person camera system with orbit camera, input handling (mouse/keyboard/touch/gamepad), display actor cloning, and camera-relative movement. Includes shared character utilities (animator, cloner, customizer) and an IExtraAnimHandler interface for optional animation extensions. Also includes generic base game fixes and extension system improvements. --- .gitignore | 3 +- 3rdparty/CMakeLists.txt | 4 +- CMakeLists.txt | 16 + ISLE/emscripten/filesystem.cpp | 12 +- ISLE/isleapp.cpp | 6 +- LEGO1/lego/legoomni/include/islepathactor.h | 3 + .../legoomni/include/legocharactermanager.h | 3 + .../lego/legoomni/include/legoinputmanager.h | 4 + .../lego/legoomni/include/legonavcontroller.h | 4 + .../legoomni/src/actors/islepathactor.cpp | 7 + LEGO1/lego/legoomni/src/audio/lego3dsound.cpp | 5 + .../src/common/legoanimmmpresenter.cpp | 7 + .../src/common/legocharactermanager.cpp | 6 +- .../legoomni/src/common/legotextureinfo.cpp | 2 +- LEGO1/lego/legoomni/src/common/legoutils.cpp | 9 +- .../legoomni/src/entity/legonavcontroller.cpp | 9 + LEGO1/lego/legoomni/src/entity/legoworld.cpp | 9 +- .../legoomni/src/input/legoinputmanager.cpp | 28 +- LEGO1/lego/legoomni/src/main/legomain.cpp | 10 +- LEGO1/lego/legoomni/src/worlds/infocenter.cpp | 5 +- LEGO1/lego/legoomni/src/worlds/isle.cpp | 2 +- LEGO1/modeldb/modeldb.cpp | 4 +- .../omni/src/notify/mxnotificationmanager.cpp | 4 +- .../include/extensions/common/animutils.h | 107 ++++ .../extensions/common/arearestriction.h | 31 ++ .../extensions/common/characteranimator.h | 191 +++++++ .../extensions/common/charactercloner.h | 42 ++ .../extensions/common/charactercustomizer.h | 49 ++ .../extensions/common/charactertables.h | 32 ++ .../include/extensions/common/constants.h | 44 ++ .../extensions/common/customizestate.h | 22 + .../include/extensions/common/pathutils.h | 17 + extensions/include/extensions/extensions.h | 24 +- extensions/include/extensions/fwd.h | 18 + extensions/include/extensions/siloader.h | 50 +- extensions/include/extensions/textureloader.h | 13 +- .../include/extensions/thirdpersoncamera.h | 91 +++ .../extensions/thirdpersoncamera/controller.h | 150 +++++ .../thirdpersoncamera/displayactor.h | 47 ++ .../thirdpersoncamera/inputhandler.h | 61 ++ .../thirdpersoncamera/orbitcamera.h | 76 +++ extensions/src/common/animutils.cpp | 270 +++++++++ extensions/src/common/characteranimator.cpp | 470 ++++++++++++++++ extensions/src/common/charactercloner.cpp | 156 ++++++ extensions/src/common/charactercustomizer.cpp | 370 ++++++++++++ extensions/src/common/charactertables.cpp | 75 +++ extensions/src/common/customizestate.cpp | 38 ++ extensions/src/common/pathutils.cpp | 24 + extensions/src/extensions.cpp | 46 +- extensions/src/siloader.cpp | 68 ++- extensions/src/textureloader.cpp | 32 +- extensions/src/thirdpersoncamera.cpp | 276 +++++++++ .../src/thirdpersoncamera/controller.cpp | 526 ++++++++++++++++++ .../src/thirdpersoncamera/displayactor.cpp | 90 +++ .../src/thirdpersoncamera/inputhandler.cpp | 257 +++++++++ .../src/thirdpersoncamera/orbitcamera.cpp | 285 ++++++++++ tools/ncc/skip.yml | 3 +- 57 files changed, 4082 insertions(+), 131 deletions(-) create mode 100644 extensions/include/extensions/common/animutils.h create mode 100644 extensions/include/extensions/common/arearestriction.h create mode 100644 extensions/include/extensions/common/characteranimator.h create mode 100644 extensions/include/extensions/common/charactercloner.h create mode 100644 extensions/include/extensions/common/charactercustomizer.h create mode 100644 extensions/include/extensions/common/charactertables.h create mode 100644 extensions/include/extensions/common/constants.h create mode 100644 extensions/include/extensions/common/customizestate.h create mode 100644 extensions/include/extensions/common/pathutils.h create mode 100644 extensions/include/extensions/fwd.h create mode 100644 extensions/include/extensions/thirdpersoncamera.h create mode 100644 extensions/include/extensions/thirdpersoncamera/controller.h create mode 100644 extensions/include/extensions/thirdpersoncamera/displayactor.h create mode 100644 extensions/include/extensions/thirdpersoncamera/inputhandler.h create mode 100644 extensions/include/extensions/thirdpersoncamera/orbitcamera.h create mode 100644 extensions/src/common/animutils.cpp create mode 100644 extensions/src/common/characteranimator.cpp create mode 100644 extensions/src/common/charactercloner.cpp create mode 100644 extensions/src/common/charactercustomizer.cpp create mode 100644 extensions/src/common/charactertables.cpp create mode 100644 extensions/src/common/customizestate.cpp create mode 100644 extensions/src/common/pathutils.cpp create mode 100644 extensions/src/thirdpersoncamera.cpp create mode 100644 extensions/src/thirdpersoncamera/controller.cpp create mode 100644 extensions/src/thirdpersoncamera/displayactor.cpp create mode 100644 extensions/src/thirdpersoncamera/inputhandler.cpp create mode 100644 extensions/src/thirdpersoncamera/orbitcamera.cpp diff --git a/.gitignore b/.gitignore index a4bd8a41f..ae4d7a113 100644 --- a/.gitignore +++ b/.gitignore @@ -14,8 +14,7 @@ VENV/ env.bak/ venv.bak/ local.properties -/build/ -/build_debug/ +/build*/ /legobin/ *.swp LEGO1PROGRESS.* diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index 2e8fcd74f..2f21379dd 100644 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -55,8 +55,8 @@ if(DOWNLOAD_DEPENDENCIES) include(FetchContent) FetchContent_Populate( libweaver - URL https://github.com/isledecomp/SIEdit/archive/afd4933844b95ef739a7e77b097deb7efe4ec576.tar.gz - URL_MD5 59fd3c36f4f380f730cd9bedfc846397 + URL https://github.com/isledecomp/SIEdit/archive/17c7736a6ff31413f1e74ab4e989011b545b6926.tar.gz + URL_MD5 04edbc974df8884f283d920ded10f1f6 ) add_library(libweaver STATIC ${libweaver_SOURCE_DIR}/lib/core.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e8eee533..85dc479ad 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -531,6 +531,22 @@ if (ISLE_EXTENSIONS) extensions/src/extensions.cpp extensions/src/siloader.cpp extensions/src/textureloader.cpp + + # Common shared code + extensions/src/common/charactertables.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 + extensions/src/common/pathutils.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 ) endif() diff --git a/ISLE/emscripten/filesystem.cpp b/ISLE/emscripten/filesystem.cpp index e56446082..e643ac488 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 8135b9573..85b702b9e 100644 --- a/ISLE/isleapp.cpp +++ b/ISLE/isleapp.cpp @@ -37,7 +37,7 @@ #include "viewmanager/viewmanager.h" #include -#include +#include #include #include #include @@ -875,6 +875,10 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) } } +#ifdef EXTENSIONS + Extensions::ThirdPersonCameraExt::HandleSDLEvent(event); +#endif + return SDL_APP_CONTINUE; } diff --git a/LEGO1/lego/legoomni/include/islepathactor.h b/LEGO1/lego/legoomni/include/islepathactor.h index d1966b9c0..df747516b 100644 --- a/LEGO1/lego/legoomni/include/islepathactor.h +++ b/LEGO1/lego/legoomni/include/islepathactor.h @@ -1,6 +1,7 @@ #ifndef ISLEPATHACTOR_H #define ISLEPATHACTOR_H +#include "extensions/fwd.h" #include "legogamestate.h" #include "legopathactor.h" #include "mxtypes.h" @@ -139,6 +140,8 @@ class IslePathActor : public LegoPathActor { // IslePathActor::`scalar deleting destructor' protected: + friend class Extensions::ThirdPersonCamera::Controller; + LegoWorld* m_world; // 0x154 LegoPathActor* m_previousActor; // 0x158 MxFloat m_previousVel; // 0x15c diff --git a/LEGO1/lego/legoomni/include/legocharactermanager.h b/LEGO1/lego/legoomni/include/legocharactermanager.h index 4443ca8a5..1b34b757f 100644 --- a/LEGO1/lego/legoomni/include/legocharactermanager.h +++ b/LEGO1/lego/legoomni/include/legocharactermanager.h @@ -2,6 +2,7 @@ #define LEGOCHARACTERMANAGER_H #include "decomp.h" +#include "extensions/fwd.h" #include "mxstl/stlcompat.h" #include "mxtypes.h" #include "mxvariable.h" @@ -98,6 +99,8 @@ class LegoCharacterManager { static const char* GetCustomizeAnimFile() { return g_customizeAnimFile; } private: + friend class Extensions::Common::CharacterCloner; + LegoROI* CreateActorROI(const char* p_key); void RemoveROI(LegoROI* p_roi); LegoROI* FindChildROI(LegoROI* p_roi, const char* p_name); diff --git a/LEGO1/lego/legoomni/include/legoinputmanager.h b/LEGO1/lego/legoomni/include/legoinputmanager.h index 6ddadec19..bd31d37b6 100644 --- a/LEGO1/lego/legoomni/include/legoinputmanager.h +++ b/LEGO1/lego/legoomni/include/legoinputmanager.h @@ -2,6 +2,7 @@ #define LEGOINPUTMANAGER_H #include "decomp.h" +#include "extensions/fwd.h" #include "lego1_export.h" #include "legoeventnotificationparam.h" #include "mxlist.h" @@ -179,6 +180,8 @@ class LegoInputManager : public MxPresenter { // LegoInputManager::`scalar deleting destructor' private: + friend class Extensions::ThirdPersonCameraExt; + void InitializeHaptics(); MxCriticalSection m_criticalSection; // 0x58 @@ -204,6 +207,7 @@ class LegoInputManager : public MxPresenter { TouchScheme m_touchScheme = e_none; SDL_Point m_touchVirtualThumb = {0, 0}; SDL_FPoint m_touchVirtualThumbOrigin; + SDL_FingerID m_touchFinger = 0; std::map m_touchFlags; std::map> m_keyboards; std::map> m_mice; diff --git a/LEGO1/lego/legoomni/include/legonavcontroller.h b/LEGO1/lego/legoomni/include/legonavcontroller.h index 974966c6e..37e4fbd7b 100644 --- a/LEGO1/lego/legoomni/include/legonavcontroller.h +++ b/LEGO1/lego/legoomni/include/legonavcontroller.h @@ -2,6 +2,7 @@ #define __LEGONAVCONTROLLER_H #include "decomp.h" +#include "extensions/fwd.h" #include "mxcore.h" #include "mxtypes.h" @@ -122,6 +123,9 @@ class LegoNavController : public MxCore { // LegoNavController::`scalar deleting destructor' protected: + friend class Extensions::ThirdPersonCamera::OrbitCamera; + friend class Extensions::ThirdPersonCamera::Controller; + float CalculateNewVel(float p_targetVel, float p_currentVel, float p_accel, float p_time); float CalculateNewTargetVel(int p_pos, int p_center, float p_max); float CalculateNewAccel(int p_pos, int p_center, float p_max, int p_min); diff --git a/LEGO1/lego/legoomni/src/actors/islepathactor.cpp b/LEGO1/lego/legoomni/src/actors/islepathactor.cpp index 53349c7b4..d60d2bf83 100644 --- a/LEGO1/lego/legoomni/src/actors/islepathactor.cpp +++ b/LEGO1/lego/legoomni/src/actors/islepathactor.cpp @@ -1,6 +1,7 @@ #include "islepathactor.h" #include "3dmanager/lego3dmanager.h" +#include "extensions/thirdpersoncamera.h" #include "isle_actions.h" #include "jukebox_actions.h" #include "legoanimationmanager.h" @@ -16,6 +17,8 @@ #include "scripts.h" #include "viewmanager/viewmanager.h" +using namespace Extensions; + DECOMP_SIZE_ASSERT(IslePathActor, 0x160) DECOMP_SIZE_ASSERT(IslePathActor::SpawnLocation, 0x38) @@ -95,6 +98,8 @@ void IslePathActor::Enter() TurnAround(); TransformPointOfView(); } + + Extension::Call(TP::HandleActorEnter, this); } // FUNCTION: LEGO1 0x1001a3f0 @@ -154,6 +159,8 @@ void IslePathActor::Exit() TurnAround(); TransformPointOfView(); ResetViewVelocity(); + + Extension::Call(TP::HandleActorExit, this); } // GLOBAL: LEGO1 0x10102b28 diff --git a/LEGO1/lego/legoomni/src/audio/lego3dsound.cpp b/LEGO1/lego/legoomni/src/audio/lego3dsound.cpp index a6224ccfa..ea8db0b52 100644 --- a/LEGO1/lego/legoomni/src/audio/lego3dsound.cpp +++ b/LEGO1/lego/legoomni/src/audio/lego3dsound.cpp @@ -186,6 +186,11 @@ void Lego3DSound::FUN_10011a60(ma_sound* p_sound, const char* p_name) } } else { + // Reset ownership flags before reassigning. Reset() only clears m_roi + // but not these flags, so stale values from a previous actor-backed play + // would cause an incorrect ReleaseActor call for non-actor ROIs + m_enabled = m_isActor = FALSE; + if (CharacterManager()->IsActor(p_name)) { m_roi = CharacterManager()->GetActorROI(p_name, TRUE); m_enabled = m_isActor = TRUE; diff --git a/LEGO1/lego/legoomni/src/common/legoanimmmpresenter.cpp b/LEGO1/lego/legoomni/src/common/legoanimmmpresenter.cpp index 3512727e3..b4397aaf4 100644 --- a/LEGO1/lego/legoomni/src/common/legoanimmmpresenter.cpp +++ b/LEGO1/lego/legoomni/src/common/legoanimmmpresenter.cpp @@ -3,6 +3,7 @@ #include "3dmanager/lego3dmanager.h" #include "decomp.h" #include "define.h" +#include "extensions/thirdpersoncamera.h" #include "islepathactor.h" #include "legoanimationmanager.h" #include "legoanimpresenter.h" @@ -20,6 +21,8 @@ #include "mxtimer.h" #include "mxutilities.h" +using namespace Extensions; + DECOMP_SIZE_ASSERT(LegoAnimMMPresenter, 0x74) // FUNCTION: LEGO1 0x1004a8d0 @@ -480,6 +483,10 @@ MxBool LegoAnimMMPresenter::FUN_1004b6d0(MxLong p_time) } actor->SetActorState(LegoPathActor::c_initial); + + if (m_tranInfo->m_unk0x29) { + Extension::Call(TP::HandleCamAnimEnd, actor); + } } return TRUE; diff --git a/LEGO1/lego/legoomni/src/common/legocharactermanager.cpp b/LEGO1/lego/legoomni/src/common/legocharactermanager.cpp index 180b8eec2..c30eb98eb 100644 --- a/LEGO1/lego/legoomni/src/common/legocharactermanager.cpp +++ b/LEGO1/lego/legoomni/src/common/legocharactermanager.cpp @@ -1,6 +1,7 @@ #include "legocharactermanager.h" #include "3dmanager/lego3dmanager.h" +#include "extensions/thirdpersoncamera.h" #include "legoactors.h" #include "legoanimactor.h" #include "legobuildingmanager.h" @@ -22,6 +23,8 @@ #include #include +using namespace Extensions; + DECOMP_SIZE_ASSERT(LegoCharacter, 0x08) DECOMP_SIZE_ASSERT(LegoCharacterManager, 0x08) DECOMP_SIZE_ASSERT(CustomizeAnimFileVariable, 0x24) @@ -279,7 +282,8 @@ LegoROI* LegoCharacterManager::GetActorROI(const char* p_name, MxBool p_createEn } if (character != NULL) { - if (p_createEntity && character->m_roi->GetEntity() == NULL) { + if (p_createEntity && character->m_roi->GetEntity() == NULL && + !Extension::Call(TP::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/legotextureinfo.cpp b/LEGO1/lego/legoomni/src/common/legotextureinfo.cpp index a0be25b74..d273b2d23 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 111e2713e..de055c269 100644 --- a/LEGO1/lego/legoomni/src/common/legoutils.cpp +++ b/LEGO1/lego/legoomni/src/common/legoutils.cpp @@ -505,8 +505,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(); } @@ -545,8 +545,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/legonavcontroller.cpp b/LEGO1/lego/legoomni/src/entity/legonavcontroller.cpp index aea8beb01..50c8ac4b1 100644 --- a/LEGO1/lego/legoomni/src/entity/legonavcontroller.cpp +++ b/LEGO1/lego/legoomni/src/entity/legonavcontroller.cpp @@ -2,6 +2,7 @@ #include "3dmanager/lego3dmanager.h" #include "act3.h" +#include "extensions/thirdpersoncamera.h" #include "infocenter.h" #include "legoanimationmanager.h" #include "legocameracontroller.h" @@ -29,6 +30,8 @@ #include #include +using namespace Extensions; + DECOMP_SIZE_ASSERT(LegoNavController, 0x70) // MSVC 4.20 didn't define a macro for this key @@ -348,6 +351,12 @@ MxBool LegoNavController::CalculateNewPosDir( ProcessJoystickInput(rotatedY); } + if (Extension< + ThirdPersonCameraExt>::Call(TP::HandleNavOverride, this, p_curPos, p_curDir, p_newPos, p_newDir, deltaTime) + .value_or(FALSE)) { + return TRUE; + } + if (m_useRotationalVel) { m_rotationalVel = CalculateNewVel(m_targetRotationalVel, m_rotationalVel, m_rotationalAccel * 40.0f, deltaTime); } diff --git a/LEGO1/lego/legoomni/src/entity/legoworld.cpp b/LEGO1/lego/legoomni/src/entity/legoworld.cpp index 976c9417d..2ed53e22e 100644 --- a/LEGO1/lego/legoomni/src/entity/legoworld.cpp +++ b/LEGO1/lego/legoomni/src/entity/legoworld.cpp @@ -2,6 +2,7 @@ #include "anim/legoanim.h" #include "extensions/siloader.h" +#include "extensions/thirdpersoncamera.h" #include "legoanimationmanager.h" #include "legoanimpresenter.h" #include "legobuildingmanager.h" @@ -639,8 +640,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(); } @@ -753,6 +754,8 @@ void LegoWorld::Enable(MxBool p_enable) #ifndef BETA10 SetIsWorldActive(TRUE); #endif + + Extension::Call(TP::HandleWorldEnable, this, TRUE); } else if (!p_enable && m_disabledObjects.size() == 0) { MxPresenter* presenter; @@ -815,6 +818,8 @@ void LegoWorld::Enable(MxBool p_enable) } GetViewManager()->RemoveAll(NULL); + + Extension::Call(TP::HandleWorldEnable, this, FALSE); } } diff --git a/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp b/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp index aa6310278..e6498ce4d 100644 --- a/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp +++ b/LEGO1/lego/legoomni/src/input/legoinputmanager.cpp @@ -1,5 +1,6 @@ #include "legoinputmanager.h" +#include "extensions/thirdpersoncamera.h" #include "legocameracontroller.h" #include "legocontrolmanager.h" #include "legomain.h" @@ -14,6 +15,8 @@ #include +using namespace Extensions; + DECOMP_SIZE_ASSERT(LegoInputManager, 0x338) DECOMP_SIZE_ASSERT(LegoNotifyList, 0x18) DECOMP_SIZE_ASSERT(LegoNotifyListCursor, 0x10) @@ -320,6 +323,12 @@ MxBool LegoInputManager::ProcessOneEvent(LegoEventNotificationParam& p_param) } else { if (!Lego()->IsPaused()) { + if ((p_param.GetModifier() & LegoEventNotificationParam::c_rButtonState) && + !(p_param.GetModifier() & LegoEventNotificationParam::c_lButtonState) && + Extension::Call(TP::IsThirdPersonCameraActive).value_or(FALSE)) { + return FALSE; + } + processRoi = TRUE; if (m_unk0x335 != 0) { @@ -393,6 +402,9 @@ MxBool LegoInputManager::ProcessOneEvent(LegoEventNotificationParam& p_param) if (entity && entity->Notify(p_param) != 0) { return TRUE; } + if (Extension::Call(TP::HandleROIClick, roi, p_param).value_or(FALSE)) { + return TRUE; + } } } @@ -626,6 +638,10 @@ void LegoInputManager::RemoveJoystick(SDL_JoystickID p_joystickID) MxBool LegoInputManager::HandleTouchEvent(SDL_Event* p_event, TouchScheme p_touchScheme) { + if (Extension::Call(TP::HandleTouchInput, p_event).value_or(FALSE)) { + return FALSE; + } + const SDL_TouchFingerEvent& event = p_event->tfinger; m_touchScheme = p_touchScheme; @@ -661,26 +677,24 @@ MxBool LegoInputManager::HandleTouchEvent(SDL_Event* p_event, TouchScheme p_touc } break; case e_gamepad: { - static SDL_FingerID g_finger = (SDL_FingerID) 0; - switch (p_event->type) { case SDL_EVENT_FINGER_DOWN: - if (!g_finger) { - g_finger = event.fingerID; + if (!m_touchFinger) { + m_touchFinger = event.fingerID; m_touchVirtualThumb = {0, 0}; m_touchVirtualThumbOrigin = {event.x, event.y}; } break; case SDL_EVENT_FINGER_UP: case SDL_EVENT_FINGER_CANCELED: - if (event.fingerID == g_finger) { - g_finger = 0; + if (event.fingerID == m_touchFinger) { + m_touchFinger = 0; m_touchVirtualThumb = {0, 0}; m_touchVirtualThumbOrigin = {0, 0}; } break; case SDL_EVENT_FINGER_MOTION: - if (event.fingerID == g_finger) { + if (event.fingerID == m_touchFinger) { const float thumbstickRadius = 0.25f; const float deltaX = SDL_clamp(event.x - m_touchVirtualThumbOrigin.x, -thumbstickRadius, thumbstickRadius); diff --git a/LEGO1/lego/legoomni/src/main/legomain.cpp b/LEGO1/lego/legoomni/src/main/legomain.cpp index 0f455d7a0..ecb9c6933 100644 --- a/LEGO1/lego/legoomni/src/main/legomain.cpp +++ b/LEGO1/lego/legoomni/src/main/legomain.cpp @@ -2,6 +2,7 @@ #include "3dmanager/lego3dmanager.h" #include "extensions/siloader.h" +#include "extensions/thirdpersoncamera.h" #include "islepathactor.h" #include "legoanimationmanager.h" #include "legobuildingmanager.h" @@ -355,6 +356,7 @@ MxResult LegoOmni::Create(MxOmniCreateParam& p_param) m_gameState->SetCurrentAct(LegoGameState::e_act1); #endif + Extension::Call(TP::HandleCreate); result = SUCCESS; } else { @@ -414,7 +416,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 @@ -482,7 +484,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; } @@ -677,7 +679,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(); } @@ -740,5 +742,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 8884551b7..bebf667d3 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 d1908fe33..349c90a3e 100644 --- a/LEGO1/lego/legoomni/src/worlds/isle.cpp +++ b/LEGO1/lego/legoomni/src/worlds/isle.cpp @@ -216,7 +216,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) { diff --git a/LEGO1/modeldb/modeldb.cpp b/LEGO1/modeldb/modeldb.cpp index 919e8685f..e72cc4a6b 100644 --- a/LEGO1/modeldb/modeldb.cpp +++ b/LEGO1/modeldb/modeldb.cpp @@ -23,7 +23,7 @@ MxResult ModelDbModel::Read(SDL_IOStream* p_file) return FAILURE; } - m_modelName = new char[len]; + m_modelName = new char[((len + 3) & ~3u)]; if (SDL_ReadIO(p_file, m_modelName, len) != len) { return FAILURE; } @@ -38,7 +38,7 @@ MxResult ModelDbModel::Read(SDL_IOStream* p_file) return FAILURE; } - m_presenterName = new char[len]; + m_presenterName = new char[((len + 3) & ~3u)]; if (SDL_ReadIO(p_file, m_presenterName, len) != len) { return FAILURE; } diff --git a/LEGO1/omni/src/notify/mxnotificationmanager.cpp b/LEGO1/omni/src/notify/mxnotificationmanager.cpp index 373c8aef3..e61177884 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/common/animutils.h b/extensions/include/extensions/common/animutils.h new file mode 100644 index 000000000..80f63a6bd --- /dev/null +++ b/extensions/include/extensions/common/animutils.h @@ -0,0 +1,107 @@ +#pragma once + +#include "mxgeometry/mxmatrix.h" +#include "mxtypes.h" +#include "realtime/vector.h" +#include "roi/legoroi.h" + +#include +#include +#include + +class LegoAnim; + +namespace Extensions +{ +namespace Common +{ + +namespace AnimUtils +{ + +// Cached ROI map entry for an animation +struct AnimCache { + LegoAnim* anim; + LegoROI** roiMap; + MxU32 roiMapSize; + + AnimCache() : anim(nullptr), roiMap(nullptr), roiMapSize(0) {} + ~AnimCache() + { + if (roiMap) { + delete[] roiMap; + } + } + + AnimCache(const AnimCache&) = delete; + AnimCache& operator=(const AnimCache&) = delete; + AnimCache(AnimCache&& p_other) noexcept : anim(p_other.anim), roiMap(p_other.roiMap), roiMapSize(p_other.roiMapSize) + { + p_other.roiMap = nullptr; + p_other.roiMapSize = 0; + p_other.anim = nullptr; + } + AnimCache& operator=(AnimCache&& p_other) noexcept + { + if (this != &p_other) { + if (roiMap) { + delete[] roiMap; + } + anim = p_other.anim; + roiMap = p_other.roiMap; + roiMapSize = p_other.roiMapSize; + p_other.roiMap = nullptr; + p_other.roiMapSize = 0; + p_other.anim = nullptr; + } + return *this; + } +}; + +// Maps an animation character name to an ROI without renaming the ROI. +// Used for participant ROIs whose real names differ from the animation +// tree node names. +struct ROIAlias { + const char* animName; // name in animation tree (lowercased) + LegoROI* roi; // actual ROI to use +}; + +void BuildROIMap( + LegoAnim* p_anim, + LegoROI* p_rootROI, + LegoROI** p_extraROIs, + int p_extraROICount, + LegoROI**& p_roiMap, + MxU32& p_roiMapSize, + const ROIAlias* p_aliases = nullptr, + int p_aliasCount = 0 +); + +AnimCache* GetOrBuildAnimCache(std::map& p_cacheMap, LegoROI* p_roi, const char* p_animName); + +inline void EnsureROIMapVisibility(LegoROI** p_roiMap, MxU32 p_roiMapSize) +{ + for (MxU32 i = 1; i < p_roiMapSize; i++) { + if (p_roiMap[i] != nullptr) { + p_roiMap[i]->SetVisibility(TRUE); + } + } +} + +// Apply animation transformation to all root children of an animation tree. +void ApplyTree(LegoAnim* p_anim, MxMatrix& p_transform, LegoTime p_time, LegoROI** p_roiMap); + +// 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 Common +} // namespace Extensions diff --git a/extensions/include/extensions/common/arearestriction.h b/extensions/include/extensions/common/arearestriction.h new file mode 100644 index 000000000..3afdae7d3 --- /dev/null +++ b/extensions/include/extensions/common/arearestriction.h @@ -0,0 +1,31 @@ +#pragma once + +#include "legogamestate.h" + +namespace Extensions +{ +namespace Common +{ + +// Overlay areas within the Isle world (e_act1) that use fixed camera angles +// and have no free-roaming player movement. The player character should not +// be visible in these areas. +inline bool IsRestrictedArea(LegoGameState::Area p_area) +{ + switch (p_area) { + case LegoGameState::e_elevride: + case LegoGameState::e_elevride2: + case LegoGameState::e_elevopen: + case LegoGameState::e_seaview: + case LegoGameState::e_observe: + case LegoGameState::e_elevdown: + case LegoGameState::e_garadoor: + case LegoGameState::e_polidoor: + return true; + default: + return false; + } +} + +} // namespace Common +} // namespace Extensions diff --git a/extensions/include/extensions/common/characteranimator.h b/extensions/include/extensions/common/characteranimator.h new file mode 100644 index 000000000..1d7327a43 --- /dev/null +++ b/extensions/include/extensions/common/characteranimator.h @@ -0,0 +1,191 @@ +#pragma once + +#include "extensions/common/animutils.h" +#include "extensions/common/constants.h" +#include "mxgeometry/mxmatrix.h" +#include "mxtypes.h" + +#include +#include +#include +#include + +class LegoCacheSound; +class LegoROI; +class LegoAnim; + +namespace Extensions +{ +namespace Common +{ + +struct PropGroup; + +// Interface for optional extra animation extensions. +// Consumers provide an implementation; CharacterAnimator delegates to it. +struct IExtraAnimHandler { + virtual ~IExtraAnimHandler() = default; + + // Returns true if the given extra animation ID is valid. + virtual bool IsValid(uint8_t p_id) const = 0; + + // Returns true if the extra animation is multi-part (has a secondary/recovery phase). + // Multi-part animations freeze at the last frame of phase 1 until phase 2 is triggered. + virtual bool IsMultiPart(uint8_t p_id) const = 0; + + // Get the animation name for a phase (0 = primary, 1 = recovery). + virtual const char* GetAnimName(uint8_t p_id, int p_phase) const = 0; + + // Get the sound key for a phase (nullptr = no sound). + virtual const char* GetSoundName(uint8_t p_id, int p_phase) const = 0; + + // Build dynamically-created prop ROIs for an animation. + virtual void BuildProps(PropGroup& p_group, LegoAnim* p_anim, LegoROI* p_playerROI, uint32_t p_propSuffix) = 0; +}; + +// Configuration for CharacterAnimator behavior that differs between consumers. +struct CharacterAnimatorConfig { + // When true, save/restore the parent ROI transform during extra animation playback + // to prevent scale accumulation (needed for ThirdPersonCameraExt's display clone). + bool saveExtraAnimTransform; + + // Suffix used for unique naming of prop ROIs. + uint32_t propSuffix; + + // Optional handler for extra animations. When nullptr, extra animation + // methods (TriggerExtraAnim, etc.) are no-ops. + IExtraAnimHandler* extraAnimHandler = nullptr; +}; + +// A group of dynamically-created prop ROIs for an animation (ride or extra). +struct PropGroup { + LegoAnim* anim = nullptr; + LegoROI** roiMap = nullptr; + MxU32 roiMapSize = 0; + LegoROI** propROIs = nullptr; + uint8_t propCount = 0; +}; + +// Character animation component for walk/idle playback, vehicle ride animations, +// and optional extra animation support via IExtraAnimHandler. +class CharacterAnimator { +public: + explicit CharacterAnimator(const CharacterAnimatorConfig& p_config); + ~CharacterAnimator(); + + // Core animation tick. Call each frame with the character's ROI and movement state. + void Tick(float p_deltaTime, LegoROI* p_roi, bool p_isMoving); + + // Walk/idle animation selection + void SetWalkAnimId(uint8_t p_walkAnimId, LegoROI* p_roi); + void SetIdleAnimId(uint8_t p_idleAnimId, LegoROI* p_roi); + uint8_t GetWalkAnimId() const { return m_walkAnimId; } + uint8_t GetIdleAnimId() const { return m_idleAnimId; } + + // Extra animation playback (no-op if no handler is set) + void TriggerExtraAnim(uint8_t p_id, LegoROI* p_roi, bool p_isMoving); + void SetExtraAnimHandler(IExtraAnimHandler* p_handler) { m_config.extraAnimHandler = p_handler; } + + // Click animation tracking + void SetClickAnimObjectId(MxU32 p_clickAnimObjectId) { m_clickAnimObjectId = p_clickAnimObjectId; } + void StopClickAnimation(); + + // Stop all sounds that were played against the character ROI. + // Must be called before the ROI is destroyed to prevent use-after-free + // in the sound system's 3D position update. + void StopROISounds(); + + // Vehicle ride animation + void BuildRideAnimation(int8_t p_vehicleType, LegoROI* p_playerROI); + void ClearRideAnimation(); + int8_t GetCurrentVehicleType() const { return m_currentVehicleType; } + void SetCurrentVehicleType(int8_t p_vehicleType) { m_currentVehicleType = p_vehicleType; } + bool IsInVehicle() const { return m_currentVehicleType != VEHICLE_NONE; } + LegoROI* GetRideVehicleROI() const { return m_ridePropGroup.propCount > 0 ? m_ridePropGroup.propROIs[0] : nullptr; } + LegoAnim* GetRideAnim() const { return m_ridePropGroup.anim; } + LegoROI** GetRideRoiMap() const { return m_ridePropGroup.roiMap; } + MxU32 GetRideRoiMapSize() const { return m_ridePropGroup.roiMapSize; } + + // Animation cache management + void InitAnimCaches(LegoROI* p_roi); + void ClearAnimCaches(); + void ClearAll(); + void ApplyIdleFrame0(LegoROI* p_roi); + + // Extra animation state accessors + bool IsExtraAnimActive() const { return m_extraAnimActive; } + + // Returns true when an extra animation is blocking movement (multi-part in any phase). + bool IsExtraAnimBlocking() const + { + return m_config.extraAnimHandler && + (m_frozenExtraAnimId >= 0 || + (m_extraAnimActive && m_config.extraAnimHandler->IsMultiPart(m_currentExtraAnimId))); + } + int8_t GetFrozenExtraAnimId() const { return m_frozenExtraAnimId; } + void SetFrozenExtraAnimId(int8_t p_id, LegoROI* p_roi); + + // 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(); + + static constexpr float ANIM_TIME_SCALE = 2000.0f; + static constexpr float EXTRA_ANIM_TIME_SCALE = 1000.0f; + static constexpr float IDLE_DELAY_SECONDS = 2.5f; + +private: + using AnimCache = AnimUtils::AnimCache; + + AnimCache* GetOrBuildAnimCache(LegoROI* p_roi, const char* p_animName); + void StartExtraAnimPhase(uint8_t p_id, int p_phaseIndex, AnimCache* p_cache, LegoROI* p_roi); + void ClearFrozenState(); + void ClearPropGroup(PropGroup& p_group); + void PlayROISound(const char* p_key, LegoROI* p_roi); + + CharacterAnimatorConfig m_config; + + // Walk/idle state + uint8_t m_walkAnimId; + uint8_t m_idleAnimId; + AnimCache* m_walkAnimCache; + AnimCache* m_idleAnimCache; + float m_animTime; + float m_idleTime; + float m_idleAnimTime; + bool m_wasMoving; + + // Extra animation state + AnimCache* m_extraAnimCache; + float m_extraAnimTime; + float m_extraAnimDuration; + bool m_extraAnimActive; + uint8_t m_currentExtraAnimId; + MxMatrix m_extraAnimParentTransform; + + // Multi-part extra animation frozen state (-1 = not frozen) + int8_t m_frozenExtraAnimId; + AnimCache* m_frozenAnimCache; + float m_frozenAnimDuration; + MxMatrix m_frozenParentTransform; + + // Click animation tracking (0 = none) + MxU32 m_clickAnimObjectId; + + // Sounds played against the character ROI, tracked so they can be + // stopped before the ROI is destroyed. + std::vector m_ROISounds; + + // ROI map cache: animation name -> cached ROI map + std::map m_animCacheMap; + + // Ride animation (vehicle-specific) + PropGroup m_ridePropGroup; + int8_t m_currentVehicleType; + + // Extra animation props + PropGroup m_extraAnimPropGroup; +}; + +} // namespace Common +} // namespace Extensions diff --git a/extensions/include/extensions/common/charactercloner.h b/extensions/include/extensions/common/charactercloner.h new file mode 100644 index 000000000..88170b0f6 --- /dev/null +++ b/extensions/include/extensions/common/charactercloner.h @@ -0,0 +1,42 @@ +#pragma once + +#include "extensions/common/constants.h" +#include "legoactors.h" +#include "misc.h" + +#include +#include + +class LegoCharacterManager; +class LegoROI; + +namespace Extensions +{ +namespace Common +{ + +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. + // Same construction logic as CreateActorROI but with a unique name and + // no side effects on g_actorInfo[].m_roi. + static LegoROI* Clone(LegoCharacterManager* p_charMgr, const char* p_uniqueName, const char* p_characterType); +}; + +} // namespace Common +} // namespace Extensions diff --git a/extensions/include/extensions/common/charactercustomizer.h b/extensions/include/extensions/common/charactercustomizer.h new file mode 100644 index 000000000..23426708b --- /dev/null +++ b/extensions/include/extensions/common/charactercustomizer.h @@ -0,0 +1,49 @@ +#pragma once + +#include "mxtypes.h" + +#include + +class LegoROI; + +namespace Extensions +{ +namespace Common +{ + +struct CustomizeState; + +class CharacterCustomizer { +public: + static uint8_t ResolveActorInfoIndex(uint8_t p_displayActorIndex); + + static bool SwitchColor(LegoROI* p_rootROI, uint8_t p_actorInfoIndex, CustomizeState& p_state, int p_partIndex); + static bool SwitchVariant(LegoROI* p_rootROI, uint8_t p_actorInfoIndex, CustomizeState& p_state); + static bool SwitchSound(CustomizeState& p_state); + static bool SwitchMove(CustomizeState& p_state); + static bool SwitchMood(CustomizeState& p_state); + static void ApplyFullState(LegoROI* p_rootROI, uint8_t p_actorInfoIndex, const CustomizeState& p_state); + static void ApplyChange( + LegoROI* p_rootROI, + uint8_t p_actorInfoIndex, + CustomizeState& p_state, + uint8_t p_changeType, + uint8_t p_partIndex + ); + static int MapClickedPartIndex(const char* p_partName); + static void PlayClickSound(LegoROI* p_roi, const CustomizeState& p_state, bool p_basedOnMood); + 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 Common +} // namespace Extensions diff --git a/extensions/include/extensions/common/charactertables.h b/extensions/include/extensions/common/charactertables.h new file mode 100644 index 000000000..e5193f845 --- /dev/null +++ b/extensions/include/extensions/common/charactertables.h @@ -0,0 +1,32 @@ +#pragma once + +#include "extensions/common/constants.h" + +#include + +class LegoPathActor; + +namespace Extensions +{ +namespace Common +{ + +// Animation and vehicle tables (defined in charactertables.cpp) +extern const char* const g_walkAnimNames[]; +extern const int g_walkAnimCount; + +extern const char* const g_idleAnimNames[]; +extern const int g_idleAnimCount; + +extern const char* const g_vehicleROINames[VEHICLE_COUNT]; +extern const char* const g_rideAnimNames[VEHICLE_COUNT]; +extern const char* const g_rideVehicleROINames[VEHICLE_COUNT]; + +// Returns true if the vehicle type has no ride animation (model swap instead) +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 Common +} // namespace Extensions diff --git a/extensions/include/extensions/common/constants.h b/extensions/include/extensions/common/constants.h new file mode 100644 index 000000000..df63af202 --- /dev/null +++ b/extensions/include/extensions/common/constants.h @@ -0,0 +1,44 @@ +#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; + +static constexpr float FIXED_TICK_DELTA = 0.016f; // ~60 Hz + +// 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/common/customizestate.h b/extensions/include/extensions/common/customizestate.h new file mode 100644 index 000000000..2a15030ea --- /dev/null +++ b/extensions/include/extensions/common/customizestate.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +namespace Extensions +{ +namespace Common +{ + +struct CustomizeState { + uint8_t colorIndices[10] = {}; // m_nameIndex per body part (matching LegoActorInfo::Part::m_nameIndex) + uint8_t hatVariantIndex = 0; // m_partNameIndex for infohat part + uint8_t sound = 0; // 0 to 8 + uint8_t move = 0; // 0 to 3 + uint8_t mood = 0; // 0 to 3 + + void InitFromActorInfo(uint8_t p_actorInfoIndex); + void DeriveDependentIndices(); +}; + +} // namespace Common +} // namespace Extensions diff --git a/extensions/include/extensions/common/pathutils.h b/extensions/include/extensions/common/pathutils.h new file mode 100644 index 000000000..ba3e7eec1 --- /dev/null +++ b/extensions/include/extensions/common/pathutils.h @@ -0,0 +1,17 @@ +#pragma once + +#include "mxstring.h" + +namespace Extensions +{ +namespace Common +{ + +// Resolve a relative game path (e.g. "\\lego\\scripts\\isle\\isle.si") +// by trying the HD path first, then falling back to CD. +// Returns true if the file exists at either location, with the +// filesystem-mapped result in p_outPath. +bool ResolveGamePath(const char* p_relativePath, MxString& p_outPath); + +} // namespace Common +} // namespace Extensions diff --git a/extensions/include/extensions/extensions.h b/extensions/include/extensions/extensions.h index 9f02a2814..26f768b12 100644 --- a/extensions/include/extensions/extensions.h +++ b/extensions/include/extensions/extensions.h @@ -6,24 +6,38 @@ #include #include #include +#include namespace Extensions { -constexpr const char* availableExtensions[] = {"extensions:texture loader", "extensions:si loader"}; +constexpr const char* availableExtensions[] = + {"extensions:texture loader", "extensions:si loader", "extensions:third person camera"}; LEGO1_EXPORT void Enable(const char* p_key, std::map p_options); template struct Extension { template - static auto Call(Function&& function, Args&&... args) -> std::optional> + static auto Call(Function&& function, Args&&... args) { + using result_t = std::invoke_result_t; + if constexpr (std::is_void_v) { #ifdef EXTENSIONS - if (T::enabled) { - return std::invoke(std::forward(function), std::forward(args)...); + if (T::enabled) { + std::invoke(std::forward(function), std::forward(args)...); + } +#endif } + else { +#ifdef EXTENSIONS + if (T::enabled) { + return std::optional( + std::invoke(std::forward(function), std::forward(args)...) + ); + } #endif - return std::nullopt; + return std::optional(std::nullopt); + } } }; }; // namespace Extensions diff --git a/extensions/include/extensions/fwd.h b/extensions/include/extensions/fwd.h new file mode 100644 index 000000000..8e987fbde --- /dev/null +++ b/extensions/include/extensions/fwd.h @@ -0,0 +1,18 @@ +#ifndef EXTENSIONS_FWD_H +#define EXTENSIONS_FWD_H + +namespace Extensions +{ +class ThirdPersonCameraExt; +namespace Common +{ +class CharacterCloner; +} +namespace ThirdPersonCamera +{ +class Controller; +class OrbitCamera; +} // namespace ThirdPersonCamera +} // namespace Extensions + +#endif // EXTENSIONS_FWD_H diff --git a/extensions/include/extensions/siloader.h b/extensions/include/extensions/siloader.h index e581f6b1f..3e727ecdc 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 a318c9b8e..234f4cdbb 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 000000000..958f3b828 --- /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 000000000..eb0fca737 --- /dev/null +++ b/extensions/include/extensions/thirdpersoncamera/controller.h @@ -0,0 +1,150 @@ +#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 +#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 p_preserveTouch = false); + 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 TriggerExtraAnim(uint8_t p_id); + bool IsExtraAnimBlocking() const; + int8_t GetFrozenExtraAnimId() const; + void SetExtraAnimHandler(Common::IExtraAnimHandler* p_handler) { m_animator.SetExtraAnimHandler(p_handler); } + + 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(); } + LegoROI* GetRideVehicleROI() const { return m_animator.GetRideVehicleROI(); } + + // Signal that an external animation is active. + // p_lockDisplay: true if the display ROI is being driven by the animation (performer), + // false if just spectating (idle anim continues). + // p_onStop is called before the display ROI is destroyed (Deactivate/OnWorldDisabled). + void SetAnimPlaying( + bool p_animPlaying, + bool p_lockDisplay = true, + std::function p_animStopCallback = nullptr + ) + { + m_animPlaying = p_animPlaying; + m_animLockDisplay = p_animPlaying && p_lockDisplay; + m_animStopCallback = p_animPlaying ? std::move(p_animStopCallback) : nullptr; + } + bool IsAnimPlaying() const { return m_animPlaying; } + + 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(); } + + bool IsLeftButtonHeld() const { return m_input.IsLeftButtonHeld(); } + bool IsLmbForwardEngaged() const { return m_lmbForwardEngaged; } + void SetLmbForwardEngaged(bool p_engaged) { m_lmbForwardEngaged = p_engaged; } + + MxBool HandleFirstPersonForward( + LegoNavController* p_nav, + const Vector3& p_curPos, + const Vector3& p_curDir, + Vector3& p_newPos, + Vector3& p_newDir, + float p_deltaTime + ); + + float GetOrbitDistance() const { return m_orbit.GetOrbitDistance(); } + void SetOrbitDistance(float p_distance) { m_orbit.SetOrbitDistance(p_distance); } + void ResetTouchState() { m_input.ResetTouchState(); } + void SuppressGestures() { m_input.SuppressGestures(); } + + bool TryClaimFinger(const SDL_TouchFingerEvent& event) { return m_input.TryClaimFinger(event); } + bool TryReleaseFinger(SDL_FingerID id) { return m_input.TryReleaseFinger(id); } + bool IsFingerTracked(SDL_FingerID id) const { return m_input.IsFingerTracked(id); } + int GetTouchCount() const { return m_input.GetTouchCount(); } + SDL_FingerID GetFingerID(int idx) const { return m_input.GetFingerID(idx); } + + 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 CancelExternalAnim(); + void Deactivate(); + void ReinitForCharacter(); + + OrbitCamera m_orbit; + InputHandler m_input; + DisplayActor m_display; + Common::CharacterAnimator m_animator; + + bool m_enabled; + bool m_active; + bool m_pendingWorldTransition; + bool m_animPlaying; + bool m_animLockDisplay; + std::function m_animStopCallback; + bool m_lmbForwardEngaged; + 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 000000000..820ac464e --- /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 000000000..28d026efe --- /dev/null +++ b/extensions/include/extensions/thirdpersoncamera/inputhandler.h @@ -0,0 +1,61 @@ +#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 TryReleaseFinger(SDL_FingerID p_id); + bool IsFingerTracked(SDL_FingerID p_id) const; + int GetTouchCount() const { return m_touch.count; } + SDL_FingerID GetFingerID(int p_idx) const { return m_touch.id[p_idx]; } + + bool IsLeftButtonHeld() const { return m_leftButtonHeld; } + bool IsLmbHeldForMovement() const; + + bool ConsumeAutoDisable(); + bool ConsumeAutoEnable(); + + void ResetTouchState() { m_touch = {}; } + void SuppressGestures(); + + static constexpr float CAMERA_ZONE_X = 0.5f; + static constexpr float PINCH_TRANSITION_THRESHOLD = 0.03f; + static constexpr Uint64 LMB_HOLD_THRESHOLD_MS = 300; + static constexpr float MOUSE_SENSITIVITY = 0.005f; + static constexpr float MOUSE_WHEEL_ZOOM_STEP = 0.5f; + static constexpr float TOUCH_YAW_PITCH_SCALE = 2.0f; + static constexpr float PINCH_ZOOM_SCALE = 6.0f; + +private: + struct TouchState { + SDL_FingerID id[2]; + float x[2], y[2]; + bool synced[2]; + int count; + float initialPinchDist; + float gesturePinchDist; + } m_touch; + + bool m_wantsAutoDisable; + bool m_wantsAutoEnable; + bool m_rightButtonHeld; + bool m_leftButtonHeld; + Uint64 m_leftButtonDownTime; + float m_savedMouseX; + float m_savedMouseY; +}; + +} // 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 000000000..d4ae2f0a8 --- /dev/null +++ b/extensions/include/extensions/thirdpersoncamera/orbitcamera.h @@ -0,0 +1,76 @@ +#pragma once + +#include "mxgeometry/mxgeometry3d.h" +#include "mxtypes.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_isBlocked, + bool p_lmbHeld + ); + + 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 SWITCH_TO_FIRST_PERSON_DISTANCE = 0.5f; + static constexpr float MAX_DISTANCE = 15.0f; + static constexpr float CHARACTER_TURN_RATE = 10.0f; + static constexpr float JOYSTICK_CENTER = 50.0f; + static constexpr float JOYSTICK_DEAD_ZONE = 0.1f; + static constexpr float MOVEMENT_DIR_EPSILON = 0.001f; + +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/common/animutils.cpp b/extensions/src/common/animutils.cpp new file mode 100644 index 000000000..ef91f4ed6 --- /dev/null +++ b/extensions/src/common/animutils.cpp @@ -0,0 +1,270 @@ +#include "extensions/common/animutils.h" + +#include "anim/legoanim.h" +#include "legoanimpresenter.h" +#include "legoworld.h" +#include "misc.h" +#include "misc/legotree.h" +#include "roi/legoroi.h" + +#include +#include + +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. +// +// Intentional divergences from LegoAnimPresenter::BuildROIMap (legoanimpresenter.cpp:413-530): +// 1. No variable substitution -- we bypass the streaming pipeline, so the variable +// table lacks our entries. Direct name comparison instead. +// 2. *-prefixed nodes search extraROIs -- the original's GetActorName() depends on +// presenter action context (m_action->GetUnknown24()). We search created extra +// ROIs directly. +// 3. No LegoAnimStructMap dedup -- sequential indices, functionally correct. +// Look up an animation node name in the alias map (case-insensitive). +static LegoROI* FindAlias(const char* p_name, const AnimUtils::ROIAlias* p_aliases, int p_aliasCount) +{ + for (int i = 0; i < p_aliasCount; i++) { + if (p_aliases[i].animName && !SDL_strcasecmp(p_name, p_aliases[i].animName)) { + return p_aliases[i].roi; + } + } + return nullptr; +} + +static void AssignROIIndices( + LegoTreeNode* p_node, + LegoROI* p_parentROI, + LegoROI* p_rootROI, + LegoROI** p_extraROIs, + int p_extraROICount, + const AnimUtils::ROIAlias* p_aliases, + int p_aliasCount, + MxU32& p_nextIndex, + std::vector& p_entries, + bool& p_rootClaimed +) +{ + LegoROI* roi = p_parentROI; + LegoAnimNodeData* data = (LegoAnimNodeData*) p_node->GetData(); + const char* name = data ? data->GetName() : nullptr; + + if (name != nullptr && *name != '-') { + LegoROI* matchedROI = nullptr; + + if (*name == '*' || p_parentROI == nullptr) { + roi = p_rootROI; + + const char* searchName = (*name == '*') ? name + 1 : name; + bool matchedExtra = false; + + // Check aliases first (participant ROIs mapped by character name). + // Claiming root prevents subsequent sibling nodes from also claiming it. + matchedROI = FindAlias(searchName, p_aliases, p_aliasCount); + if (matchedROI) { + roi = matchedROI; + matchedExtra = true; + p_rootClaimed = true; + } + + // Then check extra ROIs by name. + // This handles cases like BIKESY appearing before SY in the tree: + // BIKESY should match the vehicle extra, not claim the root. + if (!matchedExtra && p_extraROICount > 0) { + for (int e = 0; e < p_extraROICount; e++) { + matchedROI = p_extraROIs[e]->FindChildROI(searchName, p_extraROIs[e]); + if (matchedROI != nullptr) { + roi = matchedROI; + matchedExtra = true; + break; + } + } + } + + if (!matchedExtra) { + if (!p_rootClaimed) { + matchedROI = p_rootROI; + p_rootClaimed = true; + } + } + } + else { + matchedROI = p_parentROI->FindChildROI(name, p_parentROI); + if (matchedROI == nullptr) { + // Check aliases — also update roi so children resolve against the alias ROI + matchedROI = FindAlias(name, p_aliases, p_aliasCount); + if (matchedROI) { + roi = matchedROI; + } + } + if (matchedROI == nullptr) { + for (int e = 0; e < p_extraROICount; e++) { + matchedROI = p_extraROIs[e]->FindChildROI(name, p_extraROIs[e]); + if (matchedROI != nullptr) { + break; + } + } + } + // Mirrors original game (legoanimpresenter.cpp:486-490): + // If FindChildROI fails, the node might be a top-level actor that isn't + // a child of the current parent. Re-run this node with p_parentROI=NULL + // so it enters the root-claiming / top-level search path instead. + if (matchedROI == nullptr) { + bool isTopLevel = false; + // Check aliases for top-level match + if (FindAlias(name, p_aliases, p_aliasCount) != nullptr) { + isTopLevel = true; + } + if (!isTopLevel && !p_rootClaimed && p_rootROI->GetName() && + !SDL_strcasecmp(name, p_rootROI->GetName())) { + isTopLevel = true; + } + if (!isTopLevel) { + for (int e = 0; e < p_extraROICount; e++) { + if (p_extraROIs[e]->GetName() && !SDL_strcasecmp(name, p_extraROIs[e]->GetName())) { + isTopLevel = true; + break; + } + } + } + if (isTopLevel) { + AssignROIIndices( + p_node, + nullptr, + p_rootROI, + p_extraROIs, + p_extraROICount, + p_aliases, + p_aliasCount, + p_nextIndex, + p_entries, + p_rootClaimed + ); + return; + } + } + } + + if (matchedROI != nullptr) { + data->SetROIIndex(p_nextIndex); + p_entries.push_back(matchedROI); + p_nextIndex++; + } + else { + data->SetROIIndex(0); + } + } + + for (MxS32 i = 0; i < p_node->GetNumChildren(); i++) { + AssignROIIndices( + p_node->GetChild(i), + roi, + p_rootROI, + p_extraROIs, + p_extraROICount, + p_aliases, + p_aliasCount, + p_nextIndex, + p_entries, + p_rootClaimed + ); + } +} + +void AnimUtils::BuildROIMap( + LegoAnim* p_anim, + LegoROI* p_rootROI, + LegoROI** p_extraROIs, + int p_extraROICount, + LegoROI**& p_roiMap, + MxU32& p_roiMapSize, + const ROIAlias* p_aliases, + int p_aliasCount +) +{ + if (!p_anim || !p_rootROI) { + return; + } + + LegoTreeNode* root = p_anim->GetRoot(); + if (!root) { + return; + } + + MxU32 nextIndex = 1; + std::vector entries; + bool rootClaimed = false; + AssignROIIndices( + root, + nullptr, + p_rootROI, + p_extraROIs, + p_extraROICount, + p_aliases, + p_aliasCount, + nextIndex, + entries, + rootClaimed + ); + + if (entries.empty()) { + return; + } + + // 1-indexed; index 0 reserved as NULL + p_roiMapSize = entries.size() + 1; + p_roiMap = new LegoROI*[p_roiMapSize]; + p_roiMap[0] = nullptr; + for (MxU32 i = 0; i < entries.size(); i++) { + p_roiMap[i + 1] = entries[i]; + } +} + +AnimUtils::AnimCache* AnimUtils::GetOrBuildAnimCache( + std::map& p_cacheMap, + LegoROI* p_roi, + const char* p_animName +) +{ + if (!p_animName || !p_roi) { + return nullptr; + } + + // Check if already cached + auto it = p_cacheMap.find(p_animName); + if (it != p_cacheMap.end()) { + return &it->second; + } + + // Look up the animation presenter in the current world + LegoWorld* world = CurrentWorld(); + if (!world) { + return nullptr; + } + + MxCore* presenter = world->Find("LegoAnimPresenter", p_animName); + if (!presenter) { + return nullptr; + } + + LegoAnim* anim = static_cast(presenter)->GetAnimation(); + if (!anim) { + return nullptr; + } + + // Build and cache + AnimCache& cache = p_cacheMap[p_animName]; + cache.anim = anim; + BuildROIMap(anim, p_roi, nullptr, 0, cache.roiMap, cache.roiMapSize); + + return &cache; +} + +void AnimUtils::ApplyTree(LegoAnim* p_anim, MxMatrix& p_transform, LegoTime p_time, LegoROI** p_roiMap) +{ + LegoTreeNode* root = p_anim->GetRoot(); + for (LegoU32 i = 0; i < root->GetNumChildren(); i++) { + LegoROI::ApplyAnimationTransformation(root->GetChild(i), p_transform, p_time, p_roiMap); + } +} diff --git a/extensions/src/common/characteranimator.cpp b/extensions/src/common/characteranimator.cpp new file mode 100644 index 000000000..0f27dab48 --- /dev/null +++ b/extensions/src/common/characteranimator.cpp @@ -0,0 +1,470 @@ +#include "extensions/common/characteranimator.h" + +#include "3dmanager/lego3dmanager.h" +#include "anim/legoanim.h" +#include "extensions/common/charactercustomizer.h" +#include "extensions/common/charactertables.h" +#include "legoanimpresenter.h" +#include "legocachesoundmanager.h" +#include "legocachsound.h" +#include "legocharactermanager.h" +#include "legosoundmanager.h" +#include "legovideomanager.h" +#include "legoworld.h" +#include "misc.h" +#include "misc/legotree.h" +#include "realtime/realtime.h" +#include "roi/legoroi.h" + +#include + +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_extraAnimCache(nullptr), + m_extraAnimTime(0.0f), m_extraAnimDuration(0.0f), m_extraAnimActive(false), m_currentExtraAnimId(0), + m_frozenExtraAnimId(-1), m_frozenAnimCache(nullptr), m_frozenAnimDuration(0.0f), m_clickAnimObjectId(0), + m_currentVehicleType(VEHICLE_NONE) +{ +} + +CharacterAnimator::~CharacterAnimator() +{ + ClearPropGroup(m_extraAnimPropGroup); + ClearRideAnimation(); +} + +CharacterAnimator::AnimCache* CharacterAnimator::GetOrBuildAnimCache(LegoROI* p_roi, const char* p_animName) +{ + return AnimUtils::GetOrBuildAnimCache(m_animCacheMap, p_roi, p_animName); +} + +void CharacterAnimator::Tick(float p_deltaTime, LegoROI* p_roi, bool p_isMoving) +{ + if (m_currentVehicleType != VEHICLE_NONE && IsLargeVehicle(m_currentVehicleType)) { + StopClickAnimation(); + return; + } + + // Determine the active walk/ride animation and its ROI map + LegoAnim* walkAnim = nullptr; + LegoROI** walkRoiMap = nullptr; + MxU32 walkRoiMapSize = 0; + + if (m_currentVehicleType != VEHICLE_NONE && m_ridePropGroup.anim && m_ridePropGroup.roiMap) { + walkAnim = m_ridePropGroup.anim; + walkRoiMap = m_ridePropGroup.roiMap; + walkRoiMapSize = m_ridePropGroup.roiMapSize; + } + else if (m_walkAnimCache && m_walkAnimCache->anim && m_walkAnimCache->roiMap) { + walkAnim = m_walkAnimCache->anim; + walkRoiMap = m_walkAnimCache->roiMap; + walkRoiMapSize = m_walkAnimCache->roiMapSize; + } + + // Ensure visibility of all mapped ROIs + if (walkRoiMap) { + AnimUtils::EnsureROIMapVisibility(walkRoiMap, walkRoiMapSize); + } + if (m_idleAnimCache && m_idleAnimCache->roiMap) { + AnimUtils::EnsureROIMapVisibility(m_idleAnimCache->roiMap, m_idleAnimCache->roiMapSize); + } + + bool inVehicle = (m_currentVehicleType != VEHICLE_NONE); + bool isMoving = inVehicle || p_isMoving; + + // Movement interrupts click animations and extra animations (but not frozen multi-part) + if (isMoving && m_frozenExtraAnimId < 0) { + StopClickAnimation(); + if (m_extraAnimActive) { + m_extraAnimActive = false; + m_extraAnimCache = nullptr; + ClearPropGroup(m_extraAnimPropGroup); + } + } + + if (isMoving) { + // Walking / riding + if (!walkAnim || !walkRoiMap) { + return; + } + + if (p_isMoving) { + m_animTime += p_deltaTime * ANIM_TIME_SCALE; + } + float duration = (float) walkAnim->GetDuration(); + if (duration > 0.0f) { + float timeInCycle = m_animTime - duration * SDL_floorf(m_animTime / duration); + + MxMatrix transform(p_roi->GetLocal2World()); + AnimUtils::ApplyTree(walkAnim, transform, (LegoTime) timeInCycle, walkRoiMap); + } + m_wasMoving = true; + m_idleTime = 0.0f; + m_idleAnimTime = 0.0f; + } + else if (m_extraAnimActive && m_extraAnimCache && m_extraAnimCache->anim && m_extraAnimCache->roiMap) { + // Extra animation playback + m_extraAnimTime += p_deltaTime * EXTRA_ANIM_TIME_SCALE; + + if (m_extraAnimTime >= m_extraAnimDuration) { + bool isMultiPart = + m_config.extraAnimHandler && m_config.extraAnimHandler->IsMultiPart(m_currentExtraAnimId); + if (isMultiPart && m_frozenExtraAnimId < 0) { + // Phase 1 completed -> freeze at last frame + m_frozenExtraAnimId = (int8_t) m_currentExtraAnimId; + m_frozenAnimCache = m_extraAnimCache; + m_frozenAnimDuration = m_extraAnimDuration; + m_extraAnimActive = false; + if (m_config.saveExtraAnimTransform) { + m_frozenParentTransform = m_extraAnimParentTransform; + } + } + else { + if (isMultiPart && m_frozenExtraAnimId >= 0) { + // Phase 2 completed -> unfreeze + ClearFrozenState(); + } + // Extra animation completed -- return to stationary flow + m_extraAnimActive = false; + m_extraAnimCache = nullptr; + ClearPropGroup(m_extraAnimPropGroup); + m_wasMoving = false; + m_idleTime = 0.0f; + m_idleAnimTime = 0.0f; + } + } + else { + LegoROI** extraRoiMap = + m_extraAnimPropGroup.roiMap != nullptr ? m_extraAnimPropGroup.roiMap : m_extraAnimCache->roiMap; + MxMatrix transform(m_config.saveExtraAnimTransform ? m_extraAnimParentTransform : p_roi->GetLocal2World()); + + AnimUtils::ApplyTree(m_extraAnimCache->anim, transform, (LegoTime) m_extraAnimTime, extraRoiMap); + + // Restore player ROI transform (animation root overwrote it). + if (m_config.saveExtraAnimTransform) { + p_roi->WrappedSetLocal2WorldWithWorldDataUpdate(m_extraAnimParentTransform); + } + } + } + else if (m_frozenExtraAnimId >= 0 && m_frozenAnimCache && m_frozenAnimCache->anim && m_frozenAnimCache->roiMap) { + // Frozen at last frame of a multi-part extra animation's phase 1 + MxMatrix transform(m_config.saveExtraAnimTransform ? m_frozenParentTransform : p_roi->GetLocal2World()); + + AnimUtils::ApplyTree( + m_frozenAnimCache->anim, + transform, + (LegoTime) m_frozenAnimDuration, + m_frozenAnimCache->roiMap + ); + + if (m_config.saveExtraAnimTransform) { + p_roi->WrappedSetLocal2WorldWithWorldDataUpdate(m_frozenParentTransform); + } + } + else if (m_idleAnimCache && m_idleAnimCache->anim && m_idleAnimCache->roiMap) { + // Idle animation + if (m_wasMoving) { + m_wasMoving = false; + m_idleTime = 0.0f; + m_idleAnimTime = 0.0f; + } + + m_idleTime += p_deltaTime; + + // Hold standing pose, then loop breathing/swaying + if (m_idleTime >= IDLE_DELAY_SECONDS) { + m_idleAnimTime += p_deltaTime * 1000.0f; + } + + float duration = (float) m_idleAnimCache->anim->GetDuration(); + if (duration > 0.0f) { + float timeInCycle = m_idleAnimTime - duration * SDL_floorf(m_idleAnimTime / duration); + + MxMatrix transform(p_roi->GetLocal2World()); + AnimUtils::ApplyTree(m_idleAnimCache->anim, transform, (LegoTime) timeInCycle, m_idleAnimCache->roiMap); + } + } +} + +void CharacterAnimator::SetWalkAnimId(uint8_t p_walkAnimId, LegoROI* p_roi) +{ + if (p_walkAnimId >= g_walkAnimCount) { + return; + } + if (p_walkAnimId != m_walkAnimId) { + m_walkAnimId = p_walkAnimId; + if (p_roi) { + m_walkAnimCache = GetOrBuildAnimCache(p_roi, g_walkAnimNames[m_walkAnimId]); + } + } +} + +void CharacterAnimator::SetIdleAnimId(uint8_t p_idleAnimId, LegoROI* p_roi) +{ + if (p_idleAnimId >= g_idleAnimCount) { + return; + } + if (p_idleAnimId != m_idleAnimId) { + m_idleAnimId = p_idleAnimId; + if (p_roi) { + m_idleAnimCache = GetOrBuildAnimCache(p_roi, g_idleAnimNames[m_idleAnimId]); + } + } +} + +void CharacterAnimator::StartExtraAnimPhase(uint8_t p_id, int p_phaseIndex, AnimCache* p_cache, LegoROI* p_roi) +{ + StopClickAnimation(); + ClearPropGroup(m_extraAnimPropGroup); + + m_currentExtraAnimId = p_id; + m_extraAnimCache = p_cache; + m_extraAnimTime = 0.0f; + m_extraAnimDuration = (float) p_cache->anim->GetDuration(); + m_extraAnimActive = true; + + if (m_config.extraAnimHandler) { + m_config.extraAnimHandler->BuildProps(m_extraAnimPropGroup, p_cache->anim, p_roi, m_config.propSuffix); + } + + const char* sound = + m_config.extraAnimHandler ? m_config.extraAnimHandler->GetSoundName(p_id, p_phaseIndex) : nullptr; + if (sound) { + PlayROISound(sound, p_roi); + } +} + +void CharacterAnimator::TriggerExtraAnim(uint8_t p_id, LegoROI* p_roi, bool p_isMoving) +{ + if (!m_config.extraAnimHandler || !m_config.extraAnimHandler->IsValid(p_id)) { + return; + } + if (!p_roi || m_currentVehicleType != VEHICLE_NONE) { + return; + } + + bool isMultiPart = m_config.extraAnimHandler->IsMultiPart(p_id); + + if (isMultiPart) { + if (m_frozenExtraAnimId == (int8_t) p_id) { + // Phase 2: play the recovery animation to unfreeze + const char* animName = m_config.extraAnimHandler->GetAnimName(p_id, 1); + AnimCache* cache = animName ? GetOrBuildAnimCache(p_roi, animName) : nullptr; + if (!cache || !cache->anim) { + return; + } + + StartExtraAnimPhase(p_id, 1, cache, p_roi); + + if (m_config.saveExtraAnimTransform) { + m_extraAnimParentTransform = m_frozenParentTransform; + } + return; + } + else if (m_frozenExtraAnimId >= 0) { + // Already frozen in a different extra animation, ignore + return; + } + // Phase 1: fall through to play the primary animation + } + else { + // One-shot: block if moving or frozen in any multi-part extra animation + if (p_isMoving || m_frozenExtraAnimId >= 0) { + return; + } + } + + const char* animName = m_config.extraAnimHandler->GetAnimName(p_id, 0); + AnimCache* cache = animName ? GetOrBuildAnimCache(p_roi, animName) : nullptr; + if (!cache || !cache->anim) { + return; + } + + StartExtraAnimPhase(p_id, 0, cache, p_roi); + + // Save clean transform to prevent scale accumulation during extra animation + if (m_config.saveExtraAnimTransform) { + m_extraAnimParentTransform = p_roi->GetLocal2World(); + } +} + +void CharacterAnimator::StopClickAnimation() +{ + if (m_clickAnimObjectId != 0) { + CharacterCustomizer::StopClickAnimation(m_clickAnimObjectId); + m_clickAnimObjectId = 0; + } +} + +void CharacterAnimator::PlayROISound(const char* p_key, LegoROI* p_roi) +{ + LegoCacheSound* sound = SoundManager()->GetCacheSoundManager()->Play(p_key, p_roi->GetName(), FALSE); + if (sound) { + m_ROISounds.push_back(sound); + } +} + +void CharacterAnimator::StopROISounds() +{ + LegoCacheSoundManager* mgr = SoundManager()->GetCacheSoundManager(); + for (LegoCacheSound* sound : m_ROISounds) { + mgr->Stop(sound); + } + m_ROISounds.clear(); +} + +void CharacterAnimator::BuildRideAnimation(int8_t p_vehicleType, LegoROI* p_playerROI) +{ + if (p_vehicleType < 0 || p_vehicleType >= VEHICLE_COUNT) { + return; + } + + const char* rideAnimName = g_rideAnimNames[p_vehicleType]; + const char* vehicleVariantName = g_rideVehicleROINames[p_vehicleType]; + if (!rideAnimName || !vehicleVariantName) { + return; + } + + LegoWorld* world = CurrentWorld(); + if (!world) { + return; + } + + MxCore* presenter = world->Find("LegoAnimPresenter", rideAnimName); + if (!presenter) { + return; + } + + m_ridePropGroup.anim = static_cast(presenter)->GetAnimation(); + if (!m_ridePropGroup.anim) { + return; + } + + // Create variant ROI, rename to match animation tree. + const char* baseName = g_vehicleROINames[p_vehicleType]; + char variantName[48]; + if (m_config.propSuffix != 0) { + SDL_snprintf(variantName, sizeof(variantName), "%s_%u", vehicleVariantName, m_config.propSuffix); + } + else { + SDL_snprintf(variantName, sizeof(variantName), "tp_vehicle"); + } + LegoROI* vehicleROI = CharacterManager()->CreateAutoROI(variantName, baseName, FALSE); + if (vehicleROI) { + vehicleROI->SetName(vehicleVariantName); + m_ridePropGroup.propROIs = new LegoROI*[1]; + m_ridePropGroup.propROIs[0] = vehicleROI; + m_ridePropGroup.propCount = 1; + } + + AnimUtils::BuildROIMap( + m_ridePropGroup.anim, + p_playerROI, + m_ridePropGroup.propROIs, + m_ridePropGroup.propCount, + m_ridePropGroup.roiMap, + m_ridePropGroup.roiMapSize + ); + m_animTime = 0.0f; +} + +void CharacterAnimator::ClearRideAnimation() +{ + ClearPropGroup(m_ridePropGroup); + m_currentVehicleType = VEHICLE_NONE; +} + +void CharacterAnimator::InitAnimCaches(LegoROI* p_roi) +{ + m_walkAnimCache = GetOrBuildAnimCache(p_roi, g_walkAnimNames[m_walkAnimId]); + m_idleAnimCache = GetOrBuildAnimCache(p_roi, g_idleAnimNames[m_idleAnimId]); + + // Rebuild frozen extra animation cache if the frozen state was set before the ROI + // was available (e.g. state arrived before world was ready, or world was re-enabled). + if (m_frozenExtraAnimId >= 0 && !m_frozenAnimCache) { + SetFrozenExtraAnimId(m_frozenExtraAnimId, p_roi); + } +} + +void CharacterAnimator::SetFrozenExtraAnimId(int8_t p_id, LegoROI* p_roi) +{ + if (m_config.extraAnimHandler && p_id >= 0 && m_config.extraAnimHandler->IsValid((uint8_t) p_id) && + m_config.extraAnimHandler->IsMultiPart((uint8_t) p_id)) { + const char* animName = m_config.extraAnimHandler->GetAnimName((uint8_t) p_id, 0); + AnimCache* cache = (p_roi && animName) ? GetOrBuildAnimCache(p_roi, animName) : nullptr; + m_frozenExtraAnimId = p_id; + m_frozenAnimCache = cache; + m_frozenAnimDuration = (cache && cache->anim) ? (float) cache->anim->GetDuration() : 0.0f; + m_extraAnimActive = false; + if (m_config.saveExtraAnimTransform && p_roi) { + m_frozenParentTransform = p_roi->GetLocal2World(); + } + } + else { + ClearFrozenState(); + } +} + +void CharacterAnimator::ClearFrozenState() +{ + m_frozenExtraAnimId = -1; + m_frozenAnimCache = nullptr; + m_frozenAnimDuration = 0.0f; + ClearPropGroup(m_extraAnimPropGroup); +} + +void CharacterAnimator::ClearPropGroup(PropGroup& p_group) +{ + delete[] p_group.roiMap; + p_group.roiMap = nullptr; + p_group.roiMapSize = 0; + + for (uint8_t i = 0; i < p_group.propCount; i++) { + if (p_group.propROIs[i]) { + VideoManager()->Get3DManager()->Remove(*p_group.propROIs[i]); + CharacterManager()->ReleaseAutoROI(p_group.propROIs[i]); + } + } + delete[] p_group.propROIs; + p_group.propROIs = nullptr; + p_group.propCount = 0; + p_group.anim = nullptr; +} + +void CharacterAnimator::ClearAnimCaches() +{ + m_walkAnimCache = nullptr; + m_idleAnimCache = nullptr; + m_extraAnimCache = nullptr; + m_extraAnimActive = false; + StopROISounds(); + ClearFrozenState(); +} + +void CharacterAnimator::ClearAll() +{ + m_animCacheMap.clear(); + ClearAnimCaches(); +} + +void CharacterAnimator::ResetAnimState() +{ + m_animTime = 0.0f; + m_idleTime = 0.0f; + m_idleAnimTime = 0.0f; + m_wasMoving = false; + m_extraAnimActive = false; + ClearFrozenState(); +} + +void CharacterAnimator::ApplyIdleFrame0(LegoROI* p_roi) +{ + if (!p_roi || !m_idleAnimCache || !m_idleAnimCache->anim || !m_idleAnimCache->roiMap) { + return; + } + + MxMatrix transform(p_roi->GetLocal2World()); + AnimUtils::ApplyTree(m_idleAnimCache->anim, transform, (LegoTime) 0.0f, m_idleAnimCache->roiMap); +} diff --git a/extensions/src/common/charactercloner.cpp b/extensions/src/common/charactercloner.cpp new file mode 100644 index 000000000..a75036b29 --- /dev/null +++ b/extensions/src/common/charactercloner.cpp @@ -0,0 +1,156 @@ +#include "extensions/common/charactercloner.h" + +#include "legoactors.h" +#include "legocharactermanager.h" +#include "legovideomanager.h" +#include "misc.h" +#include "misc/legocontainer.h" +#include "realtime/realtime.h" +#include "roi/legolod.h" +#include "roi/legoroi.h" +#include "viewmanager/viewlodlist.h" + +#include +#include + +using namespace Extensions::Common; + +LegoROI* CharacterCloner::Clone(LegoCharacterManager* p_charMgr, const char* p_uniqueName, const char* p_characterType) +{ + MxBool success = FALSE; + LegoROI* roi = NULL; + BoundingSphere boundingSphere; + BoundingBox boundingBox; + MxMatrix mat; + CompoundObject* comp; + MxS32 i; + + Tgl::Renderer* renderer = VideoManager()->GetRenderer(); + ViewLODListManager* lodManager = GetViewLODListManager(); + LegoTextureContainer* textureContainer = TextureContainer(); + LegoActorInfo* info = p_charMgr->GetActorInfo(p_characterType); + + if (info == NULL) { + goto done; + } + + roi = new LegoROI(renderer); + roi->SetName(p_uniqueName); + + boundingSphere.Center()[0] = g_actorLODs[c_topLOD].m_boundingSphere[0]; + boundingSphere.Center()[1] = g_actorLODs[c_topLOD].m_boundingSphere[1]; + boundingSphere.Center()[2] = g_actorLODs[c_topLOD].m_boundingSphere[2]; + boundingSphere.Radius() = g_actorLODs[c_topLOD].m_boundingSphere[3]; + roi->SetBoundingSphere(boundingSphere); + + boundingBox.Min()[0] = g_actorLODs[c_topLOD].m_boundingBox[0]; + boundingBox.Min()[1] = g_actorLODs[c_topLOD].m_boundingBox[1]; + boundingBox.Min()[2] = g_actorLODs[c_topLOD].m_boundingBox[2]; + boundingBox.Max()[0] = g_actorLODs[c_topLOD].m_boundingBox[3]; + boundingBox.Max()[1] = g_actorLODs[c_topLOD].m_boundingBox[4]; + boundingBox.Max()[2] = g_actorLODs[c_topLOD].m_boundingBox[5]; + roi->SetBoundingBox(boundingBox); + + comp = new CompoundObject(); + roi->SetComp(comp); + + for (i = 0; i < sizeOfArray(g_actorLODs) - 1; i++) { + char lodName[256]; + LegoActorInfo::Part& part = info->m_parts[i]; + + const char* parentName; + if (i == 0 || i == 1) { + parentName = part.m_partName[part.m_partNameIndices[part.m_partNameIndex]]; + } + else { + parentName = g_actorLODs[i + 1].m_parentName; + } + + ViewLODList* lodList = lodManager->Lookup(parentName); + MxS32 lodSize = lodList->Size(); + SDL_snprintf(lodName, sizeof(lodName), "%s%d", p_uniqueName, i); + ViewLODList* dupLodList = lodManager->Create(lodName, lodSize); + + for (MxS32 j = 0; j < lodSize; j++) { + LegoLOD* lod = (LegoLOD*) (*lodList)[j]; + LegoLOD* clone = lod->Clone(renderer); + dupLodList->PushBack(clone); + } + + lodList->Release(); + lodList = dupLodList; + + LegoROI* childROI = new LegoROI(renderer, lodList); + lodList->Release(); + + childROI->SetName(g_actorLODs[i + 1].m_name); + childROI->SetParentROI(roi); + + BoundingSphere childBoundingSphere; + childBoundingSphere.Center()[0] = g_actorLODs[i + 1].m_boundingSphere[0]; + childBoundingSphere.Center()[1] = g_actorLODs[i + 1].m_boundingSphere[1]; + childBoundingSphere.Center()[2] = g_actorLODs[i + 1].m_boundingSphere[2]; + childBoundingSphere.Radius() = g_actorLODs[i + 1].m_boundingSphere[3]; + childROI->SetBoundingSphere(childBoundingSphere); + + BoundingBox childBoundingBox; + childBoundingBox.Min()[0] = g_actorLODs[i + 1].m_boundingBox[0]; + childBoundingBox.Min()[1] = g_actorLODs[i + 1].m_boundingBox[1]; + childBoundingBox.Min()[2] = g_actorLODs[i + 1].m_boundingBox[2]; + childBoundingBox.Max()[0] = g_actorLODs[i + 1].m_boundingBox[3]; + childBoundingBox.Max()[1] = g_actorLODs[i + 1].m_boundingBox[4]; + childBoundingBox.Max()[2] = g_actorLODs[i + 1].m_boundingBox[5]; + childROI->SetBoundingBox(childBoundingBox); + + CalcLocalTransform( + Mx3DPointFloat(g_actorLODs[i + 1].m_position), + Mx3DPointFloat(g_actorLODs[i + 1].m_direction), + Mx3DPointFloat(g_actorLODs[i + 1].m_up), + mat + ); + childROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat); + + if (g_actorLODs[i + 1].m_flags & LegoActorLOD::c_useTexture && + (i != 0 || part.m_partNameIndices[part.m_partNameIndex] != 0)) { + + LegoTextureInfo* textureInfo = textureContainer->Get(part.m_names[part.m_nameIndices[part.m_nameIndex]]); + + if (textureInfo != NULL) { + childROI->SetTextureInfo(textureInfo); + childROI->SetLodColor(1.0F, 1.0F, 1.0F, 0.0F); + } + } + else if (g_actorLODs[i + 1].m_flags & LegoActorLOD::c_useColor || (i == 0 && part.m_partNameIndices[part.m_partNameIndex] == 0)) { + LegoFloat red, green, blue, alpha; + childROI->GetRGBAColor(part.m_names[part.m_nameIndices[part.m_nameIndex]], red, green, blue, alpha); + childROI->SetLodColor(red, green, blue, alpha); + } + + comp->push_back(childROI); + } + + CalcLocalTransform( + Mx3DPointFloat(g_actorLODs[c_topLOD].m_position), + Mx3DPointFloat(g_actorLODs[c_topLOD].m_direction), + Mx3DPointFloat(g_actorLODs[c_topLOD].m_up), + mat + ); + roi->WrappedSetLocal2WorldWithWorldDataUpdate(mat); + + { + LegoCharacter* character = new LegoCharacter(roi); + char* name = new char[SDL_strlen(p_uniqueName) + 1]; + SDL_strlcpy(name, p_uniqueName, SDL_strlen(p_uniqueName) + 1); + (*p_charMgr->m_characters)[name] = character; + } + + success = TRUE; + +done: + if (!success && roi != NULL) { + delete roi; + roi = NULL; + } + + return roi; +} diff --git a/extensions/src/common/charactercustomizer.cpp b/extensions/src/common/charactercustomizer.cpp new file mode 100644 index 000000000..680f3b793 --- /dev/null +++ b/extensions/src/common/charactercustomizer.cpp @@ -0,0 +1,370 @@ +#include "extensions/common/charactercustomizer.h" + +#include "3dmanager/lego3dmanager.h" +#include "3dmanager/lego3dview.h" +#include "extensions/common/charactercloner.h" +#include "extensions/common/constants.h" +#include "extensions/common/customizestate.h" +#include "legoactor.h" +#include "legoactors.h" +#include "legocharactermanager.h" +#include "legogamestate.h" +#include "legovideomanager.h" +#include "misc.h" +#include "mxatom.h" +#include "mxdsaction.h" +#include "mxmisc.h" +#include "roi/legolod.h" +#include "roi/legoroi.h" +#include "viewmanager/viewlodlist.h" +#include "viewmanager/viewmanager.h" + +#include + +using namespace Extensions::Common; + +static const MxU32 g_characterSoundIdOffset = 50; +static const MxU32 g_characterSoundIdMoodOffset = 66; +static const MxU32 g_characterAnimationId = 10; +static const MxU32 g_maxSound = 9; +static const MxU32 g_maxMove = 4; + +static constexpr int COLORABLE_PARTS_COUNT = 10; +static uint32_t s_variantCounter = 10000; + +// MARK: Private helpers + +LegoROI* CharacterCustomizer::FindChildROI(LegoROI* p_rootROI, const char* p_name) +{ + const CompoundObject* comp = p_rootROI->GetComp(); + + for (CompoundObject::const_iterator it = comp->begin(); it != comp->end(); it++) { + LegoROI* roi = (LegoROI*) *it; + + if (!SDL_strcasecmp(p_name, roi->GetName())) { + return roi; + } + } + + return NULL; +} + +// MARK: Public API + +uint8_t CharacterCustomizer::ResolveActorInfoIndex(uint8_t p_displayActorIndex) +{ + return p_displayActorIndex; +} + +bool CharacterCustomizer::SwitchColor( + LegoROI* p_rootROI, + uint8_t p_actorInfoIndex, + CustomizeState& p_state, + int p_partIndex +) +{ + if (p_partIndex < 0 || p_partIndex >= COLORABLE_PARTS_COUNT) { + return false; + } + + // Remap derived parts to independent parts + if (p_partIndex == c_clawlftPart) { + p_partIndex = c_armlftPart; + } + else if (p_partIndex == c_clawrtPart) { + p_partIndex = c_armrtPart; + } + else if (p_partIndex == c_headPart) { + p_partIndex = c_infohatPart; + } + else if (p_partIndex == c_bodyPart) { + p_partIndex = c_infogronPart; + } + + if (!(g_actorLODs[p_partIndex + 1].m_flags & LegoActorLOD::c_useColor)) { + return false; + } + + if (p_actorInfoIndex >= sizeOfArray(g_actorInfoInit)) { + return false; + } + + const LegoActorInfo::Part& part = g_actorInfoInit[p_actorInfoIndex].m_parts[p_partIndex]; + + p_state.colorIndices[p_partIndex]++; + if (part.m_nameIndices[p_state.colorIndices[p_partIndex]] == 0xff) { + p_state.colorIndices[p_partIndex] = 0; + } + + if (!p_rootROI) { + return true; + } + + LegoROI* targetROI = FindChildROI(p_rootROI, g_actorLODs[p_partIndex + 1].m_name); + if (!targetROI) { + return false; + } + + LegoFloat red, green, blue, alpha; + LegoROI::GetRGBAColor(part.m_names[part.m_nameIndices[p_state.colorIndices[p_partIndex]]], red, green, blue, alpha); + targetROI->SetLodColor(red, green, blue, alpha); + return true; +} + +bool CharacterCustomizer::SwitchVariant(LegoROI* p_rootROI, uint8_t p_actorInfoIndex, CustomizeState& p_state) +{ + if (p_actorInfoIndex >= sizeOfArray(g_actorInfoInit)) { + return false; + } + + const LegoActorInfo::Part& part = g_actorInfoInit[p_actorInfoIndex].m_parts[c_infohatPart]; + + p_state.hatVariantIndex++; + if (part.m_partNameIndices[p_state.hatVariantIndex] == 0xff) { + p_state.hatVariantIndex = 0; + } + + if (!p_rootROI) { + return true; + } + + ApplyHatVariant(p_rootROI, p_actorInfoIndex, p_state); + return true; +} + +bool CharacterCustomizer::SwitchSound(CustomizeState& p_state) +{ + p_state.sound++; + if (p_state.sound >= g_maxSound) { + p_state.sound = 0; + } + return true; +} + +bool CharacterCustomizer::SwitchMove(CustomizeState& p_state) +{ + p_state.move++; + if (p_state.move >= g_maxMove) { + p_state.move = 0; + } + return true; +} + +bool CharacterCustomizer::SwitchMood(CustomizeState& p_state) +{ + p_state.mood++; + if (p_state.mood > 3) { + p_state.mood = 0; + } + return true; +} + +void CharacterCustomizer::ApplyChange( + LegoROI* p_rootROI, + uint8_t p_actorInfoIndex, + CustomizeState& p_state, + uint8_t p_changeType, + uint8_t p_partIndex +) +{ + switch (p_changeType) { + case CHANGE_VARIANT: + SwitchVariant(p_rootROI, p_actorInfoIndex, p_state); + break; + case CHANGE_SOUND: + SwitchSound(p_state); + break; + case CHANGE_MOVE: + SwitchMove(p_state); + break; + case CHANGE_COLOR: + SwitchColor(p_rootROI, p_actorInfoIndex, p_state, p_partIndex); + break; + case CHANGE_MOOD: + SwitchMood(p_state); + break; + } +} + +int CharacterCustomizer::MapClickedPartIndex(const char* p_partName) +{ + for (int i = 0; i < COLORABLE_PARTS_COUNT; i++) { + if (!SDL_strcasecmp(p_partName, g_actorLODs[i + 1].m_name)) { + return i; + } + } + return -1; +} + +void CharacterCustomizer::ApplyFullState(LegoROI* p_rootROI, uint8_t p_actorInfoIndex, const CustomizeState& p_state) +{ + if (p_actorInfoIndex >= sizeOfArray(g_actorInfoInit)) { + return; + } + + // Apply colors for the 6 independent colorable parts + static const int colorableParts[] = + {c_infohatPart, c_infogronPart, c_armlftPart, c_armrtPart, c_leglftPart, c_legrtPart}; + + for (int i = 0; i < (int) sizeOfArray(colorableParts); i++) { + int partIndex = colorableParts[i]; + + if (!(g_actorLODs[partIndex + 1].m_flags & LegoActorLOD::c_useColor)) { + continue; + } + + LegoROI* childROI = FindChildROI(p_rootROI, g_actorLODs[partIndex + 1].m_name); + if (!childROI) { + continue; + } + + const LegoActorInfo::Part& part = g_actorInfoInit[p_actorInfoIndex].m_parts[partIndex]; + + LegoFloat red, green, blue, alpha; + LegoROI::GetRGBAColor( + part.m_names[part.m_nameIndices[p_state.colorIndices[partIndex]]], + red, + green, + blue, + alpha + ); + childROI->SetLodColor(red, green, blue, alpha); + } + + // Apply hat variant if different from default + const LegoActorInfo::Part& hatPart = g_actorInfoInit[p_actorInfoIndex].m_parts[c_infohatPart]; + if (p_state.hatVariantIndex != hatPart.m_partNameIndex) { + ApplyHatVariant(p_rootROI, p_actorInfoIndex, p_state); + } +} + +void CharacterCustomizer::ApplyHatVariant(LegoROI* p_rootROI, uint8_t p_actorInfoIndex, const CustomizeState& p_state) +{ + if (p_actorInfoIndex >= sizeOfArray(g_actorInfoInit)) { + return; + } + + const LegoActorInfo::Part& part = g_actorInfoInit[p_actorInfoIndex].m_parts[c_infohatPart]; + + MxU8 partNameIndex = part.m_partNameIndices[p_state.hatVariantIndex]; + if (partNameIndex == 0xff) { + return; + } + + LegoROI* childROI = FindChildROI(p_rootROI, g_actorLODs[c_infohatLOD].m_name); + + if (childROI != NULL) { + char lodName[256]; + + ViewLODList* lodList = GetViewLODListManager()->Lookup(part.m_partName[partNameIndex]); + MxS32 lodSize = lodList->Size(); + SDL_snprintf(lodName, sizeof(lodName), "%s_cv%u", p_rootROI->GetName(), s_variantCounter++); + ViewLODList* dupLodList = GetViewLODListManager()->Create(lodName, lodSize); + + Tgl::Renderer* renderer = VideoManager()->GetRenderer(); + LegoFloat red, green, blue, alpha; + LegoROI::GetRGBAColor( + part.m_names[part.m_nameIndices[p_state.colorIndices[c_infohatPart]]], + red, + green, + blue, + alpha + ); + + for (MxS32 i = 0; i < lodSize; i++) { + LegoLOD* lod = (LegoLOD*) (*lodList)[i]; + LegoLOD* clone = lod->Clone(renderer); + clone->SetColor(red, green, blue, alpha); + dupLodList->PushBack(clone); + } + + lodList->Release(); + lodList = dupLodList; + + if (childROI->GetLodLevel() >= 0) { + VideoManager()->Get3DManager()->GetLego3DView()->GetViewManager()->RemoveROIDetailFromScene(childROI); + } + + childROI->SetLODList(lodList); + lodList->Release(); + } +} + +void CharacterCustomizer::PlayClickSound(LegoROI* p_roi, const CustomizeState& p_state, bool p_basedOnMood) +{ + MxU32 objectId = + p_basedOnMood ? (p_state.mood + g_characterSoundIdMoodOffset) : (p_state.sound + g_characterSoundIdOffset); + + if (objectId) { + MxDSAction action; + action.SetAtomId(MxAtomId(LegoCharacterManager::GetCustomizeAnimFile(), e_lowerCase2)); + action.SetObjectId(objectId); + + const char* name = p_roi->GetName(); + action.AppendExtra(SDL_strlen(name) + 1, name); + Start(&action); + } +} + +MxU32 CharacterCustomizer::PlayClickAnimation(LegoROI* p_roi, const CustomizeState& p_state) +{ + MxU32 objectId = p_state.move + g_characterAnimationId; + + MxDSAction action; + action.SetAtomId(MxAtomId(LegoCharacterManager::GetCustomizeAnimFile(), e_lowerCase2)); + action.SetObjectId(objectId); + + char extra[1024]; + SDL_snprintf(extra, sizeof(extra), "SUBST:actor_01:%s", p_roi->GetName()); + action.AppendExtra(SDL_strlen(extra) + 1, extra); + StartActionIfInitialized(action); + + return objectId; +} + +void CharacterCustomizer::StopClickAnimation(MxU32 p_objectId) +{ + MxDSAction action; + action.SetAtomId(MxAtomId(LegoCharacterManager::GetCustomizeAnimFile(), e_lowerCase2)); + 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/common/charactertables.cpp b/extensions/src/common/charactertables.cpp new file mode 100644 index 000000000..ed94c3b56 --- /dev/null +++ b/extensions/src/common/charactertables.cpp @@ -0,0 +1,75 @@ +#include "extensions/common/charactertables.h" + +#include "legopathactor.h" + +namespace Extensions +{ +namespace Common +{ + +const char* const g_walkAnimNames[] = { + "CNs001xx", // 0: Normal (default) + "CNs002xx", // 1: Joyful + "CNs003xx", // 2: Gloomy + "CNs005xx", // 3: Leaning + "CNs006xx", // 4: Scared + "CNs007xx", // 5: Hyper +}; +const int g_walkAnimCount = sizeof(g_walkAnimNames) / sizeof(g_walkAnimNames[0]); + +const char* const g_idleAnimNames[] = { + "CNs008xx", // 0: Sway (default) + "CNs009xx", // 1: Groove + "CNs010xx", // 2: Excited + "CNs008Pa", // 3: Wobbly + "CNs009Pa", // 4: Peppy + "CNs012Br", // 5: Brickster +}; +const int g_idleAnimCount = sizeof(g_idleAnimNames) / sizeof(g_idleAnimNames[0]); + +// Vehicle model names (LOD names). The helicopter is a compound ROI ("copter") +// with no standalone LOD; use its body part instead. +const char* const g_vehicleROINames[VEHICLE_COUNT] = + {"chtrbody", "jsuser", "dunebugy", "bike", "board", "moto", "towtk", "ambul"}; + +// Ride animation names for small vehicles (NULL = large vehicle, no ride anim) +const char* const g_rideAnimNames[VEHICLE_COUNT] = {NULL, NULL, NULL, "CNs001Bd", "CNs001sk", "CNs011Ni", NULL, NULL}; + +// Vehicle variant ROI names used in ride animations +const char* const g_rideVehicleROINames[VEHICLE_COUNT] = {NULL, NULL, NULL, "bikebd", "board", "motoni", NULL, NULL}; + +bool IsLargeVehicle(int8_t p_vehicleType) +{ + return p_vehicleType != VEHICLE_NONE && p_vehicleType < VEHICLE_COUNT && g_rideAnimNames[p_vehicleType] == NULL; +} + +int8_t DetectVehicleType(LegoPathActor* p_actor) +{ + static const struct { + const char* className; + int8_t vehicleType; + } vehicleMap[] = { + {"Helicopter", VEHICLE_HELICOPTER}, + {"Jetski", VEHICLE_JETSKI}, + {"DuneBuggy", VEHICLE_DUNEBUGGY}, + {"Bike", VEHICLE_BIKE}, + {"SkateBoard", VEHICLE_SKATEBOARD}, + {"Motorcycle", VEHICLE_MOTOCYCLE}, + {"TowTrack", VEHICLE_TOWTRACK}, + {"Ambulance", VEHICLE_AMBULANCE}, + }; + + if (!p_actor) { + return VEHICLE_NONE; + } + + for (const auto& entry : vehicleMap) { + if (p_actor->IsA(entry.className)) { + return entry.vehicleType; + } + } + return VEHICLE_NONE; +} + +} // namespace Common +} // namespace Extensions diff --git a/extensions/src/common/customizestate.cpp b/extensions/src/common/customizestate.cpp new file mode 100644 index 000000000..3020fccd2 --- /dev/null +++ b/extensions/src/common/customizestate.cpp @@ -0,0 +1,38 @@ +#include "extensions/common/customizestate.h" + +#include "legoactors.h" +#include "misc.h" + +using namespace Extensions::Common; + +void CustomizeState::InitFromActorInfo(uint8_t p_actorInfoIndex) +{ + if (p_actorInfoIndex >= sizeOfArray(g_actorInfoInit)) { + return; + } + + const LegoActorInfo& info = g_actorInfoInit[p_actorInfoIndex]; + + // Set the 6 independent colorable parts from actor info + colorIndices[c_infohatPart] = info.m_parts[c_infohatPart].m_nameIndex; + colorIndices[c_infogronPart] = info.m_parts[c_infogronPart].m_nameIndex; + colorIndices[c_armlftPart] = info.m_parts[c_armlftPart].m_nameIndex; + colorIndices[c_armrtPart] = info.m_parts[c_armrtPart].m_nameIndex; + colorIndices[c_leglftPart] = info.m_parts[c_leglftPart].m_nameIndex; + colorIndices[c_legrtPart] = info.m_parts[c_legrtPart].m_nameIndex; + + DeriveDependentIndices(); + + hatVariantIndex = info.m_parts[c_infohatPart].m_partNameIndex; + sound = (uint8_t) info.m_sound; + move = (uint8_t) info.m_move; + mood = info.m_mood; +} + +void CustomizeState::DeriveDependentIndices() +{ + colorIndices[c_bodyPart] = colorIndices[c_infogronPart]; + colorIndices[c_headPart] = colorIndices[c_infohatPart]; + colorIndices[c_clawlftPart] = colorIndices[c_armlftPart]; + colorIndices[c_clawrtPart] = colorIndices[c_armrtPart]; +} diff --git a/extensions/src/common/pathutils.cpp b/extensions/src/common/pathutils.cpp new file mode 100644 index 000000000..de9237136 --- /dev/null +++ b/extensions/src/common/pathutils.cpp @@ -0,0 +1,24 @@ +#include "extensions/common/pathutils.h" + +#include "legomain.h" + +#include + +using namespace Extensions::Common; + +bool Extensions::Common::ResolveGamePath(const char* p_relativePath, MxString& p_outPath) +{ + p_outPath = MxString(MxOmni::GetHD()) + p_relativePath; + p_outPath.MapPathToFilesystem(); + if (SDL_GetPathInfo(p_outPath.GetData(), NULL)) { + return true; + } + + p_outPath = MxString(MxOmni::GetCD()) + p_relativePath; + p_outPath.MapPathToFilesystem(); + if (SDL_GetPathInfo(p_outPath.GetData(), NULL)) { + return true; + } + + return false; +} diff --git a/extensions/src/extensions.cpp b/extensions/src/extensions.cpp index 2eb276c69..ceff4b941 100644 --- a/extensions/src/extensions.cpp +++ b/extensions/src/extensions.cpp @@ -2,26 +2,44 @@ #include "extensions/siloader.h" #include "extensions/textureloader.h" +#include "extensions/thirdpersoncamera.h" #include -void Extensions::Enable(const char* p_key, std::map p_options) +using namespace Extensions; + +static void InitTextureLoader(std::map p_options) { - 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(); - } - else if (!SDL_strcasecmp(p_key, "extensions:si loader")) { - SiLoader::options = std::move(p_options); - SiLoader::enabled = true; - SiLoader::Initialize(); - } + TextureLoaderExt::options = std::move(p_options); + TextureLoaderExt::enabled = true; + TextureLoaderExt::Initialize(); +} +static void InitSiLoader(std::map p_options) +{ + SiLoaderExt::options = std::move(p_options); + SiLoaderExt::enabled = true; + SiLoaderExt::Initialize(); +} + +static void InitThirdPersonCamera(std::map p_options) +{ + ThirdPersonCameraExt::options = std::move(p_options); + ThirdPersonCameraExt::enabled = true; + ThirdPersonCameraExt::Initialize(); +} + +using InitFn = void (*)(std::map); + +static const InitFn extensionInits[] = {InitTextureLoader, InitSiLoader, InitThirdPersonCamera}; + +void Extensions::Enable(const char* p_key, std::map p_options) +{ + for (int i = 0; i < (int) (sizeof(availableExtensions) / sizeof(availableExtensions[0])); i++) { + if (!SDL_strcasecmp(p_key, availableExtensions[i])) { + extensionInits[i](std::move(p_options)); SDL_Log("Enabled extension: %s", p_key); - break; + return; } } } diff --git a/extensions/src/siloader.cpp b/extensions/src/siloader.cpp index 0768bf1a2..937ecdaf3 100644 --- a/extensions/src/siloader.cpp +++ b/extensions/src/siloader.cpp @@ -1,5 +1,6 @@ #include "extensions/siloader.h" +#include "extensions/common/pathutils.h" #include "legovideomanager.h" #include "misc.h" #include "mxdsaction.h" @@ -13,38 +14,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; - -void SiLoader::Initialize() +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 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 +58,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 +69,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 +131,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 +155,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 +172,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 +203,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,21 +236,16 @@ 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; - MxString path = MxString(MxOmni::GetHD()) + p_file; - path.MapPathToFilesystem(); - 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) { - SDL_Log("Could not parse SI file %s", p_file); - return false; - } + MxString path; + if (!Common::ResolveGamePath(p_file, path) || + si.Read(path.GetData(), si::Interleaf::ObjectsOnly) != si::Interleaf::ERROR_SUCCESS) { + SDL_Log("Could not parse SI file %s", p_file); + return false; } if (!(controller = OpenStream(p_file))) { @@ -260,7 +256,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 +302,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 +314,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 +374,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 d46485e1e..41550a1bf 100644 --- a/extensions/src/textureloader.cpp +++ b/extensions/src/textureloader.cpp @@ -1,4 +1,6 @@ #include "extensions/textureloader.h" + +#include "extensions/common/pathutils.h" #include "legovideomanager.h" #include "misc.h" #include "mxdirectx/mxdirect3d.h" @@ -7,11 +9,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 +22,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,22 +110,19 @@ 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; } - SDL_Surface* surface; const char* texturePath = options["texture loader:texture path"].c_str(); - MxString path = MxString(MxOmni::GetHD()) + texturePath + "/" + p_name + ".bmp"; + MxString relativePath = MxString(texturePath) + "/" + p_name + ".bmp"; - path.MapPathToFilesystem(); - if (!(surface = SDL_LoadBMP(path.GetData()))) { - path = MxString(MxOmni::GetCD()) + texturePath + "/" + p_name + ".bmp"; - path.MapPathToFilesystem(); - surface = SDL_LoadBMP(path.GetData()); + MxString path; + if (!Common::ResolveGamePath(relativePath.GetData(), path)) { + return nullptr; } - return surface; + return SDL_LoadBMP(path.GetData()); } diff --git a/extensions/src/thirdpersoncamera.cpp b/extensions/src/thirdpersoncamera.cpp new file mode 100644 index 000000000..40e8c2cb0 --- /dev/null +++ b/extensions/src/thirdpersoncamera.cpp @@ -0,0 +1,276 @@ +#include "extensions/thirdpersoncamera.h" + +#include "extensions/common/arearestriction.h" +#include "extensions/common/charactercustomizer.h" +#include "extensions/common/constants.h" +#include "extensions/thirdpersoncamera/controller.h" +#include "islepathactor.h" +#include "legoeventnotificationparam.h" +#include "legoinputmanager.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(FIXED_TICK_DELTA); + } + 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 || IsRestrictedArea(GameState()->m_currentArea)) { + return; + } + + s_camera->HandleSDLEventImpl(p_event); + + if (p_event->type == SDL_EVENT_MOUSE_BUTTON_UP && p_event->button.button == SDL_BUTTON_LEFT) { + s_camera->SetLmbForwardEngaged(false); + } + + if (s_camera->ConsumeAutoDisable() && !s_camera->IsAnimPlaying()) { + s_camera->Disable(/*p_preserveTouch=*/true); + if (s_camera->IsLeftButtonHeld()) { + s_camera->SetLmbForwardEngaged(true); + } + } + else if (s_camera->ConsumeAutoEnable()) { + // Clear the movement system's touch state for camera-owned fingers only, + // so any virtual thumbstick input from 1st-person mode is zeroed while + // leaving a left-side movement finger intact. + LegoInputManager* im = InputManager(); + if (im) { + for (int i = 0; i < s_camera->GetTouchCount(); i++) { + SDL_FingerID fid = s_camera->GetFingerID(i); + if (im->m_touchFinger == fid) { + im->m_touchFinger = 0; + im->m_touchVirtualThumb = {0, 0}; + im->m_touchVirtualThumbOrigin = {0, 0}; + } + im->m_touchFlags.erase(fid); + } + } + + // Suppress camera gestures until finger positions re-sync to avoid + // a camera jump from stale positions carried through the transition. + s_camera->SuppressGestures(); + + s_camera->SetOrbitDistance(ThirdPersonCamera::Controller::MIN_DISTANCE); + s_camera->Enable(); + if (s_camera->IsLeftButtonHeld()) { + s_camera->SetLmbForwardEngaged(true); + } + } +} + +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()) { + if (s_camera->IsLmbForwardEngaged()) { + return s_camera->HandleFirstPersonForward(p_nav, p_curPos, p_curDir, p_newPos, p_newDir, p_deltaTime); + } + 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->IsExtraAnimBlocking()) { + 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 000000000..976bd1062 --- /dev/null +++ b/extensions/src/thirdpersoncamera/controller.cpp @@ -0,0 +1,526 @@ +#include "extensions/thirdpersoncamera/controller.h" + +#include "3dmanager/lego3dmanager.h" +#include "anim/legoanim.h" +#include "extensions/common/animutils.h" +#include "extensions/common/arearestriction.h" +#include "extensions/common/charactercustomizer.h" +#include "extensions/common/charactertables.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; + +static constexpr float SPEED_EPSILON = 0.01f; + +static void ReaddROI(LegoROI& p_roi) +{ + VideoManager()->Get3DManager()->Remove(p_roi); + VideoManager()->Get3DManager()->Add(p_roi); +} + +Controller::Controller() + : m_animator(CharacterAnimatorConfig{/*.saveExtraAnimTransform=*/true, /*.propSuffix=*/0}), m_enabled(false), + m_active(false), m_pendingWorldTransition(false), m_animPlaying(false), m_animLockDisplay(false), + m_lmbForwardEngaged(false), m_playerROI(nullptr) +{ +} + +void Controller::Enable() +{ + m_enabled = true; + ReinitForCharacter(); +} + +void Controller::Disable(bool p_preserveTouch) +{ + m_enabled = false; + Deactivate(); + if (!p_preserveTouch) { + m_input.ResetTouchState(); + } +} + +void Controller::CancelExternalAnim() +{ + if (m_animPlaying) { + if (m_animStopCallback) { + m_animStopCallback(); + } + m_animPlaying = false; + m_animStopCallback = nullptr; + } +} + +void Controller::Deactivate() +{ + // Stop external animation before destroying the display ROI + CancelExternalAnim(); + + if (m_active && m_playerROI) { + m_playerROI->SetVisibility(FALSE); + VideoManager()->Get3DManager()->Remove(*m_playerROI); + m_orbit.RestoreFirstPersonCamera(); + } + + m_active = false; + m_pendingWorldTransition = false; + m_lmbForwardEngaged = false; + m_animator.StopROISounds(); + m_animator.StopClickAnimation(); + m_display.DestroyDisplayClone(); + m_playerROI = nullptr; + m_animator.ClearRideAnimation(); + m_animator.ClearAll(); + m_orbit.ResetOrbitState(); +} + +void Controller::OnActorEnter(IslePathActor* p_actor) +{ + LegoPathActor* userActor = UserActor(); + if (static_cast(p_actor) != userActor) { + return; + } + + // Prevent the previous actor from wandering on the path system with stale + // spline state while the player is in a vehicle. Exit() will later call + // SetBoundary() without updating m_destEdge, so any non-user-nav animation + // with the old spline would use a mismatched boundary/edge pair. + if (p_actor->m_previousActor) { + p_actor->m_previousActor->SetWorldSpeed(0); + } + + m_animator.SetCurrentVehicleType(DetectVehicleType(userActor)); + + if (!m_enabled || IsRestrictedArea(GameState()->m_currentArea)) { + return; + } + + if (m_pendingWorldTransition && m_active) { + return; + } + + // Stop external animation before modifying ride/display state — + // the ScenePlayer may hold a reference to the ride vehicle ROI. + CancelExternalAnim(); + + 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); + return; + } + + newROI->SetVisibility(FALSE); + if (!m_display.EnsureDisplayROI()) { + return; + } + m_playerROI = m_display.GetDisplayROI(); + m_active = true; + + m_playerROI->SetVisibility(TRUE); + + ReaddROI(*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; + } + + // Stop external animation before clearing ride animation state — + // the ScenePlayer may hold a reference to the ride vehicle ROI. + CancelExternalAnim(); + + 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 (IsRestrictedArea(GameState()->m_currentArea)) { + return; + } + + 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 (!m_animPlaying && (!UserActor() || UserActor()->GetActorState() != LegoPathActor::c_disabled)) { + m_orbit.ApplyOrbitCamera(); + } + + // Small vehicle with ride animation (skip when external animation is active — + // the animation controller handles positioning the player and vehicle ROI) + if (m_animator.GetCurrentVehicleType() != VEHICLE_NONE && !m_animPlaying) { + 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) > SPEED_EPSILON) { + m_animator.SetAnimTime(m_animator.GetAnimTime() + p_deltaTime * CharacterAnimator::ANIM_TIME_SCALE); + } + + 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); + + AnimUtils::ApplyTree( + m_animator.GetRideAnim(), + transform, + (LegoTime) timeInCycle, + m_animator.GetRideRoiMap() + ); + } + } + return; + } + + LegoPathActor* userActor = UserActor(); + if (!userActor) { + return; + } + + // When an external animation is playing, prevent movement. + // If the display ROI is being driven by the animation (performer), skip everything. + // If spectating, still sync + idle animate. + if (m_animPlaying) { + userActor->SetWorldSpeed(0.0f); + NavController()->SetLinearVel(0.0f); + if (m_animLockDisplay) { + 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) > SPEED_EPSILON; + if (m_animator.IsExtraAnimBlocking()) { + 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::IsExtraAnimBlocking() const +{ + return m_animator.IsExtraAnimBlocking(); +} + +int8_t Controller::GetFrozenExtraAnimId() const +{ + return m_animator.GetFrozenExtraAnimId(); +} + +void Controller::TriggerExtraAnim(uint8_t p_id) +{ + if (!m_active) { + return; + } + + LegoPathActor* userActor = UserActor(); + if (!userActor) { + return; + } + + bool isMoving = SDL_fabsf(userActor->GetWorldSpeed()) > SPEED_EPSILON; + if (m_animator.IsExtraAnimBlocking()) { + isMoving = false; + } + m_animator.TriggerExtraAnim(p_id, m_playerROI, isMoving); +} + +void Controller::StopClickAnimation() +{ + m_animator.StopClickAnimation(); +} + +void Controller::OnWorldEnabled(LegoWorld* p_world) +{ + if (!p_world) { + return; + } + + if (!m_enabled) { + return; + } + + if (IsRestrictedArea(GameState()->m_currentArea)) { + Deactivate(); + m_input.ResetTouchState(); + return; + } + + m_animator.ClearAll(); + + m_orbit.ResetOrbitState(); + + m_pendingWorldTransition = true; + + ReinitForCharacter(); +} + +void Controller::OnWorldDisabled(LegoWorld* p_world) +{ + if (!p_world) { + return; + } + + // Stop external animation before destroying the display ROI + CancelExternalAnim(); + + 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.IsExtraAnimBlocking() || m_animPlaying, + m_input.IsLmbHeldForMovement() + ); +} + +void Controller::HandleSDLEventImpl(SDL_Event* p_event) +{ + m_input.HandleSDLEvent(p_event, m_orbit, m_active); +} + +MxBool Controller::HandleFirstPersonForward( + LegoNavController* p_nav, + const Vector3& p_curPos, + const Vector3& p_curDir, + Vector3& p_newPos, + Vector3& p_newDir, + float p_deltaTime +) +{ + float accel = p_nav->m_maxLinearAccel; + p_nav->m_linearVel += accel * p_deltaTime; + if (p_nav->m_linearVel > p_nav->m_maxLinearVel) { + p_nav->m_linearVel = p_nav->m_maxLinearVel; + } + + float speed = p_nav->m_linearVel * p_deltaTime; + 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_rotationalVel = 0.0f; + return TRUE; +} + +void Controller::ReinitForCharacter() +{ + if (!GameState() || IsRestrictedArea(GameState()->m_currentArea)) { + m_active = false; + return; + } + + 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; + + ReaddROI(*m_playerROI); + m_active = true; + m_orbit.SetupCamera(userActor); + m_animator.BuildRideAnimation(vehicleType, m_playerROI); + return; + } + + roi->SetVisibility(FALSE); + if (!m_display.EnsureDisplayROI()) { + m_active = false; + return; + } + m_playerROI = m_display.GetDisplayROI(); + + m_playerROI->SetVisibility(TRUE); + + ReaddROI(*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 000000000..63c2449fc --- /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 000000000..4b0e40a33 --- /dev/null +++ b/extensions/src/thirdpersoncamera/inputhandler.cpp @@ -0,0 +1,257 @@ +#include "extensions/thirdpersoncamera/inputhandler.h" + +#include "extensions/thirdpersoncamera/orbitcamera.h" + +#include +#include +#include + +using namespace Extensions::ThirdPersonCamera; + +InputHandler::InputHandler() + : m_touch{}, m_wantsAutoDisable(false), m_wantsAutoEnable(false), m_rightButtonHeld(false), m_leftButtonHeld(false), + m_leftButtonDownTime(0), m_savedMouseX(0.0f), m_savedMouseY(0.0f) +{ +} + +bool InputHandler::TryClaimFinger(const SDL_TouchFingerEvent& p_event) +{ + if (m_touch.count >= 2 || p_event.x < CAMERA_ZONE_X || IsFingerTracked(p_event.fingerID)) { + 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.synced[idx] = true; + 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); + m_touch.gesturePinchDist = m_touch.initialPinchDist; + } + + 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.synced[0] = m_touch.synced[1]; + } + m_touch.count--; + m_touch.initialPinchDist = 0.0f; + m_touch.gesturePinchDist = 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); +} + +bool InputHandler::IsLmbHeldForMovement() const +{ + return m_leftButtonHeld && m_leftButtonDownTime > 0 && + (m_rightButtonHeld || (SDL_GetTicks() - m_leftButtonDownTime) >= LMB_HOLD_THRESHOLD_MS); +} + +void InputHandler::SuppressGestures() +{ + m_touch.synced[0] = false; + m_touch.synced[1] = false; + m_touch.initialPinchDist = 0.0f; + m_touch.gesturePinchDist = 0.0f; +} + +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::SWITCH_TO_FIRST_PERSON_DISTANCE && p_event->wheel.y > 0) { + m_wantsAutoDisable = true; + break; + } + p_orbit.AdjustDistance(-p_event->wheel.y * MOUSE_WHEEL_ZOOM_STEP); + p_orbit.ClampDistance(); + break; + + case SDL_EVENT_MOUSE_MOTION: + if (!p_active) { + break; + } + if (m_rightButtonHeld) { + p_orbit.AdjustYaw(-p_event->motion.xrel * MOUSE_SENSITIVITY); + p_orbit.AdjustPitch(p_event->motion.yrel * MOUSE_SENSITIVITY); + p_orbit.ClampPitch(); + } + break; + + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: { + if (p_event->button.button == SDL_BUTTON_RIGHT) { + m_rightButtonHeld = p_event->button.down; + SDL_Window* window = SDL_GetWindowFromID(p_event->button.windowID); + if (window) { + if (m_rightButtonHeld) { + if (p_active) { + SDL_GetMouseState(&m_savedMouseX, &m_savedMouseY); + SDL_SetWindowRelativeMouseMode(window, true); + } + } + else if (SDL_GetWindowRelativeMouseMode(window)) { + SDL_SetWindowRelativeMouseMode(window, false); + SDL_WarpMouseInWindow(window, m_savedMouseX, m_savedMouseY); + } + } + } + else if (p_event->button.button == SDL_BUTTON_LEFT) { + m_leftButtonHeld = p_event->button.down; + m_leftButtonDownTime = p_event->button.down ? SDL_GetTicks() : 0; + } + break; + } + + case SDL_EVENT_FINGER_DOWN: + TryClaimFinger(p_event->tfinger); + break; + + case SDL_EVENT_FINGER_MOTION: { + if (m_touch.count == 1) { + if (!p_active) { + break; + } + if (m_touch.id[0] == p_event->tfinger.fingerID) { + if (!m_touch.synced[0]) { + m_touch.x[0] = p_event->tfinger.x; + m_touch.y[0] = p_event->tfinger.y; + m_touch.synced[0] = true; + break; + } + + 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 * TOUCH_YAW_PITCH_SCALE); + p_orbit.AdjustPitch(moveY * TOUCH_YAW_PITCH_SCALE); + 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; + } + + if (!m_touch.synced[idx]) { + m_touch.x[idx] = p_event->tfinger.x; + m_touch.y[idx] = p_event->tfinger.y; + m_touch.synced[idx] = true; + + if (m_touch.synced[0] && m_touch.synced[1]) { + 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); + m_touch.gesturePinchDist = m_touch.initialPinchDist; + } + 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) { + float totalDelta = m_touch.gesturePinchDist - newDist; + if (totalDelta > PINCH_TRANSITION_THRESHOLD) { + m_wantsAutoEnable = true; + m_touch.gesturePinchDist = newDist; + } + m_touch.initialPinchDist = newDist; + break; + } + + if (p_orbit.GetOrbitDistance() <= OrbitCamera::SWITCH_TO_FIRST_PERSON_DISTANCE) { + float totalDelta = newDist - m_touch.gesturePinchDist; + if (totalDelta > PINCH_TRANSITION_THRESHOLD) { + m_wantsAutoDisable = true; + m_touch.initialPinchDist = newDist; + m_touch.gesturePinchDist = newDist; + break; + } + } + + p_orbit.AdjustDistance(pinchDelta * PINCH_ZOOM_SCALE); + 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 * TOUCH_YAW_PITCH_SCALE); + p_orbit.AdjustPitch(moveY * TOUCH_YAW_PITCH_SCALE); + 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 000000000..b8758158c --- /dev/null +++ b/extensions/src/thirdpersoncamera/orbitcamera.cpp @@ -0,0 +1,285 @@ +#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; + +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 < SWITCH_TO_FIRST_PERSON_DISTANCE) { + m_orbitDistance = SWITCH_TO_FIRST_PERSON_DISTANCE; + } + if (m_orbitDistance > MAX_DISTANCE) { + m_orbitDistance = MAX_DISTANCE; + } +} + +void OrbitCamera::RestoreFirstPersonCamera() +{ + LegoPathActor* userActor = UserActor(); + LegoWorld* world = CurrentWorld(); + + if (userActor && world && world->GetCameraController()) { + static const Mx3DPointFloat eyeOffset(0.0f, 1.25f, 0.0f); + static const Mx3DPointFloat forward(0.0f, 0.0f, 1.0f); + static const Mx3DPointFloat up(0.0f, 1.0f, 0.0f); + world->GetCameraController()->SetWorldTransform(eyeOffset, forward, up); + 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_isBlocked, + bool p_lmbHeld +) +{ + 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 (p_lmbHeld) { + moveDirX += camForwardX; + moveDirZ += camForwardZ; + } + + if (keyFlags == 0 && !p_lmbHeld && inputManager) { + MxU32 joystickX, joystickY, povPosition; + if (inputManager->GetJoystickState(&joystickX, &joystickY, &povPosition) == SUCCESS) { + float jx = (joystickX - JOYSTICK_CENTER) / JOYSTICK_CENTER; + float jy = -(joystickY - JOYSTICK_CENTER) / JOYSTICK_CENTER; + + if (SDL_fabsf(jx) < JOYSTICK_DEAD_ZONE) { + jx = 0.0f; + } + if (SDL_fabsf(jy) < JOYSTICK_DEAD_ZONE) { + jy = 0.0f; + } + + moveDirX += camForwardX * jy + camRightX * jx; + moveDirZ += camForwardZ * jy + camRightZ * jx; + } + } + + float moveDirLen = SDL_sqrtf(moveDirX * moveDirX + moveDirZ * moveDirZ); + bool hasInput = moveDirLen > MOVEMENT_DIR_EPSILON; + + if (p_isBlocked) { + 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 = CHARACTER_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; +} diff --git a/tools/ncc/skip.yml b/tools/ncc/skip.yml index e18250e92..271a4271d 100644 --- a/tools/ncc/skip.yml +++ b/tools/ncc/skip.yml @@ -78,4 +78,5 @@ SDL_KeyboardID_v: "SDL-based name" SDL_MouseID_v: "SDL-based name" SDL_JoystickID_v: "SDL-based name" SDL_TouchID_v: "SDL-based name" -Load: "Not a variable but function name" \ No newline at end of file +Load: "Not a variable but function name" +HandleCreate: "Not a variable but function name" \ No newline at end of file