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:
friend class Multiplayer::Animation::AudioPlayer;
friend class Multiplayer::NetworkManager;
void Init();
void CopyData(MxU8* p_data, MxU32 p_dataSize);

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
#include "towtrack.h"
#include "extensions/multiplayer.h"
#include "isle.h"
#include "isle_actions.h"
#include "jukebox_actions.h"
@ -22,6 +23,8 @@
#include <stdio.h>
using namespace Extensions;
DECOMP_SIZE_ASSERT(TowTrack, 0x180)
DECOMP_SIZE_ASSERT(TowTrackMissionState, 0x28)
@ -502,6 +505,7 @@ MxLong TowTrack::HandleControl(LegoControlManagerNotificationParam& p_param)
case IsleScript::c_TowHorn_Ctl:
MxSoundPresenter* presenter = (MxSoundPresenter*) CurrentWorld()->Find("MxSoundPresenter", "TowHorn_Sound");
presenter->Enable(p_param.m_enabledChild);
Extension<MultiplayerExt>::Call(MP::HandleHornPressed, (MxU32) p_param.m_clickedObjectId);
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.
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.
// Mirrors the digit-trimming in LegoAnimPresenter::CreateManagedActors/CreateSceneROIs.
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 bool enabled;
static void HandleHornPressed(MxU32 p_controlId);
static MxBool IsClonedCharacter(const char* p_name);
static void HandleBeforeSaveLoad();
static void HandleSaveLoaded();
@ -69,6 +70,7 @@ constexpr auto HandleWorldEnable = &MultiplayerExt::HandleWorldEnable;
constexpr auto HandleEntityNotify = &MultiplayerExt::HandleEntityNotify;
constexpr auto HandleSkyLightControl = &MultiplayerExt::HandleSkyLightControl;
constexpr auto HandleROIClick = &MultiplayerExt::HandleROIClick;
constexpr auto HandleHornPressed = &MultiplayerExt::HandleHornPressed;
constexpr auto IsClonedCharacter = &MultiplayerExt::IsClonedCharacter;
constexpr auto HandleBeforeSaveLoad = &MultiplayerExt::HandleBeforeSaveLoad;
constexpr auto HandleSaveLoaded = &MultiplayerExt::HandleSaveLoaded;
@ -79,6 +81,7 @@ constexpr decltype(&MultiplayerExt::HandleWorldEnable) HandleWorldEnable = nullp
constexpr decltype(&MultiplayerExt::HandleEntityNotify) HandleEntityNotify = nullptr;
constexpr decltype(&MultiplayerExt::HandleSkyLightControl) HandleSkyLightControl = nullptr;
constexpr decltype(&MultiplayerExt::HandleROIClick) HandleROIClick = nullptr;
constexpr decltype(&MultiplayerExt::HandleHornPressed) HandleHornPressed = nullptr;
constexpr decltype(&MultiplayerExt::IsClonedCharacter) IsClonedCharacter = nullptr;
constexpr decltype(&MultiplayerExt::HandleBeforeSaveLoad) HandleBeforeSaveLoad = nullptr;
constexpr decltype(&MultiplayerExt::HandleSaveLoaded) HandleSaveLoaded = nullptr;

View File

@ -81,6 +81,10 @@ class Loader {
SceneAnimData* EnsureCached(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:
class PreloadThread : public MxThread {
public:
@ -104,6 +108,7 @@ class Loader {
si::Interleaf* m_interleaf;
bool m_siReady;
std::map<uint32_t, SceneAnimData> m_cache;
std::map<uint32_t, SceneAnimData::AudioTrack> m_hornCache;
MxCriticalSection m_cacheCS;
PreloadThread* m_preloadThread;

View File

@ -65,6 +65,7 @@ class NetworkManager : public MxCore {
void SetWalkAnimation(uint8_t p_walkAnimId);
void SetIdleAnimation(uint8_t p_idleAnimId);
void SendEmote(uint8_t p_emoteId);
void SendHorn(int8_t p_vehicleType);
// 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().
@ -128,6 +129,7 @@ class NetworkManager : public MxCore {
void HandleState(const PlayerStateMsg& p_msg);
void HandleHostAssign(const HostAssignMsg& p_msg);
void HandleEmote(const EmoteMsg& p_msg);
void HandleHorn(const HornMsg& p_msg);
void HandleCustomize(const CustomizeMsg& p_msg);
// Animation coordination handlers
@ -215,6 +217,10 @@ class NetworkManager : public MxCore {
void StopAllPlayback();
void UnlockRemotesForAnim(uint16_t p_animIndex);
// Horn sound synchronization
void PreloadHornSounds();
void CleanupHornSounds();
// Animation state push
bool m_animStateDirty;
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_ATTEMPTS = 10;
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

View File

@ -1,12 +1,12 @@
#pragma once
#include "extensions/common/constants.h"
#include <SDL3/SDL_stdinc.h>
#include <cstddef>
#include <cstdint>
#include <type_traits>
#include "extensions/common/constants.h"
namespace Multiplayer
{
@ -30,20 +30,21 @@ enum MessageType : uint8_t {
MSG_ANIM_UPDATE = 13,
MSG_ANIM_START = 14,
MSG_ANIM_COMPLETE = 15,
MSG_HORN = 16,
MSG_ASSIGN_ID = 0xFF
};
using Extensions::Common::VehicleType;
using Extensions::Common::VEHICLE_NONE;
using Extensions::Common::VEHICLE_AMBULANCE;
using Extensions::Common::VEHICLE_BIKE;
using Extensions::Common::VEHICLE_COUNT;
using Extensions::Common::VEHICLE_DUNEBUGGY;
using Extensions::Common::VEHICLE_HELICOPTER;
using Extensions::Common::VEHICLE_JETSKI;
using Extensions::Common::VEHICLE_DUNEBUGGY;
using Extensions::Common::VEHICLE_BIKE;
using Extensions::Common::VEHICLE_SKATEBOARD;
using Extensions::Common::VEHICLE_MOTOCYCLE;
using Extensions::Common::VEHICLE_NONE;
using Extensions::Common::VEHICLE_SKATEBOARD;
using Extensions::Common::VEHICLE_TOWTRACK;
using Extensions::Common::VEHICLE_AMBULANCE;
using Extensions::Common::VEHICLE_COUNT;
using Extensions::Common::VehicleType;
// Entity types for world events
enum WorldEntityType : uint8_t {
@ -53,13 +54,13 @@ enum WorldEntityType : uint8_t {
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_MOOD;
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
enum SkyChangeType : uint8_t {
@ -177,9 +178,9 @@ struct AnimSlotAssignment {
struct AnimUpdateMsg {
MessageHeader header;
uint16_t animIndex;
uint8_t state; // CoordinationState (0=cleared, 1=gathering, 2=countdown, 3=playing)
uint16_t countdownMs; // Remaining countdown ms (0 if not counting)
uint8_t slotCount; // Number of valid slot entries
uint8_t state; // CoordinationState (0=cleared, 1=gathering, 2=countdown, 3=playing)
uint16_t countdownMs; // Remaining countdown ms (0 if not counting)
uint8_t slotCount; // Number of valid slot entries
AnimSlotAssignment slots[8]; // peerId per slot (0 = unfilled)
};
@ -196,11 +197,17 @@ struct AnimCompletionParticipant {
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)
struct AnimCompleteMsg {
MessageHeader header;
uint64_t eventId; // Random 64-bit ID unique to this completion event
uint32_t objectId; // SI file object ID (stable, used as frontend key)
uint64_t eventId; // Random 64-bit ID unique to this completion event
uint32_t objectId; // SI file object ID (stable, used as frontend key)
uint8_t participantCount;
AnimCompletionParticipant participants[8];
};

View File

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

View File

@ -328,6 +328,7 @@ LegoROI* AnimUtils::DeepCloneROI(LegoROI* p_source, const char* p_name)
clone->SetName(p_name);
clone->SetBoundingSphere(p_source->GetBoundingSphere());
clone->WrappedSetLocal2WorldWithWorldDataUpdate(p_source->GetLocal2World());
const CompoundObject* children = p_source->GetComp();
if (children && !children->empty()) {
@ -346,6 +347,62 @@ LegoROI* AnimUtils::DeepCloneROI(LegoROI* p_source, const char* p_name)
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 result(p_name);

View File

@ -265,6 +265,33 @@ MxBool MultiplayerExt::CheckRejected()
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()
{
return s_networkManager;

View File

@ -104,6 +104,9 @@ Loader::Loader()
Loader::~Loader()
{
CleanupPreloadThread();
for (auto& [id, track] : m_hornCache) {
delete[] track.pcmData;
}
delete m_interleaf;
delete m_siFile;
}
@ -440,3 +443,46 @@ MxResult Loader::PreloadThread::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 "actions/isle_actions.h"
#include "extensions/common/arearestriction.h"
#include "extensions/common/charactercustomizer.h"
#include "extensions/common/charactertables.h"
@ -8,6 +9,7 @@
#include "extensions/thirdpersoncamera/controller.h"
#include "legoactor.h"
#include "legoanimationmanager.h"
#include "legocachsound.h"
#include "legocharactermanager.h"
#include "legoextraactor.h"
#include "legogamestate.h"
@ -69,7 +71,7 @@ NetworkManager::NetworkManager()
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_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);
}
CleanupHornSounds();
delete m_localNameBubble;
m_localNameBubble = nullptr;
@ -379,6 +383,7 @@ void NetworkManager::OnWorldEnabled(LegoWorld* p_world)
}
m_locationProximity.Reset();
PreloadHornSounds();
}
}
@ -393,6 +398,8 @@ void NetworkManager::OnWorldDisabled(LegoWorld* p_world)
m_wasInRestrictedArea = false;
m_worldSync.SetInIsleWorld(false);
CleanupHornSounds();
// Stop animation before ROIs are destroyed (calls ResetAnimationState)
StopAnimation();
m_animStateDirty = false; // override: we push explicit empty JSON below
@ -830,6 +837,13 @@ void NetworkManager::ProcessIncomingPackets()
}
break;
}
case MSG_HORN: {
HornMsg msg;
if (DeserializeMsg(data, length, msg) && msg.header.type == MSG_HORN) {
HandleHorn(msg);
}
break;
}
case MSG_CUSTOMIZE: {
CustomizeMsg msg;
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)
{
auto it = m_remotePlayers.find(p_peerId);
@ -2185,7 +2298,8 @@ void NetworkManager::PushAnimationState()
if (player->IsAtLocation(loc)) {
int8_t charIdx = Animation::Catalog::DisplayActorToCharacterIndex(player->GetDisplayActorIndex());
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 "3dmanager/lego3dmanager.h"
#include "extensions/common/animutils.h"
#include "extensions/common/arearestriction.h"
#include "extensions/common/charactercloner.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_targetWorldId(WORLD_NOT_VISIBLE), m_lastUpdateTime(SDL_GetTicks()), m_hasReceivedUpdate(false),
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_displayName[0] = '\0';
@ -307,7 +308,12 @@ void RemotePlayer::UpdateTransform(float p_deltaTime)
if (m_vehicleROI && m_animator.GetCurrentVehicleType() != VEHICLE_NONE &&
IsLargeVehicle(m_animator.GetCurrentVehicleType())) {
m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat);
if (m_vehicleROICloned && !m_vehicleChildOffsets.empty()) {
Common::AnimUtils::ApplyHierarchyTransform(m_vehicleROI, mat, m_vehicleChildOffsets);
}
else {
m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat);
}
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);
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) {
m_roi->SetVisibility(FALSE);
MxMatrix mat(m_roi->GetLocal2World());
m_vehicleROI->WrappedSetLocal2WorldWithWorldDataUpdate(mat);
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->SetVisibility(m_visible ? TRUE : FALSE);
}
}
@ -358,8 +384,15 @@ void RemotePlayer::ExitVehicle()
if (m_vehicleROI) {
VideoManager()->Get3DManager()->Remove(*m_vehicleROI);
CharacterManager()->ReleaseAutoROI(m_vehicleROI);
if (m_vehicleROICloned) {
delete m_vehicleROI;
}
else {
CharacterManager()->ReleaseAutoROI(m_vehicleROI);
}
m_vehicleROI = nullptr;
m_vehicleROICloned = false;
m_vehicleChildOffsets.clear();
}
m_animator.ClearRideAnimation();