diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cd0dbf4..a4b62a77 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -532,6 +532,7 @@ if (ISLE_EXTENSIONS) extensions/src/siloader.cpp extensions/src/textureloader.cpp extensions/src/multiplayer.cpp + extensions/src/multiplayer/animdata.cpp extensions/src/multiplayer/animutils.cpp extensions/src/multiplayer/characteranimator.cpp extensions/src/multiplayer/charactercloner.cpp diff --git a/extensions/include/extensions/multiplayer/animdata.h b/extensions/include/extensions/multiplayer/animdata.h new file mode 100644 index 00000000..5dd82a2c --- /dev/null +++ b/extensions/include/extensions/multiplayer/animdata.h @@ -0,0 +1,49 @@ +#pragma once + +#include "extensions/multiplayer/protocol.h" + +#include + +class LegoPathActor; + +namespace Multiplayer +{ + +// Animation and vehicle tables (defined in animdata.cpp) +extern const char* const g_walkAnimNames[]; +extern const int g_walkAnimCount; + +extern const char* const g_idleAnimNames[]; +extern const int g_idleAnimCount; + +// Per-phase emote data: animation name and optional sound effect. +struct EmotePhase { + const char* anim; // Animation name (nullptr = unused phase) + const char* sound; // Sound key for LegoCacheSoundManager (nullptr = silent) +}; + +// Emote table entry: two phases (phase 1 = primary, phase 2 = recovery for multi-part emotes). +struct EmoteEntry { + EmotePhase phases[2]; +}; + +extern const EmoteEntry g_emoteEntries[]; +extern const int g_emoteAnimCount; + +// Returns true if the emote is a multi-part stateful emote (has a phase-2 animation). +inline bool IsMultiPartEmote(uint8_t p_emoteId) +{ + return p_emoteId < g_emoteAnimCount && g_emoteEntries[p_emoteId].phases[1].anim != nullptr; +} + +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 Multiplayer diff --git a/extensions/include/extensions/multiplayer/characteranimator.h b/extensions/include/extensions/multiplayer/characteranimator.h index 13efc11b..1cfe2012 100644 --- a/extensions/include/extensions/multiplayer/characteranimator.h +++ b/extensions/include/extensions/multiplayer/characteranimator.h @@ -1,7 +1,7 @@ #pragma once +#include "extensions/multiplayer/animdata.h" #include "extensions/multiplayer/animutils.h" -#include "extensions/multiplayer/protocol.h" #include "mxgeometry/mxmatrix.h" #include "mxtypes.h" diff --git a/extensions/include/extensions/multiplayer/protocol.h b/extensions/include/extensions/multiplayer/protocol.h index c8638bf1..4038e779 100644 --- a/extensions/include/extensions/multiplayer/protocol.h +++ b/extensions/include/extensions/multiplayer/protocol.h @@ -5,8 +5,6 @@ #include #include -class LegoPathActor; - namespace Multiplayer { @@ -155,33 +153,6 @@ struct CustomizeMsg { #pragma pack(pop) -// Animation and vehicle tables (defined in protocol.cpp) -extern const char* const g_walkAnimNames[]; -extern const int g_walkAnimCount; - -extern const char* const g_idleAnimNames[]; -extern const int g_idleAnimCount; - -// Emote animation table: [emoteId][phase]. Phase 0 = primary, phase 1 = phase-2 (nullptr for one-shot). -extern const char* const g_emoteAnims[][2]; -extern const int g_emoteAnimCount; - -// Returns true if the emote is a multi-part stateful emote (has a phase-2 animation). -inline bool IsMultiPartEmote(uint8_t p_emoteId) -{ - return p_emoteId < g_emoteAnimCount && g_emoteAnims[p_emoteId][1] != nullptr; -} - -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); - // Validate actorId is a playable character (1-5, not brickster) inline bool IsValidActorId(uint8_t p_actorId) { diff --git a/extensions/include/extensions/multiplayer/thirdpersoncamera.h b/extensions/include/extensions/multiplayer/thirdpersoncamera.h index 1d136865..795cb034 100644 --- a/extensions/include/extensions/multiplayer/thirdpersoncamera.h +++ b/extensions/include/extensions/multiplayer/thirdpersoncamera.h @@ -2,7 +2,6 @@ #include "extensions/multiplayer/characteranimator.h" #include "extensions/multiplayer/customizestate.h" -#include "extensions/multiplayer/protocol.h" #include "mxgeometry/mxgeometry3d.h" #include "mxtypes.h" diff --git a/extensions/src/multiplayer/animdata.cpp b/extensions/src/multiplayer/animdata.cpp new file mode 100644 index 00000000..af2e37e6 --- /dev/null +++ b/extensions/src/multiplayer/animdata.cpp @@ -0,0 +1,78 @@ +#include "extensions/multiplayer/animdata.h" + +#include "legopathactor.h" + +namespace Multiplayer +{ + +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 +}; +const int g_idleAnimCount = sizeof(g_idleAnimNames) / sizeof(g_idleAnimNames[0]); + +// Emote table. Each entry has two phases: {anim, sound}. +// Phase 2 anim is nullptr for one-shot emotes; non-null makes it a multi-part stateful emote. +const EmoteEntry g_emoteEntries[] = { + {{{"CNs011xx", nullptr}, {nullptr, nullptr}}}, // 0: Wave (one-shot) + {{{"CNs012xx", nullptr}, {nullptr, nullptr}}}, // 1: Hat Tip (one-shot) + {{{"BNsDis01", "crash5"}, {"BNsAss01", nullptr}}}, // 2: Disassemble / Reassemble (multi-part) +}; +const int g_emoteAnimCount = sizeof(g_emoteEntries) / sizeof(g_emoteEntries[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 Multiplayer diff --git a/extensions/src/multiplayer/characteranimator.cpp b/extensions/src/multiplayer/characteranimator.cpp index 2a1b26f4..b348a72e 100644 --- a/extensions/src/multiplayer/characteranimator.cpp +++ b/extensions/src/multiplayer/characteranimator.cpp @@ -5,7 +5,9 @@ #include "extensions/multiplayer/charactercustomizer.h" #include "extensions/multiplayer/namebubblerenderer.h" #include "legoanimpresenter.h" +#include "legocachesoundmanager.h" #include "legocharactermanager.h" +#include "legosoundmanager.h" #include "legovideomanager.h" #include "legoworld.h" #include "misc.h" @@ -237,7 +239,7 @@ void CharacterAnimator::TriggerEmote(uint8_t p_emoteId, LegoROI* p_roi, bool p_i if (IsMultiPartEmote(p_emoteId)) { if (m_frozenEmoteId == (int8_t) p_emoteId) { // Phase 2: play the recovery animation to unfreeze - AnimCache* cache = GetOrBuildAnimCache(p_roi, g_emoteAnims[p_emoteId][1]); + AnimCache* cache = GetOrBuildAnimCache(p_roi, g_emoteEntries[p_emoteId].phases[1].anim); if (!cache || !cache->anim) { return; } @@ -250,6 +252,11 @@ void CharacterAnimator::TriggerEmote(uint8_t p_emoteId, LegoROI* p_roi, bool p_i m_emoteDuration = (float) cache->anim->GetDuration(); m_emoteActive = true; + const char* sound = g_emoteEntries[p_emoteId].phases[1].sound; + if (sound) { + SoundManager()->GetCacheSoundManager()->Play(sound, p_roi->GetName(), FALSE); + } + if (m_config.saveEmoteTransform) { m_emoteParentTransform = m_frozenParentTransform; } @@ -268,7 +275,7 @@ void CharacterAnimator::TriggerEmote(uint8_t p_emoteId, LegoROI* p_roi, bool p_i } } - AnimCache* cache = GetOrBuildAnimCache(p_roi, g_emoteAnims[p_emoteId][0]); + AnimCache* cache = GetOrBuildAnimCache(p_roi, g_emoteEntries[p_emoteId].phases[0].anim); if (!cache || !cache->anim) { return; } @@ -281,6 +288,11 @@ void CharacterAnimator::TriggerEmote(uint8_t p_emoteId, LegoROI* p_roi, bool p_i m_emoteDuration = (float) cache->anim->GetDuration(); m_emoteActive = true; + const char* sound = g_emoteEntries[p_emoteId].phases[0].sound; + if (sound) { + SoundManager()->GetCacheSoundManager()->Play(sound, p_roi->GetName(), FALSE); + } + // Save clean transform to prevent scale accumulation during emote if (m_config.saveEmoteTransform) { m_emoteParentTransform = p_roi->GetLocal2World(); @@ -371,7 +383,7 @@ void CharacterAnimator::InitAnimCaches(LegoROI* p_roi) void CharacterAnimator::SetFrozenEmoteId(int8_t p_emoteId, LegoROI* p_roi) { if (p_emoteId >= 0 && p_emoteId < g_emoteAnimCount && IsMultiPartEmote((uint8_t) p_emoteId)) { - AnimCache* cache = p_roi ? GetOrBuildAnimCache(p_roi, g_emoteAnims[p_emoteId][0]) : nullptr; + AnimCache* cache = p_roi ? GetOrBuildAnimCache(p_roi, g_emoteEntries[p_emoteId].phases[0].anim) : nullptr; m_frozenEmoteId = p_emoteId; m_frozenAnimCache = cache; m_frozenAnimDuration = (cache && cache->anim) ? (float) cache->anim->GetDuration() : 0.0f; diff --git a/extensions/src/multiplayer/protocol.cpp b/extensions/src/multiplayer/protocol.cpp index 66ad151b..987f001d 100644 --- a/extensions/src/multiplayer/protocol.cpp +++ b/extensions/src/multiplayer/protocol.cpp @@ -1,7 +1,6 @@ #include "extensions/multiplayer/protocol.h" #include "legogamestate.h" -#include "legopathactor.h" #include "misc.h" #include @@ -9,76 +8,6 @@ namespace Multiplayer { -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 -}; -const int g_idleAnimCount = sizeof(g_idleAnimNames) / sizeof(g_idleAnimNames[0]); - -// Emote animation table. Each entry is {phase1, phase2}. -// phase2 is nullptr for one-shot emotes; non-null makes it a multi-part stateful emote. -const char* const g_emoteAnims[][2] = { - {"CNs011xx", nullptr}, // 0: Wave (one-shot) - {"CNs012xx", nullptr}, // 1: Hat Tip (one-shot) - {"BNsDis01", "BNsAss01"}, // 2: Disassemble / Reassemble (multi-part) -}; -const int g_emoteAnimCount = sizeof(g_emoteAnims) / sizeof(g_emoteAnims[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; -} - void EncodeUsername(char p_out[8]) { SDL_memset(p_out, 0, 8);