This commit is contained in:
Christian Semmler 2026-03-28 13:26:13 -07:00
parent e57164d345
commit 05716eb94f
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
16 changed files with 356 additions and 25 deletions

View File

@ -60,6 +60,7 @@ class LegoCacheSound : public MxCore {
private: private:
friend class Multiplayer::Animation::AudioPlayer; friend class Multiplayer::Animation::AudioPlayer;
friend class Multiplayer::NetworkManager;
void Init(); void Init();
void CopyData(MxU8* p_data, MxU32 p_dataSize); void CopyData(MxU8* p_data, MxU32 p_dataSize);

View File

@ -1,6 +1,7 @@
#include "ambulance.h" #include "ambulance.h"
#include "decomp.h" #include "decomp.h"
#include "extensions/multiplayer.h"
#include "isle.h" #include "isle.h"
#include "isle_actions.h" #include "isle_actions.h"
#include "jukebox_actions.h" #include "jukebox_actions.h"
@ -26,6 +27,8 @@
#include <SDL3/SDL_stdinc.h> #include <SDL3/SDL_stdinc.h>
#include <stdio.h> #include <stdio.h>
using namespace Extensions;
DECOMP_SIZE_ASSERT(Ambulance, 0x184) DECOMP_SIZE_ASSERT(Ambulance, 0x184)
DECOMP_SIZE_ASSERT(AmbulanceMissionState, 0x24) DECOMP_SIZE_ASSERT(AmbulanceMissionState, 0x24)
@ -458,6 +461,7 @@ MxLong Ambulance::HandleControl(LegoControlManagerNotificationParam& p_param)
MxSoundPresenter* presenter = MxSoundPresenter* presenter =
(MxSoundPresenter*) CurrentWorld()->Find("MxSoundPresenter", "AmbulanceHorn_Sound"); (MxSoundPresenter*) CurrentWorld()->Find("MxSoundPresenter", "AmbulanceHorn_Sound");
presenter->Enable(p_param.m_enabledChild); presenter->Enable(p_param.m_enabledChild);
Extension<MultiplayerExt>::Call(MP::HandleHornPressed, (MxU32) p_param.m_clickedObjectId);
break; break;
} }
} }

View File

@ -1,5 +1,6 @@
#include "bike.h" #include "bike.h"
#include "extensions/multiplayer.h"
#include "isle.h" #include "isle.h"
#include "isle_actions.h" #include "isle_actions.h"
#include "jukebox_actions.h" #include "jukebox_actions.h"
@ -13,6 +14,8 @@
#include "mxtransitionmanager.h" #include "mxtransitionmanager.h"
#include "scripts.h" #include "scripts.h"
using namespace Extensions;
DECOMP_SIZE_ASSERT(Bike, 0x164) DECOMP_SIZE_ASSERT(Bike, 0x164)
// FUNCTION: LEGO1 0x10076670 // FUNCTION: LEGO1 0x10076670
@ -98,6 +101,7 @@ MxLong Bike::HandleControl(LegoControlManagerNotificationParam& p_param)
MxSoundPresenter* presenter = MxSoundPresenter* presenter =
(MxSoundPresenter*) CurrentWorld()->Find("MxSoundPresenter", "BikeHorn_Sound"); (MxSoundPresenter*) CurrentWorld()->Find("MxSoundPresenter", "BikeHorn_Sound");
presenter->Enable(p_param.m_enabledChild); presenter->Enable(p_param.m_enabledChild);
Extension<MultiplayerExt>::Call(MP::HandleHornPressed, (MxU32) p_param.m_clickedObjectId);
break; break;
} }
} }

View File

@ -1,6 +1,7 @@
#include "dunebuggy.h" #include "dunebuggy.h"
#include "decomp.h" #include "decomp.h"
#include "extensions/multiplayer.h"
#include "isle.h" #include "isle.h"
#include "isle_actions.h" #include "isle_actions.h"
#include "jukebox_actions.h" #include "jukebox_actions.h"
@ -21,6 +22,8 @@
#include <SDL3/SDL_stdinc.h> #include <SDL3/SDL_stdinc.h>
#include <stdio.h> #include <stdio.h>
using namespace Extensions;
DECOMP_SIZE_ASSERT(DuneBuggy, 0x16c) DECOMP_SIZE_ASSERT(DuneBuggy, 0x16c)
// GLOBAL: LEGO1 0x100f7660 // GLOBAL: LEGO1 0x100f7660
@ -141,6 +144,7 @@ MxLong DuneBuggy::HandleControl(LegoControlManagerNotificationParam& p_param)
MxSoundPresenter* presenter = MxSoundPresenter* presenter =
(MxSoundPresenter*) CurrentWorld()->Find("MxSoundPresenter", "DuneCarHorn_Sound"); (MxSoundPresenter*) CurrentWorld()->Find("MxSoundPresenter", "DuneCarHorn_Sound");
presenter->Enable(p_param.m_enabledChild); presenter->Enable(p_param.m_enabledChild);
Extension<MultiplayerExt>::Call(MP::HandleHornPressed, (MxU32) p_param.m_clickedObjectId);
break; break;
} }
} }

View File

@ -1,5 +1,6 @@
#include "towtrack.h" #include "towtrack.h"
#include "extensions/multiplayer.h"
#include "isle.h" #include "isle.h"
#include "isle_actions.h" #include "isle_actions.h"
#include "jukebox_actions.h" #include "jukebox_actions.h"
@ -22,6 +23,8 @@
#include <stdio.h> #include <stdio.h>
using namespace Extensions;
DECOMP_SIZE_ASSERT(TowTrack, 0x180) DECOMP_SIZE_ASSERT(TowTrack, 0x180)
DECOMP_SIZE_ASSERT(TowTrackMissionState, 0x28) DECOMP_SIZE_ASSERT(TowTrackMissionState, 0x28)
@ -502,6 +505,7 @@ MxLong TowTrack::HandleControl(LegoControlManagerNotificationParam& p_param)
case IsleScript::c_TowHorn_Ctl: case IsleScript::c_TowHorn_Ctl:
MxSoundPresenter* presenter = (MxSoundPresenter*) CurrentWorld()->Find("MxSoundPresenter", "TowHorn_Sound"); MxSoundPresenter* presenter = (MxSoundPresenter*) CurrentWorld()->Find("MxSoundPresenter", "TowHorn_Sound");
presenter->Enable(p_param.m_enabledChild); presenter->Enable(p_param.m_enabledChild);
Extension<MultiplayerExt>::Call(MP::HandleHornPressed, (MxU32) p_param.m_clickedObjectId);
break; break;
} }
} }

View File

@ -97,6 +97,14 @@ void ApplyTree(LegoAnim* p_anim, MxMatrix& p_transform, LegoTime p_time, LegoROI
// Each clone gets its own transform, safe for concurrent animation playback. // Each clone gets its own transform, safe for concurrent animation playback.
LegoROI* DeepCloneROI(LegoROI* p_source, const char* p_name); LegoROI* DeepCloneROI(LegoROI* p_source, const char* p_name);
// Compute child-to-parent local offsets for a hierarchical ROI.
// Returns one MxMatrix per compound child: offset = inverse(parent) * child.
std::vector<MxMatrix> ComputeChildOffsets(LegoROI* p_parent);
// Apply a new parent transform to a hierarchical ROI, positioning children
// using precomputed local offsets: child_world = parent_world * offset.
void ApplyHierarchyTransform(LegoROI* p_parent, const MxMatrix& p_transform, const std::vector<MxMatrix>& p_offsets);
// Strip trailing digits and underscores from a name to get the LOD base name. // Strip trailing digits and underscores from a name to get the LOD base name.
// Mirrors the digit-trimming in LegoAnimPresenter::CreateManagedActors/CreateSceneROIs. // Mirrors the digit-trimming in LegoAnimPresenter::CreateManagedActors/CreateSceneROIs.
std::string TrimLODSuffix(const std::string& p_name); std::string TrimLODSuffix(const std::string& p_name);

View File

@ -42,6 +42,7 @@ class MultiplayerExt {
static std::map<std::string, std::string> options; static std::map<std::string, std::string> options;
static bool enabled; static bool enabled;
static void HandleHornPressed(MxU32 p_controlId);
static MxBool IsClonedCharacter(const char* p_name); static MxBool IsClonedCharacter(const char* p_name);
static void HandleBeforeSaveLoad(); static void HandleBeforeSaveLoad();
static void HandleSaveLoaded(); static void HandleSaveLoaded();
@ -69,6 +70,7 @@ constexpr auto HandleWorldEnable = &MultiplayerExt::HandleWorldEnable;
constexpr auto HandleEntityNotify = &MultiplayerExt::HandleEntityNotify; constexpr auto HandleEntityNotify = &MultiplayerExt::HandleEntityNotify;
constexpr auto HandleSkyLightControl = &MultiplayerExt::HandleSkyLightControl; constexpr auto HandleSkyLightControl = &MultiplayerExt::HandleSkyLightControl;
constexpr auto HandleROIClick = &MultiplayerExt::HandleROIClick; constexpr auto HandleROIClick = &MultiplayerExt::HandleROIClick;
constexpr auto HandleHornPressed = &MultiplayerExt::HandleHornPressed;
constexpr auto IsClonedCharacter = &MultiplayerExt::IsClonedCharacter; constexpr auto IsClonedCharacter = &MultiplayerExt::IsClonedCharacter;
constexpr auto HandleBeforeSaveLoad = &MultiplayerExt::HandleBeforeSaveLoad; constexpr auto HandleBeforeSaveLoad = &MultiplayerExt::HandleBeforeSaveLoad;
constexpr auto HandleSaveLoaded = &MultiplayerExt::HandleSaveLoaded; constexpr auto HandleSaveLoaded = &MultiplayerExt::HandleSaveLoaded;
@ -79,6 +81,7 @@ constexpr decltype(&MultiplayerExt::HandleWorldEnable) HandleWorldEnable = nullp
constexpr decltype(&MultiplayerExt::HandleEntityNotify) HandleEntityNotify = nullptr; constexpr decltype(&MultiplayerExt::HandleEntityNotify) HandleEntityNotify = nullptr;
constexpr decltype(&MultiplayerExt::HandleSkyLightControl) HandleSkyLightControl = nullptr; constexpr decltype(&MultiplayerExt::HandleSkyLightControl) HandleSkyLightControl = nullptr;
constexpr decltype(&MultiplayerExt::HandleROIClick) HandleROIClick = nullptr; constexpr decltype(&MultiplayerExt::HandleROIClick) HandleROIClick = nullptr;
constexpr decltype(&MultiplayerExt::HandleHornPressed) HandleHornPressed = nullptr;
constexpr decltype(&MultiplayerExt::IsClonedCharacter) IsClonedCharacter = nullptr; constexpr decltype(&MultiplayerExt::IsClonedCharacter) IsClonedCharacter = nullptr;
constexpr decltype(&MultiplayerExt::HandleBeforeSaveLoad) HandleBeforeSaveLoad = nullptr; constexpr decltype(&MultiplayerExt::HandleBeforeSaveLoad) HandleBeforeSaveLoad = nullptr;
constexpr decltype(&MultiplayerExt::HandleSaveLoaded) HandleSaveLoaded = nullptr; constexpr decltype(&MultiplayerExt::HandleSaveLoaded) HandleSaveLoaded = nullptr;

View File

@ -81,6 +81,10 @@ class Loader {
SceneAnimData* EnsureCached(uint32_t p_objectId); SceneAnimData* EnsureCached(uint32_t p_objectId);
void PreloadAsync(uint32_t p_objectId); void PreloadAsync(uint32_t p_objectId);
// Extract just the first WAV audio track from a composite SI object.
// Used for horn sounds from dashboard composites (which have no animation).
SceneAnimData::AudioTrack* EnsureHornCached(uint32_t p_objectId);
private: private:
class PreloadThread : public MxThread { class PreloadThread : public MxThread {
public: public:
@ -104,6 +108,7 @@ class Loader {
si::Interleaf* m_interleaf; si::Interleaf* m_interleaf;
bool m_siReady; bool m_siReady;
std::map<uint32_t, SceneAnimData> m_cache; std::map<uint32_t, SceneAnimData> m_cache;
std::map<uint32_t, SceneAnimData::AudioTrack> m_hornCache;
MxCriticalSection m_cacheCS; MxCriticalSection m_cacheCS;
PreloadThread* m_preloadThread; PreloadThread* m_preloadThread;

View File

@ -65,6 +65,7 @@ class NetworkManager : public MxCore {
void SetWalkAnimation(uint8_t p_walkAnimId); void SetWalkAnimation(uint8_t p_walkAnimId);
void SetIdleAnimation(uint8_t p_idleAnimId); void SetIdleAnimation(uint8_t p_idleAnimId);
void SendEmote(uint8_t p_emoteId); void SendEmote(uint8_t p_emoteId);
void SendHorn(int8_t p_vehicleType);
// Thread-safe request methods for cross-thread callers (e.g. WASM exports // Thread-safe request methods for cross-thread callers (e.g. WASM exports
// running on the browser main thread). Deferred to the game thread in Tickle(). // running on the browser main thread). Deferred to the game thread in Tickle().
@ -128,6 +129,7 @@ class NetworkManager : public MxCore {
void HandleState(const PlayerStateMsg& p_msg); void HandleState(const PlayerStateMsg& p_msg);
void HandleHostAssign(const HostAssignMsg& p_msg); void HandleHostAssign(const HostAssignMsg& p_msg);
void HandleEmote(const EmoteMsg& p_msg); void HandleEmote(const EmoteMsg& p_msg);
void HandleHorn(const HornMsg& p_msg);
void HandleCustomize(const CustomizeMsg& p_msg); void HandleCustomize(const CustomizeMsg& p_msg);
// Animation coordination handlers // Animation coordination handlers
@ -215,6 +217,10 @@ class NetworkManager : public MxCore {
void StopAllPlayback(); void StopAllPlayback();
void UnlockRemotesForAnim(uint16_t p_animIndex); void UnlockRemotesForAnim(uint16_t p_animIndex);
// Horn sound synchronization
void PreloadHornSounds();
void CleanupHornSounds();
// Animation state push // Animation state push
bool m_animStateDirty; bool m_animStateDirty;
bool m_animInterestDirty; bool m_animInterestDirty;
@ -233,6 +239,11 @@ class NetworkManager : public MxCore {
static const uint32_t RECONNECT_MAX_DELAY_MS = 30000; static const uint32_t RECONNECT_MAX_DELAY_MS = 30000;
static const uint32_t RECONNECT_MAX_ATTEMPTS = 10; static const uint32_t RECONNECT_MAX_ATTEMPTS = 10;
static const uint32_t ANIM_PUSH_COOLDOWN_MS = 250; // max ~4Hz for movement-based changes static const uint32_t ANIM_PUSH_COOLDOWN_MS = 250; // max ~4Hz for movement-based changes
// Horn sound data
static const int HORN_VEHICLE_COUNT = 4;
class LegoCacheSound* m_hornTemplates[HORN_VEHICLE_COUNT];
std::vector<class LegoCacheSound*> m_activeHorns;
}; };
} // namespace Multiplayer } // namespace Multiplayer

View File

@ -1,12 +1,12 @@
#pragma once #pragma once
#include "extensions/common/constants.h"
#include <SDL3/SDL_stdinc.h> #include <SDL3/SDL_stdinc.h>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <type_traits> #include <type_traits>
#include "extensions/common/constants.h"
namespace Multiplayer namespace Multiplayer
{ {
@ -30,20 +30,21 @@ enum MessageType : uint8_t {
MSG_ANIM_UPDATE = 13, MSG_ANIM_UPDATE = 13,
MSG_ANIM_START = 14, MSG_ANIM_START = 14,
MSG_ANIM_COMPLETE = 15, MSG_ANIM_COMPLETE = 15,
MSG_HORN = 16,
MSG_ASSIGN_ID = 0xFF MSG_ASSIGN_ID = 0xFF
}; };
using Extensions::Common::VehicleType; using Extensions::Common::VEHICLE_AMBULANCE;
using Extensions::Common::VEHICLE_NONE; using Extensions::Common::VEHICLE_BIKE;
using Extensions::Common::VEHICLE_COUNT;
using Extensions::Common::VEHICLE_DUNEBUGGY;
using Extensions::Common::VEHICLE_HELICOPTER; using Extensions::Common::VEHICLE_HELICOPTER;
using Extensions::Common::VEHICLE_JETSKI; using Extensions::Common::VEHICLE_JETSKI;
using Extensions::Common::VEHICLE_DUNEBUGGY;
using Extensions::Common::VEHICLE_BIKE;
using Extensions::Common::VEHICLE_SKATEBOARD;
using Extensions::Common::VEHICLE_MOTOCYCLE; using Extensions::Common::VEHICLE_MOTOCYCLE;
using Extensions::Common::VEHICLE_NONE;
using Extensions::Common::VEHICLE_SKATEBOARD;
using Extensions::Common::VEHICLE_TOWTRACK; using Extensions::Common::VEHICLE_TOWTRACK;
using Extensions::Common::VEHICLE_AMBULANCE; using Extensions::Common::VehicleType;
using Extensions::Common::VEHICLE_COUNT;
// Entity types for world events // Entity types for world events
enum WorldEntityType : uint8_t { enum WorldEntityType : uint8_t {
@ -53,13 +54,13 @@ enum WorldEntityType : uint8_t {
ENTITY_LIGHT = 3 ENTITY_LIGHT = 3
}; };
using Extensions::Common::WorldChangeType;
using Extensions::Common::CHANGE_VARIANT;
using Extensions::Common::CHANGE_SOUND;
using Extensions::Common::CHANGE_MOVE;
using Extensions::Common::CHANGE_COLOR; using Extensions::Common::CHANGE_COLOR;
using Extensions::Common::CHANGE_MOOD;
using Extensions::Common::CHANGE_DECREMENT; using Extensions::Common::CHANGE_DECREMENT;
using Extensions::Common::CHANGE_MOOD;
using Extensions::Common::CHANGE_MOVE;
using Extensions::Common::CHANGE_SOUND;
using Extensions::Common::CHANGE_VARIANT;
using Extensions::Common::WorldChangeType;
// Change types for ENTITY_SKY // Change types for ENTITY_SKY
enum SkyChangeType : uint8_t { enum SkyChangeType : uint8_t {
@ -196,6 +197,12 @@ struct AnimCompletionParticipant {
char displayName[8]; // 7 chars + null char displayName[8]; // 7 chars + null
}; };
// One-shot horn sound trigger, broadcast to all peers
struct HornMsg {
MessageHeader header;
uint8_t vehicleType; // VehicleType enum value
};
// Host -> All: animation completed successfully (natural completion only, not cancellation) // Host -> All: animation completed successfully (natural completion only, not cancellation)
struct AnimCompleteMsg { struct AnimCompleteMsg {
MessageHeader header; MessageHeader header;

View File

@ -4,6 +4,7 @@
#include "extensions/common/customizestate.h" #include "extensions/common/customizestate.h"
#include "extensions/multiplayer/animation/catalog.h" #include "extensions/multiplayer/animation/catalog.h"
#include "extensions/multiplayer/protocol.h" #include "extensions/multiplayer/protocol.h"
#include "mxgeometry/mxmatrix.h"
#include "mxtypes.h" #include "mxtypes.h"
#include <cstdint> #include <cstdint>
@ -103,6 +104,8 @@ class RemotePlayer {
Extensions::Common::CharacterAnimator m_animator; Extensions::Common::CharacterAnimator m_animator;
LegoROI* m_vehicleROI; LegoROI* m_vehicleROI;
bool m_vehicleROICloned;
std::vector<MxMatrix> m_vehicleChildOffsets; // child-to-parent local offsets for cloned hierarchical ROIs
NameBubbleRenderer* m_nameBubble; NameBubbleRenderer* m_nameBubble;

View File

@ -328,6 +328,7 @@ LegoROI* AnimUtils::DeepCloneROI(LegoROI* p_source, const char* p_name)
clone->SetName(p_name); clone->SetName(p_name);
clone->SetBoundingSphere(p_source->GetBoundingSphere()); clone->SetBoundingSphere(p_source->GetBoundingSphere());
clone->WrappedSetLocal2WorldWithWorldDataUpdate(p_source->GetLocal2World());
const CompoundObject* children = p_source->GetComp(); const CompoundObject* children = p_source->GetComp();
if (children && !children->empty()) { if (children && !children->empty()) {
@ -346,6 +347,62 @@ LegoROI* AnimUtils::DeepCloneROI(LegoROI* p_source, const char* p_name)
return clone; return clone;
} }
// Inverse of an orthonormal affine matrix (rotation + translation).
// R^-1 = R^T, t^-1 = -R^T * t.
static void InvertOrthonormal(MxMatrix& p_out, const MxMatrix& p_in)
{
p_out.SetIdentity();
for (int r = 0; r < 3; r++) {
for (int c = 0; c < 3; c++) {
p_out[r][c] = p_in[c][r];
}
}
for (int c = 0; c < 3; c++) {
p_out[3][c] = -(p_in[3][0] * p_out[0][c] + p_in[3][1] * p_out[1][c] + p_in[3][2] * p_out[2][c]);
}
}
std::vector<MxMatrix> AnimUtils::ComputeChildOffsets(LegoROI* p_parent)
{
std::vector<MxMatrix> offsets;
const CompoundObject* children = p_parent->GetComp();
if (!children) {
return offsets;
}
MxMatrix parentInv;
InvertOrthonormal(parentInv, p_parent->GetLocal2World());
for (auto it = children->begin(); it != children->end(); it++) {
MxMatrix offset;
offset.Product(parentInv, ((LegoROI*) *it)->GetLocal2World());
offsets.push_back(offset);
}
return offsets;
}
void AnimUtils::ApplyHierarchyTransform(
LegoROI* p_parent,
const MxMatrix& p_transform,
const std::vector<MxMatrix>& p_offsets
)
{
p_parent->WrappedSetLocal2WorldWithWorldDataUpdate(p_transform);
const CompoundObject* children = p_parent->GetComp();
if (!children) {
return;
}
size_t i = 0;
for (auto it = children->begin(); it != children->end() && i < p_offsets.size(); it++, i++) {
MxMatrix childWorld;
childWorld.Product(p_transform, p_offsets[i]);
((LegoROI*) *it)->WrappedSetLocal2WorldWithWorldDataUpdate(childWorld);
}
}
std::string AnimUtils::TrimLODSuffix(const std::string& p_name) std::string AnimUtils::TrimLODSuffix(const std::string& p_name)
{ {
std::string result(p_name); std::string result(p_name);

View File

@ -265,6 +265,33 @@ MxBool MultiplayerExt::CheckRejected()
return FALSE; return FALSE;
} }
void MultiplayerExt::HandleHornPressed(MxU32 p_controlId)
{
if (!s_networkManager) {
return;
}
int8_t vehicleType;
switch (p_controlId) {
case IsleScript::c_BikeHorn_Ctl:
vehicleType = Multiplayer::VEHICLE_BIKE;
break;
case IsleScript::c_AmbulanceHorn_Ctl:
vehicleType = Multiplayer::VEHICLE_AMBULANCE;
break;
case IsleScript::c_TowHorn_Ctl:
vehicleType = Multiplayer::VEHICLE_TOWTRACK;
break;
case IsleScript::c_DuneCarHorn_Ctl:
vehicleType = Multiplayer::VEHICLE_DUNEBUGGY;
break;
default:
return;
}
s_networkManager->SendHorn(vehicleType);
}
Multiplayer::NetworkManager* MultiplayerExt::GetNetworkManager() Multiplayer::NetworkManager* MultiplayerExt::GetNetworkManager()
{ {
return s_networkManager; return s_networkManager;

View File

@ -104,6 +104,9 @@ Loader::Loader()
Loader::~Loader() Loader::~Loader()
{ {
CleanupPreloadThread(); CleanupPreloadThread();
for (auto& [id, track] : m_hornCache) {
delete[] track.pcmData;
}
delete m_interleaf; delete m_interleaf;
delete m_siFile; delete m_siFile;
} }
@ -440,3 +443,46 @@ MxResult Loader::PreloadThread::Run()
return MxThread::Run(); return MxThread::Run();
} }
SceneAnimData::AudioTrack* Loader::EnsureHornCached(uint32_t p_objectId)
{
{
AUTOLOCK(m_cacheCS);
auto it = m_hornCache.find(p_objectId);
if (it != m_hornCache.end()) {
return &it->second;
}
}
if (!OpenSI()) {
return nullptr;
}
if (!ReadObject(p_objectId)) {
return nullptr;
}
si::Object* composite = static_cast<si::Object*>(m_interleaf->GetChildAt(p_objectId));
// Find the first WAV child in the composite (the horn sound)
for (size_t i = 0; i < composite->GetChildCount(); i++) {
si::Object* child = static_cast<si::Object*>(composite->GetChildAt(i));
if (child->filetype() == si::MxOb::WAV) {
SceneAnimData data;
if (ParseSoundChild(child, data)) {
// Take ownership of the PCM buffer before data's destructor frees it.
// AudioTrack has a raw pointer, so std::move alone doesn't transfer ownership.
SceneAnimData::AudioTrack track = data.audioTracks[0];
data.audioTracks[0].pcmData = nullptr;
AUTOLOCK(m_cacheCS);
auto result = m_hornCache.emplace(p_objectId, track);
return &result.first->second;
}
break;
}
}
return nullptr;
}

View File

@ -1,5 +1,6 @@
#include "extensions/multiplayer/networkmanager.h" #include "extensions/multiplayer/networkmanager.h"
#include "actions/isle_actions.h"
#include "extensions/common/arearestriction.h" #include "extensions/common/arearestriction.h"
#include "extensions/common/charactercustomizer.h" #include "extensions/common/charactercustomizer.h"
#include "extensions/common/charactertables.h" #include "extensions/common/charactertables.h"
@ -8,6 +9,7 @@
#include "extensions/thirdpersoncamera/controller.h" #include "extensions/thirdpersoncamera/controller.h"
#include "legoactor.h" #include "legoactor.h"
#include "legoanimationmanager.h" #include "legoanimationmanager.h"
#include "legocachsound.h"
#include "legocharactermanager.h" #include "legocharactermanager.h"
#include "legoextraactor.h" #include "legoextraactor.h"
#include "legogamestate.h" #include "legogamestate.h"
@ -69,7 +71,7 @@ NetworkManager::NetworkManager()
m_pendingAnimInterest(-1), m_pendingAnimCancel(false), m_localPendingAnimInterest(-1), m_showNameBubbles(true), m_pendingAnimInterest(-1), m_pendingAnimCancel(false), m_localPendingAnimInterest(-1), m_showNameBubbles(true),
m_lastCameraEnabled(false), m_lastVehicleState(0), m_wasInRestrictedArea(false), m_animStateDirty(false), m_lastCameraEnabled(false), m_lastVehicleState(0), m_wasInRestrictedArea(false), m_animStateDirty(false),
m_animInterestDirty(false), m_lastAnimPushTime(0), m_connectionState(STATE_DISCONNECTED), m_wasRejected(false), m_animInterestDirty(false), m_lastAnimPushTime(0), m_connectionState(STATE_DISCONNECTED), m_wasRejected(false),
m_reconnectAttempt(0), m_reconnectDelay(0), m_nextReconnectTime(0) m_reconnectAttempt(0), m_reconnectDelay(0), m_nextReconnectTime(0), m_hornTemplates{}, m_activeHorns()
{ {
} }
@ -263,6 +265,8 @@ void NetworkManager::Shutdown()
m_worldSync.SetTransport(nullptr); m_worldSync.SetTransport(nullptr);
} }
CleanupHornSounds();
delete m_localNameBubble; delete m_localNameBubble;
m_localNameBubble = nullptr; m_localNameBubble = nullptr;
@ -379,6 +383,7 @@ void NetworkManager::OnWorldEnabled(LegoWorld* p_world)
} }
m_locationProximity.Reset(); m_locationProximity.Reset();
PreloadHornSounds();
} }
} }
@ -393,6 +398,8 @@ void NetworkManager::OnWorldDisabled(LegoWorld* p_world)
m_wasInRestrictedArea = false; m_wasInRestrictedArea = false;
m_worldSync.SetInIsleWorld(false); m_worldSync.SetInIsleWorld(false);
CleanupHornSounds();
// Stop animation before ROIs are destroyed (calls ResetAnimationState) // Stop animation before ROIs are destroyed (calls ResetAnimationState)
StopAnimation(); StopAnimation();
m_animStateDirty = false; // override: we push explicit empty JSON below m_animStateDirty = false; // override: we push explicit empty JSON below
@ -830,6 +837,13 @@ void NetworkManager::ProcessIncomingPackets()
} }
break; break;
} }
case MSG_HORN: {
HornMsg msg;
if (DeserializeMsg(data, length, msg) && msg.header.type == MSG_HORN) {
HandleHorn(msg);
}
break;
}
case MSG_CUSTOMIZE: { case MSG_CUSTOMIZE: {
CustomizeMsg msg; CustomizeMsg msg;
if (DeserializeMsg(data, length, msg) && msg.header.type == MSG_CUSTOMIZE) { if (DeserializeMsg(data, length, msg) && msg.header.type == MSG_CUSTOMIZE) {
@ -1080,6 +1094,105 @@ void NetworkManager::HandleEmote(const EmoteMsg& p_msg)
} }
} }
void NetworkManager::SendHorn(int8_t p_vehicleType)
{
if (!IsConnected() || !m_inIsleWorld) {
return;
}
HornMsg msg{};
msg.header = {MSG_HORN, 0, m_localPeerId, m_sequence++, TARGET_BROADCAST};
msg.vehicleType = static_cast<uint8_t>(p_vehicleType);
SendMessage(msg);
}
void NetworkManager::HandleHorn(const HornMsg& p_msg)
{
// Sweep finished horn sounds
for (auto it = m_activeHorns.begin(); it != m_activeHorns.end();) {
if (!ma_sound_is_playing((*it)->m_cacheSound)) {
(*it)->Stop();
delete *it;
it = m_activeHorns.erase(it);
}
else {
++it;
}
}
uint32_t peerId = p_msg.header.peerId;
auto it = m_remotePlayers.find(peerId);
if (it == m_remotePlayers.end()) {
return;
}
// Map vehicle type to horn template index
static const int8_t hornVehicles[] = {VEHICLE_BIKE, VEHICLE_AMBULANCE, VEHICLE_TOWTRACK, VEHICLE_DUNEBUGGY};
int templateIdx = -1;
for (int i = 0; i < HORN_VEHICLE_COUNT; i++) {
if (hornVehicles[i] == static_cast<int8_t>(p_msg.vehicleType)) {
templateIdx = i;
break;
}
}
if (templateIdx < 0 || !m_hornTemplates[templateIdx]) {
return;
}
LegoCacheSound* horn = m_hornTemplates[templateIdx]->Clone();
if (horn) {
ma_sound_set_doppler_factor(horn->m_cacheSound, 0);
horn->Play(it->second->GetUniqueName(), FALSE);
m_activeHorns.push_back(horn);
}
}
// Dashboard composite IDs that contain horn WAV children
static const uint32_t g_hornDashboardIds[4] = {
IsleScript::c_BikeDashboard,
IsleScript::c_AmbulanceDashboard,
IsleScript::c_TowTrackDashboard,
IsleScript::c_DuneCarDashboard,
};
void NetworkManager::PreloadHornSounds()
{
for (int i = 0; i < HORN_VEHICLE_COUNT; i++) {
m_hornTemplates[i] = nullptr;
Animation::SceneAnimData::AudioTrack* track = m_animLoader.EnsureHornCached(g_hornDashboardIds[i]);
if (!track) {
continue;
}
LegoCacheSound* sound = new LegoCacheSound();
MxString mediaSrcPath(track->mediaSrcPath.c_str());
MxWavePresenter::WaveFormat format = track->format;
if (sound->Create(format, mediaSrcPath, track->volume, track->pcmData, track->pcmDataSize) == SUCCESS) {
ma_sound_set_doppler_factor(sound->m_cacheSound, 0);
m_hornTemplates[i] = sound;
}
else {
delete sound;
}
}
}
void NetworkManager::CleanupHornSounds()
{
for (auto* horn : m_activeHorns) {
horn->Stop();
delete horn;
}
m_activeHorns.clear();
for (int i = 0; i < HORN_VEHICLE_COUNT; i++) {
delete m_hornTemplates[i];
m_hornTemplates[i] = nullptr;
}
}
void NetworkManager::RemoveRemotePlayer(uint32_t p_peerId) void NetworkManager::RemoveRemotePlayer(uint32_t p_peerId)
{ {
auto it = m_remotePlayers.find(p_peerId); auto it = m_remotePlayers.find(p_peerId);
@ -2185,7 +2298,8 @@ void NetworkManager::PushAnimationState()
if (player->IsAtLocation(loc)) { if (player->IsAtLocation(loc)) {
int8_t charIdx = Animation::Catalog::DisplayActorToCharacterIndex(player->GetDisplayActorIndex()); int8_t charIdx = Animation::Catalog::DisplayActorToCharacterIndex(player->GetDisplayActorIndex());
locationCharIndices.push_back(charIdx); locationCharIndices.push_back(charIdx);
locationVehicleState.push_back(Animation::Catalog::GetVehicleState(charIdx, player->GetRideVehicleROI())); locationVehicleState.push_back(Animation::Catalog::GetVehicleState(charIdx, player->GetRideVehicleROI())
);
} }
} }

View File

@ -1,6 +1,7 @@
#include "extensions/multiplayer/remoteplayer.h" #include "extensions/multiplayer/remoteplayer.h"
#include "3dmanager/lego3dmanager.h" #include "3dmanager/lego3dmanager.h"
#include "extensions/common/animutils.h"
#include "extensions/common/arearestriction.h" #include "extensions/common/arearestriction.h"
#include "extensions/common/charactercloner.h" #include "extensions/common/charactercloner.h"
#include "extensions/common/charactercustomizer.h" #include "extensions/common/charactercustomizer.h"
@ -32,7 +33,7 @@ RemotePlayer::RemotePlayer(uint32_t p_peerId, uint8_t p_actorId, uint8_t p_displ
m_spawned(false), m_visible(false), m_targetSpeed(0.0f), m_targetVehicleType(VEHICLE_NONE), m_spawned(false), m_visible(false), m_targetSpeed(0.0f), m_targetVehicleType(VEHICLE_NONE),
m_targetWorldId(WORLD_NOT_VISIBLE), m_lastUpdateTime(SDL_GetTicks()), m_hasReceivedUpdate(false), m_targetWorldId(WORLD_NOT_VISIBLE), m_lastUpdateTime(SDL_GetTicks()), m_hasReceivedUpdate(false),
m_animator(Common::CharacterAnimatorConfig{/*.saveEmoteTransform=*/false, /*.propSuffix=*/p_peerId}), m_animator(Common::CharacterAnimatorConfig{/*.saveEmoteTransform=*/false, /*.propSuffix=*/p_peerId}),
m_vehicleROI(nullptr), m_nameBubble(nullptr), m_allowRemoteCustomize(true), m_vehicleROI(nullptr), m_vehicleROICloned(false), m_nameBubble(nullptr), m_allowRemoteCustomize(true),
m_lockedForAnimIndex(Animation::ANIM_INDEX_NONE) m_lockedForAnimIndex(Animation::ANIM_INDEX_NONE)
{ {
m_displayName[0] = '\0'; m_displayName[0] = '\0';
@ -307,7 +308,12 @@ void RemotePlayer::UpdateTransform(float p_deltaTime)
if (m_vehicleROI && m_animator.GetCurrentVehicleType() != VEHICLE_NONE && if (m_vehicleROI && m_animator.GetCurrentVehicleType() != VEHICLE_NONE &&
IsLargeVehicle(m_animator.GetCurrentVehicleType())) { IsLargeVehicle(m_animator.GetCurrentVehicleType())) {
if (m_vehicleROICloned && !m_vehicleChildOffsets.empty()) {
Common::AnimUtils::ApplyHierarchyTransform(m_vehicleROI, mat, m_vehicleChildOffsets);
}
else {
m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat); m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat);
}
VideoManager()->Get3DManager()->Moved(*m_vehicleROI); VideoManager()->Get3DManager()->Moved(*m_vehicleROI);
} }
} }
@ -338,10 +344,30 @@ void RemotePlayer::EnterVehicle(int8_t p_vehicleType)
SDL_snprintf(vehicleName, sizeof(vehicleName), "%s_mp_%u", g_vehicleROINames[p_vehicleType], m_peerId); SDL_snprintf(vehicleName, sizeof(vehicleName), "%s_mp_%u", g_vehicleROINames[p_vehicleType], m_peerId);
m_vehicleROI = CharacterManager()->CreateAutoROI(vehicleName, g_vehicleROINames[p_vehicleType], FALSE); m_vehicleROI = CharacterManager()->CreateAutoROI(vehicleName, g_vehicleROINames[p_vehicleType], FALSE);
if (!m_vehicleROI) {
// Fallback for hierarchical models whose root has 0 LODs
// and cannot be created via CreateAutoROI. Deep-clone the world's existing ROI.
LegoROI* source = FindROI(g_vehicleROINames[p_vehicleType]);
if (source) {
m_vehicleROI = Common::AnimUtils::DeepCloneROI(source, vehicleName);
if (m_vehicleROI) {
VideoManager()->Get3DManager()->Add(*m_vehicleROI);
m_vehicleROICloned = true;
}
}
}
if (m_vehicleROI) { if (m_vehicleROI) {
m_roi->SetVisibility(FALSE); m_roi->SetVisibility(FALSE);
MxMatrix mat(m_roi->GetLocal2World()); MxMatrix mat(m_roi->GetLocal2World());
if (m_vehicleROICloned) {
m_vehicleChildOffsets = Common::AnimUtils::ComputeChildOffsets(m_vehicleROI);
Common::AnimUtils::ApplyHierarchyTransform(m_vehicleROI, mat, m_vehicleChildOffsets);
}
else {
m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat); m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat);
}
m_vehicleROI->SetVisibility(m_visible ? TRUE : FALSE); m_vehicleROI->SetVisibility(m_visible ? TRUE : FALSE);
} }
} }
@ -358,8 +384,15 @@ void RemotePlayer::ExitVehicle()
if (m_vehicleROI) { if (m_vehicleROI) {
VideoManager()->Get3DManager()->Remove(*m_vehicleROI); VideoManager()->Get3DManager()->Remove(*m_vehicleROI);
if (m_vehicleROICloned) {
delete m_vehicleROI;
}
else {
CharacterManager()->ReleaseAutoROI(m_vehicleROI); CharacterManager()->ReleaseAutoROI(m_vehicleROI);
}
m_vehicleROI = nullptr; m_vehicleROI = nullptr;
m_vehicleROICloned = false;
m_vehicleChildOffsets.clear();
} }
m_animator.ClearRideAnimation(); m_animator.ClearRideAnimation();