524 lines
16 KiB
Plaintext
524 lines
16 KiB
Plaintext
|
--[[ 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 = "<ZRB>"
|
||
|
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
|