feat(component): the exhibit component (32) the client's loader leaves dead

1.10.64's DoObjectComponentLoad has an EXHIBIT case that makes nothing;
DLU makes ExhibitComponent there, built like LWOExhibitComponent on the
spawner component: its ExhibitComponent row (model offset and size) and the
exhibit_* level keys. It sends nothing in a construction (the client's
Deserialize is empty), so the 12 exhibit LOTs keep their bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-01 16:56:25 -05:00
parent 477dd9f3e3
commit f3ae06cee6
5 changed files with 120 additions and 1 deletions

View File

@@ -99,6 +99,7 @@
#include "PetCreatorComponent.h"
#include "HFLightDirectionGadgetComponent.h"
#include "SpawnerComponent.h"
#include "ExhibitComponent.h"
#include "PathfindingComponent.h"
#include "PhysicsSystemComponent.h"
#include "VehiclePhysicsComponent.h"
@@ -911,6 +912,13 @@ void Entity::DoObjectComponentLoad(ObjectLoadState& state) {
if (id > -1 && !HasComponent(eReplicaComponentType::PLAYER_FORCED_MOVEMENT)) AddComponent<PlayerForcedMovementComponent>(id);
break;
}
case eReplicaComponentType::EXHIBIT: {
// 1.10.64's loader has this case but makes nothing (dead code; showcases were cut); DLU makes the exhibit,
// which sends nothing in a construction, so unchanged clients are unaffected
const auto id = state.GetComponentID(eReplicaComponentType::EXHIBIT, -1);
if (id > -1) AddComponent<ExhibitComponent>(id);
break;
}
case eReplicaComponentType::ROCKET_LANDING: {
// LWORocketAnimationControlComponent: the client makes it from the registry
const auto id = state.GetComponentID(eReplicaComponentType::ROCKET_LANDING, -1);

View File

@@ -42,6 +42,7 @@ set(DGAME_DCOMPONENTS_SOURCES
"Physics/SimplePhysicsComponent.cpp"
"SkillComponent.cpp"
"Sound/SoundTriggerComponent.cpp"
"ExhibitComponent.cpp"
"SpawnerComponent.cpp"
"SwitchComponent.cpp"
"TriggerComponent.cpp"

View File

@@ -0,0 +1,51 @@
#include "ExhibitComponent.h"
#include "CDClientDatabase.h"
#include "Entity.h"
#include "Logger.h"
// LWOExhibitComponent::LoadDataFromTemplate 0x00c9ce40: the ExhibitComponent row
void ExhibitComponent::LoadDataFromTemplate() {
auto query = CDClientDatabase::CreatePreppedStmt(
"SELECT offsetX, offsetY, offsetZ, width, height, length, fReputationSizeMultiplier, fImaginationCost FROM ExhibitComponent WHERE id = ?;");
query.bind(1, m_ComponentID);
auto result = query.execQuery();
if (result.eof()) {
LOG("Object LOT %i couldn't find ExhibitComponent %i in database", m_Parent->GetLOT(), m_ComponentID);
return;
}
m_Offset = NiPoint3(result.getFloatField(0), result.getFloatField(1), result.getFloatField(2));
m_Size = NiPoint3(result.getFloatField(3), result.getFloatField(4), result.getFloatField(5)) * 0.8f;
m_ReputationSizeMultiplier = result.getFloatField(6);
m_ImaginationCost = result.getFloatField(7);
}
// LWOExhibitComponent::LoadConfigData 0x00c9c950: the spawn component's config, then the exhibit_* keys
void ExhibitComponent::LoadConfigData() {
SpawnerComponent::LoadConfigData();
const auto text = [this](const std::u16string& key, const size_t max) {
auto value = m_Parent->GetVar<std::u16string>(key);
if (value.size() > max) value.resize(max);
return value;
};
m_ResetTime = m_Parent->GetVar<uint32_t>(u"exhibit_reset_time");
m_InstanceName = text(u"exhibit_instance_name", 32);
m_VotingAllowed = m_Parent->GetVar<bool>(u"exhibit_voting_allowed");
m_AllowUnmoderated = m_Parent->GetVar<bool>(u"exhibit_allow_unmoderated");
m_AcceptsDroppedModels = m_Parent->GetVar<bool>(u"exhibit_accepts_dropped_models");
m_RequiredNumberOfBricks = m_Parent->GetVar<uint32_t>(u"exhibit_required_number_of_bricks");
m_MustUseBrickLots = text(u"exhibit_must_use_brick_lots", 250);
m_MustNotUseBrickLots = text(u"exhibit_must_not_use_brick_lots", 250);
m_MustUseBehaviorLots = text(u"exhibit_must_use_behavior_lots", 250);
m_MustNotUseBehaviorLots = text(u"exhibit_must_not_use_behavior_lots", 250);
m_InstanceDescription = text(u"exhibit_instance_description", 250);
m_IncludeModelsMine = m_Parent->GetVar<bool>(u"exhibit_include_models_mine");
m_IncludeModelsBffs = m_Parent->GetVar<bool>(u"exhibit_include_models_bffs");
m_IncludeModelsFriends = m_Parent->GetVar<bool>(u"exhibit_include_models_friends");
m_IncludeModelsGuild = m_Parent->GetVar<bool>(u"exhibit_include_models_guild");
m_IncludeModelsPublic = m_Parent->GetVar<bool>(u"exhibit_include_models_public");
m_DisplayRangeByRank = m_Parent->GetVar<uint32_t>(u"exhibit_display_range_by_rank");
m_TargetTopModelsInstance = text(u"exhibit_target_top_models_instance", 32);
m_IsAnimatedPedestal = m_Parent->GetVar<bool>(u"exhibit_is_animated_pedestal");
m_AnimatedPedestalActiveBone = m_Parent->GetVar<uint32_t>(u"exhibit_animated_ped_active_bone");
}

View File

@@ -0,0 +1,59 @@
#ifndef EXHIBITCOMPONENT_H
#define EXHIBITCOMPONENT_H
#include <string>
#include "NiPoint3.h"
#include "SpawnerComponent.h"
/**
* An exhibit (showcase pedestal, 32), as the client's LWOExhibitComponent: a spawn component that shows a player's
* model. The client sends and reads nothing for it in a construction (its Deserialize is empty); it spawns the shown
* model itself from the exhibit messages. 1.10.64's loader has the case but makes nothing (dead code); DLU makes it.
*/
class ExhibitComponent final : public SpawnerComponent {
public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::EXHIBIT;
ExhibitComponent(Entity* parent, const int32_t componentID) : SpawnerComponent(parent, componentID) {}
void LoadDataFromTemplate() override;
void LoadConfigData() override;
const NiPoint3& GetOffset() const { return m_Offset; }
const NiPoint3& GetSize() const { return m_Size; }
const std::u16string& GetInstanceName() const { return m_InstanceName; }
bool GetVotingAllowed() const { return m_VotingAllowed; }
private:
// The ExhibitComponent row: where the model sits on the pedestal and how big it may be (the client scales the
// row's size by 0.8)
NiPoint3 m_Offset{};
NiPoint3 m_Size{};
float m_ReputationSizeMultiplier = 1.0f;
float m_ImaginationCost = 0.0f;
// The level config (LWOExhibitComponent::LoadConfigData 0x00c9c950)
uint32_t m_ResetTime = 0;
std::u16string m_InstanceName; // at most 32 characters
bool m_VotingAllowed = false;
bool m_AllowUnmoderated = false;
bool m_AcceptsDroppedModels = false;
uint32_t m_RequiredNumberOfBricks = 0;
std::u16string m_MustUseBrickLots; // at most 250 characters each
std::u16string m_MustNotUseBrickLots;
std::u16string m_MustUseBehaviorLots;
std::u16string m_MustNotUseBehaviorLots;
std::u16string m_InstanceDescription;
bool m_IncludeModelsMine = false;
bool m_IncludeModelsBffs = false;
bool m_IncludeModelsFriends = false;
bool m_IncludeModelsGuild = false;
bool m_IncludeModelsPublic = false;
uint32_t m_DisplayRangeByRank = 0;
std::u16string m_TargetTopModelsInstance; // at most 32 characters
bool m_IsAnimatedPedestal = false;
uint32_t m_AnimatedPedestalActiveBone = 0;
};
#endif // EXHIBITCOMPONENT_H

View File

@@ -12,7 +12,7 @@ class Spawner;
* The spawner component (10) of a spawner object (LOT 176), made from the registry as the client makes it. It owns the
* spawner (its nodes, what it spawned and its timers); the zone manager indexes spawners by the object's ID.
*/
class SpawnerComponent final : public Component {
class SpawnerComponent : public Component {
public:
static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::SPAWNER;