--[[ Zone Recycle Bin -- ====================================================================== ** Author: Catspaw (CatspawMods @ ModDB) ** Source: https://www.moddb.com/members/catspawmods/addons ** Version: 1.4 ** Updated: 20230625 You may modify any part of this mod and do whatever you like with it, just give credit where due. Most of this file unavoidably duplicates the majority of Alundaio's vanilla script so that the Recycle Bin will behave just like any other placeable stash interaction. ANY AND ALL ITEMS PLACED INSIDE THE CONTAINER WILL BE PERMANENTLY DESTROYED AFTER A SHORT DELAY. Credits: * Alundaio and Tronex for the vanilla backpack script * TB's New Stash Locations for the bin's in-game model (from GSC) * PYP for the smoothing fix to GSC's vanilla model * Asametar for RUS and UKR localization * artifax for the workshop autoinject script * demonized for the time events If another addon also has a copy of the same workshop or time events scripts, it should not matter whose you use. -- ====================================================================== To uninstall without MCM, first: 1. Pick up any placed Zone Recycle Bins and save your game 2. Quit out and set the following line to true: --]] local uninstall = false -- 3. Then re-launch the game, load, and make a new separate save -- 4. It is then safe to uninstall this addon--the new save is clean local trashcan_deletion_timeout = 30 -- After this many seconds in the trashcan, an item is gone forever! -- Removing an item from the container before the timeout will prevent -- its destruction, but I don't recommend playing chicken with this fact. -- Configurable through MCM, if you have it. -- ====================================================================== local started = false local debuglogs = false -- Debug logging enabled local verbose = true -- Verbose logging (noisy!) local killswitch = false -- Disables all callbacks (for debugging/testing) local deletion_enabled = true -- If false, nothing is actually deleted but everything works (for debugging) local show_mapspots = false -- If true, show a special map icon for the Recycle Bin local mollyguard = true -- If true, the player will get warned with shouty all-caps messages about how deleting stuff works local add_new_recipe = true -- If true, the mod will add a crafting recipe for the Recycle Bin local recipe_added = false -- Flag to prevent dupe recipes from being added local gimme_one = false -- If true, the script will spawn a copy of the bin in the player's -- inventory. It will do this every single time the game loads if you -- hardcode it here, so I recommend using MCM instead. -- ====================================================================== -- Messing with these string constants will only bring you sadness -- ====================================================================== local mapspot_string = "trash" local bin_placeable = "itm_placeable_trashcan" local bin_worlditem = "inv_trashcan" local queue_name = "delete_trash_" local CreateTimeEvent = demonized_time_events.CreateTimeEvent local RemoveTimeEvent = demonized_time_events.RemoveTimeEvent -- ====================================================================== -- Definitions for the optional crafting recipe. -- Valid IDs for parts are found in configs\items\settings\parts.ltx -- ====================================================================== local req_special_mat = false -- If you want to make the bin require rare materials to craft local special_mat = "af_death_lamp" -- can be any valid item local bin_craft_string = { ["itm_placeable_trashcan"] = { index = 2, -- Workshop tab tier = "1", -- basic tools recipe = "recipe_basic_0", -- no recipe requirement mats = { [1] = {mat="prt_i_scrap",ct="10"}, -- def: 10 metal scrap [2] = {mat="prt_i_fasteners",ct="4"}, -- def: 4 fasteners -- Up to four ingredients allowed -- fourth will be overridden if req_special_mat is enabled } } } local last_interaction = time_global() local garbage_interval = 360 trash_by_trash_id = {} trash_by_story_id = {} -- ====================================================================== local scriptname = "" function dl(logtext,...) -- Debug logging local prefix = scriptname..": " if logtext and debuglogs then --if verbose then -- local cf = debug.getinfo(2, 'n').name or "" --prefix = scriptname.."."..cf.."[V]: " --end printf(prefix..logtext,...) end end function vl(logtext,...) -- Verbose logging if verbose and debuglogs then dl(logtext,...) end end function check_new_recipe(enabled,item,customstring) if enabled and item then if not recipe_added then local recipe_string = customstring or "1,recipe_basic_0,prt_i_scrap,1" local bcs = bin_craft_string[item] or nil if bcs and not customstring then local rs = "" if req_special_mat then bcs.mats[4] = {mat=special_mat,ct="1"} end for k,v in pairs(bcs.mats) do if k then local bm = bcs.mats[k] local mat = bm.mat or "" local ct = bm.ct or "" rs = rs..","..mat..","..ct end end recipe_string = bcs.tier..","..bcs.recipe..rs dl("Trying to inject new recipe for %s in workshop tab %s: %s",item,index,recipe_string) workshop_autoinject.add_new_recipe(bcs.index,item,recipe_string) end recipe_added = true end end end function clear_trash_record(id_or_tid) vl("clear_trash_record(%s) called",id_or_tid) if not id_or_tid then return end local trash,trash_id,id if type(id_or_tid) == "number" then trash = trash_by_story_id id = id_or_tid trash_id = trash and trash[id] and trash[id].trash_id elseif type(id_or_tid) == "string" then trash = trash_by_trash_id trash_id = id_or_tid id = trash and trash[trash_id] and trash[trash_id].id else return end if id and trash_by_story_id[id] then vl("clearing record for trash_by_story_id[%s]",id) trash_by_story_id[id] = nil end if trash_id and trash_by_trash_id[trash_id] then vl("clearing record for trash_by_trash_id[%s]",trash_id) trash_by_trash_id[trash_id] = nil end local obj = get_object_by_id(id) local name = obj and obj:name() or "unknown" dl("%s (%s) removed from %s, removing %s from deletion queue",name,id,bin_worlditem,trash_id) return true end function delete_item(obj,trash_id,uninstall) local id if not obj then id = trash_by_trash_id[trash_id] and trash_by_trash_id[trash_id].id if id then obj = get_object_by_id(id) if not obj then dl("WARNING: no valid object or trash_id passed to delete_item!") return end end else id = obj:id() end if deletion_enabled or uninstall then if uninstall then dl("Uninstall: deleting copy of %s",bin_placeable) else dl("%s timed out, deleting id %s from recycle bin",obj:name(),id) end local se_obj = alife_object(id) local success = alife_release_id(id) if success then dl("trash_id %s | obj id %s released successfully",id,trash_id) clear_trash_record(id) else dl("WARNING: object id %s could not be released for some reason!",id) end else dl("deletion_enabled was set to %s, nothing done with id %s",deletion_enabled,id) clear_trash_record(id) end return true end function process_queued_item(obj,trash_id) if trash_id and trash_by_trash_id and trash_by_trash_id[trash_id] then vl("queued item %s still has trash record %s, passing to delete_item",obj and obj:id(),trash_id) delete_item(obj,trash_id) end return true end function queue_item_deletion(obj) if obj and deletion_enabled then local tg = time_global() local id = obj:id() local trash_id = tostring(tg).."_"..tostring(id).."_"..tostring(math.random(1,1000)) trash_by_trash_id[trash_id] = {id = id,added = tg} trash_by_story_id[id] = {} trash_by_story_id[id].trash_id = trash_id dl("Actor put item %s in trash can, queueing for deletion at %s with trash_id %s",obj:section(),tg,trash_id) CreateTimeEvent("trashcan_item_deletion",queue_name..trash_id,trashcan_deletion_timeout,process_queued_item,obj,trash_id) else dl("deletion_enabled is %s, %s not queued for deletion",deletion_enabled,obj:section()) end end function get_trash_record(id_or_tid,tbl) if not id_or_tid then return end local trash = tbl or nil if type(id_or_tid) == "number" then trash = trash or trash_by_story_id elseif type(id_or_tid) == "string" then trash = trash or trash_by_trash_id else return end return id_or_tid and trash and trash[id_or_tid] end function garbage_collection() if trash_by_trash_id and not is_empty(trash_by_trash_id) then for trash_id,td in pairs(trash_by_trash_id) do local id = td and td.id local obj if id then obj = get_object_by_id(id) end if obj then queue_item_deletion(obj,trash_id) dl("TID %s: unprocessed trash for id %s, queueing for deletion") else trash_by_trash_id[trash_id] = nil dl("TID %s: no object for id %s, clearing trash record") end end CreateTimeEvent("zrb_garbage_collection",0,garbage_interval,garbage_collection) end return true end function uninstall_cleanup() local function search(temp , item) if item:section() == bin_placeable then delete_item(item,nil,true) end end if started then db.actor:iterate_inventory(search,nil) end uninstall = false return true end function menu_stash(obj) -- return context menu "use" name local p = obj:parent() if not (p and p:id() == AC_ID) then return end vl("player right-clicked on %s",obj:section()) if obj and (obj:section() == bin_placeable) then return game.translate_string("st_place_trashcan") end end function func_stash(obj) start(obj) end function on_option_change() if ui_mcm then -- If you have MCM, if will override the hardcoded settings deletion_enabled = ui_mcm.get("zrb/mod_enabled") trashcan_deletion_timeout = ui_mcm.get("zrb/deletion_timeout") show_mapspots = ui_mcm.get("zrb/use_mapspots") mollyguard = ui_mcm.get("zrb/warn_enabled") add_new_recipe = ui_mcm.get("zrb/add_recipe") gimme_one = ui_mcm.get("zrb/spawn_bin") uninstall = ui_mcm.get("zrb/uninstall") dl("MCM options synced, enabled=%s. | timeout %s | mapspots: %s | warn: %s | gimme: %s",deletion_enabled,trashcan_deletion_timeout,show_mapspots,mollyguard,gimme_one) end if gimme_one then dl("Giving a free Recycle Bin to the player") alife_create_item(bin_placeable, db.actor) gimme_one = false ui_mcm.set("zrb/spawn_bin",gimme_one) end if uninstall then dl("Removing all copies of the Recycle Bin from player's inventory") uninstall_cleanup() ui_mcm.set("zrb/uninstall",false) end check_new_recipe(add_new_recipe,bin_placeable) Unregister_UI("UICreateTrash") end function actor_on_item_put_in_box(box,obj) if (box:section() == bin_worlditem) then last_interaction = time_global() queue_item_deletion(obj) end end function actor_on_item_take_from_box(box,obj) if box:section() ~= bin_worlditem then return end local id = obj:id() local name = obj:name() dl("Player took %s (%s) from %s, processing",name,id,bin_worlditem) last_interaction = time_global() if (box:is_inv_box_empty()) then hide_hud_inventory() local box_id = box:id() local data = { stash_id = box_id, cancel = false, } SendScriptCallback("actor_on_stash_remove",data) if data.cancel then dl("stash removal for %s canceled by another script",box_id) else if show_mapspots then level.map_remove_object_spot(box_id, mapspot_string) end local se_obj = alife_object(box_id) if se_obj then alife_release(se_obj) end local m_data = alife_storage_manager.get_state() if (m_data.player_created_stashes and m_data.player_created_stashes[box_id]) then local section = m_data.player_created_stashes[box_id] alife_create_item(section, db.actor) m_data.player_created_stashes[box_id] = nil end end end if trash_by_story_id[id] then local trash_id = trash_by_story_id[id].trash_id if trash_id then RemoveTimeEvent("trashcan_item_deletion",queue_name..trash_id) end clear_trash_record(id) end end function actor_on_item_use(obj) func_stash() end function actor_on_first_update() on_option_change() started = true garbage_collection() end function load_state(data) if not data and data.zrb_data then return end local zrb_data = data and data.zrb_data trash_by_trash_id = (zrb_data and zrb_data.trash_by_trash_id) or {} end function save_state(data) if trash_by_trash_id and not is_empty(trash_by_trash_id) then local zrb_data = {trash_by_trash_id = trash_by_trash_id} data.zrb_data = zrb_data end end function on_game_start() if not killswitch then RegisterScriptCallback("actor_on_item_take_from_box",actor_on_item_take_from_box) RegisterScriptCallback("actor_on_item_put_in_box",actor_on_item_put_in_box) RegisterScriptCallback("actor_on_item_use",actor_on_item_use) RegisterScriptCallback("actor_on_first_update",actor_on_first_update) RegisterScriptCallback("on_option_change",on_option_change) RegisterScriptCallback("save_state",save_state) RegisterScriptCallback("load_state",load_state) end end --[[======================================================================= BACKPACK CODE Everything that follows is copied directly from the vanilla item_backpack.script file. For the most part, the only real changes have been strings and IDs, like changing the section "inv_backpack" to "inv_trashcan", etc. Could probably get rid of nearly all of this if we didn't care about the player being able to name the bin's mapspot. --=====================================================================--]] GUI = nil -- instance, don't touch function start(obj) if (not obj) then return end hide_hud_inventory() if (not GUI) then GUI = UICreateTrash() end if (GUI) and (not GUI:IsShown()) then GUI:ShowDialog(true) GUI:Reset(obj) Register_UI("UICreateTrash","item_trashcan") end end class "UICreateTrash" (CUIScriptWnd) function UICreateTrash:__init() super() self:InitControls() self:InitCallBacks() end function UICreateTrash:__finalize() end function UICreateTrash:InitControls() self:SetWndRect(Frect():set(0,0,1024,768)) self:SetAutoDelete(true) --self:Enable(true) local xml = CScriptXmlInit() xml:ParseFile ("ui_items_backpack.xml") self.dialog = xml:InitStatic("backpack", self) xml:InitStatic("backpack:background", self.dialog) self.input = xml:InitEditBox("backpack:input",self.dialog) self:Register(self.input,"fld_input") local btn = xml:Init3tButton("backpack:btn_cancel", self.dialog) self:Register(btn,"btn_cancel") btn = xml:Init3tButton("backpack:btn_ok", self.dialog) self:Register(btn,"btn_ok") end function UICreateTrash:InitCallBacks() self:AddCallback("btn_ok", ui_events.BUTTON_CLICKED, self.OnAccept, self) self:AddCallback("btn_cancel", ui_events.BUTTON_CLICKED, self.Close, self) end function UICreateTrash:Reset(obj) self.id = obj:id() self.section = obj:section() self.input:SetText("") end function UICreateTrash:Update() CUIScriptWnd.Update(self) end function UICreateTrash:OnAccept() local se_obj = alife_create(bin_worlditem,db.actor:position(),db.actor:level_vertex_id(),db.actor:game_vertex_id()) if (se_obj) then local txt = self.input:GetText() txt = txt ~= "" and txt or strformat("%s's Recycle Bin",db.actor:character_name()) if show_mapspots then level.map_add_object_spot(se_obj.id, mapspot_string, txt) end local showtxt = "Recycle Bin placed" if mollyguard then showtxt = game.translate_string("st_trashcan_mollyguard").. " "..tostring(trashcan_deletion_timeout).." ".. game.translate_string("st_trashcan_seconds").."!" else showtxt = game.translate_string("st_trashcan_shutupwesley") end dl("mollyguard=%s | "..showtxt,mollyguard) actor_menu.set_msg(1,showtxt,4) local m_data = alife_storage_manager.get_state() if not (m_data.player_created_stashes) then m_data.player_created_stashes = {} end m_data.player_created_stashes[se_obj.id] = self.section alife_release_id(self.id) local data = { stash_id = se_obj.id, stash_name = txt, stash_section = self.section, } SendScriptCallback("actor_on_stash_create",data) end self:Close() end function UICreateTrash:OnKeyboard(dik, keyboard_action) local res = CUIScriptWnd.OnKeyboard(self,dik,keyboard_action) if (res == false) then local bind = dik_to_bind(dik) if keyboard_action == ui_events.WINDOW_KEY_PRESSED then if dik == DIK_keys.DIK_ESCAPE then self:Close() end end end return res end function UICreateTrash:Close() self:HideDialog() Unregister_UI("UICreateTrash") end