Files
DarkflameServer/dGame/dGameMessages/ZoneMessages.cpp
Aaron Kimbrell 6ce261c58c fix: brick by brick and model placement work the way the client expects
Mapped from the 1.10.64 client and live captures (docs/BuildWorkflow.md).

Model placement (PropertyManagementComponent):
- A brick built model placed from the inventory spawned at the world origin
  with no rotation and without PlaceModelResponse/PreCreate, and was saved
  to properties_contents with ugc_id 0; it is now placed where the client
  put it, keeps its UGID and blueprint and is saved with them.
- Picking up, putting away and taking apart a brick built model gave it to
  MODELS_IN_BBB and then deleted it; every way off the property now puts it
  in MODELS (carried when picked up), as live, with its blueprint config.
  Taking a premade model apart no longer deletes it either.
- Placing and removing a model saves the property at once, so a crash or a
  disconnect before PropertyEditorEnd does not lose it.
- DoneArrangingWithItem only answers when something new is picked (not when
  leaving), with the subject as build area; SetBuildModeConfirmed's
  warnVisitors matches live.

Brick by brick (BrickByBrick):
- BBBLoadItemRequest moves the model to MODELS_IN_BBB keeping its id and
  fails cleanly when the player has no such model.
- MoveInventoryBatch moves bricks between BRICKS and BRICKS_IN_BBB (it was
  not handled, so the client and server disagreed until a relog).
- BBBSaveRequest uses up the opened models, places the new ones through the
  property, returns the bricks (or uses them with bbb_consume_bricks=1),
  clears the autosave and sends RequeryPropertyModels. Every save makes new
  ugc rows (is_optimized 0, so the UGC server processes them).
- Quick save: SetBBBAutosave is stored per character (bbb_autosave).
- UnUseBBBModel puts a model back on the property where it was when it came
  from there, otherwise back in MODELS.
- Leaving brick mode without a save, a disconnect or a crash: the autosave
  is rebuilt into models (RebuildBBBAutosaveMsg) or the opened models go
  back to MODELS. MODELS_IN_BBB is saved with the character now and loads
  into MODELS, BRICKS_IN_BBB into BRICKS.

Fixes #1632
Fixes #159
Fixes #1565

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:30:59 -05:00

265 lines
9.5 KiB
C++

#include "ZoneMessages.h"
#include "BitStreamUtils.h"
#include "CDClientManager.h"
#include "Character.h"
#include "CharacterComponent.h"
#include "CppScripts.h"
#include "DestroyableComponent.h"
#include "EffectsMessages.h"
#include "Entity.h"
#include "EntityManager.h"
#include "Game.h"
#include "GhostComponent.h"
#include "InventoryComponent.h"
#include "InventoryMessages.h"
#include "LevelProgressionComponent.h"
#include "RacingControlComponent.h"
#include "WorldMigration.h"
#include "BrickByBrick.h"
#include "dConfig.h"
#include "dZoneManager.h"
#include "eReplicaComponentType.h"
#include "eTriggerEventType.h"
#include "ePlayerFlag.h"
#include <sstream>
namespace GameMessages {
void PlayerLoaded::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(playerID);
}
bool PlayerLoaded::Deserialize(RakNet::BitStream& bitStream) {
return bitStream.Read(playerID);
}
void PlayerLoaded::Handle(Entity& entityRef, const SystemAddress& sysAddr) {
auto* entity = &entityRef;
PlayerReady playerReady;
playerReady.target = entity->GetObjectID();
playerReady.SendToClient(sysAddr);
entity->SetPlayerReadyForUpdates();
auto* ghostComponent = entity->GetComponent<GhostComponent>();
if (ghostComponent != nullptr) {
ghostComponent->ConstructLimboEntities();
}
InventoryComponent* inv = entity->GetComponent<InventoryComponent>();
if (inv) {
// Clear server-side skill state so AddItemSkills sends fresh AddSkill
// packets to the now-ready client. Skills sent during entity construction
// (Serialize) arrive before LWOSkillComponent is initialized and are dropped.
inv->ClearSkills();
auto items = inv->GetEquippedItems();
for (auto pair : items) {
const auto item = pair.second;
inv->AddItemSkills(item.lot);
}
// Fixes a bug where testmapping too fast causes large item inventories to become invisible.
// Only affects item inventory
GameMessages::SetInventorySize setSize;
setSize.target = entity->GetObjectID();
setSize.inventoryType = eInventoryType::ITEMS;
setSize.size = inv->GetInventory(eInventoryType::ITEMS)->GetSize();
setSize.SendToClient(entity->GetSystemAddress());
}
RestoreToPostLoadStats restoreStats;
restoreStats.target = entity->GetObjectID();
restoreStats.SendToClient(sysAddr);
auto* const destroyable = entity->GetComponent<DestroyableComponent>();
if (destroyable) destroyable->SetImagination(destroyable->GetImagination());
Game::entityManager->SerializeEntity(entity);
std::vector<Entity*> racingControllers = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::RACING_CONTROL);
for (Entity* racingController : racingControllers) {
auto* racingComponent = racingController->GetComponent<RacingControlComponent>();
if (racingComponent != nullptr) {
racingComponent->OnPlayerLoaded(entity);
}
}
Entity* zoneControl = Game::entityManager->GetZoneControlEntity();
if (zoneControl) {
zoneControl->GetScript()->OnPlayerLoaded(zoneControl, entity);
}
std::vector<Entity*> scriptedActs = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::SCRIPT);
for (Entity* scriptEntity : scriptedActs) {
if (!zoneControl || scriptEntity->GetObjectID() != zoneControl->GetObjectID()) { // Don't want to trigger twice on instance worlds
scriptEntity->GetScript()->OnPlayerLoaded(scriptEntity, entity);
}
}
//Kill player if health == 0
if (entity->GetIsDead()) {
entity->Smash(entity->GetObjectID());
}
//if the player has moved significantly, move them back:
if ((entity->GetPosition().y - entity->GetCharacter()->GetOriginalPos().y) > 2.0f) {
// Disabled until fixed
//GameMessages::Teleport(entity->GetObjectID(), entity->GetCharacter()->GetOriginalPos(), entity->GetCharacter()->GetOriginalRot(), true).Send(entity->GetSystemAddress());
}
/**
* Invoke the OnZoneLoad event on the player character
*/
auto* character = entity->GetCharacter();
if (character != nullptr) {
character->OnZoneLoad();
}
// Moved here from another instance: put back what their save doesn't keep (the pet that was out)
WorldMigration::OnPlayerLoaded(entity);
// A brick by brick build that ended without a save (disconnect, crash): rebuild the autosave or give back the
// models that were open
BrickByBrick::OnPlayerLoaded(*entity);
LOG("Player %s (%llu) loaded.", entity->GetCharacter()->GetName().c_str(), entity->GetObjectID());
// After we've done our thing, tell the client they're ready
PlayerReady zoneReady;
zoneReady.target = Game::zoneManager->GetZoneControlObject()->GetObjectID();
zoneReady.SendToClient(sysAddr);
if (Game::config->GetValue("allow_players_to_skip_cinematics") != "1"
|| !entity->GetCharacter()
|| !entity->GetCharacter()->GetPlayerFlag(ePlayerFlag::DLU_SKIP_CINEMATICS)) return;
entity->AddCallbackTimer(0.5f, [entity, sysAddr]() {
if (!entity) return;
GameMessages::EndCinematic endCinematic;
endCinematic.target = entity->GetObjectID();
endCinematic.Send(sysAddr);
});
}
void ReadyForUpdates::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(objectID);
}
bool ReadyForUpdates::Deserialize(RakNet::BitStream& bitStream) {
return bitStream.Read(objectID);
}
void InvalidZoneTransferList::Serialize(RakNet::BitStream& bitStream) const {
BitStreamUtils::WriteLengthPrefixed<uint32_t>(bitStream, customerFeedbackURL);
BitStreamUtils::WriteLengthPrefixed<uint32_t>(bitStream, invalidMapTransferList);
bitStream.Write(bCustomerFeedbackOnExit);
bitStream.Write(bCustomerFeedbackOnInvalidMapTransfer);
}
bool InvalidZoneTransferList::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed<uint32_t>(bitStream, customerFeedbackURL));
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed<uint32_t>(bitStream, invalidMapTransferList));
VALIDATE_READ(bitStream.Read(bCustomerFeedbackOnExit));
VALIDATE_READ(bitStream.Read(bCustomerFeedbackOnInvalidMapTransfer));
return true;
}
void DisplayZoneSummary::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(isPropertyMap);
bitStream.Write(isZoneStart);
BitStreamUtils::WriteOptional(bitStream, sender, LWOOBJID_EMPTY);
}
bool DisplayZoneSummary::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(bitStream.Read(isPropertyMap));
VALIDATE_READ(bitStream.Read(isZoneStart));
VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, sender, LWOOBJID_EMPTY));
return true;
}
void ZoneSummaryDismissed::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(playerID);
}
bool ZoneSummaryDismissed::Deserialize(RakNet::BitStream& bitStream) {
return bitStream.Read(playerID);
}
void ZoneSummaryDismissed::Handle(Entity& entity, const SystemAddress& sysAddr) {
auto* player = Game::entityManager->GetEntity(playerID);
entity.TriggerEvent(eTriggerEventType::ZONE_SUMMARY_DISMISSED, player);
}
void NotifyServerLevelProcessingComplete::Handle(Entity& entity, const SystemAddress& sysAddr) {
auto* levelComp = entity.GetComponent<LevelProgressionComponent>();
if (!levelComp) return;
auto* character = entity.GetComponent<CharacterComponent>();
if (!character) return;
//Update our character's level in memory:
levelComp->SetLevel(levelComp->GetLevel() + 1);
levelComp->HandleLevelUp();
auto* inventoryComponent = entity.GetComponent<InventoryComponent>();
if (inventoryComponent != nullptr) {
auto* inventory = inventoryComponent->GetInventory(ITEMS);
if (inventory != nullptr && Game::config->GetValue("disable_extra_backpack") != "1") {
inventory->SetSize(inventory->GetSize() + 2);
}
}
//Play the level up effect:
GameMessages::PlayFXEffect(entity.GetObjectID(), 7074, u"create", "7074").Send(UNASSIGNED_SYSTEM_ADDRESS);
//Send a notification in chat:
std::stringstream wss;
wss << "level=1:";
wss << levelComp->GetLevel();
wss << "\n";
wss << "name=0:";
wss << character->GetName();
// FIXME: only really need utf8 conversion for the name, so move that up?
std::u16string attrs = GeneralUtils::UTF8ToUTF16(wss.str());
std::u16string wsText = u"UI_LEVEL_PROGRESSION_LEVELUP_MESSAGE";
GameMessages::BroadcastTextToChatbox chatboxText;
chatboxText.target = entity.GetObjectID();
chatboxText.attrs = attrs;
chatboxText.wsText = wsText;
chatboxText.Send(UNASSIGNED_SYSTEM_ADDRESS);
}
void ChangeObjectWorldState::Serialize(RakNet::BitStream& bitStream) const {
bitStream.Write(newState);
}
bool ChangeObjectWorldState::Deserialize(RakNet::BitStream& bitStream) {
return bitStream.Read(newState);
}
void LocalizedAnnouncementServerToSingleClient::Serialize(RakNet::BitStream& bitStream) const {
BitStreamUtils::WriteNameValueText(bitStream, bodyParams);
bitStream.Write(bForceOpenChatBox);
bitStream.Write(bShowAnnouncement);
bitStream.Write(bShowInChat);
BitStreamUtils::WriteLengthPrefixed<uint32_t>(bitStream, body);
BitStreamUtils::WriteLengthPrefixed<uint32_t>(bitStream, title);
BitStreamUtils::WriteNameValueText(bitStream, titleParams);
}
bool LocalizedAnnouncementServerToSingleClient::Deserialize(RakNet::BitStream& bitStream) {
VALIDATE_READ(BitStreamUtils::ReadNameValueText(bitStream, bodyParams));
VALIDATE_READ(bitStream.Read(bForceOpenChatBox));
VALIDATE_READ(bitStream.Read(bShowAnnouncement));
VALIDATE_READ(bitStream.Read(bShowInChat));
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed<uint32_t>(bitStream, body));
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed<uint32_t>(bitStream, title));
VALIDATE_READ(BitStreamUtils::ReadNameValueText(bitStream, titleParams));
return true;
}
}