2024-03-17 20:18:03 -04:00
-- cache all existing barters, source of truth
barters = {}
-- list of all barters associated with trader (array index)
trader_to_barter = {}
-- track existing data when it comes to barters that have been partially completed n such, need to be reloaded, etc
barter_mdata = {
fixed = {}, -- limited
restock = {}, -- resets on restock
give_items = {} -- locks the current give item if there is a pool
}
local ini_barters = ini_file("barter\\importer.ltx")
ini_parts = itms_manager.ini_parts
gc = game.translate_string
get_config = barter_mcm.get_config
local dbg_log
function print_dbg(msg, ...)
-- printf("barter | %s | " .. msg, time_global(), ...)
if get_config("debug") then
dbg_log = dbg_log or mcm_log and mcm_log.new("DBG")
if dbg_log then --dbg_log then
dbg_log.enabled = true
dbg_log:log( msg , ...)
else
printf( "barter: | %s | "..msg ,time_global(), ...)
end
end
return nil
end
function get_desc(name)
if ini_barters:r_string_ex(name, "repeat") and ini_barters:r_string_ex(name, "desc_done") and not check_limit(name) then
return ini_barters:r_string_ex(name, "desc_done") or "st_generic_done"
else
return ini_barters:r_string_ex(name, "desc") or "st_ui_barter_rules"
end
end
function npc_has_barter(npc)
2024-04-05 05:57:24 -04:00
local name = ""
if type(npc) == "string" then
-- two cases for sid and forester
if npc == "esc_2_12_stalker_trader" then name = "m_trader"
elseif npc == "red_forester_tech" then name = "m_lesnik"
else
name = npc
end
else
name = npc and npc:section() or ""
end
2024-03-17 20:18:03 -04:00
return trader_to_barter[name] and not is_empty(trader_to_barter[name])
end
-- yoink
function eval_func(f, ...)
fc = str_explode(f, "%.")
file, func = unpack(fc)
if file and func and _G[file] and _G[file][func] then
return _G[file][func](...)
else
print_dbg("Could not find function %s", f)
end
end
local default_cond_funcs = {
["precondition"] = "barter_core.pre_std",
["postcondition"] = "barter_core.post_std",
["showcondition"] = "barter_core.check_pass",
}
-- barter name, "precondition"/*showocondition/"postcondition"
function eval_barter(name, condition)
conds = ini_barters:r_string_ex(name, condition)
if not conds then conds = default_cond_funcs[condition] or "barter_core.check_pass" end
conds = str_explode(conds, ",")
-- short circuit L->R evaluation
local result = true
for k,v in pairs(conds) do
res = eval_func(v, name)
if res then
result = result and res
else
print_dbg("%s condition false", v)
result = false
end
end
return result
end
local repeat_ranks = {
["inf"] = 3,
["restock"] = 2,
["fixed"] = 1
}
-- sorts barters
function sort_func(a, b)
-- goodwill or hard rank
local a_rep = ini_barters:r_float_ex(a.name, "sort_rank") or ini_barters:r_float_ex(a.name, "goodwill") or 0
local b_rep = ini_barters:r_float_ex(b.name, "sort_rank") or ini_barters:r_float_ex(b.name, "goodwill") or 0
if a_rep ~= b_rep then return a_rep < b_rep end
-- repeat type
local a_type = ini_barters:r_string_ex(a.name, "repeat") or "inf"
local b_type = ini_barters:r_string_ex(b.name, "repeat") or "inf"
if a_type ~= b_type then
return repeat_ranks[a_type] > repeat_ranks[b_type]
end
-- repeat limit
local a_limit = ini_barters:r_float_ex(a.name, "limit") or 0
local b_limit = ini_barters:r_float_ex(b.name, "limit") or 0
if a_limit ~= b_limit then return a_limit > b_limit end
-- cost
return (SYS_GetParam(2, a.give.sec, "cost") * a.give.amt) > (SYS_GetParam(2, b.give.sec, "cost") * b.give.amt)
end
-- get barter list with data on each barter
function get_barter_list(npc, random)
2024-04-05 05:57:24 -04:00
printf("Checking barters for %s", npc:section())
2024-03-17 20:18:03 -04:00
if not npc_has_barter(npc) then return {} end
local name = npc:section()
print_dbg("Fetching list for %s", npc:character_name())
local barter_list = {}
for k,v in pairs(trader_to_barter[name]) do
print_dbg("Loading data for trade %s", v)
local data = barter_get_data(v)
if data and eval_barter(v, "showcondition") then table.insert(barter_list, data) end
end
table.sort(barter_list, sort_func)
return barter_list
end
-- for a given barter, return data on barter eligibility and what items are taken
function barter_get_data(name)
-- eval precond
local allowed = eval_barter(name, "precondition")
-- eval items
local data = {}
copy_table(data, barters[name])
data.give = get_give_item(name)
data.name = name
-- sort rank is important as newlines will be inserted in differences between sort rank
data.sort_rank = ini_barters:r_float_ex(name, "sort_rank") or ini_barters:r_float_ex(name, "goodwill") or 0
data.amt = data.limit and get_barter_amt(name) or nil
local take_items = {}
for k,v in pairs(data.take) do
take_items[k] = {
sec = k,
ids = {},
amt = 0,
limit = v
}
if k == "money" then
take_items[k].amt = db.actor:money()
end
if IsItem("ammo", k) then
take_items[k].limit = take_items[k].limit * SYS_GetParam(2, k, "box_size")
end
end
db.actor:iterate_inventory(function(actor, item)
local sec = item:section()
local take_amt = data.take[sec]
if not take_amt or take_amt == 0 then return end
-- item specific stuff
if IsItem("ammo", sec) and item:ammo_get_count() < item:ammo_box_size() then
-- validate full box
return
end
if item.condition then
local cond_adj = math.ceil(item:condition()*100)
if SYS_GetParam(1, item:section(), "cond_part") and cond_adj <= barter_mcm.get_config("cond_part") then return
elseif cond_adj <= barter_mcm.get_config("cond") then
-- validate max condition
return end
end
local has_parts = ini_parts:r_string_ex("con_parts_list",sec)
if has_parts then
local parts = item_parts.get_parts_con(item, nil, true)
local part_cond = barter_mcm.get_config("cond_part")
for k,v in pairs(parts) do
if string.find(v, "prt_") and v <= part_cond then return end
end
end
-- process multiuse items
local item_data = take_items[sec]
if item_data.limit > item_data.amt then
table.insert(item_data.ids, item:id())
end
if IsItem("ammo", sec) then
item_data.amt = item_data.amt + IsItem("ammo", sec)
elseif IsItem("multiuse", sec) then
item_data.amt = item_data.amt + item:get_remaining_uses()
else
item_data.amt = item_data.amt + 1
end
end)
if data.type == "anyOf" then
local at_least_one = false
for k,v in pairs(take_items) do
print_dbg("Actor has %s of %s of item %s for barter",v.amt, v.limit, k)
if v.amt >= v.limit then
at_least_one = true
end
end
allowed = allowed and at_least_one
else
for k,v in pairs(take_items) do
print_dbg("Actor has %s of %s of item %s for barter",v.amt, v.limit, k)
if v.limit > v.amt then
allowed = false
end
end
end
print_dbg("Barter allowance for %s is %s", name, allowed)
data.allowed = allowed
local take_items_indx = {}
for k,v in pairs(take_items) do
table.insert(take_items_indx, v)
end
data.take_items = take_items_indx
return data
end
local clr_g = utils_xml.get_color("d_green")
local clr_r = utils_xml.get_color("d_red")
local clr_1 = utils_xml.get_color("ui_gray_2")
local clr_2 = utils_xml.get_color("ui_gray_1")
function build_string(name)
local str = ""
local funcs = ini_barters:r_string_ex(name, "precond_text") or "barter_core.str_std"
funcs = str_explode(funcs, ",")
table.sort(funcs)
for k,v in pairs(funcs) do
val = eval_func(v, name)
if val then
if type(val) ~= "table" then val = {val} end
for k,v in pairs(val) do
str = str .. " " .. gc("st_dot") .. " " .. v .. " \\n"
end
end
end
return str
end
function safe_insert(tbl, itm)
if itm then table.insert(tbl, itm) end
end
function str_any(name)
if barters[name].type == "anyOf" then
return gc("st_barter_anyOf")
end
end
function str_limit(name)
local rep = ini_barters:r_string_ex(name, "repeat")
if rep then
local b_done = barter_mdata[rep][name] or 0
local b_limit = ini_barters:r_float_ex(name, "limit") or 0
local clr = b_done < b_limit and clr_g or clr_r
return gc("st_barter_limit") .. clr .. " " ..b_done .. "/" .. b_limit .. clr_2 .. " " .. gc("st_barter_"..rep)
end
end
function str_goodwill(name)
local faction = ini_barters:r_string_ex(name, "faction")
local rep = ini_barters:r_float_ex(name, "goodwill") or 0
if faction and rep > 0 then
local actor_goodwill = relation_registry.community_goodwill(faction, AC_ID) or 0
local clr = actor_goodwill < rep and clr_r or clr_g
return gc(faction) .. " " .. gc("st_barter_goodwill") .. " " .. clr .. actor_goodwill .. "/" .. rep .. clr_2
end
end
function str_mutant(name)
local count = ini_barters:r_float_ex(name, "kill_cnt")
if count then
local current = game_statistics.get_statistic_count("killed_monsters") or 0
local clr = current < count and clr_r or clr_g
return gc("st_mutants_killed") .. " " .. clr .. current .. "/" .. count .. clr_2
end
end
function str_std(name)
local str = {}
if ini_barters:r_string_ex(name, "repeat") then
safe_insert(str, str_limit(name))
end
if ini_barters:r_string_ex(name, "faction") then
safe_insert(str, str_goodwill(name))
end
if ini_barters:r_string_ex(name, "has_info") then
safe_insert(str, gc("st_barter_info"))
end
if barters[name].type == "anyOf" then
safe_insert(str, gc("st_barter_anyOf"))
else
safe_insert(str, gc("st_barter_allOf"))
end
return str
end
local function str_toolkit(lv)
return gc("st_barter_toolkit"..lv)
end
function str_toolkit1(name)
return str_toolkit(1)
end
function str_toolkit2(name)
return str_toolkit(2)
end
function str_toolkit3(name)
return str_toolkit(3)
end
local function remove(item)
if item.sec == "money" then
db.actor:give_money(-1 * item.limit)
else
local limit = item.limit
print_dbg("Taking %s of item %s", item.limit, item.sec)
for i=1,#item.ids do
if IsItem("multiuse", item.sec) then
local obj = level.object_by_id(item.ids[i])
local to_discharge = clamp(limit, 0, obj:get_remaining_uses())
limit = limit - to_discharge
utils_item.discharge(obj, to_discharge)
else
alife_release_id(item.ids[i])
end
end
end
end
-- assume that the player has filled their end of the bargain and wants to execute on the bargain
function do_barter(name, give, items, ind)
print_dbg("Performing barter %s, index %s", name, ind)
if ind and barters[name].type == "anyOf" then
remove(items[ind])
else
for k,v in pairs(items) do
remove(v)
end
end
if IsItem("multiuse", give.sec) then
local max_uses = SYS_GetParam(2, give.sec, "max_uses")
local count = give.amt or max_uses
while count > 0 do
local uses = clamp(count, 0, max_uses)
alife_create_item(give.sec, db.actor, {["uses"] = uses})
count = clamp(count - max_uses, 0, count)
end
else
local amt = give.amt or 1
for i=1,amt do
local meta = {}
if give.type then
meta[give.type] = give.meta
end
alife_create_item(give.sec, db.actor, meta)
end
end
eval_barter(name, "postcondition")
end
function get_barter_amt(name)
local rep = ini_barters:r_string_ex(name, "repeat")
if not rep then return end
print_dbg("Barter refresh type is %s", rep)
local barter_sub = rep == "fixed" and barter_mdata.fixed or barter_mdata.restock
if not barter_sub[name] then barter_sub[name] = 0 end
return barter_sub[name]
end
function get_give_str(give)
str = ui_item.get_sec_name(give.sec)
if give.type then
str = str .. ", " .. gc("st_barter_"..give.type) ..give.meta .. " "
-- pluralize later
end
if give.amt > 1 then
str = str .. " (x" .. give.amt .. ")"
end
return str
end
-- barter condition functions
-- return true if barter is allowed (before even checking items to take)
-- check against rep level
function check_goodwill(name)
-- return true
local faction = ini_barters:r_string_ex(name, "faction")
if not faction then return true end
faction = str_explode(faction, ",")
local rep = ini_barters:r_float_ex(name, "goodwill") or 0
for i,fac in pairs(faction) do
if relation_registry.community_goodwill(fac, AC_ID) < rep then return false end
end
return true
end
-- function check_supply_lv(name)
-- local trade_lv = ini_barters:r_string_ex(name, "supply_lvl")
-- if not trade_lv then return true end
-- local npc = mob_trade.GetTalkingNpc()
-- local lv = trader_autoinject.supply_level(npc, true)
-- return lv >= trade_lv
-- end
-- check against limit with variable refresh rate
function check_limit(name)
if not barter_mdata.fixed then barter_mdata.fixed = {} end
if not barter_mdata.restock then barter_mdata.restock = {} end
local barter_amt = get_barter_amt(name)
if not barter_amt then return end
local limit = barters[name].limit--ini_barters:r_float_ex(name, "limit")
return barter_amt < limit
end
function check_info(name)
local info = ini_barters:r_string_ex(name, "has_info")
if info then info = str_explode(info, ",")
else return true end
for i,v in pairs(info) do
if not db.actor:has_info(v) then return false end
end
return true
end
-- check all basic preconds
function pre_std(name)
if ini_barters:r_string_ex(name, "repeat") and not check_limit(name) then return false end
if ini_barters:r_string_ex(name, "faction") and not check_goodwill(name) then return false end
if ini_barters:r_string_ex(name, "has_info") and not check_info(name) then return false end
return true
end
function kill_mutant(name)
local count = ini_barters:r_float_ex(name, "kill_cnt")
if count then
local current = game_statistics.get_statistic_count("killed_monsters") or 0
return current >= count
end
end
local function check_toolkit(lv)
local npc = mob_trade.GetTalkingNpc()
return has_alife_info(npc:section() .. "_upgrade_tier_" .. tostring(lv))
end
function check_toolkit1(name)
return check_toolkit(1)
end
function check_toolkit2(name)
return check_toolkit(2)
end
function check_toolkit3(name)
return check_toolkit(3)
end
-- allow
function check_pass(name)
return true
end
-- postconds
function increment_limit(name)
local rep = ini_barters:r_string_ex(name, "repeat")
if not rep then return end
if not barter_mdata.fixed then barter_mdata.fixed = {} end
if not barter_mdata.restock then barter_mdata.restock = {} end
local limit = ini_barters:r_float_ex(name, "limit") or 0
local barter_sub = rep == "fixed" and barter_mdata.fixed or barter_mdata.restock
if not barter_sub[name] then barter_sub[name] = 1
else
barter_sub[name] = clamp(barter_sub[name] + 1, 0, limit)
end
end
function give_info(name)
local info = ini_barters:r_string_ex(name, "give_info")
if not info then return end
db.actor:give_info_portion(info)
end
function post_std(name)
if ini_barters:r_string_ex(name, "repeat") then increment_limit(name) end
if ini_barters:r_string_ex(name, "give_info") then give_info(name) end
end
function trader_on_restock(npc)
local sec = npc:section()
if not trader_to_barter[sec] then return end
if not barter_mdata.fixed then barter_mdata.fixed = {} end
if not barter_mdata.restock then barter_mdata.restock = {} end
if not barter_mdata.give_items then barter_mdata.give_items = {} end
for k,v in pairs(trader_to_barter[sec]) do
if barter_mdata.restock[v] then
-- also rotate items
barter_mdata.restock[v] = 0
end
barter_mdata.give_items[v] = false
end
end
local function parse_barterstring(str)
local temp = str_explode(str, ",")
local t = {}
for k,v in pairs(temp) do
local group = str_explode(v, ":")
local item = group[1]
local uses = group[2]
if not uses or uses == "full" then
if IsItem("multiuse", item) then uses = IsItem("multiuse", item)
else uses = 1 end
end
t[item] = tonumber(uses)
end
return t
end
function check_addon(addon)
if not addon then return true end
if addon:sub(1, 1) == "!" then
return not _G[addon:sub(2)]
else
return _G[addon]
end
end
function get_give_item(name)
local to_give = ini_barters:r_string_ex(name, "give")
to_give = str_explode(to_give, ",")
local give
if #to_give > 1 then
-- search up cached if exists, select and cache if not
if not barter_mdata.give_items then barter_mdata.give_items = {} end
if not barter_mdata.give_items[name] then barter_mdata.give_items[name] = to_give[math.random(#to_give)] end
give = barter_mdata.give_items[name]
else
give = to_give[1]
end
give = str_explode(give, ":")
give_t = {
sec = give[1]
}
if IsItem("multiuse", give_t.sec) then
give_t.amt = tonumber(give[2]) or SYS_GetParam(2, give_t.sec, "max_uses")
else
give_t.amt = tonumber(give[2]) or 1
end
local meta = ini_barters:r_float_ex(name, "give_meta")
if meta then
give_t.meta = meta
if IsItem("ammo", give_t.sec) and give_t.meta <= SYS_GetParam(2, give_t.sec, "box_size") then
give_t.type = "ammo"
elseif (IsItem("outfit", give_t.sec) or IsItem("helmet", give_t.sec) or (string.find(give_t.sec, "wpn_") and not IsItem("fake_ammo_wpn", give_t.sec))) and give_t.meta <= 100 then
give_t.type = "cond"
end
end
return give_t
end
-- cycle through all barters and load only valid ones in
local function validate_barters()
ini_barters:section_for_each(function(name)
print_dbg("Parsing barter %s", name)
local npc = ini_barters:r_string_ex(name, "trader")
local give = ini_barters:r_string_ex(name, "give")
local addon = ini_barters:r_string_ex(name, "addon")
if not check_addon(addon) then
print_dbg("Barter %s failed check for addon %s", name, addon)
return
end
if not give then
print_dbg("Barter %s has no give item", name)
return
end
give = str_explode(give, ",")
-- validate each item
for k,v in pairs(give) do
g = str_explode(v, ":")
if not g[1] or (g[1] ~= "money" and not ini_sys:section_exist(g[1])) then
print_dbg("Barter %s attempted to add nonexistant give item %s", name, g[1])
return false
end
end
local take = ini_barters:r_string_ex(name, "take")
if not take then
print_dbg("Barter %s has no take items", name)
return
end
take = parse_barterstring(take)
for k,v in pairs(take) do
if (k ~= "money" and not ini_sys:section_exist(k)) then
print_dbg("Barter %s attempted to add nonexistant take item %s", name, k)
return
end
end
local data = {
-- give = give_t,
take = take,
}
local restricted = ini_barters:r_string_ex(name, "restricted")
if restricted then
restricted = str_explode(restricted, ",")
data.restricted = t2k_table(restricted)
end
data.type = ini_barters:r_string_ex(name, "type") or "allOf"
data.limit = ini_barters:r_float_ex(name, "limit")
if not trader_to_barter[npc] then trader_to_barter[npc] = {} end
table.insert(trader_to_barter[npc], name)
barters[name] = data
end)
end
local function save_state(mdata)
mdata.barter_mdata = barter_mdata
end
local function load_state(mdata)
barter_mdata = mdata.barter_mdata or {}
end
function on_game_start()
RegisterScriptCallback("save_state",save_state)
RegisterScriptCallback("load_state",load_state)
RegisterScriptCallback("actor_on_first_update", actor_on_first_update)
RegisterScriptCallback("trader_on_restock", trader_on_restock)
2024-04-05 05:57:24 -04:00
validate_barters()
2024-03-17 20:18:03 -04:00
end