diff --git a/config.lua.dist b/config.lua.dist index 7d0360d9360..7c891c84b5d 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -344,9 +344,11 @@ Setting this to false may pose risks; if a house is abandoned and contains a lar ]] -- Periods: daily/weekly/monthly/yearly/never -- Base: sqm,rent,sqm+rent +toggleCyclopediaHouseAuction = true +daysToCloseBid = 7 housePriceRentMultiplier = 0.0 housePriceEachSQM = 1000 -houseRentPeriod = "never" +houseRentPeriod = "monthly" houseRentRate = 1.0 houseOwnedByAccount = false houseBuyLevel = 100 diff --git a/data-otservbr-global/migrations/46.lua b/data-otservbr-global/migrations/46.lua index 86a6d8ffec1..dc79c7cee35 100644 --- a/data-otservbr-global/migrations/46.lua +++ b/data-otservbr-global/migrations/46.lua @@ -1,3 +1,29 @@ function onUpdateDatabase() - return false -- true = There are others migrations file | false = this is the last migration file + logger.info("Updating database to version 44 (House Auction)") + + db.query([[ + ALTER TABLE `houses` + DROP `bid`, + DROP `bid_end`, + DROP `last_bid`, + DROP `highest_bidder` + ]]) + + db.query([[ + ALTER TABLE `houses` + ADD `bidder` int(11) NOT NULL DEFAULT '0', + ADD `bidder_name` varchar(255) NOT NULL DEFAULT '', + ADD `highest_bid` int(11) NOT NULL DEFAULT '0', + ADD `internal_bid` int(11) NOT NULL DEFAULT '0', + ADD `bid_end_date` int(11) NOT NULL DEFAULT '0', + ADD `state` smallint(5) UNSIGNED NOT NULL DEFAULT '0', + ADD `transfer_status` tinyint(1) DEFAULT '0' + ]]) + + db.query([[ + ALTER TABLE `accounts` + ADD `house_bid_id` int(11) NOT NULL DEFAULT '0' + ]]) + + return true end diff --git a/data-otservbr-global/migrations/47.lua b/data-otservbr-global/migrations/47.lua new file mode 100644 index 00000000000..86a6d8ffec1 --- /dev/null +++ b/data-otservbr-global/migrations/47.lua @@ -0,0 +1,3 @@ +function onUpdateDatabase() + return false -- true = There are others migrations file | false = this is the last migration file +end diff --git a/data-otservbr-global/world/otservbr-house.xml b/data-otservbr-global/world/otservbr-house.xml index 7eff23b4606..bedef70ff1f 100644 --- a/data-otservbr-global/world/otservbr-house.xml +++ b/data-otservbr-global/world/otservbr-house.xml @@ -1,987 +1,987 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/data/scripts/globalevents/server_initialization.lua b/data/scripts/globalevents/server_initialization.lua index df29660d373..a58cf01d3a2 100644 --- a/data/scripts/globalevents/server_initialization.lua +++ b/data/scripts/globalevents/server_initialization.lua @@ -27,29 +27,6 @@ local function moveExpiredBansToHistory() end end --- Function to check and process house auctions -local function processHouseAuctions() - local resultId = db.storeQuery("SELECT `id`, `highest_bidder`, `last_bid`, " .. "(SELECT `balance` FROM `players` WHERE `players`.`id` = `highest_bidder`) AS `balance` " .. "FROM `houses` WHERE `owner` = 0 AND `bid_end` != 0 AND `bid_end` < " .. os.time()) - if resultId then - repeat - local house = House(Result.getNumber(resultId, "id")) - if house then - local highestBidder = Result.getNumber(resultId, "highest_bidder") - local balance = Result.getNumber(resultId, "balance") - local lastBid = Result.getNumber(resultId, "last_bid") - if balance >= lastBid then - db.query("UPDATE `players` SET `balance` = " .. (balance - lastBid) .. " WHERE `id` = " .. highestBidder) - house:setHouseOwner(highestBidder) - end - - db.asyncQuery("UPDATE `houses` SET `last_bid` = 0, `bid_end` = 0, `highest_bidder` = 0, `bid` = 0 " .. "WHERE `id` = " .. house:getId()) - end - until not Result.next(resultId) - - Result.free(resultId) - end -end - -- Function to store towns in the database local function storeTownsInDatabase() db.query("TRUNCATE TABLE `towns`") @@ -150,7 +127,6 @@ function serverInitialization.onStartup() cleanupDatabase() moveExpiredBansToHistory() - processHouseAuctions() storeTownsInDatabase() checkAndLogDuplicateValues({ "Global", "GlobalStorage", "Storage" }) updateEventRates() diff --git a/data/scripts/talkactions/player/buy_house.lua b/data/scripts/talkactions/player/buy_house.lua index c3784d81a6b..84d3a34aad0 100644 --- a/data/scripts/talkactions/player/buy_house.lua +++ b/data/scripts/talkactions/player/buy_house.lua @@ -60,6 +60,8 @@ function buyHouse.onSay(player, words, param) return true end -buyHouse:separator(" ") -buyHouse:groupType("normal") -buyHouse:register() +if not configManager.getBoolean(configKeys.CYCLOPEDIA_HOUSE_AUCTION) then + buyHouse:separator(" ") + buyHouse:groupType("normal") + buyHouse:register() +end diff --git a/data/scripts/talkactions/player/leave_house.lua b/data/scripts/talkactions/player/leave_house.lua index 20ad186f2d2..d954eb1dcf0 100644 --- a/data/scripts/talkactions/player/leave_house.lua +++ b/data/scripts/talkactions/player/leave_house.lua @@ -42,6 +42,8 @@ function leaveHouse.onSay(player, words, param) return true end -leaveHouse:separator(" ") -leaveHouse:groupType("normal") -leaveHouse:register() +if not configManager.getBoolean(configKeys.CYCLOPEDIA_HOUSE_AUCTION) then + leaveHouse:separator(" ") + leaveHouse:groupType("normal") + leaveHouse:register() +end diff --git a/data/scripts/talkactions/player/sell_house.lua b/data/scripts/talkactions/player/sell_house.lua index c96cb5f71c3..dadadd066d1 100644 --- a/data/scripts/talkactions/player/sell_house.lua +++ b/data/scripts/talkactions/player/sell_house.lua @@ -20,6 +20,8 @@ function sellHouse.onSay(player, words, param) return true end -sellHouse:separator(" ") -sellHouse:groupType("normal") -sellHouse:register() +if not configManager.getBoolean(configKeys.CYCLOPEDIA_HOUSE_AUCTION) then + sellHouse:separator(" ") + sellHouse:groupType("normal") + sellHouse:register() +end diff --git a/schema.sql b/schema.sql index af245067057..3b63cc67cb7 100644 --- a/schema.sql +++ b/schema.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS `server_config` ( CONSTRAINT `server_config_pk` PRIMARY KEY (`config`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '46'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); +INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '47'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); -- Table structure `accounts` CREATE TABLE IF NOT EXISTS `accounts` ( @@ -24,6 +24,7 @@ CREATE TABLE IF NOT EXISTS `accounts` ( `tournament_coins` int(12) UNSIGNED NOT NULL DEFAULT '0', `creation` int(11) UNSIGNED NOT NULL DEFAULT '0', `recruiter` INT(6) DEFAULT 0, + `house_bid_id` int(11) NOT NULL DEFAULT '0', CONSTRAINT `accounts_pk` PRIMARY KEY (`id`), CONSTRAINT `accounts_unique` UNIQUE (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -451,13 +452,16 @@ CREATE TABLE IF NOT EXISTS `houses` ( `name` varchar(255) NOT NULL, `rent` int(11) NOT NULL DEFAULT '0', `town_id` int(11) NOT NULL DEFAULT '0', - `bid` int(11) NOT NULL DEFAULT '0', - `bid_end` int(11) NOT NULL DEFAULT '0', - `last_bid` int(11) NOT NULL DEFAULT '0', - `highest_bidder` int(11) NOT NULL DEFAULT '0', `size` int(11) NOT NULL DEFAULT '0', `guildid` int(11), `beds` int(11) NOT NULL DEFAULT '0', + `bidder` int(11) NOT NULL DEFAULT '0', + `bidder_name` varchar(255) NOT NULL DEFAULT '', + `highest_bid` int(11) NOT NULL DEFAULT '0', + `internal_bid` int(11) NOT NULL DEFAULT '0', + `bid_end_date` int(11) NOT NULL DEFAULT '0', + `state` smallint(5) UNSIGNED NOT NULL DEFAULT '0', + `transfer_status` tinyint(1) DEFAULT '0', INDEX `owner` (`owner`), INDEX `town_id` (`town_id`), CONSTRAINT `houses_pk` PRIMARY KEY (`id`) diff --git a/src/account/account.cpp b/src/account/account.cpp index 93596f77b15..db79f942527 100644 --- a/src/account/account.cpp +++ b/src/account/account.cpp @@ -300,3 +300,10 @@ uint32_t Account::getAccountAgeInDays() const { [[nodiscard]] time_t Account::getPremiumLastDay() const { return m_account->premiumLastDay; } + +uint32_t Account::getHouseBidId() const { + return m_account->houseBidId; +} +void Account::setHouseBidId(uint32_t houseId) { + m_account->houseBidId = houseId; +} diff --git a/src/account/account.hpp b/src/account/account.hpp index 2c6098a8dbd..0a2bcc1a2b2 100644 --- a/src/account/account.hpp +++ b/src/account/account.hpp @@ -119,6 +119,9 @@ class Account { std::tuple, AccountErrors_t> getAccountPlayers() const; + void setHouseBidId(uint32_t houseId); + uint32_t getHouseBidId() const; + // Old protocol compat void setProtocolCompat(bool toggle); diff --git a/src/account/account_info.hpp b/src/account/account_info.hpp index b9dad60dbbc..54741419ddb 100644 --- a/src/account/account_info.hpp +++ b/src/account/account_info.hpp @@ -28,4 +28,5 @@ struct AccountInfo { time_t sessionExpires = 0; uint32_t premiumDaysPurchased = 0; uint32_t creationTime = 0; + uint32_t houseBidId = 0; }; diff --git a/src/account/account_repository_db.cpp b/src/account/account_repository_db.cpp index c3f02bfe972..b2e8fd80754 100644 --- a/src/account/account_repository_db.cpp +++ b/src/account/account_repository_db.cpp @@ -47,12 +47,13 @@ bool AccountRepositoryDB::loadBySession(const std::string &sessionKey, std::uniq bool AccountRepositoryDB::save(const std::unique_ptr &accInfo) { bool successful = g_database().executeQuery( fmt::format( - "UPDATE `accounts` SET `type` = {}, `premdays` = {}, `lastday` = {}, `creation` = {}, `premdays_purchased` = {} WHERE `id` = {}", + "UPDATE `accounts` SET `type` = {}, `premdays` = {}, `lastday` = {}, `creation` = {}, `premdays_purchased` = {}, `house_bid_id` = {} WHERE `id` = {}", accInfo->accountType, accInfo->premiumRemainingDays, accInfo->premiumLastDay, accInfo->creationTime, accInfo->premiumDaysPurchased, + accInfo->houseBidId, accInfo->id ) ); diff --git a/src/config/config_enums.hpp b/src/config/config_enums.hpp index 559045fdb9b..65e585159e8 100644 --- a/src/config/config_enums.hpp +++ b/src/config/config_enums.hpp @@ -46,6 +46,7 @@ enum ConfigKey_t : uint16_t { CONVERT_UNSAFE_SCRIPTS, CORE_DIRECTORY, CRITICALCHANCE, + CYCLOPEDIA_HOUSE_AUCTION, DATA_DIRECTORY, DAY_KILLS_TO_RED, DEATH_LOSE_PERCENT, @@ -110,6 +111,7 @@ enum ConfigKey_t : uint16_t { HAZARD_PODS_TIME_TO_DAMAGE, HAZARD_PODS_TIME_TO_SPAWN, HAZARD_SPAWN_PLUNDER_MULTIPLIER, + DAYS_TO_CLOSE_BID, HOUSE_BUY_LEVEL, HOUSE_LOSE_AFTER_INACTIVITY, HOUSE_OWNED_BY_ACCOUNT, diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index 1c00df76c2f..1034a28be3e 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -157,6 +157,7 @@ bool ConfigManager::load() { loadBoolConfig(L, VIP_SYSTEM_ENABLED, "vipSystemEnabled", false); loadBoolConfig(L, WARN_UNSAFE_SCRIPTS, "warnUnsafeScripts", true); loadBoolConfig(L, XP_DISPLAY_MODE, "experienceDisplayRates", true); + loadBoolConfig(L, CYCLOPEDIA_HOUSE_AUCTION, "toggleCyclopediaHouseAuction", true); loadFloatConfig(L, BESTIARY_RATE_CHARM_SHOP_PRICE, "bestiaryRateCharmShopPrice", 1.0); loadFloatConfig(L, COMBAT_CHAIN_SKILL_FORMULA_AXE, "combatChainSkillFormulaAxe", 0.9); @@ -255,6 +256,7 @@ bool ConfigManager::load() { loadIntConfig(L, HAZARD_PODS_TIME_TO_DAMAGE, "hazardPodsTimeToDamage", 2000); loadIntConfig(L, HAZARD_PODS_TIME_TO_SPAWN, "hazardPodsTimeToSpawn", 4000); loadIntConfig(L, HAZARD_SPAWN_PLUNDER_MULTIPLIER, "hazardSpawnPlunderMultiplier", 25); + loadIntConfig(L, DAYS_TO_CLOSE_BID, "daysToCloseBid", 7); loadIntConfig(L, HOUSE_BUY_LEVEL, "houseBuyLevel", 0); loadIntConfig(L, HOUSE_LOSE_AFTER_INACTIVITY, "houseLoseAfterInactivity", 0); loadIntConfig(L, HOUSE_PRICE_PER_SQM, "housePriceEachSQM", 1000); diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index dfdf467aa97..7cebfbf96e9 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -36,6 +36,7 @@ #include "enums/object_category.hpp" #include "enums/player_blessings.hpp" #include "enums/player_icons.hpp" +#include "enums/player_cyclopedia.hpp" #include "game/game.hpp" #include "game/modal_window/modal_window.hpp" #include "game/scheduling/dispatcher.hpp" @@ -2264,6 +2265,22 @@ void Player::sendOutfitWindow() const { } } +void Player::sendCyclopediaHouseList(const HouseMap &houses) const { + if (client) { + client->sendCyclopediaHouseList(houses); + } +} +void Player::sendResourceBalance(Resource_t resourceType, uint64_t value) const { + if (client) { + client->sendResourceBalance(resourceType, value); + } +} +void Player::sendHouseAuctionMessage(uint32_t houseId, HouseAuctionType type, uint8_t index, bool bidSuccess /* = false*/) const { + if (client) { + client->sendHouseAuctionMessage(houseId, type, index, bidSuccess); + } +} + // Imbuements void Player::onApplyImbuement(const Imbuement* imbuement, const std::shared_ptr &item, uint8_t slot, bool protectionCharm) { @@ -10427,3 +10444,108 @@ uint16_t Player::getPlayerVocationEnum() const { return Vocation_t::VOCATION_NONE; } + +BidErrorMessage Player::canBidHouse(uint32_t houseId) { + using enum BidErrorMessage; + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house) { + return Internal; + } + + if (getPlayerVocationEnum() == Vocation_t::VOCATION_NONE) { + return Rookgaard; + } + + if (!isPremium()) { + return Premium; + } + + if (getAccount()->getHouseBidId() != 0) { + return OnlyOneBid; + } + + if (getBankBalance() < (house->getRent() + house->getHighestBid())) { + return NotEnoughMoney; + } + + if (house->isGuildhall()) { + if (getGuildRank() && getGuildRank()->level != 3) { + return Guildhall; + } + + if (getGuild() && getGuild()->getBankBalance() < (house->getRent() + house->getHighestBid())) { + return NotEnoughGuildMoney; + } + } + + return NoError; +} + +TransferErrorMessage Player::canTransferHouse(uint32_t houseId, uint32_t newOwnerGUID) { + using enum TransferErrorMessage; + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house) { + return Internal; + } + + if (getGUID() != house->getOwner()) { + return NotHouseOwner; + } + + if (getGUID() == newOwnerGUID) { + return AlreadyTheOwner; + } + + const auto newOwner = g_game().getPlayerByGUID(newOwnerGUID, true); + if (!newOwner) { + return CharacterNotExist; + } + + if (newOwner->getPlayerVocationEnum() == Vocation_t::VOCATION_NONE) { + return Rookgaard; + } + + if (!newOwner->isPremium()) { + return Premium; + } + + if (newOwner->getAccount()->getHouseBidId() != 0) { + return OnlyOneBid; + } + + return Success; +} + +AcceptTransferErrorMessage Player::canAcceptTransferHouse(uint32_t houseId) { + using enum AcceptTransferErrorMessage; + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house) { + return Internal; + } + + if (getGUID() != house->getBidder()) { + return NotNewOwner; + } + + if (!isPremium()) { + return Premium; + } + + if (getAccount()->getHouseBidId() != 0) { + return AlreadyBid; + } + + if (getPlayerVocationEnum() == Vocation_t::VOCATION_NONE) { + return Rookgaard; + } + + if (getBankBalance() < (house->getRent() + house->getInternalBid())) { + return Frozen; + } + + if (house->getTransferStatus()) { + return AlreadyAccepted; + } + + return Success; +} diff --git a/src/creatures/players/player.hpp b/src/creatures/players/player.hpp index 03f248cfa2f..898e0b0e2b1 100644 --- a/src/creatures/players/player.hpp +++ b/src/creatures/players/player.hpp @@ -64,17 +64,23 @@ struct HighscoreCharacter; enum class PlayerIcon : uint8_t; enum class IconBakragore : uint8_t; +enum class HouseAuctionType : uint8_t; +enum class BidErrorMessage : uint8_t; +enum class TransferErrorMessage : uint8_t; +enum class AcceptTransferErrorMessage : uint8_t; enum ObjectCategory_t : uint8_t; enum PreySlot_t : uint8_t; enum SpeakClasses : uint8_t; enum ChannelEvent_t : uint8_t; enum SquareColor_t : uint8_t; +enum Resource_t : uint8_t; using GuildWarVector = std::vector; using StashContainerList = std::vector, uint32_t>>; using ItemVector = std::vector>; using UsersMap = std::map>; using InvitedMap = std::map>; +using HouseMap = std::map>; struct ForgeHistory { ForgeAction_t actionType = ForgeAction_t::FUSION; @@ -880,6 +886,13 @@ class Player final : public Creature, public Cylinder, public Bankable { void sendOpenPrivateChannel(const std::string &receiver) const; void sendExperienceTracker(int64_t rawExp, int64_t finalExp) const; void sendOutfitWindow() const; + // House Auction + BidErrorMessage canBidHouse(uint32_t houseId); + TransferErrorMessage canTransferHouse(uint32_t houseId, uint32_t newOwnerGUID); + AcceptTransferErrorMessage canAcceptTransferHouse(uint32_t houseId); + void sendCyclopediaHouseList(const HouseMap &houses) const; + void sendResourceBalance(Resource_t resourceType, uint64_t value) const; + void sendHouseAuctionMessage(uint32_t houseId, HouseAuctionType type, uint8_t index, bool bidSuccess = false) const; // Imbuements void onApplyImbuement(const Imbuement* imbuement, const std::shared_ptr &item, uint8_t slot, bool protectionCharm); void onClearImbuement(const std::shared_ptr &item, uint8_t slot); diff --git a/src/enums/player_cyclopedia.hpp b/src/enums/player_cyclopedia.hpp index af7ea1701ff..4d2227f8d48 100644 --- a/src/enums/player_cyclopedia.hpp +++ b/src/enums/player_cyclopedia.hpp @@ -60,3 +60,61 @@ enum class CyclopediaMapData_t : uint8_t { Donations = 9, SetCurrentArea = 10, }; + +enum class CyclopediaHouseState : uint8_t { + Available = 0, + Rented = 2, + Transfer = 3, + MoveOut = 4, +}; + +enum class HouseAuctionType : uint8_t { + Bid = 1, + MoveOut = 2, + Transfer = 3, + CancelMoveOut = 4, + CancelTransfer = 5, + AcceptTransfer = 6, + RejectTransfer = 7, +}; + +enum class BidSuccessMessage : uint8_t { + BidSuccess = 0, + LowerBid = 1, +}; + +enum class BidErrorMessage : uint8_t { + NoError = 0, + Rookgaard = 3, + Premium = 5, + Guildhall = 6, + OnlyOneBid = 7, + NotEnoughMoney = 17, + NotEnoughGuildMoney = 21, + Internal = 24, +}; + +// Bytes to: +// Move Out, Transfer +// Cancel Move Out/Transfer +enum class TransferErrorMessage : uint8_t { + Success = 0, + NotHouseOwner = 2, + CharacterNotExist = 4, + Premium = 7, + Rookgaard = 16, + AlreadyTheOwner = 19, + OnlyOneBid = 25, + Internal = 32, +}; + +enum class AcceptTransferErrorMessage : uint8_t { + Success = 0, + NotNewOwner = 2, + AlreadyBid = 3, + AlreadyAccepted = 7, + Rookgaard = 8, + Premium = 9, + Frozen = 15, + Internal = 19, +}; diff --git a/src/game/game.cpp b/src/game/game.cpp index 49f8bf21d98..87410e42f5d 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -10852,3 +10852,354 @@ void Game::updatePlayersOnline() const { g_logger().error("[Game::updatePlayersOnline] Failed to update players online."); } } + +void Game::playerCyclopediaHousesByTown(uint32_t playerId, const std::string &townName) { + std::shared_ptr player = getPlayerByID(playerId); + if (!player) { + return; + } + + HouseMap houses; + if (!townName.empty()) { + const auto &housesList = g_game().map.houses.getHouses(); + for (const auto &it : housesList) { + const auto &house = it.second; + const auto &town = g_game().map.towns.getTown(house->getTownId()); + if (!town) { + return; + } + + const std::string &houseTown = town->getName(); + if (houseTown == townName) { + houses.emplace(house->getClientId(), house); + } + } + } else { + auto playerHouses = g_game().map.houses.getAllHousesByPlayerId(player->getGUID()); + if (playerHouses.size()) { + for (const auto &playerHouse : playerHouses) { + if (!playerHouse) { + continue; + } + houses.emplace(playerHouse->getClientId(), playerHouse); + } + } + + const auto house = g_game().map.houses.getHouseByBidderName(player->getName()); + if (house) { + houses.emplace(house->getClientId(), house); + } + } + player->sendCyclopediaHouseList(houses); +} + +void Game::playerCyclopediaHouseBid(uint32_t playerId, uint32_t houseId, uint64_t bidValue) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + std::shared_ptr player = getPlayerByID(playerId); + if (!player) { + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house) { + return; + } + + auto ret = player->canBidHouse(houseId); + if (ret != BidErrorMessage::NoError) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Bid, enumToValue(ret)); + } + ret = BidErrorMessage::NotEnoughMoney; + auto retSuccess = BidSuccessMessage::BidSuccess; + + if (house->getBidderName().empty()) { + if (!processBankAuction(player, house, bidValue)) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Bid, enumToValue(ret)); + return; + } + house->setHighestBid(0); + house->setInternalBid(bidValue); + house->setBidHolderLimit(bidValue); + house->setBidderName(player->getName()); + house->setBidder(player->getGUID()); + house->calculateBidEndDate(g_configManager().getNumber(DAYS_TO_CLOSE_BID)); + } else if (house->getBidderName() == player->getName()) { + if (!processBankAuction(player, house, bidValue, true)) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Bid, enumToValue(ret)); + return; + } + house->setInternalBid(bidValue); + house->setBidHolderLimit(bidValue); + } else if (bidValue <= house->getInternalBid()) { + house->setHighestBid(bidValue); + retSuccess = BidSuccessMessage::LowerBid; + } else { + if (!processBankAuction(player, house, bidValue)) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Bid, enumToValue(ret)); + return; + } + house->setHighestBid(house->getInternalBid() + 1); + house->setInternalBid(bidValue); + house->setBidHolderLimit(bidValue); + house->setBidderName(player->getName()); + house->setBidder(player->getGUID()); + } + + const auto &town = g_game().map.towns.getTown(house->getTownId()); + if (!town) { + return; + } + + const std::string houseTown = town->getName(); + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Bid, enumToValue(retSuccess), true); + playerCyclopediaHousesByTown(playerId, houseTown); +} + +void Game::playerCyclopediaHouseMoveOut(uint32_t playerId, uint32_t houseId, uint32_t timestamp) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + std::shared_ptr player = getPlayerByID(playerId); + if (!player) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::MoveOut, enumToValue(TransferErrorMessage::Internal)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getState() != CyclopediaHouseState::Rented) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::MoveOut, enumToValue(TransferErrorMessage::Internal)); + return; + } + + if (house->getOwner() != player->getGUID()) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::MoveOut, enumToValue(TransferErrorMessage::NotHouseOwner)); + return; + } + + house->setBidEndDate(timestamp); + house->setState(CyclopediaHouseState::MoveOut); + + player->sendHouseAuctionMessage(houseId, HouseAuctionType::MoveOut, enumToValue(TransferErrorMessage::Success)); + playerCyclopediaHousesByTown(playerId, ""); +} + +void Game::playerCyclopediaHouseCancelMoveOut(uint32_t playerId, uint32_t houseId) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + std::shared_ptr player = getPlayerByID(playerId); + if (!player) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelMoveOut, enumToValue(TransferErrorMessage::Internal)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getState() != CyclopediaHouseState::MoveOut) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelMoveOut, enumToValue(TransferErrorMessage::Internal)); + return; + } + + if (house->getOwner() != player->getGUID()) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelMoveOut, enumToValue(TransferErrorMessage::NotHouseOwner)); + return; + } + + house->setBidEndDate(0); + house->setState(CyclopediaHouseState::Rented); + + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelMoveOut, enumToValue(TransferErrorMessage::Success)); + playerCyclopediaHousesByTown(playerId, ""); +} + +void Game::playerCyclopediaHouseTransfer(uint32_t playerId, uint32_t houseId, uint32_t timestamp, const std::string &newOwnerName, uint64_t bidValue) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + const std::shared_ptr &owner = getPlayerByID(playerId); + if (!owner) { + owner->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(TransferErrorMessage::Internal)); + return; + } + + const std::shared_ptr &newOwner = getPlayerByName(newOwnerName, true); + if (!newOwner) { + owner->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(TransferErrorMessage::CharacterNotExist)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getState() != CyclopediaHouseState::Rented) { + owner->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(TransferErrorMessage::Internal)); + return; + } + + auto ret = owner->canTransferHouse(houseId, newOwner->getGUID()); + if (ret != TransferErrorMessage::Success) { + owner->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(ret)); + return; + } + + house->setBidderName(newOwnerName); + house->setBidder(newOwner->getGUID()); + house->setInternalBid(bidValue); + house->setBidEndDate(timestamp); + house->setState(CyclopediaHouseState::Transfer); + + owner->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(ret)); + playerCyclopediaHousesByTown(playerId, ""); +} + +void Game::playerCyclopediaHouseCancelTransfer(uint32_t playerId, uint32_t houseId) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + const std::shared_ptr &player = getPlayerByID(playerId); + if (!player) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelTransfer, enumToValue(TransferErrorMessage::Internal)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getState() != CyclopediaHouseState::Transfer) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelTransfer, enumToValue(TransferErrorMessage::Internal)); + return; + } + + if (house->getOwner() != player->getGUID()) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelTransfer, enumToValue(TransferErrorMessage::NotHouseOwner)); + return; + } + + if (house->getTransferStatus()) { + const auto &newOwner = getPlayerByGUID(house->getBidder()); + const auto amountPaid = house->getInternalBid() + house->getRent(); + if (newOwner) { + newOwner->setBankBalance(newOwner->getBankBalance() + amountPaid); + newOwner->sendResourceBalance(RESOURCE_BANK, newOwner->getBankBalance()); + } else { + IOLoginData::increaseBankBalance(house->getBidder(), amountPaid); + } + } + + house->setBidderName(""); + house->setBidder(0); + house->setInternalBid(0); + house->setBidEndDate(0); + house->setState(CyclopediaHouseState::Rented); + house->setTransferStatus(false); + + player->sendHouseAuctionMessage(houseId, HouseAuctionType::CancelTransfer, enumToValue(TransferErrorMessage::Success)); + playerCyclopediaHousesByTown(playerId, ""); +} + +void Game::playerCyclopediaHouseAcceptTransfer(uint32_t playerId, uint32_t houseId) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + const std::shared_ptr &player = getPlayerByID(playerId); + if (!player) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::AcceptTransfer, enumToValue(AcceptTransferErrorMessage::Internal)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getState() != CyclopediaHouseState::Transfer) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::AcceptTransfer, enumToValue(AcceptTransferErrorMessage::Internal)); + return; + } + + auto ret = player->canAcceptTransferHouse(houseId); + if (ret != AcceptTransferErrorMessage::Success) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::AcceptTransfer, enumToValue(ret)); + return; + } + + if (!processBankAuction(player, house, house->getInternalBid())) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::AcceptTransfer, enumToValue(AcceptTransferErrorMessage::Frozen)); + return; + } + + house->setTransferStatus(true); + + player->sendHouseAuctionMessage(houseId, HouseAuctionType::AcceptTransfer, enumToValue(ret)); + playerCyclopediaHousesByTown(playerId, ""); +} + +void Game::playerCyclopediaHouseRejectTransfer(uint32_t playerId, uint32_t houseId) { + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + return; + } + + const std::shared_ptr &player = getPlayerByID(playerId); + if (!player) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(TransferErrorMessage::Internal)); + return; + } + + const auto house = g_game().map.houses.getHouseByClientId(houseId); + if (!house || house->getBidder() != player->getGUID() || house->getState() != CyclopediaHouseState::Transfer) { + player->sendHouseAuctionMessage(houseId, HouseAuctionType::Transfer, enumToValue(TransferErrorMessage::NotHouseOwner)); + return; + } + + if (house->getTransferStatus()) { + const auto &newOwner = getPlayerByGUID(house->getBidder()); + const auto amountPaid = house->getInternalBid() + house->getRent(); + if (newOwner) { + newOwner->setBankBalance(newOwner->getBankBalance() + amountPaid); + newOwner->sendResourceBalance(RESOURCE_BANK, newOwner->getBankBalance()); + } else { + IOLoginData::increaseBankBalance(house->getBidder(), amountPaid); + } + } + + house->setBidderName(""); + house->setBidder(0); + house->setInternalBid(0); + house->setBidEndDate(0); + house->setState(CyclopediaHouseState::Rented); + house->setTransferStatus(false); + + player->sendHouseAuctionMessage(houseId, HouseAuctionType::RejectTransfer, enumToValue(TransferErrorMessage::Success)); + playerCyclopediaHousesByTown(playerId, ""); +} + +bool Game::processBankAuction(std::shared_ptr player, const std::shared_ptr &house, uint64_t bid, bool replace /* = false*/) { + if (!replace && player->getBankBalance() < (house->getRent() + bid)) { + return false; + } + + if (player->getBankBalance() < bid) { + return false; + } + + uint64_t balance = player->getBankBalance(); + if (replace) { + player->setBankBalance(balance - (bid - house->getInternalBid())); + } else { + player->setBankBalance(balance - (house->getRent() + bid)); + } + + player->sendResourceBalance(RESOURCE_BANK, player->getBankBalance()); + + if (house->getBidderName() != player->getName()) { + const auto otherPlayer = g_game().getPlayerByName(house->getBidderName()); + if (!otherPlayer) { + uint32_t bidderGuid = IOLoginData::getGuidByName(house->getBidderName()); + IOLoginData::increaseBankBalance(bidderGuid, (house->getBidHolderLimit() + house->getRent())); + } else { + otherPlayer->setBankBalance(otherPlayer->getBankBalance() + (house->getBidHolderLimit() + house->getRent())); + otherPlayer->sendResourceBalance(RESOURCE_BANK, otherPlayer->getBankBalance()); + } + } + + return true; +} diff --git a/src/game/game.hpp b/src/game/game.hpp index b1afd810100..fbc21e4511a 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -290,6 +290,17 @@ class Game { void playerHighscores(const std::shared_ptr &player, HighscoreType_t type, uint8_t category, uint32_t vocation, const std::string &worldName, uint16_t page, uint8_t entriesPerPage); static std::string getSkillNameById(uint8_t &skill); + // House Auction + void playerCyclopediaHousesByTown(uint32_t playerId, const std::string &townName); + void playerCyclopediaHouseBid(uint32_t playerId, uint32_t houseId, uint64_t bidValue); + void playerCyclopediaHouseMoveOut(uint32_t playerId, uint32_t houseId, uint32_t timestamp); + void playerCyclopediaHouseCancelMoveOut(uint32_t playerId, uint32_t houseId); + void playerCyclopediaHouseTransfer(uint32_t playerId, uint32_t houseId, uint32_t timestamp, const std::string &newOwnerName, uint64_t bidValue); + void playerCyclopediaHouseCancelTransfer(uint32_t playerId, uint32_t houseId); + void playerCyclopediaHouseAcceptTransfer(uint32_t playerId, uint32_t houseId); + void playerCyclopediaHouseRejectTransfer(uint32_t playerId, uint32_t houseId); + bool processBankAuction(std::shared_ptr player, const std::shared_ptr &house, uint64_t bid, bool replace = false); + void updatePlayerSaleItems(uint32_t playerId); bool internalStartTrade(const std::shared_ptr &player, const std::shared_ptr &partner, const std::shared_ptr &tradeItem); diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp index 0249d70a517..7e850564870 100644 --- a/src/io/iologindata.cpp +++ b/src/io/iologindata.cpp @@ -333,14 +333,6 @@ void IOLoginData::increaseBankBalance(uint32_t guid, uint64_t bankBalance) { Database::getInstance().executeQuery(query.str()); } -bool IOLoginData::hasBiddedOnHouse(uint32_t guid) { - Database &db = Database::getInstance(); - - std::ostringstream query; - query << "SELECT `id` FROM `houses` WHERE `highest_bidder` = " << guid << " LIMIT 1"; - return db.storeQuery(query.str()).get() != nullptr; -} - std::vector IOLoginData::getVIPEntries(uint32_t accountId) { std::string query = fmt::format("SELECT `player_id`, (SELECT `name` FROM `players` WHERE `id` = `player_id`) AS `name`, `description`, `icon`, `notify` FROM `account_viplist` WHERE `account_id` = {}", accountId); std::vector entries; diff --git a/src/io/iologindata.hpp b/src/io/iologindata.hpp index 29eac0c812f..5e189d9fafe 100644 --- a/src/io/iologindata.hpp +++ b/src/io/iologindata.hpp @@ -28,7 +28,6 @@ class IOLoginData { static std::string getNameByGuid(uint32_t guid); static bool formatPlayerName(std::string &name); static void increaseBankBalance(uint32_t guid, uint64_t bankBalance); - static bool hasBiddedOnHouse(uint32_t guid); 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); diff --git a/src/io/iomapserialize.cpp b/src/io/iomapserialize.cpp index b1de604dd14..1479197c2c5 100644 --- a/src/io/iomapserialize.cpp +++ b/src/io/iomapserialize.cpp @@ -273,7 +273,7 @@ void IOMapSerialize::saveTile(PropWriteStream &stream, const std::shared_ptrgetNumber("id"); const auto house = g_game().map.houses.getHouse(houseId); - if (house) { - auto owner = result->getNumber("owner"); - auto newOwner = result->getNumber("new_owner"); - // Transfer house owner - auto isTransferOnRestart = g_configManager().getBoolean(TOGGLE_HOUSE_TRANSFER_ON_SERVER_RESTART); - if (isTransferOnRestart && newOwner >= 0) { + if (!house) { + continue; + } + + auto owner = result->getNumber("owner"); + auto newOwner = result->getNumber("new_owner"); + uint32_t bidder = result->getNumber("bidder"); + std::string bidderName = result->getString("bidder_name"); + uint32_t highestBid = result->getNumber("highest_bid"); + uint32_t internalBid = result->getNumber("internal_bid"); + uint32_t bidEndDate = result->getNumber("bid_end_date"); + auto state = static_cast(result->getNumber("state")); + auto transferStatus = result->getNumber("transfer_status"); + const auto timeNow = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + + // Transfer house owner + auto isTransferOnRestart = g_configManager().getBoolean(TOGGLE_HOUSE_TRANSFER_ON_SERVER_RESTART); + if (isTransferOnRestart && newOwner >= 0) { + g_game().setTransferPlayerHouseItems(houseId, owner); + if (newOwner == 0) { + g_logger().debug("Removing house id '{}' owner", houseId); + house->setOwner(0); + } else { + g_logger().debug("Setting house id '{}' owner to player GUID '{}'", houseId, newOwner); + house->setOwner(newOwner); + } + } else if (state == CyclopediaHouseState::Available && timeNow > bidEndDate && bidder > 0) { + g_logger().debug("[BID] - Setting house id '{}' owner to player GUID '{}'", houseId, bidder); + if (highestBid < internalBid) { + uint32_t diff = internalBid - highestBid; + IOLoginData::increaseBankBalance(bidder, diff); + } + house->setOwner(bidder); + bidder = 0; + bidderName = ""; + highestBid = 0; + internalBid = 0; + bidEndDate = 0; + } else if (state == CyclopediaHouseState::Transfer && timeNow > bidEndDate && bidder > 0) { + g_logger().debug("[TRANSFER] - Removing house id '{}' from owner GUID '{}' and transfering to new owner GUID '{}'", houseId, owner, bidder); + if (transferStatus) { g_game().setTransferPlayerHouseItems(houseId, owner); - if (newOwner == 0) { - g_logger().debug("Removing house id '{}' owner", houseId); - house->setOwner(0); - } else { - g_logger().debug("Setting house id '{}' owner to player GUID '{}'", houseId, newOwner); - house->setOwner(newOwner); - } + house->setOwner(bidder); + IOLoginData::increaseBankBalance(owner, internalBid); } else { - house->setOwner(owner, false); + house->setOwner(owner); } - house->setPaidUntil(result->getNumber("paid")); - house->setPayRentWarnings(result->getNumber("warnings")); + bidder = 0; + bidderName = ""; + internalBid = 0; + bidEndDate = 0; + transferStatus = false; + } else if (state == CyclopediaHouseState::MoveOut && timeNow > bidEndDate) { + g_logger().debug("[MOVE OUT] - Removing house id '{}' owner", houseId); + g_game().setTransferPlayerHouseItems(houseId, owner); + house->setOwner(0); + bidEndDate = 0; + } else { + house->setOwner(owner, false); + house->setState(state); } + house->setBidder(bidder); + house->setBidderName(bidderName); + house->setHighestBid(highestBid); + house->setInternalBid(internalBid); + house->setBidHolderLimit(internalBid); + house->setBidEndDate(bidEndDate); + house->setTransferStatus(transferStatus); } while (result->next()); result = db.storeQuery("SELECT `house_id`, `listid`, `list` FROM `house_lists`"); @@ -331,11 +379,12 @@ bool IOMapSerialize::SaveHouseInfoGuard() { Database &db = Database::getInstance(); std::ostringstream query; - DBInsert houseUpdate("INSERT INTO `houses` (`id`, `owner`, `paid`, `warnings`, `name`, `town_id`, `rent`, `size`, `beds`) VALUES "); - houseUpdate.upsert({ "owner", "paid", "warnings", "name", "town_id", "rent", "size", "beds" }); + DBInsert houseUpdate("INSERT INTO `houses` (`id`, `owner`, `paid`, `warnings`, `name`, `town_id`, `rent`, `size`, `beds`, `bidder`, `bidder_name`, `highest_bid`, `internal_bid`, `bid_end_date`, `state`, `transfer_status`) VALUES "); + houseUpdate.upsert({ "owner", "paid", "warnings", "name", "town_id", "rent", "size", "beds", "bidder", "bidder_name", "highest_bid", "internal_bid", "bid_end_date", "state", "transfer_status" }); for (const auto &[key, house] : g_game().map.houses.getHouses()) { - std::string values = fmt::format("{},{},{},{},{},{},{},{},{}", house->getId(), house->getOwner(), house->getPaidUntil(), house->getPayRentWarnings(), db.escapeString(house->getName()), house->getTownId(), house->getRent(), house->getSize(), house->getBedCount()); + auto stateValue = magic_enum::enum_integer(house->getState()); + std::string values = fmt::format("{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}", house->getId(), house->getOwner(), house->getPaidUntil(), house->getPayRentWarnings(), db.escapeString(house->getName()), house->getTownId(), house->getRent(), house->getSize(), house->getBedCount(), house->getBidder(), db.escapeString(house->getBidderName()), house->getHighestBid(), house->getInternalBid(), house->getBidEndDate(), std::to_string(stateValue), (house->getTransferStatus() ? 1 : 0)); if (!houseUpdate.addRow(values)) { return false; diff --git a/src/lua/functions/map/house_functions.cpp b/src/lua/functions/map/house_functions.cpp index 708b9233a3a..ae4707d0a7d 100644 --- a/src/lua/functions/map/house_functions.cpp +++ b/src/lua/functions/map/house_functions.cpp @@ -199,7 +199,7 @@ int HouseFunctions::luaHouseStartTrade(lua_State* L) { return 1; } - if (IOLoginData::hasBiddedOnHouse(tradePartner->getGUID())) { + if (tradePartner->getAccount()->getHouseBidId() != 0) { lua_pushnumber(L, RETURNVALUE_TRADEPLAYERHIGHESTBIDDER); return 1; } diff --git a/src/map/house/house.cpp b/src/map/house/house.cpp index 334144cfbd1..e0c0e9d9d86 100644 --- a/src/map/house/house.cpp +++ b/src/map/house/house.cpp @@ -94,7 +94,7 @@ void House::setOwner(uint32_t guid, bool updateDatabase /* = true*/, const std:: Database &db = Database::getInstance(); std::ostringstream query; - query << "UPDATE `houses` SET `owner` = " << guid << ", `new_owner` = -1, `bid` = 0, `bid_end` = 0, `last_bid` = 0, `highest_bidder` = 0 WHERE `id` = " << id; + query << "UPDATE `houses` SET `owner` = " << guid << ", `new_owner` = -1, `paid` = 0, `bidder` = 0, `bidder_name` = '', `highest_bid` = 0, `internal_bid` = 0, `bid_end_date` = 0, `state` = " << (guid > 0 ? 2 : 0) << " WHERE `id` = " << id; db.executeQuery(query.str()); } @@ -106,7 +106,9 @@ void House::setOwner(uint32_t guid, bool updateDatabase /* = true*/, const std:: if (owner != 0) { tryTransferOwnership(player, false); - } else { + } + + if (guid != 0) { std::string strRentPeriod = asLowerCaseString(g_configManager().getString(HOUSE_RENT_PERIOD)); time_t currentTime = time(nullptr); if (strRentPeriod == "yearly") { @@ -122,6 +124,8 @@ void House::setOwner(uint32_t guid, bool updateDatabase /* = true*/, const std:: } paidUntil = currentTime; + } else { + paidUntil = 0; } rentWarnings = 0; @@ -140,6 +144,7 @@ void House::setOwner(uint32_t guid, bool updateDatabase /* = true*/, const std:: owner = guid; ownerName = name; ownerAccountId = result->getNumber("account_id"); + m_state = CyclopediaHouseState::Rented; } } @@ -154,15 +159,17 @@ void House::updateDoorDescription() const { ss << "It belongs to house '" << houseName << "'. Nobody owns this house."; } - ss << " It is " << getSize() << " square meters."; - const int32_t housePrice = getPrice(); - if (housePrice != -1) { - if (g_configManager().getBoolean(HOUSE_PURSHASED_SHOW_PRICE) || owner == 0) { - ss << " It costs " << formatNumber(getPrice()) << " gold coins."; - } - std::string strRentPeriod = asLowerCaseString(g_configManager().getString(HOUSE_RENT_PERIOD)); - if (strRentPeriod != "never") { - ss << " The rent cost is " << formatNumber(getRent()) << " gold coins and it is billed " << strRentPeriod << "."; + if (!g_configManager().getBoolean(CYCLOPEDIA_HOUSE_AUCTION)) { + ss << " It is " << getSize() << " square meters."; + const int32_t housePrice = getPrice(); + if (housePrice != -1) { + if (g_configManager().getBoolean(HOUSE_PURSHASED_SHOW_PRICE) || owner == 0) { + ss << " It costs " << formatNumber(getPrice()) << " gold coins."; + } + std::string strRentPeriod = asLowerCaseString(g_configManager().getString(HOUSE_RENT_PERIOD)); + if (strRentPeriod != "never") { + ss << " The rent cost is " << formatNumber(getRent()) << " gold coins and it is billed " << strRentPeriod << "."; + } } } @@ -478,6 +485,43 @@ void House::resetTransferItem() { } } +void House::calculateBidEndDate(uint8_t daysToEnd) { + auto currentTimeMs = std::chrono::system_clock::now().time_since_epoch(); + + auto now = std::chrono::system_clock::time_point( + std::chrono::duration_cast(currentTimeMs) + ); + + // Truncate to whole days since epoch + days daysSinceEpoch = std::chrono::duration_cast(now.time_since_epoch()); + + // Get today's date at 00:00:00 UTC + auto todayMidnight = std::chrono::system_clock::time_point(daysSinceEpoch); + + std::chrono::system_clock::time_point targetDay = todayMidnight + days(daysToEnd); + + const auto serverSaveTime = g_configManager().getString(GLOBAL_SERVER_SAVE_TIME); + + std::vector params = vectorAtoi(explodeString(serverSaveTime, ":")); + int32_t hour = params.front(); + int32_t min = 0; + int32_t sec = 0; + if (params.size() > 1) { + min = params[1]; + + if (params.size() > 2) { + sec = params[2]; + } + } + std::chrono::system_clock::time_point targetTime = targetDay + std::chrono::hours(hour) + std::chrono::minutes(min) + std::chrono::seconds(sec); + + std::time_t resultTime = std::chrono::system_clock::to_time_t(targetTime); + std::tm* localTime = std::localtime(&resultTime); + auto bidEndDate = static_cast(std::mktime(localTime)); + + this->m_bidEndDate = bidEndDate; +} + std::shared_ptr HouseTransferItem::createHouseTransferItem(const std::shared_ptr &house) { auto transferItem = std::make_shared(house); transferItem->setID(ITEM_DOCUMENT_RO); @@ -724,6 +768,35 @@ std::shared_ptr Houses::getHouseByPlayerId(uint32_t playerId) const { return nullptr; } +std::vector> Houses::getAllHousesByPlayerId(uint32_t playerId) { + std::vector> playerHouses; + for (const auto &[id, house] : houseMap) { + if (house->getOwner() == playerId) { + playerHouses.emplace_back(house); + } + } + return playerHouses; +} + +std::shared_ptr Houses::getHouseByBidderName(const std::string &bidderName) { + for (const auto &[id, house] : houseMap) { + if (house->getBidderName() == bidderName) { + return house; + } + } + return nullptr; +} + +uint16_t Houses::getHouseCountByAccount(uint32_t accountId) { + uint16_t count = 0; + for (const auto &[id, house] : houseMap) { + if (house->getOwnerAccountId() == accountId) { + ++count; + } + } + return count; +} + bool Houses::loadHousesXML(const std::string &filename) { pugi::xml_document doc; const pugi::xml_parse_result result = doc.load_file(filename.c_str()); @@ -763,6 +836,13 @@ bool Houses::loadHousesXML(const std::string &filename) { house->setRent(pugi::cast(houseNode.attribute("rent").value())); house->setSize(pugi::cast(houseNode.attribute("size").value())); house->setTownId(pugi::cast(houseNode.attribute("townid").value())); + house->setClientId(pugi::cast(houseNode.attribute("clientid").value())); + + auto guildhallAttr = houseNode.attribute("guildhall"); + if (!guildhallAttr.empty()) { + house->setGuildhall(static_cast(guildhallAttr.as_bool())); + } + auto maxBedsAttr = houseNode.attribute("beds"); int32_t maxBeds = -1; if (!maxBedsAttr.empty()) { @@ -771,6 +851,7 @@ bool Houses::loadHousesXML(const std::string &filename) { house->setMaxBeds(maxBeds); house->setOwner(0, false); + addHouseClientId(house->getClientId(), house); } return true; } diff --git a/src/map/house/house.hpp b/src/map/house/house.hpp index a3dc765988f..d994fdbe3b2 100644 --- a/src/map/house/house.hpp +++ b/src/map/house/house.hpp @@ -13,11 +13,14 @@ #include "declarations.hpp" #include "map/house/housetile.hpp" #include "game/movement/position.hpp" +#include "enums/player_cyclopedia.hpp" class House; class BedItem; class Player; +using days = std::chrono::duration>; + class AccessList { public: void parseList(const std::string &list); @@ -233,6 +236,84 @@ class House final : public SharedObject { bool hasNewOwnership() const; void setNewOwnership(); + void setClientId(uint32_t newClientId) { + this->m_clientId = newClientId; + } + uint32_t getClientId() const { + return m_clientId; + } + + void setBidder(int32_t bidder) { + this->m_bidder = bidder; + } + int32_t getBidder() const { + return m_bidder; + } + + void setBidderName(const std::string &bidderName) { + this->m_bidderName = bidderName; + } + std::string getBidderName() const { + return m_bidderName; + } + + void setHighestBid(uint64_t bidValue) { + this->m_highestBid = bidValue; + } + uint64_t getHighestBid() const { + return m_highestBid; + } + + void setInternalBid(uint64_t bidValue) { + this->m_internalBid = bidValue; + } + uint64_t getInternalBid() const { + return m_internalBid; + } + + void setBidHolderLimit(uint64_t bidValue) { + this->m_bidHolderLimit = bidValue; + } + uint64_t getBidHolderLimit() const { + return m_bidHolderLimit; + } + + void calculateBidEndDate(uint8_t daysToEnd); + void setBidEndDate(uint32_t bidEndDate) { + this->m_bidEndDate = bidEndDate; + }; + uint32_t getBidEndDate() const { + return m_bidEndDate; + } + + void setState(CyclopediaHouseState state) { + this->m_state = state; + } + CyclopediaHouseState getState() const { + return m_state; + } + + void setTransferStatus(bool transferStatus) { + this->m_transferStatus = transferStatus; + } + bool getTransferStatus() const { + return m_transferStatus; + } + + void setOwnerAccountId(uint32_t accountId) { + this->ownerAccountId = accountId; + } + uint32_t getOwnerAccountId() const { + return ownerAccountId; + } + + void setGuildhall(bool isGuildHall) { + this->guildHall = isGuildHall; + } + bool isGuildhall() const { + return guildHall; + } + private: bool transferToDepot() const; @@ -263,9 +344,21 @@ class House final : public SharedObject { uint32_t townId = 0; uint32_t maxBeds = 4; int32_t bedsCount = -1; + bool guildHall = false; Position posEntry = {}; + // House Auction + uint32_t m_clientId; + int32_t m_bidder = 0; + std::string m_bidderName = ""; + uint64_t m_highestBid = 0; + uint64_t m_internalBid = 0; + uint64_t m_bidHolderLimit = 0; + uint32_t m_bidEndDate = 0; + CyclopediaHouseState m_state = CyclopediaHouseState::Available; + bool m_transferStatus = false; + bool isLoaded = false; void handleContainer(ItemList &moveItemList, const std::shared_ptr &item) const; @@ -299,7 +392,26 @@ class Houses { return it->second; } + void addHouseClientId(uint32_t clientId, std::shared_ptr house) { + if (auto it = houseMapClientId.find(clientId); it != houseMapClientId.end()) { + return; + } + + houseMapClientId.emplace(clientId, house); + } + + std::shared_ptr getHouseByClientId(uint32_t clientId) { + auto it = houseMapClientId.find(clientId); + if (it == houseMapClientId.end()) { + return nullptr; + } + return it->second; + } + std::shared_ptr getHouseByPlayerId(uint32_t playerId) const; + std::vector> getAllHousesByPlayerId(uint32_t playerId); + std::shared_ptr getHouseByBidderName(const std::string &bidderName); + uint16_t getHouseCountByAccount(uint32_t accountId); bool loadHousesXML(const std::string &filename); @@ -311,4 +423,5 @@ class Houses { private: HouseMap houseMap; + HouseMap houseMapClientId; }; diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 99c8a960677..6d93f522538 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -53,6 +53,7 @@ #include "enums/account_type.hpp" #include "enums/object_category.hpp" #include "enums/player_blessings.hpp" +#include "enums/player_cyclopedia.hpp" /* * NOTE: This namespace is used so that we can add functions without having to declare them in the ".hpp/.hpp" file @@ -1235,6 +1236,9 @@ void ProtocolGame::parsePacketFromDispatcher(NetworkMessage &msg, uint8_t recvby case 0xAC: parseChannelExclude(msg); break; + case 0xAD: + parseCyclopediaHouseAuction(msg); + break; case 0xAE: parseSendBosstiary(); break; @@ -6837,6 +6841,7 @@ void ProtocolGame::sendAddCreature(const std::shared_ptr &creature, co sendLootContainers(); sendBasicData(); + sendHousesInfo(); // Wheel of destiny cooldown if (!oldProtocol && g_configManager().getBoolean(TOGGLE_WHEELSYSTEM)) { player->wheel()->sendGiftOfLifeCooldown(); @@ -9267,3 +9272,192 @@ void ProtocolGame::sendTakeScreenshot(Screenshot_t screenshotType) { msg.addByte(screenshotType); writeToOutputBuffer(msg); } + +void ProtocolGame::parseCyclopediaHouseAuction(NetworkMessage &msg) { + if (oldProtocol) { + return; + } + + uint8_t houseActionType = msg.getByte(); + switch (houseActionType) { + case 0: { + const auto townName = msg.getString(); + g_game().playerCyclopediaHousesByTown(player->getID(), townName); + break; + } + case 1: { + const uint32_t houseId = msg.get(); + const uint64_t bidValue = msg.get(); + g_game().playerCyclopediaHouseBid(player->getID(), houseId, bidValue); + break; + } + case 2: { + const uint32_t houseId = msg.get(); + const uint32_t timestamp = msg.get(); + g_game().playerCyclopediaHouseMoveOut(player->getID(), houseId, timestamp); + break; + } + case 3: { + const uint32_t houseId = msg.get(); + const uint32_t timestamp = msg.get(); + const std::string &newOwner = msg.getString(); + const uint64_t bidValue = msg.get(); + g_game().playerCyclopediaHouseTransfer(player->getID(), houseId, timestamp, newOwner, bidValue); + break; + } + case 4: { + const uint32_t houseId = msg.get(); + g_game().playerCyclopediaHouseCancelMoveOut(player->getID(), houseId); + break; + } + case 5: { + const uint32_t houseId = msg.get(); + g_game().playerCyclopediaHouseCancelTransfer(player->getID(), houseId); + break; + } + case 6: { + const uint32_t houseId = msg.get(); + g_game().playerCyclopediaHouseAcceptTransfer(player->getID(), houseId); + break; + } + case 7: { + const uint32_t houseId = msg.get(); + g_game().playerCyclopediaHouseRejectTransfer(player->getID(), houseId); + break; + } + } +} + +void ProtocolGame::sendCyclopediaHouseList(HouseMap houses) { + NetworkMessage msg; + msg.addByte(0xC7); + msg.add(houses.size()); + for (const auto &[clientId, houseData] : houses) { + msg.add(clientId); + msg.addByte(0x01); // 0x00 = Renovation; 0x01 = Available + + auto houseState = houseData->getState(); + auto stateValue = magic_enum::enum_integer(houseState); + msg.addByte(stateValue); + if (houseState == CyclopediaHouseState::Available) { + bool bidder = houseData->getBidderName() == player->getName(); + msg.addString(houseData->getBidderName()); + msg.addByte(bidder); + uint8_t disableIndex = enumToValue(player->canBidHouse(clientId)); + msg.addByte(disableIndex); + + if (!houseData->getBidderName().empty()) { + msg.add(houseData->getBidEndDate()); + msg.add(houseData->getHighestBid()); + if (bidder) { + msg.add(houseData->getBidHolderLimit()); + } + } + } else if (houseState == CyclopediaHouseState::Rented) { + auto ownerName = IOLoginData::getNameByGuid(houseData->getOwner()); + msg.addString(ownerName); + msg.add(houseData->getPaidUntil()); + + bool rented = ownerName.compare(player->getName()) == 0; + msg.addByte(rented); + if (rented) { + msg.addByte(0); + msg.addByte(0); + } + } else if (houseState == CyclopediaHouseState::Transfer) { + auto ownerName = IOLoginData::getNameByGuid(houseData->getOwner()); + msg.addString(ownerName); + msg.add(houseData->getPaidUntil()); + + bool isOwner = ownerName.compare(player->getName()) == 0; + msg.addByte(isOwner); + if (isOwner) { + msg.addByte(0); // ? + msg.addByte(0); // ? + } + msg.add(houseData->getBidEndDate()); + msg.addString(houseData->getBidderName()); + msg.addByte(0); // ? + msg.add(houseData->getInternalBid()); + + bool isNewOwner = player->getName() == houseData->getBidderName(); + msg.addByte(isNewOwner); + if (isNewOwner) { + uint8_t disableIndex = enumToValue(player->canAcceptTransferHouse(clientId)); + msg.addByte(disableIndex); // Accept Transfer Error + msg.addByte(0); // Reject Transfer Error + } + + if (isOwner) { + msg.addByte(0); // Cancel Transfer Error + } + } else if (houseState == CyclopediaHouseState::MoveOut) { + auto ownerName = IOLoginData::getNameByGuid(houseData->getOwner()); + msg.addString(ownerName); + msg.add(houseData->getPaidUntil()); + + bool isOwner = ownerName.compare(player->getName()) == 0; + msg.addByte(isOwner); + if (isOwner) { + msg.addByte(0); // ? + msg.addByte(0); // ? + msg.add(houseData->getBidEndDate()); + msg.addByte(0); + } else { + msg.add(houseData->getBidEndDate()); + } + } + } + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendHouseAuctionMessage(uint32_t houseId, HouseAuctionType type, uint8_t index, bool bidSuccess /* = false*/) { + NetworkMessage msg; + const auto typeValue = enumToValue(type); + + msg.addByte(0xC3); + msg.add(houseId); + msg.addByte(typeValue); + if (bidSuccess && typeValue == 1) { + msg.addByte(0x00); + } + msg.addByte(index); + + writeToOutputBuffer(msg); +} + +void ProtocolGame::sendHousesInfo() { + NetworkMessage msg; + + uint32_t houseClientId = 0; + const auto accountHouseCount = g_game().map.houses.getHouseCountByAccount(player->getAccountId()); + const auto house = g_game().map.houses.getHouseByPlayerId(player->getGUID()); + if (house) { + houseClientId = house->getClientId(); + } + + msg.addByte(0xC6); + msg.add(houseClientId); + msg.addByte(0x00); + + msg.addByte(accountHouseCount); // Houses Account + + msg.addByte(0x00); + + msg.addByte(3); + msg.addByte(3); + + msg.addByte(0x01); + + msg.addByte(0x01); + msg.add(houseClientId); + + const auto &housesList = g_game().map.houses.getHouses(); + msg.add(housesList.size()); + for (const auto &it : housesList) { + msg.add(it.second->getClientId()); + } + + writeToOutputBuffer(msg); +} diff --git a/src/server/network/protocol/protocolgame.hpp b/src/server/network/protocol/protocolgame.hpp index 27fa6c8716c..22f135e05f1 100644 --- a/src/server/network/protocol/protocolgame.hpp +++ b/src/server/network/protocol/protocolgame.hpp @@ -29,6 +29,7 @@ enum Slots_t : uint8_t; enum CombatType_t : uint8_t; enum SoundEffect_t : uint16_t; enum class SourceEffect_t : uint8_t; +enum class HouseAuctionType : uint8_t; class NetworkMessage; class Player; @@ -68,6 +69,7 @@ using MarketOfferList = std::list; using HistoryMarketOfferList = std::list; using ItemsTierCountList = std::map>; using StashItemList = std::map; +using HouseMap = std::map>; struct TextMessage { TextMessage() = default; @@ -353,6 +355,11 @@ class ProtocolGame final : public Protocol { void sendCyclopediaCharacterBadges(); void sendCyclopediaCharacterTitles(); + void sendHousesInfo(); + void parseCyclopediaHouseAuction(NetworkMessage &msg); + void sendCyclopediaHouseList(HouseMap houses); + void sendHouseAuctionMessage(uint32_t houseId, HouseAuctionType type, uint8_t index, bool bidSuccess); + void sendCreatureWalkthrough(const std::shared_ptr &creature, bool walkthrough); void sendCreatureShield(const std::shared_ptr &creature); void sendCreatureEmblem(const std::shared_ptr &creature);