diff --git a/config.lua.dist b/config.lua.dist index 3515116b06..cc7310efa4 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -101,6 +101,10 @@ depotPremiumLimit = 15000 questTrackerFreeLimit = 10 questTrackerPremiumLimit = 15 +-- VIP Group limits +vipGroupFreeLimit = 3 +vipGroupPremiumLimit = 8 + -- World Light -- NOTE: if defaultWorldLight is set to true the world light algorithm will -- be handled in the sources. set it to false to avoid conflicts if you wish diff --git a/data/migrations/36.lua b/data/migrations/36.lua index d0ffd9c0cb..0f97a1c892 100644 --- a/data/migrations/36.lua +++ b/data/migrations/36.lua @@ -1,3 +1,26 @@ function onUpdateDatabase() - return false + print("> Updating database to version 37 (vipgroups)") + db.query("CREATE TABLE IF NOT EXISTS `account_vipgroups` (`id` int NOT NULL AUTO_INCREMENT, `account_id` int NOT NULL, `name` varchar(128) NOT NULL DEFAULT '', `editable` tinyint NOT NULL DEFAULT '1', PRIMARY KEY (`id`), FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB;") + db.query("ALTER TABLE `account_viplist` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST") + db.query("CREATE TABLE IF NOT EXISTS `account_vipgroup_entry` (`group_id` int NOT NULL, `entry_id` int NOT NULL, UNIQUE KEY `group_entry_index` (`group_id`, `entry_id`), FOREIGN KEY (`group_id`) REFERENCES `account_vipgroups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (`entry_id`) REFERENCES `account_viplist` (`id`) ON DELETE CASCADE ON UPDATE CASCADE) ENGINE=InnoDB;") + + local resultId = db.storeQuery("SELECT `accounts`.`id` AS `account_id` FROM `accounts`") + if resultId ~= false then + local stmt = "INSERT INTO `account_vipgroups` (`account_id`, `name`, `editable`) VALUES " + repeat + stmt = stmt .. "(" .. result.getNumber(resultId, "account_id") .. ", 'Enemies', 0)," + stmt = stmt .. "(" .. result.getNumber(resultId, "account_id") .. ", 'Friends', 0)," + stmt = stmt .. "(" .. result.getNumber(resultId, "account_id") .. ", 'Trading Partners', 0)," + until not result.next(resultId) + result.free(resultId) + + local stmtLen = string.len(stmt) + if stmtLen > 74 then + stmt = string.sub(stmt, 1, stmtLen - 1) + db.query(stmt) + end + end + + db.query("CREATE TRIGGER `oncreate_accounts` AFTER INSERT ON `accounts` FOR EACH ROW BEGIN INSERT INTO `account_vipgroups` (`account_id`, `name`, `editable`) VALUES (NEW.id, 'Enemies', 0), (NEW.id, 'Friends', 0), (NEW.id, 'Trading Partners', 0); END") + return true end diff --git a/data/migrations/37.lua b/data/migrations/37.lua new file mode 100644 index 0000000000..e738126d1f --- /dev/null +++ b/data/migrations/37.lua @@ -0,0 +1,3 @@ +function onUpdateDatabase() + return false +end \ No newline at end of file diff --git a/schema.sql b/schema.sql index 88ff960c01..c78cace5f0 100644 --- a/schema.sql +++ b/schema.sql @@ -132,16 +132,35 @@ CREATE TABLE IF NOT EXISTS `player_namelocks` ( ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; CREATE TABLE IF NOT EXISTS `account_viplist` ( + `id` int NOT NULL AUTO_INCREMENT, `account_id` int NOT NULL COMMENT 'id of account whose viplist entry it is', `player_id` int NOT NULL COMMENT 'id of target player of viplist entry', `description` varchar(128) NOT NULL DEFAULT '', `icon` tinyint unsigned NOT NULL DEFAULT '0', `notify` tinyint NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), UNIQUE KEY `account_player_index` (`account_id`,`player_id`), FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; +CREATE TABLE IF NOT EXISTS `account_vipgroups` ( + `id` int NOT NULL AUTO_INCREMENT, + `account_id` int NOT NULL, + `name` varchar(128) NOT NULL DEFAULT '', + `editable` tinyint NOT NULL DEFAULT '1', + PRIMARY KEY (`id`), + FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + +CREATE TABLE IF NOT EXISTS `account_vipgroup_entry` ( + `group_id` int NOT NULL, + `entry_id` int NOT NULL, + UNIQUE KEY `group_entry_index` (`group_id`, `entry_id`), + FOREIGN KEY (`group_id`) REFERENCES `account_vipgroups` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (`entry_id`) REFERENCES `account_viplist` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; + CREATE TABLE IF NOT EXISTS `guilds` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, @@ -379,10 +398,11 @@ CREATE TABLE IF NOT EXISTS `towns` ( UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; -INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '36'), ('players_record', '0'); +INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '37'), ('players_record', '0'); DROP TRIGGER IF EXISTS `ondelete_players`; DROP TRIGGER IF EXISTS `oncreate_guilds`; +DROP TRIGGER IF EXISTS `oncreate_accounts`; DELIMITER // CREATE TRIGGER `ondelete_players` BEFORE DELETE ON `players` @@ -397,4 +417,11 @@ CREATE TRIGGER `oncreate_guilds` AFTER INSERT ON `guilds` INSERT INTO `guild_ranks` (`name`, `level`, `guild_id`) VALUES ('a Member', 1, NEW.`id`); END // +CREATE TRIGGER `oncreate_accounts` AFTER INSERT ON `accounts` + FOR EACH ROW BEGIN + INSERT INTO `account_vipgroups` (`account_id`, `name`, `editable`) VALUES (NEW.`id`, 'Enemies', 0); + INSERT INTO `account_vipgroups` (`account_id`, `name`, `editable`) VALUES (NEW.`id`, 'Friends', 0); + INSERT INTO `account_vipgroups` (`account_id`, `name`, `editable`) VALUES (NEW.`id`, 'Trading Partners', 0); +END +// DELIMITER ; diff --git a/src/configmanager.cpp b/src/configmanager.cpp index d9230e5951..7046712573 100644 --- a/src/configmanager.cpp +++ b/src/configmanager.cpp @@ -288,6 +288,8 @@ bool ConfigManager::load() integer[QUEST_TRACKER_PREMIUM_LIMIT] = getGlobalNumber(L, "questTrackerPremiumLimit", 15); integer[STAMINA_REGEN_MINUTE] = getGlobalNumber(L, "timeToRegenMinuteStamina", 3 * 60); integer[STAMINA_REGEN_PREMIUM] = getGlobalNumber(L, "timeToRegenMinutePremiumStamina", 6 * 60); + integer[VIPGROUP_FREE_LIMIT] = getGlobalNumber(L, "vipGroupFreeLimit", 3); + integer[VIPGROUP_PREMIUM_LIMIT] = getGlobalNumber(L, "vipGroupPremiumLimit", 8); expStages = loadXMLStages(); if (expStages.empty()) { diff --git a/src/configmanager.h b/src/configmanager.h index 1930fe0b90..f036f42e06 100644 --- a/src/configmanager.h +++ b/src/configmanager.h @@ -120,6 +120,8 @@ enum integer_config_t QUEST_TRACKER_PREMIUM_LIMIT, STAMINA_REGEN_MINUTE, STAMINA_REGEN_PREMIUM, + VIPGROUP_FREE_LIMIT, + VIPGROUP_PREMIUM_LIMIT, LAST_INTEGER_CONFIG /* this must be the last one */ }; diff --git a/src/enums.h b/src/enums.h index 03d054e7d1..056c2486ce 100644 --- a/src/enums.h +++ b/src/enums.h @@ -97,6 +97,13 @@ enum VipStatus_t : uint8_t VIPSTATUS_TRAINING = 3 }; +enum VipGroupAction_t : uint8_t +{ + VIPGROUPACTION_CREATE = 1, + VIPGROUPACTION_EDIT = 2, + VIPGROUPACTION_REMOVE = 3 +}; + enum MarketAction_t { MARKETACTION_BUY = 0, diff --git a/src/game.cpp b/src/game.cpp index 1db8dbbb1f..0b9f07eed9 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -3328,14 +3328,44 @@ void Game::playerRequestRemoveVip(uint32_t playerId, uint32_t guid) } void Game::playerRequestEditVip(uint32_t playerId, uint32_t guid, const std::string& description, uint32_t icon, - bool notify) + bool notify, const std::vector& groupIds) { Player* player = getPlayerByID(playerId); if (!player) { return; } - player->editVIP(guid, description, icon, notify); + player->editVIP(guid, description, icon, notify, groupIds); +} + +void Game::playerRequestAddVipGroup(uint32_t playerId, const std::string& name) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->addVIPGroup(name); +} + +void Game::playerRequestEditVipGroup(uint32_t playerId, uint16_t vipGroupId, const std::string& name) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->editVIPGroup(vipGroupId, name); +} + +void Game::playerRequestRemoveVipGroup(uint32_t playerId, uint16_t vipGroupId) +{ + Player* player = getPlayerByID(playerId); + if (!player) { + return; + } + + player->removeVIPGroup(vipGroupId); } void Game::playerTurn(uint32_t playerId, Direction dir) diff --git a/src/game.h b/src/game.h index 1075ee7868..86b681df4b 100644 --- a/src/game.h +++ b/src/game.h @@ -368,7 +368,10 @@ class Game void playerRequestAddVip(uint32_t playerId, const std::string& name); void playerRequestRemoveVip(uint32_t playerId, uint32_t guid); void playerRequestEditVip(uint32_t playerId, uint32_t guid, const std::string& description, uint32_t icon, - bool notify); + bool notify, const std::vector& groupIds); + void playerRequestAddVipGroup(uint32_t playerId, const std::string& name); + void playerRequestEditVipGroup(uint32_t playerId, uint16_t vipGroupId, const std::string& name); + void playerRequestRemoveVipGroup(uint32_t playerId, uint16_t vipGroupId); void playerTurn(uint32_t playerId, Direction dir); void playerRequestOutfit(uint32_t playerId); void playerRequestEditPodium(uint32_t playerId, const Position& position, uint8_t stackPos, diff --git a/src/groups.cpp b/src/groups.cpp index acda519a1a..8990996a1f 100644 --- a/src/groups.cpp +++ b/src/groups.cpp @@ -65,6 +65,7 @@ bool Groups::load() group.access = groupNode.attribute("access").as_bool(); group.maxDepotItems = pugi::cast(groupNode.attribute("maxdepotitems").value()); group.maxVipEntries = pugi::cast(groupNode.attribute("maxvipentries").value()); + group.maxVipGroups = pugi::cast(groupNode.attribute("maxvipgroups").value()); group.flags = pugi::cast(groupNode.attribute("flags").value()); if (pugi::xml_node node = groupNode.child("flags")) { for (auto flagNode : node.children()) { diff --git a/src/groups.h b/src/groups.h index f1a89edb20..0eae822b7c 100644 --- a/src/groups.h +++ b/src/groups.h @@ -10,6 +10,7 @@ struct Group uint64_t flags; uint32_t maxDepotItems; uint32_t maxVipEntries; + uint32_t maxVipGroups; uint16_t id; bool access; }; diff --git a/src/iologindata.cpp b/src/iologindata.cpp index 965ef3b8de..2f78024da4 100644 --- a/src/iologindata.cpp +++ b/src/iologindata.cpp @@ -633,6 +633,14 @@ bool IOLoginData::loadPlayer(Player* player, DBResult_ptr result) } while (result->next()); } + // load vip groups + if ((result = db.storeQuery( + fmt::format("SELECT `id` FROM `account_vipgroups` WHERE `account_id` = {:d}", player->getAccount())))) { + do { + player->addVIPGroupInternal(result->getNumber("id")); + } while (result->next()); + } + player->updateBaseSpeed(); player->updateInventoryWeight(); player->updateItemsLight(true); @@ -1100,20 +1108,54 @@ bool IOLoginData::hasBiddedOnHouse(uint32_t guid) return db.storeQuery(fmt::format("SELECT `id` FROM `houses` WHERE `highest_bidder` = {:d} LIMIT 1", guid)).get(); } -std::forward_list IOLoginData::getVIPEntries(uint32_t accountId) +std::vector IOLoginData::getVIPGroups(uint32_t accountId) { - std::forward_list entries; + std::vector groups; - DBResult_ptr result = Database::getInstance().storeQuery(fmt::format( - "SELECT `player_id`, (SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `name`, `description`, `icon`, `notify` FROM `account_viplist` WHERE `account_id` = {:d}", + DBResult_ptr result = Database::getInstance().storeQuery( + fmt::format("SELECT `id`, `name`, `editable` FROM `account_vipgroups` WHERE `account_id` = {:d}", accountId)); + if (result) { + do { + groups.emplace_back(result->getNumber("id"), result->getString("name"), + result->getNumber("editable") != 0); + } while (result->next()); + } + return groups; +} + +std::vector IOLoginData::getVIPEntries(uint32_t accountId) +{ + Database& db = Database::getInstance(); + std::vector entries; + + DBResult_ptr result = db.storeQuery(fmt::format( + "SELECT `id`, `player_id`, (SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `name`, `description`, `icon`, `notify` FROM `account_viplist` WHERE `account_id` = {:d}", accountId)); if (result) { do { - entries.emplace_front(result->getNumber("player_id"), result->getString("name"), - result->getString("description"), result->getNumber("icon"), - result->getNumber("notify") != 0); + entries.emplace_back(result->getNumber("id"), result->getNumber("player_id"), + result->getString("name"), result->getString("description"), + result->getNumber("icon"), result->getNumber("notify") != 0); } while (result->next()); } + + std::vector> entryGroups; + if (result = db.storeQuery(fmt::format( + "SELECT `entry_id`, `group_id` FROM `account_vipgroup_entry` INNER JOIN `account_viplist` ON `id` = `entry_id` WHERE `account_id` = {:d}", + accountId))) { + do { + entryGroups.emplace_back(result->getNumber("entry_id"), result->getNumber("group_id")); + } while (result->next()); + } + + for (VIPEntry& entry : entries) { + for (auto [entryId, groupId] : entryGroups) { + if (entry.id == entryId) { + entry.groupIds.push_back(groupId); + } + } + } + return entries; } @@ -1127,12 +1169,36 @@ void IOLoginData::addVIPEntry(uint32_t accountId, uint32_t guid, const std::stri } void IOLoginData::editVIPEntry(uint32_t accountId, uint32_t guid, const std::string& description, uint32_t icon, - bool notify) + bool notify, const std::vector& groupIds) { Database& db = Database::getInstance(); db.executeQuery(fmt::format( "UPDATE `account_viplist` SET `description` = {:s}, `icon` = {:d}, `notify` = {:d} WHERE `account_id` = {:d} AND `player_id` = {:d}", db.escapeString(description), icon, notify, accountId, guid)); + + DBResult_ptr result = db.storeQuery(fmt::format( + "SELECT `id` FROM `account_viplist` WHERE `account_id` = {:d} AND `player_id` = {:d}", accountId, guid)); + if (!result) { + return; + } + + uint16_t entryId = result->getNumber("id"); + + db.executeQuery(fmt::format("DELETE FROM `account_vipgroup_entry` WHERE `entry_id` = {:d}", entryId)); + if (groupIds.empty()) { + return; + } + + DBInsert insertQuery("INSERT INTO `account_vipgroup_entry` (`group_id`, `entry_id`) VALUES "); + for (const uint16_t& groupId : groupIds) { + if (!insertQuery.addRow(fmt::format("{:d}, {:d}", groupId, entryId))) { + return; + } + } + + if (!insertQuery.execute()) { + return; + } } void IOLoginData::removeVIPEntry(uint32_t accountId, uint32_t guid) @@ -1141,6 +1207,38 @@ void IOLoginData::removeVIPEntry(uint32_t accountId, uint32_t guid) fmt::format("DELETE FROM `account_viplist` WHERE `account_id` = {:d} AND `player_id` = {:d}", accountId, guid)); } +bool IOLoginData::checkVIPGroupName(uint32_t accountId, const std::string& name) +{ + Database& db = Database::getInstance(); + return db + .storeQuery( + fmt::format("SELECT `id` FROM `account_vipgroups` WHERE `account_id` = {:d} AND `name` = {:s} LIMIT 1", + accountId, db.escapeString(name))) + .get(); +} + +uint32_t IOLoginData::addVIPGroup(uint32_t accountId, const std::string& name, bool isEditable) +{ + Database& db = Database::getInstance(); + db.executeQuery( + fmt::format("INSERT INTO `account_vipgroups` (`account_id`, `name`, `editable`) VALUES ({:d}, {:s}, {:d})", + accountId, db.escapeString(name), isEditable ? 1 : 0)); + + return db.getLastInsertId(); +} + +void IOLoginData::editVIPGroup(uint16_t vipGroupId, const std::string& name) +{ + Database& db = Database::getInstance(); + db.executeQuery(fmt::format("UPDATE `account_vipgroups` SET `name` = {:s} WHERE `id` = {:d}", db.escapeString(name), + vipGroupId)); +} + +void IOLoginData::removeVIPGroup(uint16_t vipGroupId) +{ + Database::getInstance().executeQuery(fmt::format("DELETE FROM `account_vipgroups` WHERE `id` = {:d}", vipGroupId)); +} + void IOLoginData::updatePremiumTime(uint32_t accountId, time_t endTime) { Database::getInstance().executeQuery( diff --git a/src/iologindata.h b/src/iologindata.h index 1997d2bb85..40f328bf55 100644 --- a/src/iologindata.h +++ b/src/iologindata.h @@ -11,6 +11,7 @@ class Item; class Player; class PropWriteStream; struct VIPEntry; +struct VIPGroup; using ItemBlockList = std::list>; @@ -43,12 +44,17 @@ class IOLoginData static void increaseBankBalance(uint32_t guid, uint64_t bankBalance); static bool hasBiddedOnHouse(uint32_t guid); - static std::forward_list getVIPEntries(uint32_t accountId); + static std::vector getVIPGroups(uint32_t accountId); + static std::vector getVIPEntries(uint32_t accountId); static void addVIPEntry(uint32_t accountId, uint32_t guid, const std::string& description, uint32_t icon, bool notify); static void editVIPEntry(uint32_t accountId, uint32_t guid, const std::string& description, uint32_t icon, - bool notify); + bool notify, const std::vector& groupIds); static void removeVIPEntry(uint32_t accountId, uint32_t guid); + static bool checkVIPGroupName(uint32_t accountId, const std::string& name); + static uint32_t addVIPGroup(uint32_t accountId, const std::string& name, bool isEditable); + static void editVIPGroup(uint16_t vipGroupId, const std::string& name); + static void removeVIPGroup(uint16_t vipGroupId); static void updatePremiumTime(uint32_t accountId, time_t endTime); diff --git a/src/luascript.cpp b/src/luascript.cpp index 7f059b934a..e2ab9db374 100644 --- a/src/luascript.cpp +++ b/src/luascript.cpp @@ -12037,6 +12037,18 @@ int LuaScriptInterface::luaGroupGetMaxVipEntries(lua_State* L) return 1; } +int LuaScriptInterface::luaGroupGetMaxVipGroups(lua_State* L) +{ + // group:getMaxVipGroups() + Group* group = tfs::lua::getUserdata(L, 1); + if (group) { + lua_pushnumber(L, group->maxVipGroups); + } else { + lua_pushnil(L); + } + return 1; +} + int LuaScriptInterface::luaGroupHasFlag(lua_State* L) { // group:hasFlag(flag) diff --git a/src/luascript.h b/src/luascript.h index 5493d9b2df..27a4c7ebcc 100644 --- a/src/luascript.h +++ b/src/luascript.h @@ -876,6 +876,7 @@ class LuaScriptInterface static int luaGroupGetAccess(lua_State* L); static int luaGroupGetMaxDepotItems(lua_State* L); static int luaGroupGetMaxVipEntries(lua_State* L); + static int luaGroupGetMaxVipGroups(lua_State* L); static int luaGroupHasFlag(lua_State* L); // Vocation diff --git a/src/player.cpp b/src/player.cpp index fd76dbd925..9d755bf129 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -2334,7 +2334,7 @@ bool Player::addVIP(uint32_t vipGuid, const std::string& vipName, VipStatus_t st IOLoginData::addVIPEntry(accountNumber, vipGuid, "", 0, false); if (client) { - client->sendVIP(vipGuid, vipName, "", 0, false, status); + client->sendVIP(vipGuid, vipName, "", 0, false, status, {}); } return true; } @@ -2348,14 +2348,89 @@ bool Player::addVIPInternal(uint32_t vipGuid) return VIPList.insert(vipGuid).second; } -bool Player::editVIP(uint32_t vipGuid, const std::string& description, uint32_t icon, bool notify) +bool Player::addVIPGroupInternal(uint32_t vipGroupId) +{ + if (VIPGroups.size() >= getMaxVIPGroups()) { + return false; + } + + return VIPGroups.insert(vipGroupId).second; +} + +bool Player::editVIP(uint32_t vipGuid, const std::string& description, uint32_t icon, bool notify, + const std::vector& groupIds) { auto it = VIPList.find(vipGuid); if (it == VIPList.end()) { return false; // player is not in VIP } - IOLoginData::editVIPEntry(accountNumber, vipGuid, description, icon, notify); + IOLoginData::editVIPEntry(accountNumber, vipGuid, description, icon, notify, groupIds); + return true; +} + +bool Player::addVIPGroup(const std::string& name) +{ + if (VIPGroups.size() >= getMaxVIPGroups()) { + sendTextMessage(MESSAGE_STATUS_SMALL, + "You have already reached the maximum of groups you can create yourself."); + return false; + } + + auto reservedNames = std::array{"Friends", "Enemies", "Trading Partners"}; + if (std::find(reservedNames.begin(), reservedNames.end(), name) != reservedNames.end()) { + sendTextMessage(MESSAGE_STATUS_SMALL, "You have selected an invalid group name. Please choose another one."); + return false; + } + + if (IOLoginData::checkVIPGroupName(accountNumber, name)) { + sendTextMessage(MESSAGE_STATUS_SMALL, "A group with this name already exists. Please choose another name."); + return false; + } + + uint32_t newId = IOLoginData::addVIPGroup(accountNumber, name, true); + auto result = VIPGroups.insert(newId); + if (!result.second) { + sendTextMessage(MESSAGE_STATUS_SMALL, "This group is already in your group list."); + return false; + } + + sendVIPGroups(); + + return true; +} + +bool Player::editVIPGroup(uint16_t vipGroupId, const std::string& name) +{ + auto reservedNames = std::array{"Friends", "Enemies", "Trading Partners"}; + if (std::find(reservedNames.begin(), reservedNames.end(), name) != reservedNames.end()) { + sendTextMessage(MESSAGE_STATUS_SMALL, "You have selected an invalid group name. Please choose another one."); + return false; + } + + if (IOLoginData::checkVIPGroupName(accountNumber, name)) { + sendTextMessage(MESSAGE_STATUS_SMALL, "A group with this name already exists. Please choose another name."); + return false; + } + + IOLoginData::editVIPGroup(vipGroupId, name); + + sendVIPGroups(); + + return true; +} + +bool Player::removeVIPGroup(uint16_t vipGroupId) +{ + if (VIPGroups.erase(vipGroupId) == 0) { + return false; + } + + IOLoginData::removeVIPGroup(vipGroupId); + + sendVIPEntries(); + sendVIPGroups(); + return true; } @@ -4639,6 +4714,15 @@ size_t Player::getMaxVIPEntries() const return getNumber(isPremium() ? ConfigManager::VIP_PREMIUM_LIMIT : ConfigManager::VIP_FREE_LIMIT); } +size_t Player::getMaxVIPGroups() const +{ + if (group->maxVipGroups != 0) { + return group->maxVipGroups; + } + + return getNumber(isPremium() ? ConfigManager::VIPGROUP_PREMIUM_LIMIT : ConfigManager::VIPGROUP_FREE_LIMIT); +} + size_t Player::getMaxDepotItems() const { if (group->maxDepotItems != 0) { diff --git a/src/player.h b/src/player.h index fd1b3e87c2..2083be1aa1 100644 --- a/src/player.h +++ b/src/player.h @@ -55,15 +55,27 @@ enum tradestate_t : uint8_t struct VIPEntry { - VIPEntry(uint32_t guid, std::string_view name, std::string_view description, uint32_t icon, bool notify) : - guid{guid}, name{name}, description{description}, icon{icon}, notify{notify} + VIPEntry(uint32_t id, uint32_t playerId, std::string_view name, std::string_view description, uint32_t icon, + bool notify) : + id{id}, playerId{playerId}, name{name}, description{description}, icon{icon}, notify{notify} {} - uint32_t guid; + uint32_t id; + uint32_t playerId; std::string name; std::string description; uint32_t icon; bool notify; + std::vector groupIds; +}; + +struct VIPGroup +{ + VIPGroup(uint16_t id, std::string_view name, bool isEditable) : id{id}, name{name}, isEditable{isEditable} {} + + uint16_t id; + std::string name; + bool isEditable = true; }; struct OpenContainer @@ -403,7 +415,12 @@ class Player final : public Creature, public Cylinder bool removeVIP(uint32_t vipGuid); bool addVIP(uint32_t vipGuid, const std::string& vipName, VipStatus_t status); bool addVIPInternal(uint32_t vipGuid); - bool editVIP(uint32_t vipGuid, const std::string& description, uint32_t icon, bool notify); + bool editVIP(uint32_t vipGuid, const std::string& description, uint32_t icon, bool notify, + const std::vector& groupIds); + bool addVIPGroup(const std::string& name); + bool addVIPGroupInternal(uint32_t vipGroupId); + bool editVIPGroup(uint16_t vipGroupId, const std::string& name); + bool removeVIPGroup(uint16_t vipGroupId); // follow functions bool setFollowCreature(Creature* creature) override; @@ -529,6 +546,7 @@ class Player final : public Creature, public Cylinder bool getOutfitAddons(const Outfit& outfit, uint8_t& addons) const; size_t getMaxVIPEntries() const; + size_t getMaxVIPGroups() const; size_t getMaxDepotItems() const; // tile @@ -1067,6 +1085,18 @@ class Player final : public Creature, public Cylinder client->sendCombatAnalyzer(type, amount, impactType, target); } } + void sendVIPEntries() + { + if (client) { + client->sendVIPEntries(); + } + } + void sendVIPGroups() + { + if (client) { + client->sendVIPGroups(); + } + } void sendResourceBalance(const ResourceTypes_t resourceType, uint64_t amount) { if (client) { @@ -1164,6 +1194,7 @@ class Player final : public Creature, public Cylinder std::unordered_set attackedSet; std::unordered_set VIPList; + std::unordered_set VIPGroups; std::map openContainers; std::map depotChests; diff --git a/src/protocolgame.cpp b/src/protocolgame.cpp index 33db2d15eb..a4cb9542a2 100644 --- a/src/protocolgame.cpp +++ b/src/protocolgame.cpp @@ -762,7 +762,9 @@ void ProtocolGame::parsePacket(NetworkMessage& msg) case 0xDE: parseEditVip(msg); break; - // case 0xDF: break; // premium shop (?) + case 0xDF: + parseVipGroupAction(msg); + break; // case 0xE0: break; // premium shop (?) // case 0xE4: break; // buy charm rune // case 0xE5: break; // request character info (cyclopedia) @@ -1417,9 +1419,45 @@ void ProtocolGame::parseEditVip(NetworkMessage& msg) auto description = msg.getString(); uint32_t icon = std::min(10, msg.get()); // 10 is max icon in 9.63 bool notify = msg.getByte() != 0; - g_dispatcher.addTask([=, playerID = player->getID(), description = std::string{description}]() { - g_game.playerRequestEditVip(playerID, guid, description, icon, notify); - }); + uint8_t groups = msg.getByte(); + std::vector groupIds; + groupIds.reserve(groups); + for (uint8_t i = 0; i < groups; i++) { + groupIds.push_back(msg.getByte()); + } + g_dispatcher.addTask( + [=, playerID = player->getID(), description = std::string{description}, groupIds = std::move(groupIds)]() { + g_game.playerRequestEditVip(playerID, guid, description, icon, notify, groupIds); + }); +} + +void ProtocolGame::parseVipGroupAction(NetworkMessage& msg) +{ + uint8_t action = msg.getByte(); + + switch (action) { + case VIPGROUPACTION_CREATE: { + auto name = msg.getString(); + g_dispatcher.addTask([=, playerID = player->getID(), name = std::string{name}]() { + g_game.playerRequestAddVipGroup(playerID, name); + }); + break; + } + case VIPGROUPACTION_EDIT: { + uint8_t id = msg.getByte(); + auto name = msg.getString(); + g_dispatcher.addTask([=, playerID = player->getID(), name = std::string{name}]() { + g_game.playerRequestEditVipGroup(playerID, id, name); + }); + break; + } + case VIPGROUPACTION_REMOVE: { + uint8_t id = msg.getByte(); + g_dispatcher.addTask( + [=, playerID = player->getID()]() { g_game.playerRequestRemoveVipGroup(playerID, id); }); + break; + } + } } void ProtocolGame::parseRotateItem(NetworkMessage& msg) @@ -2836,6 +2874,9 @@ void ProtocolGame::sendAddCreature(const Creature* creature, const Position& pos // player light level sendCreatureLight(creature); + // player vip groups + sendVIPGroups(); + // player vip list sendVIPEntries(); @@ -3314,7 +3355,7 @@ void ProtocolGame::sendUpdatedVIPStatus(uint32_t guid, VipStatus_t newStatus) } void ProtocolGame::sendVIP(uint32_t guid, const std::string& name, const std::string& description, uint32_t icon, - bool notify, VipStatus_t status) + bool notify, VipStatus_t status, const std::vector& groupIds) { NetworkMessage msg; msg.addByte(0xD2); @@ -3324,25 +3365,51 @@ void ProtocolGame::sendVIP(uint32_t guid, const std::string& name, const std::st msg.add(std::min(10, icon)); msg.addByte(notify ? 0x01 : 0x00); msg.addByte(status); - msg.addByte(0x00); // vipGroups (placeholder) + msg.addByte(groupIds.size()); + for (uint8_t groupId : groupIds) { + msg.addByte(groupId); + } writeToOutputBuffer(msg); } void ProtocolGame::sendVIPEntries() { - const std::forward_list& vipEntries = IOLoginData::getVIPEntries(player->getAccount()); + const std::vector& vipEntries = IOLoginData::getVIPEntries(player->getAccount()); for (const VIPEntry& entry : vipEntries) { VipStatus_t vipStatus = VIPSTATUS_ONLINE; - Player* vipPlayer = g_game.getPlayerByGUID(entry.guid); + Player* vipPlayer = g_game.getPlayerByGUID(entry.playerId); if (!vipPlayer || !player->canSeeCreature(vipPlayer)) { vipStatus = VIPSTATUS_OFFLINE; } - sendVIP(entry.guid, entry.name, entry.description, entry.icon, entry.notify, vipStatus); + sendVIP(entry.playerId, entry.name, entry.description, entry.icon, entry.notify, vipStatus, entry.groupIds); + } +} + +void ProtocolGame::sendVIPGroups() +{ + const std::vector& vipGroups = IOLoginData::getVIPGroups(player->getAccount()); + if (vipGroups.empty()) { + return; } + + size_t groupsCount = vipGroups.size(); + + NetworkMessage msg; + msg.addByte(0xD4); + msg.addByte(groupsCount); + + for (const VIPGroup& group : vipGroups) { + msg.addByte(group.id); + msg.addString(group.name); + msg.addByte(group.isEditable ? 0x01 : 0x00); + } + + msg.addByte(std::max(0, static_cast(player->getMaxVIPGroups() - groupsCount))); + writeToOutputBuffer(msg); } void ProtocolGame::sendItemClasses() diff --git a/src/protocolgame.h b/src/protocolgame.h index 7b38125d2f..c83e86ba8f 100644 --- a/src/protocolgame.h +++ b/src/protocolgame.h @@ -145,6 +145,7 @@ class ProtocolGame final : public Protocol void parseAddVip(NetworkMessage& msg); void parseRemoveVip(NetworkMessage& msg); void parseEditVip(NetworkMessage& msg); + void parseVipGroupAction(NetworkMessage& msg); void parseRotateItem(NetworkMessage& msg); @@ -223,8 +224,9 @@ class ProtocolGame final : public Protocol void sendUpdatedVIPStatus(uint32_t guid, VipStatus_t newStatus); void sendVIP(uint32_t guid, const std::string& name, const std::string& description, uint32_t icon, bool notify, - VipStatus_t status); + VipStatus_t status, const std::vector& groupIds); void sendVIPEntries(); + void sendVIPGroups(); void sendItemClasses();