Divergent/mods/Zone Recycle Bin/gamedata/scripts/item_trashcan.script

524 lines
16 KiB
Plaintext
Raw Normal View History

2024-03-17 20:18:03 -04:00
--[[ 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