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{};
LWOOBJID ugcId{};
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.

View File

@@ -3,7 +3,7 @@
std::vector<IPropertyContents::Model> MySQLDatabase::GetPropertyModels(const LWOOBJID& propertyId) {
auto result = ExecuteSelect(
"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);
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[3] = result->getUInt64("behavior_4");
model.behaviors[4] = result->getUInt64("behavior_5");
model.placedBy = result->getUInt64("placed_by");
toReturn.push_back(std::move(model));
}
@@ -35,9 +36,9 @@ void MySQLDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const IPr
try {
ExecuteInsert(
"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)"
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 18
"(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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
// 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.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.
@@ -45,7 +46,8 @@ void MySQLDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const IPr
model.behaviors[1], // behavior 2
model.behaviors[2], // behavior 3
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) {
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[3] = result->getUInt64("behavior_4");
model->behaviors[4] = result->getUInt64("behavior_5");
model->placedBy = result->getUInt64("placed_by");
}
return model;

View File

@@ -3,7 +3,7 @@
std::vector<IPropertyContents::Model> SQLiteDatabase::GetPropertyModels(const LWOOBJID& propertyId) {
auto [_, result] = ExecuteSelect(
"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);
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[3] = result.getInt64Field("behavior_4");
model.behaviors[4] = result.getInt64Field("behavior_5");
model.placedBy = result.getInt64Field("placed_by");
toReturn.push_back(std::move(model));
result.nextRow();
@@ -35,9 +36,9 @@ void SQLiteDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const IP
try {
ExecuteInsert(
"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)"
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 18
"(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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
// 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.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.
@@ -45,7 +46,8 @@ void SQLiteDatabase::InsertNewPropertyModel(const LWOOBJID& propertyId, const IP
model.behaviors[1], // behavior 2
model.behaviors[2], // behavior 3
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) {
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[3] = result.getInt64Field("behavior_4");
model->behaviors[4] = result.getInt64Field("behavior_5");
model->placedBy = result.getInt64Field("placed_by");
} 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 <algorithm>
#include <chrono>
#include <cstdlib>
#include <filesystem>
@@ -679,6 +680,21 @@ TEST_F(ParitySeeded, Properties) {
model.id = 1152921510000400002LL;
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) }; });
for (const auto sort : { IProperty::ShowcaseSort::REPUTATION, IProperty::ShowcaseSort::NEWEST, IProperty::ShowcaseSort::NAME }) {
Both("GetShowcaseProperties " + std::to_string(static_cast<int>(sort)), [&](GameDatabase& db) {