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:
Christian Semmler 2026-04-03 20:21:02 -07:00
parent 90d11d98e0
commit 11bf290396
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
5 changed files with 192 additions and 195 deletions

View File

@ -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 {

View File

@ -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();

View File

@ -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)

View File

@ -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);

View File

@ -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());