From 1234b3a3491921a65430f5448c0e28d5cc9df869 Mon Sep 17 00:00:00 2001 From: brightrim Date: Mon, 19 Feb 2024 20:41:00 +0100 Subject: [PATCH] Housing Demolition #358 --- content/messenger.lua | 8 +- gm/items/id_99_lockpicks.lua | 26 +-- housing/utility.lua | 122 ++++++++++-- item/builderstool.lua | 31 +++- scheduled/housing_demolition.lua | 307 +++++++++++++++++++++++++++++++ 5 files changed, 460 insertions(+), 34 deletions(-) create mode 100644 scheduled/housing_demolition.lua diff --git a/content/messenger.lua b/content/messenger.lua index d046d49ac..7b7493eb2 100644 --- a/content/messenger.lua +++ b/content/messenger.lua @@ -63,7 +63,7 @@ local function convertContentsIntoString(contents) for index, message in pairs(contents) do if not message.sender then - message.sender = "script" + return false --script message not to be logged to avoid spam in the log end retString = retString.." (Sender "..index..": "..message.sender.." Message"..index..": "..garbleTheMessage(message.text)..")" end @@ -84,7 +84,11 @@ local function logThatMessagesWereReceived(recipient, contents) loggedMessage = loggedMessage..convertContentsIntoString(contents) - log(loggedMessage) + if loggedMessage then + + log(loggedMessage) + + end end local function sendPlayerMessages(numberOfMessages, recipient) diff --git a/gm/items/id_99_lockpicks.lua b/gm/items/id_99_lockpicks.lua index 9d987d9fa..3626821fd 100644 --- a/gm/items/id_99_lockpicks.lua +++ b/gm/items/id_99_lockpicks.lua @@ -1833,31 +1833,31 @@ function M.UseItem(user, SourceItem, ltstate) teleporter(user, SourceItem) elseif index == 3 then summon(user, SourceItem) - elseif index == 3 then - godMode(user, SourceItem, ltstate) elseif index == 4 then - settingsForChar(user) + godMode(user, SourceItem, ltstate) elseif index == 5 then - ambientAction(user) + settingsForChar(user) elseif index == 6 then - actionOnChar(user, SourceItem) + ambientAction(user) elseif index == 7 then - actionOnGroup(user, SourceItem) + actionOnChar(user, SourceItem) elseif index == 8 then - factionInfoOfCharsInRadius(user, SourceItem, ltstate) + actionOnGroup(user, SourceItem) elseif index == 9 then - questEvents(user, SourceItem, ltstate) + factionInfoOfCharsInRadius(user, SourceItem, ltstate) elseif index == 10 then - setuserTeleporter(user, SourceItem) + questEvents(user, SourceItem, ltstate) elseif index == 11 then - setuserActionOnChar(user, SourceItem) + setuserTeleporter(user, SourceItem) elseif index == 12 then - setuserActionOnGroup(user, SourceItem) + setuserActionOnChar(user, SourceItem) elseif index == 13 then - testArea(user) + setuserActionOnGroup(user, SourceItem) elseif index == 14 then - resetTutorial(user) + testArea(user) elseif index == 15 then + resetTutorial(user) + elseif index == 16 then changePersistence(user) end end diff --git a/housing/utility.lua b/housing/utility.lua index 2710e2202..1f24983d6 100644 --- a/housing/utility.lua +++ b/housing/utility.lua @@ -412,6 +412,44 @@ function M.checkIfWallOrWindow(user, suspectedWall) return false end +function M.isBuilder(user) + + local frontPos = common.GetFrontPosition(user) + + local propertyName = M.fetchPropertyName(user, frontPos) + + local deed = M.getPropertyDeed(propertyName) + + for i = 1, M.max_builder_number do + + local builderID = deed:getData("builderID"..i) + + if builderID ~= "" and tonumber(builderID) == user.id then + return true + end + end + + return false +end + +function M.isTenant(user) + + local frontPos = common.GetFrontPosition(user) + + local propertyName = M.fetchPropertyName(user, frontPos) + + local deed = M.getPropertyDeed(propertyName) + + local tenantID = deed:getData("tenantID") + + if tenantID ~= "" and tonumber(tenantID) == user.id then + return true + end + + return false +end + + function M.allowBuilding(user, alternatePosition) @@ -422,6 +460,11 @@ function M.allowBuilding(user, alternatePosition) local propertyName = M.fetchPropertyName(user, frontPos) local deed = M.getPropertyDeed(propertyName) + if deed:getData("demolishmentInProgress") == "true" then + user:inform("GERMAN TRANSLATION", "You can not build at an estate that is being demolished.") + return false + end + if M.fetchPropertyName(user, frontPos) then local tenantID = deed:getData("tenantID") @@ -577,7 +620,6 @@ end function M.setPersistenceForProperties() for _, property in pairs(propertyList.properties) do - log("Setting persistence for "..property.name) for x = property.lower.x, property.upper.x do for y = property.lower.y, property.upper.y do for z = property.lower.z, property.upper.z do @@ -587,19 +629,14 @@ function M.setPersistenceForProperties() end end end - log("Done setting persistence for "..property.name) end - log("Creating estate basements") M.createEstateBasements() -- in case any estates are lacking basement tiles and walls, this function will fix that - log("Done creating estate basements") - log("Now checking that all property deeds are where they should be") for i = 1, #propertyList.propertyTable do local location = propertyList.propertyTable[i][3] if not world:isPersistentAt(location) then - log("Setting persistence for property deed belonging to "..propertyList.propertyTable[i][1]) world:makePersistentAt(location) end @@ -614,7 +651,6 @@ function M.setPersistenceForProperties() end if not propertyDeedFound then - log("Creating missing property deed for property "..propertyList.propertyTable[i][1]) world:createItemFromId(3772, 1, location, true, 333, nil) --This will lead to some deeds that should be 3773 facing the wrong direction, but this is also only ever called if one is missing to begin with when a GM sets persistence end end @@ -649,6 +685,74 @@ function M.createLock(user) end end +local function scheduleDemolishment(user, propertyName) + + local callback = function(dialog) + + if not dialog:getSuccess() then + return + end + + local selected = dialog:getSelectedIndex()+1 + + if selected == 1 then + local deed = M.getPropertyDeed(propertyName) + deed:setData("demolish", "true") --Used to determine which property will be demolished when the scheduled script triggers + + -- Below is a little hack since we do not use global variables, to not have to check every single property each time if there are none scheduled at all + + local pauldronDeed = M.getPropertyDeed("Pauldron Estate") -- refresh in case the deed was changed above as one of the demolished properties + + if deed.pos == pauldronDeed.pos then + deed:setData("newDemolishments", "true") + else + pauldronDeed:setData("newDemolishments", "true") + world:changeItem(pauldronDeed) + end + + -- end of the hack + + world:changeItem(deed) + + user:inform("GERMAN TRANSLATION", "Upon your request of a demolition, a bunch of dwarves appear to demolish the estate. One by one they scurry off with the broken down material as their payment, at this rate they should be done in no time. ") + else + return + end + end + + local dialog = SelectionDialog(common.GetNLS(user,"Bestätigung","Confirmation Check"), common.GetNLS(user,"GERMAN TRANSLATION", "Are you certain that you are absolutely positively sure you want to go through with this? This is the final warning. The demolition can not be stopped or undone after this, and will destroy everything on your property including items you made static or items that are not static!"), callback) + dialog:addOption(0,common.GetNLS(user,"Ja","Yes")) + dialog:addOption(0,common.GetNLS(user,"Nein, das ist Kunst.","No, I changed my mind.")) + dialog:setCloseOnMove() + user:requestSelectionDialog(dialog) +end + +function M.demolishConfirmation(user, propertyName) + + local callback = function(dialog) + + if not dialog:getSuccess() then + return + end + + local selected = dialog:getSelectedIndex()+1 + + if selected == 1 then + scheduleDemolishment(user, propertyName) + else + return + end + end + + local dialog = SelectionDialog(common.GetNLS(user,"Bestätigung","Confirmation Check"), common.GetNLS(user,"GERMAN TRANSLATION", "Are you certain you want to demolish your estate property? This can not be undone and once initiated, it can not stop."), callback) + dialog:addOption(0,common.GetNLS(user,"Ja","Yes")) + dialog:addOption(0,common.GetNLS(user,"Nein, das ist Kunst.","No, I changed my mind.")) + dialog:setCloseOnMove() + user:requestSelectionDialog(dialog) + +end + + function M.createKey(user) local propertyName = M.fetchPropertyName(user) @@ -963,9 +1067,7 @@ function M.createEstateBasements() for _, property in pairs(basementProperties) do local tileID, wallID = M.getBasementTileWall(property.name) if property.lower.z == -21 then --only for basement layers - log("Creating basement walls for property "..property.name) placeBasementWalls(wallID, property.lower, property.upper) - log("Creating basement tiles for property "..property.name) placeBasementTiles(tileID, property.lower, property.upper) end end @@ -1167,7 +1269,6 @@ function M.deletePreviewItem(propertyName, bypassTTL) local propertyDeed = M.getPropertyDeed(propertyName) if propertyDeed == nil then - log("The property deed for "..propertyName.." is missing from the map, causing script errors.") return end @@ -1244,7 +1345,6 @@ function M.ifNoSurroundingTilesDeleteStairTiles(tile1pos, tile2pos) local field = world:getField(thePos) local tileID = field:tile() if tileID ~= 0 then - log("tileID: "..tostring(tileID).." at position: "..tostring(thePos)) deleteTiles = false break end diff --git a/item/builderstool.lua b/item/builderstool.lua index 9faf73842..16447bcca 100644 --- a/item/builderstool.lua +++ b/item/builderstool.lua @@ -48,7 +48,6 @@ local function carpentrySelection(user) user:requestSelectionDialog(dialog) end - local function craftSelection(user) local skills = utility.getSkillsToShow(user) @@ -72,7 +71,9 @@ local function craftSelection(user) user:requestSelectionDialog(dialog) end -local function destroySelection(user ) +local function destroySelection(user) + + local propertyName = utility.fetchPropertyName(user) local callback = function(dialog) local success = dialog:getSuccess() @@ -84,8 +85,10 @@ local function destroySelection(user ) utility.destroyTile(user) elseif selected == 3 then utility.deleteRoofItemOrTile(user, false) - else + elseif selected == 4 then utility.deleteRoofItemOrTile(user, true) + elseif selected == 5 then + utility.demolishConfirmation(user, propertyName) end end end @@ -94,8 +97,14 @@ local function destroySelection(user ) dialog:addOption(0,common.GetNLS(user,"Schindeln","Tiles")) dialog:addOption(0,common.GetNLS(user,"Dachausrüstung","Roof Objects")) dialog:addOption(0,common.GetNLS(user,"Dachschindeln","Roof Tiles")) + dialog:addOption(0, common.GetNLS(user, "GERMAN TRANSLATION", "Demolish the entire estate")) dialog:setCloseOnMove() - user:requestSelectionDialog(dialog) + + if utility.checkIfEstate(user) then + user:requestSelectionDialog(dialog) + else + utility.destroyItem(user) + end end local function miscDialog(user) @@ -219,14 +228,20 @@ local function mainDialog(user, sourceItem) dialog:setCloseOnMove() - user:requestSelectionDialog(dialog) + if utility.isTenant(user) then + + user:requestSelectionDialog(dialog) + + elseif utility.isBuilder(user) then + + craftSelection(user, sourceItem) + + end end function M.UseItem(user, sourceItem) - local thePosition = common.GetFrontPosition(user) --To prevent cheating by turning, the position is set at the very start(dialogue can only be set to close on movement) - if not utility.checkIfIsInHand(user, sourceItem) then return end @@ -236,7 +251,7 @@ function M.UseItem(user, sourceItem) if not propertyName then user:inform("Du kannst nicht außerhalb eines Grundstückes bauen.","You can't build outside of property land.") elseif utility.allowBuilding(user) then - mainDialog(user, sourceItem, thePosition) + mainDialog(user, sourceItem) else user:inform("Du musst ein Bewohner sein, um hier bauen zu dürfen, oder eine Genehmigung haben.","To build here you must be the tenant of the property or have their permission.") end diff --git a/scheduled/housing_demolition.lua b/scheduled/housing_demolition.lua new file mode 100644 index 000000000..e88eed904 --- /dev/null +++ b/scheduled/housing_demolition.lua @@ -0,0 +1,307 @@ +--[[ +Illarion Server + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU Affero General Public License as published by the Free +Software Foundation, either version 3 of the License, or (at your option) any +later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU Affero General Public License for more +details. + +You should have received a copy of the GNU Affero General Public License along +with this program. If not, see . +]] + +-- Called upon every 10 seconds + +local propertyList = require("housing.propertyList") +local utility = require("housing.utility") + +local M = {} + +M.queuedDemolishments = {} + +local function addPropertyToBeDemolishedToListAndRemoveWarps(propertyName) + + local deed = utility.getPropertyDeed(propertyName) + + if deed:getData("demolish") ~= "true" then -- This is a string because item data can not be a boolean + return -- Not scheduled for demolishment! + end + + deed:setData("demolish", "false") --We no longer need to know if it is to be demolished, so we remove this for the next time it gets checked + deed:setData("demolishmentInProgress", "true") -- Value used after initiation of demolishment to prevent building new items, particularly stairs that could lead to warps without stairs + world:changeItem(deed) + + local propertyInfo = { + time = 3000, -- The time it should take in total to demolish any property. It is set here instead of statically as it gets used as a countdown value. + name = propertyName, + layers = { + roof = { + items = {}, + tiles = {} + }, + upper = { + items = {}, + tiles = {} + }, + ground = { + items = {} + }, + basement = { + items = {} + } + } + } + + for _, propertyToCheck in pairs(propertyList.properties) do + if propertyToCheck.name == propertyName then + if propertyToCheck.lower.z ~= -21 then -- Basement done separately as it can have different coords + for x = propertyToCheck.lower.x, propertyToCheck.upper.x do -- Check each x coord + for y = propertyToCheck.lower.y, propertyToCheck.upper.y do --Check each y coord for each x coord + + local theFieldGround = world:getField(position(x, y, 0)) + local theFieldUpper = world:getField(position(x, y, 1)) + local theFieldRoof = world:getField(position(x, y, 2)) + + if theFieldGround:isWarp() then + theFieldGround:removeWarp() + end + + if theFieldUpper:isWarp() then + theFieldUpper:removeWarp() + end + + if theFieldRoof:isWarp() then + theFieldRoof:removeWarp() + end + + local tileIdRoof = theFieldRoof:tile() + local tileIdUpper = theFieldUpper:tile() + + if theFieldRoof:countItems() > 0 then + table.insert(propertyInfo.layers.roof.items, {amount = theFieldRoof:countItems(), pos = position(x, y, 2)}) + end + + if theFieldUpper:countItems() > 0 then + table.insert(propertyInfo.layers.upper.items, {amount = theFieldUpper:countItems(), pos = position(x, y, 1)}) + end + + if theFieldGround:countItems() > 0 then + table.insert(propertyInfo.layers.ground.items, {amount = theFieldGround:countItems(), pos = position(x, y, 0)}) + end + + if tileIdRoof ~= 34 then + table.insert(propertyInfo.layers.roof.tiles, {field = theFieldRoof, pos = position(x, y, 2)}) + end + + if tileIdUpper ~= 34 then + table.insert(propertyInfo.layers.upper.tiles, {field = theFieldUpper, pos = position(x, y, 1)}) + end + end + end + elseif propertyToCheck.lower.z == -21 then + for x = propertyToCheck.lower.x, propertyToCheck.upper.x do -- Check each x coord + for y = propertyToCheck.lower.y, propertyToCheck.upper.y do --Check each y coord for each x coord + local theField = world:getField(position(x, y, -21)) + local itemsOnField = theField:countItems() + + if theField:isWarp() then + theField:removeWarp() + end + + if itemsOnField > 0 then + table.insert(propertyInfo.layers.basement.items, {amount = itemsOnField, pos = position(x, y, -21)}) + end + end + end + end + end + end + + table.insert(M.queuedDemolishments, propertyInfo) +end + +local function deleteBasementItems(property) + + if #property.layers.basement.items < 0 then + return + end + + for index , basementItem in pairs(property.layers.basement.items) do + + for i = 1, basementItem.amount do + if world:isItemOnField(basementItem.pos) then + local theItem = world:getItemOnField(basementItem.pos) + world:erase(theItem, theItem.number) + end + end + + table.remove(property.layers.basement.items, index) + end + +end + +local function gfxSfxOnLocation(location) + + local rand = math.random() + + if rand > 0.90 then -- 10% chance + world:gfx(12, location) -- Cloud smoke + end + + if rand < 0.10 then -- 10% chance + world:makeSound(6, location) -- Chopping sound + end + + if rand > 0.10 and rand < 0.20 then -- 10% chance + world:makeSound(18, location) -- Mining sound + end +end + +local function deleteItemsOnFloor(property, toDestroy, layer) + + local amountOfItems = #property.layers[layer].items + + if amountOfItems == 0 or toDestroy == 0 then --No items to destroy or none planned until next cycle of the script + return toDestroy + end + + local howManyToDestroy = math.min(amountOfItems, toDestroy) -- We choose whichever is smaller, the amount of items we want to destroy or the amount remaining + + local destroyed = 0 + + for index, itemOnLayer in pairs(property.layers[layer].items) do + + for i = 1, itemOnLayer.amount do + + if howManyToDestroy == destroyed then + return toDestroy + end + + if world:isItemOnField(itemOnLayer.pos) then -- Double checks that item is still there + local theItem = world:getItemOnField(itemOnLayer.pos) + world:erase(theItem, theItem.number) + destroyed = destroyed + 1 + gfxSfxOnLocation(itemOnLayer.pos) + toDestroy = toDestroy - 1 + end + + table.remove(property.layers[layer].items, index) + + end + end + + return toDestroy + +end + +local function deleteTilesOnFloor(property, toDestroy, layer) + + local amountOfTiles = #property.layers[layer].tiles + + if amountOfTiles == 0 or toDestroy == 0 then --No tiles to destroy or none planned until next cycle of the script + return toDestroy + end + + local howManyToDestroy = math.min(amountOfTiles, toDestroy) -- We choose whichever is smaller, the amount of items we want to destroy or the amount remaining + + local destroyed = 0 + + for index, tileOnLayer in pairs(property.layers[layer].tiles) do + + if howManyToDestroy == destroyed then + return toDestroy + end + + local tileId = tileOnLayer.field:tile() + + if tileId ~= 0 then + world:changeTile(0, tileOnLayer.pos) + destroyed = destroyed + 1 + gfxSfxOnLocation(tileOnLayer.pos) + toDestroy = toDestroy - 1 + end + + table.remove(property.layers[layer].tiles, index) + + end + + return toDestroy +end + +function M.demolish() + + local pauldronDeed = utility.getPropertyDeed("Pauldron Estate") -- using the pauldron deed as a bit of a hack to store a variable instead of using the database, to avoid lag, since we are not allowed to use global variables + + if pauldronDeed:getData("newDemolishments") == "true" then -- Any new demolishments scheduled? + + for i = 1, #propertyList.propertyTable do --This table only lists each property once, so we use it to cycle through all the properties + + local propertyName = propertyList.propertyTable[i][1] + + addPropertyToBeDemolishedToListAndRemoveWarps(propertyName) + end + + pauldronDeed = utility.getPropertyDeed("Pauldron Estate") -- refreshed in case it was set for demolishment above so we do not overwrite any of that + + pauldronDeed:setData("newDemolishments", "false") -- This ensures the script does not strain the server, only going through all properties if we know at least one is scheduled for demolishment + + world:changeItem(pauldronDeed) + + end + + for index, property in pairs(M.queuedDemolishments) do --Anything beyond this point only happens if an estate is actually scheduled for demolition + + deleteBasementItems(property) + + local timeLeft = property.time + + local totalDestructionLeft = #property.layers.roof.tiles + #property.layers.roof.items + #property.layers.upper.tiles + #property.layers.upper.items + #property.layers.ground.items + + if totalDestructionLeft == 0 then -- All done, remove it from the list + table.remove(M.queuedDemolishments, index) + local deed = utility.getPropertyDeed(property.name) + deed:setData("demolishmentInProgress", "false") + world:changeItem(deed) + return + end + + local toDestroy = totalDestructionLeft/timeLeft*10 -- The amount of objects to destroy divided by the time left and multiplied by the time it takes for how often this script triggers + + if toDestroy < 1 then -- At least one item destroyed each cycle + toDestroy = 1 + else + toDestroy = math.floor(toDestroy) + end + + toDestroy = deleteItemsOnFloor(property, toDestroy, "roof") + + toDestroy = deleteTilesOnFloor(property, toDestroy, "roof") + + toDestroy = deleteItemsOnFloor(property, toDestroy, "upper") + + toDestroy = deleteTilesOnFloor(property, toDestroy, "upper") + + toDestroy = deleteItemsOnFloor(property, toDestroy,"ground") + + if toDestroy > 0 then --Something must have been removed by other means while destruction was ongoing, so we remove it from the schedule + table.remove(M.queuedDemolishments, index) + local deed = utility.getPropertyDeed(property.name) + deed:setData("demolishmentInProgress", "false") + world:changeItem(deed) + return + end + + property.time = property.time - 10 + + M.queuedDemolishments[index] = property + + end +end + + +return M