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);