mirror of
https://github.com/isledecomp/isle-portable.git
synced 2026-05-01 18:13:57 +00:00
Add multi-world animation support (ACT2/ACT3)
Extend the multiplayer animation system to support animations from all three game worlds (ACT1, ACT2, ACT3) while playing in the Isle world. Catalog: Parse DTA files directly for all worlds instead of borrowing from LegoAnimationManager. World-encoded animIndex (top 2 bits = world slot) provides globally unique IDs without wire protocol changes. Loader: Support multiple SI files (isle.si, act2main.si, act3.si) with lazy opening and composite (worldId, objectId) cache keys. WDB: Load missing model LODs from WORLD.WDB for all worlds during catalog refresh, using LegoPartPresenter for parts and LegoModelPresenter for compound models (ray, chptr). Protocol: AnimCompleteMsg now carries animIndex instead of objectId. Also fix pre-existing bugs: - PhonemePlayer UAF when multiple tracks target the same ROI - ModelDbModel buffer overflow on word-aligned strlcpy reads - SIReader UBSan violation on uninitialized filetype enum Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e61f47abb2
commit
0c986aff2a
@ -1,6 +1,7 @@
|
||||
#ifndef LEGOMODELPRESENTER_H
|
||||
#define LEGOMODELPRESENTER_H
|
||||
|
||||
#include "extensions/fwd.h"
|
||||
#include "lego1_export.h"
|
||||
#include "mxvideopresenter.h"
|
||||
|
||||
@ -62,6 +63,8 @@ class LegoModelPresenter : public MxVideoPresenter {
|
||||
void Destroy(MxBool p_fromDestructor);
|
||||
|
||||
private:
|
||||
friend class Multiplayer::Animation::Catalog;
|
||||
|
||||
LegoROI* m_roi; // 0x64
|
||||
MxBool m_addedToView; // 0x68
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ MxResult ModelDbModel::Read(SDL_IOStream* p_file)
|
||||
return FAILURE;
|
||||
}
|
||||
|
||||
m_modelName = new char[len];
|
||||
m_modelName = new char[((len + 3) & ~3u)];
|
||||
if (SDL_ReadIO(p_file, m_modelName, len) != len) {
|
||||
return FAILURE;
|
||||
}
|
||||
@ -38,7 +38,7 @@ MxResult ModelDbModel::Read(SDL_IOStream* p_file)
|
||||
return FAILURE;
|
||||
}
|
||||
|
||||
m_presenterName = new char[len];
|
||||
m_presenterName = new char[((len + 3) & ~3u)];
|
||||
if (SDL_ReadIO(p_file, m_presenterName, len) != len) {
|
||||
return FAILURE;
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
class LegoAnimationManager;
|
||||
class LegoROI;
|
||||
struct AnimInfo;
|
||||
|
||||
@ -26,11 +25,35 @@ static const uint8_t ALL_CORE_ACTORS_MASK = (1 << CORE_CHARACTER_COUNT) - 1;
|
||||
// Sentinel value for "no animation selected"
|
||||
static const uint16_t ANIM_INDEX_NONE = 0xFFFF;
|
||||
|
||||
// World slot constants for animIndex encoding (top 2 bits)
|
||||
static const uint8_t WORLD_SLOT_ACT1 = 0;
|
||||
static const uint8_t WORLD_SLOT_ACT2 = 1;
|
||||
static const uint8_t WORLD_SLOT_ACT3 = 2;
|
||||
|
||||
// Compose a globally unique animIndex from a world slot and a local index within that world's AnimInfo array.
|
||||
static constexpr uint16_t WorldAnimIndex(uint8_t p_worldSlot, uint16_t p_localIndex)
|
||||
{
|
||||
return (uint16_t(p_worldSlot) << 14) | (p_localIndex & 0x3FFF);
|
||||
}
|
||||
|
||||
// Extract the world slot (0-2) from a world-encoded animIndex.
|
||||
static constexpr uint8_t GetWorldSlot(uint16_t p_animIndex)
|
||||
{
|
||||
return p_animIndex >> 14;
|
||||
}
|
||||
|
||||
// Extract the local index (0-16383) from a world-encoded animIndex.
|
||||
static constexpr uint16_t GetLocalIndex(uint16_t p_animIndex)
|
||||
{
|
||||
return p_animIndex & 0x3FFF;
|
||||
}
|
||||
|
||||
// Extract the character indices from a performer bitmask.
|
||||
std::vector<int8_t> GetPerformerIndices(uint64_t p_performerMask);
|
||||
|
||||
struct CatalogEntry {
|
||||
uint16_t animIndex; // Index into LegoAnimationManager::m_anims[]
|
||||
uint16_t animIndex; // World-encoded index: top 2 bits = world slot, bottom 14 = local index
|
||||
int8_t worldId; // LegoOmni::World enum value for this animation's source world
|
||||
AnimCategory category;
|
||||
uint8_t spectatorMask; // Which core actors can trigger (bit0=Pepper..bit4=Laura)
|
||||
uint64_t performerMask; // Bitmask of g_actorInfoInit[] indices that appear as character models
|
||||
@ -41,7 +64,10 @@ struct CatalogEntry {
|
||||
|
||||
class Catalog {
|
||||
public:
|
||||
void Refresh(LegoAnimationManager* p_am);
|
||||
~Catalog();
|
||||
|
||||
// Parse DTA files for all supported worlds and build the catalog.
|
||||
void Refresh();
|
||||
|
||||
const AnimInfo* GetAnimInfo(uint16_t p_animIndex) const;
|
||||
const CatalogEntry* FindEntry(uint16_t p_animIndex) const;
|
||||
@ -75,9 +101,9 @@ class Catalog {
|
||||
|
||||
// Vehicle riding state for eligibility checks.
|
||||
enum VehicleState : uint8_t {
|
||||
e_onFoot = 0, // Not riding anything
|
||||
e_onOwnVehicle = 1, // Riding character's own vehicle (e.g. Pepper on skateboard)
|
||||
e_onOtherVehicle = 2 // Riding a vehicle that isn't the character's own
|
||||
e_onFoot = 0, // Not riding anything
|
||||
e_onOwnVehicle = 1, // Riding character's own vehicle (e.g. Pepper on skateboard)
|
||||
e_onOtherVehicle = 2 // Riding a vehicle that isn't the character's own
|
||||
};
|
||||
|
||||
// Check if a player's vehicle state is compatible with the animation's vehicle requirements.
|
||||
@ -94,11 +120,29 @@ class Catalog {
|
||||
// Returns -1 if no match.
|
||||
static int8_t DisplayActorToCharacterIndex(uint8_t p_displayActorIndex);
|
||||
|
||||
// Map a LegoOmni::World enum value to a world slot index (0-2).
|
||||
// Returns 0xFF if the world is not supported.
|
||||
static uint8_t WorldIdToSlot(int8_t p_worldId);
|
||||
|
||||
private:
|
||||
struct WorldAnimData {
|
||||
int8_t worldId;
|
||||
uint8_t worldSlot;
|
||||
AnimInfo* anims;
|
||||
uint16_t animCount;
|
||||
};
|
||||
|
||||
bool ParseDTAFile(int8_t p_worldId, AnimInfo*& p_outAnims, uint16_t& p_outCount);
|
||||
void BuildEntries(const WorldAnimData& p_world);
|
||||
void LoadWorldParts();
|
||||
void Cleanup();
|
||||
|
||||
static void FreeAnimInfo(AnimInfo* p_anims, uint16_t p_count);
|
||||
|
||||
std::vector<CatalogEntry> m_entries;
|
||||
std::map<int16_t, std::vector<size_t>> m_locationIndex; // location ID → indices into m_entries
|
||||
AnimInfo* m_animsBase;
|
||||
uint16_t m_animCount;
|
||||
std::vector<WorldAnimData> m_worldData;
|
||||
std::vector<LegoROI*> m_modelROIs; // keep model ROIs alive to preserve LOD refcounts
|
||||
};
|
||||
|
||||
} // namespace Multiplayer::Animation
|
||||
|
||||
@ -61,8 +61,8 @@ struct SceneAnimData {
|
||||
void ReleaseTracks();
|
||||
};
|
||||
|
||||
// Loads animation data from ISLE.SI on demand.
|
||||
// Delegates SI file access to a SIReader instance.
|
||||
// Loads animation data from SI files on demand.
|
||||
// Supports multiple worlds' SI files (isle.si, act2main.si, act3.si).
|
||||
class Loader {
|
||||
public:
|
||||
Loader();
|
||||
@ -70,30 +70,51 @@ class Loader {
|
||||
|
||||
void SetSIReader(SIReader* p_reader) { m_reader = p_reader; }
|
||||
|
||||
SceneAnimData* EnsureCached(uint32_t p_objectId);
|
||||
void PreloadAsync(uint32_t p_objectId);
|
||||
SceneAnimData* EnsureCached(int8_t p_worldId, uint32_t p_objectId);
|
||||
void PreloadAsync(int8_t p_worldId, uint32_t p_objectId);
|
||||
|
||||
// Get the SI file path for a world. Returns nullptr if unsupported.
|
||||
static const char* GetSIPath(int8_t p_worldId);
|
||||
|
||||
private:
|
||||
class PreloadThread : public MxThread {
|
||||
public:
|
||||
PreloadThread(Loader* p_loader, uint32_t p_objectId);
|
||||
PreloadThread(Loader* p_loader, int8_t p_worldId, uint32_t p_objectId);
|
||||
MxResult Run() override;
|
||||
|
||||
private:
|
||||
Loader* m_loader;
|
||||
int8_t m_worldId;
|
||||
uint32_t m_objectId;
|
||||
};
|
||||
|
||||
// SI file handle for non-act1 worlds (act1 uses the external SIReader).
|
||||
struct SIHandle {
|
||||
si::File* file;
|
||||
si::Interleaf* interleaf;
|
||||
bool ready;
|
||||
};
|
||||
|
||||
bool OpenWorldSI(int8_t p_worldId);
|
||||
bool ReadWorldObject(int8_t p_worldId, uint32_t p_objectId, si::Object*& p_outObj);
|
||||
|
||||
static bool ParseAnimationChild(si::Object* p_child, SceneAnimData& p_data);
|
||||
static bool ParsePhonemeChild(si::Object* p_child, SceneAnimData& p_data);
|
||||
static bool ParseComposite(si::Object* p_composite, SceneAnimData& p_data);
|
||||
void CleanupPreloadThread();
|
||||
|
||||
SIReader* m_reader;
|
||||
std::map<uint32_t, SceneAnimData> m_cache;
|
||||
static uint64_t CacheKey(int8_t p_worldId, uint32_t p_objectId)
|
||||
{
|
||||
return (uint64_t((uint8_t) p_worldId) << 32) | p_objectId;
|
||||
}
|
||||
|
||||
SIReader* m_reader; // external reader for isle.si (act1)
|
||||
std::map<int8_t, SIHandle> m_extraSI; // SI handles for non-act1 worlds
|
||||
std::map<uint64_t, SceneAnimData> m_cache; // keyed by CacheKey(worldId, objectId)
|
||||
MxCriticalSection m_cacheCS;
|
||||
|
||||
PreloadThread* m_preloadThread;
|
||||
int8_t m_preloadWorldId;
|
||||
uint32_t m_preloadObjectId;
|
||||
std::atomic<bool> m_preloadDone;
|
||||
};
|
||||
|
||||
@ -37,6 +37,7 @@ class ScenePlayer {
|
||||
// When p_observerMode is true, participants are only remote performers (no local player).
|
||||
void Play(
|
||||
const AnimInfo* p_animInfo,
|
||||
int8_t p_worldId,
|
||||
AnimCategory p_category,
|
||||
const ParticipantROI* p_participants,
|
||||
uint8_t p_participantCount,
|
||||
|
||||
@ -101,12 +101,12 @@ struct PlayerStateMsg {
|
||||
float direction[3];
|
||||
float up[3];
|
||||
float speed;
|
||||
uint8_t walkAnimId; // Index into walk animation table (0 = default)
|
||||
uint8_t idleAnimId; // Index into idle animation table (0 = default)
|
||||
uint8_t walkAnimId; // Index into walk animation table (0 = default)
|
||||
uint8_t idleAnimId; // Index into idle animation table (0 = default)
|
||||
char name[USERNAME_BUFFER_SIZE]; // Player display name (7 chars + null terminator)
|
||||
uint8_t displayActorIndex; // Index into g_actorInfoInit (0-65)
|
||||
uint8_t customizeData[5]; // Packed CustomizeState
|
||||
uint8_t customizeFlags; // Bit 0 = allowRemoteCustomize
|
||||
uint8_t displayActorIndex; // Index into g_actorInfoInit (0-65)
|
||||
uint8_t customizeData[5]; // Packed CustomizeState
|
||||
uint8_t customizeFlags; // Bit 0 = allowRemoteCustomize
|
||||
};
|
||||
|
||||
// Server -> all: announces which peer is the host
|
||||
@ -202,15 +202,15 @@ struct AnimStartMsg {
|
||||
// Per-participant data in AnimCompleteMsg
|
||||
struct AnimCompletionParticipant {
|
||||
uint32_t peerId;
|
||||
int8_t charIndex; // Participant's character (g_actorInfoInit index)
|
||||
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
|
||||
uint32_t objectId; // SI file object ID (stable, used as frontend key)
|
||||
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];
|
||||
};
|
||||
|
||||
@ -2,11 +2,23 @@
|
||||
|
||||
#include "actions/isle_actions.h"
|
||||
#include "decomp.h"
|
||||
#include "extensions/common/pathutils.h"
|
||||
#include "legoactors.h"
|
||||
#include "legoanimationmanager.h"
|
||||
#include "legomain.h"
|
||||
#include "legomodelpresenter.h"
|
||||
#include "legopartpresenter.h"
|
||||
#include "legovideomanager.h"
|
||||
#include "misc.h"
|
||||
#include "misc/legostorage.h"
|
||||
#include "modeldb/modeldb.h"
|
||||
#include "mxdsaction.h"
|
||||
#include "mxdschunk.h"
|
||||
#include "roi/legoroi.h"
|
||||
#include "viewmanager/viewlodlist.h"
|
||||
|
||||
#include <SDL3/SDL_iostream.h>
|
||||
#include <SDL3/SDL_log.h>
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
|
||||
using namespace Multiplayer::Animation;
|
||||
@ -61,40 +73,161 @@ std::vector<int8_t> Multiplayer::Animation::GetPerformerIndices(uint64_t p_perfo
|
||||
return indices;
|
||||
}
|
||||
|
||||
void Catalog::Refresh(LegoAnimationManager* p_am)
|
||||
uint8_t Catalog::WorldIdToSlot(int8_t p_worldId)
|
||||
{
|
||||
switch (p_worldId) {
|
||||
case LegoOmni::e_act1:
|
||||
return WORLD_SLOT_ACT1;
|
||||
case LegoOmni::e_act2:
|
||||
return WORLD_SLOT_ACT2;
|
||||
case LegoOmni::e_act3:
|
||||
return WORLD_SLOT_ACT3;
|
||||
default:
|
||||
return 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
Catalog::~Catalog()
|
||||
{
|
||||
Cleanup();
|
||||
}
|
||||
|
||||
void Catalog::Cleanup()
|
||||
{
|
||||
m_entries.clear();
|
||||
m_locationIndex.clear();
|
||||
m_animsBase = nullptr;
|
||||
m_animCount = 0;
|
||||
|
||||
if (!p_am) {
|
||||
for (auto& wd : m_worldData) {
|
||||
FreeAnimInfo(wd.anims, wd.animCount);
|
||||
}
|
||||
m_worldData.clear();
|
||||
|
||||
for (auto* roi : m_modelROIs) {
|
||||
VideoManager()->Get3DManager()->Remove(*roi);
|
||||
delete roi;
|
||||
}
|
||||
m_modelROIs.clear();
|
||||
}
|
||||
|
||||
void Catalog::FreeAnimInfo(AnimInfo* p_anims, uint16_t p_count)
|
||||
{
|
||||
if (!p_anims) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_animCount = p_am->m_animCount;
|
||||
m_animsBase = p_am->m_anims;
|
||||
for (uint16_t i = 0; i < p_count; i++) {
|
||||
delete[] p_anims[i].m_name;
|
||||
if (p_anims[i].m_models) {
|
||||
for (uint8_t j = 0; j < p_anims[i].m_modelCount; j++) {
|
||||
delete[] p_anims[i].m_models[j].m_name;
|
||||
}
|
||||
delete[] p_anims[i].m_models;
|
||||
}
|
||||
}
|
||||
delete[] p_anims;
|
||||
}
|
||||
|
||||
if (!m_animsBase || m_animCount == 0) {
|
||||
bool Catalog::ParseDTAFile(int8_t p_worldId, AnimInfo*& p_outAnims, uint16_t& p_outCount)
|
||||
{
|
||||
p_outAnims = nullptr;
|
||||
p_outCount = 0;
|
||||
|
||||
const char* worldName = Lego()->GetWorldName((LegoOmni::World) p_worldId);
|
||||
if (!worldName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char relativePath[128];
|
||||
SDL_snprintf(relativePath, sizeof(relativePath), "\\lego\\data\\%sinf.dta", worldName);
|
||||
|
||||
MxString path;
|
||||
if (!Extensions::Common::ResolveGamePath(relativePath, path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LegoFile storage;
|
||||
if (storage.Open(path.GetData(), LegoStorage::c_read) != SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MxU32 version;
|
||||
if (storage.Read(&version, sizeof(MxU32)) != SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (version != 3) {
|
||||
SDL_Log("DTA version mismatch for world %s: expected 3, got %u", worldName, version);
|
||||
return false;
|
||||
}
|
||||
|
||||
MxU16 animCount;
|
||||
if (storage.Read(&animCount, sizeof(MxU16)) != SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (animCount == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AnimInfo* anims = new AnimInfo[animCount];
|
||||
SDL_memset(anims, 0, animCount * sizeof(AnimInfo));
|
||||
|
||||
for (uint16_t i = 0; i < animCount; i++) {
|
||||
if (AnimationManager()->ReadAnimInfo(&storage, &anims[i]) != SUCCESS) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
// Compute derived fields (mirrors LoadWorldInfo logic)
|
||||
anims[i].m_characterIndex = -1;
|
||||
anims[i].m_unk0x29 = FALSE;
|
||||
for (int k = 0; k < 3; k++) {
|
||||
anims[i].m_unk0x2a[k] = -1;
|
||||
}
|
||||
|
||||
// Compute vehicle indices from model names
|
||||
int vehicleCount = 0;
|
||||
for (uint8_t m = 0; m < anims[i].m_modelCount && vehicleCount < 3; m++) {
|
||||
MxU32 vehicleIdx;
|
||||
if (AnimationManager()->FindVehicle(anims[i].m_models[m].m_name, vehicleIdx) &&
|
||||
anims[i].m_models[m].m_unk0x2c) {
|
||||
anims[i].m_unk0x2a[vehicleCount++] = (MxS8) vehicleIdx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p_outAnims = anims;
|
||||
p_outCount = animCount;
|
||||
return true;
|
||||
|
||||
fail:
|
||||
FreeAnimInfo(anims, animCount);
|
||||
return false;
|
||||
}
|
||||
|
||||
void Catalog::BuildEntries(const WorldAnimData& p_world)
|
||||
{
|
||||
if (!p_world.anims || p_world.animCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint16_t i = 0; i < m_animCount; i++) {
|
||||
if (!m_animsBase[i].m_name || m_animsBase[i].m_objectId == 0) {
|
||||
for (uint16_t i = 0; i < p_world.animCount; i++) {
|
||||
const AnimInfo& animInfo = p_world.anims[i];
|
||||
if (!animInfo.m_name || animInfo.m_objectId == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CatalogEntry entry;
|
||||
entry.animIndex = i;
|
||||
entry.spectatorMask = m_animsBase[i].m_unk0x0c;
|
||||
entry.location = m_animsBase[i].m_location;
|
||||
entry.modelCount = m_animsBase[i].m_modelCount;
|
||||
entry.animIndex = WorldAnimIndex(p_world.worldSlot, i);
|
||||
entry.worldId = p_world.worldId;
|
||||
entry.spectatorMask = animInfo.m_unk0x0c;
|
||||
entry.location = animInfo.m_location;
|
||||
entry.modelCount = animInfo.m_modelCount;
|
||||
|
||||
// Compute performerMask by matching models against g_actorInfoInit[].m_name
|
||||
entry.performerMask = 0;
|
||||
for (uint8_t m = 0; m < entry.modelCount; m++) {
|
||||
if (m_animsBase[i].m_models && m_animsBase[i].m_models[m].m_name) {
|
||||
int8_t charIdx = GetCharacterIndex(m_animsBase[i].m_models[m].m_name);
|
||||
if (animInfo.m_models && animInfo.m_models[m].m_name) {
|
||||
int8_t charIdx = GetCharacterIndex(animInfo.m_models[m].m_name);
|
||||
if (charIdx >= 0) {
|
||||
entry.performerMask |= (uint64_t(1) << charIdx);
|
||||
}
|
||||
@ -102,12 +235,10 @@ void Catalog::Refresh(LegoAnimationManager* p_am)
|
||||
}
|
||||
|
||||
// Compute vehicleMask from the pre-populated vehicle list (m_unk0x2a).
|
||||
// Each entry is a g_vehicles[] index set during LoadWorldInfo for models
|
||||
// with m_unk0x2c=1 that match a known vehicle name.
|
||||
entry.vehicleMask = 0;
|
||||
for (int k = 0; k < 3; k++) {
|
||||
if (m_animsBase[i].m_unk0x2a[k] >= 0 && m_animsBase[i].m_unk0x2a[k] < (int8_t) sizeOfArray(g_vehicles)) {
|
||||
entry.vehicleMask |= (1 << m_animsBase[i].m_unk0x2a[k]);
|
||||
if (animInfo.m_unk0x2a[k] >= 0 && animInfo.m_unk0x2a[k] < (int8_t) sizeOfArray(g_vehicles)) {
|
||||
entry.vehicleMask |= (1 << animInfo.m_unk0x2a[k]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,23 +253,31 @@ void Catalog::Refresh(LegoAnimationManager* p_am)
|
||||
|
||||
// Manual overrides for prop-only animations that have no character
|
||||
// performers but are valid scene animations with spectator-only slots.
|
||||
MxU32 objectId = m_animsBase[i].m_objectId;
|
||||
if (objectId == IsleScript::c_snsx31sh_RunAnim || objectId == IsleScript::c_fpz166p1_RunAnim ||
|
||||
objectId == IsleScript::c_nic002pr_RunAnim || objectId == IsleScript::c_nic003pr_RunAnim ||
|
||||
objectId == IsleScript::c_nic004pr_RunAnim || objectId == IsleScript::c_prp101pr_RunAnim) {
|
||||
if (objectId == IsleScript::c_prp101pr_RunAnim) {
|
||||
entry.location = 11; // Hospital
|
||||
// These are Isle-specific (ACT1) object IDs.
|
||||
bool overridden = false;
|
||||
if (p_world.worldId == LegoOmni::e_act1) {
|
||||
MxU32 objectId = animInfo.m_objectId;
|
||||
if (objectId == IsleScript::c_snsx31sh_RunAnim || objectId == IsleScript::c_fpz166p1_RunAnim ||
|
||||
objectId == IsleScript::c_nic002pr_RunAnim || objectId == IsleScript::c_nic003pr_RunAnim ||
|
||||
objectId == IsleScript::c_nic004pr_RunAnim || objectId == IsleScript::c_prp101pr_RunAnim) {
|
||||
if (objectId == IsleScript::c_prp101pr_RunAnim) {
|
||||
entry.location = 11; // Hospital
|
||||
}
|
||||
entry.category = e_camAnim;
|
||||
overridden = true;
|
||||
}
|
||||
entry.category = e_camAnim;
|
||||
}
|
||||
else if (!hasNamedPerformer) {
|
||||
entry.category = e_otherAnim;
|
||||
}
|
||||
else if (entry.location == -1) {
|
||||
entry.category = e_npcAnim;
|
||||
}
|
||||
else {
|
||||
entry.category = e_camAnim;
|
||||
|
||||
if (!overridden) {
|
||||
if (!hasNamedPerformer) {
|
||||
entry.category = e_otherAnim;
|
||||
}
|
||||
else if (entry.location == -1) {
|
||||
entry.category = e_npcAnim;
|
||||
}
|
||||
else {
|
||||
entry.category = e_camAnim;
|
||||
}
|
||||
}
|
||||
|
||||
size_t idx = m_entries.size();
|
||||
@ -149,12 +288,56 @@ void Catalog::Refresh(LegoAnimationManager* p_am)
|
||||
}
|
||||
}
|
||||
|
||||
void Catalog::Refresh()
|
||||
{
|
||||
Cleanup();
|
||||
|
||||
static const int8_t worldIds[] = {
|
||||
(int8_t) LegoOmni::e_act1,
|
||||
(int8_t) LegoOmni::e_act2,
|
||||
(int8_t) LegoOmni::e_act3,
|
||||
};
|
||||
|
||||
for (int w = 0; w < (int) sizeOfArray(worldIds); w++) {
|
||||
int8_t worldId = worldIds[w];
|
||||
uint8_t slot = WorldIdToSlot(worldId);
|
||||
if (slot == 0xFF) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AnimInfo* anims = nullptr;
|
||||
uint16_t count = 0;
|
||||
if (!ParseDTAFile(worldId, anims, count)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
WorldAnimData wd;
|
||||
wd.worldId = worldId;
|
||||
wd.worldSlot = slot;
|
||||
wd.anims = anims;
|
||||
wd.animCount = count;
|
||||
m_worldData.push_back(wd);
|
||||
|
||||
BuildEntries(wd);
|
||||
}
|
||||
|
||||
LoadWorldParts();
|
||||
}
|
||||
|
||||
const AnimInfo* Catalog::GetAnimInfo(uint16_t p_animIndex) const
|
||||
{
|
||||
if (!m_animsBase || p_animIndex >= m_animCount) {
|
||||
return nullptr;
|
||||
uint8_t slot = GetWorldSlot(p_animIndex);
|
||||
uint16_t localIndex = GetLocalIndex(p_animIndex);
|
||||
|
||||
for (const auto& wd : m_worldData) {
|
||||
if (wd.worldSlot == slot) {
|
||||
if (localIndex < wd.animCount) {
|
||||
return &wd.anims[localIndex];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return &m_animsBase[p_animIndex];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int8_t Catalog::DisplayActorToCharacterIndex(uint8_t p_displayActorIndex)
|
||||
@ -225,8 +408,8 @@ bool Catalog::CheckVehicleEligibility(const CatalogEntry* p_entry, int8_t p_char
|
||||
case e_onOwnVehicle:
|
||||
return animUsesVehicle; // Only animations that use this character's vehicle
|
||||
case e_onOtherVehicle:
|
||||
return false; // On a foreign vehicle — no animations eligible
|
||||
default: // e_onFoot
|
||||
return false; // On a foreign vehicle — no animations eligible
|
||||
default: // e_onFoot
|
||||
return !animUsesVehicle; // Only animations that don't use this character's vehicle
|
||||
}
|
||||
}
|
||||
@ -339,3 +522,112 @@ bool Catalog::CanTrigger(
|
||||
|
||||
return allPerformersCovered && *p_spectatorFilled;
|
||||
}
|
||||
|
||||
void Catalog::LoadWorldParts()
|
||||
{
|
||||
MxString wdbPath;
|
||||
if (!Extensions::Common::ResolveGamePath("\\lego\\data\\world.wdb", wdbPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_IOStream* wdbFile = SDL_IOFromFile(wdbPath.GetData(), "rb");
|
||||
if (!wdbFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
ModelDbWorld* worlds = nullptr;
|
||||
MxS32 numWorlds = 0;
|
||||
ReadModelDbWorlds(wdbFile, worlds, numWorlds);
|
||||
|
||||
if (!worlds || numWorlds == 0) {
|
||||
SDL_CloseIO(wdbFile);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip the global textures + parts section (same offset cached by the game)
|
||||
// We need to read it if the game hasn't already (g_wdbSkipGlobalPartsOffset == 0),
|
||||
// but the game always loads before us, so just skip past it.
|
||||
// The game's LoadWorld() sets g_wdbSkipGlobalPartsOffset after reading globals.
|
||||
|
||||
for (MxS32 i = 0; i < numWorlds; i++) {
|
||||
// Load parts from all worlds (skip check: Lookup returns non-null if already registered)
|
||||
ModelDbPartListCursor cursor(worlds[i].m_partList);
|
||||
ModelDbPart* part;
|
||||
|
||||
while (cursor.Next(part)) {
|
||||
ViewLODList* existing = GetViewLODListManager()->Lookup(part->m_roiName.GetData());
|
||||
if (existing) {
|
||||
existing->Release();
|
||||
continue;
|
||||
}
|
||||
|
||||
MxU8* buff = new MxU8[part->m_partDataLength];
|
||||
SDL_SeekIO(wdbFile, part->m_partDataOffset, SDL_IO_SEEK_SET);
|
||||
if (SDL_ReadIO(wdbFile, buff, part->m_partDataLength) != part->m_partDataLength) {
|
||||
delete[] buff;
|
||||
continue;
|
||||
}
|
||||
|
||||
MxDSChunk chunk;
|
||||
chunk.SetLength(part->m_partDataLength);
|
||||
chunk.SetData(buff);
|
||||
|
||||
LegoPartPresenter partPresenter;
|
||||
if (partPresenter.Read(chunk) == SUCCESS) {
|
||||
partPresenter.Store();
|
||||
}
|
||||
|
||||
delete[] buff;
|
||||
}
|
||||
|
||||
// Load models whose LODs aren't registered yet
|
||||
for (MxS32 j = 0; j < worlds[i].m_numModels; j++) {
|
||||
ModelDbModel& model = worlds[i].m_models[j];
|
||||
if (!model.m_modelName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only load models that aren't already available as LODs
|
||||
char loweredName[256];
|
||||
SDL_strlcpy(loweredName, model.m_modelName, sizeof(loweredName));
|
||||
SDL_strlwr(loweredName);
|
||||
|
||||
ViewLODList* existing = GetViewLODListManager()->Lookup(loweredName);
|
||||
if (existing) {
|
||||
existing->Release();
|
||||
continue;
|
||||
}
|
||||
|
||||
MxU8* buff = new MxU8[model.m_modelDataLength];
|
||||
SDL_SeekIO(wdbFile, model.m_modelDataOffset, SDL_IO_SEEK_SET);
|
||||
if (SDL_ReadIO(wdbFile, buff, model.m_modelDataLength) != model.m_modelDataLength) {
|
||||
delete[] buff;
|
||||
continue;
|
||||
}
|
||||
|
||||
MxDSChunk chunk;
|
||||
chunk.SetLength(model.m_modelDataLength);
|
||||
chunk.SetData(buff);
|
||||
|
||||
// Use friend access to LegoModelPresenter's private CreateROI + m_roi
|
||||
LegoModelPresenter modelPresenter;
|
||||
MxDSAction action;
|
||||
modelPresenter.SetAction(&action);
|
||||
|
||||
if (modelPresenter.CreateROI(&chunk) == SUCCESS && modelPresenter.m_roi) {
|
||||
// Add to 3D scene (hidden) so ScenePlayer::cloneSceneROI can find it
|
||||
modelPresenter.m_roi->SetVisibility(FALSE);
|
||||
VideoManager()->Get3DManager()->Add(*modelPresenter.m_roi);
|
||||
|
||||
// Steal the ROI to keep it alive (Destroy() just nulls m_roi)
|
||||
m_modelROIs.push_back(modelPresenter.m_roi);
|
||||
modelPresenter.m_roi = nullptr;
|
||||
}
|
||||
|
||||
delete[] buff;
|
||||
}
|
||||
}
|
||||
|
||||
FreeModelDbWorlds(worlds, numWorlds);
|
||||
SDL_CloseIO(wdbFile);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
#include "anim/legoanim.h"
|
||||
#include "flic.h"
|
||||
#include "legomain.h"
|
||||
#include "misc/legostorage.h"
|
||||
#include "mxautolock.h"
|
||||
|
||||
@ -92,13 +93,98 @@ SceneAnimData& SceneAnimData::operator=(SceneAnimData&& p_other) noexcept
|
||||
return *this;
|
||||
}
|
||||
|
||||
Loader::Loader() : m_reader(nullptr), m_preloadThread(nullptr), m_preloadObjectId(0), m_preloadDone(false)
|
||||
Loader::Loader()
|
||||
: m_reader(nullptr), m_preloadThread(nullptr), m_preloadWorldId(0), m_preloadObjectId(0), m_preloadDone(false)
|
||||
{
|
||||
}
|
||||
|
||||
Loader::~Loader()
|
||||
{
|
||||
CleanupPreloadThread();
|
||||
|
||||
for (auto& pair : m_extraSI) {
|
||||
delete pair.second.interleaf;
|
||||
delete pair.second.file;
|
||||
}
|
||||
}
|
||||
|
||||
const char* Loader::GetSIPath(int8_t p_worldId)
|
||||
{
|
||||
switch (p_worldId) {
|
||||
case LegoOmni::e_act1:
|
||||
return "\\lego\\scripts\\isle\\isle.si";
|
||||
case LegoOmni::e_act2:
|
||||
return "\\lego\\scripts\\act2\\act2main.si";
|
||||
case LegoOmni::e_act3:
|
||||
return "\\lego\\scripts\\act3\\act3.si";
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool Loader::OpenWorldSI(int8_t p_worldId)
|
||||
{
|
||||
// Act1 uses the external SIReader
|
||||
if (p_worldId == LegoOmni::e_act1) {
|
||||
return m_reader && m_reader->Open();
|
||||
}
|
||||
|
||||
auto it = m_extraSI.find(p_worldId);
|
||||
if (it != m_extraSI.end() && it->second.ready) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* siPath = GetSIPath(p_worldId);
|
||||
if (!siPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SIHandle handle = {nullptr, nullptr, false};
|
||||
if (!SIReader::OpenHeaderOnly(siPath, handle.file, handle.interleaf)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
handle.ready = true;
|
||||
m_extraSI[p_worldId] = handle;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Loader::ReadWorldObject(int8_t p_worldId, uint32_t p_objectId, si::Object*& p_outObj)
|
||||
{
|
||||
p_outObj = nullptr;
|
||||
|
||||
if (p_worldId == LegoOmni::e_act1) {
|
||||
// Act1: use external SIReader
|
||||
if (!m_reader || !m_reader->ReadObject(p_objectId)) {
|
||||
return false;
|
||||
}
|
||||
p_outObj = m_reader->GetObject(p_objectId);
|
||||
return p_outObj != nullptr;
|
||||
}
|
||||
|
||||
auto it = m_extraSI.find(p_worldId);
|
||||
if (it == m_extraSI.end() || !it->second.ready) {
|
||||
return false;
|
||||
}
|
||||
|
||||
si::Interleaf* interleaf = it->second.interleaf;
|
||||
si::File* file = it->second.file;
|
||||
|
||||
size_t childCount = interleaf->GetChildCount();
|
||||
if (p_objectId >= childCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
si::Object* obj = static_cast<si::Object*>(interleaf->GetChildAt(p_objectId));
|
||||
if (obj->type() == si::MxOb::Null) {
|
||||
if (interleaf->ReadObject(file, p_objectId) != si::Interleaf::ERROR_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
obj = static_cast<si::Object*>(interleaf->GetChildAt(p_objectId));
|
||||
}
|
||||
|
||||
p_outObj = obj;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Loader::ParseAnimationChild(si::Object* p_child, SceneAnimData& p_data)
|
||||
@ -233,38 +319,36 @@ bool Loader::ParseComposite(si::Object* p_composite, SceneAnimData& p_data)
|
||||
return hasAnim;
|
||||
}
|
||||
|
||||
SceneAnimData* Loader::EnsureCached(uint32_t p_objectId)
|
||||
SceneAnimData* Loader::EnsureCached(int8_t p_worldId, uint32_t p_objectId)
|
||||
{
|
||||
uint64_t key = CacheKey(p_worldId, p_objectId);
|
||||
|
||||
{
|
||||
AUTOLOCK(m_cacheCS);
|
||||
auto it = m_cache.find(p_objectId);
|
||||
auto it = m_cache.find(key);
|
||||
if (it != m_cache.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
}
|
||||
|
||||
// If a preload is in progress for this object, wait for it to finish
|
||||
if (m_preloadThread && m_preloadObjectId == p_objectId) {
|
||||
if (m_preloadThread && m_preloadWorldId == p_worldId && m_preloadObjectId == p_objectId) {
|
||||
CleanupPreloadThread();
|
||||
|
||||
AUTOLOCK(m_cacheCS);
|
||||
auto it = m_cache.find(p_objectId);
|
||||
auto it = m_cache.find(key);
|
||||
if (it != m_cache.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
// Preload failed — fall through to synchronous load
|
||||
}
|
||||
|
||||
if (!m_reader || !m_reader->Open()) {
|
||||
if (!OpenWorldSI(p_worldId)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!m_reader->ReadObject(p_objectId)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
si::Object* composite = m_reader->GetObject(p_objectId);
|
||||
if (!composite) {
|
||||
si::Object* composite = nullptr;
|
||||
if (!ReadWorldObject(p_worldId, p_objectId, composite)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@ -274,7 +358,7 @@ SceneAnimData* Loader::EnsureCached(uint32_t p_objectId)
|
||||
}
|
||||
|
||||
AUTOLOCK(m_cacheCS);
|
||||
auto result = m_cache.emplace(p_objectId, std::move(data));
|
||||
auto result = m_cache.emplace(key, std::move(data));
|
||||
return &result.first->second;
|
||||
}
|
||||
|
||||
@ -286,37 +370,47 @@ void Loader::CleanupPreloadThread()
|
||||
}
|
||||
}
|
||||
|
||||
void Loader::PreloadAsync(uint32_t p_objectId)
|
||||
void Loader::PreloadAsync(int8_t p_worldId, uint32_t p_objectId)
|
||||
{
|
||||
uint64_t key = CacheKey(p_worldId, p_objectId);
|
||||
|
||||
{
|
||||
AUTOLOCK(m_cacheCS);
|
||||
if (m_cache.find(p_objectId) != m_cache.end()) {
|
||||
if (m_cache.find(key) != m_cache.end()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_preloadThread && m_preloadObjectId == p_objectId && !m_preloadDone) {
|
||||
if (m_preloadThread && m_preloadWorldId == p_worldId && m_preloadObjectId == p_objectId && !m_preloadDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
CleanupPreloadThread();
|
||||
|
||||
m_preloadWorldId = p_worldId;
|
||||
m_preloadObjectId = p_objectId;
|
||||
m_preloadDone = false;
|
||||
m_preloadThread = new PreloadThread(this, p_objectId);
|
||||
m_preloadThread = new PreloadThread(this, p_worldId, p_objectId);
|
||||
m_preloadThread->Start(0x1000, 0);
|
||||
}
|
||||
|
||||
Loader::PreloadThread::PreloadThread(Loader* p_loader, uint32_t p_objectId) : m_loader(p_loader), m_objectId(p_objectId)
|
||||
Loader::PreloadThread::PreloadThread(Loader* p_loader, int8_t p_worldId, uint32_t p_objectId)
|
||||
: m_loader(p_loader), m_worldId(p_worldId), m_objectId(p_objectId)
|
||||
{
|
||||
}
|
||||
|
||||
MxResult Loader::PreloadThread::Run()
|
||||
{
|
||||
const char* siPath = GetSIPath(m_worldId);
|
||||
if (!siPath) {
|
||||
m_loader->m_preloadDone = true;
|
||||
return MxThread::Run();
|
||||
}
|
||||
|
||||
si::File* siFile = nullptr;
|
||||
si::Interleaf* interleaf = nullptr;
|
||||
|
||||
if (!SIReader::OpenHeaderOnly("\\lego\\scripts\\isle\\isle.si", siFile, interleaf)) {
|
||||
if (!SIReader::OpenHeaderOnly(siPath, siFile, interleaf)) {
|
||||
m_loader->m_preloadDone = true;
|
||||
return MxThread::Run();
|
||||
}
|
||||
@ -327,8 +421,9 @@ MxResult Loader::PreloadThread::Run()
|
||||
|
||||
SceneAnimData data;
|
||||
if (ParseComposite(composite, data)) {
|
||||
uint64_t key = CacheKey(m_worldId, m_objectId);
|
||||
AUTOLOCK(m_loader->m_cacheCS);
|
||||
m_loader->m_cache.emplace(m_objectId, std::move(data));
|
||||
m_loader->m_cache.emplace(key, std::move(data));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -47,7 +47,8 @@ void PhonemePlayer::Init(
|
||||
const std::vector<std::pair<std::string, LegoROI*>>& p_actorAliases
|
||||
)
|
||||
{
|
||||
for (auto& track : p_tracks) {
|
||||
for (size_t trackIdx = 0; trackIdx < p_tracks.size(); trackIdx++) {
|
||||
auto& track = p_tracks[trackIdx];
|
||||
PhonemeState state;
|
||||
state.targetROI = nullptr;
|
||||
state.originalTexture = nullptr;
|
||||
@ -63,6 +64,25 @@ void PhonemePlayer::Init(
|
||||
}
|
||||
state.targetROI = targetROI;
|
||||
|
||||
// If a previous track already set up a cached texture for this ROI, reuse it.
|
||||
// Otherwise the second track's "original" would be the first track's cached texture,
|
||||
// causing a use-after-free during cleanup.
|
||||
PhonemeState* existing = nullptr;
|
||||
for (size_t j = 0; j < m_states.size(); j++) {
|
||||
if (m_states[j].targetROI == targetROI && m_states[j].cachedTexture) {
|
||||
existing = &m_states[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
state.cachedTexture = existing->cachedTexture;
|
||||
state.bitmap = new MxBitmap();
|
||||
state.bitmap->SetSize(track.width, track.height, NULL, FALSE);
|
||||
m_states.push_back(state);
|
||||
continue;
|
||||
}
|
||||
|
||||
LegoROI* head = targetROI->FindChildROI("head", targetROI);
|
||||
if (!head) {
|
||||
m_states.push_back(state);
|
||||
@ -173,11 +193,13 @@ void PhonemePlayer::Cleanup()
|
||||
for (size_t i = 0; i < m_states.size(); i++) {
|
||||
auto& state = m_states[i];
|
||||
|
||||
// Only the state that owns the original texture (i.e. performed the initial setup)
|
||||
// should restore and erase. Other states sharing the same cachedTexture are secondary.
|
||||
if (state.targetROI && state.originalTexture) {
|
||||
CharacterManager()->SetHeadTexture(state.targetROI, state.originalTexture);
|
||||
}
|
||||
|
||||
if (state.cachedTexture) {
|
||||
if (state.originalTexture && state.cachedTexture) {
|
||||
TextureContainer()->EraseCached(state.cachedTexture);
|
||||
}
|
||||
|
||||
|
||||
@ -22,8 +22,8 @@
|
||||
#include <SDL3/SDL_timer.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
using namespace Multiplayer::Animation;
|
||||
@ -267,6 +267,7 @@ void ScenePlayer::SetupROIs(const AnimInfo* p_animInfo)
|
||||
|
||||
void ScenePlayer::Play(
|
||||
const AnimInfo* p_animInfo,
|
||||
int8_t p_worldId,
|
||||
AnimCategory p_category,
|
||||
const ParticipantROI* p_participants,
|
||||
uint8_t p_participantCount,
|
||||
@ -281,7 +282,7 @@ void ScenePlayer::Play(
|
||||
return;
|
||||
}
|
||||
|
||||
SceneAnimData* data = m_loader->EnsureCached(p_animInfo->m_objectId);
|
||||
SceneAnimData* data = m_loader->EnsureCached(p_worldId, p_animInfo->m_objectId);
|
||||
if (!data || !data->anim) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -381,12 +381,10 @@ void NetworkManager::OnWorldEnabled(LegoWorld* p_world)
|
||||
NotifyPlayerCountChanged();
|
||||
EnforceDisableNPCs();
|
||||
|
||||
// Refresh animation catalog from the animation manager
|
||||
if (AnimationManager()) {
|
||||
m_animCatalog.Refresh(AnimationManager());
|
||||
m_animCoordinator.SetCatalog(&m_animCatalog);
|
||||
m_animSessionHost.SetCatalog(&m_animCatalog);
|
||||
}
|
||||
// Refresh animation catalog from DTA files for all supported worlds
|
||||
m_animCatalog.Refresh();
|
||||
m_animCoordinator.SetCatalog(&m_animCatalog);
|
||||
m_animSessionHost.SetCatalog(&m_animCatalog);
|
||||
|
||||
m_locationProximity.Reset();
|
||||
PreloadHornSounds();
|
||||
@ -1515,9 +1513,10 @@ void NetworkManager::TickHostSessions()
|
||||
m_animSessionHost.StartCountdown(animIndex);
|
||||
|
||||
if (m_animCoordinator.IsLocalPlayerInSession(animIndex)) {
|
||||
const AnimInfo* ai = m_animCatalog.GetAnimInfo(animIndex);
|
||||
const Animation::CatalogEntry* ce = m_animCatalog.FindEntry(animIndex);
|
||||
const AnimInfo* ai = ce ? m_animCatalog.GetAnimInfo(animIndex) : nullptr;
|
||||
if (ai) {
|
||||
m_animLoader.PreloadAsync(ai->m_objectId);
|
||||
m_animLoader.PreloadAsync(ce->worldId, ai->m_objectId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1644,9 +1643,10 @@ void NetworkManager::HandleAnimUpdate(const AnimUpdateMsg& p_msg)
|
||||
m_animCoordinator.ApplySessionUpdate(p_msg.animIndex, p_msg.state, p_msg.countdownMs, slots, p_msg.slotCount);
|
||||
|
||||
if (p_msg.state == static_cast<uint8_t>(Animation::CoordinationState::e_countdown)) {
|
||||
const AnimInfo* ai = m_animCatalog.GetAnimInfo(p_msg.animIndex);
|
||||
const Animation::CatalogEntry* ce = m_animCatalog.FindEntry(p_msg.animIndex);
|
||||
const AnimInfo* ai = ce ? m_animCatalog.GetAnimInfo(p_msg.animIndex) : nullptr;
|
||||
if (ai) {
|
||||
m_animLoader.PreloadAsync(ai->m_objectId);
|
||||
m_animLoader.PreloadAsync(ce->worldId, ai->m_objectId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1783,7 +1783,14 @@ void NetworkManager::HandleAnimStartLocally(uint16_t p_animIndex, bool p_localIn
|
||||
});
|
||||
}
|
||||
|
||||
scenePlayer->Play(animInfo, entry->category, participants.data(), (uint8_t) participants.size(), observerMode);
|
||||
scenePlayer->Play(
|
||||
animInfo,
|
||||
entry->worldId,
|
||||
entry->category,
|
||||
participants.data(),
|
||||
(uint8_t) participants.size(),
|
||||
observerMode
|
||||
);
|
||||
|
||||
if (!scenePlayer->IsPlaying()) {
|
||||
if (!observerMode) {
|
||||
@ -1860,7 +1867,7 @@ void NetworkManager::BroadcastAnimComplete(uint16_t p_animIndex)
|
||||
AnimCompleteMsg msg{};
|
||||
msg.header = {MSG_ANIM_COMPLETE, 0, m_localPeerId, m_sequence++, TARGET_BROADCAST};
|
||||
msg.eventId = (static_cast<uint64_t>(SDL_rand_bits()) << 32) | static_cast<uint64_t>(SDL_rand_bits());
|
||||
msg.objectId = animInfo->m_objectId;
|
||||
msg.animIndex = p_animIndex;
|
||||
msg.participantCount = 0;
|
||||
|
||||
char localName[8];
|
||||
@ -1940,8 +1947,8 @@ void NetworkManager::HandleAnimComplete(const AnimCompleteMsg& p_msg)
|
||||
|
||||
std::string json = "{\"eventId\":\"";
|
||||
json += eventIdHex;
|
||||
json += "\",\"objectId\":";
|
||||
json += std::to_string(p_msg.objectId);
|
||||
json += "\",\"animIndex\":";
|
||||
json += std::to_string(p_msg.animIndex);
|
||||
json += ",\"participants\":[";
|
||||
|
||||
// Emit local player first so frontend can rely on participants[0] being self
|
||||
@ -2189,8 +2196,6 @@ static void BuildAnimationJson(
|
||||
p_json += std::to_string(p_info.animIndex);
|
||||
p_json += ",\"name\":";
|
||||
JsonAppendString(p_json, p_animInfo->m_name ? p_animInfo->m_name : "");
|
||||
p_json += ",\"objectId\":";
|
||||
p_json += std::to_string(p_animInfo->m_objectId);
|
||||
p_json += ",\"category\":";
|
||||
p_json += std::to_string(static_cast<uint8_t>(p_info.entry->category));
|
||||
p_json += ",\"eligible\":";
|
||||
|
||||
@ -149,7 +149,7 @@ AudioTrack* SIReader::ExtractFirstAudio(uint32_t p_objectId)
|
||||
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) {
|
||||
if (child->presenter_.find("MxWavePresenter") != std::string::npos) {
|
||||
AudioTrack* track = new AudioTrack();
|
||||
if (ExtractAudioTrack(child, *track)) {
|
||||
return track;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user