mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-01 18:13:57 +00:00
WIP
This commit is contained in:
parent
e57164d345
commit
05716eb94f
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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];
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user