feat: store each named pet's LOT in pet_names

pet_names gets a pet_lot column (mysql 80, sqlite 63). The world writes
it whenever it saves a pet name, from the pet entity's LOT, and fills it
in for older rows when the owner loads into a world (from the pets the
game loads for that character, only where it is still missing).

The dashboard's pet name tables read pet_lot instead of scanning the
owner's character XML; pets without it yet show as Unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-27 03:51:39 -05:00
parent 4735ce4f42
commit 7284422320
14 changed files with 72 additions and 36 deletions

View File

@@ -16,6 +16,8 @@ public:
int32_t approvalStatus{};
// The character that owns the pet (0 when not known). Saving with 0 keeps the owner already stored.
LWOOBJID ownerId{};
// The pet's LOT (0 when not known). Saving with 0 keeps the LOT already stored.
LOT petLot{};
};
// Set the pet name moderation status for the given pet id.
@@ -36,6 +38,9 @@ public:
// Pets whose owner was never recorded (named before owners were saved), and recording one (0: nobody has it)
virtual std::vector<LWOOBJID> GetPetsWithUnknownOwner() = 0;
virtual void SetPetOwner(const LWOOBJID petId, const LWOOBJID ownerId) = 0;
// Record the pet's LOT on its pet_names row, only when the row has none yet (NULL or 0). Does nothing without a row.
virtual void SetPetLotIfMissing(const LWOOBJID petId, const LOT petLot) = 0;
};
#endif //!__IPETNAMES__H__

View File

@@ -406,6 +406,7 @@ public:
void RejectPetName(const int64_t id) override;
std::vector<LWOOBJID> GetPetsWithUnknownOwner() override;
void SetPetOwner(const LWOOBJID petId, const LWOOBJID ownerId) override;
void SetPetLotIfMissing(const LWOOBJID petId, const LOT petLot) override;
nlohmann::json GetAccountCharacters(uint32_t accountId) override;
void DeleteAccount(const uint32_t accountId) override;
void InsertAuditLog(uint32_t accountId, const std::string_view accountName, const std::string_view action, const std::string_view description, uint32_t targetAccountId, LWOOBJID targetCharacterId) override;

View File

@@ -4,20 +4,23 @@
void MySQLDatabase::SetPetNameModerationStatus(const LWOOBJID& petId, const IPetNames::Info& info) {
const auto owner = info.ownerId == 0 ? std::optional<LWOOBJID>{} : std::optional<LWOOBJID>{ info.ownerId };
const auto lot = info.petLot <= 0 ? std::optional<uint32_t>{} : std::optional<uint32_t>{ static_cast<uint32_t>(info.petLot) };
ExecuteInsert(
"INSERT INTO `pet_names` (`id`, `pet_name`, `approved`, `owner_id`) VALUES (?, ?, ?, ?) "
"ON DUPLICATE KEY UPDATE pet_name = ?, approved = ?, owner_id = COALESCE(?, owner_id);",
"INSERT INTO `pet_names` (`id`, `pet_name`, `approved`, `owner_id`, `pet_lot`) VALUES (?, ?, ?, ?, ?) "
"ON DUPLICATE KEY UPDATE pet_name = ?, approved = ?, owner_id = COALESCE(?, owner_id), pet_lot = COALESCE(?, pet_lot);",
petId,
info.petName,
info.approvalStatus,
owner,
lot,
info.petName,
info.approvalStatus,
owner);
owner,
lot);
}
std::optional<IPetNames::Info> MySQLDatabase::GetPetNameInfo(const LWOOBJID& petId) {
auto result = ExecuteSelect("SELECT pet_name, approved, owner_id FROM pet_names WHERE id = ? LIMIT 1;", petId);
auto result = ExecuteSelect("SELECT pet_name, approved, owner_id, pet_lot FROM pet_names WHERE id = ? LIMIT 1;", petId);
if (!result->next()) {
return std::nullopt;
@@ -27,6 +30,7 @@ std::optional<IPetNames::Info> MySQLDatabase::GetPetNameInfo(const LWOOBJID& pet
toReturn.petName = result->getString("pet_name").c_str();
toReturn.approvalStatus = result->getInt("approved");
toReturn.ownerId = result->isNull("owner_id") ? 0 : result->getInt64("owner_id");
toReturn.petLot = result->isNull("pet_lot") ? 0 : result->getInt("pet_lot");
return toReturn;
}
@@ -51,7 +55,7 @@ std::string MySQLDatabase::GetPetNamesTable(uint32_t start, uint32_t length, con
case 3: orderColumnName = "c.name"; break;
}
std::string orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
std::string mainQuery = "SELECT p.id, p.pet_name, p.approved, p.owner_id, c.name AS owner_name" + from + whereClause + orderClause + " LIMIT ?, ?;";
std::string mainQuery = "SELECT p.id, p.pet_name, p.approved, p.owner_id, p.pet_lot, c.name AS owner_name" + from + whereClause + orderClause + " LIMIT ?, ?;";
auto totalCountResult = ExecuteSelect("SELECT COUNT(*) as count FROM pet_names p" + (pendingOnly ? " WHERE " + pendingFilter : std::string{}) + ";");
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
@@ -74,6 +78,7 @@ std::string MySQLDatabase::GetPetNamesTable(uint32_t start, uint32_t length, con
{"pet_name", result->getString("pet_name")},
{"approved", result->getInt("approved")},
{"owner_id", hasOwner ? std::to_string(result->getInt64("owner_id")) : ""},
{"lot", result->isNull("pet_lot") ? 0 : result->getInt("pet_lot")},
{"owner_name", std::string(result->isNull("owner_name") ? "" : result->getString("owner_name").c_str())}
});
}
@@ -98,3 +103,8 @@ std::vector<LWOOBJID> MySQLDatabase::GetPetsWithUnknownOwner() {
void MySQLDatabase::SetPetOwner(const LWOOBJID petId, const LWOOBJID ownerId) {
ExecuteUpdate("UPDATE pet_names SET owner_id = ? WHERE id = ?;", ownerId, petId);
}
void MySQLDatabase::SetPetLotIfMissing(const LWOOBJID petId, const LOT petLot) {
if (petLot <= 0) return;
ExecuteUpdate("UPDATE pet_names SET pet_lot = ? WHERE id = ? AND (pet_lot IS NULL OR pet_lot = 0);", petLot, petId);
}

View File

@@ -390,6 +390,7 @@ public:
void RejectPetName(const int64_t id) override;
std::vector<LWOOBJID> GetPetsWithUnknownOwner() override;
void SetPetOwner(const LWOOBJID petId, const LWOOBJID ownerId) override;
void SetPetLotIfMissing(const LWOOBJID petId, const LOT petLot) override;
nlohmann::json GetAccountCharacters(uint32_t accountId) override;
void DeleteAccount(const uint32_t accountId) override;
void InsertAuditLog(uint32_t accountId, const std::string_view accountName, const std::string_view action, const std::string_view description, uint32_t targetAccountId, LWOOBJID targetCharacterId) override;

View File

@@ -4,20 +4,23 @@
void SQLiteDatabase::SetPetNameModerationStatus(const LWOOBJID& petId, const IPetNames::Info& info) {
const auto owner = info.ownerId == 0 ? std::optional<LWOOBJID>{} : std::optional<LWOOBJID>{ info.ownerId };
const auto lot = info.petLot <= 0 ? std::optional<uint32_t>{} : std::optional<uint32_t>{ static_cast<uint32_t>(info.petLot) };
ExecuteInsert(
"INSERT INTO `pet_names` (`id`, `pet_name`, `approved`, `owner_id`) VALUES (?, ?, ?, ?) "
"ON CONFLICT(id) DO UPDATE SET pet_name = ?, approved = ?, owner_id = COALESCE(?, owner_id);",
"INSERT INTO `pet_names` (`id`, `pet_name`, `approved`, `owner_id`, `pet_lot`) VALUES (?, ?, ?, ?, ?) "
"ON CONFLICT(id) DO UPDATE SET pet_name = ?, approved = ?, owner_id = COALESCE(?, owner_id), pet_lot = COALESCE(?, pet_lot);",
petId,
info.petName,
info.approvalStatus,
owner,
lot,
info.petName,
info.approvalStatus,
owner);
owner,
lot);
}
std::optional<IPetNames::Info> SQLiteDatabase::GetPetNameInfo(const LWOOBJID& petId) {
auto [_, result] = ExecuteSelect("SELECT pet_name, approved, owner_id FROM pet_names WHERE id = ? LIMIT 1;", petId);
auto [_, result] = ExecuteSelect("SELECT pet_name, approved, owner_id, pet_lot FROM pet_names WHERE id = ? LIMIT 1;", petId);
if (result.eof()) {
return std::nullopt;
@@ -27,6 +30,7 @@ std::optional<IPetNames::Info> SQLiteDatabase::GetPetNameInfo(const LWOOBJID& pe
toReturn.petName = result.getStringField("pet_name");
toReturn.approvalStatus = result.getIntField("approved");
toReturn.ownerId = result.fieldIsNull("owner_id") ? 0 : result.getInt64Field("owner_id");
toReturn.petLot = result.fieldIsNull("pet_lot") ? 0 : result.getIntField("pet_lot");
return toReturn;
}
@@ -51,7 +55,7 @@ std::string SQLiteDatabase::GetPetNamesTable(uint32_t start, uint32_t length, co
case 3: orderColumnName = "c.name"; break;
}
std::string orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
std::string mainQuery = "SELECT p.id, p.pet_name, p.approved, p.owner_id, c.name AS owner_name" + from + whereClause + orderClause + " LIMIT ? OFFSET ?;";
std::string mainQuery = "SELECT p.id, p.pet_name, p.approved, p.owner_id, p.pet_lot, c.name AS owner_name" + from + whereClause + orderClause + " LIMIT ? OFFSET ?;";
auto [__, totalCountResult] = ExecuteSelect("SELECT COUNT(*) as count FROM pet_names p" + (pendingOnly ? " WHERE " + pendingFilter : std::string{}) + ";");
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
@@ -74,6 +78,7 @@ std::string SQLiteDatabase::GetPetNamesTable(uint32_t start, uint32_t length, co
{"pet_name", result.getStringField("pet_name")},
{"approved", result.getIntField("approved")},
{"owner_id", hasOwner ? std::to_string(result.getInt64Field("owner_id")) : ""},
{"lot", result.fieldIsNull("pet_lot") ? 0 : result.getIntField("pet_lot")},
{"owner_name", std::string(result.fieldIsNull("owner_name") ? "" : result.getStringField("owner_name"))}
});
result.nextRow();
@@ -99,3 +104,8 @@ std::vector<LWOOBJID> SQLiteDatabase::GetPetsWithUnknownOwner() {
void SQLiteDatabase::SetPetOwner(const LWOOBJID petId, const LWOOBJID ownerId) {
ExecuteUpdate("UPDATE pet_names SET owner_id = ? WHERE id = ?;", ownerId, petId);
}
void SQLiteDatabase::SetPetLotIfMissing(const LWOOBJID petId, const LOT petLot) {
if (petLot <= 0) return;
ExecuteUpdate("UPDATE pet_names SET pet_lot = ? WHERE id = ? AND (pet_lot IS NULL OR pet_lot = 0);", petLot, petId);
}

View File

@@ -364,6 +364,7 @@ class TestSQLDatabase : public GameDatabase {
void RejectPetName(const int64_t id) override {};
std::vector<LWOOBJID> GetPetsWithUnknownOwner() override { return {}; }
void SetPetOwner(const LWOOBJID petId, const LWOOBJID ownerId) override {}
void SetPetLotIfMissing(const LWOOBJID petId, const LOT petLot) override {}
nlohmann::json GetAccountCharacters(uint32_t accountId) override { return nlohmann::json::array(); };
void ApproveProperty(const LWOOBJID propertyId) override {};
bool IsNameInUse(const std::string_view name) override { return false; };