Fix vehicle filter bug and refactor: wrong index space in GetCharacterVehicleId

GetCharacterVehicleId used hardcoded g_characters[] indices but was
called with g_actorInfoInit[] indices. Since g_actorInfoInit has an
extra "infoman" entry at index 5, all characters after Laura were
off-by-one — bikers got the wrong vehicle and sy (Shiney Doris) fell
off the switch entirely, disabling filtering completely.

Replace with data-driven lookup (actorInfoInit name → g_characters
vehicleId), consolidate duplicate GetVehicleCategory into Catalog,
remove dead characterIndex field, fix stale g_characters comments,
remove temporary debug logging, and DRY local vehicle state computation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Semmler 2026-03-28 10:33:07 -07:00
parent aa9df3370b
commit 3396d42db8
No known key found for this signature in database
GPG Key ID: 086DAA1360BEEE5C
7 changed files with 42 additions and 94 deletions

View File

@ -16,7 +16,7 @@ enum AnimCategory : uint8_t {
e_otherAnim // no named character performers (ambient/prop-only) e_otherAnim // no named character performers (ambient/prop-only)
}; };
// Number of core playable characters (Pepper, Mama, Papa, Nick, Laura) = g_characters indices 0-4 // Number of core playable characters (Pepper, Mama, Papa, Nick, Laura) = g_actorInfoInit indices 0-4
static const int8_t CORE_CHARACTER_COUNT = 5; static const int8_t CORE_CHARACTER_COUNT = 5;
// Spectator mask with all core characters enabled // Spectator mask with all core characters enabled
@ -32,9 +32,8 @@ struct CatalogEntry {
uint16_t animIndex; // Index into LegoAnimationManager::m_anims[] uint16_t animIndex; // Index into LegoAnimationManager::m_anims[]
AnimCategory category; AnimCategory category;
uint8_t spectatorMask; // Which core actors can trigger (bit0=Pepper..bit4=Laura) uint8_t spectatorMask; // Which core actors can trigger (bit0=Pepper..bit4=Laura)
uint64_t performerMask; // Bitmask of g_characters[] indices that appear as character models uint64_t performerMask; // Bitmask of g_actorInfoInit[] indices that appear as character models
int16_t location; // -1 = anywhere, >= 0 = specific location int16_t location; // -1 = anywhere, >= 0 = specific location
int8_t characterIndex; // Primary character index into g_characters[]
uint8_t modelCount; // Number of models in animation uint8_t modelCount; // Number of models in animation
uint8_t vehicleMask; // Bitmask of g_vehicles[] indices required (bit0=bikebd..bit6=board) uint8_t vehicleMask; // Bitmask of g_vehicles[] indices required (bit0=bikebd..bit6=board)
}; };
@ -50,10 +49,10 @@ class Catalog {
std::vector<const CatalogEntry*> GetAnimationsAtLocation(int16_t p_location) const; std::vector<const CatalogEntry*> GetAnimationsAtLocation(int16_t p_location) const;
// Check if a player can fill any role (spectator or participant) in this animation. // Check if a player can fill any role (spectator or participant) in this animation.
// Accepts a display actor index (converted to g_characters index internally). // Accepts a display actor index (converted to g_actorInfoInit index internally).
bool CanParticipate(const CatalogEntry* p_entry, uint8_t p_displayActorIndex) const; bool CanParticipate(const CatalogEntry* p_entry, uint8_t p_displayActorIndex) const;
// Same check but using a g_characters index directly. // Same check but using a g_actorInfoInit index directly.
static bool CanParticipateChar(const CatalogEntry* p_entry, int8_t p_charIndex); static bool CanParticipateChar(const CatalogEntry* p_entry, int8_t p_charIndex);
// Check if a set of character indices can collectively trigger this animation. // Check if a set of character indices can collectively trigger this animation.
@ -86,7 +85,11 @@ class Catalog {
// Determine the vehicle state for a character given their current ride vehicle ROI. // Determine the vehicle state for a character given their current ride vehicle ROI.
static VehicleState GetVehicleState(int8_t p_charIndex, class LegoROI* p_vehicleROI); static VehicleState GetVehicleState(int8_t p_charIndex, class LegoROI* p_vehicleROI);
// Convert a display actor index to the g_characters[] index used by animations. // Classify a g_vehicles[] index into a vehicle category.
// Returns 0=bike, 1=motorcycle, 2=skateboard, -1=invalid.
static int8_t GetVehicleCategory(int8_t p_vehicleIdx);
// Convert a display actor index to the g_actorInfoInit[] index used by animations.
// Returns -1 if no match. // Returns -1 if no match.
static int8_t DisplayActorToCharacterIndex(uint8_t p_displayActorIndex); static int8_t DisplayActorToCharacterIndex(uint8_t p_displayActorIndex);

View File

@ -23,7 +23,7 @@ struct ParticipantROI {
LegoROI* vehicleROI; // Ride vehicle ROI (bike/board/moto), or nullptr LegoROI* vehicleROI; // Ride vehicle ROI (bike/board/moto), or nullptr
MxMatrix savedTransform; MxMatrix savedTransform;
std::string savedName; std::string savedName;
int8_t charIndex; // g_characters[] 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 charIndex < 0; }
}; };

View File

@ -13,7 +13,7 @@ enum class CoordinationState : uint8_t;
struct SessionSlot { struct SessionSlot {
uint32_t peerId; // 0 = unfilled uint32_t peerId; // 0 = unfilled
int8_t charIndex; // g_characters 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 charIndex < 0; }
}; };

View File

@ -197,7 +197,6 @@ class NetworkManager : public MxCore {
bool m_showNameBubbles; bool m_showNameBubbles;
bool m_lastCameraEnabled; bool m_lastCameraEnabled;
uint8_t m_lastVehicleState; uint8_t m_lastVehicleState;
bool m_vehicleFilterLogPending; // TODO(vehicle-filter): Remove after verification
bool m_wasInRestrictedArea; bool m_wasInRestrictedArea;
// NPC animation playback // NPC animation playback

View File

@ -11,28 +11,28 @@
using namespace Multiplayer::Animation; using namespace Multiplayer::Animation;
// Static mapping of character index to g_vehicles[] index. // Defined in legoanimationmanager.cpp — not exported in headers.
// Mirrors g_characters[].m_vehicleId for characters that own a vehicle. extern LegoAnimationManager::Character g_characters[47];
static int8_t GetCharacterVehicleId(int8_t p_charIndex) extern LegoAnimationManager::Vehicle g_vehicles[7];
// Look up the g_vehicles[] index for a character's owned vehicle.
// p_actorInfoIndex is an index into g_actorInfoInit[].
// Returns -1 if the character has no vehicle.
static int8_t GetCharacterVehicleId(int8_t p_actorInfoIndex)
{ {
switch (p_charIndex) { if (p_actorInfoIndex < 0 || p_actorInfoIndex >= (int8_t) SDL_min(sizeOfArray(g_actorInfoInit), (size_t) 64)) {
case 0:
return 6; // pepper -> board (skateboard)
case 3:
return 4; // nick -> motoni (motorcycle)
case 4:
return 5; // laura -> motola (motorcycle)
case 36:
return 2; // rd -> bikerd
case 37:
return 1; // pg -> bikepg
case 38:
return 0; // bd -> bikebd
case 39:
return 3; // sy -> bikesy
default:
return -1; return -1;
} }
const char* name = g_actorInfoInit[p_actorInfoIndex].m_name;
if (!name) {
return -1;
}
for (int i = 0; i < (int) sizeOfArray(g_characters); i++) {
if (!SDL_strcasecmp(name, g_characters[i].m_name)) {
return g_characters[i].m_vehicleId;
}
}
return -1;
} }
// Exact-match a model name against g_actorInfoInit[].m_name. // Exact-match a model name against g_actorInfoInit[].m_name.
@ -88,7 +88,6 @@ void Catalog::Refresh(LegoAnimationManager* p_am)
entry.animIndex = i; entry.animIndex = i;
entry.spectatorMask = m_animsBase[i].m_unk0x0c; entry.spectatorMask = m_animsBase[i].m_unk0x0c;
entry.location = m_animsBase[i].m_location; entry.location = m_animsBase[i].m_location;
entry.characterIndex = m_animsBase[i].m_characterIndex;
entry.modelCount = m_animsBase[i].m_modelCount; entry.modelCount = m_animsBase[i].m_modelCount;
// Compute performerMask by matching models against g_actorInfoInit[].m_name // Compute performerMask by matching models against g_actorInfoInit[].m_name
@ -107,7 +106,7 @@ void Catalog::Refresh(LegoAnimationManager* p_am)
// with m_unk0x2c=1 that match a known vehicle name. // with m_unk0x2c=1 that match a known vehicle name.
entry.vehicleMask = 0; entry.vehicleMask = 0;
for (int k = 0; k < 3; k++) { for (int k = 0; k < 3; k++) {
if (m_animsBase[i].m_unk0x2a[k] >= 0 && m_animsBase[i].m_unk0x2a[k] < 8) { 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]); entry.vehicleMask |= (1 << m_animsBase[i].m_unk0x2a[k]);
} }
} }
@ -232,17 +231,16 @@ bool Catalog::CheckVehicleEligibility(const CatalogEntry* p_entry, int8_t p_char
} }
} }
// Vehicle category grouping (matches ScenePlayer::GetVehicleCategory) int8_t Catalog::GetVehicleCategory(int8_t p_vehicleIdx)
static int8_t GetVehicleCategory(int8_t p_vehicleIdx)
{ {
if (p_vehicleIdx >= 0 && p_vehicleIdx <= 3) { if (p_vehicleIdx >= 0 && p_vehicleIdx <= 3) {
return 0; // bike return 0; // bike (bikebd, bikepg, bikerd, bikesy)
} }
if (p_vehicleIdx >= 4 && p_vehicleIdx <= 5) { if (p_vehicleIdx >= 4 && p_vehicleIdx <= 5) {
return 1; // motorcycle return 1; // motorcycle (motoni, motola)
} }
if (p_vehicleIdx == 6) { if (p_vehicleIdx == 6) {
return 2; // skateboard return 2; // skateboard (board)
} }
return -1; return -1;
} }

View File

@ -29,27 +29,6 @@ using namespace Multiplayer::Animation;
namespace AnimUtils = Extensions::Common::AnimUtils; namespace AnimUtils = Extensions::Common::AnimUtils;
using Extensions::Common::CharacterCloner; using Extensions::Common::CharacterCloner;
enum VehicleCategory {
e_bike,
e_motorcycle,
e_skateboard,
e_unknownVehicle
};
static VehicleCategory GetVehicleCategory(MxU32 p_vehicleIdx)
{
if (p_vehicleIdx <= 3) {
return e_bike;
}
if (p_vehicleIdx <= 5) {
return e_motorcycle;
}
if (p_vehicleIdx == 6) {
return e_skateboard;
}
return e_unknownVehicle;
}
static bool MatchesCharacter(const std::string& p_actorName, int8_t p_charIndex) static bool MatchesCharacter(const std::string& p_actorName, int8_t p_charIndex)
{ {
if (p_charIndex < 0 || p_charIndex >= (int8_t) sizeOfArray(g_actorInfoInit)) { if (p_charIndex < 0 || p_charIndex >= (int8_t) sizeOfArray(g_actorInfoInit)) {
@ -200,7 +179,8 @@ void ScenePlayer::SetupROIs(const AnimInfo* p_animInfo)
MxU32 perfVehicleIdx; MxU32 perfVehicleIdx;
if (AnimationManager()->FindVehicle(m_participants[p].vehicleROI->GetName(), perfVehicleIdx)) { if (AnimationManager()->FindVehicle(m_participants[p].vehicleROI->GetName(), perfVehicleIdx)) {
if (GetVehicleCategory(animVehicleIdx) == GetVehicleCategory(perfVehicleIdx)) { if (Catalog::GetVehicleCategory((int8_t) animVehicleIdx) ==
Catalog::GetVehicleCategory((int8_t) perfVehicleIdx)) {
m_vehicleROI = m_participants[p].vehicleROI; m_vehicleROI = m_participants[p].vehicleROI;
addAlias(lowered, m_vehicleROI); addAlias(lowered, m_vehicleROI);
roi = m_vehicleROI; roi = m_vehicleROI;

View File

@ -20,7 +20,6 @@
#include "mxticklemanager.h" #include "mxticklemanager.h"
#include "roi/legoroi.h" #include "roi/legoroi.h"
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_stdinc.h> #include <SDL3/SDL_stdinc.h>
#include <SDL3/SDL_timer.h> #include <SDL3/SDL_timer.h>
#include <algorithm> #include <algorithm>
@ -68,8 +67,7 @@ NetworkManager::NetworkManager()
m_inIsleWorld(false), m_registered(false), m_pendingToggleThirdPerson(false), m_pendingToggleNameBubbles(false), m_inIsleWorld(false), m_registered(false), m_pendingToggleThirdPerson(false), m_pendingToggleNameBubbles(false),
m_pendingWalkAnim(-1), m_pendingIdleAnim(-1), m_pendingEmote(-1), m_pendingToggleAllowCustomize(false), m_pendingWalkAnim(-1), m_pendingIdleAnim(-1), m_pendingEmote(-1), m_pendingToggleAllowCustomize(false),
m_pendingAnimInterest(-1), m_pendingAnimCancel(false), m_localPendingAnimInterest(-1), m_showNameBubbles(true), m_pendingAnimInterest(-1), m_pendingAnimCancel(false), m_localPendingAnimInterest(-1), m_showNameBubbles(true),
m_lastCameraEnabled(false), m_lastVehicleState(0), m_vehicleFilterLogPending(false), m_lastCameraEnabled(false), m_lastVehicleState(0), m_wasInRestrictedArea(false), m_animStateDirty(false),
m_wasInRestrictedArea(false), m_animStateDirty(false),
m_animInterestDirty(false), m_lastAnimPushTime(0), m_connectionState(STATE_DISCONNECTED), m_wasRejected(false), m_animInterestDirty(false), m_lastAnimPushTime(0), m_connectionState(STATE_DISCONNECTED), m_wasRejected(false),
m_reconnectAttempt(0), m_reconnectDelay(0), m_nextReconnectTime(0) m_reconnectAttempt(0), m_reconnectDelay(0), m_nextReconnectTime(0)
{ {
@ -127,7 +125,6 @@ MxResult NetworkManager::Tickle()
if (vehicleState != m_lastVehicleState) { if (vehicleState != m_lastVehicleState) {
m_lastVehicleState = vehicleState; m_lastVehicleState = vehicleState;
m_animStateDirty = true; m_animStateDirty = true;
m_vehicleFilterLogPending = true;
// Cancel active session if the current animation is no longer eligible. // Cancel active session if the current animation is no longer eligible.
// Only cancel if the local player is a performer — spectators aren't vehicle-constrained. // Only cancel if the local player is a performer — spectators aren't vehicle-constrained.
@ -2141,11 +2138,13 @@ void NetworkManager::PushAnimationState()
const float* localPos = userActor->GetROI()->GetWorldPosition(); const float* localPos = userActor->GetROI()->GetWorldPosition();
float localX = localPos[0], localZ = localPos[2]; float localX = localPos[0], localZ = localPos[2];
uint8_t localVehicleState = Animation::Catalog::GetVehicleState(localCharIndex, cam->GetRideVehicleROI());
// Build proximity character indices and vehicle state (for NPC anims — position-based, not location-based) // Build proximity character indices and vehicle state (for NPC anims — position-based, not location-based)
std::vector<int8_t> proximityCharIndices; std::vector<int8_t> proximityCharIndices;
std::vector<uint8_t> proximityVehicleState; std::vector<uint8_t> proximityVehicleState;
proximityCharIndices.push_back(localCharIndex); proximityCharIndices.push_back(localCharIndex);
proximityVehicleState.push_back(Animation::Catalog::GetVehicleState(localCharIndex, cam->GetRideVehicleROI())); proximityVehicleState.push_back(localVehicleState);
for (const auto& [peerId, player] : m_remotePlayers) { for (const auto& [peerId, player] : m_remotePlayers) {
if (!player->IsSpawned() || !player->GetROI() || player->GetWorldId() != (int8_t) LegoOmni::e_act1) { if (!player->IsSpawned() || !player->GetROI() || player->GetWorldId() != (int8_t) LegoOmni::e_act1) {
@ -2177,7 +2176,7 @@ void NetworkManager::PushAnimationState()
std::vector<int8_t> locationCharIndices; std::vector<int8_t> locationCharIndices;
std::vector<uint8_t> locationVehicleState; std::vector<uint8_t> locationVehicleState;
locationCharIndices.push_back(localCharIndex); locationCharIndices.push_back(localCharIndex);
locationVehicleState.push_back(Animation::Catalog::GetVehicleState(localCharIndex, cam->GetRideVehicleROI())); locationVehicleState.push_back(localVehicleState);
for (const auto& [peerId, player] : m_remotePlayers) { for (const auto& [peerId, player] : m_remotePlayers) {
if (!player->IsSpawned() || !player->GetROI() || player->GetWorldId() != (int8_t) LegoOmni::e_act1) { if (!player->IsSpawned() || !player->GetROI() || player->GetWorldId() != (int8_t) LegoOmni::e_act1) {
@ -2207,37 +2206,6 @@ void NetworkManager::PushAnimationState()
} }
} }
// TODO(vehicle-filter): Remove this logging block after verification
if (m_vehicleFilterLogPending) {
m_vehicleFilterLogPending = false;
uint8_t vehState = Animation::Catalog::GetVehicleState(localCharIndex, cam->GetRideVehicleROI());
const char* stateNames[] = {"onFoot", "onOwnVehicle", "onOtherVehicle"};
SDL_Log(
"[VehicleFilter] Vehicle state changed: char=%d state=%s — %zu eligible animations",
localCharIndex,
stateNames[vehState < 3 ? vehState : 0],
eligibility.size()
);
uint32_t vehicleAnimCount = 0;
for (const auto& info : eligibility) {
const AnimInfo* ai = m_animCatalog.GetAnimInfo(info.animIndex);
if (ai && info.entry && info.entry->vehicleMask) {
vehicleAnimCount++;
SDL_Log(
" [%u] %s (objId=%u loc=%d vmask=0x%02x)",
info.animIndex,
ai->m_name,
ai->m_objectId,
ai->m_location,
info.entry->vehicleMask
);
}
}
if (vehicleAnimCount == 0) {
SDL_Log(" (no vehicle animations in eligible set)");
}
}
// Build JSON // Build JSON
std::string json; std::string json;
json.reserve(2048); json.reserve(2048);