mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
feat(bbb): answer FetchModelMetadataRequest for brick built models
The client asks for a model's metadata (name, owner, behaviors and its blueprint's bricks and box) when it shows a brick built model item's tooltip, a model on a property or an exhibit, and shows BBB_LOADING_BLUEPRINT until it gets it. The world server never answered. It now does as live did: UG data for the model (found among the player's items by subkey, else among placed models) and, for a brick built model, the blueprint data from its ugc row and LXFML. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
#include "StringifiedEnum.h"
|
||||
#include "TinyXmlUtils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
@@ -30,6 +31,48 @@ namespace {
|
||||
</LXFML>)";
|
||||
}
|
||||
|
||||
Lxfml::Contents Lxfml::ReadContents(const std::string_view data) {
|
||||
Contents contents;
|
||||
if (data.empty()) return contents;
|
||||
|
||||
tinyxml2::XMLDocument doc;
|
||||
if (doc.Parse(data.data(), data.size()) != tinyxml2::XML_SUCCESS) return contents;
|
||||
const auto* bricks = doc.FirstChildElement("LXFML") ? doc.FirstChildElement("LXFML")->FirstChildElement("Bricks") : nullptr;
|
||||
if (!bricks) return contents;
|
||||
|
||||
bool anyBone = false;
|
||||
for (const auto* brick = bricks->FirstChildElement("Brick"); brick; brick = brick->NextSiblingElement("Brick")) {
|
||||
// designID may carry a suffix ("3001;A")
|
||||
const std::string_view design = brick->Attribute("designID") ? brick->Attribute("designID") : "";
|
||||
const auto digits = design.substr(0, design.find_first_not_of("0123456789"));
|
||||
const auto designId = GeneralUtils::TryParse<uint32_t>(digits);
|
||||
if (designId) contents.designIds.push_back(*designId);
|
||||
|
||||
for (const auto* part = brick->FirstChildElement("Part"); part; part = part->NextSiblingElement("Part")) {
|
||||
for (const auto* bone = part->FirstChildElement("Bone"); bone; bone = bone->NextSiblingElement("Bone")) {
|
||||
const auto* transformation = bone->Attribute("transformation");
|
||||
if (!transformation) continue;
|
||||
const auto split = GeneralUtils::SplitString(transformation, ',');
|
||||
if (split.size() < 12) continue;
|
||||
const auto x = GeneralUtils::TryParse<float>(split[9]);
|
||||
const auto y = GeneralUtils::TryParse<float>(split[10]);
|
||||
const auto z = GeneralUtils::TryParse<float>(split[11]);
|
||||
if (!x || !y || !z) continue;
|
||||
const NiPoint3 position{ *x, *y, *z };
|
||||
if (!anyBone) {
|
||||
contents.boxMin = position;
|
||||
contents.boxMax = position;
|
||||
anyBone = true;
|
||||
continue;
|
||||
}
|
||||
contents.boxMin = NiPoint3(std::min(contents.boxMin.x, position.x), std::min(contents.boxMin.y, position.y), std::min(contents.boxMin.z, position.z));
|
||||
contents.boxMax = NiPoint3(std::max(contents.boxMax.x, position.x), std::max(contents.boxMax.y, position.y), std::max(contents.boxMax.z, position.z));
|
||||
}
|
||||
}
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
|
||||
Lxfml::Result Lxfml::NormalizePosition(const std::string_view data, const NiPoint3& curPosition) {
|
||||
Result toReturn;
|
||||
|
||||
|
||||
@@ -21,6 +21,16 @@ namespace Lxfml {
|
||||
[[nodiscard]] Result NormalizePosition(const std::string_view data, const NiPoint3& curPosition = NiPoint3Constant::ZERO);
|
||||
[[nodiscard]] std::vector<Result> Split(const std::string_view data, const NiPoint3& curPosition = NiPoint3Constant::ZERO);
|
||||
|
||||
// What a model is made of: every brick's design id (one per Brick, in file order) and the box around the bricks'
|
||||
// origins (their bones' positions; zero with no bricks). Brick shapes aren't known here, so the box is the
|
||||
// smallest one holding every brick's origin.
|
||||
struct Contents {
|
||||
std::vector<uint32_t> designIds;
|
||||
NiPoint3 boxMin{};
|
||||
NiPoint3 boxMax{};
|
||||
};
|
||||
[[nodiscard]] Contents ReadContents(const std::string_view data);
|
||||
|
||||
// these are only for the migrations due to a bug in one of the implementations.
|
||||
[[nodiscard]] Result NormalizePositionOnlyFirstPart(const std::string_view data);
|
||||
[[nodiscard]] Result NormalizePositionAfterFirstPart(const std::string_view data, const NiPoint3& position);
|
||||
|
||||
@@ -603,6 +603,110 @@ namespace GameMessages {
|
||||
return true;
|
||||
}
|
||||
|
||||
void UGObjectMetadata::Serialize(RakNet::BitStream& bitStream) const {
|
||||
bitStream.Write(userModelID);
|
||||
bitStream.Write(blueprintID);
|
||||
BitStreamUtils::WriteLengthPrefixed(bitStream, userModelName);
|
||||
BitStreamUtils::WriteLengthPrefixed(bitStream, userModelDesc);
|
||||
bitStream.Write(owningPlayerID);
|
||||
bitStream.Write(accountID);
|
||||
BitStreamUtils::WriteLengthPrefixed(bitStream, owningPlayerName);
|
||||
const auto count = static_cast<uint8_t>(std::min<size_t>(userModelBehaviors.size(), UINT8_MAX));
|
||||
bitStream.Write(count);
|
||||
for (uint8_t i = 0; i < count; i++) bitStream.Write(userModelBehaviors[i]);
|
||||
}
|
||||
|
||||
bool UGObjectMetadata::Deserialize(RakNet::BitStream& bitStream) {
|
||||
VALIDATE_READ(bitStream.Read(userModelID));
|
||||
VALIDATE_READ(bitStream.Read(blueprintID));
|
||||
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, userModelName));
|
||||
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, userModelDesc));
|
||||
VALIDATE_READ(bitStream.Read(owningPlayerID));
|
||||
VALIDATE_READ(bitStream.Read(accountID));
|
||||
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, owningPlayerName));
|
||||
uint8_t count{};
|
||||
VALIDATE_READ(bitStream.Read(count));
|
||||
userModelBehaviors.resize(count);
|
||||
for (auto& behavior : userModelBehaviors) VALIDATE_READ(bitStream.Read(behavior));
|
||||
return true;
|
||||
}
|
||||
|
||||
void BlueprintMetadata::Serialize(RakNet::BitStream& bitStream) const {
|
||||
bitStream.Write(blueprintID);
|
||||
bitStream.Write(blueprintCreationTimestamp);
|
||||
bitStream.Write(userModelMod);
|
||||
bitStream.Write(modelBoxMins);
|
||||
bitStream.Write(modelBoxMaxs);
|
||||
bitStream.Write(userModelOpt);
|
||||
bitStream.Write(ugcIconReady);
|
||||
BitStreamUtils::WriteLengthPrefixed(bitStream, brickListColonDelim);
|
||||
bitStream.Write(neverFalseIfPresentInCaps);
|
||||
bitStream.Write(numberOfBricks);
|
||||
}
|
||||
|
||||
bool BlueprintMetadata::Deserialize(RakNet::BitStream& bitStream) {
|
||||
VALIDATE_READ(bitStream.Read(blueprintID));
|
||||
VALIDATE_READ(bitStream.Read(blueprintCreationTimestamp));
|
||||
VALIDATE_READ(bitStream.Read(userModelMod));
|
||||
VALIDATE_READ(bitStream.Read(modelBoxMins));
|
||||
VALIDATE_READ(bitStream.Read(modelBoxMaxs));
|
||||
VALIDATE_READ(bitStream.Read(userModelOpt));
|
||||
VALIDATE_READ(bitStream.Read(ugcIconReady));
|
||||
VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, brickListColonDelim));
|
||||
VALIDATE_READ(bitStream.Read(neverFalseIfPresentInCaps));
|
||||
VALIDATE_READ(bitStream.Read(numberOfBricks));
|
||||
return true;
|
||||
}
|
||||
|
||||
void FetchModelMetadataRequest::Serialize(RakNet::BitStream& bitStream) const {
|
||||
bitStream.Write(context);
|
||||
bitStream.Write(objectID);
|
||||
bitStream.Write(requestorID);
|
||||
bitStream.Write(ugID);
|
||||
}
|
||||
|
||||
bool FetchModelMetadataRequest::Deserialize(RakNet::BitStream& bitStream) {
|
||||
VALIDATE_READ(bitStream.Read(context));
|
||||
VALIDATE_READ(bitStream.Read(objectID));
|
||||
VALIDATE_READ(bitStream.Read(requestorID));
|
||||
VALIDATE_READ(bitStream.Read(ugID));
|
||||
return true;
|
||||
}
|
||||
|
||||
void FetchModelMetadataRequest::Handle(Entity& entity, const SystemAddress& sysAddr) {
|
||||
FetchModelMetadataResponse response;
|
||||
response.target = entity.GetObjectID();
|
||||
response.ugID = ugID;
|
||||
response.objectID = objectID;
|
||||
response.requestorID = requestorID;
|
||||
response.context = context;
|
||||
BrickByBrick::FillModelMetadata(entity, ugID, response);
|
||||
response.Send(sysAddr);
|
||||
}
|
||||
|
||||
void FetchModelMetadataResponse::Serialize(RakNet::BitStream& bitStream) const {
|
||||
bitStream.Write(ugID);
|
||||
bitStream.Write(objectID);
|
||||
bitStream.Write(requestorID);
|
||||
bitStream.Write(context);
|
||||
bitStream.Write(bHasUGData);
|
||||
bitStream.Write(bHasBPData);
|
||||
if (bHasUGData) ugData.Serialize(bitStream);
|
||||
if (bHasBPData) bpData.Serialize(bitStream);
|
||||
}
|
||||
|
||||
bool FetchModelMetadataResponse::Deserialize(RakNet::BitStream& bitStream) {
|
||||
VALIDATE_READ(bitStream.Read(ugID));
|
||||
VALIDATE_READ(bitStream.Read(objectID));
|
||||
VALIDATE_READ(bitStream.Read(requestorID));
|
||||
VALIDATE_READ(bitStream.Read(context));
|
||||
VALIDATE_READ(bitStream.Read(bHasUGData));
|
||||
VALIDATE_READ(bitStream.Read(bHasBPData));
|
||||
if (bHasUGData) VALIDATE_READ(ugData.Deserialize(bitStream));
|
||||
if (bHasBPData) VALIDATE_READ(bpData.Deserialize(bitStream));
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetModelToBuild::Serialize(RakNet::BitStream& bitStream) const {
|
||||
bitStream.Write(modelLot != -1);
|
||||
if (modelLot != -1) bitStream.Write(modelLot);
|
||||
|
||||
@@ -242,6 +242,70 @@ namespace GameMessages {
|
||||
int32_t count{};
|
||||
};
|
||||
|
||||
// A model's user generated data (UGObjectMetadata::Serialize, 0x00f5f590)
|
||||
struct UGObjectMetadata {
|
||||
LWOOBJID userModelID{};
|
||||
LWOOBJID blueprintID{};
|
||||
std::u16string userModelName;
|
||||
std::u16string userModelDesc;
|
||||
LWOOBJID owningPlayerID{};
|
||||
LWOOBJID accountID{};
|
||||
std::u16string owningPlayerName;
|
||||
std::vector<LWOOBJID> userModelBehaviors; // a u8 count on the wire; live always sent 5
|
||||
|
||||
void Serialize(RakNet::BitStream& bitStream) const;
|
||||
bool Deserialize(RakNet::BitStream& bitStream);
|
||||
};
|
||||
|
||||
// A brick built model's blueprint data (BlueprintMetadata::Serialize, 0x00f5f760)
|
||||
struct BlueprintMetadata {
|
||||
LWOOBJID blueprintID{};
|
||||
int64_t blueprintCreationTimestamp{}; // Unix seconds
|
||||
int32_t userModelMod{ 1 };
|
||||
NiPoint3 modelBoxMins{}; // the model's box, relative to its origin (the exhibit checks its size)
|
||||
NiPoint3 modelBoxMaxs{};
|
||||
bool userModelOpt{ true };
|
||||
bool ugcIconReady{ true };
|
||||
std::u16string brickListColonDelim; // every brick's LOT, each followed by ':'
|
||||
bool neverFalseIfPresentInCaps{ true };
|
||||
int32_t numberOfBricks{};
|
||||
|
||||
void Serialize(RakNet::BitStream& bitStream) const;
|
||||
bool Deserialize(RakNet::BitStream& bitStream);
|
||||
};
|
||||
|
||||
// Client -> server. The client wants the name, owner, behaviors and blueprint of a model (its UGID) it has no
|
||||
// metadata for: a brick built model item's tooltip (LWOInventoryComponent_Client::FillinDetailsBlueprint, 0x00cddb40, via
|
||||
// UGModelMetadataCache::GetModelMetadata, 0x00b5e680), a model on the property, an exhibit.
|
||||
struct FetchModelMetadataRequest : public NetGameMsg {
|
||||
FetchModelMetadataRequest() : NetGameMsg(MessageType::Game::FETCH_MODEL_METADATA_REQUEST) {}
|
||||
void Serialize(RakNet::BitStream& bitStream) const override;
|
||||
bool Deserialize(RakNet::BitStream& bitStream) override;
|
||||
void Handle(Entity& entity, const SystemAddress& sysAddr) override;
|
||||
|
||||
int32_t context{};
|
||||
LWOOBJID objectID{};
|
||||
LWOOBJID requestorID{};
|
||||
LWOOBJID ugID{};
|
||||
};
|
||||
|
||||
// Server -> client. The answer: the client caches it by UGID and blueprint id and writes it into the config of
|
||||
// the inventory item objectID (LWOInventoryComponent_Client::msgFetchModelMetadataResponse, 0x00c64300).
|
||||
struct FetchModelMetadataResponse : public NetGameMsg {
|
||||
FetchModelMetadataResponse() : NetGameMsg(MessageType::Game::FETCH_MODEL_METADATA_RESPONSE) {}
|
||||
void Serialize(RakNet::BitStream& bitStream) const override;
|
||||
bool Deserialize(RakNet::BitStream& bitStream) override;
|
||||
|
||||
LWOOBJID ugID{};
|
||||
LWOOBJID objectID{};
|
||||
LWOOBJID requestorID{};
|
||||
int32_t context{};
|
||||
bool bHasUGData{};
|
||||
bool bHasBPData{};
|
||||
UGObjectMetadata ugData; // when bHasUGData
|
||||
BlueprintMetadata bpData; // when bHasBPData
|
||||
};
|
||||
|
||||
struct SetModelToBuild : public NetGameMsg {
|
||||
SetModelToBuild() : NetGameMsg(MessageType::Game::SET_MODEL_TO_BUILD) {}
|
||||
void Serialize(RakNet::BitStream& bitStream) const override;
|
||||
|
||||
@@ -141,6 +141,7 @@ namespace {
|
||||
{ SET_BUILD_MODE, []() { return std::make_unique<SetBuildMode>(); } },
|
||||
{ BUILD_MODE_SET, []() { return std::make_unique<BuildModeSet>(); } },
|
||||
{ UN_USE_BBB_MODEL, []() { return std::make_unique<UnUseBBBModel>(); } },
|
||||
{ FETCH_MODEL_METADATA_REQUEST, []() { return std::make_unique<FetchModelMetadataRequest>(); } },
|
||||
{ BBB_LOAD_ITEM_REQUEST, []() { return std::make_unique<BBBLoadItemRequest>(); } },
|
||||
{ BBB_SAVE_REQUEST, []() { return std::make_unique<BBBSaveRequest>(); } },
|
||||
{ SET_BBB_AUTOSAVE, []() { return std::make_unique<SetBBBAutosave>(); } },
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "BrickByBrick.h"
|
||||
|
||||
#include "BuildingMessages.h"
|
||||
#include "CDBrickIDTableTable.h"
|
||||
#include "CDClientManager.h"
|
||||
#include "Character.h"
|
||||
#include "ClientPackets.h"
|
||||
#include "DashboardNotify.h"
|
||||
@@ -33,6 +35,7 @@
|
||||
#include <fstream>
|
||||
#include <ranges>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace {
|
||||
// Every item in one inventory, copied so the inventory can change while going through them
|
||||
@@ -128,6 +131,45 @@ namespace {
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
// A config value as text (empty when missing)
|
||||
std::string ConfigText(const LwoNameValue& config, const std::u16string& key) {
|
||||
const auto it = config.find(key);
|
||||
if (it == config.end() || !it->second) return "";
|
||||
return it->second->GetValueAsString();
|
||||
}
|
||||
|
||||
// A brick's LOT by its LEGO design id (BrickIDTable); 0 when the design has no item
|
||||
LOT BrickLot(const uint32_t designId) {
|
||||
static std::unordered_map<uint32_t, LOT> lots;
|
||||
if (lots.empty()) {
|
||||
for (const auto& entry : CDClientManager::GetTable<CDBrickIDTableTable>()->Query([](const CDBrickIDTable&) { return true; })) {
|
||||
lots.try_emplace(entry.LEGOBrickID, static_cast<LOT>(entry.NDObjectID));
|
||||
}
|
||||
}
|
||||
const auto it = lots.find(designId);
|
||||
return it == lots.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
// The blueprint data of a stored brick built model (its ugc row); false when there is no such row
|
||||
bool FillBlueprintMetadata(const LWOOBJID blueprintId, GameMessages::BlueprintMetadata& data) {
|
||||
auto model = Database::Get()->GetUgcModel(blueprintId);
|
||||
if (!model) return false;
|
||||
|
||||
data.blueprintID = blueprintId;
|
||||
Sd0 sd0(model->lxfmlData);
|
||||
const auto contents = Lxfml::ReadContents(sd0.GetAsStringUncompressed());
|
||||
std::vector<LOT> lots;
|
||||
for (const auto designId : contents.designIds) {
|
||||
const auto lot = BrickLot(designId);
|
||||
if (lot != 0) lots.push_back(lot);
|
||||
}
|
||||
data.brickListColonDelim = BrickByBrick::BrickList(lots);
|
||||
data.numberOfBricks = static_cast<int32_t>(contents.designIds.size());
|
||||
data.modelBoxMins = contents.boxMin;
|
||||
data.modelBoxMaxs = contents.boxMax;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool BrickByBrick::IsEmptyModel(const std::string_view sd0) {
|
||||
@@ -393,3 +435,86 @@ uint32_t BrickByBrick::RecoverUnfinishedBuild(Entity& player) {
|
||||
}
|
||||
return rebuilt;
|
||||
}
|
||||
|
||||
std::vector<LWOOBJID> BrickByBrick::BehaviorIds(const std::string_view behaviors) {
|
||||
std::vector<LWOOBJID> ids(5, LWOOBJID_EMPTY);
|
||||
size_t index = 0;
|
||||
for (const auto& part : GeneralUtils::SplitString(std::string(behaviors), ',')) {
|
||||
if (index >= ids.size()) break;
|
||||
ids[index++] = GeneralUtils::TryParse<LWOOBJID>(part).value_or(LWOOBJID_EMPTY);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
std::u16string BrickByBrick::BrickList(const std::vector<LOT>& brickLots) {
|
||||
std::u16string list;
|
||||
for (const auto lot : brickLots) {
|
||||
list += GeneralUtils::to_u16string(lot);
|
||||
list += u':';
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
void BrickByBrick::FillModelMetadata(Entity& player, const LWOOBJID ugId, GameMessages::FetchModelMetadataResponse& response) {
|
||||
if (ugId == LWOOBJID_EMPTY) return;
|
||||
|
||||
bool found = false;
|
||||
LWOOBJID blueprintId = LWOOBJID_EMPTY;
|
||||
std::string behaviors;
|
||||
std::u16string name;
|
||||
std::u16string description;
|
||||
|
||||
// One of the player's model items: a brick built model keeps its UGID as the item's subkey
|
||||
auto* inventory = player.GetComponent<InventoryComponent>();
|
||||
if (inventory) {
|
||||
for (auto* bag : inventory->GetInventories() | std::views::values) {
|
||||
for (const auto* item : bag->GetItems() | std::views::values) {
|
||||
if (item->GetSubKey() != ugId) continue;
|
||||
const auto& config = item->GetConfig();
|
||||
blueprintId = ConfigObjectId(config, u"blueprintid");
|
||||
behaviors = ConfigText(config, u"userModelBehaviors");
|
||||
name = GeneralUtils::UTF8ToUTF16(ConfigText(config, u"userModelName"));
|
||||
description = GeneralUtils::UTF8ToUTF16(ConfigText(config, u"userModelDesc"));
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (found) break;
|
||||
}
|
||||
}
|
||||
|
||||
// A model placed on a property (this one or one the player visits)
|
||||
if (!found) {
|
||||
const auto model = Database::Get()->GetModel(ugId);
|
||||
if (model) {
|
||||
found = true;
|
||||
blueprintId = model->ugcId;
|
||||
for (size_t i = 0; i < model->behaviors.size(); i++) {
|
||||
if (i != 0) behaviors += ',';
|
||||
behaviors += std::to_string(model->behaviors[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
LOG_DEBUG("Player %llu asked for the metadata of model %llu, which is neither theirs nor placed", player.GetObjectID(), ugId);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& ug = response.ugData;
|
||||
response.bHasUGData = true;
|
||||
ug.userModelID = ugId;
|
||||
ug.userModelName = name;
|
||||
ug.userModelDesc = description;
|
||||
ug.userModelBehaviors = BehaviorIds(behaviors);
|
||||
|
||||
// A premade model (no blueprint) has no owner or blueprint data, as live sent it
|
||||
if (blueprintId == LWOOBJID_EMPTY) return;
|
||||
ug.blueprintID = blueprintId;
|
||||
const auto owner = Database::Get()->GetUgcProcessInfo(blueprintId);
|
||||
if (owner) {
|
||||
ug.owningPlayerID = owner->characterId;
|
||||
ug.owningPlayerName = GeneralUtils::UTF8ToUTF16(owner->characterName);
|
||||
const auto character = Database::Get()->GetCharacterInfo(owner->characterId);
|
||||
if (character) ug.accountID = character->accountId;
|
||||
}
|
||||
response.bHasBPData = FillBlueprintMetadata(blueprintId, response.bpData);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ class InventoryComponent;
|
||||
|
||||
namespace GameMessages {
|
||||
struct MoveInventoryBatch;
|
||||
struct FetchModelMetadataResponse;
|
||||
}
|
||||
|
||||
// Brick by brick building and the model items it works on, as the 1.10.64 client expects them (docs/BuildWorkflow.md).
|
||||
@@ -70,8 +71,19 @@ namespace BrickByBrick {
|
||||
// An object id kept in a config (blueprintid, userModelID), or LWOOBJID_EMPTY
|
||||
LWOOBJID ConfigObjectId(const LwoNameValue& config, const std::u16string& key);
|
||||
|
||||
// A model's behavior ids from its userModelBehaviors config ("id,id,id,id,id"): always 5, missing ones 0 (live
|
||||
// always sent 5 in FetchModelMetadataResponse)
|
||||
std::vector<LWOOBJID> BehaviorIds(std::string_view behaviors);
|
||||
|
||||
// BlueprintMetadata's brick list: every brick's LOT followed by ':' ("66:" for one 1x4 plate), as live sent it
|
||||
std::u16string BrickList(const std::vector<LOT>& brickLots);
|
||||
|
||||
// ---- Handlers ----
|
||||
|
||||
// FetchModelMetadataRequest: fills in the UG data (and, for a brick built model, the blueprint data) of the model
|
||||
// ugId: one of the player's model items (its subkey), else a model placed on any property. Neither: both left out.
|
||||
void FillModelMetadata(Entity& player, LWOOBJID ugId, GameMessages::FetchModelMetadataResponse& response);
|
||||
|
||||
// BBBLoadItemRequest: moves the model item from MODELS to MODELS_IN_BBB keeping its object id (as live did).
|
||||
// Returns the id in MODELS_IN_BBB, or LWOOBJID_EMPTY when the player has no such model.
|
||||
LWOOBJID LoadModel(Entity& player, LWOOBJID itemId);
|
||||
|
||||
@@ -39,6 +39,35 @@ A brick built model item's config (as live sent it): `blueprintid`, `userModelID
|
||||
`userModelHasBhvr`, `userModelBehaviors` ("id,id,id,id,id"), `userModelBehaviorSourceIDs`, `userModelOpt`,
|
||||
`userModelMod`, `userModelPhysicsType` (`BrickByBrick::ModelItemConfig`).
|
||||
|
||||
### Brick built models in the inventory
|
||||
|
||||
How the client shows a brick built model item (LOT 6662) in the backpack, and what it asks the server:
|
||||
|
||||
- **Icon**: `LWOInventoryComponent_Client::LoadBlueprintIcon` (`0x00c64980`) loads the item's `blueprintid` config
|
||||
(else its subkey) as a 3D services resource (`ResourceSpecifier(blueprint id, Dds)`: the UGC server's
|
||||
`IMAGE128DDS/` file, docs/UgcServer.md) and shows it with `InventoryLoadCustomIcon`; until then the item shows
|
||||
`RenderComponent.icon_asset`. Nothing is sent to the world server for the icon.
|
||||
- **Tooltip** (`LWOInventoryComponent_Client::FillinDetailsBlueprint`, `0x00cddb40`): for LOT 6662 with a persistent
|
||||
subkey (the UGID) it reads the model's metadata from the client's cache (`UGModelMetadataCache::GetModelMetadata`,
|
||||
`0x00b5e680`). Not cached: `C->S FetchModelMetadataRequest(context, objectID, requestorID, ugID)` and the name shows
|
||||
`BBB_LOADING_BLUEPRINT` until the answer. With no name the tooltip says whose model it is (`UGG_MODEL_owner_model`
|
||||
with `owningPlayerName`). Exhibits need both parts too: they compare the model's box to the exhibit's size.
|
||||
- **Answer**: `S->C FetchModelMetadataResponse(ugID, objectID, requestorID, context, bHasUGData, bHasBPData, UG data,
|
||||
blueprint data)` (`0x00e3ccb0`, `0x00f5f590`, `0x00f5f760`). The client caches it and writes `blueprintid`,
|
||||
`userModelMod`, `userModelOpt`, `userModelID`, `userModelName`, `userModelDesc`, `userModelHasBhvr` and
|
||||
`userModelBehaviors` into the item's config (`msgFetchModelMetadataResponse`, `0x00c64300`), then refreshes the item.
|
||||
|
||||
What live sent (2014 captures, one request per model the client shows, the item's own ids 0): UG data with the UGID,
|
||||
blueprint id, name and description (mostly empty), owner character, account and name, and always 5 behavior ids; for
|
||||
a brick built model also the blueprint data: blueprint id, creation time, `userModelMod` 1, the model's box relative
|
||||
to its origin, `userModelOpt` 1, `ugcIconReady` 1, the brick list (every brick's LOT followed by `:`), 1, and the
|
||||
number of bricks. Premade models got UG data only (behaviors, no owner); an unknown UGID got neither.
|
||||
|
||||
DLU (`BrickByBrick::FillModelMetadata`) finds the UGID among the player's items (their subkey), else among the placed
|
||||
models (`properties_contents`), and fills the rest from the `ugc` row (owner) and its LXFML (bricks and box). Two
|
||||
differences: there is no creation time stored (0 is sent), and the box holds the bricks' origins (brick shapes are
|
||||
not known to the world server), so it is smaller than the model.
|
||||
|
||||
## 2. Entering and leaving property editing
|
||||
|
||||
1. `C->S StartBuildingWithItem` (subject: property plaque/build area; source = the thinking hat, `sourceType` 1).
|
||||
@@ -213,6 +242,12 @@ equipped), `S->C FinishArrangingWithItem` / `ModularBuildEnd`, `C->S DoneArrangi
|
||||
| `0x00b73c70` | `BBBManager::RegisterUIListeners` |
|
||||
| `0x00cfabd0` | `LWOBBBComponent_Client::msgRebuildBBBAutosaveMsg` |
|
||||
| `0x00d8ecb0` | `GameMessage::ActivateBrickMode::Deserialize` |
|
||||
| `0x00b5e680` | `UGModelMetadataCache::GetModelMetadata` (sends `FetchModelMetadataRequest`) |
|
||||
| `0x00c64300` | `LWOInventoryComponent_Client::msgFetchModelMetadataResponse` |
|
||||
| `0x00e3ccb0` | `GameMessage::FetchModelMetadataResponse::Serialize` |
|
||||
| `0x00f5f590` / `0x00f5f760` | `UGObjectMetadata::Serialize` / `BlueprintMetadata::Serialize` |
|
||||
| `0x00cddb40` | `LWOInventoryComponent_Client::FillinDetailsBlueprint` (model item tooltip) |
|
||||
| `0x00c64980` | `LWOInventoryComponent_Client::LoadBlueprintIcon` |
|
||||
| `0x00f2af60` | `GameMessage::SetBBBAutosave::Deserialize` |
|
||||
| `0x00dc0170` | `GameMessage::PlaceModelResponse::Deserialize` |
|
||||
| `0x00ce1310` | `LWOInventoryComponent_Common::msgMoveInventoryBatch` |
|
||||
|
||||
@@ -411,3 +411,22 @@ TEST(LxfmlTests, DeepCloneDepthProtection) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LxfmlTests, ReadContentsListsBricksAndTheirBox) {
|
||||
const std::string lxfml = R"(<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<LXFML versionMajor="5" versionMinor="0">
|
||||
<Bricks>
|
||||
<Brick refID="0" designID="3710"><Part refID="0" designID="3710" materials="21"><Bone refID="0" transformation="1,0,0,0,1,0,0,0,1,-1.2,0,0.4"/></Part></Brick>
|
||||
<Brick refID="1" designID="3001;A"><Part refID="1" designID="3001" materials="1"><Bone refID="1" transformation="1,0,0,0,1,0,0,0,1,0.8,0.96,-0.4"/></Part></Brick>
|
||||
</Bricks>
|
||||
</LXFML>)";
|
||||
const auto contents = Lxfml::ReadContents(lxfml);
|
||||
EXPECT_EQ(contents.designIds, (std::vector<uint32_t>{ 3710, 3001 }));
|
||||
EXPECT_EQ(contents.boxMin, NiPoint3(-1.2f, 0.0f, -0.4f));
|
||||
EXPECT_EQ(contents.boxMax, NiPoint3(0.8f, 0.96f, 0.4f));
|
||||
|
||||
const auto nothing = Lxfml::ReadContents("");
|
||||
EXPECT_TRUE(nothing.designIds.empty());
|
||||
EXPECT_EQ(nothing.boxMin, NiPoint3Constant::ZERO);
|
||||
EXPECT_TRUE(Lxfml::ReadContents("<LXFML><Bricks/></LXFML>").designIds.empty());
|
||||
}
|
||||
|
||||
@@ -119,3 +119,14 @@ TEST_F(BrickByBrickTests, AReturnedModelIsNeverUsedUpByTheAutosave) {
|
||||
ASSERT_TRUE(saved.has_value());
|
||||
EXPECT_EQ(saved->sourceItems, (std::vector<LWOOBJID>{ 8 }));
|
||||
}
|
||||
|
||||
TEST_F(BrickByBrickTests, ModelMetadataHasFiveBehaviorsAndAColonBrickList) {
|
||||
EXPECT_EQ(BrickByBrick::BehaviorIds(""), std::vector<LWOOBJID>(5, 0));
|
||||
EXPECT_EQ(BrickByBrick::BehaviorIds("0,0,0,0,0"), std::vector<LWOOBJID>(5, 0));
|
||||
EXPECT_EQ(BrickByBrick::BehaviorIds("10449,7"), (std::vector<LWOOBJID>{ 10449, 7, 0, 0, 0 }));
|
||||
EXPECT_EQ(BrickByBrick::BehaviorIds("1,2,3,4,5,6"), (std::vector<LWOOBJID>{ 1, 2, 3, 4, 5 }));
|
||||
|
||||
EXPECT_EQ(BrickByBrick::BrickList({}), u"");
|
||||
EXPECT_EQ(BrickByBrick::BrickList({ 66 }), u"66:");
|
||||
EXPECT_EQ(BrickByBrick::BrickList({ 529, 529, 41 }), u"529:529:41:");
|
||||
}
|
||||
|
||||
@@ -435,3 +435,58 @@ TEST_F(BuildingMessagesTests, BrickModeMessagesRoundTrip) {
|
||||
EXPECT_EQ(RoundTrip(rebuilt).count, 2);
|
||||
ExpectTruncatedFails(rebuilt);
|
||||
}
|
||||
|
||||
// FetchModelMetadataRequest / Response as the client writes and reads them (GameMessage::FetchModelMetadataResponse::
|
||||
// Serialize, 0x00e3ccb0; UGObjectMetadata::Serialize, 0x00f5f590; BlueprintMetadata::Serialize, 0x00f5f760)
|
||||
TEST_F(BuildingMessagesTests, FetchModelMetadataWire) {
|
||||
GameMessages::FetchModelMetadataRequest request;
|
||||
request.context = 1;
|
||||
request.objectID = 2;
|
||||
request.requestorID = 3;
|
||||
request.ugID = 4;
|
||||
EXPECT_PACKET_EQ(FromHex("01 00 00 00 02 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00"), Payload(request));
|
||||
const auto requestCopy = RoundTrip(request);
|
||||
EXPECT_EQ(requestCopy.context, 1);
|
||||
EXPECT_EQ(requestCopy.objectID, 2);
|
||||
EXPECT_EQ(requestCopy.requestorID, 3);
|
||||
EXPECT_EQ(requestCopy.ugID, 4);
|
||||
ExpectTruncatedFails(request);
|
||||
|
||||
// Nothing known: ugID, objectID, requestorID, context, then the two flags
|
||||
GameMessages::FetchModelMetadataResponse none;
|
||||
none.ugID = 4;
|
||||
EXPECT_EQ(Bits(none), 8 * 28 + 2);
|
||||
|
||||
GameMessages::FetchModelMetadataResponse full;
|
||||
full.ugID = 4;
|
||||
full.bHasUGData = true;
|
||||
full.bHasBPData = true;
|
||||
full.ugData.userModelID = 4;
|
||||
full.ugData.blueprintID = 5;
|
||||
full.ugData.userModelName = u"Car";
|
||||
full.ugData.owningPlayerID = 6;
|
||||
full.ugData.accountID = 7;
|
||||
full.ugData.owningPlayerName = u"Builder";
|
||||
full.ugData.userModelBehaviors = { 8, 0, 0, 0, 0 };
|
||||
full.bpData.blueprintID = 5;
|
||||
full.bpData.modelBoxMins = NiPoint3(-1.6f, 0.0f, -0.8f);
|
||||
full.bpData.modelBoxMaxs = NiPoint3(1.6f, 0.5f, 0.8f);
|
||||
full.bpData.brickListColonDelim = u"66:";
|
||||
full.bpData.numberOfBricks = 1;
|
||||
// UG: 2 ids, name (4 + 6), desc (4), 2 ids, owner name (4 + 14), u8 count, 5 ids
|
||||
const uint32_t ugBits = 8 * (16 + 10 + 4 + 16 + 18 + 1 + 40);
|
||||
// BP: id, timestamp, mod, 2 points, 2 flags, bricks (4 + 6), flag, count
|
||||
const uint32_t bpBits = 8 * (8 + 8 + 4 + 24 + 10 + 4) + 3;
|
||||
EXPECT_EQ(Bits(full), 8 * 28 + 2 + ugBits + bpBits);
|
||||
const auto copy = RoundTrip(full);
|
||||
EXPECT_EQ(copy.ugData.userModelName, u"Car");
|
||||
EXPECT_EQ(copy.ugData.owningPlayerName, u"Builder");
|
||||
EXPECT_EQ(copy.ugData.userModelBehaviors, full.ugData.userModelBehaviors);
|
||||
EXPECT_EQ(copy.bpData.brickListColonDelim, u"66:");
|
||||
EXPECT_EQ(copy.bpData.modelBoxMaxs, full.bpData.modelBoxMaxs);
|
||||
EXPECT_EQ(copy.bpData.numberOfBricks, 1);
|
||||
EXPECT_TRUE(copy.bpData.userModelOpt);
|
||||
EXPECT_TRUE(copy.bpData.ugcIconReady);
|
||||
EXPECT_TRUE(copy.bpData.neverFalseIfPresentInCaps);
|
||||
ExpectTruncatedFails(full);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user