-- each datum consists of the following: -- .loaded = this is a stack of rounds loaded in the magazine, each round is int, depending on what kind of round it is -- .section = for magazines in weapons, this tracks the type of magazine that is loaded local mags_storage = {} -- this storage is for vested mags in specific local carried_mags = {} -- reverse lookups for magazine properties local mags_by_basetype = {} local mags_by_retool_group = {} local basetypes_by_ammo_type ={} local loadout_slots = { small = 2, medium = 0, large = 0, } function print_dbg(...) magazines.print_dbg(...) end function print_err(...) magazines.print_err(...) end local function parent_section(sec) return SYS_GetParam(0, sec, "parent_section", sec) or sec end function dump_data(data) if not data or type(data) ~= "table" then print_dbg("Mag data unavailable: %s (%s)", data, type(data)) else local s = "" for k,v in pairs(data.loaded) do s = s .. v .. " " end print_dbg("Mag section: %s. Rounds loaded: %s. Rounds: %s", data.section, #data.loaded, s) end end local look_up_table = ini_file_ex("magazines\\lookups.ltx") local weapons_lookup = ini_file_ex("magazines\\weapons\\importer.ltx") local loadout_lookup = ini_file_ex("magazines\\outfitloadouts\\importer.ltx") ------------------------------- -- SECTION acess functions -- ------------------------------- function get_carried_mags(tbl) --return carried_mags --efficency v acess control hud won't call this very often so went with acess control copy_table(tbl, carried_mags ) end function get_data(id) tbl = {} if mags_storage[id] then copy_table(tbl, mags_storage[id] ) return tbl end return mags_storage[id] --could be nil or false want to return either. end function set_data(id, data) mags_storage[id] = data if carried_mags[id] then carried_mags[id] = data end end function create_mag_data(id, sec, is_weapon) local mag_data = {} mag_data.loaded = {} mag_data.section = sec mag_data.is_weapon = is_weapon or false set_data(id, mag_data) return mag_data end -- loops through mag data and check/validate entries function clean_data() for id, data in pairs(mags_storage) do local se = alife_object(id) local is_mag = se and SYS_GetParam(1, se:section_name(), "is_mag") local is_wpn = se and is_supported_weapon(parent_section(se:section_name())) if not (is_mag or is_wpn) then set_data(id, nil) elseif data == false then set_data(id, { section = "no_mag", loaded = {}, is_weapon = true, }) elseif is_wpn then print_dbg("Checking wpn %s", se:section_name()) validate_wep(id, wpn_sec) else validate_mag(id, se:section_name()) end end end function get_mag_property(id,key) --print_dbg("wtf:".. tostring((mags_storage[id] and mags_storage[id][key] or "no dice"))) return mags_storage[id] and mags_storage[id][key] end function get_size(id, mag_data) mag_data = mag_data or get_data(id) return SYS_GetParam(0, mag_data.section, "mag_size") or "small" end function get_total_carried(exact) local carried = { ["small"] = 0, ["medium"] = 0, ["large"] = 0 } for id, mag in pairs(carried_mags) do local size = get_size(id) carried[size] = carried[size] + 1 end if not exact then --shift exess up to next size if carried.small > loadout_slots.small then carried.medium = carried.medium + carried.small - loadout_slots.small carried.small = loadout_slots.small end if carried.medium > loadout_slots.medium then carried.large = carried.large + carried.medium - loadout_slots.medium carried.medium = loadout_slots.medium end end return carried end function get_loadout_size() local copy = {} copy_table(copy, loadout_slots) return copy end function get_mags_for_basetype(basetype) return mags_by_basetype[basetype] and dup_table( mags_by_basetype[basetype]) end function get_basetypes_by_ammo_type(sec) --print_dbg("#basetypes_by_ammo_type:%s basetypes_by_ammo_type[%s]:%s",#basetypes_by_ammo_type,sec,basetypes_by_ammo_type[sec] and #basetypes_by_ammo_type[sec]) return basetypes_by_ammo_type[sec] and dup_table( basetypes_by_ammo_type[sec]) end function get_mags_by_ammo_type(sec) local t = {} local basetypes = get_basetypes_by_ammo_type(sec) or {} for _,v in ipairs(basetypes) do local mags = get_mags_for_basetype(v) or {} for __,v2 in ipairs(mags) do table.insert(t, v2) end end return t end ------------------------------- -- SECTION utility functions -- ------------------------------- local function type_correction(val) if not val then return end -- print_dbg(type(val)) local id, section, obj, se_obj if type(val) == "string" then section = val elseif type(val) == "number" then id = val obj = level.object_by_id(id) se_obj = alife_object(id) if obj then section = obj:section() elseif se_obj then section = se_obj:section_name() else print_dbg("WTF is:%s any way?", id) end elseif type(val.id) == "number" then id = val.id se_obj = val obj = level.object_by_id(id) section = val:section_name() elseif type(val.id) == "function" then id = val:id() obj = val se_obj = alife_object(id) section = val:section() end return id, section, obj, se_obj end function is_carried_mag(id) return carried_mags[id] and true or false end function toggle_carried_mag(id) -- print_dbg("tcm1") if carried_mags[id] then carried_mags[id] = nil return false --print_dbg("tcm2") elseif room_in_pouch(id) then carried_mags[id] = mags_storage[id] return true --print_dbg("tcm3"..tostring(carried_mags[id] and carried_mags[id].section)) end end function update_loadout_slots() local outfit = db.actor:item_in_slot(7) local small,medium,large = 0,0,0 if outfit then small,medium,large = get_loadout_slots(outfit) --print_dbg("Outfit:%s||S:%s|M:%s|L:%s",outfit:section(), small,medium,large) else small,medium,large = get_loadout_slots("o_none") --print_dbg("Outfit:%s||S:%s|M:%s|L:%s","o_none", small,medium,large) end local s,m,l = 0,0,0 local backpack = db.actor:item_in_slot(13) s,m,l = get_loadout_slots(backpack) --print_dbg("backpack:%s||S:%s|M:%s|L:%s",backpack and backpack:section(), s,m,l) small = small + s medium = medium + m large = large + l --print_dbg("Outfit+backpack||S:%s|M:%s|L:%s", small,medium,large) local ss,mm,ll = 0,0,0 db.actor:iterate_belt( function(owner, obj) s,m,l = get_loadout_slots(obj) --print_dbg("belt:%s||S:%s|M:%s|L:%s",obj and obj:section(), s,m,l) ss = ss + s mm = mm + m ll = ll + l end) --print_dbg("Belt total||S:%s|M:%s|L:%s", ss,mm,ll) small = small + ss medium = medium + mm large = large + ll --print_dbg("total slots||S:%s|M:%s|L:%s", small,medium,large) loadout_slots.small = small loadout_slots.medium = medium loadout_slots.large = large end function validate_loadout() update_loadout_slots() local carried = get_total_carried() local excess = carried.large - loadout_slots.large if excess < 1 then -- if they shift arround and don't over fill large all good --print_dbg("validate_loadout s:%s m:%s l:%s excess:%s", carried.small, carried.medium, carried.large, excess) return end local found = {small = 0, medium = 0, large = 0} for id, mag in pairs(carried_mags) do --shed exess favoring shifted mags. local size = get_size(id) if size then found[size] = found[size] + 1 if found[size] > loadout_slots[size] then carried_mags[id] = nil excess = excess - 1 if excess == 0 then break end end end end magazines.inventory_refresh() end function room_in_pouch(id) local s = mags_storage[id] and get_size(id) or false if not s then return false end for i, mag in pairs(carried_mags) do -- safety purge of escaped mags local itm = level.object_by_id(i) if not (itm and utils_item.in_actor_inv(itm)) then carried_mags[i] = nil end end carried = get_total_carried() return carried.large < loadout_slots.large or ((s == "medium" or s == "small") and (carried.medium < loadout_slots.medium)) or (s == "small" and (carried.small < loadout_slots.small)) end function build_mag_revers_lookups() local base_types = {} local function itr(section) if not is_magazine(section) or section == "tch_mag_base" or SYS_GetParam(1, section, "old_mag", false) then return end local basetype = SYS_GetParam(0, section, "base_type") or nil local retool_group = SYS_GetParam(0, section, "retool_group") or nil local caliber = get_magazine_caliber(section) if basetype then if not mags_by_basetype[basetype] then mags_by_basetype[basetype] = {} end mags_by_basetype[basetype][#mags_by_basetype[basetype]+1] = section end if retool_group then if not mags_by_retool_group[retool_group] then mags_by_retool_group[retool_group] = {} end mags_by_retool_group[retool_group][#mags_by_retool_group[retool_group]+1] = section end if caliber and basetype and not base_types[basetype] then base_types[basetype] = true for i = 1, #caliber do if not basetypes_by_ammo_type[caliber[i]] then basetypes_by_ammo_type[caliber[i]] = {} end basetypes_by_ammo_type[caliber[i]][#basetypes_by_ammo_type[caliber[i]]+1] = basetype end end end ini_sys:section_for_each(itr) end -- these functions work equally well given an object id, item section, gameobject or server object. function is_supported_weapon(val) if not val then return end local id, section, obj, se_obj = type_correction(val) local is_valid_by_sec = weapons_lookup:section_exist(parent_section(section)) --print_dbg("is_valid_by_sec:%s and obj: %s", is_valid_by_sec, obj and true) if (not (obj and is_valid_by_sec)) or obj:weapon_in_grenade_mode() then return is_valid_by_sec end local is_valid = is_valid_by_sec and get_weapon_base_type(id) and true or false --print_dbg("is_valid:%s and obj: %s", is_valid, obj and true) -- if it's a valid weapon, bind it to the mag binder -- HarukaSai: we don't need a binder just for first update, instead we can create data here if is_valid then local mag_data = get_data(id) if not mag_data then print_dbg("Valid weapon %s has no data, creating default data", obj:section()) local default_mag = weapon_default_magazine(section) mag_data = create_mag_data(id, default_mag, true) -- also need to do some things to convert existing ammo local ammo_max = SYS_GetParam(2, default_mag, "max_mag_size") local ammo_type = obj:get_ammo_type() local ammo_map = utils_item.get_ammo(section, id) or SYS_GetParam(2, section, "ammo_mag_size") or 999 print_dbg("Weapon %s uses mags, assigning default mag %s with %s rounds, type is %s", section, default_mag, ammo_max, ammo_map[ammo_type+1]) for i=1,ammo_max do stack.push(mag_data.loaded, ammo_map[ammo_type+1]) end set_data(id, mag_data) end end return is_valid end function is_open_bolt_weapon(val) if not val then return end local id, section, obj, se_obj = type_correction(val) return look_up_table:r_value(parent_section(section), "open_bolt") end function has_loadout_slots(val) if not val then return end local id, section, obj, se_obj = type_correction(val) local in_list = loadout_lookup:section_exist(parent_section(section)) --print_dbg("has_loadout_slots %s|%s:%s|%s", section,parent_section(section), in_list, IsItem("outfit", section, obj) ) return IsItem("outfit", section, obj) or in_list end function get_retool_section(val) if not val then return end local id, section, obj, se_obj = type_correction(val) local retool_group = SYS_GetParam(0, section, "retool_group") or nil local retool_section = nil if retool_group and mags_by_retool_group[retool_group] and #mags_by_retool_group[retool_group] > 1 then --want to return nil of mag has no retool group or is only memeber for i,v in ipairs(mags_by_retool_group[retool_group]) do print_dbg("retool section: %s|%s", section, v) if v == section then if i < #mags_by_retool_group[retool_group] then retool_section = mags_by_retool_group[retool_group][i+1] else retool_section = mags_by_retool_group[retool_group][1] end break end end end return retool_section end function get_loadout_slots(val, combine, force_outfit) if not val then return 0,0,0 end local id, section, obj, se_obj = type_correction(val) local s,m,l = 0,0,0 if look_up_table:section_exist(section) then s = look_up_table:r_value(section, "mag_limit_small", 2) or s m = look_up_table:r_value(section, "mag_limit_medium", 2) or m l = look_up_table:r_value(section, "mag_limit_large", 2) or l elseif IsOutfit(obj) or force_outfit then local kind = SYS_GetParam(0, section, "kind") if look_up_table:section_exist(kind) then s = look_up_table:r_value(kind, "mag_limit_small", 2) or s m = look_up_table:r_value(kind, "mag_limit_medium", 2) or m l = look_up_table:r_value(kind, "mag_limit_large", 2) or l else print_dbg("Outfit defaulted: sec:%s kind:%s",section ,kind ) s = look_up_table:r_value("o_none", "mag_limit_small", 2) or s m = look_up_table:r_value("o_none", "mag_limit_medium", 2) or m l = look_up_table:r_value("o_none", "mag_limit_large", 2) or l end end if combine then return {small = s,medium = m, large = l} else return s, m, l end end function is_magazine(val) if not val then return false end local id, section, obj, se_obj = type_correction(val) return SYS_GetParam(1, section, "is_mag") end function get_magazine_base_type(val) if not val then return end local id, section, obj, se_obj = type_correction(val) return SYS_GetParam(0, section, "base_type") or nil end function get_magazine_caliber(val) return str_explode(look_up_table:r_value(get_magazine_base_type(val) or print_dbg("Missing basetype for: %s",val), "caliber") or print_dbg("Invalid basetyper for: %s",val), ",") end -- check if this weapon takes a magazine, and return the base type. false if it does not function get_weapon_base_type(val) if not val then return end local id, section, obj, se_obj = type_correction(val) id = obj and id or nil --utils_item.get_ammo will not work if given an id of an offline object. nil id if no gameobject exists local ammo = utils_item.get_ammo(section, id)[1] local parent = parent_section(section) print_dbg("for weapon %s, using ammo %s, bt is %s", parent, ammo, is_supported_weapon(section) and look_up_table:r_value(parent, ammo)) return is_supported_weapon(section) and look_up_table:r_value(parent, ammo) -- base_type is in the ltx based on the first entry in the weapons ammo list. end function weapon_default_magazine(val) if not val then return end local id, section, obj, se_obj = type_correction(val) return look_up_table:r_value(parent_section(section), "default_mag") end function weapon_improved_magazine(val) if not val then return end local id, section, obj, se_obj = type_correction(val) return look_up_table:r_value(parent_section(section), "improved_mag") end -- check if mag is compatible w. weapon function is_compatible(weapon, magazine) --both called functions do type correction, added argument order corection if not is_magazine(magazine) then local t = magazine magazine = weapon weapon = t end local weapon_base = get_weapon_base_type(weapon) local magazine_base = get_magazine_base_type(magazine) print_dbg("wpn base type is %s, mag base type is %s", weapon_base, magazine_base) return weapon_base == magazine_base end function valid_mag_data(mag_data) return (mag_data and mag_data.section ~= "no_mag") and mag_data or nil end -- get mag data with validation for fake mag items function get_mag_loaded(id) return valid_mag_data(get_data(id)) end function validate_wep(id, sec) local wpn_obj = level.object_by_id(id) local mag_data = get_mag_loaded(id) if not mag_data then return end if wpn_obj and wpn_obj:weapon_in_grenade_mode() then return end local w_bt = get_weapon_base_type(id) local m_bt = get_magazine_base_type(mag_data.section) if w_bt ~= m_bt then print_err("(CLEANUP) Incompatible magazine %s (bt %s) found on gun %s (typ %s, bt %s). Granting default magazine.", mag_data.section, m_bt, id, sec, w_bt) mag_data.section = weapon_default_magazine(wpn_obj) empty_table(mag_data.loaded) set_data(id, mag_data) end end function validate_mag(id, sec) local mag_data = get_mag_loaded(id) if not mag_data then return end if sec ~= mag_data.section then print_err("(CLEANUP) Mag section mismatch! Saved type is %s, actual %s for id %s", mag_data.section, sec, id) mag_data.section = sec end -- check for ammo that shouldn't belong in the magazine, and replace with generic ammo local ammo_map = invert_table(get_magazine_caliber(mag_data.section)) -- remove excess rounds local ammo_cap = SYS_GetParam(2, mag_data.section, "max_mag_size") while #mag_data.loaded > ammo_cap do print_err("(CLEANUP) Loaded magazine %s (%s) has capacity %s, current %s rounds loaded", mag_data.section, id, ammo_cap, #mag_data.loaded) stack.pop(mag_data.loaded) end local ammo_replace = random_key_table(ammo_map) for k,v in pairs(mag_data.loaded) do if not ammo_map[v] then print_err("(CLEANUP) Invalid round %s loaded in mag %s, id %s, replacing", v, mag_data.section, id) mag_data.loaded[k] = ammo_replace end end set_data(id, mag_data) end -- class function bind(obj) obj:bind_object(magazine_binder(obj)) end class "magazine_binder" (object_binder) function magazine_binder:__init(obj) super(obj) self.first_update = nil end -- global flag used to cap condition at 100 for trade local freeze = false -- only update every half second local update_tick = 500 function magazine_binder:update(delta) object_binder.update(self, delta) local tg = time_global() local obj = self.object local id = obj:id() local sec = obj:section() local mag_data = get_data(id) if not self.first_update then self.first_update = true self.last_update = tg + update_tick -- associate mag data to empty magazine object if not mag_data and is_magazine(obj) then mag_data = create_mag_data(id, sec, false) end end if tg < self.last_update then return end self.last_update = tg + update_tick -- update mag weight and cond if is_magazine(obj) then if freeze then obj:set_condition(0.999) else local mag_weight = SYS_GetParam(2, sec, "inv_weight") local capacity = SYS_GetParam(2, sec, "max_mag_size") -- for simplicity we take the weight of each bullet to be the same local cond = 0 if mag_data and #mag_data.loaded > 0 then local ammoType = get_magazine_caliber(sec)[1] local box_size = SYS_GetParam(2, ammoType, "box_size") or 1 local box_weight = SYS_GetParam(2, ammoType, "inv_weight") or 0 local cartridge_weight = box_weight / box_size cond = #mag_data.loaded / capacity mag_weight = mag_weight + (#mag_data.loaded * cartridge_weight) end set_data(id, mag_data) obj:set_weight(mag_weight) obj:set_condition(cond) end end end function magazine_binder:reload(section) object_binder.reload(self, section) end function magazine_binder:reinit() object_binder.reinit(self) end function magazine_binder:net_spawn(se_abstract) if not(object_binder.net_spawn(self, se_abstract)) then return false end return true end function magazine_binder:net_destroy() object_binder.net_destroy(self) end function magazine_binder:save(stpk) end function magazine_binder:load(stpk) end -- end class ------------------------------- -- SECTION inventory highlight -- ------------------------------- -- this gets called _ALOT_ so putting it here where table can be read local bags = {actor_bag = true,actor_trade_bag= true} --player inv in normal/looting and in the merchant UI. ready_color = GetARGB(100, 255, 159, 82) function check_ready(cell) if bags[cell.container.ID] then return carried_mags[cell.ID] and ready_color --cell.ID and cell.sec are the object id and section end end function icon_check_ready(cell) if bags[cell.container.ID] and carried_mags[cell.ID] then return {texture = "ui_mags_loadout", x = 1, y = 1, w = 15, h = 15} end end ------------------------------- -- SECTION callbacks -- ------------------------------- local function save_state(mdata) mdata.mags_storage = mags_storage mdata.carried_mags = carried_mags end function load_state(mdata) mags_storage = mdata.mags_storage or {} carried_mags = mdata.carried_mags or {} end -- attempt to keep mstorage clean local function on_register(se_obj, typ) local id = se_obj.id local sec = se_obj:section_name() if mags_storage[id] and not (is_magazine(sec) or is_supported_weapon(sec)) then mags_storage[id] = nil end end local function se_item_on_unregister(se_obj, typ) local id = se_obj.id mags_storage[id] = nil carried_mags[id] = nil end local past_first_update = false function actor_on_first_update() CreateTimeEvent("mag_binder","firstupdatedelay",1,function() past_first_update = true validate_loadout() clean_data() return true end) end function actor_item_to_slot(obj) if past_first_update and has_loadout_slots(obj) then validate_loadout() end end function on_trade_opened() freeze = true end function on_trade_closed() freeze = false end function on_game_start() build_mag_revers_lookups() RegisterScriptCallback("save_state",save_state) RegisterScriptCallback("load_state",load_state) rax_persistent_highlight.register("ready_mag", check_ready) --used like a callback register rax_icon_layers.register("ready_mag", icon_check_ready) --used like a callback register RegisterScriptCallback("actor_item_to_slot",actor_item_to_slot) RegisterScriptCallback("actor_item_to_ruck",actor_item_to_slot) RegisterScriptCallback("actor_item_to_belt",actor_item_to_slot) RegisterScriptCallback("actor_on_item_drop",actor_item_to_slot) RegisterScriptCallback("server_entity_on_unregister",se_item_on_unregister) RegisterScriptCallback("actor_on_first_update", actor_on_first_update) RegisterScriptCallback("ActorMenu_on_trade_started",on_trade_opened) RegisterScriptCallback("ActorMenu_on_trade_closed",on_trade_closed) end