feat(db): properties_contents.placed_by, the character who placed a model

NULL is the owner (every model placed before this, and dashboard imports).
Read by GetPropertyModels and GetModel, written by InsertNewPropertyModel on
MySQL and SQLite, with a parity test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-28 12:08:29 -05:00
parent 462a152a66
commit bfa46408cb
6 changed files with 43 additions and 10 deletions

View File

@@ -18,6 +18,8 @@ public:
LOT lot{}; LOT lot{};
LWOOBJID ugcId{}; LWOOBJID ugcId{};
std::array<LWOOBJID, 5> behaviors{}; std::array<LWOOBJID, 5> behaviors{};
// The character who placed the model; 0 (NULL in the database) for the property's owner and older models
LWOOBJID placedBy{};
}; };
// Inserts a new UGC model into the database. // Inserts a new UGC model into the database.

View File

@@ -3,7 +3,7 @@
std::vector<IPropertyContents::Model> MySQLDatabase::GetPropertyModels(const LWOOBJID& propertyId) { std::vector<IPropertyContents::Model> MySQLDatabase::GetPropertyModels(const LWOOBJID& propertyId) {
auto result = ExecuteSelect( auto result = ExecuteSelect(
"SELECT id, lot, x, y, z, rx, ry, rz, rw, ugc_id, " "SELECT id, lot, x, y, z, rx, ry, rz, rw, ugc_id, "
"behavior_1, behavior_2, behavior_3, behavior_4, behavior_5 " "behavior_1, behavior_2, behavior_3, behavior_4, behavior_5, placed_by "
"FROM properties_contents WHERE property_id = ?;", propertyId); "FROM properties_contents WHERE property_id = ?;", propertyId);
std::vector<IPropertyContents::Model> toReturn; std::vector<IPropertyContents::Model> toReturn;
@@ -25,6 +25,7 @@ std::vector<IPropertyContents::Model> MySQLDatabase::GetPropertyModels(const LWO
model.behaviors[2] = result->getUInt64("behavior_3"); model.behaviors[2] = result->getUInt64("behavior_3");
model.behaviors[3] = result->getUInt64("behavior_4"); model.behaviors[3] = result->getUInt64("behavior_4");
model.behaviors[4] = result->getUInt64("behavior_5"); model.behaviors[4] = result->getUInt64("behavior_5");
model.placedBy = result->getUInt64("placed_by");
toReturn.push_back(std::move(model)); toReturn.push_back(std::move(model));
} }
@@ -35,9 +36,9 @@ void MySQLDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const IPr
try { try {
ExecuteInsert( ExecuteInsert(
"INSERT INTO properties_contents" "INSERT INTO properties_contents"
"(id, property_id, ugc_id, lot, x, y, z, rx, ry, rz, rw, model_name, model_description, behavior_1, behavior_2, behavior_3, behavior_4, behavior_5)" "(id, property_id, ugc_id, lot, x, y, z, rx, ry, rz, rw, model_name, model_description, behavior_1, behavior_2, behavior_3, behavior_4, behavior_5, placed_by)"
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 18 // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 18 19
model.id, propertyId, model.ugcId == 0 ? std::nullopt : std::optional(model.ugcId), static_cast<uint32_t>(model.lot), model.id, propertyId, model.ugcId == 0 ? std::nullopt : std::optional(model.ugcId), static_cast<uint32_t>(model.lot),
model.position.x, model.position.y, model.position.z, model.rotation.x, model.rotation.y, model.rotation.z, model.rotation.w, model.position.x, model.position.y, model.position.z, model.rotation.x, model.rotation.y, model.rotation.z, model.rotation.w,
name, "", // Model description. TODO implement this. name, "", // Model description. TODO implement this.
@@ -45,7 +46,8 @@ void MySQLDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const IPr
model.behaviors[1], // behavior 2 model.behaviors[1], // behavior 2
model.behaviors[2], // behavior 3 model.behaviors[2], // behavior 3
model.behaviors[3], // behavior 4 model.behaviors[3], // behavior 4
model.behaviors[4] // behavior 5 model.behaviors[4], // behavior 5
model.placedBy == 0 ? std::nullopt : std::optional(model.placedBy)
); );
} catch (sql::SQLException& e) { } catch (sql::SQLException& e) {
LOG("Error inserting new property model: %s", e.what()); LOG("Error inserting new property model: %s", e.what());
@@ -85,6 +87,7 @@ std::optional<IPropertyContents::Model> MySQLDatabase::GetModel(const LWOOBJID m
model->behaviors[2] = result->getUInt64("behavior_3"); model->behaviors[2] = result->getUInt64("behavior_3");
model->behaviors[3] = result->getUInt64("behavior_4"); model->behaviors[3] = result->getUInt64("behavior_4");
model->behaviors[4] = result->getUInt64("behavior_5"); model->behaviors[4] = result->getUInt64("behavior_5");
model->placedBy = result->getUInt64("placed_by");
} }
return model; return model;

View File

@@ -3,7 +3,7 @@
std::vector<IPropertyContents::Model> SQLiteDatabase::GetPropertyModels(const LWOOBJID& propertyId) { std::vector<IPropertyContents::Model> SQLiteDatabase::GetPropertyModels(const LWOOBJID& propertyId) {
auto [_, result] = ExecuteSelect( auto [_, result] = ExecuteSelect(
"SELECT id, lot, x, y, z, rx, ry, rz, rw, ugc_id, " "SELECT id, lot, x, y, z, rx, ry, rz, rw, ugc_id, "
"behavior_1, behavior_2, behavior_3, behavior_4, behavior_5 " "behavior_1, behavior_2, behavior_3, behavior_4, behavior_5, placed_by "
"FROM properties_contents WHERE property_id = ?;", propertyId); "FROM properties_contents WHERE property_id = ?;", propertyId);
std::vector<IPropertyContents::Model> toReturn; std::vector<IPropertyContents::Model> toReturn;
@@ -24,6 +24,7 @@ std::vector<IPropertyContents::Model> SQLiteDatabase::GetPropertyModels(const LW
model.behaviors[2] = result.getInt64Field("behavior_3"); model.behaviors[2] = result.getInt64Field("behavior_3");
model.behaviors[3] = result.getInt64Field("behavior_4"); model.behaviors[3] = result.getInt64Field("behavior_4");
model.behaviors[4] = result.getInt64Field("behavior_5"); model.behaviors[4] = result.getInt64Field("behavior_5");
model.placedBy = result.getInt64Field("placed_by");
toReturn.push_back(std::move(model)); toReturn.push_back(std::move(model));
result.nextRow(); result.nextRow();
@@ -35,9 +36,9 @@ void SQLiteDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const IP
try { try {
ExecuteInsert( ExecuteInsert(
"INSERT INTO properties_contents" "INSERT INTO properties_contents"
"(id, property_id, ugc_id, lot, x, y, z, rx, ry, rz, rw, model_name, model_description, behavior_1, behavior_2, behavior_3, behavior_4, behavior_5)" "(id, property_id, ugc_id, lot, x, y, z, rx, ry, rz, rw, model_name, model_description, behavior_1, behavior_2, behavior_3, behavior_4, behavior_5, placed_by)"
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 18 // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 18 19
model.id, propertyId, model.ugcId == 0 ? std::nullopt : std::optional(model.ugcId), static_cast<uint32_t>(model.lot), model.id, propertyId, model.ugcId == 0 ? std::nullopt : std::optional(model.ugcId), static_cast<uint32_t>(model.lot),
model.position.x, model.position.y, model.position.z, model.rotation.x, model.rotation.y, model.rotation.z, model.rotation.w, model.position.x, model.position.y, model.position.z, model.rotation.x, model.rotation.y, model.rotation.z, model.rotation.w,
name, "", // Model description. TODO implement this. name, "", // Model description. TODO implement this.
@@ -45,7 +46,8 @@ void SQLiteDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const IP
model.behaviors[1], // behavior 2 model.behaviors[1], // behavior 2
model.behaviors[2], // behavior 3 model.behaviors[2], // behavior 3
model.behaviors[3], // behavior 4 model.behaviors[3], // behavior 4
model.behaviors[4] // behavior 5 model.behaviors[4], // behavior 5
model.placedBy == 0 ? std::nullopt : std::optional(model.placedBy)
); );
} catch (std::exception& e) { } catch (std::exception& e) {
LOG("Error inserting new property model: %s", e.what()); LOG("Error inserting new property model: %s", e.what());
@@ -86,6 +88,7 @@ std::optional<IPropertyContents::Model> SQLiteDatabase::GetModel(const LWOOBJID
model->behaviors[2] = result.getInt64Field("behavior_3"); model->behaviors[2] = result.getInt64Field("behavior_3");
model->behaviors[3] = result.getInt64Field("behavior_4"); model->behaviors[3] = result.getInt64Field("behavior_4");
model->behaviors[4] = result.getInt64Field("behavior_5"); model->behaviors[4] = result.getInt64Field("behavior_5");
model->placedBy = result.getInt64Field("placed_by");
} while (result.nextRow()); } while (result.nextRow());
} }

View File

@@ -0,0 +1,7 @@
/* properties_contents.placed_by is the character who placed the model (NULL: the property's owner, and every model
placed before this column). A model picked up by another player who can build there goes back to them. */
SET @dlu_column = (SELECT IF(COUNT(*) = 0, 'ALTER TABLE properties_contents ADD COLUMN placed_by BIGINT NULL DEFAULT NULL', 'DO 0') FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = 'properties_contents' AND column_name = 'placed_by');
PREPARE dlu_column_stmt FROM @dlu_column;
EXECUTE dlu_column_stmt;
DEALLOCATE PREPARE dlu_column_stmt;

View File

@@ -0,0 +1,2 @@
/* The character who placed a property model (NULL: the owner). See the MySQL migration. */
ALTER TABLE properties_contents ADD COLUMN placed_by BIGINT DEFAULT NULL;

View File

@@ -14,6 +14,7 @@
#include <conncpp.hpp> #include <conncpp.hpp>
#include <algorithm>
#include <chrono> #include <chrono>
#include <cstdlib> #include <cstdlib>
#include <filesystem> #include <filesystem>
@@ -679,6 +680,21 @@ TEST_F(ParitySeeded, Properties) {
model.id = 1152921510000400002LL; model.id = 1152921510000400002LL;
db.InsertNewPropertyModel(PROP1, model, "Objects_14_name"); db.InsertNewPropertyModel(PROP1, model, "Objects_14_name");
}); });
Both("InsertNewPropertyModel placed by", [](GameDatabase& db) {
// NULL placed_by (the owner's) reads back as 0; a builder's id reads back as itself
IPropertyContents::Model model;
model.id = 1152921510000400003LL; model.lot = 6326; model.placedBy = CHAR_BOB;
db.InsertNewPropertyModel(PROP1, model, "Objects_6326_name");
auto models = db.GetPropertyModels(PROP1);
std::ranges::sort(models, {}, &IPropertyContents::Model::id);
json out = json::array();
for (const auto& placed : models) out.push_back({ placed.id, placed.lot, placed.placedBy });
const auto one = db.GetModel(1152921510000400003LL);
out.push_back(one ? json{ one->id, one->placedBy } : json());
const auto owners = db.GetModel(1152921510000400001LL);
out.push_back(owners ? json{ owners->id, owners->placedBy } : json());
return out;
});
Both("GetModelPropertyId", [](GameDatabase& db) { return json{ db.GetModelPropertyId(1152921510000400001LL), db.GetModelPropertyId(1) }; }); Both("GetModelPropertyId", [](GameDatabase& db) { return json{ db.GetModelPropertyId(1152921510000400001LL), db.GetModelPropertyId(1) }; });
for (const auto sort : { IProperty::ShowcaseSort::REPUTATION, IProperty::ShowcaseSort::NEWEST, IProperty::ShowcaseSort::NAME }) { for (const auto sort : { IProperty::ShowcaseSort::REPUTATION, IProperty::ShowcaseSort::NEWEST, IProperty::ShowcaseSort::NAME }) {
Both("GetShowcaseProperties " + std::to_string(static_cast<int>(sort)), [&](GameDatabase& db) { Both("GetShowcaseProperties " + std::to_string(static_cast<int>(sort)), [&](GameDatabase& db) {