Add third person camera extension (#786)
Some checks failed
CI / clang-format (push) Has been cancelled
CI / ${{ matrix.name }} (false, --toolchain /usr/local/vitasdk/share/vita.toolchain.cmake, false, false, Ninja, Vita, ubuntu-latest, true, true) (push) Has been cancelled
CI / ${{ matrix.name }} (false, -DCMAKE_SYSTEM_NAME=WindowsStore -DCMAKE_SYSTEM_VERSION=10.0.26100.0, false, false, Visual Studio 17 2022, true, Xbox One, windows-latest, amd64, false, true) (push) Has been cancelled
CI / ${{ matrix.name }} (false, -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/3DS.cmake, false, devkitpro/devkitarm:latest, false, Ninja, true, Nintendo 3DS, ubuntu-latest, true) (push) Has been cancelled
CI / ${{ matrix.name }} (false, -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Switch.cmake, false, devkitpro/devkita64:latest, false, Ninja, Nintendo Switch, true, ubuntu-latest, true) (push) Has been cancelled
CI / ${{ matrix.name }} (false, emcmake, false, false, true, Ninja, Emscripten, ubuntu-latest, true) (push) Has been cancelled
CI / ${{ matrix.name }} (false, false, false, Ninja, true, MSVC (arm64), windows-latest, amd64_arm64, false) (push) Has been cancelled
CI / ${{ matrix.name }} (false, false, true, Ninja, true, MSVC (x86), windows-latest, amd64_x86, false) (push) Has been cancelled
CI / ${{ matrix.name }} (false, true, false, Ninja, true, MSVC (x64), windows-latest, amd64, false) (push) Has been cancelled
CI / ${{ matrix.name }} (false, true, true, false, Ninja, true, MSVC (x64 Debug), windows-latest, amd64, false) (push) Has been cancelled
CI / ${{ matrix.name }} (true, false, -DCMAKE_SYSTEM_NAME=iOS, false, false, Xcode, true, iOS, macos-15, true) (push) Has been cancelled
CI / ${{ matrix.name }} (true, false, false, false, Ninja, Android, ubuntu-latest, true) (push) Has been cancelled
CI / ${{ matrix.name }} (true, false, true, false, Ninja, macOS, macos-latest, true) (push) Has been cancelled
CI / ${{ matrix.name }} (true, true, false, Ninja, true, mingw-w64-x86_64, mingw64, msys2 mingw64, windows-latest, msys2 {0}, true) (push) Has been cancelled
CI / ${{ matrix.name }} (true, true, true, false, Ninja, true, Linux (Debug), ubuntu-latest, true) (push) Has been cancelled
CI / ${{ matrix.name }} (true, true, true, false, Ninja, true, Linux, ubuntu-latest, true) (push) Has been cancelled
CI / Flatpak (${{ matrix.arch }}) (aarch64, ubuntu-22.04-arm) (push) Has been cancelled
CI / Flatpak (${{ matrix.arch }}) (x86_64, ubuntu-latest) (push) Has been cancelled
CI / C++ (push) Has been cancelled
Docker / Publish web port (push) Has been cancelled
CI / Release (push) Has been cancelled

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.
This commit is contained in:
foxtacles 2026-03-30 16:00:07 -07:00 committed by GitHub
parent 82a7ead84a
commit 7e4a86fb39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 4082 additions and 131 deletions

3
.gitignore vendored
View File

@ -14,8 +14,7 @@ VENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
local.properties local.properties
/build/ /build*/
/build_debug/
/legobin/ /legobin/
*.swp *.swp
LEGO1PROGRESS.* LEGO1PROGRESS.*

View File

@ -55,8 +55,8 @@ if(DOWNLOAD_DEPENDENCIES)
include(FetchContent) include(FetchContent)
FetchContent_Populate( FetchContent_Populate(
libweaver libweaver
URL https://github.com/isledecomp/SIEdit/archive/afd4933844b95ef739a7e77b097deb7efe4ec576.tar.gz URL https://github.com/isledecomp/SIEdit/archive/17c7736a6ff31413f1e74ab4e989011b545b6926.tar.gz
URL_MD5 59fd3c36f4f380f730cd9bedfc846397 URL_MD5 04edbc974df8884f283d920ded10f1f6
) )
add_library(libweaver STATIC add_library(libweaver STATIC
${libweaver_SOURCE_DIR}/lib/core.cpp ${libweaver_SOURCE_DIR}/lib/core.cpp

View File

@ -531,6 +531,22 @@ if (ISLE_EXTENSIONS)
extensions/src/extensions.cpp extensions/src/extensions.cpp
extensions/src/siloader.cpp extensions/src/siloader.cpp
extensions/src/textureloader.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() endif()

View File

@ -89,10 +89,10 @@ void Emscripten_SetupFilesystem()
} }
#ifdef EXTENSIONS #ifdef EXTENSIONS
if (Extensions::TextureLoader::enabled) { if (Extensions::TextureLoaderExt::enabled) {
MxString directory = MxString directory =
MxString("/LEGO") + Extensions::TextureLoader::options["texture loader:texture path"].c_str(); MxString("/LEGO") + Extensions::TextureLoaderExt::options["texture loader:texture path"].c_str();
Extensions::TextureLoader::options["texture loader:texture path"] = directory.GetData(); Extensions::TextureLoaderExt::options["texture loader:texture path"] = directory.GetData();
wasmfs_create_directory(directory.GetData(), 0644, fetchfs); wasmfs_create_directory(directory.GetData(), 0644, fetchfs);
MxU32 i = 0; MxU32 i = 0;
@ -102,17 +102,17 @@ void Emscripten_SetupFilesystem()
registerFile(path.GetData()); registerFile(path.GetData());
if (!preloadFile(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)); Emscripten_SendExtensionProgress("HD Textures", (++i * 100) / sizeOfArray(g_textures));
} }
} }
if (Extensions::SiLoader::enabled) { if (Extensions::SiLoaderExt::enabled) {
wasmfs_create_directory("/LEGO/extra", 0644, fetchfs); 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()); registerFile(file.c_str());
} }
} }

View File

@ -37,7 +37,7 @@
#include "viewmanager/viewmanager.h" #include "viewmanager/viewmanager.h"
#include <array> #include <array>
#include <extensions/extensions.h> #include <extensions/thirdpersoncamera.h>
#include <miniwin/miniwindevice.h> #include <miniwin/miniwindevice.h>
#include <type_traits> #include <type_traits>
#include <vec.h> #include <vec.h>
@ -875,6 +875,10 @@ SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
} }
} }
#ifdef EXTENSIONS
Extensions::ThirdPersonCameraExt::HandleSDLEvent(event);
#endif
return SDL_APP_CONTINUE; return SDL_APP_CONTINUE;
} }

View File

@ -1,6 +1,7 @@
#ifndef ISLEPATHACTOR_H #ifndef ISLEPATHACTOR_H
#define ISLEPATHACTOR_H #define ISLEPATHACTOR_H
#include "extensions/fwd.h"
#include "legogamestate.h" #include "legogamestate.h"
#include "legopathactor.h" #include "legopathactor.h"
#include "mxtypes.h" #include "mxtypes.h"
@ -139,6 +140,8 @@ class IslePathActor : public LegoPathActor {
// IslePathActor::`scalar deleting destructor' // IslePathActor::`scalar deleting destructor'
protected: protected:
friend class Extensions::ThirdPersonCamera::Controller;
LegoWorld* m_world; // 0x154 LegoWorld* m_world; // 0x154
LegoPathActor* m_previousActor; // 0x158 LegoPathActor* m_previousActor; // 0x158
MxFloat m_previousVel; // 0x15c MxFloat m_previousVel; // 0x15c

View File

@ -2,6 +2,7 @@
#define LEGOCHARACTERMANAGER_H #define LEGOCHARACTERMANAGER_H
#include "decomp.h" #include "decomp.h"
#include "extensions/fwd.h"
#include "mxstl/stlcompat.h" #include "mxstl/stlcompat.h"
#include "mxtypes.h" #include "mxtypes.h"
#include "mxvariable.h" #include "mxvariable.h"
@ -98,6 +99,8 @@ class LegoCharacterManager {
static const char* GetCustomizeAnimFile() { return g_customizeAnimFile; } static const char* GetCustomizeAnimFile() { return g_customizeAnimFile; }
private: private:
friend class Extensions::Common::CharacterCloner;
LegoROI* CreateActorROI(const char* p_key); LegoROI* CreateActorROI(const char* p_key);
void RemoveROI(LegoROI* p_roi); void RemoveROI(LegoROI* p_roi);
LegoROI* FindChildROI(LegoROI* p_roi, const char* p_name); LegoROI* FindChildROI(LegoROI* p_roi, const char* p_name);

View File

@ -2,6 +2,7 @@
#define LEGOINPUTMANAGER_H #define LEGOINPUTMANAGER_H
#include "decomp.h" #include "decomp.h"
#include "extensions/fwd.h"
#include "lego1_export.h" #include "lego1_export.h"
#include "legoeventnotificationparam.h" #include "legoeventnotificationparam.h"
#include "mxlist.h" #include "mxlist.h"
@ -179,6 +180,8 @@ class LegoInputManager : public MxPresenter {
// LegoInputManager::`scalar deleting destructor' // LegoInputManager::`scalar deleting destructor'
private: private:
friend class Extensions::ThirdPersonCameraExt;
void InitializeHaptics(); void InitializeHaptics();
MxCriticalSection m_criticalSection; // 0x58 MxCriticalSection m_criticalSection; // 0x58
@ -204,6 +207,7 @@ class LegoInputManager : public MxPresenter {
TouchScheme m_touchScheme = e_none; TouchScheme m_touchScheme = e_none;
SDL_Point m_touchVirtualThumb = {0, 0}; SDL_Point m_touchVirtualThumb = {0, 0};
SDL_FPoint m_touchVirtualThumbOrigin; SDL_FPoint m_touchVirtualThumbOrigin;
SDL_FingerID m_touchFinger = 0;
std::map<SDL_FingerID, MxU32> m_touchFlags; std::map<SDL_FingerID, MxU32> m_touchFlags;
std::map<SDL_KeyboardID, std::pair<void*, void*>> m_keyboards; std::map<SDL_KeyboardID, std::pair<void*, void*>> m_keyboards;
std::map<SDL_MouseID, std::pair<void*, SDL_Haptic*>> m_mice; std::map<SDL_MouseID, std::pair<void*, SDL_Haptic*>> m_mice;

View File

@ -2,6 +2,7 @@
#define __LEGONAVCONTROLLER_H #define __LEGONAVCONTROLLER_H
#include "decomp.h" #include "decomp.h"
#include "extensions/fwd.h"
#include "mxcore.h" #include "mxcore.h"
#include "mxtypes.h" #include "mxtypes.h"
@ -122,6 +123,9 @@ class LegoNavController : public MxCore {
// LegoNavController::`scalar deleting destructor' // LegoNavController::`scalar deleting destructor'
protected: 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 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 CalculateNewTargetVel(int p_pos, int p_center, float p_max);
float CalculateNewAccel(int p_pos, int p_center, float p_max, int p_min); float CalculateNewAccel(int p_pos, int p_center, float p_max, int p_min);

View File

@ -1,6 +1,7 @@
#include "islepathactor.h" #include "islepathactor.h"
#include "3dmanager/lego3dmanager.h" #include "3dmanager/lego3dmanager.h"
#include "extensions/thirdpersoncamera.h"
#include "isle_actions.h" #include "isle_actions.h"
#include "jukebox_actions.h" #include "jukebox_actions.h"
#include "legoanimationmanager.h" #include "legoanimationmanager.h"
@ -16,6 +17,8 @@
#include "scripts.h" #include "scripts.h"
#include "viewmanager/viewmanager.h" #include "viewmanager/viewmanager.h"
using namespace Extensions;
DECOMP_SIZE_ASSERT(IslePathActor, 0x160) DECOMP_SIZE_ASSERT(IslePathActor, 0x160)
DECOMP_SIZE_ASSERT(IslePathActor::SpawnLocation, 0x38) DECOMP_SIZE_ASSERT(IslePathActor::SpawnLocation, 0x38)
@ -95,6 +98,8 @@ void IslePathActor::Enter()
TurnAround(); TurnAround();
TransformPointOfView(); TransformPointOfView();
} }
Extension<ThirdPersonCameraExt>::Call(TP::HandleActorEnter, this);
} }
// FUNCTION: LEGO1 0x1001a3f0 // FUNCTION: LEGO1 0x1001a3f0
@ -154,6 +159,8 @@ void IslePathActor::Exit()
TurnAround(); TurnAround();
TransformPointOfView(); TransformPointOfView();
ResetViewVelocity(); ResetViewVelocity();
Extension<ThirdPersonCameraExt>::Call(TP::HandleActorExit, this);
} }
// GLOBAL: LEGO1 0x10102b28 // GLOBAL: LEGO1 0x10102b28

View File

@ -186,6 +186,11 @@ void Lego3DSound::FUN_10011a60(ma_sound* p_sound, const char* p_name)
} }
} }
else { 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)) { if (CharacterManager()->IsActor(p_name)) {
m_roi = CharacterManager()->GetActorROI(p_name, TRUE); m_roi = CharacterManager()->GetActorROI(p_name, TRUE);
m_enabled = m_isActor = TRUE; m_enabled = m_isActor = TRUE;

View File

@ -3,6 +3,7 @@
#include "3dmanager/lego3dmanager.h" #include "3dmanager/lego3dmanager.h"
#include "decomp.h" #include "decomp.h"
#include "define.h" #include "define.h"
#include "extensions/thirdpersoncamera.h"
#include "islepathactor.h" #include "islepathactor.h"
#include "legoanimationmanager.h" #include "legoanimationmanager.h"
#include "legoanimpresenter.h" #include "legoanimpresenter.h"
@ -20,6 +21,8 @@
#include "mxtimer.h" #include "mxtimer.h"
#include "mxutilities.h" #include "mxutilities.h"
using namespace Extensions;
DECOMP_SIZE_ASSERT(LegoAnimMMPresenter, 0x74) DECOMP_SIZE_ASSERT(LegoAnimMMPresenter, 0x74)
// FUNCTION: LEGO1 0x1004a8d0 // FUNCTION: LEGO1 0x1004a8d0
@ -480,6 +483,10 @@ MxBool LegoAnimMMPresenter::FUN_1004b6d0(MxLong p_time)
} }
actor->SetActorState(LegoPathActor::c_initial); actor->SetActorState(LegoPathActor::c_initial);
if (m_tranInfo->m_unk0x29) {
Extension<ThirdPersonCameraExt>::Call(TP::HandleCamAnimEnd, actor);
}
} }
return TRUE; return TRUE;

View File

@ -1,6 +1,7 @@
#include "legocharactermanager.h" #include "legocharactermanager.h"
#include "3dmanager/lego3dmanager.h" #include "3dmanager/lego3dmanager.h"
#include "extensions/thirdpersoncamera.h"
#include "legoactors.h" #include "legoactors.h"
#include "legoanimactor.h" #include "legoanimactor.h"
#include "legobuildingmanager.h" #include "legobuildingmanager.h"
@ -22,6 +23,8 @@
#include <stdio.h> #include <stdio.h>
#include <vec.h> #include <vec.h>
using namespace Extensions;
DECOMP_SIZE_ASSERT(LegoCharacter, 0x08) DECOMP_SIZE_ASSERT(LegoCharacter, 0x08)
DECOMP_SIZE_ASSERT(LegoCharacterManager, 0x08) DECOMP_SIZE_ASSERT(LegoCharacterManager, 0x08)
DECOMP_SIZE_ASSERT(CustomizeAnimFileVariable, 0x24) DECOMP_SIZE_ASSERT(CustomizeAnimFileVariable, 0x24)
@ -279,7 +282,8 @@ LegoROI* LegoCharacterManager::GetActorROI(const char* p_name, MxBool p_createEn
} }
if (character != NULL) { if (character != NULL) {
if (p_createEntity && character->m_roi->GetEntity() == NULL) { if (p_createEntity && character->m_roi->GetEntity() == NULL &&
!Extension<ThirdPersonCameraExt>::Call(TP::IsClonedCharacter, p_name).value_or(FALSE)) {
LegoExtraActor* actor = new LegoExtraActor(); LegoExtraActor* actor = new LegoExtraActor();
actor->SetROI(character->m_roi, FALSE, FALSE); actor->SetROI(character->m_roi, FALSE, FALSE);

View File

@ -59,7 +59,7 @@ LegoTextureInfo* LegoTextureInfo::Create(const char* p_name, LegoTexture* p_text
strcpy(textureInfo->m_name, p_name); strcpy(textureInfo->m_name, p_name);
} }
if (Extension<TextureLoader>::Call(PatchTexture, textureInfo).value_or(false)) { if (Extension<TextureLoaderExt>::Call(TL::PatchTexture, textureInfo).value_or(false)) {
return textureInfo; return textureInfo;
} }

View File

@ -505,8 +505,8 @@ MxBool RemoveFromCurrentWorld(const MxAtomId& p_atomId, MxS32 p_id)
{ {
LegoWorld* world = CurrentWorld(); LegoWorld* world = CurrentWorld();
auto result = auto result = Extension<SiLoaderExt>::Call(SI::HandleRemove, SiLoaderExt::StreamObject{p_atomId, p_id}, world)
Extension<SiLoader>::Call(HandleRemove, SiLoader::StreamObject{p_atomId, p_id}, world).value_or(std::nullopt); .value_or(std::nullopt);
if (result) { if (result) {
return result.value(); return result.value();
} }
@ -545,8 +545,9 @@ MxBool RemoveFromWorld(
{ {
LegoWorld* world = FindWorld(p_worldAtom, p_worldEntityId); LegoWorld* world = FindWorld(p_worldAtom, p_worldEntityId);
auto result = Extension<SiLoader>::Call(HandleRemove, SiLoader::StreamObject{p_entityAtom, p_entityId}, world) auto result =
.value_or(std::nullopt); Extension<SiLoaderExt>::Call(SI::HandleRemove, SiLoaderExt::StreamObject{p_entityAtom, p_entityId}, world)
.value_or(std::nullopt);
if (result) { if (result) {
return result.value(); return result.value();
} }

View File

@ -2,6 +2,7 @@
#include "3dmanager/lego3dmanager.h" #include "3dmanager/lego3dmanager.h"
#include "act3.h" #include "act3.h"
#include "extensions/thirdpersoncamera.h"
#include "infocenter.h" #include "infocenter.h"
#include "legoanimationmanager.h" #include "legoanimationmanager.h"
#include "legocameracontroller.h" #include "legocameracontroller.h"
@ -29,6 +30,8 @@
#include <SDL3/SDL_stdinc.h> #include <SDL3/SDL_stdinc.h>
#include <vec.h> #include <vec.h>
using namespace Extensions;
DECOMP_SIZE_ASSERT(LegoNavController, 0x70) DECOMP_SIZE_ASSERT(LegoNavController, 0x70)
// MSVC 4.20 didn't define a macro for this key // MSVC 4.20 didn't define a macro for this key
@ -348,6 +351,12 @@ MxBool LegoNavController::CalculateNewPosDir(
ProcessJoystickInput(rotatedY); 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) { if (m_useRotationalVel) {
m_rotationalVel = CalculateNewVel(m_targetRotationalVel, m_rotationalVel, m_rotationalAccel * 40.0f, deltaTime); m_rotationalVel = CalculateNewVel(m_targetRotationalVel, m_rotationalVel, m_rotationalAccel * 40.0f, deltaTime);
} }

View File

@ -2,6 +2,7 @@
#include "anim/legoanim.h" #include "anim/legoanim.h"
#include "extensions/siloader.h" #include "extensions/siloader.h"
#include "extensions/thirdpersoncamera.h"
#include "legoanimationmanager.h" #include "legoanimationmanager.h"
#include "legoanimpresenter.h" #include "legoanimpresenter.h"
#include "legobuildingmanager.h" #include "legobuildingmanager.h"
@ -639,8 +640,8 @@ MxCore* LegoWorld::Find(const char* p_class, const char* p_name)
// FUNCTION: BETA10 0x100db3de // FUNCTION: BETA10 0x100db3de
MxCore* LegoWorld::Find(const MxAtomId& p_atom, MxS32 p_entityId) MxCore* LegoWorld::Find(const MxAtomId& p_atom, MxS32 p_entityId)
{ {
auto result = auto result = Extension<SiLoaderExt>::Call(SI::HandleFind, SiLoaderExt::StreamObject{p_atom, p_entityId}, this)
Extension<SiLoader>::Call(HandleFind, SiLoader::StreamObject{p_atom, p_entityId}, this).value_or(std::nullopt); .value_or(std::nullopt);
if (result) { if (result) {
return result.value(); return result.value();
} }
@ -753,6 +754,8 @@ void LegoWorld::Enable(MxBool p_enable)
#ifndef BETA10 #ifndef BETA10
SetIsWorldActive(TRUE); SetIsWorldActive(TRUE);
#endif #endif
Extension<ThirdPersonCameraExt>::Call(TP::HandleWorldEnable, this, TRUE);
} }
else if (!p_enable && m_disabledObjects.size() == 0) { else if (!p_enable && m_disabledObjects.size() == 0) {
MxPresenter* presenter; MxPresenter* presenter;
@ -815,6 +818,8 @@ void LegoWorld::Enable(MxBool p_enable)
} }
GetViewManager()->RemoveAll(NULL); GetViewManager()->RemoveAll(NULL);
Extension<ThirdPersonCameraExt>::Call(TP::HandleWorldEnable, this, FALSE);
} }
} }

View File

@ -1,5 +1,6 @@
#include "legoinputmanager.h" #include "legoinputmanager.h"
#include "extensions/thirdpersoncamera.h"
#include "legocameracontroller.h" #include "legocameracontroller.h"
#include "legocontrolmanager.h" #include "legocontrolmanager.h"
#include "legomain.h" #include "legomain.h"
@ -14,6 +15,8 @@
#include <SDL3/SDL_log.h> #include <SDL3/SDL_log.h>
using namespace Extensions;
DECOMP_SIZE_ASSERT(LegoInputManager, 0x338) DECOMP_SIZE_ASSERT(LegoInputManager, 0x338)
DECOMP_SIZE_ASSERT(LegoNotifyList, 0x18) DECOMP_SIZE_ASSERT(LegoNotifyList, 0x18)
DECOMP_SIZE_ASSERT(LegoNotifyListCursor, 0x10) DECOMP_SIZE_ASSERT(LegoNotifyListCursor, 0x10)
@ -320,6 +323,12 @@ MxBool LegoInputManager::ProcessOneEvent(LegoEventNotificationParam& p_param)
} }
else { else {
if (!Lego()->IsPaused()) { if (!Lego()->IsPaused()) {
if ((p_param.GetModifier() & LegoEventNotificationParam::c_rButtonState) &&
!(p_param.GetModifier() & LegoEventNotificationParam::c_lButtonState) &&
Extension<ThirdPersonCameraExt>::Call(TP::IsThirdPersonCameraActive).value_or(FALSE)) {
return FALSE;
}
processRoi = TRUE; processRoi = TRUE;
if (m_unk0x335 != 0) { if (m_unk0x335 != 0) {
@ -393,6 +402,9 @@ MxBool LegoInputManager::ProcessOneEvent(LegoEventNotificationParam& p_param)
if (entity && entity->Notify(p_param) != 0) { if (entity && entity->Notify(p_param) != 0) {
return TRUE; return TRUE;
} }
if (Extension<ThirdPersonCameraExt>::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) MxBool LegoInputManager::HandleTouchEvent(SDL_Event* p_event, TouchScheme p_touchScheme)
{ {
if (Extension<ThirdPersonCameraExt>::Call(TP::HandleTouchInput, p_event).value_or(FALSE)) {
return FALSE;
}
const SDL_TouchFingerEvent& event = p_event->tfinger; const SDL_TouchFingerEvent& event = p_event->tfinger;
m_touchScheme = p_touchScheme; m_touchScheme = p_touchScheme;
@ -661,26 +677,24 @@ MxBool LegoInputManager::HandleTouchEvent(SDL_Event* p_event, TouchScheme p_touc
} }
break; break;
case e_gamepad: { case e_gamepad: {
static SDL_FingerID g_finger = (SDL_FingerID) 0;
switch (p_event->type) { switch (p_event->type) {
case SDL_EVENT_FINGER_DOWN: case SDL_EVENT_FINGER_DOWN:
if (!g_finger) { if (!m_touchFinger) {
g_finger = event.fingerID; m_touchFinger = event.fingerID;
m_touchVirtualThumb = {0, 0}; m_touchVirtualThumb = {0, 0};
m_touchVirtualThumbOrigin = {event.x, event.y}; m_touchVirtualThumbOrigin = {event.x, event.y};
} }
break; break;
case SDL_EVENT_FINGER_UP: case SDL_EVENT_FINGER_UP:
case SDL_EVENT_FINGER_CANCELED: case SDL_EVENT_FINGER_CANCELED:
if (event.fingerID == g_finger) { if (event.fingerID == m_touchFinger) {
g_finger = 0; m_touchFinger = 0;
m_touchVirtualThumb = {0, 0}; m_touchVirtualThumb = {0, 0};
m_touchVirtualThumbOrigin = {0, 0}; m_touchVirtualThumbOrigin = {0, 0};
} }
break; break;
case SDL_EVENT_FINGER_MOTION: case SDL_EVENT_FINGER_MOTION:
if (event.fingerID == g_finger) { if (event.fingerID == m_touchFinger) {
const float thumbstickRadius = 0.25f; const float thumbstickRadius = 0.25f;
const float deltaX = const float deltaX =
SDL_clamp(event.x - m_touchVirtualThumbOrigin.x, -thumbstickRadius, thumbstickRadius); SDL_clamp(event.x - m_touchVirtualThumbOrigin.x, -thumbstickRadius, thumbstickRadius);

View File

@ -2,6 +2,7 @@
#include "3dmanager/lego3dmanager.h" #include "3dmanager/lego3dmanager.h"
#include "extensions/siloader.h" #include "extensions/siloader.h"
#include "extensions/thirdpersoncamera.h"
#include "islepathactor.h" #include "islepathactor.h"
#include "legoanimationmanager.h" #include "legoanimationmanager.h"
#include "legobuildingmanager.h" #include "legobuildingmanager.h"
@ -355,6 +356,7 @@ MxResult LegoOmni::Create(MxOmniCreateParam& p_param)
m_gameState->SetCurrentAct(LegoGameState::e_act1); m_gameState->SetCurrentAct(LegoGameState::e_act1);
#endif #endif
Extension<ThirdPersonCameraExt>::Call(TP::HandleCreate);
result = SUCCESS; result = SUCCESS;
} }
else { else {
@ -414,7 +416,7 @@ void LegoOmni::AddWorld(LegoWorld* p_world)
{ {
m_worldList->Append(p_world); m_worldList->Append(p_world);
Extension<SiLoader>::Call(HandleWorld, p_world); Extension<SiLoaderExt>::Call(SI::HandleWorld, p_world);
} }
// FUNCTION: LEGO1 0x1005adb0 // FUNCTION: LEGO1 0x1005adb0
@ -482,7 +484,7 @@ LegoWorld* LegoOmni::FindWorld(const MxAtomId& p_atom, MxS32 p_entityid)
// STUB: BETA10 0x1008e93e // STUB: BETA10 0x1008e93e
void LegoOmni::DeleteObject(MxDSAction& p_dsAction) void LegoOmni::DeleteObject(MxDSAction& p_dsAction)
{ {
auto result = Extension<SiLoader>::Call(HandleDelete, p_dsAction).value_or(std::nullopt); auto result = Extension<SiLoaderExt>::Call(SI::HandleDelete, p_dsAction).value_or(std::nullopt);
if (result && result.value()) { if (result && result.value()) {
return; return;
} }
@ -677,7 +679,7 @@ void LegoOmni::CreateBackgroundAudio()
MxResult LegoOmni::Start(MxDSAction* p_dsAction) MxResult LegoOmni::Start(MxDSAction* p_dsAction)
{ {
{ {
auto result = Extension<SiLoader>::Call(HandleStart, *p_dsAction).value_or(std::nullopt); auto result = Extension<SiLoaderExt>::Call(SI::HandleStart, *p_dsAction).value_or(std::nullopt);
if (result) { if (result) {
return result.value(); return result.value();
} }
@ -740,5 +742,5 @@ void LegoOmni::Resume()
void LegoOmni::LoadSiLoader() void LegoOmni::LoadSiLoader()
{ {
Extension<SiLoader>::Call(Load); Extension<SiLoaderExt>::Call(SI::Load);
} }

View File

@ -342,8 +342,9 @@ MxLong Infocenter::HandleEndAction(MxEndActionNotificationParam& p_param)
MxLong result = m_radio.Notify(p_param); MxLong result = m_radio.Notify(p_param);
if (result || (action->GetAtomId() != m_atomId && action->GetAtomId() != *g_introScript && if (result ||
!Extension<SiLoader>::Call(ReplacedIn, *action, m_atomId, *g_introScript).value_or(std::nullopt))) { (action->GetAtomId() != m_atomId && action->GetAtomId() != *g_introScript &&
!Extension<SiLoaderExt>::Call(SI::ReplacedIn, *action, m_atomId, *g_introScript).value_or(std::nullopt))) {
return result; return result;
} }

View File

@ -216,7 +216,7 @@ MxLong Isle::HandleEndAction(MxEndActionNotificationParam& p_param)
result = 1; result = 1;
} }
} }
else if (auto replacedObject = Extension<SiLoader>::Call(ReplacedIn, *p_param.GetAction(), *g_jukeboxScript).value_or(std::nullopt)) { else if (auto replacedObject = Extension<SiLoaderExt>::Call(SI::ReplacedIn, *p_param.GetAction(), *g_jukeboxScript).value_or(std::nullopt)) {
MxS32 script = replacedObject->second; MxS32 script = replacedObject->second;
if (script >= JukeboxScript::c_JBMusic1 && script <= JukeboxScript::c_JBMusic6) { if (script >= JukeboxScript::c_JBMusic1 && script <= JukeboxScript::c_JBMusic6) {

View File

@ -23,7 +23,7 @@ MxResult ModelDbModel::Read(SDL_IOStream* p_file)
return FAILURE; return FAILURE;
} }
m_modelName = new char[len]; m_modelName = new char[((len + 3) & ~3u)];
if (SDL_ReadIO(p_file, m_modelName, len) != len) { if (SDL_ReadIO(p_file, m_modelName, len) != len) {
return FAILURE; return FAILURE;
} }
@ -38,7 +38,7 @@ MxResult ModelDbModel::Read(SDL_IOStream* p_file)
return FAILURE; return FAILURE;
} }
m_presenterName = new char[len]; m_presenterName = new char[((len + 3) & ~3u)];
if (SDL_ReadIO(p_file, m_presenterName, len) != len) { if (SDL_ReadIO(p_file, m_presenterName, len) != len) {
return FAILURE; return FAILURE;
} }

View File

@ -110,7 +110,7 @@ MxResult MxNotificationManager::Tickle()
m_sendList->pop_front(); m_sendList->pop_front();
if (notif->GetParam()->GetNotification() == c_notificationEndAction) { if (notif->GetParam()->GetNotification() == c_notificationEndAction) {
Extension<SiLoader>::Call(HandleEndAction, (MxEndActionNotificationParam&) *notif->GetParam()); Extension<SiLoaderExt>::Call(SI::HandleEndAction, (MxEndActionNotificationParam&) *notif->GetParam());
} }
notif->GetTarget()->Notify(*notif->GetParam()); notif->GetTarget()->Notify(*notif->GetParam());
@ -169,7 +169,7 @@ void MxNotificationManager::FlushPending(MxCore* p_listener)
pending.pop_front(); pending.pop_front();
if (notif->GetParam()->GetNotification() == c_notificationEndAction) { if (notif->GetParam()->GetNotification() == c_notificationEndAction) {
Extension<SiLoader>::Call(HandleEndAction, (MxEndActionNotificationParam&) *notif->GetParam()); Extension<SiLoaderExt>::Call(SI::HandleEndAction, (MxEndActionNotificationParam&) *notif->GetParam());
} }
notif->GetTarget()->Notify(*notif->GetParam()); notif->GetTarget()->Notify(*notif->GetParam());

View File

@ -0,0 +1,107 @@
#pragma once
#include "mxgeometry/mxmatrix.h"
#include "mxtypes.h"
#include "realtime/vector.h"
#include "roi/legoroi.h"
#include <map>
#include <string>
#include <vector>
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<std::string, AnimCache>& 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

View File

@ -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

View File

@ -0,0 +1,191 @@
#pragma once
#include "extensions/common/animutils.h"
#include "extensions/common/constants.h"
#include "mxgeometry/mxmatrix.h"
#include "mxtypes.h"
#include <cstdint>
#include <map>
#include <string>
#include <vector>
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<LegoCacheSound*> m_ROISounds;
// ROI map cache: animation name -> cached ROI map
std::map<std::string, AnimCache> m_animCacheMap;
// Ride animation (vehicle-specific)
PropGroup m_ridePropGroup;
int8_t m_currentVehicleType;
// Extra animation props
PropGroup m_extraAnimPropGroup;
};
} // namespace Common
} // namespace Extensions

View File

@ -0,0 +1,42 @@
#pragma once
#include "extensions/common/constants.h"
#include "legoactors.h"
#include "misc.h"
#include <SDL3/SDL_stdinc.h>
#include <cstdint>
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<int>(sizeOfArray(g_actorInfoInit)); i++) {
if (!SDL_strcasecmp(g_actorInfoInit[i].m_name, p_name)) {
return static_cast<uint8_t>(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

View File

@ -0,0 +1,49 @@
#pragma once
#include "mxtypes.h"
#include <cstdint>
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

View File

@ -0,0 +1,32 @@
#pragma once
#include "extensions/common/constants.h"
#include <cstdint>
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

View File

@ -0,0 +1,44 @@
#pragma once
#include <cstdint>
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

View File

@ -0,0 +1,22 @@
#pragma once
#include <cstdint>
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

View File

@ -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

View File

@ -6,24 +6,38 @@
#include <map> #include <map>
#include <optional> #include <optional>
#include <string> #include <string>
#include <type_traits>
namespace Extensions 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<std::string, std::string> p_options); LEGO1_EXPORT void Enable(const char* p_key, std::map<std::string, std::string> p_options);
template <typename T> template <typename T>
struct Extension { struct Extension {
template <typename Function, typename... Args> template <typename Function, typename... Args>
static auto Call(Function&& function, Args&&... args) -> std::optional<std::invoke_result_t<Function, Args...>> static auto Call(Function&& function, Args&&... args)
{ {
using result_t = std::invoke_result_t<Function, Args...>;
if constexpr (std::is_void_v<result_t>) {
#ifdef EXTENSIONS #ifdef EXTENSIONS
if (T::enabled) { if (T::enabled) {
return std::invoke(std::forward<Function>(function), std::forward<Args>(args)...); std::invoke(std::forward<Function>(function), std::forward<Args>(args)...);
} }
#endif #endif
return std::nullopt; }
else {
#ifdef EXTENSIONS
if (T::enabled) {
return std::optional<result_t>(
std::invoke(std::forward<Function>(function), std::forward<Args>(args)...)
);
}
#endif
return std::optional<result_t>(std::nullopt);
}
} }
}; };
}; // namespace Extensions }; // namespace Extensions

View File

@ -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

View File

@ -15,7 +15,7 @@ class Core;
namespace Extensions namespace Extensions
{ {
class SiLoader { class SiLoaderExt {
public: public:
typedef std::pair<MxAtomId, MxU32> StreamObject; typedef std::pair<MxAtomId, MxU32> StreamObject;
@ -31,12 +31,14 @@ class SiLoader {
template <typename... Args> template <typename... Args>
static std::optional<StreamObject> ReplacedIn(MxDSAction& p_action, Args... p_args); static std::optional<StreamObject> ReplacedIn(MxDSAction& p_action, Args... p_args);
static const std::vector<std::string>& GetFiles() { return files; }
static std::map<std::string, std::string> options; static std::map<std::string, std::string> options;
static std::vector<std::string> files;
static std::vector<std::string> directives;
static bool enabled; static bool enabled;
private: private:
static std::vector<std::string> files;
static std::vector<std::string> directives;
static std::vector<std::pair<StreamObject, StreamObject>> startWith; static std::vector<std::pair<StreamObject, StreamObject>> startWith;
static std::vector<std::pair<StreamObject, StreamObject>> removeWith; static std::vector<std::pair<StreamObject, StreamObject>> removeWith;
static std::vector<std::pair<StreamObject, StreamObject>> replace; static std::vector<std::pair<StreamObject, StreamObject>> replace;
@ -53,7 +55,7 @@ class SiLoader {
#ifdef EXTENSIONS #ifdef EXTENSIONS
template <typename... Args> template <typename... Args>
std::optional<SiLoader::StreamObject> SiLoader::ReplacedIn(MxDSAction& p_action, Args... p_args) std::optional<SiLoaderExt::StreamObject> SiLoaderExt::ReplacedIn(MxDSAction& p_action, Args... p_args)
{ {
StreamObject object{p_action.GetAtomId(), p_action.GetObjectId()}; StreamObject object{p_action.GetAtomId(), p_action.GetObjectId()};
auto checkAtomId = [&p_action, &object](const auto& p_atomId) -> std::optional<StreamObject> { auto checkAtomId = [&p_action, &object](const auto& p_atomId) -> std::optional<StreamObject> {
@ -70,26 +72,34 @@ std::optional<SiLoader::StreamObject> SiLoader::ReplacedIn(MxDSAction& p_action,
((void) (!result.has_value() && (result = checkAtomId(p_args), true)), ...); ((void) (!result.has_value() && (result = checkAtomId(p_args), true)), ...);
return result; return result;
} }
#endif
constexpr auto Load = &SiLoader::Load; namespace SI
constexpr auto HandleFind = &SiLoader::HandleFind; {
constexpr auto HandleStart = &SiLoader::HandleStart; #ifdef EXTENSIONS
constexpr auto HandleWorld = &SiLoader::HandleWorld; constexpr auto Load = &SiLoaderExt::Load;
constexpr auto HandleRemove = &SiLoader::HandleRemove; constexpr auto HandleFind = &SiLoaderExt::HandleFind;
constexpr auto HandleDelete = &SiLoader::HandleDelete; constexpr auto HandleStart = &SiLoaderExt::HandleStart;
constexpr auto HandleEndAction = &SiLoader::HandleEndAction; constexpr auto HandleWorld = &SiLoaderExt::HandleWorld;
constexpr auto ReplacedIn = [](auto&&... args) { return SiLoader::ReplacedIn(std::forward<decltype(args)>(args)...); }; 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<decltype(args)>(args)...);
};
#else #else
constexpr decltype(&SiLoader::Load) Load = nullptr; constexpr decltype(&SiLoaderExt::Load) Load = nullptr;
constexpr decltype(&SiLoader::HandleFind) HandleFind = nullptr; constexpr decltype(&SiLoaderExt::HandleFind) HandleFind = nullptr;
constexpr decltype(&SiLoader::HandleStart) HandleStart = nullptr; constexpr decltype(&SiLoaderExt::HandleStart) HandleStart = nullptr;
constexpr decltype(&SiLoader::HandleWorld) HandleWorld = nullptr; constexpr decltype(&SiLoaderExt::HandleWorld) HandleWorld = nullptr;
constexpr decltype(&SiLoader::HandleRemove) HandleRemove = nullptr; constexpr decltype(&SiLoaderExt::HandleRemove) HandleRemove = nullptr;
constexpr decltype(&SiLoader::HandleDelete) HandleDelete = nullptr; constexpr decltype(&SiLoaderExt::HandleDelete) HandleDelete = nullptr;
constexpr decltype(&SiLoader::HandleEndAction) HandleEndAction = nullptr; constexpr decltype(&SiLoaderExt::HandleEndAction) HandleEndAction = nullptr;
constexpr auto ReplacedIn = [](auto&&... args) -> std::optional<SiLoader::StreamObject> { constexpr auto ReplacedIn = [](auto&&... args) -> std::optional<SiLoaderExt::StreamObject> {
((void) args, ...); ((void) args, ...);
return std::nullopt; return std::nullopt;
}; };
#endif #endif
} // namespace SI
}; // namespace Extensions }; // namespace Extensions

View File

@ -9,13 +9,13 @@
namespace Extensions namespace Extensions
{ {
class TextureLoader { class TextureLoaderExt {
public: public:
static void Initialize(); static void Initialize();
static bool PatchTexture(LegoTextureInfo* p_textureInfo); static bool PatchTexture(LegoTextureInfo* p_textureInfo);
static void AddExcludedFile(const std::string& p_file);
static std::map<std::string, std::string> options; static std::map<std::string, std::string> options;
static std::vector<std::string> excludedFiles;
static bool enabled; static bool enabled;
static constexpr std::array<std::pair<std::string_view, std::string_view>, 1> defaults = { static constexpr std::array<std::pair<std::string_view, std::string_view>, 1> defaults = {
@ -23,12 +23,17 @@ class TextureLoader {
}; };
private: private:
static std::vector<std::string> excludedFiles;
static SDL_Surface* FindTexture(const char* p_name); static SDL_Surface* FindTexture(const char* p_name);
}; };
namespace TL
{
#ifdef EXTENSIONS #ifdef EXTENSIONS
constexpr auto PatchTexture = &TextureLoader::PatchTexture; constexpr auto PatchTexture = &TextureLoaderExt::PatchTexture;
#else #else
constexpr decltype(&TextureLoader::PatchTexture) PatchTexture = nullptr; constexpr decltype(&TextureLoaderExt::PatchTexture) PatchTexture = nullptr;
#endif #endif
} // namespace TL
}; // namespace Extensions }; // namespace Extensions

View File

@ -0,0 +1,91 @@
#pragma once
#include "extensions/extensions.h"
#include "mxtypes.h"
#include <SDL3/SDL_events.h>
#include <map>
#include <string>
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<std::string, std::string> 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

View File

@ -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 <SDL3/SDL_events.h>
#include <cstdint>
#include <functional>
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<void()> 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<void()> m_animStopCallback;
bool m_lmbForwardEngaged;
LegoROI* m_playerROI;
};
} // namespace ThirdPersonCamera
} // namespace Extensions

View File

@ -0,0 +1,47 @@
#pragma once
#include "extensions/common/customizestate.h"
#include <cstdint>
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

View File

@ -0,0 +1,61 @@
#pragma once
#include <SDL3/SDL_events.h>
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

View File

@ -0,0 +1,76 @@
#pragma once
#include "mxgeometry/mxgeometry3d.h"
#include "mxtypes.h"
#include <SDL3/SDL_stdinc.h>
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

View File

@ -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 <SDL3/SDL_stdinc.h>
#include <vector>
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<LegoROI*>& 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<LegoROI*> 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<std::string, AnimCache>& 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<LegoAnimPresenter*>(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);
}
}

View File

@ -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 <SDL3/SDL_stdinc.h>
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<LegoAnimPresenter*>(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);
}

View File

@ -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 <SDL3/SDL_stdinc.h>
#include <vec.h>
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;
}

View File

@ -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 <SDL3/SDL_stdinc.h>
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;
}

View File

@ -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

View File

@ -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];
}

View File

@ -0,0 +1,24 @@
#include "extensions/common/pathutils.h"
#include "legomain.h"
#include <SDL3/SDL_filesystem.h>
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;
}

View File

@ -2,26 +2,44 @@
#include "extensions/siloader.h" #include "extensions/siloader.h"
#include "extensions/textureloader.h" #include "extensions/textureloader.h"
#include "extensions/thirdpersoncamera.h"
#include <SDL3/SDL_log.h> #include <SDL3/SDL_log.h>
using namespace Extensions;
static void InitTextureLoader(std::map<std::string, std::string> p_options)
{
TextureLoaderExt::options = std::move(p_options);
TextureLoaderExt::enabled = true;
TextureLoaderExt::Initialize();
}
static void InitSiLoader(std::map<std::string, std::string> p_options)
{
SiLoaderExt::options = std::move(p_options);
SiLoaderExt::enabled = true;
SiLoaderExt::Initialize();
}
static void InitThirdPersonCamera(std::map<std::string, std::string> p_options)
{
ThirdPersonCameraExt::options = std::move(p_options);
ThirdPersonCameraExt::enabled = true;
ThirdPersonCameraExt::Initialize();
}
using InitFn = void (*)(std::map<std::string, std::string>);
static const InitFn extensionInits[] = {InitTextureLoader, InitSiLoader, InitThirdPersonCamera};
void Extensions::Enable(const char* p_key, std::map<std::string, std::string> p_options) void Extensions::Enable(const char* p_key, std::map<std::string, std::string> p_options)
{ {
for (const char* key : availableExtensions) { for (int i = 0; i < (int) (sizeof(availableExtensions) / sizeof(availableExtensions[0])); i++) {
if (!SDL_strcasecmp(p_key, key)) { if (!SDL_strcasecmp(p_key, availableExtensions[i])) {
if (!SDL_strcasecmp(p_key, "extensions:texture loader")) { extensionInits[i](std::move(p_options));
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();
}
SDL_Log("Enabled extension: %s", p_key); SDL_Log("Enabled extension: %s", p_key);
break; return;
} }
} }
} }

View File

@ -1,5 +1,6 @@
#include "extensions/siloader.h" #include "extensions/siloader.h"
#include "extensions/common/pathutils.h"
#include "legovideomanager.h" #include "legovideomanager.h"
#include "misc.h" #include "misc.h"
#include "mxdsaction.h" #include "mxdsaction.h"
@ -13,38 +14,38 @@ using namespace Extensions;
const char prependedMarker[] = ";;prepended;;"; const char prependedMarker[] = ";;prepended;;";
std::map<std::string, std::string> SiLoader::options; std::map<std::string, std::string> SiLoaderExt::options;
std::vector<std::string> SiLoader::files; std::vector<std::string> SiLoaderExt::files;
std::vector<std::string> SiLoader::directives; std::vector<std::string> SiLoaderExt::directives;
std::vector<std::pair<SiLoader::StreamObject, SiLoader::StreamObject>> SiLoader::startWith; std::vector<std::pair<SiLoaderExt::StreamObject, SiLoaderExt::StreamObject>> SiLoaderExt::startWith;
std::vector<std::pair<SiLoader::StreamObject, SiLoader::StreamObject>> SiLoader::removeWith; std::vector<std::pair<SiLoaderExt::StreamObject, SiLoaderExt::StreamObject>> SiLoaderExt::removeWith;
std::vector<std::pair<SiLoader::StreamObject, SiLoader::StreamObject>> SiLoader::replace; std::vector<std::pair<SiLoaderExt::StreamObject, SiLoaderExt::StreamObject>> SiLoaderExt::replace;
std::vector<std::pair<SiLoader::StreamObject, SiLoader::StreamObject>> SiLoader::prepend; std::vector<std::pair<SiLoaderExt::StreamObject, SiLoaderExt::StreamObject>> SiLoaderExt::prepend;
std::vector<SiLoader::StreamObject> SiLoader::fullScreenMovie; std::vector<SiLoaderExt::StreamObject> SiLoaderExt::fullScreenMovie;
std::vector<SiLoader::StreamObject> SiLoader::disable3d; std::vector<SiLoaderExt::StreamObject> SiLoaderExt::disable3d;
bool SiLoader::enabled = false; bool SiLoaderExt::enabled = false;
void SiLoader::Initialize() void SiLoaderExt::Initialize()
{ {
char* files = SDL_strdup(options["si loader:files"].c_str()); char* files = SDL_strdup(options["si loader:files"].c_str());
char* saveptr; char* saveptr;
for (char* file = SDL_strtok_r(files, ",\n\r ", &saveptr); file; file = SDL_strtok_r(NULL, ",\n\r ", &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()); char* directives = SDL_strdup(options["si loader:directives"].c_str());
for (char* directive = SDL_strtok_r(directives, ",\n\r ", &saveptr); directive; for (char* directive = SDL_strtok_r(directives, ",\n\r ", &saveptr); directive;
directive = SDL_strtok_r(NULL, ",\n\r ", &saveptr)) { directive = SDL_strtok_r(NULL, ",\n\r ", &saveptr)) {
SiLoader::directives.emplace_back(directive); SiLoaderExt::directives.emplace_back(directive);
} }
SDL_free(files); SDL_free(files);
SDL_free(directives); SDL_free(directives);
} }
bool SiLoader::Load() bool SiLoaderExt::Load()
{ {
for (const auto& file : files) { for (const auto& file : files) {
LoadFile(file.c_str()); LoadFile(file.c_str());
@ -57,7 +58,7 @@ bool SiLoader::Load()
return true; return true;
} }
std::optional<MxCore*> SiLoader::HandleFind(StreamObject p_object, LegoWorld* world) std::optional<MxCore*> SiLoaderExt::HandleFind(StreamObject p_object, LegoWorld* world)
{ {
for (const auto& key : replace) { for (const auto& key : replace) {
if (key.first == p_object) { if (key.first == p_object) {
@ -68,7 +69,7 @@ std::optional<MxCore*> SiLoader::HandleFind(StreamObject p_object, LegoWorld* wo
return std::nullopt; return std::nullopt;
} }
std::optional<MxResult> SiLoader::HandleStart(MxDSAction& p_action) std::optional<MxResult> SiLoaderExt::HandleStart(MxDSAction& p_action)
{ {
StreamObject object{p_action.GetAtomId(), p_action.GetObjectId()}; StreamObject object{p_action.GetAtomId(), p_action.GetObjectId()};
auto start = [](const StreamObject& p_object, MxDSAction& p_in, MxDSAction& p_out) -> MxResult { auto start = [](const StreamObject& p_object, MxDSAction& p_in, MxDSAction& p_out) -> MxResult {
@ -130,7 +131,7 @@ std::optional<MxResult> SiLoader::HandleStart(MxDSAction& p_action)
return std::nullopt; return std::nullopt;
} }
MxBool SiLoader::HandleWorld(LegoWorld* p_world) MxBool SiLoaderExt::HandleWorld(LegoWorld* p_world)
{ {
StreamObject object{p_world->GetAtomId(), p_world->GetEntityId()}; StreamObject object{p_world->GetAtomId(), p_world->GetEntityId()};
auto start = [](const StreamObject& p_object, MxDSAction& p_out) { auto start = [](const StreamObject& p_object, MxDSAction& p_out) {
@ -154,7 +155,7 @@ MxBool SiLoader::HandleWorld(LegoWorld* p_world)
return TRUE; return TRUE;
} }
std::optional<MxBool> SiLoader::HandleRemove(StreamObject p_object, LegoWorld* world) std::optional<MxBool> SiLoaderExt::HandleRemove(StreamObject p_object, LegoWorld* world)
{ {
for (const auto& key : removeWith) { for (const auto& key : removeWith) {
if (key.first == p_object) { if (key.first == p_object) {
@ -171,7 +172,7 @@ std::optional<MxBool> SiLoader::HandleRemove(StreamObject p_object, LegoWorld* w
return std::nullopt; return std::nullopt;
} }
std::optional<MxBool> SiLoader::HandleDelete(MxDSAction& p_action) std::optional<MxBool> SiLoaderExt::HandleDelete(MxDSAction& p_action)
{ {
StreamObject object{p_action.GetAtomId(), p_action.GetObjectId()}; StreamObject object{p_action.GetAtomId(), p_action.GetObjectId()};
auto deleteObject = [](const StreamObject& p_object, MxDSAction& p_in, MxDSAction& p_out) { auto deleteObject = [](const StreamObject& p_object, MxDSAction& p_in, MxDSAction& p_out) {
@ -202,7 +203,7 @@ std::optional<MxBool> SiLoader::HandleDelete(MxDSAction& p_action)
return std::nullopt; 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()}; StreamObject object{p_param.GetAction()->GetAtomId(), p_param.GetAction()->GetObjectId()};
auto start = [](const StreamObject& p_object, MxDSAction& p_in, MxDSAction& p_out) -> MxResult { 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; return TRUE;
} }
bool SiLoader::LoadFile(const char* p_file) bool SiLoaderExt::LoadFile(const char* p_file)
{ {
si::Interleaf si; si::Interleaf si;
MxStreamController* controller; MxStreamController* controller;
MxString path = MxString(MxOmni::GetHD()) + p_file; MxString path;
path.MapPathToFilesystem(); if (!Common::ResolveGamePath(p_file, path) ||
if (si.Read(path.GetData(), si::Interleaf::ObjectsOnly) != si::Interleaf::ERROR_SUCCESS) { si.Read(path.GetData(), si::Interleaf::ObjectsOnly) != si::Interleaf::ERROR_SUCCESS) {
path = MxString(MxOmni::GetCD()) + p_file; SDL_Log("Could not parse SI file %s", p_file);
path.MapPathToFilesystem(); return false;
if (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))) { if (!(controller = OpenStream(p_file))) {
@ -260,7 +256,7 @@ bool SiLoader::LoadFile(const char* p_file)
return true; return true;
} }
bool SiLoader::LoadDirective(const char* p_directive) bool SiLoaderExt::LoadDirective(const char* p_directive)
{ {
char originAtom[256], targetAtom[256]; char originAtom[256], targetAtom[256];
uint32_t originId, targetId; uint32_t originId, targetId;
@ -306,7 +302,7 @@ bool SiLoader::LoadDirective(const char* p_directive)
return true; return true;
} }
MxStreamController* SiLoader::OpenStream(const char* p_file) MxStreamController* SiLoaderExt::OpenStream(const char* p_file)
{ {
MxStreamController* controller; MxStreamController* controller;
@ -318,7 +314,7 @@ MxStreamController* SiLoader::OpenStream(const char* p_file)
return controller; 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()) { for (si::Core* child : p_core->GetChildren()) {
if (si::Object* object = dynamic_cast<si::Object*>(child)) { if (si::Object* object = dynamic_cast<si::Object*>(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 // The convention in LEGO Island is that world objects are always at ID 0
if (p_object.second == 0) { if (p_object.second == 0) {

View File

@ -1,4 +1,6 @@
#include "extensions/textureloader.h" #include "extensions/textureloader.h"
#include "extensions/common/pathutils.h"
#include "legovideomanager.h" #include "legovideomanager.h"
#include "misc.h" #include "misc.h"
#include "mxdirectx/mxdirect3d.h" #include "mxdirectx/mxdirect3d.h"
@ -7,11 +9,11 @@
using namespace Extensions; using namespace Extensions;
std::map<std::string, std::string> TextureLoader::options; std::map<std::string, std::string> TextureLoaderExt::options;
std::vector<std::string> TextureLoader::excludedFiles; std::vector<std::string> TextureLoaderExt::excludedFiles;
bool TextureLoader::enabled = false; bool TextureLoaderExt::enabled = false;
void TextureLoader::Initialize() void TextureLoaderExt::Initialize()
{ {
for (const auto& option : defaults) { for (const auto& option : defaults) {
if (!options.count(option.first.data())) { 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); SDL_Surface* surface = FindTexture(p_textureInfo->m_name);
if (!surface) { if (!surface) {
@ -103,22 +110,19 @@ bool TextureLoader::PatchTexture(LegoTextureInfo* p_textureInfo)
return true; 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()) { if (std::find(excludedFiles.begin(), excludedFiles.end(), p_name) != excludedFiles.end()) {
return nullptr; return nullptr;
} }
SDL_Surface* surface;
const char* texturePath = options["texture loader:texture path"].c_str(); 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(); MxString path;
if (!(surface = SDL_LoadBMP(path.GetData()))) { if (!Common::ResolveGamePath(relativePath.GetData(), path)) {
path = MxString(MxOmni::GetCD()) + texturePath + "/" + p_name + ".bmp"; return nullptr;
path.MapPathToFilesystem();
surface = SDL_LoadBMP(path.GetData());
} }
return surface; return SDL_LoadBMP(path.GetData());
} }

View File

@ -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 <SDL3/SDL_stdinc.h>
using namespace Extensions;
using namespace Extensions::Common;
std::map<std::string, std::string> 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<uint8_t>(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<ThirdPersonCameraExt>::Call(TP::HandleSDLEvent, p_event);
}

View File

@ -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 <SDL3/SDL_stdinc.h>
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<LegoPathActor*>(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<LegoPathActor*>(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<LegoActor*>(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);
}
}

View File

@ -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 <SDL3/SDL_stdinc.h>
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);
}
}

View File

@ -0,0 +1,257 @@
#include "extensions/thirdpersoncamera/inputhandler.h"
#include "extensions/thirdpersoncamera/orbitcamera.h"
#include <SDL3/SDL_stdinc.h>
#include <SDL3/SDL_timer.h>
#include <utility>
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;
}
}

View File

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

View File

@ -78,4 +78,5 @@ SDL_KeyboardID_v: "SDL-based name"
SDL_MouseID_v: "SDL-based name" SDL_MouseID_v: "SDL-based name"
SDL_JoystickID_v: "SDL-based name" SDL_JoystickID_v: "SDL-based name"
SDL_TouchID_v: "SDL-based name" SDL_TouchID_v: "SDL-based name"
Load: "Not a variable but function name" Load: "Not a variable but function name"
HandleCreate: "Not a variable but function name"