diff --git a/data/items/items.xml b/data/items/items.xml index c3f3f9dd6a..049db54ba8 100644 --- a/data/items/items.xml +++ b/data/items/items.xml @@ -33359,10 +33359,12 @@ + + diff --git a/data/migrations/27.lua b/data/migrations/27.lua index d0ffd9c0cb..57e7775526 100644 --- a/data/migrations/27.lua +++ b/data/migrations/27.lua @@ -1,3 +1,22 @@ function onUpdateDatabase() - return false + print("> Updating database to version 27 (guildhalls, guild banks #2213)") + db.query("ALTER TABLE `houses` ADD `type` ENUM('HOUSE', 'GUILDHALL') NOT NULL DEFAULT 'HOUSE' AFTER `id`") + db.query("ALTER TABLE `guilds` ADD `balance` bigint(20) UNSIGNED NOT NULL DEFAULT '0'") + db.query([[ + CREATE TABLE IF NOT EXISTS `guild_transactions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `guild_id` int(11) NOT NULL, + `guild_associated` int(11) DEFAULT NULL, + `player_associated` int(11) DEFAULT NULL, + `type` ENUM('DEPOSIT', 'WITHDRAW') NOT NULL, + `category` ENUM ('OTHER', 'RENT', 'MATERIAL', 'SERVICES', 'REVENUE', 'CONTRIBUTION') NOT NULL DEFAULT 'OTHER', + `balance` bigint(20) UNSIGNED NOT NULL DEFAULT '0', + `time` bigint(20) NOT NULL, + PRIMARY KEY (`id`), + FOREIGN KEY (`guild_id`) REFERENCES `guilds`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`guild_associated`) REFERENCES `guilds`(`id`) ON DELETE SET NULL, + FOREIGN KEY (`player_associated`) REFERENCES `players`(`id`) ON DELETE SET NULL + ) ENGINE=InnoDB; + ]]) + return true end diff --git a/data/migrations/28.lua b/data/migrations/28.lua new file mode 100644 index 0000000000..d0ffd9c0cb --- /dev/null +++ b/data/migrations/28.lua @@ -0,0 +1,3 @@ +function onUpdateDatabase() + return false +end diff --git a/data/npc/GuildBanker.xml b/data/npc/GuildBanker.xml new file mode 100644 index 0000000000..b067bda0f7 --- /dev/null +++ b/data/npc/GuildBanker.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/data/npc/scripts/guildbank.lua b/data/npc/scripts/guildbank.lua new file mode 100644 index 0000000000..a7bf3fcbb4 --- /dev/null +++ b/data/npc/scripts/guildbank.lua @@ -0,0 +1,490 @@ +local keywordHandler = KeywordHandler:new() +local npcHandler = NpcHandler:new(keywordHandler) +NpcSystem.parseParameters(npcHandler) + +local count = {} +local transfer = {} + +function onCreatureAppear(cid) npcHandler:onCreatureAppear(cid) end +function onCreatureDisappear(cid) npcHandler:onCreatureDisappear(cid) end +function onCreatureSay(cid, type, msg) npcHandler:onCreatureSay(cid, type, msg) end +function onThink() npcHandler:onThink() end + +local function greetCallback(cid) + count[cid], transfer[cid] = nil, nil + return true +end + +local topicList = { + NONE = 0, + DEPOSIT_GOLD = 1, + DEPOSIT_CONSENT = 2, + WITHDRAW_GOLD = 3, + WITHDRAW_CONSENT = 4, + TRANSFER_TYPE = 5, + TRANSFER_PLAYER_GOLD = 6, + TRANSFER_PLAYER_WHO = 7, + TRANSFER_PLAYER_CONSENT = 8, + TRANSFER_GUILD_GOLD = 9, + TRANSFER_GUILD_WHO = 10, + TRANSFER_GUILD_CONSENT = 11, + LEDGER_CONSENT = 12 +} + +local function creatureSayCallback(cid, type, msg) + if not npcHandler:isFocused(cid) then + return false + end + local player = Player(cid) + local guild = player:getGuild() + if not guild then + npcHandler:say("I'm too busy serving guilds, perhaps my colleague the {Banker} can assist you with your personal bank account.", cid) + npcHandler.topic[cid] = topicList.NONE + return true + end + if msgcontains(msg, "balance") then + npcHandler.topic[cid] = topicList.NONE + npcHandler:say("The {guild account} balance of " .. guild:getName() .. " is " .. guild:getBankBalance() .. " gold.", cid) + return true + elseif msgcontains(msg, "deposit") then + count[cid] = player:getBankBalance() + if count[cid] < 1 then + npcHandler:say("Your {personal} bank account looks awefully empty, please deposit money there first, I don't like dealing with heavy coins.", cid) + npcHandler.topic[cid] = topicList.NONE + return false + end + if string.match(msg,"%d+") then + count[cid] = getMoneyCount(msg) + if count[cid] > 0 and count[cid] <= player:getBankBalance() then + npcHandler:say("Would you really like to deposit " .. count[cid] .. " gold to the guild " .. guild:getName() .. "?", cid) + npcHandler.topic[cid] = topicList.DEPOSIT_CONSENT + return true + else + npcHandler:say("You cannot afford to deposit " .. count[cid] .. " gold to the guild " .. guild:getName() .. ". You only have " .. player:getBankBalance() .. " gold in your account!", cid) + npcHandler.topic[cid] = topicList.NONE + return false + end + else + npcHandler:say("Please tell me how much gold it is you would like to deposit.", cid) + npcHandler.topic[cid] = topicList.DEPOSIT_GOLD + return true + end + elseif npcHandler.topic[cid] == topicList.DEPOSIT_GOLD then + count[cid] = getMoneyCount(msg) + if count[cid] > 0 and count[cid] <= player:getBankBalance() then + npcHandler:say("Would you really like to deposit " .. count[cid] .. " gold to the guild " .. guild:getName() .. "?", cid) + npcHandler.topic[cid] = topicList.DEPOSIT_CONSENT + return true + else + npcHandler:say("You do not have enough gold.", cid) + npcHandler.topic[cid] = topicList.NONE + return true + end + elseif npcHandler.topic[cid] == topicList.DEPOSIT_CONSENT then + if msgcontains(msg, "yes") then + local deposit = tonumber(count[cid]) + if deposit > 0 and player:getBankBalance() >= deposit then + player:setBankBalance(player:getBankBalance() - deposit) + player:save() + guild:setBankBalance(guild:getBankBalance() + deposit) + npcHandler:say("Alright, we have added the amount of " .. deposit .. " gold to the guild " .. guild:getName() .. ".", cid) + local currentTime = os.time() + local insertData = table.concat({ + guild:getId(), + player:getGuid(), + "'DEPOSIT'", + "'CONTRIBUTION'", + deposit, + currentTime + },',') + db.query("INSERT INTO `guild_transactions` (`guild_id`,`player_associated`,`type`,`category`,`balance`,`time`) VALUES ("..insertData..");") + local receipt = Game.createItem(ITEM_RECEIPT_SUCCESS, 1) + receipt:setAttribute(ITEM_ATTRIBUTE_TEXT, "Date: " .. os.date("%d. %b %Y - %H:%M:%S", currentTime) .. "\nType: Guild Deposit\nGold Amount: " .. deposit .. "\nReceipt Owner: " .. player:getName() .. "\nRecipient: The " .. guild:getName() .. "\n\nWe are happy to inform you that your transfer request was successfully carried out.") + player:addItemEx(receipt) + else + npcHandler:say("You do not have enough gold.", cid) + end + elseif msgcontains(msg, "no") then + npcHandler:say("As you wish. Is there something else I can do for you?", cid) + end + npcHandler.topic[cid] = topicList.NONE + return true + elseif msgcontains(msg, "withdraw") then + if string.match(msg,"%d+") then + count[cid] = getMoneyCount(msg) + if count[cid] > 0 and count[cid] <= guild:getBankBalance() then + npcHandler:say("Are you sure you wish to withdraw " .. count[cid] .. " gold from the guild " .. guild:getName() .. "?", cid) + npcHandler.topic[cid] = topicList.WITHDRAW_CONSENT + else + npcHandler:say("There is not enough gold in the guild " .. guild:getName() .. ". Their available balance is currently " .. guild:getBankBalance() .. ".", cid) + npcHandler.topic[cid] = topicList.NONE + end + return true + else + npcHandler:say("Please tell me how much gold you would like to withdraw.", cid) + npcHandler.topic[cid] = topicList.WITHDRAW_GOLD + return true + end + elseif npcHandler.topic[cid] == topicList.WITHDRAW_GOLD then + count[cid] = getMoneyCount(msg) + if count[cid] > 0 and count[cid] <= guild:getBankBalance() then + npcHandler:say("Are you sure you wish to withdraw " .. count[cid] .. " gold from the guild " .. guild:getName() .. "?", cid) + npcHandler.topic[cid] = topicList.WITHDRAW_CONSENT + else + npcHandler:say("There is not enough gold in the guild " .. guild:getName() .. ". Their available balance is currently " .. guild:getBankBalance() .. ".", cid) + npcHandler.topic[cid] = topicList.NONE + end + return true + elseif npcHandler.topic[cid] == topicList.WITHDRAW_CONSENT then + if msgcontains(msg, "yes") then + local withdraw = count[cid] + if withdraw > 0 and withdraw <= guild:getBankBalance() then + if player:getGuid() == guild:getOwnerGUID() or player:getGuildLevel() == 2 then + guild:setBankBalance(guild:getBankBalance() - withdraw) + player:setBankBalance(player:getBankBalance() + withdraw) + player:save() + npcHandler:say("Alright, we have removed the amount of " .. withdraw .. " gold from the guild " .. guild:getName() .. ", and added it to your {personal} account.", cid) + local currentTime = os.time() + local insertData = table.concat({ + guild:getId(), + player:getGuid(), + "'WITHDRAW'", + withdraw, + currentTime + },',') + db.query("INSERT INTO `guild_transactions` (`guild_id`,`player_associated`,`type`,`balance`,`time`) VALUES ("..insertData..");") + local receipt = Game.createItem(ITEM_RECEIPT_SUCCESS, 1) + receipt:setAttribute(ITEM_ATTRIBUTE_TEXT, "Date: " .. os.date("%d. %b %Y - %H:%M:%S", currentTime) .. "\nType: Guild Withdraw\nGold Amount: " .. withdraw .. "\nReceipt Owner: " .. player:getName() .. "\nRecipient: The " .. guild:getName() .. "\n\nWe are happy to inform you that your transfer request was successfully carried out.") + player:addItemEx(receipt) + else + npcHandler:say("Sorry, you are not authorized for withdrawals. Only Leaders and Vice-leaders are allowed to withdraw funds from guild accounts.", cid) + end + else + npcHandler:say("There is not enough gold in the guild " .. guild:getName() .. ". Their available balance is currently " .. guild:getBankBalance() .. ".", cid) + end + npcHandler.topic[cid] = topicList.NONE + elseif msgcontains(msg, "no") then + npcHandler:say("Come back anytime you want to if you wish to {withdraw} your money.", cid) + npcHandler.topic[cid] = topicList.NONE + end + return true + elseif msgcontains(msg, "guild transfer") or (npcHandler.topic[cid] == topicList.TRANSFER_TYPE and msgcontains(msg, "guild")) then + if player:getGuid() ~= guild:getOwnerGUID() then + npcHandler:say("Sorry, you are not authorized for withdrawals. Only Guild Leaders are allowed to transfer funds between guilds.", cid) + npcHandler.topic[cid] = topicList.NONE + return true + end + + npcHandler:say("Please tell me the amount of gold you would like to transfer to another guild.", cid) + npcHandler.topic[cid] = topicList.TRANSFER_GUILD_GOLD + elseif npcHandler.topic[cid] == topicList.TRANSFER_GUILD_GOLD then + count[cid] = getMoneyCount(msg) + if count[cid] < 0 or guild:getBankBalance() < count[cid] then + npcHandler:say("There is not enough gold in your guild account.", cid) + npcHandler.topic[cid] = topicList.NONE + return true + end + npcHandler:say("Which guild would you like transfer " .. count[cid] .. " gold to?", cid) + npcHandler.topic[cid] = topicList.TRANSFER_GUILD_WHO + elseif npcHandler.topic[cid] == topicList.TRANSFER_GUILD_WHO then + + local query = db.storeQuery("SELECT `id`, `name` FROM `guilds` WHERE `name`=" .. db.escapeString(msg)) + if not query then + npcHandler:say("There are no guild in my record who has the name: ["..msg.."]", cid) + npcHandler.topic[cid] = topicList.NONE + return true + end + transfer[cid] = { + ["id"] = result.getNumber(query, "id"), + ["name"] = result.getString(query, "name") + } + result.free(query) + + if guild:getName() == transfer[cid].name then + npcHandler:say("Fill in this field with guild who receives your gold!", cid) + npcHandler.topic[cid] = topicList.NONE + return true + end + + npcHandler:say("So you would like to transfer " .. count[cid] .. " gold from the guild " .. guild:getName() .. " to the guild " .. transfer[cid].name .. "?", cid) + npcHandler.topic[cid] = topicList.TRANSFER_GUILD_CONSENT + elseif npcHandler.topic[cid] == topicList.TRANSFER_GUILD_CONSENT then + if msgcontains(msg, "yes") then + if not transfer[cid] or count[cid] < 1 or count[cid] > guild:getBankBalance() then + transfer[cid] = nil + npcHandler:say("Your guild account cannot afford this transfer.", cid) + npcHandler.topic[cid] = topicList.NONE + return true + end + + -- Transfer between the guilds + local transferAmount = count[cid] + guild:setBankBalance(guild:getBankBalance() - transferAmount) + local transferGuild = Guild(transfer[cid].name) + if transferGuild then + transferGuild:setBankBalance(transferGuild:getBankBalance() + transferAmount) + else + db.query("UPDATE `guilds` SET `balance` = (`balance`+"..transferAmount..") WHERE `id`="..transfer[cid].id) + end + + -- Log: Withdraw from main guild + local currentTime = os.time() + local insertData = table.concat({ + guild:getId(), + transfer[cid].id, + player:getGuid(), + "'WITHDRAW'", + transferAmount, + currentTime + },',') + db.query("INSERT INTO `guild_transactions` (`guild_id`,`guild_associated`,`player_associated`,`type`,`balance`,`time`) VALUES ("..insertData..");") + + -- Log: Deposit to transfer guild + insertData = table.concat({ + transfer[cid].id, + guild:getId(), + player:getGuid(), + "'DEPOSIT'", + transferAmount, + currentTime + },',') + db.query("INSERT INTO `guild_transactions` (`guild_id`,`guild_associated`,`player_associated`,`type`,`balance`,`time`) VALUES ("..insertData..");") + + npcHandler:say("Very well. You have transfered " .. transferAmount .. " gold to " .. transfer[cid].name ..".", cid) + transfer[cid] = nil + return true + elseif msgcontains(msg, "no") then + npcHandler:say("Alright, is there something else I can do for you?", cid) + end + npcHandler.topic[cid] = topicList.NONE + elseif msgcontains(msg, "player transfer") or (npcHandler.topic[cid] == topicList.TRANSFER_TYPE and msgcontains(msg, "player")) then + local parts = msg:split(" ") + + if #parts < 3 then + if #parts == 2 then + -- Immediate topic 11 simulation + count[cid] = getMoneyCount(parts[2]) + if count[cid] < 0 or guild:getBankBalance() < count[cid] then + npcHandler:say("There is not enough gold in your guild account.", cid) + npcHandler.topic[cid] = topicList.NONE + return true + end + npcHandler:say("Who would you like transfer " .. count[cid] .. " gold to?", cid) + npcHandler.topic[cid] = topicList.TRANSFER_PLAYER_WHO + else + npcHandler:say("Please tell me the amount of gold you would like to transfer.", cid) + npcHandler.topic[cid] = topicList.TRANSFER_PLAYER_GOLD + end + else -- "transfer 250 playerName" or "transfer 250 to playerName" + local receiver = "" + + local seed = 3 + if #parts > 3 then + seed = parts[3] == "to" and 4 or 3 + end + for i = seed, #parts do + receiver = receiver .. " " .. parts[i] + end + receiver = receiver:trim() + + -- Immediate topic 11 simulation + count[cid] = getMoneyCount(parts[2]) + if count[cid] < 0 or guild:getBankBalance() < count[cid] then + npcHandler:say("There is not enough gold in your guild account.", cid) + npcHandler.topic[cid] = topicList.NONE + return true + end + -- Topic 12 + transfer[cid] = getPlayerDatabaseInfo(receiver) + if player:getName() == transfer[cid].name then + npcHandler:say("Fill in this field with person who receives your gold!", cid) + npcHandler.topic[cid] = topicList.NONE + return true + end + + if transfer[cid] then + if transfer[cid].vocation == VOCATION_NONE and Player(cid):getVocation() ~= 0 then + npcHandler:say("I'm afraid this character only holds a junior account at our bank. Do not worry, though. Once he has chosen his vocation, his account will be upgraded.", cid) + npcHandler.topic[cid] = topicList.NONE + return true + end + npcHandler:say("So you would like to transfer " .. count[cid] .. " gold from the guild " .. guild:getName() .. " to " .. transfer[cid].name .. "?", cid) + npcHandler.topic[cid] = topicList.TRANSFER_PLAYER_CONSENT + else + npcHandler:say("This player does not exist.", cid) + npcHandler.topic[cid] = topicList.NONE + end + end + return true + elseif msgcontains(msg, "transfer") then + if player:getGuid() == guild:getOwnerGUID() or player:getGuildLevel() == 2 then + npcHandler:say("Would you like to transfer money to a {guild} or a {player}?", cid) + npcHandler.topic[cid] = topicList.TRANSFER_TYPE + else + npcHandler:say("Sorry, you are not authorized for withdrawals. Only Leaders and Vice-leaders are allowed to withdraw funds from guild accounts.", cid) + npcHandler.topic[cid] = topicList.NONE + end + return true + elseif npcHandler.topic[cid] == topicList.TRANSFER_PLAYER_GOLD then + count[cid] = getMoneyCount(msg) + if count[cid] < 0 or guild:getBankBalance() < count[cid] then + npcHandler:say("There is not enough gold in your guild account.", cid) + npcHandler.topic[cid] = topicList.NONE + return true + end + npcHandler:say("Who would you like transfer " .. count[cid] .. " gold to?", cid) + npcHandler.topic[cid] = topicList.TRANSFER_PLAYER_WHO + elseif npcHandler.topic[cid] == topicList.TRANSFER_PLAYER_WHO then + transfer[cid] = getPlayerDatabaseInfo(msg) + if player:getName() == transfer[cid].name then + npcHandler:say("Fill in this field with person who receives your gold!", cid) + npcHandler.topic[cid] = topicList.NONE + return true + end + + if transfer[cid] then + if transfer[cid].vocation == VOCATION_NONE and Player(cid):getVocation() ~= 0 then + npcHandler:say("I'm afraid this character only holds a junior account at our bank. Do not worry, though. Once he has chosen his vocation, his account will be upgraded.", cid) + npcHandler.topic[cid] = topicList.NONE + return true + end + npcHandler:say("So you would like to transfer " .. count[cid] .. " gold from the guild " .. guild:getName() .. " to " .. transfer[cid].name .. "?", cid) + npcHandler.topic[cid] = topicList.TRANSFER_PLAYER_CONSENT + else + npcHandler:say("This player does not exist.", cid) + npcHandler.topic[cid] = topicList.NONE + end + elseif npcHandler.topic[cid] == topicList.TRANSFER_PLAYER_CONSENT then + if msgcontains(msg, "yes") then + if not transfer[cid] or count[cid] < 1 or count[cid] > guild:getBankBalance() then + transfer[cid] = nil + npcHandler:say("Your guild account cannot afford this transfer.", cid) + npcHandler.topic[cid] = topicList.NONE + return true + end + local transferAmount = count[cid] + guild:setBankBalance(guild:getBankBalance() - transferAmount) + player:setBankBalance(player:getBankBalance() + transferAmount) + if not player:transferMoneyTo(transfer[cid], transferAmount) then + npcHandler:say("You cannot transfer money to this account.", cid) + player:setBankBalance(player:getBankBalance() - transferAmount) + else + npcHandler:say("Very well. You have transfered " .. transferAmount .. " gold to " .. transfer[cid].name ..".", cid) + transfer[cid] = nil + local currentTime = os.time() + local insertData = table.concat({ + guild:getId(), + player:getGuid(), + "'WITHDRAW'", + transferAmount, + currentTime + },',') + db.query("INSERT INTO `guild_transactions` (`guild_id`,`player_associated`,`type`,`balance`,`time`) VALUES ("..insertData..");") + end + elseif msgcontains(msg, "no") then + npcHandler:say("Alright, is there something else I can do for you?", cid) + end + npcHandler.topic[cid] = topicList.NONE + elseif msgcontains(msg, "ledger") then + if player:getGuid() ~= guild:getOwnerGUID() then + npcHandler.topic[cid] = topicList.NONE + npcHandler:say("Sorry, this is confidential between me and your Guild Leader!", cid) + return true + end + npcHandler.topic[cid] = topicList.LEDGER_CONSENT + npcHandler:say("To your advantage, I'm a man who got his papers sorted out. I have ledger records of all transaction requests for your {guild account}. Would you like to get a copy?", cid) + return true + elseif msgcontains(msg, "yes") and npcHandler.topic[cid] == topicList.LEDGER_CONSENT then + local dbTransactions = db.storeQuery([[ + SELECT + `g`.`name` as `guild_name`, + `g2`.`name` as `guild_associated_name`, + `p`.`name` as `player_name`, + `t`.`type`, + `t`.`balance`, + `t`.`time` + FROM `guild_transactions` as `t` + JOIN `guilds` as `g` + ON `t`.`guild_id` = `g`.`id` + LEFT JOIN `guilds` as `g2` + ON `t`.`guild_associated` = `g2`.`id` + LEFT JOIN `players` as `p` + ON `t`.`player_associated` = `p`.`id` + WHERE `guild_id` = ]] .. guild:getId() .. [[ + ORDER BY `t`.`time` DESC + ]]) + local ledger_text = "Ledger Date: " .. os.date("%d. %b %Y - %H:%M:%S", os.time()) .. ".\nOfficial ledger for Guild: " .. guild:getName() .. ".\nGuild balance: " .. guild:getBankBalance() .. ".\n\n" + local records = {} + + if dbTransactions ~= false then + repeat + local guild_name = result.getString(dbTransactions, 'guild_name') + local guild_associated_name = result.getString(dbTransactions, 'guild_associated_name') + local player_name = result.getString(dbTransactions, 'player_name') + local type = (result.getString(dbTransactions, 'type') == "WITHDRAW" and "Withdraw" or "Deposit") + local balance = result.getNumber(dbTransactions, 'balance') + local time = result.getNumber(dbTransactions, 'time') + if guild_associated_name ~= "" then + guild_associated_name = "\nReceiving Guild: The " .. guild_associated_name + else + guild_associated_name = "" + end + table.insert(records, "Date: " .. os.date("%d. %b %Y - %H:%M:%S", time) .. "\nType: Guild "..type.."\nGold Amount: " .. balance .. "\nReceipt Owner: " .. player_name .. "\nReceipt Guild: The " .. guild_name .. guild_associated_name) + + until not result.next(dbTransactions) + result.free(dbTransactions) + else -- No transactions exist + npcHandler.topic[cid] = topicList.NONE + npcHandler:say("Ohh, your ledger is actually empty. You should start using your {guild account}!", cid) + return true + end + + local ledger = Game.createItem(ITEM_DOCUMENT_RO, 1) + ledger:setAttribute(ITEM_ATTRIBUTE_TEXT, ledger_text .. table.concat(records, "\n\n")) + player:addItemEx(ledger) + + npcHandler.topic[cid] = topicList.NONE + npcHandler:say("Here is your ledger "..player:getName()..". Feel free to come back anytime should you need an updated copy.", cid) + + return true + elseif msgcontains(msg, "no") and npcHandler.topic[cid] == topicList.LEDGER_CONSENT then + npcHandler.topic[cid] = topicList.NONE + npcHandler:say("No worries, I will keep it updated for a later date then.", cid) + return true + end + return true +end + +keywordHandler:addKeyword({"help"}, StdModule.say, { + npcHandler = npcHandler, + text = "You can check the {balance} of your guild account and {deposit} money to it. Guild Leaders and Vice-leaders can also {withdraw}, Guild Leaders can {transfer} money to other guilds and check their guild {ledger}." +}) +keywordHandler:addAliasKeyword({'money'}) +keywordHandler:addAliasKeyword({'guild account'}) + +keywordHandler:addKeyword({"job"}, StdModule.say, { + npcHandler = npcHandler, + text = "I work in this bank. I can {help} you with your {guild account}." +}) +keywordHandler:addAliasKeyword({'functions'}) +keywordHandler:addAliasKeyword({'basic'}) + +keywordHandler:addKeyword({"rent"}, StdModule.say, { + npcHandler = npcHandler, + text = "Once you have acquired a guildhall the rent will be charged automatically from your {guild account} every month." +}) + +keywordHandler:addKeyword({"personal"}, StdModule.say, { + npcHandler = npcHandler, + text = "Head over to my colleague known as {Banker}, he will help you get your funds into your own bank account." +}) + +keywordHandler:addKeyword({"banker"}, StdModule.say, { + npcHandler = npcHandler, + text = "Banker is my colleague, he loves flipping coins between his fingers. He will help you exchange money, check your balance and help you withdraw and deposit your funds." +}) + +npcHandler:setMessage(MESSAGE_GREET, "Welcome to the bank, |PLAYERNAME|! Need some help with your {guild account}?") +npcHandler:setCallback(CALLBACK_GREET, greetCallback) +npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback) +npcHandler:addModule(FocusModule:new()) diff --git a/data/talkactions/scripts/buyhouse.lua b/data/talkactions/scripts/buyhouse.lua index 3320556fce..c6638fafb3 100644 --- a/data/talkactions/scripts/buyhouse.lua +++ b/data/talkactions/scripts/buyhouse.lua @@ -19,23 +19,81 @@ function onSay(player, words, param) return false end - if house:getOwnerGuid() > 0 then - player:sendCancelMessage("This house already has an owner.") + -- HOUSE_TYPE_NORMAL + if house:getType() == HOUSE_TYPE_NORMAL then + if house:getOwnerGuid() > 0 then + player:sendCancelMessage("This house already has an owner.") + return false + end + + if player:getHouse() then + player:sendCancelMessage("You are already the owner of a house.") + return false + end + + local price = house:getTileCount() * housePrice + if not player:removeMoney(price) then + player:sendCancelMessage("You do not have enough money.") + return false + end + + house:setOwnerGuid(player:getGuid()) + player:sendTextMessage(MESSAGE_INFO_DESCR, "You have successfully bought this house, be sure to have the money for the rent in the bank.") return false end - if player:getHouse() then - player:sendCancelMessage("You are already the owner of a house.") + -- HOUSE_TYPE_GUILDHALL + if house:getOwnerGuild() > 0 then + player:sendCancelMessage("This guildhall already has an owner.") + return false + end + + local guild = player:getGuild() + if guild:getHouseId() then + player:sendCancelMessage("Your guild already owns a guildhall.") + return false + end + + if guild:getOwnerGuid() ~= player:getGuid() then + player:sendCancelMessage("Only Guild Leaders can buy guildhalls.") return false end local price = house:getTileCount() * housePrice - if not player:removeTotalMoney(price) then - player:sendCancelMessage("You do not have enough money.") + local balance = guild:getBankBalance() + if price > balance then + player:sendCancelMessage("Your guild bank do not have enough money, it is missing ".. price - balance .. " gold.") return false end + guild:setBankBalance(balance - price) + local currentTime = os.time() + local insertData = table.concat({ + guild:getId(), + player:getGuid(), + "'WITHDRAW'", + "'RENT'", + price, + currentTime + },',') + db.query("INSERT INTO `guild_transactions` (`guild_id`, `player_associated`, `type`, `category`, `balance`, `time`) VALUES ("..insertData..");") + + local receipt = Game.createItem(ITEM_RECEIPT_SUCCESS, 1) + + receipt:setAttribute(ITEM_ATTRIBUTE_TEXT, table.concat({ + "Date: " .. os.date("%d. %b %Y - %H:%M:%S", currentTime), + "Type: Guild Withdraw", + "Category: House rent", + "House: ".. house:getName(), + "Gold Amount: " .. price, + "Receipt Owner: " .. player:getName(), + "Recipient: The " .. guild:getName(), + "\nWe are happy to inform you that your transfer request was successfully carried out." + },"\n")) + + player:addItemEx(receipt) + house:setOwnerGuid(player:getGuid()) - player:sendTextMessage(MESSAGE_INFO_DESCR, "You have successfully bought this house, be sure to have the money for the rent in the bank.") + player:sendTextMessage(MESSAGE_INFO_DESCR, "You have successfully bought this guildhall, be sure to have the money for the rent in the guild bank.") return false end diff --git a/data/world/forgotten-house.xml b/data/world/forgotten-house.xml index 4279d5650a..3d97b635ce 100644 --- a/data/world/forgotten-house.xml +++ b/data/world/forgotten-house.xml @@ -27,7 +27,7 @@ - + @@ -55,8 +55,8 @@ - - + + @@ -102,7 +102,7 @@ - + diff --git a/schema.sql b/schema.sql index 54b444c157..371495974e 100644 --- a/schema.sql +++ b/schema.sql @@ -132,6 +132,7 @@ CREATE TABLE IF NOT EXISTS `guilds` ( `name` varchar(255) NOT NULL, `ownerid` int(11) NOT NULL, `creationdata` int(11) NOT NULL, + `balance` bigint(20) unsigned NOT NULL DEFAULT '0', `motd` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY (`name`), @@ -139,6 +140,21 @@ CREATE TABLE IF NOT EXISTS `guilds` ( FOREIGN KEY (`ownerid`) REFERENCES `players`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; +CREATE TABLE IF NOT EXISTS `guild_transactions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `guild_id` int(11) NOT NULL, + `guild_associated` int(11) DEFAULT NULL, + `player_associated` int(11) DEFAULT NULL, + `type` ENUM('DEPOSIT', 'WITHDRAW') NOT NULL, + `category` ENUM ('OTHER', 'RENT', 'MATERIAL', 'SERVICES', 'REVENUE', 'CONTRIBUTION') NOT NULL DEFAULT 'OTHER', + `balance` bigint(20) UNSIGNED NOT NULL DEFAULT '0', + `time` bigint(20) NOT NULL, + PRIMARY KEY (`id`), + FOREIGN KEY (`guild_id`) REFERENCES `guilds`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`guild_associated`) REFERENCES `guilds`(`id`) ON DELETE SET NULL, + FOREIGN KEY (`player_associated`) REFERENCES `players`(`id`) ON DELETE SET NULL +) ENGINE=InnoDB; + CREATE TABLE IF NOT EXISTS `guild_invites` ( `player_id` int(11) NOT NULL DEFAULT '0', `guild_id` int(11) NOT NULL DEFAULT '0', @@ -207,6 +223,7 @@ CREATE TABLE IF NOT EXISTS `houses` ( `highest_bidder` int(11) NOT NULL DEFAULT '0', `size` int(11) NOT NULL DEFAULT '0', `beds` int(11) NOT NULL DEFAULT '0', + `type` ENUM('HOUSE', 'GUILDHALL') NOT NULL DEFAULT 'HOUSE', PRIMARY KEY (`id`), KEY `owner` (`owner`), KEY `town_id` (`town_id`) @@ -349,7 +366,7 @@ CREATE TABLE IF NOT EXISTS `towns` ( UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARACTER SET=utf8; -INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '26'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); +INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '27'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); DROP TRIGGER IF EXISTS `ondelete_players`; DROP TRIGGER IF EXISTS `oncreate_guilds`; diff --git a/src/const.h b/src/const.h index 5280ea7a71..77f9a6bfd9 100644 --- a/src/const.h +++ b/src/const.h @@ -501,6 +501,8 @@ enum item_t : uint16_t { ITEM_LETTER = 2597, ITEM_LETTER_STAMPED = 2598, ITEM_LABEL = 2599, + ITEM_RECEIPT_SUCCESS = 24301, + ITEM_RECEIPT_FAIL = 24302, ITEM_AMULETOFLOSS = 2173, diff --git a/src/enums.h b/src/enums.h index 5168a33f0f..9bb0b887fc 100644 --- a/src/enums.h +++ b/src/enums.h @@ -459,6 +459,10 @@ enum ReturnValue { RETURNVALUE_TRADEPLAYERHIGHESTBIDDER, RETURNVALUE_YOUCANNOTTRADETHISHOUSE, RETURNVALUE_YOUDONTHAVEREQUIREDPROFESSION, + RETURNVALUE_TRADEPLAYERNOTINAGUILD, + RETURNVALUE_TRADEGUILDALREADYOWNSAHOUSE, + RETURNVALUE_TRADEPLAYERNOTGUILDLEADER, + RETURNVALUE_YOUARENOTGUILDLEADER, RETURNVALUE_CANNOTMOVEITEMISNOTSTOREITEM, RETURNVALUE_ITEMCANNOTBEMOVEDTHERE, }; diff --git a/src/guild.cpp b/src/guild.cpp index dd211103bd..00e6325f58 100644 --- a/src/guild.cpp +++ b/src/guild.cpp @@ -81,3 +81,11 @@ void Guild::addRank(uint32_t rankId, const std::string& rankName, uint8_t level) { ranks.emplace_back(std::make_shared(rankId, rankName, level)); } + +void Guild::setBankBalance(uint64_t balance) { + bankBalance = balance; + Database& db = Database::getInstance(); + std::ostringstream query; + query << "UPDATE `guilds` SET `balance`=" << bankBalance << " WHERE `id`=" << id; + db.executeQuery(query.str()); +} diff --git a/src/guild.h b/src/guild.h index d3343c8dea..d8434e02fa 100644 --- a/src/guild.h +++ b/src/guild.h @@ -57,6 +57,25 @@ class Guild memberCount = count; } + uint64_t getBankBalance() const { + return bankBalance; + } + void setBankBalance(uint64_t balance); + + uint32_t getHouseId() const { + return houseId; + } + void setHouseId(uint32_t id) { + houseId = id; + } + + uint32_t getOwnerGUID() const { + return ownerGUID; + } + void setOwnerGUID(uint32_t guid) { + ownerGUID = guid; + } + const std::vector& getRanks() const { return ranks; } @@ -79,6 +98,9 @@ class Guild std::string motd; uint32_t id; uint32_t memberCount = 0; + uint64_t bankBalance = 0; + uint32_t ownerGUID = 0; + uint32_t houseId = 0; }; #endif diff --git a/src/house.cpp b/src/house.cpp index e011080395..07d5498212 100644 --- a/src/house.cpp +++ b/src/house.cpp @@ -38,26 +38,80 @@ void House::addTile(HouseTile* tile) houseTiles.push_back(tile); } -void House::setOwner(uint32_t guid, bool updateDatabase/* = true*/, Player* player/* = nullptr*/) +std::tuple House::initializeOwnerDataFromDatabase(uint32_t guid_guild, HouseType_t type) { + if (guid_guild == 0) { + return std::make_tuple(0, 0, "", 0, ""); + } + + Database& db = Database::getInstance(); + + if (type == HOUSE_TYPE_NORMAL) { + std::ostringstream query; + query << "SELECT `id`, `name`, `account_id` FROM `players` WHERE `id`=" << guid_guild; + if (DBResult_ptr result = db.storeQuery(query.str())) { + return std::make_tuple( + result->getNumber("id"), // sqlPlayerGuid + result->getNumber("account_id"), // sqlAccountId + result->getString("name"), // sqlPlayerName + 0, // sqlGuildId + "" // sqlGuildName + ); + } + throw std::runtime_error("Error in House::setOwner - Failed to find player GUID"); + } + + // HOUSE_TYPE_GUILDHALL + std::ostringstream query; + query << "SELECT `g`.`id`, `g`.`name` as `guild_name`, `g`.`ownerid`, `p`.`name`, `p`.`account_id` "; + query << "FROM `guilds` as `g` INNER JOIN `players` AS `p` ON `g`.`ownerid` = `p`.`id` "; + query << "WHERE `g`.`id`=" << guid_guild; + if (DBResult_ptr result = db.storeQuery(query.str())) { + return std::make_tuple( + result->getNumber("ownerid"), // sqlPlayerGuid + result->getNumber("account_id"), // sqlAccountId + result->getString("name"), // sqlPlayerName + result->getNumber("id"), // sqlGuildId + result->getString("guild_name") // sqlGuildName + ); + } + throw std::runtime_error("Error in House::setOwner - Failed to find guild ID"); +} + +// Param: guid_guild is either 0 (remove owner), player->getGUID() or guild->getId() +void House::setOwner(uint32_t guid_guild, bool updateDatabase/* = true*/, Player* previousPlayer/* = nullptr*/) { - if (updateDatabase && owner != guid) { + + uint32_t sqlAccountId, sqlPlayerGuid, sqlGuildId; + std::string sqlPlayerName, sqlGuildName; + + try { + std::tie(sqlPlayerGuid, sqlAccountId, sqlPlayerName, sqlGuildId, sqlGuildName) = initializeOwnerDataFromDatabase(guid_guild, type); + } catch (const std::runtime_error& err) { + std::cout << err.what(); + return; + } + + // If the old owner of this house is not the new owner + if (updateDatabase && owner != guid_guild) { Database& db = Database::getInstance(); std::ostringstream query; - query << "UPDATE `houses` SET `owner` = " << guid << ", `bid` = 0, `bid_end` = 0, `last_bid` = 0, `highest_bidder` = 0 WHERE `id` = " << id; + query << "UPDATE `houses` SET `owner` = " << guid_guild << ", `bid` = 0, `bid_end` = 0, `last_bid` = 0, `highest_bidder` = 0 WHERE `id` = " << id; db.executeQuery(query.str()); } - if (isLoaded && owner == guid) { + if (isLoaded && owner == guid_guild) { return; } isLoaded = true; + // If there was a previous owner to his house + // clean the house and return items to owner if (owner != 0) { //send items to depot - if (player) { - transferToDepot(player); + if (previousPlayer) { + transferToDepot(previousPlayer); } else { transferToDepot(); } @@ -106,12 +160,16 @@ void House::setOwner(uint32_t guid, bool updateDatabase/* = true*/, Player* play rentWarnings = 0; - if (guid != 0) { - std::string name = IOLoginData::getNameByGuid(guid); - if (!name.empty()) { - owner = guid; - ownerName = name; - ownerAccountId = IOLoginData::getAccountIdByPlayerName(name); + // Save the new owner to the house object + if (guid_guild != 0) { + owner = guid_guild; + ownerAccountId = sqlAccountId; + if (type == HOUSE_TYPE_GUILDHALL) { + std::ostringstream ss; + ss << "The " << sqlGuildName; + ownerName = ss.str(); + } else { + ownerName = sqlPlayerName; } } @@ -120,11 +178,16 @@ void House::setOwner(uint32_t guid, bool updateDatabase/* = true*/, Player* play void House::updateDoorDescription() const { + std::string houseType = "house"; + if (type == HOUSE_TYPE_GUILDHALL) { + houseType = "guildhall"; + } + std::ostringstream ss; if (owner != 0) { - ss << "It belongs to house '" << houseName << "'. " << ownerName << " owns this house."; + ss << "It belongs to " << houseType << " '" << houseName << "'. " << ownerName << " owns this " << houseType << "."; } else { - ss << "It belongs to house '" << houseName << "'. Nobody owns this house."; + ss << "It belongs to " << houseType << " '" << houseName << "'. Nobody owns this " << houseType << "."; const int32_t housePrice = g_config.getNumber(ConfigManager::HOUSE_PRICE); if (housePrice != -1 && g_config.getBoolean(ConfigManager::HOUSE_DOOR_SHOW_PRICE)) { @@ -142,25 +205,37 @@ AccessHouseLevel_t House::getHouseAccessLevel(const Player* player) if (!player) { return HOUSE_OWNER; } - - if (g_config.getBoolean(ConfigManager::HOUSE_OWNED_BY_ACCOUNT)) { - if (ownerAccountId == player->getAccount()) { - return HOUSE_OWNER; - } - } - if (player->hasFlag(PlayerFlag_CanEditHouses)) { return HOUSE_OWNER; } - if (player->getGUID() == owner) { - return HOUSE_OWNER; + uint32_t guid = player->getGUID(); + + if (type == HOUSE_TYPE_NORMAL) { + if (g_config.getBoolean(ConfigManager::HOUSE_OWNED_BY_ACCOUNT)) { + if (ownerAccountId == player->getAccount()) { + return HOUSE_OWNER; + } + } + if (guid == owner) { + return HOUSE_OWNER; + } + + } else { // HOUSE_TYPE_GUILDHALL + Guild* guild = player->getGuild(); + if (guild && guild->getId() == owner) { + if (guild->getOwnerGUID() == guid) { + return HOUSE_OWNER; + } + if (player->getGuildRank() == guild->getRankByLevel(2)) { + return HOUSE_SUBOWNER; + } + } } if (subOwnerList.isInList(player)) { return HOUSE_SUBOWNER; } - if (guestList.isInList(player)) { return HOUSE_GUEST; } @@ -226,18 +301,42 @@ bool House::transferToDepot() const return false; } - Player* player = g_game.getPlayerByGUID(owner); - if (player) { - transferToDepot(player); - } else { - Player tmpPlayer(nullptr); - if (!IOLoginData::loadPlayerById(&tmpPlayer, owner)) { - return false; + if (type == HOUSE_TYPE_NORMAL) { + Player* player = g_game.getPlayerByGUID(owner); + if (player) { + transferToDepot(player); + } else { + Player tmpPlayer(nullptr); + if (!IOLoginData::loadPlayerById(&tmpPlayer, owner)) { + return false; + } + + transferToDepot(&tmpPlayer); + IOLoginData::savePlayer(&tmpPlayer); } + } else { // HOUSE_TYPE_GUILDHALL + Guild* guild = g_game.getGuild(owner); + if (!guild) { + guild = IOGuild::loadGuild(owner); + if (!guild) { + std::cout << "Warning: [Houses::transferToDepot] Failed to find guild associated to guildhall = " << id << ". Guild = " << owner << std::endl; + return false; + } + } + Player* player = g_game.getPlayerByGUID(guild->getOwnerGUID()); + if (player) { + transferToDepot(player); + } else { + Player tmpPlayer(nullptr); + if (!IOLoginData::loadPlayerById(&tmpPlayer, guild->getOwnerGUID())) { + return false; + } - transferToDepot(&tmpPlayer); - IOLoginData::savePlayer(&tmpPlayer); + transferToDepot(&tmpPlayer); + IOLoginData::savePlayer(&tmpPlayer); + } } + return true; } @@ -408,7 +507,13 @@ bool House::executeTransfer(HouseTransferItem* item, Player* newOwner) return false; } - setOwner(newOwner->getGUID()); + if (type == HOUSE_TYPE_NORMAL) { + setOwner(newOwner->getGUID()); + } else { + Guild* newOwnerGuild = newOwner->getGuild(); + setOwner(newOwnerGuild->getId()); + } + transferItem = nullptr; return true; } @@ -652,12 +757,51 @@ bool Houses::loadHousesXML(const std::string& filename) house->setRent(pugi::cast(houseNode.attribute("rent").value())); house->setTownId(pugi::cast(houseNode.attribute("townid").value())); + if (houseNode.attribute("guildhall").as_bool()) { + house->setType(HOUSE_TYPE_GUILDHALL); + } house->setOwner(0, false); } return true; } +time_t Houses::increasePaidUntil(RentPeriod_t rentPeriod, time_t paidUntil) const +{ + switch (rentPeriod) { + case RENTPERIOD_DAILY: + return paidUntil += 24 * 60 * 60; + case RENTPERIOD_WEEKLY: + return paidUntil += 7 * 24 * 60 * 60; + case RENTPERIOD_MONTHLY: + return paidUntil += 30 * 24 * 60 * 60; + case RENTPERIOD_YEARLY: + return paidUntil += 365 * 24 * 60 * 60; + case RENTPERIOD_DEV: + return paidUntil += 5 * 60; + default: + return paidUntil; + } +} + +std::string Houses::getRentPeriod(RentPeriod_t rentPeriod) const +{ + switch (rentPeriod) { + case RENTPERIOD_DAILY: + return "daily"; + case RENTPERIOD_WEEKLY: + return "weekly"; + case RENTPERIOD_MONTHLY: + return "monthly"; + case RENTPERIOD_YEARLY: + return "annual"; + case RENTPERIOD_DEV: + return "dev"; + default: + return "never"; + } +} + void Houses::payHouses(RentPeriod_t rentPeriod) const { if (rentPeriod == RENTPERIOD_NEVER) { @@ -682,73 +826,93 @@ void Houses::payHouses(RentPeriod_t rentPeriod) const continue; } - Player player(nullptr); - if (!IOLoginData::loadPlayerById(&player, ownerId)) { - // Player doesn't exist, reset house owner - house->setOwner(0); - continue; - } - - if (player.getBankBalance() >= rent) { - player.setBankBalance(player.getBankBalance() - rent); - - time_t paidUntil = currentTime; - switch (rentPeriod) { - case RENTPERIOD_DAILY: - paidUntil += 24 * 60 * 60; - break; - case RENTPERIOD_WEEKLY: - paidUntil += 24 * 60 * 60 * 7; - break; - case RENTPERIOD_MONTHLY: - paidUntil += 24 * 60 * 60 * 30; - break; - case RENTPERIOD_YEARLY: - paidUntil += 24 * 60 * 60 * 365; - break; - default: - break; + if (house->getType() == HOUSE_TYPE_NORMAL) { + Player player(nullptr); + if (!IOLoginData::loadPlayerById(&player, ownerId)) { + // Player doesn't exist, reset house owner + house->setOwner(0); + continue; } - house->setPaidUntil(paidUntil); - } else { - if (house->getPayRentWarnings() < 7) { - int32_t daysLeft = 7 - house->getPayRentWarnings(); + if (player.getBankBalance() >= rent) { + player.setBankBalance(player.getBankBalance() - rent); + + time_t paidUntil = increasePaidUntil(rentPeriod, currentTime); + house->setPaidUntil(paidUntil); + } else { + if (house->getPayRentWarnings() < 7) { + int32_t daysLeft = 7 - house->getPayRentWarnings(); - Item* letter = Item::CreateItem(ITEM_LETTER_STAMPED); - std::string period; + Item* letter = Item::CreateItem(ITEM_LETTER_STAMPED); + std::string period = getRentPeriod(rentPeriod); - switch (rentPeriod) { - case RENTPERIOD_DAILY: - period = "daily"; - break; + std::ostringstream ss; + ss << "Warning! \nThe " << period << " rent of " << house->getRent() << " gold for your house \"" << house->getName() << "\" is payable. Have it within " << daysLeft << " days or you will lose this house."; + letter->setText(ss.str()); + g_game.internalAddItem(player.getInbox(), letter, INDEX_WHEREEVER, FLAG_NOLIMIT); + house->setPayRentWarnings(house->getPayRentWarnings() + 1); + } else { + house->setOwner(0, true, &player); + } + } + IOLoginData::savePlayer(&player); + + } else { // HOUSE_TYPE_GUILDHALL + Guild* guild = g_game.getGuild(ownerId); + if (!guild) { + guild = IOGuild::loadGuild(ownerId); + if (!guild) { + house->setOwner(0); + continue; + } + } - case RENTPERIOD_WEEKLY: - period = "weekly"; - break; + // If guild can afford paying rent + if (guild->getBankBalance() >= rent) { + std::cout << "[Info - Houses::payHouses] Paying rent info" + << " - Name: " << house->getName() + << " - House id: " << house->getId() + << " - Guild: " << guild->getName() + << " - Balance " << guild->getBankBalance() + << " - Rent " << rent + << " - New balance " << guild->getBankBalance() - rent << std::endl; + guild->setBankBalance(guild->getBankBalance() - rent); + + // Log guild transaction + Database& db = Database::getInstance(); + std::ostringstream query; + query << "INSERT INTO `guild_transactions` (`guild_id`,`type`,`category`,`balance`,`time`) VALUES (" << ownerId << ",'WITHDRAW','RENT'," << rent << "," << currentTime << ");"; + db.executeQuery(query.str()); + + time_t paidUntil = increasePaidUntil(rentPeriod, currentTime); + house->setPaidUntil(paidUntil); + } else { // guild cannot afford rent + std::cout << "a guild cannot afford their rent " << house->getPayRentWarnings() << std::endl; + Player player(nullptr); + if (!IOLoginData::loadPlayerById(&player, guild->getOwnerGUID())) { + // Player doesn't exist, reset house owner + house->setOwner(0); + std::ostringstream ss; + ss << "Error: Guild " << guild->getName() << " has an owner that does not exist: " << guild->getOwnerGUID(); + std::cout << ss.str() << std::endl; + continue; + } - case RENTPERIOD_MONTHLY: - period = "monthly"; - break; + if (house->getPayRentWarnings() < 7) { + int32_t daysLeft = 7 - house->getPayRentWarnings(); - case RENTPERIOD_YEARLY: - period = "annual"; - break; + Item* letter = Item::CreateItem(ITEM_LETTER_STAMPED); + std::string period = getRentPeriod(rentPeriod); - default: - break; + std::ostringstream ss; + ss << "Warning! \nThe " << period << " rent of " << house->getRent() << " gold for your house \"" << house->getName() << "\" is payable. Have it within " << daysLeft << " days or you will lose this house."; + letter->setText(ss.str()); + g_game.internalAddItem(player.getInbox(), letter, INDEX_WHEREEVER, FLAG_NOLIMIT); + house->setPayRentWarnings(house->getPayRentWarnings() + 1); + } else { + house->setOwner(0, true, &player); } - - std::ostringstream ss; - ss << "Warning! \nThe " << period << " rent of " << house->getRent() << " gold for your house \"" << house->getName() << "\" is payable. Have it within " << daysLeft << " days or you will lose this house."; - letter->setText(ss.str()); - g_game.internalAddItem(player.getInbox(), letter, INDEX_WHEREEVER, FLAG_NOLIMIT); - house->setPayRentWarnings(house->getPayRentWarnings() + 1); - } else { - house->setOwner(0, true, &player); } } - - IOLoginData::savePlayer(&player); } } diff --git a/src/house.h b/src/house.h index 1a73a02f1f..0738fbb4a9 100644 --- a/src/house.h +++ b/src/house.h @@ -108,6 +108,13 @@ enum AccessHouseLevel_t { HOUSE_OWNER = 3, }; +// this enum should represent DB `houses`.`type` +// MySQL enum indexes start at 1 +enum HouseType_t { + HOUSE_TYPE_NORMAL = 1, + HOUSE_TYPE_GUILDHALL = 2, +}; + using HouseTileList = std::list; using HouseBedItemList = std::list; @@ -161,7 +168,7 @@ class House return houseName; } - void setOwner(uint32_t guid, bool updateDatabase = true, Player* player = nullptr); + void setOwner(uint32_t guid_guild, bool updateDatabase = true, Player* previousPlayer = nullptr); uint32_t getOwner() const { return owner; } @@ -198,6 +205,13 @@ class House return id; } + void setType(HouseType_t type) { + this->type = type; + } + HouseType_t getType() const { + return type; + } + void addDoor(Door* door); void removeDoor(Door* door); Door* getDoorByNumber(uint32_t doorId) const; @@ -224,6 +238,8 @@ class House } private: + std::tuple initializeOwnerDataFromDatabase(uint32_t guid_guild, HouseType_t type); + bool transferToDepot() const; bool transferToDepot(Player* player) const; @@ -252,6 +268,8 @@ class House Position posEntry = {}; + HouseType_t type = HOUSE_TYPE_NORMAL; + bool isLoaded = false; }; @@ -263,6 +281,7 @@ enum RentPeriod_t { RENTPERIOD_MONTHLY, RENTPERIOD_YEARLY, RENTPERIOD_NEVER, + RENTPERIOD_DEV,// 5 minutes rent period for testing purposes }; class Houses @@ -303,6 +322,8 @@ class Houses bool loadHousesXML(const std::string& filename); void payHouses(RentPeriod_t rentPeriod) const; + std::string getRentPeriod(RentPeriod_t rentPeriod) const; + time_t increasePaidUntil(RentPeriod_t rentPeriod, time_t paidUntil) const; const HouseMap& getHouses() const { return houseMap; diff --git a/src/ioguild.cpp b/src/ioguild.cpp index ed48b14560..a27c8eb9e4 100644 --- a/src/ioguild.cpp +++ b/src/ioguild.cpp @@ -27,9 +27,25 @@ Guild* IOGuild::loadGuild(uint32_t guildId) { Database& db = Database::getInstance(); std::ostringstream query; - query << "SELECT `name` FROM `guilds` WHERE `id` = " << guildId; + /* Readable SQL representation + query << " + SELECT + `g`.`name`, + `g`.`balance`, + `g`.`ownerid`, + IFNULL(`h`.`id`, 0) as `house_id` + FROM `guilds` AS `g` + LEFT JOIN `houses` AS `h` + ON `h`.`type` = 'Guildhall' + AND `h`.`owner` = " << guildId << " + WHERE `g`.`id` = " << guildId; + */ + query << "SELECT `g`.`name`, `g`.`balance`, `g`.`ownerid`, IFNULL(`h`.`id`, 0) as `house_id` FROM `guilds` AS `g` LEFT JOIN `houses` AS `h` ON `h`.`type` = 'Guildhall' AND `h`.`owner` = " << guildId << " WHERE `g`.`id` = " << guildId; if (DBResult_ptr result = db.storeQuery(query.str())) { Guild* guild = new Guild(guildId, result->getString("name")); + guild->setBankBalance(result->getNumber("balance")); + guild->setOwnerGUID(result->getNumber("ownerid")); + guild->setHouseId(result->getNumber("house_id")); query.str(std::string()); query << "SELECT `id`, `name`, `level` FROM `guild_ranks` WHERE `guild_id` = " << guildId; diff --git a/src/iologindata.cpp b/src/iologindata.cpp index 49b506ee25..c1fd6df112 100644 --- a/src/iologindata.cpp +++ b/src/iologindata.cpp @@ -1011,12 +1011,13 @@ void IOLoginData::increaseBankBalance(uint32_t guid, uint64_t bankBalance) Database::getInstance().executeQuery(query.str()); } -bool IOLoginData::hasBiddedOnHouse(uint32_t guid) +// guid_guild = player->getGUID() or guild->getId() +bool IOLoginData::hasBiddedOnHouse(uint32_t guid_guild) { Database& db = Database::getInstance(); std::ostringstream query; - query << "SELECT `id` FROM `houses` WHERE `highest_bidder` = " << guid << " LIMIT 1"; + query << "SELECT `id` FROM `houses` WHERE `highest_bidder` = " << guid_guild << " LIMIT 1"; return db.storeQuery(query.str()).get() != nullptr; } diff --git a/src/iomapserialize.cpp b/src/iomapserialize.cpp index af606c2147..a05decc357 100644 --- a/src/iomapserialize.cpp +++ b/src/iomapserialize.cpp @@ -272,7 +272,7 @@ bool IOMapSerialize::loadHouseInfo() { Database& db = Database::getInstance(); - DBResult_ptr result = db.storeQuery("SELECT `id`, `owner`, `paid`, `warnings` FROM `houses`"); + DBResult_ptr result = db.storeQuery("SELECT `id`, CAST(`type` as UNSIGNED) AS `type`, `owner`, `paid`, `warnings` FROM `houses`"); if (!result) { return false; } @@ -318,10 +318,10 @@ bool IOMapSerialize::saveHouseInfo() DBResult_ptr result = db.storeQuery(query.str()); if (result) { query.str(std::string()); - query << "UPDATE `houses` SET `owner` = " << house->getOwner() << ", `paid` = " << house->getPaidUntil() << ", `warnings` = " << house->getPayRentWarnings() << ", `name` = " << db.escapeString(house->getName()) << ", `town_id` = " << house->getTownId() << ", `rent` = " << house->getRent() << ", `size` = " << house->getTiles().size() << ", `beds` = " << house->getBedCount() << " WHERE `id` = " << house->getId(); + query << "UPDATE `houses` SET `owner` = " << house->getOwner() << ", `type` = " << house->getType() << ", `paid` = " << house->getPaidUntil() << ", `warnings` = " << house->getPayRentWarnings() << ", `name` = " << db.escapeString(house->getName()) << ", `town_id` = " << house->getTownId() << ", `rent` = " << house->getRent() << ", `size` = " << house->getTiles().size() << ", `beds` = " << house->getBedCount() << " WHERE `id` = " << house->getId(); } else { query.str(std::string()); - query << "INSERT INTO `houses` (`id`, `owner`, `paid`, `warnings`, `name`, `town_id`, `rent`, `size`, `beds`) VALUES (" << house->getId() << ',' << house->getOwner() << ',' << house->getPaidUntil() << ',' << house->getPayRentWarnings() << ',' << db.escapeString(house->getName()) << ',' << house->getTownId() << ',' << house->getRent() << ',' << house->getTiles().size() << ',' << house->getBedCount() << ')'; + query << "INSERT INTO `houses` (`id`, `type`, `owner`, `paid`, `warnings`, `name`, `town_id`, `rent`, `size`, `beds`) VALUES (" << house->getId() << ',' << house->getType() << ',' << house->getOwner() << ',' << house->getPaidUntil() << ',' << house->getPayRentWarnings() << ',' << db.escapeString(house->getName()) << ',' << house->getTownId() << ',' << house->getRent() << ',' << house->getTiles().size() << ',' << house->getBedCount() << ')'; } db.executeQuery(query.str()); diff --git a/src/luascript.cpp b/src/luascript.cpp index cb4f828926..6052055dce 100644 --- a/src/luascript.cpp +++ b/src/luascript.cpp @@ -1533,6 +1533,11 @@ void LuaScriptInterface::registerFunctions() registerEnum(ITEM_WILDGROWTH) registerEnum(ITEM_WILDGROWTH_PERSISTENT) registerEnum(ITEM_WILDGROWTH_SAFE) + registerEnum(ITEM_LETTER) + registerEnum(ITEM_LETTER_STAMPED) + registerEnum(ITEM_DOCUMENT_RO) + registerEnum(ITEM_RECEIPT_SUCCESS) + registerEnum(ITEM_RECEIPT_FAIL) registerEnum(PlayerFlag_CannotUseCombat) registerEnum(PlayerFlag_CannotAttackPlayer) @@ -1735,6 +1740,9 @@ void LuaScriptInterface::registerFunctions() // Use with house:getAccessList, house:setAccessList registerEnum(GUEST_LIST) registerEnum(SUBOWNER_LIST) + // Use with house:getType + registerEnum(HOUSE_TYPE_NORMAL) + registerEnum(HOUSE_TYPE_GUILDHALL) // Use with npc:setSpeechBubble registerEnum(SPEECHBUBBLE_NONE) @@ -1836,6 +1844,10 @@ void LuaScriptInterface::registerFunctions() registerEnum(RETURNVALUE_TRADEPLAYERHIGHESTBIDDER) registerEnum(RETURNVALUE_YOUCANNOTTRADETHISHOUSE) registerEnum(RETURNVALUE_YOUDONTHAVEREQUIREDPROFESSION) + registerEnum(RETURNVALUE_TRADEPLAYERNOTINAGUILD) + registerEnum(RETURNVALUE_TRADEGUILDALREADYOWNSAHOUSE) + registerEnum(RETURNVALUE_TRADEPLAYERNOTGUILDLEADER) + registerEnum(RETURNVALUE_YOUARENOTGUILDLEADER) registerEnum(RELOAD_TYPE_ALL) registerEnum(RELOAD_TYPE_ACTIONS) @@ -2510,6 +2522,12 @@ void LuaScriptInterface::registerFunctions() registerMethod("Guild", "getMotd", LuaScriptInterface::luaGuildGetMotd); registerMethod("Guild", "setMotd", LuaScriptInterface::luaGuildSetMotd); + registerMethod("Guild", "getBankBalance", LuaScriptInterface::luaGuildGetBankBalance); + registerMethod("Guild", "setBankBalance", LuaScriptInterface::luaGuildSetBankBalance); + + registerMethod("Guild", "getOwnerGUID", LuaScriptInterface::luaGuildGetOwnerGUID); + registerMethod("Guild", "getHouseId", LuaScriptInterface::luaGuildGetHouseId); + // Group registerClass("Group", "", LuaScriptInterface::luaGroupCreate); registerMetaMethod("Group", "__eq", LuaScriptInterface::luaUserdataCompare); @@ -2566,6 +2584,7 @@ void LuaScriptInterface::registerFunctions() registerMetaMethod("House", "__eq", LuaScriptInterface::luaUserdataCompare); registerMethod("House", "getId", LuaScriptInterface::luaHouseGetId); + registerMethod("House", "getType", LuaScriptInterface::luaHouseGetType); registerMethod("House", "getName", LuaScriptInterface::luaHouseGetName); registerMethod("House", "getTown", LuaScriptInterface::luaHouseGetTown); registerMethod("House", "getExitPosition", LuaScriptInterface::luaHouseGetExitPosition); @@ -2573,6 +2592,7 @@ void LuaScriptInterface::registerFunctions() registerMethod("House", "getOwnerGuid", LuaScriptInterface::luaHouseGetOwnerGuid); registerMethod("House", "setOwnerGuid", LuaScriptInterface::luaHouseSetOwnerGuid); + registerMethod("House", "getOwnerGuild", LuaScriptInterface::luaHouseGetOwnerGuild); registerMethod("House", "startTrade", LuaScriptInterface::luaHouseStartTrade); registerMethod("House", "getBeds", LuaScriptInterface::luaHouseGetBeds); @@ -10426,6 +10446,61 @@ int LuaScriptInterface::luaGuildSetMotd(lua_State* L) return 1; } +int LuaScriptInterface::luaGuildGetBankBalance(lua_State* L) +{ + // guild:getBankBalance() + Guild* guild = getUserdata(L, 1); + if (guild) { + lua_pushnumber(L, guild->getBankBalance()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGuildSetBankBalance(lua_State* L) +{ + // guild:setBankBalance(balance) + uint64_t balance = getNumber(L, 2); + Guild* guild = getUserdata(L, 1); + if (guild) { + guild->setBankBalance(balance); + pushBoolean(L, true); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGuildGetOwnerGUID(lua_State* L) +{ + // guild:getOwnerGUID() + Guild* guild = getUserdata(L, 1); + if (guild) { + lua_pushnumber(L, guild->getOwnerGUID()); + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaGuildGetHouseId(lua_State* L) +{ + // guild:getHouseId() + Guild* guild = getUserdata(L, 1); + if (guild) { + uint32_t houseId = guild->getHouseId(); + if (houseId > 0) { + lua_pushnumber(L, houseId); + } else { + lua_pushnil(L); + } + } else { + lua_pushnil(L); + } + return 1; +} + // Group int LuaScriptInterface::luaGroupCreate(lua_State* L) { @@ -10889,6 +10964,18 @@ int LuaScriptInterface::luaHouseGetId(lua_State* L) return 1; } +int LuaScriptInterface::luaHouseGetType(lua_State* L) +{ + // house:getType() + House* house = getUserdata(L, 1); + if (house) { + lua_pushnumber(L, house->getType()); + } else { + lua_pushnil(L); + } + return 1; +} + int LuaScriptInterface::luaHouseGetName(lua_State* L) { // house:getName() @@ -10949,7 +11036,27 @@ int LuaScriptInterface::luaHouseGetOwnerGuid(lua_State* L) // house:getOwnerGuid() House* house = getUserdata(L, 1); if (house) { - lua_pushnumber(L, house->getOwner()); + if (house->getType() == HOUSE_TYPE_NORMAL) { + lua_pushnumber(L, house->getOwner()); + } else { + lua_pushnil(L); + } + } else { + lua_pushnil(L); + } + return 1; +} + +int LuaScriptInterface::luaHouseGetOwnerGuild(lua_State* L) +{ + // house:getOwnerGuild() + House* house = getUserdata(L, 1); + if (house) { + if (house->getType() == HOUSE_TYPE_GUILDHALL) { + lua_pushnumber(L, house->getOwner()); + } else { + lua_pushnil(L); + } } else { lua_pushnil(L); } @@ -10961,9 +11068,22 @@ int LuaScriptInterface::luaHouseSetOwnerGuid(lua_State* L) // house:setOwnerGuid(guid[, updateDatabase = true]) House* house = getUserdata(L, 1); if (house) { - uint32_t guid = getNumber(L, 2); + uint32_t guid_guild = getNumber(L, 2); bool updateDatabase = getBoolean(L, 3, true); - house->setOwner(guid, updateDatabase); + // If house is guildhall and player happens to be in a guild + // We can pull the guild_id and pass it over to house->setOwner + if (house->getType() == HOUSE_TYPE_GUILDHALL) { + Database& db = Database::getInstance(); + std::ostringstream query; + query << "SELECT `guild_id` FROM `guild_membership` WHERE `player_id`=" << guid_guild; + if (DBResult_ptr result = db.storeQuery(query.str())) { + guid_guild = result->getNumber("guild_id"); + } else { + pushBoolean(L, false); + return 1; + } + } + house->setOwner(guid_guild, updateDatabase); pushBoolean(L, true); } else { lua_pushnil(L); @@ -10988,17 +11108,63 @@ int LuaScriptInterface::luaHouseStartTrade(lua_State* L) return 1; } - if (house->getOwner() != player->getGUID()) { + if (house->getType() == HOUSE_TYPE_NORMAL) { + if (house->getOwner() != player->getGUID()) { + lua_pushnumber(L, RETURNVALUE_YOUDONTOWNTHISHOUSE); + return 1; + } + + if (g_game.map.houses.getHouseByPlayerId(tradePartner->getGUID())) { + lua_pushnumber(L, RETURNVALUE_TRADEPLAYERALREADYOWNSAHOUSE); + return 1; + } + + if (IOLoginData::hasBiddedOnHouse(tradePartner->getGUID())) { + lua_pushnumber(L, RETURNVALUE_TRADEPLAYERHIGHESTBIDDER); + return 1; + } + + Item* transferItem = house->getTransferItem(); + if (!transferItem) { + lua_pushnumber(L, RETURNVALUE_YOUCANNOTTRADETHISHOUSE); + return 1; + } + + transferItem->getParent()->setParent(player); + if (!g_game.internalStartTrade(player, tradePartner, transferItem)) { + house->resetTransferItem(); + } + + lua_pushnumber(L, RETURNVALUE_NOERROR); + return 1; + } + + // HOUSE_TYPE_GUILDHALL + Guild* guild = player->getGuild(); + if (!guild || house->getOwner() != guild->getId()) { lua_pushnumber(L, RETURNVALUE_YOUDONTOWNTHISHOUSE); return 1; } - if (g_game.map.houses.getHouseByPlayerId(tradePartner->getGUID())) { - lua_pushnumber(L, RETURNVALUE_TRADEPLAYERALREADYOWNSAHOUSE); + Guild* tradeGuild = tradePartner->getGuild(); + if (!tradeGuild) { + lua_pushnumber(L, RETURNVALUE_TRADEPLAYERNOTINAGUILD); + return 1; + } + if (tradeGuild->getHouseId() > 0) { + lua_pushnumber(L, RETURNVALUE_TRADEGUILDALREADYOWNSAHOUSE); + return 1; + } + if (player->getGUID() != guild->getOwnerGUID()) { + lua_pushnumber(L, RETURNVALUE_YOUARENOTGUILDLEADER); + return 1; + } + if (tradePartner->getGUID() != tradeGuild->getOwnerGUID()) { + lua_pushnumber(L, RETURNVALUE_TRADEPLAYERNOTGUILDLEADER); return 1; } - if (IOLoginData::hasBiddedOnHouse(tradePartner->getGUID())) { + if (IOLoginData::hasBiddedOnHouse(tradeGuild->getId())) { lua_pushnumber(L, RETURNVALUE_TRADEPLAYERHIGHESTBIDDER); return 1; } diff --git a/src/luascript.h b/src/luascript.h index 8332b391de..afd41a636f 100644 --- a/src/luascript.h +++ b/src/luascript.h @@ -1049,6 +1049,12 @@ class LuaScriptInterface static int luaGuildGetMotd(lua_State* L); static int luaGuildSetMotd(lua_State* L); + static int luaGuildGetBankBalance(lua_State* L); + static int luaGuildSetBankBalance(lua_State* L); + + static int luaGuildGetOwnerGUID(lua_State* L); + static int luaGuildGetHouseId(lua_State* L); + // Group static int luaGroupCreate(lua_State* L); @@ -1101,12 +1107,14 @@ class LuaScriptInterface static int luaHouseCreate(lua_State* L); static int luaHouseGetId(lua_State* L); + static int luaHouseGetType(lua_State* L); static int luaHouseGetName(lua_State* L); static int luaHouseGetTown(lua_State* L); static int luaHouseGetExitPosition(lua_State* L); static int luaHouseGetRent(lua_State* L); static int luaHouseGetOwnerGuid(lua_State* L); + static int luaHouseGetOwnerGuild(lua_State* L); static int luaHouseSetOwnerGuid(lua_State* L); static int luaHouseStartTrade(lua_State* L); diff --git a/src/otserv.cpp b/src/otserv.cpp index e92c4e9fde..344c196f42 100644 --- a/src/otserv.cpp +++ b/src/otserv.cpp @@ -303,6 +303,8 @@ void mainLoader(int, char*[], ServiceManager* services) rentPeriod = RENTPERIOD_MONTHLY; } else if (strRentPeriod == "daily") { rentPeriod = RENTPERIOD_DAILY; + } else if (strRentPeriod == "dev") { + rentPeriod = RENTPERIOD_DEV; } else { rentPeriod = RENTPERIOD_NEVER; } diff --git a/src/tools.cpp b/src/tools.cpp index a95f18862e..d92fe2c8ae 100644 --- a/src/tools.cpp +++ b/src/tools.cpp @@ -1213,6 +1213,18 @@ const char* getReturnMessage(ReturnValue value) case RETURNVALUE_TRADEPLAYERALREADYOWNSAHOUSE: return "Trade player already owns a house."; + case RETURNVALUE_TRADEPLAYERNOTINAGUILD: + return "Trade player is not in a guild."; + + case RETURNVALUE_TRADEGUILDALREADYOWNSAHOUSE: + return "Trade guild already owns a guildhall."; + + case RETURNVALUE_TRADEPLAYERNOTGUILDLEADER: + return "Trade player is not a guild leader."; + + case RETURNVALUE_YOUARENOTGUILDLEADER: + return "You are not a guild leader."; + case RETURNVALUE_TRADEPLAYERHIGHESTBIDDER: return "Trade player is currently the highest bidder of an auctioned house.";