mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-02 02:23:56 +00:00
Fix animation system to work when host is outside ISLE world
- Move TickHostSessions outside m_inIsleWorld gate so the host can coordinate animations from any world - Load animation catalog early in HandleCreate so the host can coordinate before entering the ISLE world - Use network-reported positions for remote player location detection instead of requiring spawned ROIs - Always erase sessions at launch — the host's job ends when the animation starts; clients play and complete independently - Replace BroadcastAnimComplete with locally-driven completion callbacks: host generates eventId at launch, clients cache completion JSON at start time, fire it when ScenePlayer finishes - Make StopAnimation only do local cleanup (stop playback, cancel own interest, reset coordinator) without destroying the session host, so other players' sessions survive world transitions - Broadcast state=0 in ResetAnimationState for full teardown paths (shutdown, reconnect, host migration) so clients aren't left with stale session state
This commit is contained in:
parent
90d11d98e0
commit
11bf290396
@ -15,7 +15,8 @@ struct SessionSlot {
|
|||||||
uint32_t peerId; // 0 = unfilled
|
uint32_t peerId; // 0 = unfilled
|
||||||
int8_t charIndex; // g_actorInfoInit index, or -1 for spectator
|
int8_t charIndex; // g_actorInfoInit index, or -1 for spectator
|
||||||
|
|
||||||
bool IsSpectator() const { return charIndex < 0; }
|
bool IsSpectator() const { return IsSpectatorCharIndex(charIndex); }
|
||||||
|
static bool IsSpectatorCharIndex(int8_t p_charIndex) { return p_charIndex < 0; }
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AnimSession {
|
struct AnimSession {
|
||||||
|
|||||||
@ -137,13 +137,11 @@ class NetworkManager : public MxCore {
|
|||||||
void HandleAnimCancel(uint32_t p_peerId);
|
void HandleAnimCancel(uint32_t p_peerId);
|
||||||
void HandleAnimUpdate(const AnimUpdateMsg& p_msg);
|
void HandleAnimUpdate(const AnimUpdateMsg& p_msg);
|
||||||
void HandleAnimStart(const AnimStartMsg& p_msg);
|
void HandleAnimStart(const AnimStartMsg& p_msg);
|
||||||
void HandleAnimStartLocally(uint16_t p_animIndex, bool p_localInSession);
|
void HandleAnimStartLocally(uint16_t p_animIndex, bool p_localInSession, uint64_t p_eventId);
|
||||||
AnimUpdateMsg BuildAnimUpdateMsg(uint16_t p_animIndex, uint32_t p_target);
|
AnimUpdateMsg BuildAnimUpdateMsg(uint16_t p_animIndex, uint32_t p_target);
|
||||||
void BroadcastAnimUpdate(uint16_t p_animIndex);
|
void BroadcastAnimUpdate(uint16_t p_animIndex);
|
||||||
void SendAnimUpdateToPlayer(uint16_t p_animIndex, uint32_t p_targetPeerId);
|
void SendAnimUpdateToPlayer(uint16_t p_animIndex, uint32_t p_targetPeerId);
|
||||||
void BroadcastAnimStart(uint16_t p_animIndex);
|
void BroadcastAnimStart(uint16_t p_animIndex, uint64_t p_eventId);
|
||||||
void BroadcastAnimComplete(uint16_t p_animIndex);
|
|
||||||
void HandleAnimComplete(const AnimCompleteMsg& p_msg);
|
|
||||||
bool IsPeerAtLocation(uint32_t p_peerId, int16_t p_location) const;
|
bool IsPeerAtLocation(uint32_t p_peerId, int16_t p_location) const;
|
||||||
bool GetPeerPosition(uint32_t p_peerId, float& p_x, float& p_z) const;
|
bool GetPeerPosition(uint32_t p_peerId, float& p_x, float& p_z) const;
|
||||||
bool IsPeerNearby(uint32_t p_peerId, float p_refX, float p_refZ) const;
|
bool IsPeerNearby(uint32_t p_peerId, float p_refX, float p_refZ) const;
|
||||||
@ -216,6 +214,11 @@ class NetworkManager : public MxCore {
|
|||||||
// Concurrent animation playback: one ScenePlayer per playing animation
|
// Concurrent animation playback: one ScenePlayer per playing animation
|
||||||
std::map<uint16_t, std::unique_ptr<Multiplayer::Animation::ScenePlayer>> m_playingAnims;
|
std::map<uint16_t, std::unique_ptr<Multiplayer::Animation::ScenePlayer>> m_playingAnims;
|
||||||
|
|
||||||
|
// Pre-built completion JSON per playing animation (non-observer participants only).
|
||||||
|
// Cached at animation start so it survives host migration/dropout.
|
||||||
|
std::map<uint16_t, std::string> m_pendingCompletionJson;
|
||||||
|
std::string BuildCompletionJson(uint16_t p_animIndex, uint64_t p_eventId);
|
||||||
|
|
||||||
void TickAnimation();
|
void TickAnimation();
|
||||||
void StopScenePlayback(uint16_t p_animIndex, bool p_unlockRemotes);
|
void StopScenePlayback(uint16_t p_animIndex, bool p_unlockRemotes);
|
||||||
void StopAllPlayback();
|
void StopAllPlayback();
|
||||||
|
|||||||
@ -32,7 +32,6 @@ enum MessageType : uint8_t {
|
|||||||
MSG_ANIM_CANCEL = 12,
|
MSG_ANIM_CANCEL = 12,
|
||||||
MSG_ANIM_UPDATE = 13,
|
MSG_ANIM_UPDATE = 13,
|
||||||
MSG_ANIM_START = 14,
|
MSG_ANIM_START = 14,
|
||||||
MSG_ANIM_COMPLETE = 15,
|
|
||||||
MSG_HORN = 16,
|
MSG_HORN = 16,
|
||||||
MSG_ASSIGN_ID = 0xFF
|
MSG_ASSIGN_ID = 0xFF
|
||||||
};
|
};
|
||||||
@ -197,22 +196,7 @@ struct AnimUpdateMsg {
|
|||||||
struct AnimStartMsg {
|
struct AnimStartMsg {
|
||||||
MessageHeader header;
|
MessageHeader header;
|
||||||
uint16_t animIndex;
|
uint16_t animIndex;
|
||||||
};
|
uint64_t eventId; // Random 64-bit ID for the completion event (host-generated, same for all clients)
|
||||||
|
|
||||||
// Per-participant data in AnimCompleteMsg
|
|
||||||
struct AnimCompletionParticipant {
|
|
||||||
uint32_t peerId;
|
|
||||||
int8_t charIndex; // Participant's character (g_actorInfoInit index)
|
|
||||||
char displayName[USERNAME_BUFFER_SIZE]; // 7 chars + null
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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
|
|
||||||
uint16_t animIndex; // World-encoded animation index (globally unique key)
|
|
||||||
uint8_t participantCount;
|
|
||||||
AnimCompletionParticipant participants[8];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#pragma pack(pop)
|
#pragma pack(pop)
|
||||||
|
|||||||
@ -43,6 +43,12 @@ class RemotePlayer {
|
|||||||
const std::vector<int16_t>& GetLocations() const { return m_locations; }
|
const std::vector<int16_t>& GetLocations() const { return m_locations; }
|
||||||
void SetLocations(std::vector<int16_t> p_locations) { m_locations = std::move(p_locations); }
|
void SetLocations(std::vector<int16_t> p_locations) { m_locations = std::move(p_locations); }
|
||||||
bool IsAtLocation(int16_t p_location) const;
|
bool IsAtLocation(int16_t p_location) const;
|
||||||
|
bool HasReceivedUpdate() const { return m_hasReceivedUpdate; }
|
||||||
|
void GetTargetPosition(float& p_x, float& p_z) const
|
||||||
|
{
|
||||||
|
p_x = m_targetPosition[0];
|
||||||
|
p_z = m_targetPosition[2];
|
||||||
|
}
|
||||||
uint32_t GetLastUpdateTime() const { return m_lastUpdateTime; }
|
uint32_t GetLastUpdateTime() const { return m_lastUpdateTime; }
|
||||||
void SetVisible(bool p_visible);
|
void SetVisible(bool p_visible);
|
||||||
void TriggerExtraAnim(uint8_t p_emoteId);
|
void TriggerExtraAnim(uint8_t p_emoteId);
|
||||||
|
|||||||
@ -193,14 +193,15 @@ MxResult NetworkManager::Tickle()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsHost()) {
|
if (!IsHost() && m_animCoordinator.GetState() == Animation::CoordinationState::e_countdown) {
|
||||||
TickHostSessions();
|
|
||||||
}
|
|
||||||
else if (m_animCoordinator.GetState() == Animation::CoordinationState::e_countdown) {
|
|
||||||
m_animStateDirty = true;
|
m_animStateDirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IsHost()) {
|
||||||
|
TickHostSessions();
|
||||||
|
}
|
||||||
|
|
||||||
if (!m_transport) {
|
if (!m_transport) {
|
||||||
return SUCCESS;
|
return SUCCESS;
|
||||||
}
|
}
|
||||||
@ -259,6 +260,12 @@ void NetworkManager::HandleCreate()
|
|||||||
TickleManager()->RegisterClient(this, 10);
|
TickleManager()->RegisterClient(this, 10);
|
||||||
m_registered = true;
|
m_registered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load animation catalog early so the host can coordinate animations
|
||||||
|
// even before entering the Isle world (e.g. while in infocenter).
|
||||||
|
m_animCatalog.Refresh();
|
||||||
|
m_animCoordinator.SetCatalog(&m_animCatalog);
|
||||||
|
m_animSessionHost.SetCatalog(&m_animCatalog);
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkManager::Shutdown()
|
void NetworkManager::Shutdown()
|
||||||
@ -312,8 +319,22 @@ bool NetworkManager::WasRejected() const
|
|||||||
|
|
||||||
void NetworkManager::ResetAnimationState()
|
void NetworkManager::ResetAnimationState()
|
||||||
{
|
{
|
||||||
|
// Notify clients that all active sessions are cancelled so they don't get stuck
|
||||||
|
// waiting for a countdown/start that will never come.
|
||||||
|
if (IsHost() && IsConnected()) {
|
||||||
|
std::vector<uint16_t> activeAnims;
|
||||||
|
for (const auto& [animIndex, session] : m_animSessionHost.GetSessions()) {
|
||||||
|
activeAnims.push_back(animIndex);
|
||||||
|
}
|
||||||
|
m_animSessionHost.Reset();
|
||||||
|
for (uint16_t animIndex : activeAnims) {
|
||||||
|
SendMessage(BuildAnimUpdateMsg(animIndex, TARGET_BROADCAST)); // Session gone → state=0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_animSessionHost.Reset();
|
||||||
|
}
|
||||||
m_animCoordinator.Reset();
|
m_animCoordinator.Reset();
|
||||||
m_animSessionHost.Reset();
|
|
||||||
m_localPendingAnimInterest = -1;
|
m_localPendingAnimInterest = -1;
|
||||||
m_pendingAnimInterest.store(-1, std::memory_order_relaxed);
|
m_pendingAnimInterest.store(-1, std::memory_order_relaxed);
|
||||||
m_pendingAnimCancel.store(false, std::memory_order_relaxed);
|
m_pendingAnimCancel.store(false, std::memory_order_relaxed);
|
||||||
@ -348,8 +369,12 @@ void NetworkManager::CancelLocalAnimInterest()
|
|||||||
|
|
||||||
void NetworkManager::StopAnimation()
|
void NetworkManager::StopAnimation()
|
||||||
{
|
{
|
||||||
ResetAnimationState();
|
|
||||||
StopAllPlayback();
|
StopAllPlayback();
|
||||||
|
CancelLocalAnimInterest();
|
||||||
|
m_animCoordinator.Reset();
|
||||||
|
m_pendingAnimInterest.store(-1, std::memory_order_relaxed);
|
||||||
|
m_pendingAnimCancel.store(false, std::memory_order_relaxed);
|
||||||
|
m_animStateDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkManager::OnWorldEnabled(LegoWorld* p_world)
|
void NetworkManager::OnWorldEnabled(LegoWorld* p_world)
|
||||||
@ -909,13 +934,6 @@ void NetworkManager::ProcessIncomingPackets()
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case MSG_ANIM_COMPLETE: {
|
|
||||||
AnimCompleteMsg msg;
|
|
||||||
if (DeserializeMsg(data, length, msg)) {
|
|
||||||
HandleAnimComplete(msg);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -931,14 +949,16 @@ void NetworkManager::UpdateRemotePlayers(float p_deltaTime)
|
|||||||
for (auto& [peerId, player] : m_remotePlayers) {
|
for (auto& [peerId, player] : m_remotePlayers) {
|
||||||
player->Tick(p_deltaTime);
|
player->Tick(p_deltaTime);
|
||||||
|
|
||||||
// Derive locations from remote player's current position
|
// Derive locations from remote player's network-reported position.
|
||||||
// Skip players not in the isle world — their position is stale
|
// Skip players not in the isle world — their position is stale.
|
||||||
if (player->IsSpawned() && player->GetROI() && player->GetWorldId() == (int8_t) LegoOmni::e_act1) {
|
if (player->GetWorldId() == (int8_t) LegoOmni::e_act1 && player->HasReceivedUpdate()) {
|
||||||
anyInIsle = true;
|
anyInIsle = true;
|
||||||
|
|
||||||
|
float px, pz;
|
||||||
|
player->GetTargetPosition(px, pz);
|
||||||
|
|
||||||
auto oldLocs = player->GetLocations();
|
auto oldLocs = player->GetLocations();
|
||||||
const float* pos = player->GetROI()->GetWorldPosition();
|
auto newLocs = Animation::LocationProximity::ComputeAll(px, pz, radius);
|
||||||
auto newLocs = Animation::LocationProximity::ComputeAll(pos[0], pos[2], radius);
|
|
||||||
player->SetLocations(std::move(newLocs));
|
player->SetLocations(std::move(newLocs));
|
||||||
if (oldLocs != player->GetLocations()) {
|
if (oldLocs != player->GetLocations()) {
|
||||||
// Dirty if remote's locations changed and any overlap with local player's locations
|
// Dirty if remote's locations changed and any overlap with local player's locations
|
||||||
@ -1384,6 +1404,7 @@ void NetworkManager::StopScenePlayback(uint16_t p_animIndex, bool p_unlockRemote
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_pendingCompletionJson.erase(p_animIndex);
|
||||||
m_playingAnims.erase(it);
|
m_playingAnims.erase(it);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1395,6 +1416,7 @@ void NetworkManager::StopAllPlayback()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_playingAnims.clear();
|
m_playingAnims.clear();
|
||||||
|
m_pendingCompletionJson.clear();
|
||||||
|
|
||||||
for (auto& [peerId, player] : m_remotePlayers) {
|
for (auto& [peerId, player] : m_remotePlayers) {
|
||||||
player->ForceUnlockAnimation();
|
player->ForceUnlockAnimation();
|
||||||
@ -1439,20 +1461,21 @@ void NetworkManager::TickAnimation()
|
|||||||
for (auto& [animIndex, wasObserver] : completed) {
|
for (auto& [animIndex, wasObserver] : completed) {
|
||||||
UnlockRemotesForAnim(animIndex);
|
UnlockRemotesForAnim(animIndex);
|
||||||
|
|
||||||
// Release camera if local player was a participant (not observer)
|
|
||||||
if (!wasObserver) {
|
if (!wasObserver) {
|
||||||
|
// Fire cached completion callback before cleanup destroys state
|
||||||
|
auto compIt = m_pendingCompletionJson.find(animIndex);
|
||||||
|
if (compIt != m_pendingCompletionJson.end()) {
|
||||||
|
if (m_callbacks && !compIt->second.empty()) {
|
||||||
|
m_callbacks->OnAnimationCompleted(compIt->second.c_str());
|
||||||
|
}
|
||||||
|
m_pendingCompletionJson.erase(compIt);
|
||||||
|
}
|
||||||
|
|
||||||
ThirdPersonCamera::Controller* cam = GetCamera();
|
ThirdPersonCamera::Controller* cam = GetCamera();
|
||||||
if (cam) {
|
if (cam) {
|
||||||
cam->SetAnimPlaying(false);
|
cam->SetAnimPlaying(false);
|
||||||
}
|
}
|
||||||
m_animCoordinator.ResetLocalState();
|
m_animCoordinator.ResetLocalState();
|
||||||
m_animCoordinator.RemoveSession(animIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsHost()) {
|
|
||||||
BroadcastAnimComplete(animIndex); // Must fire before EraseSession destroys participant data
|
|
||||||
m_animSessionHost.EraseSession(animIndex);
|
|
||||||
BroadcastAnimUpdate(animIndex); // Broadcast cleared state
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_playingAnims.erase(animIndex);
|
m_playingAnims.erase(animIndex);
|
||||||
@ -1547,8 +1570,16 @@ void NetworkManager::TickHostSessions()
|
|||||||
// Check countdown expiry — multiple animations may be ready simultaneously
|
// Check countdown expiry — multiple animations may be ready simultaneously
|
||||||
std::vector<uint16_t> readyAnims = m_animSessionHost.Tick(SDL_GetTicks());
|
std::vector<uint16_t> readyAnims = m_animSessionHost.Tick(SDL_GetTicks());
|
||||||
for (uint16_t readyAnim : readyAnims) {
|
for (uint16_t readyAnim : readyAnims) {
|
||||||
BroadcastAnimStart(readyAnim);
|
uint64_t eventId = (static_cast<uint64_t>(SDL_rand_bits()) << 32) | static_cast<uint64_t>(SDL_rand_bits());
|
||||||
HandleAnimStartLocally(readyAnim, m_animCoordinator.IsLocalPlayerInSession(readyAnim));
|
BroadcastAnimStart(readyAnim, eventId);
|
||||||
|
|
||||||
|
// Erase session immediately — the host's job ends at launch.
|
||||||
|
// Clients play independently and fire completion callbacks locally.
|
||||||
|
m_animSessionHost.EraseSession(readyAnim);
|
||||||
|
|
||||||
|
if (m_inIsleWorld) {
|
||||||
|
HandleAnimStartLocally(readyAnim, m_animCoordinator.IsLocalPlayerInSession(readyAnim), eventId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// During countdown, push state every tick so countdownMs reaches the frontend
|
// During countdown, push state every tick so countdownMs reaches the frontend
|
||||||
@ -1692,13 +1723,13 @@ void NetworkManager::HandleAnimStart(const AnimStartMsg& p_msg)
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_animCoordinator.ApplyAnimStart(p_msg.animIndex);
|
m_animCoordinator.ApplyAnimStart(p_msg.animIndex);
|
||||||
HandleAnimStartLocally(p_msg.animIndex, m_animCoordinator.IsLocalPlayerInSession(p_msg.animIndex));
|
HandleAnimStartLocally(p_msg.animIndex, m_animCoordinator.IsLocalPlayerInSession(p_msg.animIndex), p_msg.eventId);
|
||||||
|
|
||||||
m_animStateDirty = true;
|
m_animStateDirty = true;
|
||||||
m_animInterestDirty = true;
|
m_animInterestDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkManager::HandleAnimStartLocally(uint16_t p_animIndex, bool p_localInSession)
|
void NetworkManager::HandleAnimStartLocally(uint16_t p_animIndex, bool p_localInSession, uint64_t p_eventId)
|
||||||
{
|
{
|
||||||
auto abortSession = [&]() {
|
auto abortSession = [&]() {
|
||||||
// Observers must not abort the authoritative session — only participants may do that
|
// Observers must not abort the authoritative session — only participants may do that
|
||||||
@ -1816,10 +1847,112 @@ void NetworkManager::HandleAnimStartLocally(uint16_t p_animIndex, bool p_localIn
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_playingAnims[p_animIndex] = std::move(scenePlayer);
|
m_playingAnims[p_animIndex] = std::move(scenePlayer);
|
||||||
|
|
||||||
|
// Cache completion JSON for local firing when ScenePlayer finishes.
|
||||||
|
// Must happen before RemoveSession which destroys the session view.
|
||||||
|
if (!observerMode && m_callbacks) {
|
||||||
|
m_pendingCompletionJson[p_animIndex] = BuildCompletionJson(p_animIndex, p_eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session data has been captured — free the animation for reuse so all clients
|
||||||
|
// (not just the host) show it as available immediately after launch.
|
||||||
|
m_animCoordinator.RemoveSession(p_animIndex);
|
||||||
|
|
||||||
m_localPendingAnimInterest = -1;
|
m_localPendingAnimInterest = -1;
|
||||||
m_animStateDirty = true;
|
m_animStateDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string NetworkManager::BuildCompletionJson(uint16_t p_animIndex, uint64_t p_eventId)
|
||||||
|
{
|
||||||
|
const Animation::CatalogEntry* entry = m_animCatalog.FindEntry(p_animIndex);
|
||||||
|
const Animation::SessionView* view = m_animCoordinator.GetSessionView(p_animIndex);
|
||||||
|
if (!entry || !view) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int8_t> slotChars = Animation::SessionHost::ComputeSlotCharIndices(entry);
|
||||||
|
uint8_t count = view->slotCount < static_cast<uint8_t>(slotChars.size())
|
||||||
|
? view->slotCount
|
||||||
|
: static_cast<uint8_t>(slotChars.size());
|
||||||
|
|
||||||
|
char eventIdHex[17];
|
||||||
|
SDL_snprintf(
|
||||||
|
eventIdHex,
|
||||||
|
sizeof(eventIdHex),
|
||||||
|
"%08x%08x",
|
||||||
|
static_cast<uint32_t>(p_eventId >> 32),
|
||||||
|
static_cast<uint32_t>(p_eventId & 0xFFFFFFFF)
|
||||||
|
);
|
||||||
|
|
||||||
|
std::string json = "{\"eventId\":\"";
|
||||||
|
json += eventIdHex;
|
||||||
|
json += "\",\"animIndex\":";
|
||||||
|
json += std::to_string(p_animIndex);
|
||||||
|
json += ",\"participants\":[";
|
||||||
|
|
||||||
|
// Emit local player first so frontend can rely on participants[0] being self
|
||||||
|
bool first = true;
|
||||||
|
auto appendParticipant = [&](uint32_t peerId, uint8_t slotIndex) {
|
||||||
|
int8_t charIndex;
|
||||||
|
char displayName[USERNAME_BUFFER_SIZE] = {};
|
||||||
|
|
||||||
|
if (Animation::SessionSlot::IsSpectatorCharIndex(slotChars[slotIndex])) {
|
||||||
|
// Resolve spectator's actual character from their display actor
|
||||||
|
if (peerId == m_localPeerId) {
|
||||||
|
ThirdPersonCamera::Controller* cam = GetCamera();
|
||||||
|
charIndex = cam ? Animation::Catalog::DisplayActorToCharacterIndex(cam->GetDisplayActorIndex()) : -1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
auto it = m_remotePlayers.find(peerId);
|
||||||
|
charIndex = it != m_remotePlayers.end()
|
||||||
|
? Animation::Catalog::DisplayActorToCharacterIndex(it->second->GetDisplayActorIndex())
|
||||||
|
: -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
charIndex = slotChars[slotIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (peerId == m_localPeerId) {
|
||||||
|
EncodeUsername(displayName);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
auto it = m_remotePlayers.find(peerId);
|
||||||
|
if (it != m_remotePlayers.end()) {
|
||||||
|
SDL_strlcpy(displayName, it->second->GetDisplayName(), sizeof(displayName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!first) {
|
||||||
|
json += ',';
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
json += "{\"charIndex\":";
|
||||||
|
json += std::to_string(static_cast<int>(charIndex));
|
||||||
|
json += ",\"displayName\":\"";
|
||||||
|
json += displayName;
|
||||||
|
json += "\"}";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Local player first
|
||||||
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
|
if (view->peerSlots[i] == m_localPeerId) {
|
||||||
|
appendParticipant(m_localPeerId, i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Then remote players
|
||||||
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
|
uint32_t peerId = view->peerSlots[i];
|
||||||
|
if (peerId != 0 && peerId != m_localPeerId) {
|
||||||
|
appendParticipant(peerId, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json += "]}";
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
AnimUpdateMsg NetworkManager::BuildAnimUpdateMsg(uint16_t p_animIndex, uint32_t p_target)
|
AnimUpdateMsg NetworkManager::BuildAnimUpdateMsg(uint16_t p_animIndex, uint32_t p_target)
|
||||||
{
|
{
|
||||||
AnimUpdateMsg msg{};
|
AnimUpdateMsg msg{};
|
||||||
@ -1855,147 +1988,18 @@ void NetworkManager::SendAnimUpdateToPlayer(uint16_t p_animIndex, uint32_t p_tar
|
|||||||
SendMessage(BuildAnimUpdateMsg(p_animIndex, p_targetPeerId));
|
SendMessage(BuildAnimUpdateMsg(p_animIndex, p_targetPeerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkManager::BroadcastAnimStart(uint16_t p_animIndex)
|
void NetworkManager::BroadcastAnimStart(uint16_t p_animIndex, uint64_t p_eventId)
|
||||||
{
|
{
|
||||||
AnimStartMsg msg{};
|
AnimStartMsg msg{};
|
||||||
msg.header = MakeHeader(MSG_ANIM_START, TARGET_BROADCAST);
|
msg.header = MakeHeader(MSG_ANIM_START, TARGET_BROADCAST);
|
||||||
msg.animIndex = p_animIndex;
|
msg.animIndex = p_animIndex;
|
||||||
|
msg.eventId = p_eventId;
|
||||||
SendMessage(msg);
|
SendMessage(msg);
|
||||||
|
|
||||||
// Also update local coordinator
|
// Also update local coordinator
|
||||||
m_animCoordinator.ApplyAnimStart(p_animIndex);
|
m_animCoordinator.ApplyAnimStart(p_animIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkManager::BroadcastAnimComplete(uint16_t p_animIndex)
|
|
||||||
{
|
|
||||||
const Animation::AnimSession* session = m_animSessionHost.FindSession(p_animIndex);
|
|
||||||
if (!session) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AnimInfo* animInfo = m_animCatalog.GetAnimInfo(p_animIndex);
|
|
||||||
if (!animInfo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimCompleteMsg msg{};
|
|
||||||
msg.header = MakeHeader(MSG_ANIM_COMPLETE, TARGET_BROADCAST);
|
|
||||||
msg.eventId = (static_cast<uint64_t>(SDL_rand_bits()) << 32) | static_cast<uint64_t>(SDL_rand_bits());
|
|
||||||
msg.animIndex = p_animIndex;
|
|
||||||
msg.participantCount = 0;
|
|
||||||
|
|
||||||
char localName[8];
|
|
||||||
EncodeUsername(localName);
|
|
||||||
|
|
||||||
for (const auto& slot : session->slots) {
|
|
||||||
if (slot.peerId == 0 || msg.participantCount >= 8) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimCompletionParticipant& p = msg.participants[msg.participantCount];
|
|
||||||
p.peerId = slot.peerId;
|
|
||||||
|
|
||||||
if (slot.IsSpectator()) {
|
|
||||||
// Resolve spectator's actual character from their display actor
|
|
||||||
if (slot.peerId == m_localPeerId) {
|
|
||||||
ThirdPersonCamera::Controller* cam = GetCamera();
|
|
||||||
p.charIndex = cam ? Animation::Catalog::DisplayActorToCharacterIndex(cam->GetDisplayActorIndex()) : -1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
auto it = m_remotePlayers.find(slot.peerId);
|
|
||||||
p.charIndex = it != m_remotePlayers.end()
|
|
||||||
? Animation::Catalog::DisplayActorToCharacterIndex(it->second->GetDisplayActorIndex())
|
|
||||||
: -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
p.charIndex = slot.charIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slot.peerId == m_localPeerId) {
|
|
||||||
SDL_memcpy(p.displayName, localName, sizeof(p.displayName));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
auto it = m_remotePlayers.find(slot.peerId);
|
|
||||||
if (it != m_remotePlayers.end()) {
|
|
||||||
SDL_memcpy(p.displayName, it->second->GetDisplayName(), sizeof(p.displayName));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
p.displayName[0] = '\0';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.participantCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
SendMessage(msg);
|
|
||||||
|
|
||||||
// Also handle locally on the host (message sent to TARGET_BROADCAST excludes sender)
|
|
||||||
HandleAnimComplete(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
void NetworkManager::HandleAnimComplete(const AnimCompleteMsg& p_msg)
|
|
||||||
{
|
|
||||||
// Only fire callback for actual participants, not observers
|
|
||||||
int localIdx = -1;
|
|
||||||
for (uint8_t i = 0; i < p_msg.participantCount; i++) {
|
|
||||||
if (p_msg.participants[i].peerId == m_localPeerId) {
|
|
||||||
localIdx = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localIdx < 0 || !m_callbacks) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build JSON for frontend
|
|
||||||
char eventIdHex[17];
|
|
||||||
SDL_snprintf(
|
|
||||||
eventIdHex,
|
|
||||||
sizeof(eventIdHex),
|
|
||||||
"%08x%08x",
|
|
||||||
static_cast<uint32_t>(p_msg.eventId >> 32),
|
|
||||||
static_cast<uint32_t>(p_msg.eventId & 0xFFFFFFFF)
|
|
||||||
);
|
|
||||||
|
|
||||||
std::string json = "{\"eventId\":\"";
|
|
||||||
json += eventIdHex;
|
|
||||||
json += "\",\"animIndex\":";
|
|
||||||
json += std::to_string(p_msg.animIndex);
|
|
||||||
json += ",\"participants\":[";
|
|
||||||
|
|
||||||
// Emit local player first so frontend can rely on participants[0] being self
|
|
||||||
bool first = true;
|
|
||||||
auto appendParticipant = [&](uint8_t i) {
|
|
||||||
if (!first) {
|
|
||||||
json += ',';
|
|
||||||
}
|
|
||||||
first = false;
|
|
||||||
const AnimCompletionParticipant& p = p_msg.participants[i];
|
|
||||||
// Ensure null-termination safety for displayName
|
|
||||||
char name[USERNAME_BUFFER_SIZE];
|
|
||||||
SDL_memcpy(name, p.displayName, sizeof(name));
|
|
||||||
name[USERNAME_BUFFER_SIZE - 1] = '\0';
|
|
||||||
json += "{\"charIndex\":";
|
|
||||||
json += std::to_string(static_cast<int>(p.charIndex));
|
|
||||||
json += ",\"displayName\":\"";
|
|
||||||
json += name;
|
|
||||||
json += "\"}";
|
|
||||||
};
|
|
||||||
|
|
||||||
appendParticipant(static_cast<uint8_t>(localIdx));
|
|
||||||
for (uint8_t i = 0; i < p_msg.participantCount; i++) {
|
|
||||||
if (i != static_cast<uint8_t>(localIdx)) {
|
|
||||||
appendParticipant(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
json += "]}";
|
|
||||||
|
|
||||||
m_callbacks->OnAnimationCompleted(json.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
bool NetworkManager::IsPeerAtLocation(uint32_t p_peerId, int16_t p_location) const
|
bool NetworkManager::IsPeerAtLocation(uint32_t p_peerId, int16_t p_location) const
|
||||||
{
|
{
|
||||||
if (p_peerId == m_localPeerId) {
|
if (p_peerId == m_localPeerId) {
|
||||||
@ -2021,13 +2025,11 @@ bool NetworkManager::GetPeerPosition(uint32_t p_peerId, float& p_x, float& p_z)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto it = m_remotePlayers.find(p_peerId);
|
auto it = m_remotePlayers.find(p_peerId);
|
||||||
if (it != m_remotePlayers.end() && it->second->IsSpawned() && it->second->GetROI()) {
|
if (it == m_remotePlayers.end() || !it->second->HasReceivedUpdate()) {
|
||||||
const float* pos = it->second->GetROI()->GetWorldPosition();
|
return false;
|
||||||
p_x = pos[0];
|
|
||||||
p_z = pos[2];
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
it->second->GetTargetPosition(p_x, p_z);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NetworkManager::IsPeerNearby(uint32_t p_peerId, float p_refX, float p_refZ) const
|
bool NetworkManager::IsPeerNearby(uint32_t p_peerId, float p_refX, float p_refZ) const
|
||||||
@ -2039,13 +2041,14 @@ bool NetworkManager::IsPeerNearby(uint32_t p_peerId, float p_refX, float p_refZ)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
auto it = m_remotePlayers.find(p_peerId);
|
auto it = m_remotePlayers.find(p_peerId);
|
||||||
if (it == m_remotePlayers.end() || !it->second->IsSpawned() || !it->second->GetROI() ||
|
if (it == m_remotePlayers.end() || it->second->GetWorldId() != (int8_t) LegoOmni::e_act1 ||
|
||||||
it->second->GetWorldId() != (int8_t) LegoOmni::e_act1) {
|
!it->second->HasReceivedUpdate()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const float* pos = it->second->GetROI()->GetWorldPosition();
|
float px, pz;
|
||||||
float dx = pos[0] - p_refX;
|
it->second->GetTargetPosition(px, pz);
|
||||||
float dz = pos[2] - p_refZ;
|
float dx = px - p_refX;
|
||||||
|
float dz = pz - p_refZ;
|
||||||
return (dx * dx + dz * dz) <= NPC_ANIM_NEARBY_RADIUS_SQ;
|
return (dx * dx + dz * dz) <= NPC_ANIM_NEARBY_RADIUS_SQ;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2057,7 +2060,7 @@ uint8_t NetworkManager::GetPeerVehicleState(uint32_t p_peerId, int8_t p_charInde
|
|||||||
: Animation::Catalog::e_onFoot;
|
: Animation::Catalog::e_onFoot;
|
||||||
}
|
}
|
||||||
auto it = m_remotePlayers.find(p_peerId);
|
auto it = m_remotePlayers.find(p_peerId);
|
||||||
if (it == m_remotePlayers.end() || !it->second->IsSpawned()) {
|
if (it == m_remotePlayers.end()) {
|
||||||
return Animation::Catalog::e_onFoot;
|
return Animation::Catalog::e_onFoot;
|
||||||
}
|
}
|
||||||
return Animation::Catalog::GetVehicleState(p_charIndex, it->second->GetRideVehicleROI());
|
return Animation::Catalog::GetVehicleState(p_charIndex, it->second->GetRideVehicleROI());
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user