diff --git a/dDashboardServer/routes/APIRoutes.cpp b/dDashboardServer/routes/APIRoutes.cpp index 3387d1bee..3c44328f0 100644 --- a/dDashboardServer/routes/APIRoutes.cpp +++ b/dDashboardServer/routes/APIRoutes.cpp @@ -90,24 +90,13 @@ namespace { } /** - * Adds each pet's LOT and kind (its CDClient name) to pet_names rows. pet_names doesn't hold the LOT; the owner's - * save does (

), so each owner on the page is read once. Pets whose owner is unknown, or who have - * left the owner's save, get lot 0. + * Adds each pet's kind (its CDClient name) to pet_names rows, from the pet_lot the game writes (lot 0: not known yet). */ std::string WithPetKinds(const std::string& raw) { auto json = nlohmann::json::parse(raw, nullptr, false); if (json.is_discarded() || !json.contains("data") || !json["data"].is_array()) return raw; - std::map> owners; for (auto& row : json["data"]) { - const auto owner = GeneralUtils::TryParse(row.value("owner_id", std::string{})).value_or(0); - const auto pet = GeneralUtils::TryParse(row.value("id", std::string{})).value_or(0); - LOT lot = 0; - if (owner && pet) { - auto [it, added] = owners.try_emplace(owner); - if (added) for (const auto& held : CharacterXml::Pets(Database::Get()->GetCharacterXml(owner))) it->second[held.id] = held.lot; - if (const auto found = it->second.find(pet); found != it->second.end()) lot = found->second; - } - row["lot"] = lot; + const LOT lot = row.value("lot", 0); row["kind"] = lot > 0 ? ClientAssets::ItemName(lot) : ""; } return json.dump(); diff --git a/dDashboardServer/static/js/common.js b/dDashboardServer/static/js/common.js index 8ab20bb66..fc1010be2 100644 --- a/dDashboardServer/static/js/common.js +++ b/dDashboardServer/static/js/common.js @@ -137,7 +137,7 @@ }, // A pet's kind: its icon and CDClient name (rows from the pet name tables carry lot and kind) pet: function (lot, kind) { - if (!lot) return 'Unknown'; + if (!lot) return 'Unknown'; return '' + esc(kind || ('LOT ' + lot)) + ' (' + esc(lot) + ')'; }, diff --git a/dDatabase/GameDatabase/ITables/IPetNames.h b/dDatabase/GameDatabase/ITables/IPetNames.h index cba64d6a5..92845108d 100644 --- a/dDatabase/GameDatabase/ITables/IPetNames.h +++ b/dDatabase/GameDatabase/ITables/IPetNames.h @@ -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 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__ diff --git a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h index 84bfdb16f..73f96e6f2 100644 --- a/dDatabase/GameDatabase/MySQL/MySQLDatabase.h +++ b/dDatabase/GameDatabase/MySQL/MySQLDatabase.h @@ -406,6 +406,7 @@ public: void RejectPetName(const int64_t id) override; std::vector 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; diff --git a/dDatabase/GameDatabase/MySQL/Tables/PetNames.cpp b/dDatabase/GameDatabase/MySQL/Tables/PetNames.cpp index 71d3be222..7b26dc050 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/PetNames.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/PetNames.cpp @@ -4,20 +4,23 @@ void MySQLDatabase::SetPetNameModerationStatus(const LWOOBJID& petId, const IPetNames::Info& info) { const auto owner = info.ownerId == 0 ? std::optional{} : std::optional{ info.ownerId }; + const auto lot = info.petLot <= 0 ? std::optional{} : std::optional{ static_cast(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 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 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 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); +} diff --git a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h index cda50f010..b7ba05302 100644 --- a/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h +++ b/dDatabase/GameDatabase/SQLite/SQLiteDatabase.h @@ -390,6 +390,7 @@ public: void RejectPetName(const int64_t id) override; std::vector 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; diff --git a/dDatabase/GameDatabase/SQLite/Tables/PetNames.cpp b/dDatabase/GameDatabase/SQLite/Tables/PetNames.cpp index 02c7f0cd7..feb31f6c6 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/PetNames.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/PetNames.cpp @@ -4,20 +4,23 @@ void SQLiteDatabase::SetPetNameModerationStatus(const LWOOBJID& petId, const IPetNames::Info& info) { const auto owner = info.ownerId == 0 ? std::optional{} : std::optional{ info.ownerId }; + const auto lot = info.petLot <= 0 ? std::optional{} : std::optional{ static_cast(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 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 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 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); +} diff --git a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h index 92373718f..0e3f39eed 100644 --- a/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h +++ b/dDatabase/GameDatabase/TestSQL/TestSQLDatabase.h @@ -364,6 +364,7 @@ class TestSQLDatabase : public GameDatabase { void RejectPetName(const int64_t id) override {}; std::vector 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; }; diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index 12b3c89e0..bf90b201c 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -1731,9 +1731,9 @@ void InventoryComponent::LoadPetXml(const tinyxml2::XMLDocument& document) { auto* petElement = petInventoryElement->FirstChildElement(); while (petElement != nullptr) { - LWOOBJID id; - LOT lot; - int32_t moderationStatus; + LWOOBJID id{}; + LOT lot{}; + int32_t moderationStatus{}; petElement->QueryAttribute("id", &id); petElement->QueryAttribute("l", &lot); @@ -1747,6 +1747,9 @@ void InventoryComponent::LoadPetXml(const tinyxml2::XMLDocument& document) { SetDatabasePet(id, databasePet); + // Pets named before pet_names recorded their LOT get it now that their owner is loaded + if (id != LWOOBJID_EMPTY && lot > 0) Database::Get()->SetPetLotIfMissing(id, lot); + petElement = petElement->NextSiblingElement(); } } diff --git a/dGame/dComponents/PetComponent.cpp b/dGame/dComponents/PetComponent.cpp index fe0c17448..d72a85892 100644 --- a/dGame/dComponents/PetComponent.cpp +++ b/dGame/dComponents/PetComponent.cpp @@ -613,7 +613,7 @@ void PetComponent::RequestSetPetName(std::u16string name) { if (autoRejectNames && owner && owner->GetCharacter() && owner->GetCharacter()->GetParentUser()->GetIsMuted()) { m_ModerationStatus = 2; // Approved std::string forcedName = "Pet"; - Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast(m_ModerationStatus), m_Owner }); + Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast(m_ModerationStatus), m_Owner, m_Parent->GetLOT() }); DashboardNotify::Changed("pet_names", m_DatabaseId); { GameMessages::SetPetName msg; @@ -678,7 +678,7 @@ void PetComponent::RequestSetPetName(std::u16string name) { m_Name = ""; std::string forcedName = "Pet"; - Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast(m_ModerationStatus), m_Tamer }); + Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ forcedName, static_cast(m_ModerationStatus), m_Tamer, m_Parent->GetLOT() }); DashboardNotify::Changed("pet_names", m_DatabaseId); LOG("AccountID: %i is muted, forcing use of predefined pet name", tamer->GetCharacter()->GetParentUser()->GetAccountID()); } else { @@ -1235,7 +1235,7 @@ void PetComponent::SetPetNameForModeration(const std::string& petName, const LWO } //Save to db: - Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ petName, approved, owner }); + Database::Get()->SetPetNameModerationStatus(m_DatabaseId, IPetNames::Info{ petName, approved, owner, m_Parent->GetLOT() }); DashboardNotify::Changed("pet_names", m_DatabaseId); } diff --git a/docs/Dashboard.md b/docs/Dashboard.md index 222f80f81..939ef0259 100644 --- a/docs/Dashboard.md +++ b/docs/Dashboard.md @@ -725,8 +725,9 @@ tells the player in game if they're online. Rejecting asks for an optional reaso account page next to what they asked for. The queues show only what the game itself would still review: rejected properties and names whose player must pick a new one no longer appear. -Pet names show what kind of pet each one is (its icon and name from the CDClient), read from the pet's entry in its -owner's save. A pet whose owner is unknown or no longer has it shows as Unknown. +Pet names show what kind of pet each one is (its icon and name from the CDClient), from the pet's LOT that the game +stores with the name (`pet_names.pet_lot`). Names set before that column existed get their LOT the next time the pet's +owner loads into a world; until then they show as Unknown. ### Moderation history, warnings and bans diff --git a/migrations/dlu/mysql/80_pet_name_lot.sql b/migrations/dlu/mysql/80_pet_name_lot.sql new file mode 100644 index 000000000..4bf56fbb7 --- /dev/null +++ b/migrations/dlu/mysql/80_pet_name_lot.sql @@ -0,0 +1,7 @@ +/* Which kind of pet (its LOT) each named pet is, written by the game when the name is set and filled in for older + rows when the owner next loads into a world. NULL or 0: not known yet. Only added when it is not there yet. */ +SET @dlu_column = (SELECT IF(COUNT(*) = 0, 'ALTER TABLE pet_names ADD COLUMN pet_lot INT NULL DEFAULT NULL', 'DO 0') FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'pet_names' AND column_name = 'pet_lot'); +PREPARE dlu_column_stmt FROM @dlu_column; +EXECUTE dlu_column_stmt; +DEALLOCATE PREPARE dlu_column_stmt; diff --git a/migrations/dlu/sqlite/63_pet_name_lot.sql b/migrations/dlu/sqlite/63_pet_name_lot.sql new file mode 100644 index 000000000..901717d5b --- /dev/null +++ b/migrations/dlu/sqlite/63_pet_name_lot.sql @@ -0,0 +1,2 @@ +/* Which kind of pet (its LOT) each named pet is. See the MySQL migration. */ +ALTER TABLE pet_names ADD COLUMN pet_lot INTEGER NULL DEFAULT NULL; diff --git a/tests/dDatabaseTests/DatabaseParityTests.cpp b/tests/dDatabaseTests/DatabaseParityTests.cpp index 6074159f9..c2d96af44 100644 --- a/tests/dDatabaseTests/DatabaseParityTests.cpp +++ b/tests/dDatabaseTests/DatabaseParityTests.cpp @@ -89,7 +89,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ICharInfo::Info, name, pendingName, id, accou NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IProperty::Info, name, description, rejectionReason, id, ownerId, cloneId, privacyOption, modApproved, lastUpdatedTime, claimedTime, reputation, performanceCost, zoneId); NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IProperty::ShowcaseEntry, info, ownerName, modelCount); NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IProperty::ShowcaseResult, total, entries); -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IPetNames::Info, petName, approvalStatus, ownerId); +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(IPetNames::Info, petName, approvalStatus, ownerId, petLot); NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ILeaderboard::Entry, charId, lastPlayedTimestamp, primaryScore, secondaryScore, tertiaryScore, numWins, numTimesPlayed, ranking, name); NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ILeaderboard::Score, primaryScore, secondaryScore, tertiaryScore); NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ILiveOps::LiveEvent, id, type, title, message, zones, instanceId, config, startsAt, endsAt, state, endedAt, endReason, createdBy, endedBy); @@ -604,11 +604,17 @@ TEST_F(ParitySeeded, BugReports) { TEST_F(ParitySeeded, PetNames) { Both("SetPetNameModerationStatus", [](GameDatabase& db) { - db.SetPetNameModerationStatus(PET1, { "Sparky", 1, CHAR_BOB }); - db.SetPetNameModerationStatus(PET2, { "Sparky", 2, CHAR_ALICE }); + db.SetPetNameModerationStatus(PET1, { "Sparky", 1, CHAR_BOB, 3520 }); + db.SetPetNameModerationStatus(PET2, { "Sparky", 2, CHAR_ALICE, 12432 }); db.SetPetNameModerationStatus(PET3, { "Rex", 1, 0 }); }); - Both("SetPetNameModerationStatus keeps owner", [](GameDatabase& db) { db.SetPetNameModerationStatus(PET1, { "Sparky", 1, 0 }); return db.GetPetNameInfo(PET1); }); + Both("SetPetNameModerationStatus keeps owner and lot", [](GameDatabase& db) { db.SetPetNameModerationStatus(PET1, { "Sparky", 1, 0 }); return db.GetPetNameInfo(PET1); }); + Both("SetPetLotIfMissing", [](GameDatabase& db) { + db.SetPetLotIfMissing(PET2, 3520); // PET2 already has its LOT: kept + db.SetPetLotIfMissing(PET3, 12434); // PET3 has none yet: written + db.SetPetLotIfMissing(5, 3520); // no row: nothing + return json{ db.GetPetNameInfo(PET2), db.GetPetNameInfo(PET3), db.GetPetNameInfo(5) }; + }); Both("GetPetNameInfo", [](GameDatabase& db) { return json{ db.GetPetNameInfo(PET2), db.GetPetNameInfo(PET3), db.GetPetNameInfo(5) }; }); Both("GetPetNamesTable", [](GameDatabase& db) { return db.GetPetNamesTable(0, 10); }); Both("GetPetNamesTable pending", [](GameDatabase& db) { return db.GetPetNamesTable(0, 10, "", 0, true, true); });