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>
This commit is contained in:
Aaron Kimbrell
2026-09-27 04:21:48 -05:00
parent 9256b31b23
commit 6ce261c58c
19 changed files with 976 additions and 451 deletions

View File

@@ -1,4 +1,5 @@
#include "InventoryComponent.h"
#include "BrickByBrick.h"
#include "Contraband.h"
#include "EconomyLedger.h"
@@ -663,6 +664,9 @@ void InventoryComponent::LoadXml(const tinyxml2::XMLDocument& document) {
bag->QueryAttribute("t", &type);
// A build in progress does not survive a reload: what was in the BBB inventories is back in MODELS and BRICKS
const auto savedType = type;
type = BrickByBrick::InventoryToLoadInto(type);
auto* inventory = GetInventory(static_cast<eInventoryType>(type));
if (inventory == nullptr) {
@@ -696,6 +700,12 @@ void InventoryComponent::LoadXml(const tinyxml2::XMLDocument& document) {
itemElement->QueryAttribute("parent", &parent);
// End custom xml
if (savedType != type && !inventory->IsSlotEmpty(slot)) {
const auto freeSlot = inventory->FindEmptySlot();
if (freeSlot == -1) inventory->SetSize(inventory->GetSize() + 1);
slot = freeSlot != -1 ? freeSlot : inventory->FindEmptySlot();
}
auto* item = new Item(id, lot, inventory, slot, count, bound, {}, parent, subKey);
item->LoadConfigXml(*itemElement);
@@ -740,7 +750,9 @@ void InventoryComponent::UpdateXml(tinyxml2::XMLDocument& document) {
for (const auto& pair : this->m_Inventories) {
auto* inventory = pair.second;
static const auto EXCLUDED_INVENTORIES = { VENDOR_BUYBACK, MODELS_IN_BBB, ITEM_SETS };
// MODELS_IN_BBB is saved so a model open in brick by brick building survives a disconnect or a crash; it loads
// back into MODELS (BrickByBrick::InventoryToLoadInto)
static const auto EXCLUDED_INVENTORIES = { VENDOR_BUYBACK, ITEM_SETS };
if (std::ranges::find(EXCLUDED_INVENTORIES, inventory->GetType()) != EXCLUDED_INVENTORIES.end()) {
continue;
}
@@ -1438,7 +1450,7 @@ BehaviorSlot InventoryComponent::FindBehaviorSlot(const std::string& equipLocati
bool InventoryComponent::IsTransferInventory(eInventoryType type, bool includeVault) {
return type == VENDOR_BUYBACK || (includeVault && (type == VAULT_ITEMS || type == VAULT_MODELS)) || type == TEMP_ITEMS || type == TEMP_MODELS || type == MODELS_IN_BBB;
return type == VENDOR_BUYBACK || (includeVault && (type == VAULT_ITEMS || type == VAULT_MODELS)) || type == TEMP_ITEMS || type == TEMP_MODELS || type == MODELS_IN_BBB || type == BRICKS_IN_BBB;
}
uint32_t InventoryComponent::FindSkill(const LOT lot) {