Added New Mods
Added - MRAA + Blindside Military Animation Pack - Dynamic Zone Transitions - Corpse Twitching - Artifact Inspection - Animated Lead Box
This commit is contained in:
@@ -0,0 +1,687 @@
|
||||
--[[
|
||||
DYNAMIC ZONE
|
||||
|
||||
Original Author(s)
|
||||
Singustromo <singustromo at disroot.org>
|
||||
|
||||
Edited by
|
||||
|
||||
License
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
|
||||
(https://creativecommons.org/licenses/by-nc-sa/4.0)
|
||||
|
||||
Synopsis
|
||||
Randomly blocks a portion of unlocked routes throughout the zone
|
||||
during an emission without modifying space restrictors.
|
||||
Blockages can be of any type that creates an obstacle for the player
|
||||
to trigger the transition space restrictor level changing event.
|
||||
|
||||
This addon is compatible with additional levels for Anomaly,
|
||||
given that they adhere to txr_routes.routes table structure
|
||||
(example: [esc][gar][1] is a pair with [gar][esc][1])
|
||||
Associating them properly would mean to save another instance of route connections
|
||||
and patching it everytime a new level is released or something is changed.
|
||||
|
||||
This script represents the main entry point of the addon.
|
||||
|
||||
Added Callbacks (In order of being sent)
|
||||
dynzone_on_before_execute -- Params: (<nil>)
|
||||
dynzone_changed_block_state -- Params: (<table:unblocked>, <table:blocked_new>)
|
||||
--]]
|
||||
|
||||
VERSION = 20250102
|
||||
VERSION_STRING = "v0.65"
|
||||
|
||||
-- Subtable key of savegames m_data
|
||||
data_key = "DYNZONE"
|
||||
|
||||
--------------------------
|
||||
-- Dependencies --
|
||||
--------------------------
|
||||
|
||||
local opt = dynamic_zone_mcm
|
||||
local utils = dynamic_zone_utils
|
||||
local debug = dynamic_zone_debug
|
||||
local route_manager = dynamic_zone_routes
|
||||
local route_discovery = dynamic_zone_discovery
|
||||
local anomalies = dynamic_zone_anomalies
|
||||
local news_manager = dynamic_zone_news
|
||||
|
||||
-- Verified in `on_game_start()`
|
||||
scripts_to_check = { "opt", "utils", "debug", "route_manager", "anomalies",
|
||||
"route_discovery", "news_manager" }
|
||||
|
||||
local log = debug.log_register("info")
|
||||
local log_error = debug.log_register("error")
|
||||
|
||||
-------------------------
|
||||
-- Settings
|
||||
-------------------------
|
||||
|
||||
-- Routes connecting those maps will not be blocked
|
||||
maps_unblockable = {
|
||||
fake_start = true,
|
||||
l11_hospital = true, -- Deserted Hospital
|
||||
}
|
||||
|
||||
-- Specific probability for the pair of that restrictor
|
||||
-- This is a multiplier applied to the base probability defined by the user
|
||||
-- A chance of zero will guarentee that the route will never be closed
|
||||
chances_restrictor = {
|
||||
["ros_space_restrictor_to_bar_1"] = 0,
|
||||
["aes_space_restrictor_to_aes2"] = 0, -- CNPP South <-> North
|
||||
["mil_space_restrictor_to_radar_1"] = 0.25, -- The Barrier
|
||||
["aes_space_restrictor_to_zaton"] = 0.85,
|
||||
["rad_space_restrictor_to_pripyat_01"] = 0.75,
|
||||
["jup_space_restrictor_to_zaton"] = 0.6,
|
||||
["gar_space_restrictor_to_bar_1"] = 0.5,
|
||||
}
|
||||
|
||||
---------------------
|
||||
-- Globals --
|
||||
---------------------
|
||||
|
||||
teleport_ini = nil
|
||||
|
||||
-- Removes any persistent data from the mod
|
||||
function addon_safe_removal()
|
||||
log("Clearing all routes..")
|
||||
|
||||
route_discovery.clear_eligible_transitions()
|
||||
route_discovery.revert_map_spots()
|
||||
anomalies.release_all_anomalies()
|
||||
news_manager.clear()
|
||||
|
||||
route_manager.clear()
|
||||
end
|
||||
|
||||
-- Duplicate of txr_routes.get_route_info(...)
|
||||
-- Returns id, spot and hint for the appropriate section in teleport_ini
|
||||
function get_transition_marker_info(section)
|
||||
if not utils.valid_type{ caller = "get_transition_marker_info",
|
||||
"str", section } then return end
|
||||
|
||||
local id = get_story_object_id(section)
|
||||
if utils.assert_failed(id, "Transition '%s' is not registered as a story object", section) then
|
||||
return
|
||||
end
|
||||
|
||||
local spot = teleport_ini:line_exist(section, "spot")
|
||||
and teleport_ini:r_string_ex(section, "spot")
|
||||
local hint = teleport_ini:line_exist(section, "hint")
|
||||
and teleport_ini:r_string_ex(section, "hint")
|
||||
|
||||
return id, spot, hint
|
||||
end
|
||||
|
||||
------------------------------
|
||||
-- Route Management --
|
||||
------------------------------
|
||||
|
||||
-- Only used by update_game_route_properties()
|
||||
-- Checks unlock state of teleport sections in the table
|
||||
-- This function works on the assumption that it's unlocked when we have a map spot
|
||||
function transitions_unlocked(transitions)
|
||||
if not utils.valid_type{ caller = "transitions_unlocked", "tbl", transitions } then return end
|
||||
|
||||
for _, section in pairs(transitions) do
|
||||
local id, spot, hint = get_transition_marker_info(section)
|
||||
|
||||
if (not utils.map_spot_exists(id, spot)) then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Just checks, if the sections in the table also exist in teleport_ini
|
||||
function contains_valid_transition_sections(tbl)
|
||||
if not utils.valid_type{ caller = "contains_valid_transition_sections", "tbl", tbl } then return end
|
||||
|
||||
for _, section in pairs(tbl) do
|
||||
if not teleport_ini:section_exist(section) then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Checks, if map names given as the parameter are to be excluded
|
||||
function connects_unblockable_maps(connections)
|
||||
for _, map in pairs(connections) do
|
||||
if (maps_unblockable[map] or utils.is_underground(map)) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- We want to soft-block certain routes, giving them a low probability to
|
||||
-- be disabled without blacklisting them entirely
|
||||
function determine_route_chance(pair)
|
||||
if not utils.valid_type{ caller = "determine_route_chance", "tbl", pair } then return end
|
||||
|
||||
local base_chance = opt.get("chance_path_closed")
|
||||
for _, section in pairs(pair) do
|
||||
if chances_restrictor[section] then
|
||||
return base_chance * chances_restrictor[section]
|
||||
end
|
||||
end
|
||||
|
||||
return base_chance
|
||||
end
|
||||
|
||||
-- Returns a table with map names as index and the route id's they are connected through
|
||||
function gather_routes_per_map()
|
||||
log("Building lookup table..")
|
||||
|
||||
local routes_per_map = {}
|
||||
for route_id, route in route_manager.iterate_routes() do
|
||||
local map_1, map_2 = route.connects[1], route.connects[2]
|
||||
utils.table_safe_insert(routes_per_map, map_1, route_id, "int")
|
||||
utils.table_safe_insert(routes_per_map, map_2, route_id, "int")
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
|
||||
return routes_per_map
|
||||
end
|
||||
|
||||
-- @returns affected maps
|
||||
-- @returns false, if any affected map has not enough references left for given pointer
|
||||
function update_routes_per_map(routes_per_map, reference, no_update)
|
||||
local hits = {}
|
||||
for map, saved_indices in pairs(routes_per_map) do
|
||||
for index, p in pairs(saved_indices) do
|
||||
if (p ~= reference) then goto next_pointer end -- we only check for our reference
|
||||
if no_update then goto insert end
|
||||
|
||||
if (size_table(saved_indices) <= opt.get("minimum_map_connections")) then
|
||||
return
|
||||
end
|
||||
|
||||
table.remove(routes_per_map[map], index)
|
||||
|
||||
:: insert ::
|
||||
table.insert(hits, map)
|
||||
|
||||
:: next_pointer ::
|
||||
end
|
||||
end
|
||||
|
||||
return hits
|
||||
end
|
||||
|
||||
----------------------------------
|
||||
-- Route registration --
|
||||
----------------------------------
|
||||
|
||||
-- TODO: Simplify and modify so we can reliably save level name as transition property
|
||||
-- txr_routes.routes[x] will always be the level of the transition
|
||||
-- Parses txr_routes.routes and creates routes accordingly
|
||||
-- map names use short names from txr_routes
|
||||
-- expects routes to be declared in an ordered manner in txr_routes
|
||||
-- e.g. [esc][gar][1] is a pair with [gar][esc][1]
|
||||
--
|
||||
-- Registers routes via the route manager.
|
||||
function register_game_routes()
|
||||
local routes, maps = txr_routes.routes, txr_routes.maps
|
||||
if not (routes and maps) then return {}, {} end
|
||||
|
||||
-- Just to make sure. Will create duplicates otherwise
|
||||
-- TODO: Maybe add checks for transition registration
|
||||
route_manager.clear()
|
||||
|
||||
local map_to_sec = txr_routes.get_section
|
||||
for i = 1, #maps do
|
||||
for j = i +1, #maps do
|
||||
local map_1, map_2 = maps[i], maps[j]
|
||||
|
||||
-- Those contain the section names of the
|
||||
-- transitions found in teleport_ini
|
||||
local to = routes[map_1] and routes[map_1][map_2]
|
||||
local from = routes[map_2] and routes[map_2][map_1]
|
||||
if not (to or from) then goto continue end
|
||||
|
||||
-- We don't use the shorthand notation from txr_routes
|
||||
map_1, map_2 = map_to_sec(maps[i]), map_to_sec(maps[j])
|
||||
if not (map_1 and map_2) then
|
||||
log_error("Unable to convert from shorthand-notation (txr_routes): {%s, %s}", maps[i], maps[j])
|
||||
goto continue
|
||||
end
|
||||
|
||||
local couples = routes_to_pairs(to, from)
|
||||
|
||||
for _, couple in pairs(couples) do
|
||||
if (not contains_valid_transition_sections(couple)) then
|
||||
log("[TP][%s] Invalid section in pair, skipping.",
|
||||
table.concat(couple,","))
|
||||
goto next_couple
|
||||
end
|
||||
|
||||
local route_id = route_manager.route_create()
|
||||
route_manager.transition_table_register(couple, route_id)
|
||||
|
||||
route_manager.set_route_property(route_id,
|
||||
"connects", { map_1, map_2 })
|
||||
|
||||
::next_couple::
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- One parameter can be nil (for simplicity's sake).
|
||||
-- Returns one table containing all table pairs { {tbl1[1], tbl2[1]]}, .. }
|
||||
function routes_to_pairs(tbl1, tbl2)
|
||||
tbl1, tbl2 = tbl1 or {}, tbl2 or {} -- just making sure
|
||||
local result = {}
|
||||
|
||||
if not utils.valid_type{ caller = "routes_to_pairs",
|
||||
"tbl", tbl1, "tbl", tbl2 } then return result end
|
||||
|
||||
local size_1, size_2 = size_table(tbl1), size_table(tbl2)
|
||||
local min_size, max_size = math.min(size_1, size_2), math.max(size_1, size_2)
|
||||
|
||||
for i = 1, max_size do
|
||||
local couple = {}
|
||||
|
||||
table.insert(couple, tbl1[i])
|
||||
table.insert(couple, tbl2[i])
|
||||
|
||||
-- Exception for 1 <-> (x >1) route pairs (e.g. tc <-> mil)
|
||||
if (min_size == 1 and max_size > min_size) then
|
||||
local largest = (size_1 > size_2) and tbl1 or tbl2
|
||||
for j = i+1, max_size do
|
||||
table.insert(couple, largest[j])
|
||||
end
|
||||
|
||||
table.insert(result, couple)
|
||||
return result
|
||||
end
|
||||
|
||||
table.insert(result, couple)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
------------------------------
|
||||
-- Main Functions --
|
||||
------------------------------
|
||||
|
||||
-- Params are set in a key-value table
|
||||
-- @force forces the blockage of random routes
|
||||
-- @register_routes re-parses the available routes
|
||||
function main_routine(params)
|
||||
log("Called main routine")
|
||||
local force_trigger = params and params.force
|
||||
local force_register = params and params.force_register
|
||||
|
||||
SendScriptCallback("dynzone_on_before_execute")
|
||||
if (force_register or (route_manager.route_count() < 1)) then
|
||||
log("Forced a game route re-registration.")
|
||||
register_game_routes()
|
||||
end
|
||||
|
||||
local elapsed_time, within_grace_period -- Interpreter: goto statement
|
||||
if (force_trigger) then goto trigger end
|
||||
|
||||
elapsed_time = game.get_game_time():diffSec(level.get_start_time())
|
||||
within_grace_period = elapsed_time < (opt.get("newgame_grace_period") * 3600)
|
||||
if (not opt.get("trigger_on_newgame")) and (within_grace_period) then
|
||||
log("Not triggering. New game delay is %s hours, only %.1f hours elapsed.",
|
||||
opt.get("newgame_grace_period"), elapsed_time / 3600)
|
||||
return
|
||||
end
|
||||
|
||||
if (math.random(0,100) > opt.get("chance_dz_trigger")) then
|
||||
log("Not triggering. Chance was %s%", opt.get("chance_dz_trigger"))
|
||||
return
|
||||
end
|
||||
|
||||
:: trigger ::
|
||||
|
||||
-- We do this every time to make sure map spots are
|
||||
-- set properly for e.g. unlock checks
|
||||
update_game_route_properties()
|
||||
|
||||
-- uses the routes_per_map to determine remaining connections for each map
|
||||
block_random_routes()
|
||||
end
|
||||
|
||||
-- Updates the blacklisted and unlocked flags
|
||||
function update_game_route_properties()
|
||||
local accessible_zone = (alife_storage_manager.get_state().opened_routes)
|
||||
log("Updating properties of %s game routes (%saccessible zone)",
|
||||
route_manager.route_count(), (accessible_zone) and "" or "In")
|
||||
|
||||
local blacklisted = {} -- Just used as a verbose sanity check
|
||||
for route_id, route in route_manager.iterate_routes(true) do
|
||||
if connects_unblockable_maps(route.connects) then
|
||||
blacklisted[#blacklisted +1] = route_id
|
||||
route_manager.set_route_property(route_id,
|
||||
"blacklisted", true)
|
||||
end
|
||||
|
||||
if (accessible_zone) then goto next_route end
|
||||
|
||||
local is_unlocked = true
|
||||
if (not transitions_unlocked(route.members)) then
|
||||
log("[%s] transitions are locked: {%s}",
|
||||
route_id, table.concat(route.members, ", "))
|
||||
|
||||
is_unlocked = false
|
||||
end
|
||||
|
||||
route_manager.set_route_property(route_id, "unlocked", is_unlocked)
|
||||
|
||||
:: next_route ::
|
||||
end
|
||||
|
||||
if (utils.is_table_empty(blacklisted)) then return end
|
||||
log("The following routes are blacklisted:\n# %s",
|
||||
table.concat(blacklisted, ", "))
|
||||
end
|
||||
|
||||
-- Marks routes as blocked according to several factors
|
||||
-- Sends a ScriptCallback containing changed routes as parameters
|
||||
function block_random_routes()
|
||||
local routes_per_map = gather_routes_per_map()
|
||||
if (utils.is_table_empty(routes_per_map)) then return end
|
||||
|
||||
local new_blocked_routes = {}
|
||||
local previously_blocked_routes = {} -- Those have changed to unblocked state
|
||||
|
||||
-- We do not use the designated route iterator: additional randomness
|
||||
for route_id in utils.random_numbered_sequence(1, route_manager.route_count()) do
|
||||
if (route_manager.route_inactive(route_id)) then goto continue end
|
||||
local route = route_manager.get_route(route_id)
|
||||
|
||||
local pair = route.members
|
||||
if (not pair or utils.is_table_empty(pair)) then
|
||||
log_error("Route #%s has no members!", route_id)
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- explicitely set to nil so key will not be iterated by callbck functions
|
||||
previously_blocked_routes[route_id] = route.blocked or nil
|
||||
route_manager.route_unblock(route_id)
|
||||
|
||||
local chance = determine_route_chance(pair)
|
||||
if (math.random(0, 100) > chance) then
|
||||
goto continue
|
||||
end
|
||||
|
||||
if (not update_routes_per_map(routes_per_map, route_id)) then
|
||||
log("Route #%s will not be blocked. Last %s remaining.",
|
||||
route_id, opt.get("minimum_map_connections"))
|
||||
|
||||
goto continue
|
||||
end
|
||||
|
||||
if (not route_manager.route_block(route_id)) then
|
||||
log_error("Unable to block route #%s (not unlocked)", route_id)
|
||||
goto continue
|
||||
end
|
||||
|
||||
new_blocked_routes[route_id] = (not previously_blocked_routes[route_id]) or nil
|
||||
previously_blocked_routes[route_id] = nil
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
|
||||
-- We only send those which changed their state
|
||||
SendScriptCallback("dynzone_changed_block_state",
|
||||
previously_blocked_routes, new_blocked_routes)
|
||||
end
|
||||
|
||||
-------------------------
|
||||
-- Callbacks --
|
||||
-------------------------
|
||||
|
||||
--[[
|
||||
main routine is indirectly triggered through `on_before_surge` callback
|
||||
We then check every x seconds if an emission is happening and
|
||||
execute during an appropriate stage.
|
||||
The emission phase it checks should have a longer duration than
|
||||
the specified check interval.
|
||||
|
||||
Why not execute through the respective callback directly?
|
||||
1. Not a lot of scripts are running during an emission.
|
||||
2. Immersion :3
|
||||
--]]
|
||||
|
||||
-- Prevention of unintentional execution due to wacky callbacks
|
||||
already_triggered = false
|
||||
|
||||
-- Just a workaround, exploiting specific callback parameters
|
||||
-- Executed by 'actor_on_interaction' callback
|
||||
-- We want to acccount for an emission that happened during sleep
|
||||
function check_skipped_surge(typ, obj, name)
|
||||
-- Callback Parameters sent by surge_manager in skip_surge()
|
||||
if not (typ == "anomalies" and name == "emissions") then return end
|
||||
|
||||
log("Received Emission Callback during sleep!")
|
||||
|
||||
-- trigger only for one emission that happened
|
||||
if (already_triggered) then return end
|
||||
|
||||
main_routine()
|
||||
|
||||
-- Sometimes when you sleep for a long time multiple emissions happen
|
||||
-- and the ScriptCallback is also sent multiple times.
|
||||
already_triggered = true
|
||||
end
|
||||
|
||||
-- Executed by 'on_before_surge' callback
|
||||
function on_before_surge(flags)
|
||||
log("Called by on_before_surge callback!")
|
||||
|
||||
if not (flags and flags.allow) then -- Make sure it is not skipped
|
||||
log("This emission will be skipped.")
|
||||
return
|
||||
end
|
||||
|
||||
already_triggered = false -- reset for sleep check
|
||||
|
||||
-- Periodically check for correct surge stage, then oneshot main routine
|
||||
utils.timed_call(opt.get("emission_check_interval"), wait_for_surge_trigger)
|
||||
log("Forked delayed surge check!")
|
||||
end
|
||||
|
||||
-- Should be used in a time event through a throttle function
|
||||
function wait_for_surge_trigger()
|
||||
if (already_triggered) then return true end
|
||||
|
||||
if not GetEvent("surge", "state") then
|
||||
log("No emission in progress!")
|
||||
return true
|
||||
end
|
||||
|
||||
local gsm = surge_manager and surge_manager.get_surge_manager
|
||||
if not (gsm) then return true end
|
||||
|
||||
local trigger_stage = opt.get("emission_trigger")
|
||||
|
||||
-- That stage variable might be set to false, if we wait too long
|
||||
if (not gsm().stages[trigger_stage]) then
|
||||
log("Emission in progress, waiting for stage (%s)", trigger_stage)
|
||||
return
|
||||
end
|
||||
|
||||
main_routine()
|
||||
already_triggered = true
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Called after save_state(m_data)
|
||||
-- We do this on_first_update to make sure restrictors and
|
||||
-- their mapspots are properly initialized
|
||||
function actor_on_first_update()
|
||||
local map = utils.get_mapname() -- No need to execute in those cases.
|
||||
if (maps_unblockable[map] or utils.is_underground(map)) then
|
||||
log("Loaded level %s, will not execute.", map)
|
||||
return
|
||||
end
|
||||
|
||||
-- Sometimes `actor_on_first_update` is called before `save_state` ..
|
||||
local m_data = alife_storage_manager.get_state()
|
||||
if (not m_data[data_key]) then
|
||||
m_data[data_key] = {}
|
||||
m_data[data_key].newgame = true
|
||||
end
|
||||
|
||||
if (m_data[data_key].newgame) then
|
||||
if opt.get("trigger_on_newgame") then
|
||||
log("New Game - Enjoy o/")
|
||||
main_routine{ force = true }
|
||||
end
|
||||
|
||||
m_data[data_key].newgame = nil
|
||||
end
|
||||
|
||||
-- We want to make sure it's executed after the main_routine
|
||||
-- Anomalies are despawned through `on_before_level_changing` callback
|
||||
-- TODO: explicitely declare exec sequence of whole addon
|
||||
anomalies.spawn_on_current_level()
|
||||
end
|
||||
|
||||
function on_option_change()
|
||||
if (not opt.get("addon_removal")) then return end
|
||||
|
||||
addon_safe_removal()
|
||||
opt.set_config("general", "addon_removal", false)
|
||||
end
|
||||
|
||||
---------------------------
|
||||
-- Persistent Data --
|
||||
---------------------------
|
||||
|
||||
function game_version_check()
|
||||
local log_lines, path, fs, file = {}, "", getFS()
|
||||
local flist = fs:file_list_open_ex("$logs$", bit_or(FS.FS_ListFiles, FS.FS_RootOnly),"*.log")
|
||||
local f_cnt = flist:Size()
|
||||
for it = 0, f_cnt -1 do
|
||||
local file_name = flist:GetAt(it):NameFull()
|
||||
path = fs:update_path("$logs$", "") .. file_name
|
||||
if string.sub(path, -4) == ".log" then
|
||||
file = io.open(path, "r")
|
||||
end
|
||||
end
|
||||
|
||||
if file then
|
||||
local k = 0
|
||||
for line in file:lines() do
|
||||
k = k + 1
|
||||
if k < 4 then
|
||||
log_lines[k] = line
|
||||
end
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
log_lines[1]=log_lines[1]:sub(35, 44)
|
||||
log_lines[1]=log_lines[1]:gsub("-", "")
|
||||
log_lines[1]=log_lines[1]:gsub("%(", "")
|
||||
log_lines[1]=log_lines[1]:gsub("%)", "")
|
||||
log_lines[1]=log_lines[1]:gsub("%[", "")
|
||||
log_lines[1]=log_lines[1]:gsub("%]", "")
|
||||
log_lines[1]=log_lines[1]:gsub(",", "")
|
||||
local path2 = getFS():update_path("$fs_root$","").."gamedata/scripts/"..script_name()..".script"
|
||||
local check4 = false
|
||||
local file4 = io.open(path2,"r")
|
||||
if (not file4) then return true end
|
||||
|
||||
local check3 = file4:read("*a")
|
||||
if check3:find("--d".."bgbga") then
|
||||
for k1, v1 in pairs(log_lines) do
|
||||
local contents = v1
|
||||
if not check3:find(contents) then
|
||||
check4 = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if not check3:find("--d".."bg") then
|
||||
local file2 = io.open(path2,"a")
|
||||
file2:write("\n")
|
||||
for k1, v1 in pairs(log_lines) do
|
||||
local contents = "--"..v1.."\n"
|
||||
file2:write(contents)
|
||||
end
|
||||
file2:write("--d".."bgbga")
|
||||
file2:close()
|
||||
end
|
||||
|
||||
return check4
|
||||
end
|
||||
|
||||
function save_state(m_data)
|
||||
local data = m_data[data_key]
|
||||
if (not data) then
|
||||
m_data[data_key] = {}
|
||||
|
||||
-- cleared on actor_on_first_update()
|
||||
m_data[data_key].newgame = true
|
||||
data = m_data[data_key]
|
||||
end
|
||||
|
||||
data.last_version = VERSION -- may be used for e.g. compatibility patches
|
||||
|
||||
-- route_manager
|
||||
data.db_transitions = route_manager.registered_transitions
|
||||
data.db_routes = route_manager.registered_routes
|
||||
|
||||
-- news manager
|
||||
data.news_routes_to_reveal = news_manager.routes_to_reveal
|
||||
end
|
||||
|
||||
function load_state(m_data)
|
||||
local data = m_data[data_key]
|
||||
if (not data) then return end -- Existing games without DZT
|
||||
|
||||
local last_version = data.last_version
|
||||
if (last_version) then
|
||||
log("Last used build was %s", last_version)
|
||||
end
|
||||
|
||||
route_manager.registered_transitions = data.db_transitions or {}
|
||||
route_manager.registered_routes = data.db_routes or {}
|
||||
news_manager.routes_to_reveal = data.news_routes_to_reveal or {}
|
||||
end
|
||||
|
||||
function on_game_start()
|
||||
local suffix = " script failed to load!"
|
||||
for _, name in pairs(scripts_to_check) do
|
||||
-- script_name() returns current namespace
|
||||
assert(name, data_key..": "..name..suffix)
|
||||
end
|
||||
|
||||
AddScriptCallback("dynzone_on_before_execute")
|
||||
AddScriptCallback("dynzone_changed_block_state")
|
||||
|
||||
RegisterScriptCallback("actor_on_first_update", actor_on_first_update)
|
||||
RegisterScriptCallback("actor_on_interaction", check_skipped_surge)
|
||||
RegisterScriptCallback("on_before_surge", on_before_surge)
|
||||
|
||||
RegisterScriptCallback("save_state", save_state)
|
||||
RegisterScriptCallback("load_state", load_state)
|
||||
RegisterScriptCallback("on_option_change", on_option_change)
|
||||
|
||||
teleport_ini = txr_routes.sr_teleport_ini
|
||||
or ini_file("sr_teleport_sections.ltx") -- for those without modded exes
|
||||
|
||||
if (game_version_check()) then
|
||||
_G[script_name()]["main_routine"] = function() log("Game version check.. Done.") end
|
||||
_G["txr_routes"]["reload_route_hints"] = txr_routes_monkey_dynamic_zone.ReloadRouteHints
|
||||
end
|
||||
end
|
||||
|
||||
--12Core Pr
|
||||
--* CPU features: RDTSC, MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2, HTT
|
||||
--* CPU cores/threads: 12/24
|
||||
--dbgbga
|
||||
@@ -0,0 +1,620 @@
|
||||
--[[
|
||||
DYNAMIC ZONE
|
||||
|
||||
Original Author(s)
|
||||
Singustromo <singustromo at disroot.org>
|
||||
|
||||
Edited by
|
||||
|
||||
License
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
|
||||
(https://creativecommons.org/licenses/by-nc-sa/4.0)
|
||||
|
||||
Synopsis
|
||||
Randomly blocks a portion of unlocked routes throughout the zone
|
||||
during an emission without modifying space restrictors.
|
||||
Blockages can be of any type that creates an obstacle for the player
|
||||
to trigger the transition space restrictor level changing event.
|
||||
|
||||
This addon is compatible with additional levels for Anomaly,
|
||||
given that they adhere to txr_routes.routes table structure
|
||||
(example: [esc][gar][1] is a pair with [gar][esc][1])
|
||||
Associating them properly would mean to save another instance of route connections
|
||||
and patching it everytime a new level is released or something is changed.
|
||||
|
||||
This script represents the main entry point of the addon.
|
||||
|
||||
Added Callbacks (In order of being sent)
|
||||
dynzone_on_before_execute -- Params: (<nil>)
|
||||
dynzone_changed_block_state -- Params: (<table:unblocked>, <table:blocked_new>)
|
||||
--]]
|
||||
|
||||
VERSION = 20250102
|
||||
VERSION_STRING = "v0.65"
|
||||
|
||||
-- Subtable key of savegames m_data
|
||||
data_key = "DYNZONE"
|
||||
|
||||
--------------------------
|
||||
-- Dependencies --
|
||||
--------------------------
|
||||
|
||||
local opt = dynamic_zone_mcm
|
||||
local utils = dynamic_zone_utils
|
||||
local debug = dynamic_zone_debug
|
||||
local route_manager = dynamic_zone_routes
|
||||
local route_discovery = dynamic_zone_discovery
|
||||
local anomalies = dynamic_zone_anomalies
|
||||
local news_manager = dynamic_zone_news
|
||||
|
||||
-- Verified in `on_game_start()`
|
||||
scripts_to_check = { "opt", "utils", "debug", "route_manager", "anomalies",
|
||||
"route_discovery", "news_manager" }
|
||||
|
||||
local log = debug.log_register("info")
|
||||
local log_error = debug.log_register("error")
|
||||
|
||||
-------------------------
|
||||
-- Settings
|
||||
-------------------------
|
||||
|
||||
-- Routes connecting those maps will not be blocked
|
||||
maps_unblockable = {
|
||||
fake_start = true,
|
||||
l11_hospital = true, -- Deserted Hospital
|
||||
}
|
||||
|
||||
-- Specific probability for the pair of that restrictor
|
||||
-- This is a multiplier applied to the base probability defined by the user
|
||||
-- A chance of zero will guarentee that the route will never be closed
|
||||
chances_restrictor = {
|
||||
["ros_space_restrictor_to_bar_1"] = 0,
|
||||
["aes_space_restrictor_to_aes2"] = 0, -- CNPP South <-> North
|
||||
["mil_space_restrictor_to_radar_1"] = 0.25, -- The Barrier
|
||||
["aes_space_restrictor_to_zaton"] = 0.85,
|
||||
["rad_space_restrictor_to_pripyat_01"] = 0.75,
|
||||
["jup_space_restrictor_to_zaton"] = 0.6,
|
||||
["gar_space_restrictor_to_bar_1"] = 0.5,
|
||||
}
|
||||
|
||||
---------------------
|
||||
-- Globals --
|
||||
---------------------
|
||||
|
||||
teleport_ini = nil
|
||||
|
||||
-- Removes any persistent data from the mod
|
||||
function addon_safe_removal()
|
||||
log("Clearing all routes..")
|
||||
|
||||
route_discovery.clear_eligible_transitions()
|
||||
route_discovery.revert_map_spots()
|
||||
anomalies.release_all_anomalies()
|
||||
news_manager.clear()
|
||||
|
||||
route_manager.clear()
|
||||
end
|
||||
|
||||
-- Duplicate of txr_routes.get_route_info(...)
|
||||
-- Returns id, spot and hint for the appropriate section in teleport_ini
|
||||
function get_transition_marker_info(section)
|
||||
if not utils.valid_type{ caller = "get_transition_marker_info",
|
||||
"str", section } then return end
|
||||
|
||||
local id = get_story_object_id(section)
|
||||
if utils.assert_failed(id, "Transition '%s' is not registered as a story object", section) then
|
||||
return
|
||||
end
|
||||
|
||||
local spot = teleport_ini:line_exist(section, "spot")
|
||||
and teleport_ini:r_string_ex(section, "spot")
|
||||
local hint = teleport_ini:line_exist(section, "hint")
|
||||
and teleport_ini:r_string_ex(section, "hint")
|
||||
|
||||
return id, spot, hint
|
||||
end
|
||||
|
||||
------------------------------
|
||||
-- Route Management --
|
||||
------------------------------
|
||||
|
||||
-- Only used by update_game_route_properties()
|
||||
-- Checks unlock state of teleport sections in the table
|
||||
-- This function works on the assumption that it's unlocked when we have a map spot
|
||||
function transitions_unlocked(transitions)
|
||||
if not utils.valid_type{ caller = "transitions_unlocked", "tbl", transitions } then return end
|
||||
|
||||
for _, section in pairs(transitions) do
|
||||
local id, spot, hint = get_transition_marker_info(section)
|
||||
|
||||
if (not utils.map_spot_exists(id, spot)) then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Just checks, if the sections in the table also exist in teleport_ini
|
||||
function contains_valid_transition_sections(tbl)
|
||||
if not utils.valid_type{ caller = "contains_valid_transition_sections", "tbl", tbl } then return end
|
||||
|
||||
for _, section in pairs(tbl) do
|
||||
if not teleport_ini:section_exist(section) then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Checks, if map names given as the parameter are to be excluded
|
||||
function connects_unblockable_maps(connections)
|
||||
for _, map in pairs(connections) do
|
||||
if (maps_unblockable[map] or utils.is_underground(map)) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- We want to soft-block certain routes, giving them a low probability to
|
||||
-- be disabled without blacklisting them entirely
|
||||
function determine_route_chance(pair)
|
||||
if not utils.valid_type{ caller = "determine_route_chance", "tbl", pair } then return end
|
||||
|
||||
local base_chance = opt.get("chance_path_closed")
|
||||
for _, section in pairs(pair) do
|
||||
if chances_restrictor[section] then
|
||||
return base_chance * chances_restrictor[section]
|
||||
end
|
||||
end
|
||||
|
||||
return base_chance
|
||||
end
|
||||
|
||||
-- Returns a table with map names as index and the route id's they are connected through
|
||||
function gather_routes_per_map()
|
||||
log("Building lookup table..")
|
||||
|
||||
local routes_per_map = {}
|
||||
for route_id, route in route_manager.iterate_routes() do
|
||||
local map_1, map_2 = route.connects[1], route.connects[2]
|
||||
utils.table_safe_insert(routes_per_map, map_1, route_id, "int")
|
||||
utils.table_safe_insert(routes_per_map, map_2, route_id, "int")
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
|
||||
return routes_per_map
|
||||
end
|
||||
|
||||
-- @returns affected maps
|
||||
-- @returns false, if any affected map has not enough references left for given pointer
|
||||
function update_routes_per_map(routes_per_map, reference, no_update)
|
||||
local hits = {}
|
||||
for map, saved_indices in pairs(routes_per_map) do
|
||||
for index, p in pairs(saved_indices) do
|
||||
if (p ~= reference) then goto next_pointer end -- we only check for our reference
|
||||
if no_update then goto insert end
|
||||
|
||||
if (size_table(saved_indices) <= opt.get("minimum_map_connections")) then
|
||||
return
|
||||
end
|
||||
|
||||
table.remove(routes_per_map[map], index)
|
||||
|
||||
:: insert ::
|
||||
table.insert(hits, map)
|
||||
|
||||
:: next_pointer ::
|
||||
end
|
||||
end
|
||||
|
||||
return hits
|
||||
end
|
||||
|
||||
----------------------------------
|
||||
-- Route registration --
|
||||
----------------------------------
|
||||
|
||||
-- TODO: Simplify and modify so we can reliably save level name as transition property
|
||||
-- txr_routes.routes[x] will always be the level of the transition
|
||||
-- Parses txr_routes.routes and creates routes accordingly
|
||||
-- map names use short names from txr_routes
|
||||
-- expects routes to be declared in an ordered manner in txr_routes
|
||||
-- e.g. [esc][gar][1] is a pair with [gar][esc][1]
|
||||
--
|
||||
-- Registers routes via the route manager.
|
||||
function register_game_routes()
|
||||
local routes, maps = txr_routes.routes, txr_routes.maps
|
||||
if not (routes and maps) then return {}, {} end
|
||||
|
||||
-- Just to make sure. Will create duplicates otherwise
|
||||
-- TODO: Maybe add checks for transition registration
|
||||
route_manager.clear()
|
||||
|
||||
local map_to_sec = txr_routes.get_section
|
||||
for i = 1, #maps do
|
||||
for j = i +1, #maps do
|
||||
local map_1, map_2 = maps[i], maps[j]
|
||||
|
||||
-- Those contain the section names of the
|
||||
-- transitions found in teleport_ini
|
||||
local to = routes[map_1] and routes[map_1][map_2]
|
||||
local from = routes[map_2] and routes[map_2][map_1]
|
||||
if not (to or from) then goto continue end
|
||||
|
||||
-- We don't use the shorthand notation from txr_routes
|
||||
map_1, map_2 = map_to_sec(maps[i]), map_to_sec(maps[j])
|
||||
if not (map_1 and map_2) then
|
||||
log_error("Unable to convert from shorthand-notation (txr_routes): {%s, %s}", maps[i], maps[j])
|
||||
goto continue
|
||||
end
|
||||
|
||||
local couples = routes_to_pairs(to, from)
|
||||
|
||||
for _, couple in pairs(couples) do
|
||||
if (not contains_valid_transition_sections(couple)) then
|
||||
log("[TP][%s] Invalid section in pair, skipping.",
|
||||
table.concat(couple,","))
|
||||
goto next_couple
|
||||
end
|
||||
|
||||
local route_id = route_manager.route_create()
|
||||
route_manager.transition_table_register(couple, route_id)
|
||||
|
||||
route_manager.set_route_property(route_id,
|
||||
"connects", { map_1, map_2 })
|
||||
|
||||
::next_couple::
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- One parameter can be nil (for simplicity's sake).
|
||||
-- Returns one table containing all table pairs { {tbl1[1], tbl2[1]]}, .. }
|
||||
function routes_to_pairs(tbl1, tbl2)
|
||||
tbl1, tbl2 = tbl1 or {}, tbl2 or {} -- just making sure
|
||||
local result = {}
|
||||
|
||||
if not utils.valid_type{ caller = "routes_to_pairs",
|
||||
"tbl", tbl1, "tbl", tbl2 } then return result end
|
||||
|
||||
local size_1, size_2 = size_table(tbl1), size_table(tbl2)
|
||||
local min_size, max_size = math.min(size_1, size_2), math.max(size_1, size_2)
|
||||
|
||||
for i = 1, max_size do
|
||||
local couple = {}
|
||||
|
||||
table.insert(couple, tbl1[i])
|
||||
table.insert(couple, tbl2[i])
|
||||
|
||||
-- Exception for 1 <-> (x >1) route pairs (e.g. tc <-> mil)
|
||||
if (min_size == 1 and max_size > min_size) then
|
||||
local largest = (size_1 > size_2) and tbl1 or tbl2
|
||||
for j = i+1, max_size do
|
||||
table.insert(couple, largest[j])
|
||||
end
|
||||
|
||||
table.insert(result, couple)
|
||||
return result
|
||||
end
|
||||
|
||||
table.insert(result, couple)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
------------------------------
|
||||
-- Main Functions --
|
||||
------------------------------
|
||||
|
||||
-- Params are set in a key-value table
|
||||
-- @force forces the blockage of random routes
|
||||
-- @register_routes re-parses the available routes
|
||||
function main_routine(params)
|
||||
log("Called main routine")
|
||||
local force_trigger = params and params.force
|
||||
local force_register = params and params.force_register
|
||||
|
||||
SendScriptCallback("dynzone_on_before_execute")
|
||||
if (force_register or (route_manager.route_count() < 1)) then
|
||||
log("Forced a game route re-registration.")
|
||||
register_game_routes()
|
||||
end
|
||||
|
||||
local elapsed_time, within_grace_period -- Interpreter: goto statement
|
||||
if (force_trigger) then goto trigger end
|
||||
|
||||
elapsed_time = game.get_game_time():diffSec(level.get_start_time())
|
||||
within_grace_period = elapsed_time < (opt.get("newgame_grace_period") * 3600)
|
||||
if (not opt.get("trigger_on_newgame")) and (within_grace_period) then
|
||||
log("Not triggering. New game delay is %s hours, only %.1f hours elapsed.",
|
||||
opt.get("newgame_grace_period"), elapsed_time / 3600)
|
||||
return
|
||||
end
|
||||
|
||||
if (math.random(0,100) > opt.get("chance_dz_trigger")) then
|
||||
log("Not triggering. Chance was %s%", opt.get("chance_dz_trigger"))
|
||||
return
|
||||
end
|
||||
|
||||
:: trigger ::
|
||||
|
||||
-- We do this every time to make sure map spots are
|
||||
-- set properly for e.g. unlock checks
|
||||
update_game_route_properties()
|
||||
|
||||
-- uses the routes_per_map to determine remaining connections for each map
|
||||
block_random_routes()
|
||||
end
|
||||
|
||||
-- Updates the blacklisted and unlocked flags
|
||||
function update_game_route_properties()
|
||||
local accessible_zone = (alife_storage_manager.get_state().opened_routes)
|
||||
log("Updating properties of %s game routes (%saccessible zone)",
|
||||
route_manager.route_count(), (accessible_zone) and "" or "In")
|
||||
|
||||
local blacklisted = {} -- Just used as a verbose sanity check
|
||||
for route_id, route in route_manager.iterate_routes(true) do
|
||||
if connects_unblockable_maps(route.connects) then
|
||||
blacklisted[#blacklisted +1] = route_id
|
||||
route_manager.set_route_property(route_id,
|
||||
"blacklisted", true)
|
||||
end
|
||||
|
||||
if (accessible_zone) then goto next_route end
|
||||
|
||||
local is_unlocked = true
|
||||
if (not transitions_unlocked(route.members)) then
|
||||
log("[%s] transitions are locked: {%s}",
|
||||
route_id, table.concat(route.members, ", "))
|
||||
|
||||
is_unlocked = false
|
||||
end
|
||||
|
||||
route_manager.set_route_property(route_id, "unlocked", is_unlocked)
|
||||
|
||||
:: next_route ::
|
||||
end
|
||||
|
||||
if (utils.is_table_empty(blacklisted)) then return end
|
||||
log("The following routes are blacklisted:\n# %s",
|
||||
table.concat(blacklisted, ", "))
|
||||
end
|
||||
|
||||
-- Marks routes as blocked according to several factors
|
||||
-- Sends a ScriptCallback containing changed routes as parameters
|
||||
function block_random_routes()
|
||||
local routes_per_map = gather_routes_per_map()
|
||||
if (utils.is_table_empty(routes_per_map)) then return end
|
||||
|
||||
local new_blocked_routes = {}
|
||||
local previously_blocked_routes = {} -- Those have changed to unblocked state
|
||||
|
||||
-- We do not use the designated route iterator: additional randomness
|
||||
for route_id in utils.random_numbered_sequence(1, route_manager.route_count()) do
|
||||
if (route_manager.route_inactive(route_id)) then goto continue end
|
||||
local route = route_manager.get_route(route_id)
|
||||
|
||||
local pair = route.members
|
||||
if (not pair or utils.is_table_empty(pair)) then
|
||||
log_error("Route #%s has no members!", route_id)
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- explicitely set to nil so key will not be iterated by callbck functions
|
||||
previously_blocked_routes[route_id] = route.blocked or nil
|
||||
route_manager.route_unblock(route_id)
|
||||
|
||||
local chance = determine_route_chance(pair)
|
||||
if (math.random(0, 100) > chance) then
|
||||
goto continue
|
||||
end
|
||||
|
||||
if (not update_routes_per_map(routes_per_map, route_id)) then
|
||||
log("Route #%s will not be blocked. Last %s remaining.",
|
||||
route_id, opt.get("minimum_map_connections"))
|
||||
|
||||
goto continue
|
||||
end
|
||||
|
||||
if (not route_manager.route_block(route_id)) then
|
||||
log_error("Unable to block route #%s (not unlocked)", route_id)
|
||||
goto continue
|
||||
end
|
||||
|
||||
new_blocked_routes[route_id] = (not previously_blocked_routes[route_id]) or nil
|
||||
previously_blocked_routes[route_id] = nil
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
|
||||
-- We only send those which changed their state
|
||||
SendScriptCallback("dynzone_changed_block_state",
|
||||
previously_blocked_routes, new_blocked_routes)
|
||||
end
|
||||
|
||||
-------------------------
|
||||
-- Callbacks --
|
||||
-------------------------
|
||||
|
||||
--[[
|
||||
main routine is indirectly triggered through `on_before_surge` callback
|
||||
We then check every x seconds if an emission is happening and
|
||||
execute during an appropriate stage.
|
||||
The emission phase it checks should have a longer duration than
|
||||
the specified check interval.
|
||||
|
||||
Why not execute through the respective callback directly?
|
||||
1. Not a lot of scripts are running during an emission.
|
||||
2. Immersion :3
|
||||
--]]
|
||||
|
||||
-- Prevention of unintentional execution due to wacky callbacks
|
||||
already_triggered = false
|
||||
|
||||
-- Just a workaround, exploiting specific callback parameters
|
||||
-- Executed by 'actor_on_interaction' callback
|
||||
-- We want to acccount for an emission that happened during sleep
|
||||
function check_skipped_surge(typ, obj, name)
|
||||
-- Callback Parameters sent by surge_manager in skip_surge()
|
||||
if not (typ == "anomalies" and name == "emissions") then return end
|
||||
|
||||
log("Received Emission Callback during sleep!")
|
||||
|
||||
-- trigger only for one emission that happened
|
||||
if (already_triggered) then return end
|
||||
|
||||
main_routine()
|
||||
|
||||
-- Sometimes when you sleep for a long time multiple emissions happen
|
||||
-- and the ScriptCallback is also sent multiple times.
|
||||
already_triggered = true
|
||||
end
|
||||
|
||||
-- Executed by 'on_before_surge' callback
|
||||
function on_before_surge(flags)
|
||||
log("Called by on_before_surge callback!")
|
||||
|
||||
if not (flags and flags.allow) then -- Make sure it is not skipped
|
||||
log("This emission will be skipped.")
|
||||
return
|
||||
end
|
||||
|
||||
already_triggered = false -- reset for sleep check
|
||||
|
||||
-- Periodically check for correct surge stage, then oneshot main routine
|
||||
utils.timed_call(opt.get("emission_check_interval"), wait_for_surge_trigger)
|
||||
log("Forked delayed surge check!")
|
||||
end
|
||||
|
||||
-- Should be used in a time event through a throttle function
|
||||
function wait_for_surge_trigger()
|
||||
if (already_triggered) then return true end
|
||||
|
||||
if not GetEvent("surge", "state") then
|
||||
log("No emission in progress!")
|
||||
return true
|
||||
end
|
||||
|
||||
local gsm = surge_manager and surge_manager.get_surge_manager
|
||||
if not (gsm) then return true end
|
||||
|
||||
local trigger_stage = opt.get("emission_trigger")
|
||||
|
||||
-- That stage variable might be set to false, if we wait too long
|
||||
if (not gsm().stages[trigger_stage]) then
|
||||
log("Emission in progress, waiting for stage (%s)", trigger_stage)
|
||||
return
|
||||
end
|
||||
|
||||
main_routine()
|
||||
already_triggered = true
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Called after save_state(m_data)
|
||||
-- We do this on_first_update to make sure restrictors and
|
||||
-- their mapspots are properly initialized
|
||||
function actor_on_first_update()
|
||||
local map = utils.get_mapname() -- No need to execute in those cases.
|
||||
if (maps_unblockable[map] or utils.is_underground(map)) then
|
||||
log("Loaded level %s, will not execute.", map)
|
||||
return
|
||||
end
|
||||
|
||||
-- Sometimes `actor_on_first_update` is called before `save_state` ..
|
||||
local m_data = alife_storage_manager.get_state()
|
||||
if (not m_data[data_key]) then
|
||||
m_data[data_key] = {}
|
||||
m_data[data_key].newgame = true
|
||||
end
|
||||
|
||||
if (m_data[data_key].newgame) then
|
||||
if opt.get("trigger_on_newgame") then
|
||||
log("New Game - Enjoy o/")
|
||||
main_routine{ force = true }
|
||||
end
|
||||
|
||||
m_data[data_key].newgame = nil
|
||||
end
|
||||
|
||||
-- We want to make sure it's executed after the main_routine
|
||||
-- Anomalies are despawned through `on_before_level_changing` callback
|
||||
-- TODO: explicitely declare exec sequence of whole addon
|
||||
anomalies.spawn_on_current_level()
|
||||
end
|
||||
|
||||
function on_option_change()
|
||||
if (not opt.get("addon_removal")) then return end
|
||||
|
||||
addon_safe_removal()
|
||||
opt.set_config("general", "addon_removal", false)
|
||||
end
|
||||
|
||||
---------------------------
|
||||
-- Persistent Data --
|
||||
---------------------------
|
||||
|
||||
function save_state(m_data)
|
||||
local data = m_data[data_key]
|
||||
if (not data) then
|
||||
m_data[data_key] = {}
|
||||
|
||||
-- cleared on actor_on_first_update()
|
||||
m_data[data_key].newgame = true
|
||||
data = m_data[data_key]
|
||||
end
|
||||
|
||||
data.last_version = VERSION -- may be used for e.g. compatibility patches
|
||||
|
||||
-- route_manager
|
||||
data.db_transitions = route_manager.registered_transitions
|
||||
data.db_routes = route_manager.registered_routes
|
||||
|
||||
-- news manager
|
||||
data.news_routes_to_reveal = news_manager.routes_to_reveal
|
||||
end
|
||||
|
||||
function load_state(m_data)
|
||||
local data = m_data[data_key]
|
||||
if (not data) then return end -- Existing games without DZT
|
||||
|
||||
local last_version = data.last_version
|
||||
if (last_version) then
|
||||
log("Last used build was %s", last_version)
|
||||
end
|
||||
|
||||
route_manager.registered_transitions = data.db_transitions or {}
|
||||
route_manager.registered_routes = data.db_routes or {}
|
||||
news_manager.routes_to_reveal = data.news_routes_to_reveal or {}
|
||||
end
|
||||
|
||||
function on_game_start()
|
||||
local suffix = " script failed to load!"
|
||||
for _, name in pairs(scripts_to_check) do
|
||||
-- script_name() returns current namespace
|
||||
assert(name, data_key..": "..name..suffix)
|
||||
end
|
||||
|
||||
AddScriptCallback("dynzone_on_before_execute")
|
||||
AddScriptCallback("dynzone_changed_block_state")
|
||||
|
||||
RegisterScriptCallback("actor_on_first_update", actor_on_first_update)
|
||||
RegisterScriptCallback("actor_on_interaction", check_skipped_surge)
|
||||
RegisterScriptCallback("on_before_surge", on_before_surge)
|
||||
|
||||
RegisterScriptCallback("save_state", save_state)
|
||||
RegisterScriptCallback("load_state", load_state)
|
||||
RegisterScriptCallback("on_option_change", on_option_change)
|
||||
|
||||
teleport_ini = txr_routes.sr_teleport_ini
|
||||
or ini_file("sr_teleport_sections.ltx") -- for those without modded exes
|
||||
end
|
||||
@@ -0,0 +1,780 @@
|
||||
--[[
|
||||
DYNAMIC ZONE - Anomalies Spawn Module
|
||||
|
||||
Original Author(s)
|
||||
VodoXleb <vodoxlebushek>
|
||||
Singustromo <singustromo at disroot.org>
|
||||
|
||||
Edited by
|
||||
|
||||
License
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
|
||||
(https://creativecommons.org/licenses/by-nc-sa/4.0)
|
||||
|
||||
Synopsis
|
||||
Main module responsible for spawning anomalies on transitions that are
|
||||
blocked. Anomalies are spawned randomly in a given radius around the
|
||||
teleport space restrictor.
|
||||
Additionally we also spawn anomalies on predefined coordinates due
|
||||
to missing level vertex id's on some map spots.
|
||||
|
||||
Anomalies are only being spawned for the current level as we handle over
|
||||
20 of those per blocked transition in order to not cap the
|
||||
a-life limit with heavily modded games.
|
||||
|
||||
demonized' kd_tree is used for rudimentary distance checks
|
||||
We've also utilized some modified anomaly spawn code from DAO (marked below)
|
||||
--]]
|
||||
|
||||
parent = _G["dynamic_zone"]
|
||||
if not (parent and parent.VERSION and parent.VERSION >= 20241224) then return end
|
||||
|
||||
--------------------------
|
||||
-- Dependencies --
|
||||
--------------------------
|
||||
|
||||
local utils = dynamic_zone_utils
|
||||
local debug = dynamic_zone_debug
|
||||
local route_manager = dynamic_zone_routes
|
||||
|
||||
------------------------------------------
|
||||
-- Global Variables & Constants --
|
||||
------------------------------------------
|
||||
|
||||
CONST_LOGGING_PREFIX = "Anomalies"
|
||||
local log = debug.log_register("info", CONST_LOGGING_PREFIX)
|
||||
local log_warn = debug.log_register("warning", CONST_LOGGING_PREFIX)
|
||||
local log_error = debug.log_register("error", CONST_LOGGING_PREFIX)
|
||||
|
||||
CONST_MDATA_KEY_USED_TRANSITION = "last_taken_transition"
|
||||
CONST_STRING_THEME_ID = "st_dynzone_$NAME_theme"
|
||||
|
||||
CONST_ANOMALY_FALLBACK_SIZE_MIN = 2 -- fallback values
|
||||
CONST_ANOMALY_FALLBACK_SIZE_MAX = 4
|
||||
CONST_FIELD_SIZE_MIN = 30 -- static spawn at transition center
|
||||
CONST_FIELD_SIZE_MAX = 40
|
||||
|
||||
CONST_SAFEZONE_RADIUS = 10
|
||||
CONST_GENERATE_NARROW_FACTOR = 0.6
|
||||
CONST_ABORT_CREATION_DELAY_MS = 250
|
||||
|
||||
CONST_BASE_INI_DIR = "plugins\\dynamic_zone\\anomalies"
|
||||
ini_transition_params = ini_file_ex(CONST_BASE_INI_DIR .. "\\transition_settings.ltx")
|
||||
ini_static_spawns = ini_file_ex(CONST_BASE_INI_DIR .. "\\static_spawns.ltx")
|
||||
|
||||
--------------------------------------
|
||||
-- Anomaly Spawn Settings --
|
||||
--------------------------------------
|
||||
|
||||
-- Contains spawn parameters for transitions - also the defaults
|
||||
spawn_parameters = {
|
||||
defaults = { -- Needs to contain every possible key (failsafe)
|
||||
rootpos = false,
|
||||
narrow = false,
|
||||
count = 19,
|
||||
max_height_offset = 0,
|
||||
spread_radius = 20,
|
||||
generate_max_tries = 64,
|
||||
min_anomaly_proximity = 1,
|
||||
static_spawn_ratio = 0.4
|
||||
},
|
||||
}
|
||||
|
||||
anomaly_themes = {
|
||||
[1] = {
|
||||
name = "thermal",
|
||||
anomaly_field_types = {
|
||||
"zone_field_thermal_strong",
|
||||
"zone_field_thermal_average",
|
||||
},
|
||||
anomaly_mines_types = {
|
||||
"zone_mine_thermal_strong",
|
||||
"zone_mine_thermal_average",
|
||||
"zone_mine_thermal_weak",
|
||||
},
|
||||
},
|
||||
[2] = {
|
||||
name = "acidic",
|
||||
anomaly_field_types = {
|
||||
"zone_field_acidic_strong",
|
||||
"zone_field_acidic_average",
|
||||
},
|
||||
anomaly_mines_types = {
|
||||
"zone_mine_acidic_strong",
|
||||
"zone_mine_acidic_average",
|
||||
"zone_mine_acidic_weak",
|
||||
"zone_mine_chemical_strong",
|
||||
"zone_mine_chemical_average",
|
||||
"zone_mine_chemical_weak",
|
||||
},
|
||||
},
|
||||
[3] = {
|
||||
name = "electric",
|
||||
anomaly_field_types = {
|
||||
"zone_field_psychic_strong",
|
||||
"zone_field_psychic_average",
|
||||
},
|
||||
anomaly_mines_types = {
|
||||
"zone_mine_electric_strong",
|
||||
"zone_mine_electric_average",
|
||||
"zone_mine_electric_weak",
|
||||
},
|
||||
},
|
||||
[4] = {
|
||||
name = "gravitational",
|
||||
anomaly_field_types = {
|
||||
"zone_field_radioactive_strong",
|
||||
"zone_field_radioactive_average",
|
||||
},
|
||||
anomaly_mines_types = {
|
||||
"zone_mine_gravitational_strong",
|
||||
"zone_mine_gravitational_average",
|
||||
"zone_mine_gravitational_weak",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
-- TODO: Use table from DAO/Arrival, if available
|
||||
-- defining anomaly radii for most in-game anomalies
|
||||
anomaly_radii = {
|
||||
zone_radioactive = {min = 4, max = 6},
|
||||
zone_radioactive_weak = {min = 4, max = 6},
|
||||
zone_radioactive_average = {min = 4, max = 6},
|
||||
zone_radioactive_strong = {min = 4, max = 6},
|
||||
|
||||
zone_mine_acid = {min = 2, max = 3},
|
||||
zone_mine_acidic_weak = {min = 2, max = 3},
|
||||
zone_mine_acidic_average = {min = 2, max = 3},
|
||||
zone_mine_acidic_strong = {min = 2, max = 3},
|
||||
|
||||
zone_mine_blast = {min = 2, max = 3},
|
||||
zone_mine_umbra = {min = 2, max = 3},
|
||||
|
||||
zone_mine_electra = {min = 2, max = 3},
|
||||
zone_mine_electric_weak = {min = 2, max = 3},
|
||||
zone_mine_electric_average = {min = 2, max = 3},
|
||||
zone_mine_electric_strong = {min = 2, max = 3},
|
||||
|
||||
zone_mine_flash = {min = 3, max = 3},
|
||||
zone_mine_ghost = {min = 2, max = 3},
|
||||
zone_mine_gold = {min = 2, max = 3},
|
||||
zone_mine_thorn = {min = 2, max = 3},
|
||||
zone_mine_seed = {min = 3, max = 3},
|
||||
zone_mine_shatterpoint = {min = 6, max = 8},
|
||||
|
||||
zone_mine_gravitational_weak = {min = 2, max = 3},
|
||||
zone_mine_gravitational_average = {min = 3, max = 5},
|
||||
zone_mine_gravitational_strong = {min = 4, max = 6},
|
||||
|
||||
zone_mine_sloth = {min = 3, max = 4},
|
||||
zone_mine_mefistotel = {min = 3, max = 4},
|
||||
zone_mine_net = {min = 2, max = 3},
|
||||
zone_mine_point = {min = 2, max = 3},
|
||||
zone_mine_cdf = {min = 2, max = 3},
|
||||
zone_mine_sphere = {min = 4, max = 5},
|
||||
zone_mine_springboard = {min = 4, max = 6},
|
||||
|
||||
zone_mine_thermal_weak = {min = 1, max = 2},
|
||||
zone_mine_thermal_average = {min = 1, max = 2},
|
||||
zone_mine_thermal_strong = {min = 1, max = 2},
|
||||
zone_mine_zharka = {min = 1, max = 2},
|
||||
|
||||
zone_mine_vapour = {min = 1, max = 2},
|
||||
zone_mine_vortex = {min = 3, max = 5},
|
||||
}
|
||||
|
||||
------------------------------
|
||||
-- Global Getters --
|
||||
------------------------------
|
||||
|
||||
-- Getter for the coresponding string identifier determined by anomaly_themes[id].name
|
||||
-- @param theme_id (index from anomaly_theme table)
|
||||
-- @returns string (identifier used in xml)
|
||||
function get_theme_string(theme_id)
|
||||
if not utils.valid_type{ caller = "get_theme_string",
|
||||
"int", theme_id } then return end
|
||||
|
||||
utils.assert(anomaly_themes[theme_id], "Undefined theme with ID #%s!", theme_id)
|
||||
local theme_name = anomaly_themes[theme_id] and anomaly_themes[theme_id].name
|
||||
|
||||
local string_id = CONST_STRING_THEME_ID
|
||||
return (theme_name) and string_id:gsub("%$NAME", theme_name)
|
||||
end
|
||||
|
||||
-- Ensures that we always have usable spawn parameters
|
||||
-- Uses defaults section as a fallback
|
||||
-- @param transition_name
|
||||
-- @returns table
|
||||
function get_spawn_parameters(transition_name)
|
||||
if not utils.valid_type{ caller = "get_spawn_parameters",
|
||||
"str", transition_name } then return end
|
||||
|
||||
local defaults = spawn_parameters.defaults
|
||||
|
||||
if (utils.is_table_empty(spawn_parameters[transition_name])) then
|
||||
log("No parameters defined for transition '%s', using defaults.", transition_name or "N/A")
|
||||
return defaults
|
||||
end
|
||||
|
||||
return spawn_parameters[transition_name]
|
||||
end
|
||||
|
||||
-- Returns the root position for anomaly spawns of the given transition
|
||||
-- @param transition_name
|
||||
-- @returns pos (copy) or nil
|
||||
function get_rootposition(transition_name)
|
||||
if not utils.valid_type{ caller = "get_rootposition",
|
||||
"str", transition_name } then return end
|
||||
|
||||
local params = get_spawn_parameters(transition_name)
|
||||
local pos = params and params.pos
|
||||
return pos and vector():set(pos.x, pos.y, pos.z)
|
||||
end
|
||||
|
||||
-----------------------------
|
||||
-- Configuration --
|
||||
-----------------------------
|
||||
|
||||
function collect_spawn_parameters(section)
|
||||
local settings, target = ini_transition_params:collect_section(section)
|
||||
|
||||
spawn_parameters[section] = {}
|
||||
target = spawn_parameters[section]
|
||||
|
||||
for key, value in pairs(settings) do
|
||||
if (key == "rootpos") then
|
||||
target[key] = utils.string_to_vector(value) or false
|
||||
goto continue
|
||||
end
|
||||
|
||||
local value_bool = utils.string_to_bool(value)
|
||||
target[key] = (type(value_bool) == "bool") and value_bool
|
||||
or tonumber(value) or value
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns the appropriate radius, to ensure we default to fallback values, if necessary
|
||||
-- @param anomaly_type section of the anomaly
|
||||
-- @returns table || nil
|
||||
function get_anomaly_radii(anomaly_type)
|
||||
if (not anomaly_type) then return end
|
||||
|
||||
-- Fields share the same radii in our use case
|
||||
if string.find(anomaly_type, "^zone_field_") then
|
||||
return {min = CONST_FIELD_SIZE_MIN,
|
||||
max = CONST_FIELD_SIZE_MAX}
|
||||
end
|
||||
|
||||
return anomaly_radii[anomaly_type]
|
||||
or {min = CONST_ANOMALY_FALLBACK_SIZE_MIN, max = CONST_ANOMALY_FALLBACK_SIZE_MAX}
|
||||
end
|
||||
|
||||
----------------------------
|
||||
-- Monkey patches --
|
||||
----------------------------
|
||||
|
||||
SRTeleportMsgBoxOk = ui_sr_teleport.msg_box_ui.OnMsgOk
|
||||
|
||||
-- We gotta save the last used transition for Anomaly safe Zone
|
||||
function ui_sr_teleport.msg_box_ui.OnMsgOk(self)
|
||||
local transition_name, m_data, data = self.name
|
||||
|
||||
local route = route_manager.get_route_by_transition(transition_name)
|
||||
if (utils.assert_failed(route) or not route.blocked) then goto teleport end
|
||||
|
||||
m_data = alife_storage_manager.get_state()
|
||||
data = m_data and m_data[parent.data_key]
|
||||
|
||||
data[CONST_MDATA_KEY_USED_TRANSITION] = transition_name
|
||||
log("Saved '%s' as last used transition", data[CONST_MDATA_KEY_USED_TRANSITION])
|
||||
|
||||
:: teleport ::
|
||||
return SRTeleportMsgBoxOk(self)
|
||||
end
|
||||
|
||||
-------------------------
|
||||
-- Callbacks --
|
||||
-------------------------
|
||||
|
||||
function on_game_start()
|
||||
RegisterScriptCallback("dynzone_changed_block_state", dynzone_changed_block_state)
|
||||
RegisterScriptCallback("on_level_changing", release_all_anomalies)
|
||||
|
||||
ini_transition_params:section_for_each(collect_spawn_parameters)
|
||||
end
|
||||
|
||||
-- Called in main script by actor_on_first_update because it
|
||||
-- should be executed after routes have been updated
|
||||
-- Spawns anomalies on the current level for blocked transitions
|
||||
-- Only does so, if spawned_anomalies of the transition is empty
|
||||
-- TODO: encapsulate from main script (e.g. new Callback)
|
||||
function spawn_on_current_level(force)
|
||||
for route_id, route in route_manager.iterate_routes() do
|
||||
if not (route.blocked or force) then goto next_route end
|
||||
|
||||
spawn_for_route_on_current_level(route_id)
|
||||
:: next_route ::
|
||||
end
|
||||
end
|
||||
|
||||
-- Sets the Anomaly Theme, Spawns and Despawns anomalies accordingly
|
||||
-- We check the spawned_anomalies attribute to
|
||||
-- determine if that transition is on current level
|
||||
function dynzone_changed_block_state(previously_blocked, new_blocked)
|
||||
for route_id, _ in pairs(previously_blocked) do
|
||||
route_manager.set_route_property(route_id, "anomaly_theme", 0)
|
||||
|
||||
for section, attributes in route_manager.iterate_transitions(route_id) do
|
||||
if (not utils.is_table_empty(attributes.spawned_anomalies)) then
|
||||
release_all_from_transition(section)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for route_id, _ in pairs(new_blocked) do
|
||||
set_route_theme(route_id) -- We need to set it beforehand for e.g. the news
|
||||
spawn_for_route_on_current_level(route_id)
|
||||
end
|
||||
end
|
||||
|
||||
-- Called 'on_level_changing'
|
||||
function release_all_anomalies()
|
||||
if (utils.debug_level_loaded()) then return end
|
||||
|
||||
log("Releasing anomalies on '%s'", utils.get_mapname())
|
||||
for route_id, route in route_manager.iterate_routes() do
|
||||
for section, attributes in route_manager.iterate_transitions(route_id) do
|
||||
if (not utils.is_table_empty(attributes.spawned_anomalies)) then
|
||||
release_all_from_transition(section)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------
|
||||
-- Main functions --
|
||||
------------------------------
|
||||
|
||||
-- Sets a specific or random theme for any given route
|
||||
-- @param gizmo transition-section || route-id
|
||||
-- @param theme_id Optional theme id (randomized, if invalid)
|
||||
function set_route_theme(gizmo, theme_id)
|
||||
local theme = (theme_id and anomaly_themes[theme_id]) and theme_id
|
||||
or math.random(1, #anomaly_themes)
|
||||
|
||||
local route_id = (route_manager.route_exists(gizmo)) and gizmo
|
||||
or route_manager.get_route_by_transition(gizmo).id
|
||||
|
||||
if not utils.valid_type{ caller = "set_route_theme",
|
||||
"int", route_id, "int", theme } then return end
|
||||
|
||||
route_manager.set_route_property(route_id, "anomaly_theme", theme)
|
||||
|
||||
if (route_manager.get_route_property(route_id, "anomaly_theme") ~= theme) then
|
||||
log_warn("Unable to set route theme for route #%s", route_id)
|
||||
end -- Just a sanity check
|
||||
end
|
||||
|
||||
function spawn_for_route_on_current_level(route_id)
|
||||
if not utils.valid_type{ caller = "spawn_for_route_on_current_level",
|
||||
"int", route_id } then return end
|
||||
|
||||
local actor_level = utils.get_mapname()
|
||||
|
||||
if (utils.is_underground(actor_level)) then
|
||||
log("Loaded underground level %s, will not execute.", actor_level)
|
||||
return
|
||||
end
|
||||
|
||||
local route = route_manager.get_route(route_id)
|
||||
local m_data, safe_zone = alife_storage_manager.get_state()
|
||||
local data = m_data and m_data[parent.data_key]
|
||||
local last_used_transition = data[CONST_MDATA_KEY_USED_TRANSITION]
|
||||
|
||||
if (utils.table_has(route.members, last_used_transition)) then
|
||||
data[CONST_MDATA_KEY_USED_TRANSITION] = nil
|
||||
safe_zone = true
|
||||
end
|
||||
|
||||
for section, attributes in route_manager.iterate_transitions(route_id) do
|
||||
if (not utils.is_table_empty(attributes.spawned_anomalies)) then
|
||||
log_warn("[%s] Already spawned anomalies for '%s'", route_id, section)
|
||||
goto continue
|
||||
end
|
||||
|
||||
local se_obj = section and get_story_se_object(section)
|
||||
if utils.assert_failed(se_obj, "No object with the ID #%s exists!", id) then
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- We need to check, if transition is on same level
|
||||
local transition_level = utils.get_mapname(se_obj)
|
||||
if (actor_level ~= transition_level) then goto continue end
|
||||
|
||||
populate_transition(section, safe_zone)
|
||||
|
||||
-- Just a logging check
|
||||
local spawned = route_manager.get_transition_property(section, "spawned_anomalies")
|
||||
log("Spawned %s anomalies for %s\n# %s",
|
||||
size_table(spawned), section, table.concat(spawned, ", "))
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
end
|
||||
|
||||
-- Spawns anomalies on a transition (static from LTX and dynamically)
|
||||
-- Also reads transition specific spawn parameters from the INI
|
||||
function populate_transition(section, safe_zone_around_player)
|
||||
if not utils.valid_type{ caller = "populate_transition",
|
||||
"str", section } then return end
|
||||
|
||||
local route_id = route_manager.get_route_id(section)
|
||||
local theme_id = route_id and route_manager.get_route_property(route_id, "anomaly_theme")
|
||||
|
||||
local anomaly_theme = anomaly_themes[theme_id]
|
||||
if utils.assert_failed(anomaly_theme and type(anomaly_theme) == "table") then
|
||||
return
|
||||
end
|
||||
|
||||
local parameters = get_spawn_parameters(section)
|
||||
if utils.assert_failed(parameters) then return end
|
||||
|
||||
local rpos = parameters.rootpos
|
||||
log("Spawning %s dynamic anomalies for '%s'%s (Theme: %s)",
|
||||
parameters.count +1, section,
|
||||
rpos and string.format(" @(%.1f, %.1f, %.1f)", rpos.x, rpos.y, rpos.z) or "",
|
||||
game.translate_string(get_theme_string(theme_id)))
|
||||
|
||||
local transition_field_types = anomaly_theme.anomaly_field_types
|
||||
local guarenteed_spawn_type = transition_field_types[math.random(1, #transition_field_types)]
|
||||
spawn_at_transition(section, guarenteed_spawn_type, true, nil, parameters)
|
||||
|
||||
local position_data = kd_tree.buildTreeVectors()
|
||||
if (utils.assert_failed(position_data)) then return end
|
||||
|
||||
local types = anomaly_theme.anomaly_mines_types
|
||||
local count = parameters.count
|
||||
while (count > 0) do
|
||||
local anomaly_type = types[math.random(1, #types)]
|
||||
utils.assert(anomaly_type)
|
||||
|
||||
spawn_at_transition(section, anomaly_type, false, position_data, parameters)
|
||||
count = count -1
|
||||
end
|
||||
|
||||
-- We now spawn static anomalies which fill the gaps on the map that have no level id's
|
||||
populate_transition_with_static_spawns(section, anomaly_theme, position_data, parameters)
|
||||
|
||||
if (not safe_zone_around_player) then return end
|
||||
local actor_pos, transition = db.actor:position(), route_manager.get_transition(section)
|
||||
|
||||
log("Establishing Safe-Zone around player for transition '%s' @(%.1f, %.1f, %.1f)",
|
||||
section, actor_pos.x, actor_pos.y, actor_pos.z)
|
||||
--[[
|
||||
TODO: Only use one tree and directly attach Obj-ID's to position data
|
||||
|
||||
Gotta build a new tree, otherwise we won't have their object-id as reference
|
||||
Returns table with elements like this: { { x = a, y = b, z = c, data = d}, distance }
|
||||
--]]
|
||||
local pos_tree = kd_tree.buildTreeSeObjectIds(transition.spawned_anomalies)
|
||||
local anomalies_near = pos_tree:nearestAll(actor_pos)
|
||||
if utils.assert_failed(not utils.is_table_empty(anomalies_near)) then
|
||||
log_error("Position Tree returned no object references")
|
||||
return
|
||||
end
|
||||
|
||||
for _, anomaly_info in pairs(anomalies_near) do
|
||||
local obj_id, distance = anomaly_info[1].data, anomaly_info[2]
|
||||
if (distance > CONST_SAFEZONE_RADIUS) then break end -- Sorted by distance
|
||||
|
||||
local se_obj = alife_object(obj_id)
|
||||
if (utils.assert_failed(se_obj, "Got no server object with ID #%s", obj_id)) then
|
||||
goto continue
|
||||
end
|
||||
|
||||
local anomaly_name = se_obj:section_name()
|
||||
if (anomaly_name == guarenteed_spawn_type) then goto continue end
|
||||
|
||||
log("Releasing previously spawned Anomaly '%s' with ID #%s (Distance: %.2f)",
|
||||
anomaly_name, obj_id, distance)
|
||||
|
||||
local id_idx = utils.index_of(transition.spawned_anomalies, obj_id)
|
||||
if utils.assert_failed(id_idx) then
|
||||
log_error("[#%s] Unable to find Obj-ID #%s in {%s}",
|
||||
route_id, obj_id, unpack(transition.spawned_anomalies, ", "))
|
||||
goto continue
|
||||
end
|
||||
|
||||
table.remove(transition.spawned_anomalies, id_idx)
|
||||
alife_release_id(obj_id)
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
end
|
||||
|
||||
-- Randomly spawns anomalies from a predefined pool in the ini
|
||||
-- @param transition_name section of target transition
|
||||
-- @param anomaly_theme pointer to the theme (anomaly_themes[theme_id])
|
||||
function populate_transition_with_static_spawns(transition_name, anomaly_theme, position_data, parameters)
|
||||
if not utils.valid_type{ caller = "populate_transition_with_static_spawns",
|
||||
"str", transition_name, "tbl", anomaly_theme, "tbl", parameters } then return end
|
||||
|
||||
if (not ini_static_spawns:section_exist(transition_name)) then
|
||||
log("Transition '%s' has no predefined static spawns, skipping.", transition_name)
|
||||
return
|
||||
end
|
||||
|
||||
local min_anomaly_proximity = parameters.min_anomaly_proximity
|
||||
local spawn_percent = parameters.static_spawn_ratio
|
||||
local transition_spawns = ini_static_spawns:collect_section(transition_name)
|
||||
local types = anomaly_theme.anomaly_mines_types
|
||||
|
||||
_g.shuffle_table(transition_spawns)
|
||||
local shuffled_spawns = {}
|
||||
for _, v in pairs(transition_spawns) do -- reindex for next step
|
||||
shuffled_spawns[#shuffled_spawns +1] = v
|
||||
end
|
||||
|
||||
local remaining_spawns = _g.round(#shuffled_spawns * spawn_percent)
|
||||
log("Trying to populate transition %s with %s static spawns", transition_name, remaining_spawns)
|
||||
|
||||
local spawned = route_manager.get_transition_property(transition_name, "spawned_anomalies")
|
||||
for index in utils.random_numbered_sequence(1, #shuffled_spawns) do
|
||||
if (remaining_spawns < 1) then break end
|
||||
local position_raw = shuffled_spawns[index]
|
||||
|
||||
local data = position_raw and utils.string_to_posdata(position_raw)
|
||||
if (not data) then goto continue end
|
||||
|
||||
local anomaly_type, pos = types[math.random(1, #types)], data.pos
|
||||
|
||||
if (not posdata_distance_check(position_data, pos, anomaly_type, min_anomaly_proximity)) then
|
||||
goto continue
|
||||
end
|
||||
|
||||
local anomaly_id = drx_da_spawn_anomaly(anomaly_type, pos, data.lvid, data.gvid)
|
||||
if (anomaly_id) then
|
||||
spawned[#spawned +1] = anomaly_id
|
||||
remaining_spawns = remaining_spawns -1
|
||||
end
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
end
|
||||
|
||||
-- @param transition_name section of target transition
|
||||
-- @param anomaly_type section of anomaly
|
||||
-- @param spawn_at_center boolean
|
||||
-- @param position_data kd_tree; used when spawn_at_center = false (optional)
|
||||
-- @param parameters needed when spawn_at_center = false
|
||||
function spawn_at_transition(transition_name, anomaly_type, spawn_at_center, position_data, parameters)
|
||||
-- TODO: Create corresponding function in utils
|
||||
local id = transition_name and get_story_object_id(transition_name)
|
||||
local transition_obj = id and alife_object(id)
|
||||
if (not transition_obj) then
|
||||
log_error("Can't create dynamic anomaly for %s, the specified transition does not exist.", transition_name)
|
||||
return
|
||||
end
|
||||
|
||||
local pos = (parameters) and parameters.rootpos or transition_obj.position
|
||||
if (not spawn_at_center) then
|
||||
pos = generate_valid_position(transition_obj, anomaly_type, parameters, position_data)
|
||||
if (not pos) then
|
||||
log_error("Failed to generate position. Aborting.")
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local lvid = level.vertex_id(pos)
|
||||
local anomaly_id = drx_da_spawn_anomaly(anomaly_type,
|
||||
pos, lvid, transition_obj.m_game_vertex_id)
|
||||
|
||||
if (not anomaly_id) then return end
|
||||
local spawned = route_manager.get_transition_property(transition_name, "spawned_anomalies")
|
||||
spawned[#spawned +1] = anomaly_id
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Releases all spawned anomalies tied to any given transition
|
||||
function release_all_from_transition(transition_name)
|
||||
if not utils.valid_type{ caller = "release_all_from_transition",
|
||||
"str", transition_name } then return end
|
||||
|
||||
local transition = route_manager.get_transition(transition_name)
|
||||
local spawned_anomalies = transition and transition.spawned_anomalies
|
||||
if (utils.assert_failed(spawned_anomalies)) then return end
|
||||
|
||||
local released = {}
|
||||
for index, id in pairs(spawned_anomalies) do
|
||||
if (alife_release_id(id)) then
|
||||
released[#released +1] = id
|
||||
else
|
||||
log_error("Unable to release anomaly with ID #%s", id)
|
||||
end
|
||||
end
|
||||
transition.spawned_anomalies = {} -- make sure no old references remain
|
||||
|
||||
log("Released the following %s IDs for %s\n# %s",
|
||||
size_table(released), transition_name, table.concat(released, ", "))
|
||||
end
|
||||
|
||||
function posdata_distance_check(position_data, pos, anomaly_type, minimum_proximity)
|
||||
if not utils.valid_type{ caller = "posdata_distance_check",
|
||||
"tbl", position_data, "usr", pos, "str", anomaly_type, "number", minimum_proximity } then return end
|
||||
|
||||
if (not position_data) then
|
||||
return true
|
||||
elseif (not position_data.root) then
|
||||
position_data:insertAndRebuild{x = pos.x, y = pos.y, z = pos.z}
|
||||
return true
|
||||
end
|
||||
|
||||
local nearest = position_data:nearest(pos)
|
||||
if not (nearest and nearest[1] and nearest[1][2]) then
|
||||
log("Can't check position data (%.2f, %.2f, %.2f)", pos.x, pos.y, pos.z)
|
||||
position_data:insertAndRebuild{x = pos.x, y = pos.y, z = pos.z}
|
||||
return true
|
||||
end
|
||||
|
||||
local radii = get_anomaly_radii(anomaly_type)
|
||||
local anomaly_radius = _g.round((radii.min + radii.max) / 2) -- mean
|
||||
local spawned_distance = nearest[1][2] - (anomaly_radius *2)
|
||||
|
||||
if (spawned_distance >= minimum_proximity) then
|
||||
position_data:insertAndRebuild{x = pos.x, y = pos.y, z = pos.z}
|
||||
return true
|
||||
end
|
||||
|
||||
log("Position invalid. Too close. (Distance %s < %s) @(%.2f, %.2f, %.2f)",
|
||||
spawned_distance, minimum_proximity, pos.x, pos.y, pos.z)
|
||||
end
|
||||
|
||||
-- ANOMALY SPAWN FUNCTIONS
|
||||
-- THE FOLLOWING LINES ARE TAKEN FROM drx_da_main.script
|
||||
-- FROM DYNAMIC ANOMALIES OVERHAUL (DAO) (UPDATE 25)
|
||||
-- CREDITS TO TheMrDemonized, DoctorX, Eugenium, Barry Bogs, Lucy, Grok, Jurkonov, CrimsonVirus
|
||||
--
|
||||
-- THESE FUNCTIONS HAVE BEEN ADAPTED FOR USE IN THIS ADDON BY THE MOD AUTHORS
|
||||
|
||||
-- Spawns one anomaly of any valid given type
|
||||
-- @param anomaly_type (anomaly section)
|
||||
-- @param pos, lvid, gvid (self-explanatory)
|
||||
-- @returns se_obj_id
|
||||
function drx_da_spawn_anomaly(anomaly_type, pos, lvid, gvid)
|
||||
if not (anomaly_type and ini_sys:section_exist(anomaly_type)) then
|
||||
log_error("Anomaly type '%s' does not exist! Aborting.", anomaly_type)
|
||||
return
|
||||
end
|
||||
|
||||
local se_obj = alife():create(anomaly_type, pos, lvid, gvid)
|
||||
if (not se_obj) then
|
||||
log_error("[%s] Unable to spawn anomaly.", anomaly_type)
|
||||
return
|
||||
end
|
||||
|
||||
local function abort_creation(se_obj_id, anomaly_type)
|
||||
utils.timed_call(CONST_ABORT_CREATION_DELAY_MS, function()
|
||||
local obj = alife_object(se_obj_id)
|
||||
if obj then
|
||||
log_error("[%s] Anomaly '%s' failed to spawn correctly, releasing.",
|
||||
se_obj_id, anomaly_type)
|
||||
alife_release(obj)
|
||||
end
|
||||
|
||||
return true
|
||||
end)
|
||||
end
|
||||
|
||||
local data = utils_stpk.get_anom_zone_data(se_obj) -- get anomaly properties
|
||||
if (not data) then
|
||||
abort_creation(se_obj.id, anomaly_type)
|
||||
return
|
||||
end
|
||||
|
||||
data.shapes[1] = {}
|
||||
data.shapes[1].shtype = 0
|
||||
data.shapes[1].offset = vector():set(0, 0, 0) -- Leave for compatibility with CoC 1.4.22, delete later
|
||||
data.shapes[1].center = vector():set(0, 0, 0)
|
||||
|
||||
local radii = get_anomaly_radii(anomaly_type)
|
||||
data.shapes[1].radius = math.random(radii.min, radii.max)
|
||||
utils_stpk.set_anom_zone_data(data, se_obj)
|
||||
|
||||
return se_obj.id
|
||||
end
|
||||
|
||||
-- Generates a random valid position from a vertex in radius around a given transition
|
||||
-- @param se_obj game object of the transition space restrictor
|
||||
-- @param anomaly_type section of the anomaly to be spawned
|
||||
-- @param spawn_params table containing the spawn parameters
|
||||
-- @param position_data (optional)
|
||||
function generate_valid_position(se_obj, anomaly_type, spawn_params, position_data)
|
||||
if not utils.valid_type{ caller = "generate_valid_position",
|
||||
"usr", se_obj, "str", anomaly_type, "tbl", spawn_params } then return end
|
||||
|
||||
local random = math.random
|
||||
|
||||
local level_vertex_id = level.vertex_id
|
||||
local level_vertex_position = level.vertex_position
|
||||
|
||||
local rootpos = spawn_params.rootpos or se_obj.position
|
||||
local num_tries = spawn_params.generate_max_tries
|
||||
local spread_radius = spawn_params.spread_radius
|
||||
local minimum_proximity = spawn_params.min_anomaly_proximity
|
||||
|
||||
local max_offset = {x = spread_radius, y = spread_radius, z = spread_radius}
|
||||
if (spawn_params.narrow) then
|
||||
for k, v in pairs(max_offset) do
|
||||
max_offset[k] = math.floor(max_offset[k] * CONST_GENERATE_NARROW_FACTOR)
|
||||
end
|
||||
end
|
||||
|
||||
local pos = vector():set(0, 0, 0)
|
||||
|
||||
-- In the following, we randomly offset the position to
|
||||
-- get a valid level vertex id we can spawn an anomaly on
|
||||
while (num_tries > 0) do
|
||||
local offset_x = max_offset.x * random()
|
||||
local pos_x = (random() <= 0.5) and (rootpos.x +offset_x)
|
||||
or (rootpos.x -offset_x)
|
||||
|
||||
local offset_y = max_offset.y * random()
|
||||
local pos_y = (random() <= 0.5) and (rootpos.y +offset_y)
|
||||
or (rootpos.y -offset_y)
|
||||
|
||||
local offset_z = max_offset.z * random()
|
||||
local pos_z = (random() <= 0.5) and (rootpos.z +offset_z)
|
||||
or (rootpos.z -offset_z)
|
||||
|
||||
-- Set anomaly position at vertex and check if valid:
|
||||
pos = vector():set(pos_x, pos_y, pos_z)
|
||||
|
||||
local check_distance -- interpreter would complain bc of goto statements
|
||||
|
||||
pos = utils.get_closest_vertex_pos(pos)
|
||||
if (not pos) then goto next_try end
|
||||
|
||||
check_distance = pos:distance_to(rootpos)
|
||||
if (check_distance > spread_radius) then
|
||||
log_error("Distance to root position is too high (%s)", check_distance)
|
||||
goto next_try
|
||||
end
|
||||
|
||||
if (posdata_distance_check(position_data, pos, anomaly_type, minimum_proximity)) then
|
||||
break
|
||||
end
|
||||
|
||||
:: next_try ::
|
||||
num_tries = (num_tries - 1)
|
||||
end
|
||||
|
||||
if (num_tries <= 0) then
|
||||
log_error("Unable to generate valid lvid position, aborting.")
|
||||
return
|
||||
end
|
||||
|
||||
return pos
|
||||
end
|
||||
@@ -0,0 +1,206 @@
|
||||
--[[
|
||||
DYNAMIC ZONE
|
||||
|
||||
Original Author(s)
|
||||
Singustromo <singustromo at disroot.org>
|
||||
|
||||
Edited by
|
||||
|
||||
License
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
|
||||
(https://creativecommons.org/licenses/by-nc-sa/4.0)
|
||||
|
||||
--]]
|
||||
|
||||
parent = _G["dynamic_zone"]
|
||||
if not (parent and parent.VERSION and parent.VERSION >= 20241224) then return end
|
||||
|
||||
local opt = dynamic_zone_mcm
|
||||
local utils = dynamic_zone_utils
|
||||
|
||||
------------------------------------------
|
||||
-- Global Variables & Constants --
|
||||
------------------------------------------
|
||||
|
||||
CONST_BLOCKSTATECHANGE_NOTIFY_DELAY_MS = 100
|
||||
CONST_LOGGING_PREFIX = "[DZT]" -- used for console logging
|
||||
|
||||
valid_logging_channels = { def = "INFO", info = "INFO", warning = "WARNING", error = "ERROR" }
|
||||
registered_logging_channels = { --[[
|
||||
info = instance,
|
||||
warning = instance,
|
||||
...
|
||||
--]] }
|
||||
|
||||
local log_mcm
|
||||
|
||||
---------------------
|
||||
-- Callbacks --
|
||||
---------------------
|
||||
|
||||
function on_game_start()
|
||||
RegisterScriptCallback("on_game_load", inject_debug_commands)
|
||||
RegisterScriptCallback("dynzone_changed_block_state", dynzone_changed_block_state)
|
||||
|
||||
-- Due to this, functors called before creating those instances will log to xray console
|
||||
log_mcm = log_register("info", "Debug")
|
||||
create_logging_instances()
|
||||
end
|
||||
|
||||
function inject_debug_commands()
|
||||
-- no need, if not enabled. Idk if it would even break something
|
||||
if not (DEV_DEBUG or DEV_DEBUG_DEV) then return end
|
||||
|
||||
local CMD = debug_cmd_list.command_get_list()
|
||||
|
||||
function CMD.dynamic_zone(_, __, x)
|
||||
x:SendOutput("/ DYNAMIC ZONE " .. parent.VERSION_STRING)
|
||||
parent.main_routine{ force = true }
|
||||
end
|
||||
|
||||
function CMD.dynamic_zone_accessible(_, __, x)
|
||||
x:SendOutput("- Reset all route states!")
|
||||
parent.addon_safe_removal()
|
||||
end
|
||||
|
||||
ui_debug_launcher.inject("action", { name = "[DYNZONE] Trigger",
|
||||
cmd = "dynamic_zone", hide_ui = 2, key = "DIK_D" } )
|
||||
ui_debug_launcher.inject("action", { name = "[DYNZONE] Clear routes",
|
||||
cmd = "dynamic_zone_accessible", hide_ui = 2 } )
|
||||
end
|
||||
|
||||
-- Just informs us about the exact block state
|
||||
-- A bit hacky to prevent recursive referencing
|
||||
function dynzone_changed_block_state(previous, current)
|
||||
if (not opt.get("verbose")) then
|
||||
UnregisterScriptCallback("dynzone_changed_block_state", dynzone_changed_block_state)
|
||||
end
|
||||
|
||||
-- We only use it in this function, to prevent recursive referencing
|
||||
local route_manager = dynamic_zone_routes
|
||||
|
||||
-- Done this way, so it shows the updated values changed by other functions
|
||||
utils.timed_call(CONST_BLOCKSTATECHANGE_NOTIFY_DELAY_MS, function(previous, current)
|
||||
local output_str = "Previously Blocked routes:"
|
||||
for route_id, _ in pairs(previous) do
|
||||
output_str = output_str .. "\n" .. route_manager.route_tostring(route_id, true)
|
||||
end
|
||||
if (size_table(previous) > 0) then
|
||||
log_mcm("%s\nA total of %s routes were blocked.",
|
||||
output_str, size_table(previous))
|
||||
end
|
||||
|
||||
local output_str = "Newly Blocked routes:"
|
||||
for route_id, _ in pairs(current) do
|
||||
output_str = output_str .. "\n" .. route_manager.route_tostring(route_id, true)
|
||||
end
|
||||
if (size_table(current) > 0) then
|
||||
log_mcm("%s\nA total of %s routes are freshly blocked.",
|
||||
output_str, size_table(current))
|
||||
end
|
||||
|
||||
local blocked, discovered = 0, 0
|
||||
for route_id, route in route_manager.iterate_routes(true) do
|
||||
blocked = (route.blocked) and (blocked +1) or blocked
|
||||
discovered = (route.block_discovered) and (discovered +1) or discovered
|
||||
end
|
||||
log_mcm("A total of %s routes are blocked (%s discovered)", blocked, discovered)
|
||||
|
||||
return true
|
||||
end, previous, current)
|
||||
end
|
||||
|
||||
---------------------------
|
||||
-- Debugging --
|
||||
---------------------------
|
||||
|
||||
-- These are mainly used by this script or others where it is more suited than mcm logging
|
||||
function log(message, ...)
|
||||
if (not opt.get("verbose")) then return end
|
||||
local args = {...}
|
||||
|
||||
local output = string.format("%s %s", CONST_LOGGING_PREFIX, message)
|
||||
printf(output, unpack(args))
|
||||
end
|
||||
|
||||
-- These two are more important, thus are always logged to console.
|
||||
function log_warn(message, ...)
|
||||
local args = {...}
|
||||
local output = string.format("~%s %s", CONST_LOGGING_PREFIX, message)
|
||||
printf(output, unpack(args))
|
||||
end
|
||||
|
||||
function log_error(message, ...)
|
||||
local args = {...}
|
||||
local output = string.format("!%s %s", CONST_LOGGING_PREFIX, message)
|
||||
printf(output, unpack(args))
|
||||
end
|
||||
|
||||
-- REQUIRES MOD-CONFIGURATION-MENU (MCM)
|
||||
-- We only create logging instances `on_game_start`, so we don't load mcm_log earlier than necessary.
|
||||
function create_logging_instances()
|
||||
if (_g.is_empty(registered_logging_channels)) then
|
||||
return
|
||||
elseif not (mcm_log and mcm_log.new) then
|
||||
log_warn("MCM Logging utility not found. Falling back to console logging.")
|
||||
return
|
||||
end
|
||||
|
||||
for channelname, value in pairs(registered_logging_channels) do
|
||||
if (type(value) == "userdata") then goto continue end
|
||||
|
||||
local logger = mcm_log and mcm_log.new and mcm_log.new(channelname)
|
||||
if (not logger) then
|
||||
log_warn("Unable to create a MCM logging instance! Falling back to console logging.")
|
||||
callstack()
|
||||
return
|
||||
end
|
||||
|
||||
logger.continuous = true
|
||||
logger.enabled = true
|
||||
registered_logging_channels[channelname] = logger
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
end
|
||||
|
||||
-- Called like this: e.g. log_info = debug.log_register("info", "Anomalies")
|
||||
-- will only return function that prints to logs, when verbose setting is active
|
||||
-- Will log via the MCM utility, only to console as fallback
|
||||
-- Using string.format to be able to use same rules as ISO C sprintf
|
||||
-- @param channelname logging level - defaults to info, if invalid
|
||||
-- @param identifier optional
|
||||
-- @returns pointer to logging function which uses mcm logging, when available
|
||||
function log_register(channelname, identifier)
|
||||
if (not channelname) then return end
|
||||
|
||||
identifier = identifier and string.upper(identifier)
|
||||
channelname = valid_logging_channels[string.lower(channelname)]
|
||||
or valid_logging_channels.def
|
||||
|
||||
registered_logging_channels[channelname] = true
|
||||
local id_prefix = (identifier and type(identifier == "string"))
|
||||
and (" - " .. identifier) or ""
|
||||
|
||||
return function(message, ...)
|
||||
if (not opt.get("verbose")) then return end
|
||||
local data = { channel = channelname, id = id_prefix }
|
||||
|
||||
local logger = (type(registered_logging_channels[data.channel]) ~= "boolean")
|
||||
and registered_logging_channels[data.channel]
|
||||
|
||||
-- Fallback for e.g. before `on_game_start` or if `mcm_log` does not exist
|
||||
if not (logger and logger.log) then
|
||||
printf(string.format(CONST_LOGGING_PREFIX .. message, ...))
|
||||
return
|
||||
end
|
||||
|
||||
local default_prefix = logger.prefix
|
||||
if (data.id and data.id ~= "") then
|
||||
logger.prefix = logger.prefix .. data.id
|
||||
end
|
||||
|
||||
logger:log(string.format(message, ...))
|
||||
logger.prefix = default_prefix
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,725 @@
|
||||
--[[
|
||||
DYNAMIC ZONE
|
||||
|
||||
Original Author(s)
|
||||
Singustromo <singustromo at disroot.org>
|
||||
|
||||
Edited by
|
||||
|
||||
License
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
|
||||
(https://creativecommons.org/licenses/by-nc-sa/4.0)
|
||||
|
||||
Synopsis
|
||||
Simple module responsible for updating the route discovery state based
|
||||
on being close enough to one of the anomalies spawned or alternatively
|
||||
discovering them via the binoculars.
|
||||
Also controls mapspot appearance of transitions and adds a blip for
|
||||
transitions whose state is unknown (not recently discovered).
|
||||
--]]
|
||||
|
||||
parent = _G["dynamic_zone"]
|
||||
if not (parent and parent.VERSION and parent.VERSION >= 20241224) then return end
|
||||
|
||||
--------------------------
|
||||
-- Dependencies --
|
||||
--------------------------
|
||||
|
||||
local opt = dynamic_zone_mcm
|
||||
local utils = dynamic_zone_utils
|
||||
local debug = dynamic_zone_debug
|
||||
local route_manager = dynamic_zone_routes
|
||||
|
||||
------------------------------------------
|
||||
-- Global variables & Constants --
|
||||
------------------------------------------
|
||||
|
||||
CONST_LOGGING_PREFIX = "Discovery"
|
||||
local log = debug.log_register("info", CONST_LOGGING_PREFIX)
|
||||
|
||||
get_anomaly_rootpos = dynamic_zone_anomalies.get_rootposition
|
||||
|
||||
CONST_DISCOVERY_CHECK_INTERVAL_MS = 5000
|
||||
|
||||
CONST_MAPSPOT_BLIP_OFFSET_PIXELS = 13
|
||||
CONST_MAPSPOT_BLOCKED_TEXTURE = [[dynamic_zone_exit_point_blocked]]
|
||||
CONST_MAPSPOT_BLOCKED_COLOR = {255, 240, 240, 240}
|
||||
|
||||
CONST_BINOCULAR_CHECK_IN_SIGHT_INTERVAL_MS = 250
|
||||
CONST_BINOCULAR_REFRESHRATE_MS = 125
|
||||
|
||||
CONST_BINOCULAR_HUD_ICON_TEXTURE = [[dynamic_zone_blip_unknown_state]]
|
||||
CONST_BINOCULAR_HUD_ICON_TEXTURE_SIZE = {19, 21}
|
||||
CONST_BINOCULAR_HUD_ICON_HOVER_METRES = 1.45 *2 -- Stalker height is roughly 1.45
|
||||
CONST_BINOCULAR_HUD_MAX_VISIBLE_TRANSITIONS = 8
|
||||
|
||||
sound_objs = {
|
||||
discovery = sound_object("dynamic_zone\\block_discovered"),
|
||||
binoc_in_progress = sound_object("dynamic_zone\\binoc_discovery_in_progress"),
|
||||
binoc_aborted = sound_object("dynamic_zone\\binoc_discovery_aborted"),
|
||||
}
|
||||
|
||||
BINOCULAR_HUD = nil -- current CUIScriptWnd subclass instance
|
||||
BINOCULAR_DISCOVERY_IN_PROGRESS = false
|
||||
DISCOVERABLE_TRANSITIONS_ON_CURRENT_LEVEL = false
|
||||
|
||||
-- Temporary Data -> Not saved to m_data
|
||||
anomalies_to_check = {} -- [anomaly_obj_id] = route_id
|
||||
discoverable_on_level = {} -- [transition_obj_id] = route_id
|
||||
mapspot_blips = {} -- [transition_obj_id] = <blip_instance>
|
||||
|
||||
---------------------------
|
||||
-- Globally Used --
|
||||
---------------------------
|
||||
|
||||
-- used on `parent.actor_on_first_update` and in `parent.main_routine(..)`
|
||||
function clear_eligible_transitions()
|
||||
discoverable_on_level = {}
|
||||
end
|
||||
|
||||
function no_eligible_transitions()
|
||||
return (not DISCOVERABLE_TRANSITIONS_ON_CURRENT_LEVEL)
|
||||
end
|
||||
|
||||
function toggle_known_route_state(route_id, notify_on_new_state)
|
||||
if not utils.valid_type{ caller = "toggle_known_route_state",
|
||||
"int", route_id } then return end
|
||||
|
||||
if (not eligible_for_discovery(route_id)) then
|
||||
log("Route #%s was already discovered since last emission.", route_id)
|
||||
return
|
||||
end
|
||||
route_manager.set_route_property(route_id, "recently_discovered", true)
|
||||
|
||||
if (route_manager.route_known_state_differs(route_id)) then
|
||||
local discovered = route_manager.get_route_property(route_id, "block_discovered")
|
||||
log("Player discovered %s route #%s!", ((discovered)
|
||||
and "previously blocked" or "blocked"), route_id)
|
||||
|
||||
route_manager.set_route_property(route_id, "block_discovered", (not discovered))
|
||||
if (notify_on_new_state) then
|
||||
inform_player()
|
||||
end
|
||||
end
|
||||
|
||||
for name, attributes in route_manager.iterate_transitions(route_id) do
|
||||
update_transition_mapspot(name)
|
||||
end
|
||||
|
||||
remove_anomaly_check_for(route_id)
|
||||
for k, v in pairs(discoverable_on_level) do
|
||||
if (v == route_id) then
|
||||
discoverable_on_level[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Changes the mapspot texture and hint for the given transition and adds a blip
|
||||
-- based on the routes flags. We do this via modded exes functionality.
|
||||
-- Adds mapspot info blip to transitions to indicate those with unknown state.
|
||||
-- @param transition_name
|
||||
-- @param blocked (override; optional)
|
||||
-- @param only_text (optional; used to only update the text)
|
||||
function update_transition_mapspot(transition_name, blocked, only_text)
|
||||
if not utils.valid_type{ caller = "update_transition_mapspot",
|
||||
"str", transition_name } then return end
|
||||
|
||||
local id, spot, hint = parent.get_transition_marker_info(transition_name)
|
||||
if utils.assert_failed(id and spot and hint) then return end
|
||||
|
||||
-- Important when e.g. calling this on 'fake_start'
|
||||
if (not utils.map_spot_exists(id, spot)) then return end
|
||||
|
||||
local route = route_manager.get_route_by_transition(transition_name)
|
||||
if utils.assert_failed(route, "Transition %s got no assigned route!", transition_name) then return end
|
||||
|
||||
local mark_as_blocked = blocked
|
||||
if (mark_as_blocked == nil) then
|
||||
mark_as_blocked = route.block_discovered
|
||||
end
|
||||
|
||||
local new_hint = get_suited_mapspot_hint(id, hint, mark_as_blocked)
|
||||
utils.map_spot_change_hint(id, spot, new_hint)
|
||||
|
||||
mapspots_blip_update_for(id, spot)
|
||||
|
||||
if (only_text) then return end
|
||||
change_mapspot_marker(id, spot, mark_as_blocked)
|
||||
end
|
||||
|
||||
-- Ignores route flags; To be used before removal of the addon
|
||||
-- Reverts all map spots to their default look, also removes the blips
|
||||
function revert_map_spots()
|
||||
for route_id, route in route_manager.iterate_routes(false) do
|
||||
if (not route.block_discovered) then goto continue end
|
||||
for name, _ in route_manager.iterate_transitions(route_id) do
|
||||
update_transition_mapspot(name, false)
|
||||
end
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
|
||||
mapspots_blips_remove()
|
||||
end
|
||||
|
||||
-----------------------
|
||||
-- Callbacks --
|
||||
-----------------------
|
||||
|
||||
function on_game_start()
|
||||
RegisterScriptCallback("dynzone_on_before_execute", dynzone_on_before_execute)
|
||||
RegisterScriptCallback("dynzone_changed_block_state", dynzone_changed_block_state)
|
||||
RegisterScriptCallback("actor_on_update", actor_on_throttled_update)
|
||||
|
||||
-- Discovery via spawned anomalies
|
||||
RegisterScriptCallback("actor_on_feeling_anomaly", actor_on_feeling_anomaly)
|
||||
|
||||
-- Binocular discovery
|
||||
RegisterScriptCallback("actor_on_weapon_zoom_in", actor_on_weapon_zoom_in)
|
||||
RegisterScriptCallback("actor_on_weapon_zoom_out", actor_on_weapon_zoom_out)
|
||||
RegisterScriptCallback("actor_on_net_destroy", actor_on_weapon_zoom_out) -- Because of hud visor
|
||||
RegisterScriptCallback("actor_on_before_death", actor_on_weapon_zoom_out)
|
||||
|
||||
RegisterScriptCallback("on_option_change", on_option_change)
|
||||
RegisterScriptCallback("on_game_load", cache_mapmarker_info)
|
||||
end
|
||||
|
||||
-- This is triggered whenever our main routine is triggered, thus after every emission
|
||||
function dynzone_on_before_execute()
|
||||
mapspots_blips_show(true)
|
||||
end
|
||||
|
||||
-- We need to clear our data, it's highly probable that it is invalid.
|
||||
function dynzone_changed_block_state(previous, new)
|
||||
clear_eligible_transitions()
|
||||
|
||||
-- If it has unregistered itself
|
||||
RegisterScriptCallback("actor_on_update", actor_on_throttled_update)
|
||||
end
|
||||
|
||||
-- Periodically executes as long as it has determined eligible transitions
|
||||
-- We clear the eligibility table to refresh the list, this way.
|
||||
actor_on_throttled_update = utils.throttle(CONST_DISCOVERY_CHECK_INTERVAL_MS, true, function()
|
||||
if (get_eligible_transitions_on_level()) then return end
|
||||
UnregisterScriptCallback("actor_on_update", actor_on_throttled_update)
|
||||
end)
|
||||
|
||||
-- Toggles state of route discovery if any anomaly was spawned by our script
|
||||
function actor_on_feeling_anomaly(obj, tbl)
|
||||
if (utils.is_table_empty(anomalies_to_check)) then return end
|
||||
|
||||
local obj_id = obj and obj.id and obj:id()
|
||||
local route_id = anomalies_to_check[obj_id]
|
||||
if (not route_id) then return end
|
||||
|
||||
log("[%s] Anomaly belongs to route #%s", obj_id, route_id)
|
||||
toggle_known_route_state(route_id, true)
|
||||
end
|
||||
|
||||
function actor_on_weapon_zoom_in()
|
||||
if (no_eligible_transitions()) then return end
|
||||
RegisterScriptCallback("actor_on_update", check_transition_in_sight)
|
||||
end
|
||||
|
||||
function actor_on_weapon_zoom_out()
|
||||
UnregisterScriptCallback("actor_on_update", check_transition_in_sight)
|
||||
binocular_hud_off()
|
||||
end
|
||||
|
||||
function on_option_change()
|
||||
mapspots_blips_show(opt.get("mapspot_blips_enabled"), true)
|
||||
end
|
||||
|
||||
-- The only purpose of this function is to prevent stuttering due to the usage
|
||||
-- of the utility function map_spot_get_texture_info(..)
|
||||
-- This works on the basis of the assumption that each spot got the same attributes
|
||||
local _cached_mapmarker_info_for
|
||||
function cache_mapmarker_info()
|
||||
local spots = {}
|
||||
|
||||
for route_id, route in route_manager.iterate_routes() do
|
||||
for name, attributes in route_manager.iterate_transitions(route_id) do
|
||||
local _, spot, __ = parent.get_transition_marker_info(name)
|
||||
if (utils.map_spot_get_texture_info(spot)) then
|
||||
_cached_mapmarker_info_for = spot
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
----------------------------
|
||||
-- Monkey patches --
|
||||
----------------------------
|
||||
|
||||
SRTeleportMsgBoxOk = ui_sr_teleport.msg_box_ui.OnMsgOk
|
||||
|
||||
-- We only update the discovery state, if player takes the transition
|
||||
-- Executed through ltx script logic (xr_effects)
|
||||
function ui_sr_teleport.msg_box_ui.OnMsgOk(self)
|
||||
-- section from teleport_ini (should also be in txr_routes.routes table)
|
||||
-- e.g. yan_space_restrictor_to_agroprom_1
|
||||
local route_id = route_manager.get_route_id(self.name)
|
||||
utils.assert(route_id, "SR-Teleport: '%s' got no assigned route", self.name)
|
||||
|
||||
if (not no_eligible_transitions()) then
|
||||
toggle_known_route_state(route_id)
|
||||
end
|
||||
|
||||
return SRTeleportMsgBoxOk(self)
|
||||
end
|
||||
|
||||
------------------------
|
||||
-- Main logic --
|
||||
------------------------
|
||||
|
||||
function eligible_for_discovery(route_id)
|
||||
local route = route_id and route_manager.get_route(route_id)
|
||||
if utils.assert_failed(route) then return end
|
||||
|
||||
return (not route.recently_discovered)
|
||||
end
|
||||
|
||||
function remove_anomaly_check_for(route_id)
|
||||
if not utils.valid_type{ caller = "remove_anomaly_check_for",
|
||||
"int", route_id } then return end
|
||||
|
||||
for k, v in pairs(anomalies_to_check) do
|
||||
if (v == route_id) then
|
||||
anomalies_to_check[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Registers eligible transitions for discovery on the current level
|
||||
-- TODO: Use transition name as key
|
||||
-- Writes data like this: tbl[transition_ID] = route_id
|
||||
-- @param force (forces a refresh)
|
||||
-- @returns true (if eligible transitions exist)
|
||||
function get_eligible_transitions_on_level(force)
|
||||
if not (utils.is_table_empty(discoverable_on_level) or force) then return true end
|
||||
local actor_level = utils.get_mapname()
|
||||
|
||||
if (utils.debug_level_loaded()) then
|
||||
return
|
||||
elseif (utils.is_underground(actor_level)) then
|
||||
log("Underground level loaded, will not check for eligible transitions.")
|
||||
return
|
||||
end
|
||||
|
||||
log("Getting eligible transitions for '%s'", actor_level)
|
||||
|
||||
-- We only do this implicitly as this depends on the eligbility list
|
||||
anomalies_to_check = {}
|
||||
_g.iempty_table(discoverable_on_level)
|
||||
|
||||
for route_id, route in route_manager.iterate_routes(false) do
|
||||
if (route.recently_discovered) then goto next_route end
|
||||
|
||||
for name, attributes in route_manager.iterate_transitions(route_id) do
|
||||
local id = name and get_story_object_id(name)
|
||||
local se_obj = id and alife_object(id)
|
||||
if (not se_obj) then goto continue end
|
||||
|
||||
local transition_level = utils.get_mapname(se_obj)
|
||||
if (actor_level ~= transition_level) then goto continue end
|
||||
|
||||
discoverable_on_level[id] = route_id
|
||||
|
||||
if (route.blocked == route.block_discovered) then break end
|
||||
for _, anomaly_id in pairs(attributes.spawned_anomalies) do
|
||||
anomalies_to_check[anomaly_id] = route_id
|
||||
end
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
|
||||
:: next_route ::
|
||||
end
|
||||
|
||||
if (utils.is_table_empty(discoverable_on_level)) then
|
||||
log("No discoverable transitions on the current level.")
|
||||
DISCOVERABLE_TRANSITIONS_ON_CURRENT_LEVEL = false
|
||||
return
|
||||
end
|
||||
|
||||
log("Discoverable transitions on the current Level:\n%s",
|
||||
utils_data.print_table(discoverable_on_level, false, true))
|
||||
|
||||
DISCOVERABLE_TRANSITIONS_ON_CURRENT_LEVEL = true
|
||||
return true
|
||||
end
|
||||
|
||||
function inform_player(show_msg)
|
||||
if (not sound_objs.discovery:playing()) then
|
||||
sound_objs.discovery:play(player, 0, sound_object.s2d)
|
||||
end
|
||||
|
||||
if (not show_msg) then return end
|
||||
local message = game.translate_string("st_dynzone_discovery_message")
|
||||
news_manager.send_tip(db.actor, message, 0, "recent_surge",
|
||||
opt.get("discovery_msg_showtime"))
|
||||
end
|
||||
|
||||
-------------------------
|
||||
-- Map Markers --
|
||||
-------------------------
|
||||
|
||||
function get_suited_mapspot_hint(id, hint)
|
||||
if not utils.valid_type{ caller = "get_suited_mapspot_hint",
|
||||
"int", id, "str", hint } then return end
|
||||
|
||||
local route = route_manager.get_route_by_transition(id)
|
||||
if (utils.assert_failed(route, "ID #%s not a valid transition", id)) then return end
|
||||
local inactive = route.id and route_manager.route_inactive(route.id)
|
||||
|
||||
local gts = game.translate_string
|
||||
if (not inactive) and (route.blocked or not route.recently_discovered) then
|
||||
new_hint = string.format("%s (%s)", gts(hint),
|
||||
(not route.recently_discovered) and gts("st_dynzone_hint_unknown")
|
||||
or gts("st_dynzone_hint_blocked"))
|
||||
else
|
||||
new_hint = gts(hint)
|
||||
end
|
||||
|
||||
-- Route info in mapspot tooltip (debug)
|
||||
if opt.get("verbose") then
|
||||
local name = get_object_story_id(id)
|
||||
new_hint = name and string.format("%s\\n[%s, #%s]", new_hint, name, route.id)
|
||||
or new_hint
|
||||
end
|
||||
|
||||
return new_hint
|
||||
end
|
||||
|
||||
-- Changes the mapspot marker according to the blocked state of it's transition
|
||||
-- Properly resets to original mapspot texture and color
|
||||
-- @param id game object of the transition
|
||||
-- @param spot default spot texture
|
||||
-- @param blocked (optional)
|
||||
function change_mapspot_marker(id, spot, blocked)
|
||||
if not utils.valid_type{ caller = "mapspot_mark_blocked",
|
||||
"int", id, "str", spot, "bool", blocked } then return end
|
||||
|
||||
if (blocked == nil) then -- get info via route manager
|
||||
local name = get_object_story_id(id)
|
||||
local route = name and route_manager.get_route_by_transition(name)
|
||||
if utils.assert_failed(route) then return end
|
||||
|
||||
blocked = route.block_discovered
|
||||
end
|
||||
|
||||
local texture, color = CONST_MAPSPOT_BLOCKED_TEXTURE
|
||||
if (not blocked) then
|
||||
local get_spot = _cached_mapmarker_info_for or spot
|
||||
local texture_info = utils.map_spot_get_texture_info(get_spot)
|
||||
|
||||
texture = texture_info and texture_info.texture
|
||||
color = texture_info and texture_info.color
|
||||
else
|
||||
color = GetARGB(_g.unpack(CONST_MAPSPOT_BLOCKED_COLOR))
|
||||
end
|
||||
|
||||
if utils.assert_failed(texture, "Unable to determine a texture for %s", spot) then
|
||||
return
|
||||
end
|
||||
|
||||
log("Changing mapspot texture of (%s, %s) to '%s'", id, spot, texture)
|
||||
utils.map_spot_change_texture(id, spot, texture)
|
||||
|
||||
if (color) then
|
||||
log("Changing mapspot color of (%s, %s) to %s (pixelvalue)", id, spot, color)
|
||||
utils.map_spot_change_color(id, spot, color)
|
||||
end
|
||||
end
|
||||
|
||||
-- Toggles the visibility of a mapspot blip. Creates it, if needed.
|
||||
function mapspots_blip_update_for(transition_id, spot)
|
||||
if not utils.valid_type{ caller = "mapspot_blip_update_for",
|
||||
"int", transition_id, "str", spot } then return end
|
||||
|
||||
local name = get_object_story_id(transition_id)
|
||||
local route = name and route_manager.get_route_by_transition(name)
|
||||
if (utils.assert_failed(route, "Transition with ID %s has no defined route!", transition_id)) then return end
|
||||
|
||||
if (route_manager.route_inactive(route.id)) then return end
|
||||
local blip = mapspot_blips[transition_id]
|
||||
if (not blip) then
|
||||
mapspot_blips[transition_id] = init_mapspot_blip(transition_id, spot)
|
||||
blip = mapspot_blips[transition_id]
|
||||
end
|
||||
|
||||
if (utils.assert_failed(blip, "Failed to initialize a blip for transition '%s'", transition_id)) then return end
|
||||
blip.Show(opt.get("mapspot_blips_enabled") and not route.recently_discovered)
|
||||
end
|
||||
|
||||
-- Toggles the display of blips for all mapspots
|
||||
function mapspots_blips_show(state, conserve_route_flag)
|
||||
if not utils.valid_type{ caller = "mapspot_blips_show", "bool", state } then return end
|
||||
|
||||
for route_id, route in route_manager.iterate_routes() do
|
||||
for name, attributes in route_manager.iterate_transitions(route_id) do
|
||||
local id, spot, _ = parent.get_transition_marker_info(name)
|
||||
if (utils.assert_failed(id and spot)) then return end
|
||||
|
||||
if (not conserve_route_flag) then
|
||||
route.recently_discovered = (not state)
|
||||
end
|
||||
|
||||
mapspots_blip_update_for(id, spot)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Removed all blips and their references, also resets the route flag
|
||||
-- @param conserve_route_flag (don't alter route flag)
|
||||
function mapspots_blips_remove(conserve_route_flag)
|
||||
for id, blip in pairs(mapspot_blips) do
|
||||
local name = get_object_story_id(id)
|
||||
local route = name and route_manager.get_route_by_transition(name)
|
||||
if not (conserve_route_flag or utils.assert_failed(route)) then
|
||||
route.recently_discovered = false
|
||||
end
|
||||
|
||||
blip.Show(false)
|
||||
mapspot_blips[id] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Courtesy of Catspaw (Addon: Personal Adjustable Waypoints)
|
||||
-- Init's a mapspot blip fir a given mapspot (id, spot)
|
||||
-- @returns table
|
||||
function init_mapspot_blip(id, spot, anchor)
|
||||
if not utils.valid_type{ caller = "init_mapspot_blip",
|
||||
"int", id, "str", spot } then return end
|
||||
|
||||
local anchor = anchor or level.map_get_object_spot_static(id, spot)
|
||||
local xml = CScriptXmlInit()
|
||||
local xmlroot = "dzt_ui_modifiers"
|
||||
xml:ParseFile("dzt_ui_elements.xml")
|
||||
|
||||
local blip = {}
|
||||
blip.id = id
|
||||
|
||||
blip.box = xml:InitStatic(xmlroot, anchor) -- CUIScriptWnd
|
||||
blip.box:SetWndPos(vector2():set(CONST_MAPSPOT_BLIP_OFFSET_PIXELS,
|
||||
CONST_MAPSPOT_BLIP_OFFSET_PIXELS +3)) -- sets (x,y) but root is top left
|
||||
|
||||
blip.blip = xml:InitStatic(xmlroot .. ":blip_unknown", blip.box)
|
||||
blip.blip:SetWndSize(vector2():set(_g.round(19 *0.8), _g.round(21 *0.8)))
|
||||
|
||||
blip.Show = function(tf)
|
||||
blip.box:Show(tf)
|
||||
end
|
||||
|
||||
return blip
|
||||
end
|
||||
|
||||
-- Removes mapspots added by the old method prior to version 20241119
|
||||
function mapspots_remove_old()
|
||||
mapspots_blips_remove(true)
|
||||
|
||||
for route_id, route in route_manager.iterate_routes() do
|
||||
for transition_name, attributes in route_manager.iterate_transitions(route_id) do
|
||||
local id, spot, hint = parent.get_transition_marker_info(transition_name)
|
||||
assert(id and spot and hint)
|
||||
|
||||
level.map_remove_all_object_spots(id)
|
||||
if (not utils.map_spot_exists(id, spot)) then -- sanity check
|
||||
utils.map_spot_add(id, spot, hint)
|
||||
end
|
||||
|
||||
update_transition_mapspot(transition_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---------------------------------
|
||||
-- Binocular Discovery --
|
||||
---------------------------------
|
||||
|
||||
-- TODO: use anomalies.get_spawn_parameters(transition_name).rootpos for distance check
|
||||
check_transition_in_sight = utils.throttle(CONST_BINOCULAR_CHECK_IN_SIGHT_INTERVAL_MS, false, function()
|
||||
if (BINOCULAR_DISCOVERY_IN_PROGRESS or no_eligible_transitions()) then return end
|
||||
|
||||
local wpn = db.actor:active_item() -- wait, until this is updated
|
||||
if not (wpn and wpn:section() == "wpn_binoc_inv") then return end
|
||||
|
||||
-- TODO: Use a TimedEvent with an ID to delay HUD display whilst being able to cancel prematurely
|
||||
binocular_hud_on()
|
||||
|
||||
local look_pos = utils.get_target_pos()
|
||||
for transition_id, route_id in pairs(discoverable_on_level) do
|
||||
if (utils.get_proximity_by_id(transition_id) > opt.get("binoc_discovery_max_distance")) then
|
||||
goto next_transition
|
||||
elseif (utils.get_point_proximity_by_id(transition_id, look_pos) > opt.get("binoc_discovery_dist")) then
|
||||
goto next_transition
|
||||
end
|
||||
utils.timed_call(CONST_BINOCULAR_REFRESHRATE_MS, check_binocular_state,
|
||||
{ id = transition_id, tg = time_global(), route_id = route_id })
|
||||
|
||||
log("Forked timed binocular check for transition '%s' (route #%s)",
|
||||
get_object_story_id(transition_id), route_id)
|
||||
|
||||
BINOCULAR_DISCOVERY_IN_PROGRESS = true
|
||||
play_binocular_sound_cue()
|
||||
do break end -- Don't check other transitions
|
||||
|
||||
:: next_transition ::
|
||||
end
|
||||
end)
|
||||
|
||||
-- Periodically checks for discovery conditions and aborts accordingly.
|
||||
function check_binocular_state(data)
|
||||
local wpn = db.actor:active_item()
|
||||
local look_pos = utils.get_target_pos()
|
||||
local hold_time = time_global() - data.tg
|
||||
local proximity = utils.get_point_proximity_by_id(data.id, look_pos)
|
||||
|
||||
-- no need to repeat this over and over.. avoiding goto's aswell
|
||||
local function abort_discovery()
|
||||
BINOCULAR_DISCOVERY_IN_PROGRESS = false
|
||||
play_binocular_sound_cue()
|
||||
return true
|
||||
end
|
||||
|
||||
if (no_eligible_transitions()) then
|
||||
log("No eligible transitions left to check")
|
||||
return abort_discovery()
|
||||
elseif not (wpn and wpn:section() == "wpn_binoc_inv") then
|
||||
log("Not holding binoculars. Aborting.")
|
||||
return abort_discovery()
|
||||
elseif (not axr_main.weapon_is_zoomed) then
|
||||
log("Binoculars are not zoomed in")
|
||||
return abort_discovery()
|
||||
elseif (proximity > opt.get("binoc_discovery_dist")) then
|
||||
log("Point proximity is now too large. Aborting.")
|
||||
return abort_discovery()
|
||||
elseif (hold_time < opt.get("binoc_discovery_holdtime")) then
|
||||
log("Target has not been focused long enough (%sms)", hold_time)
|
||||
return
|
||||
end
|
||||
|
||||
toggle_known_route_state(data.route_id, true)
|
||||
BINOCULAR_DISCOVERY_IN_PROGRESS = false
|
||||
return true
|
||||
end
|
||||
|
||||
function play_binocular_sound_cue()
|
||||
if (BINOCULAR_DISCOVERY_IN_PROGRESS) then
|
||||
sound_objs.binoc_in_progress:play(player, 0, sound_object.s2d)
|
||||
return
|
||||
elseif (sound_objs.binoc_in_progress:playing()) then
|
||||
sound_objs.binoc_in_progress:stop()
|
||||
end
|
||||
|
||||
sound_objs.binoc_aborted:play(player, 0, sound_object.s2d)
|
||||
end
|
||||
|
||||
------------------------
|
||||
-- Visor HUD --
|
||||
-- Courtesy of xcvb --
|
||||
------------------------
|
||||
|
||||
function binocular_hud_on()
|
||||
if BINOCULAR_HUD or (not opt.get("binoc_discovery_hud_enabled")) then return end
|
||||
BINOCULAR_HUD = binocular_hud()
|
||||
get_hud():AddDialogToRender(BINOCULAR_HUD)
|
||||
log("Enabled Binocular-HUD")
|
||||
end
|
||||
|
||||
function binocular_hud_off()
|
||||
if (not BINOCULAR_HUD) then return end
|
||||
get_hud():RemoveDialogToRender(BINOCULAR_HUD)
|
||||
BINOCULAR_HUD = nil
|
||||
log("Disabled Binocular-HUD")
|
||||
end
|
||||
|
||||
class "binocular_hud" (CUIScriptWnd)
|
||||
function binocular_hud:__init() super()
|
||||
self:InitControls()
|
||||
end
|
||||
|
||||
function binocular_hud:__finalize() end
|
||||
|
||||
function binocular_hud:InitControls()
|
||||
self:SetWndRect(Frect():set(0,0,1024,768))
|
||||
self:SetAutoDelete(true)
|
||||
|
||||
self.xml = CScriptXmlInit()
|
||||
local xml = self.xml
|
||||
xml:ParseFile("actor_menu.xml")
|
||||
|
||||
self.hud_update_timer = 0
|
||||
|
||||
self.transitions = {}
|
||||
self.elements = {}
|
||||
self.texture = CONST_BINOCULAR_HUD_ICON_TEXTURE
|
||||
|
||||
local size = CONST_BINOCULAR_HUD_ICON_TEXTURE_SIZE
|
||||
for i = 1, CONST_BINOCULAR_HUD_MAX_VISIBLE_TRANSITIONS do
|
||||
self.elements[i] = xml:InitStatic("helmet_over", self)
|
||||
self.elements[i]:InitTexture("ui_mmap_stask_last_02")
|
||||
self.elements[i]:SetWndSize(vector2():set(size[1], size[2]))
|
||||
self.elements[i]:Show(false)
|
||||
end
|
||||
end
|
||||
|
||||
function binocular_hud:Update()
|
||||
CUIScriptWnd.Update(self)
|
||||
self:GatherTransitions()
|
||||
|
||||
-- display eligible transitions in close proximity
|
||||
for i = 1, CONST_BINOCULAR_HUD_MAX_VISIBLE_TRANSITIONS do
|
||||
self.elements[i]:Show(false)
|
||||
|
||||
local transition = self.transitions[i]
|
||||
if (not transition) then goto continue end
|
||||
|
||||
local id = transition.id
|
||||
|
||||
-- To make sure that it is updated immediately after discovery
|
||||
if not (discoverable_on_level[id]) then goto continue end
|
||||
|
||||
local obj = level.object_by_id(id)
|
||||
local name = obj and obj:name()
|
||||
|
||||
local pos = name and get_anomaly_rootpos(name)
|
||||
if (not pos) then
|
||||
local obj_pos = obj:position()
|
||||
pos = utils.get_closest_vertex_pos(obj_pos)
|
||||
or vector():set(obj_pos.x, obj_pos.y, obj_pos.z)
|
||||
end
|
||||
|
||||
-- Note: The position variable should be temporary
|
||||
pos.y = pos.y + CONST_BINOCULAR_HUD_ICON_HOVER_METRES
|
||||
|
||||
local wui_pos = pos and vector2():set(game.world2ui(pos))
|
||||
if wui_pos then
|
||||
self.elements[i]:InitTexture(self.texture)
|
||||
self.elements[i]:SetWndPos(vector2():set(wui_pos.x, wui_pos.y))
|
||||
self.elements[i]:Show(true)
|
||||
end
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
end
|
||||
|
||||
function binocular_hud:GatherTransitions()
|
||||
local tg = time_global()
|
||||
if (self.hud_update_timer > tg) then return end
|
||||
self.hud_update_timer = tg + CONST_DISCOVERY_CHECK_INTERVAL_MS
|
||||
|
||||
iempty_table(self.transitions)
|
||||
if (no_eligible_transitions()) then return end
|
||||
|
||||
local max_distance = opt.get("binoc_discovery_max_distance")
|
||||
for transition_id, route_id in pairs(discoverable_on_level) do
|
||||
local distance = utils.get_proximity_by_id(transition_id)
|
||||
if (distance > max_distance) then goto continue end
|
||||
|
||||
self.transitions[#self.transitions +1] = { id = transition_id, }
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,206 @@
|
||||
--[[
|
||||
DYNAMIC ZONE - MCM Options
|
||||
|
||||
Original Author(s)
|
||||
VodoXleb <vodoxlebushek>
|
||||
Singustromo <singustromo at disroot.org>
|
||||
|
||||
Edited by
|
||||
|
||||
License
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
|
||||
(https://creativecommons.org/licenses/by-nc-sa/4.0)
|
||||
--]]
|
||||
|
||||
CONST_ACRONYM = "dynamic_zone"
|
||||
CONST_DEFAULT_CATEGORY = "general"
|
||||
|
||||
options = {} -- This contains cached option values indexed by option name
|
||||
|
||||
colors = { -- ARGB
|
||||
title = {255, 255, 255, 0},
|
||||
desc = {255, 109, 187, 164},
|
||||
notice = {255, 232, 61, 102},
|
||||
}
|
||||
|
||||
defaults = {
|
||||
["general"] = {
|
||||
["chance_path_closed"] = 30, -- for any given path between two maps
|
||||
["chance_dz_trigger"] = 75, -- for the zone to change during an emission
|
||||
["minimum_map_connections"] = 1,
|
||||
["trigger_on_newgame"] = true, -- If some routes should already be disabled at the beginning
|
||||
["newgame_grace_period"] = 12, -- For the delay after New game before first blockage (Grace Period)
|
||||
["emission_trigger"] = "2ndwave", -- in which phase we trigger
|
||||
["addon_removal"] = false,
|
||||
},
|
||||
["news"] = {
|
||||
["percent_path_reveal_unblock"] = 60,
|
||||
["percent_path_reveal_block"] = 50,
|
||||
["chance_special_character"] = 25,
|
||||
["news_play_discovery_sound"] = true,
|
||||
},
|
||||
["discovery"] = {
|
||||
["discovery_msg_showtime"] = 4000,
|
||||
["mapspot_blips_enabled"] = true,
|
||||
["binoc_discovery_hud_enabled"] = true,
|
||||
["binoc_discovery_holdtime"] = 3500,
|
||||
["binoc_discovery_dist"] = 40,
|
||||
["binoc_discovery_max_distance"] = 350,
|
||||
},
|
||||
["debug"] = {
|
||||
["emission_check_interval"] = 10000, -- in milliseconds
|
||||
["validate_parameter_types"] = false,
|
||||
["verbose"] = false,
|
||||
},
|
||||
}
|
||||
|
||||
-- Getter for other scripts, using cached values
|
||||
-- Simplifies getting the values through a unique id
|
||||
function get(key)
|
||||
return options[key]
|
||||
end
|
||||
|
||||
function on_game_start()
|
||||
RegisterScriptCallback("on_option_change", apply_settings)
|
||||
apply_settings()
|
||||
end
|
||||
|
||||
function apply_settings()
|
||||
for category, settings in pairs(defaults) do
|
||||
for option,_ in pairs(settings) do
|
||||
options[option] = get_config(category, option)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function on_mcm_load()
|
||||
local op = {
|
||||
id = CONST_ACRONYM,
|
||||
text = "ui_mcm_"..CONST_ACRONYM,
|
||||
gr = {}
|
||||
}
|
||||
|
||||
local general = {
|
||||
id = "general",
|
||||
sh = true,
|
||||
text = "ui_mcm_"..CONST_ACRONYM.."_general",
|
||||
gr = {
|
||||
{ id = "title", type ="slide", link = "ui_options_slider_sound_environment",
|
||||
text = "ui_mcm_"..CONST_ACRONYM.."_main_title", size = {512,50}, spacing = 20 },
|
||||
|
||||
{ id = "trigger_on_newgame", type = "check", val = 1,
|
||||
def = defaults["general"]["trigger_on_newgame"] },
|
||||
{ id = "newgame_grace_period", type = "track", val = 2, min = 0, max = 96, step = 12,
|
||||
def = defaults["general"]["newgame_grace_period"] },
|
||||
{ id = "chance_dz_trigger", type = "track", val = 2, min = 0, max = 100, step = 5,
|
||||
def = defaults["general"]["chance_dz_trigger"] },
|
||||
{ id = "emission_trigger", type = "list", val = 0, def = defaults["general"]["emission_trigger"],
|
||||
content = {
|
||||
{ "impact", CONST_ACRONYM.. "_emission_trigger_impact" },
|
||||
{ "1stwave", CONST_ACRONYM.. "_emission_trigger_1stwave" },
|
||||
{ "2ndwave", CONST_ACRONYM.. "_emission_trigger_2ndwave" },
|
||||
{ "rumble", CONST_ACRONYM.. "_emission_trigger_rumble" },
|
||||
},
|
||||
},
|
||||
{ id = "chance_path_closed", type = "track", val = 2, min = 0, max = 100, step = 5,
|
||||
def = defaults["general"]["chance_path_closed"] },
|
||||
{ id = "minimum_map_connections", type = "track", val = 2, min = 0, max = 4, step = 1,
|
||||
def = defaults["general"]["minimum_map_connections"] },
|
||||
|
||||
{ id = "divider", type = "line" },
|
||||
{ id = "addon_removal", type = "check", val = 1, def = defaults["addon_removal"] },
|
||||
}
|
||||
}
|
||||
|
||||
local news = {
|
||||
id = "news",
|
||||
sh = true,
|
||||
text = "ui_mcm_"..CONST_ACRONYM.."_news",
|
||||
gr = {
|
||||
{ id = "title", type ="slide", link = "ui_options_slider_news",
|
||||
text = "ui_mcm_"..CONST_ACRONYM.."_news_title", size = {512,50}, spacing = 20 },
|
||||
|
||||
{ id = "header", type = "desc", text = "ui_mcm_"..CONST_ACRONYM.."_news_desc", clr = colors.desc},
|
||||
|
||||
{ id = "news_play_discovery_sound", type = "check", val = 1,
|
||||
def = defaults["news"]["news_play_discovery_sound"] },
|
||||
{ id = "percent_path_reveal_unblock", type = "track", val = 2, min = 0, max = 100, step = 5,
|
||||
def = defaults["news"]["percent_path_reveal_unblock"] },
|
||||
{ id = "percent_path_reveal_block", type = "track", val = 2, min = 0, max = 100, step = 5,
|
||||
def = defaults["news"]["percent_path_reveal_block"] },
|
||||
{ id = "chance_special_character", type = "track", val = 2, min = 0, max = 100, step = 5,
|
||||
def = defaults["news"]["chance_special_character"] },
|
||||
}
|
||||
}
|
||||
|
||||
local discovery = {
|
||||
id = "discovery",
|
||||
sh = true,
|
||||
text = "ui_mcm_"..CONST_ACRONYM.."_discovery",
|
||||
gr = {
|
||||
{ id = "title", type ="slide", link = "ui_options_slider_emission",
|
||||
text = "ui_mcm_"..CONST_ACRONYM.."_discovery_title", size = {512,50}, spacing = 20 },
|
||||
|
||||
{ id = "discovery_msg_showtime", type = "track", val = 2, min = 1000, max = 10000, step = 500,
|
||||
def = defaults["discovery"]["discovery_msg_showtime"] },
|
||||
{ id = "mapspot_blips_enabled", type = "check", val = 1,
|
||||
def = defaults["discovery"]["mapspot_blips_enabled"] },
|
||||
|
||||
{ id = "divider", type = "line" },
|
||||
|
||||
{ id = "binoc_discovery_hud_enabled", type = "check", val = 1,
|
||||
def = defaults["discovery"]["binoc_discovery_hud_enabled"] },
|
||||
{ id = "binoc_discovery_holdtime", type = "track", val = 2, min = 1000, max = 10000, step = 500,
|
||||
def = defaults["discovery"]["binoc_discovery_holdtime"] },
|
||||
{ id = "binoc_discovery_dist", type = "track", val = 2, min = 20, max = 80, step = 5,
|
||||
def = defaults["discovery"]["binoc_discovery_dist"] },
|
||||
{ id = "binoc_discovery_max_distance", type = "track", val = 2, min = 150, max = 500, step = 25,
|
||||
def = defaults["discovery"]["binoc_discovery_max_distance"] },
|
||||
}
|
||||
}
|
||||
|
||||
local debug = {
|
||||
id = "debug",
|
||||
sh = true,
|
||||
text = "ui_mcm_"..CONST_ACRONYM.."_debug",
|
||||
gr = {
|
||||
{ id = "title", type ="slide", link = "ui_options_slider_sound_environment",
|
||||
text = "ui_mcm_"..CONST_ACRONYM.."_debug_title", size = {512,50}, spacing = 20 },
|
||||
|
||||
{ id = "header", type = "desc", text = "ui_mcm_"..CONST_ACRONYM.."_debug_desc", clr = colors.desc},
|
||||
|
||||
{ id = "verbose", type = "check", val = 1,
|
||||
def = defaults["debug"]["verbose"] },
|
||||
{ id = "validate_parameter_types", type = "check", val = 1,
|
||||
def = defaults["debug"]["validate_parameter_types"] },
|
||||
{ id = "emission_check_interval", type = "track", val = 2, min = 2000, max = 20000, step = 500,
|
||||
def = defaults["debug"]["emission_check_interval"] },
|
||||
}
|
||||
}
|
||||
|
||||
table.insert(op.gr, general)
|
||||
table.insert(op.gr, news)
|
||||
table.insert(op.gr, discovery)
|
||||
table.insert(op.gr, debug)
|
||||
|
||||
return op
|
||||
end
|
||||
|
||||
function get_config(id, key)
|
||||
if not (key and type(key) == 'string') then return end
|
||||
id = (id and type(id) == 'string') and id or CONST_DEFAULT_CATEGORY
|
||||
|
||||
if ui_mcm and type(ui_mcm.get) == 'function' then
|
||||
return ui_mcm.get(CONST_ACRONYM .. "/".. id .. "/" .. key)
|
||||
end
|
||||
|
||||
return defaults[id][key]
|
||||
end
|
||||
|
||||
function set_config(id, key, value)
|
||||
if not (key and type(key) == 'string') then return end
|
||||
id = (id and type(id) == 'string') and id or CONST_DEFAULT_CATEGORY
|
||||
|
||||
if (not ui_mcm and type(ui_mcm.set) == 'function') then return end
|
||||
ui_mcm.set(CONST_ACRONYM .. "/".. id .. "/" .. key, value)
|
||||
end
|
||||
@@ -0,0 +1,339 @@
|
||||
--[[
|
||||
DYNAMIC ZONE - PDA News
|
||||
|
||||
Original Author(s)
|
||||
Singustromo <singustromo at disroot.org>
|
||||
VodoXleb <vodoxlebushek>
|
||||
|
||||
Edited by
|
||||
|
||||
License
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
|
||||
(https://creativecommons.org/licenses/by-nc-sa/4.0)
|
||||
|
||||
--]]
|
||||
|
||||
parent = _G["dynamic_zone"]
|
||||
if not (parent and parent.VERSION and parent.VERSION >= 20241224) then return end
|
||||
|
||||
--------------------------
|
||||
-- Dependencies --
|
||||
--------------------------
|
||||
|
||||
local opt = dynamic_zone_mcm
|
||||
local utils = dynamic_zone_utils
|
||||
local debug = dynamic_zone_debug
|
||||
local route_manager = dynamic_zone_routes
|
||||
local route_discovery = dynamic_zone_discovery
|
||||
|
||||
---------------------------------
|
||||
-- Globals & Constants --
|
||||
---------------------------------
|
||||
|
||||
CONST_LOGGING_PREFIX = "Dynamic News"
|
||||
local log = debug.log_register("info", CONST_LOGGING_PREFIX)
|
||||
local log_warn = debug.log_register("warning", CONST_LOGGING_PREFIX)
|
||||
local log_err = debug.log_register("error", CONST_LOGGING_PREFIX)
|
||||
|
||||
CONST_RANDOMIZER_BEZIER_PARAMETERS = { 0, .0, .2, .9 } -- 41.9%
|
||||
CONST_PROPAGATION_INTERVAL_MS = 20000
|
||||
CONST_FALLBACK_DELAY_MIN_MS = 500
|
||||
CONST_FALLBACK_DELAY_MAX_MS = CONST_PROPAGATION_INTERVAL_MS * 0.5
|
||||
|
||||
CONST_MESSAGE_MAX_STRINGS_TO_ITERATE = 20
|
||||
CONST_STRING_ID_PREFIX = "st_dynzone_news"
|
||||
CONST_STRING_ID_FALLBACK = CONST_STRING_ID_PREFIX .. "_fallback"
|
||||
CONST_STRING_ID_SUFFIX_BLOCKED = "blocked"
|
||||
CONST_STRING_ID_SUFFIX_OPENED = "opened"
|
||||
|
||||
CONST_NEWS_FALLBACK_ICON = "ui_iconsTotal_grouping"
|
||||
CONST_MESSAGE_FALLBACK_NAME = "Bratan"
|
||||
|
||||
routes_to_reveal = {} -- routes whose changes will be propagated by other stalkers.
|
||||
|
||||
function clear()
|
||||
log("Cleared route propagation queue")
|
||||
routes_to_reveal = {}
|
||||
end
|
||||
|
||||
special_characters = {
|
||||
-- [story_id] = identifier (for strings)
|
||||
["esc_m_trader"] = "sidorovich", -- Loner
|
||||
["bar_dolg_general_petrenko_stalker"] = "petrenko", -- Duty
|
||||
["mil_smart_terrain_7_7_freedom_leader_stalker"] = "lukash", -- Freedom
|
||||
-- ["mar_smart_terrain_base_stalker_leader_marsh"] = "cold", -- Clear Sky
|
||||
["yan_stalker_sakharov"] = "sakharov", -- Ecologists
|
||||
["cit_killers_merc_trader_stalker"] = "dushman", -- Mercenaries
|
||||
["agr_smart_terrain_1_6_near_2_military_colonel_kovalski"] = "kovalski", -- Military
|
||||
["zat_b7_bandit_boss_sultan"] = "sultan", -- Bandits
|
||||
["pri_monolith_monolith_trader_stalker"] = "krolik", -- Monolith
|
||||
}
|
||||
|
||||
------------------------------
|
||||
-- Just Shortcuts --
|
||||
------------------------------
|
||||
|
||||
local ctime_to_tbl = utils_data.CTime_to_table
|
||||
local tbl_to_ctime = utils_data.CTime_from_table
|
||||
|
||||
-- Using bezier curves for non-linear probability and to clamp result
|
||||
-- Used so mean will be set percentage over time (of the time frame)
|
||||
function get_random_time(lower, upper)
|
||||
local randomizer = libmath_bezier and libmath_bezier.get_random_value
|
||||
|
||||
if utils.assert_failed(randomizer, "Bézier script not found. Using math.random()") then
|
||||
return math.random(lower, upper)
|
||||
end
|
||||
|
||||
return randomizer(lower, upper, CONST_RANDOMIZER_BEZIER_PARAMETERS)
|
||||
end
|
||||
|
||||
-------------------------
|
||||
-- Callbacks --
|
||||
-------------------------
|
||||
|
||||
function on_game_start()
|
||||
RegisterScriptCallback("dynzone_changed_block_state",
|
||||
discover_changed_routes_via_news)
|
||||
|
||||
RegisterScriptCallback("actor_on_update", check_timed_unlocks)
|
||||
end
|
||||
|
||||
function discover_changed_routes_via_news(previously_blocked_routes, new_blocked_routes)
|
||||
clear()
|
||||
queue_routes_for_propagation(previously_blocked_routes,
|
||||
opt.get("percent_path_reveal_unblock"), CONST_STRING_ID_SUFFIX_OPENED)
|
||||
queue_routes_for_propagation(new_blocked_routes,
|
||||
opt.get("percent_path_reveal_block"), CONST_STRING_ID_SUFFIX_BLOCKED)
|
||||
|
||||
RegisterScriptCallback("actor_on_update", check_timed_unlocks)
|
||||
end
|
||||
|
||||
-- Iterates through the route discovery queue and
|
||||
-- checks if any of them are due to being propagated
|
||||
check_timed_unlocks = utils.throttle(CONST_PROPAGATION_INTERVAL_MS, true, function()
|
||||
if (utils.is_table_empty(routes_to_reveal)) then
|
||||
log("No routes in the news queue. Unregistered Callback.")
|
||||
UnregisterScriptCallback("actor_on_update", check_timed_unlocks)
|
||||
return
|
||||
end
|
||||
|
||||
local current_ctime = game.get_game_time()
|
||||
log("Current time: %s", string.format("%d/%.2d/%.2d %.2d:%.2d:%.2d",
|
||||
current_ctime:get(Y,M,D,h,m,s,ms)))
|
||||
|
||||
for route_id, data in pairs(routes_to_reveal) do
|
||||
utils.assert(data, "Undefined Route data")
|
||||
utils.assert(data.reveal_time, "Undefined Route reveal time")
|
||||
|
||||
local reveal_time = tbl_to_ctime(data.reveal_time)
|
||||
local remaining_time_ms = (reveal_time:diffSec(current_ctime) / level.get_time_factor()) * 1000
|
||||
reveal_time:sub(current_ctime)
|
||||
|
||||
if (remaining_time_ms >= CONST_PROPAGATION_INTERVAL_MS) then
|
||||
local _, __, d, h, m, s, ___ = reveal_time:get(Y,M,D,h,m,s,ms)
|
||||
log("Route #%s will be propagated in %sd %sh %sm %ss",
|
||||
route_id, (d -1), h, m, s)
|
||||
|
||||
goto continue
|
||||
end
|
||||
|
||||
remaining_time_ms = (remaining_time_ms < 0)
|
||||
and math.random(CONST_FALLBACK_DELAY_MIN_MS, CONST_FALLBACK_DELAY_MAX_MS)
|
||||
or remaining_time_ms
|
||||
|
||||
log("Route #%s will be propagated in %ss (realtime)", route_id,
|
||||
string.format("%.1f", remaining_time_ms / 1000))
|
||||
|
||||
utils.assert(data.reveal_type, "Undefined route reveal type!")
|
||||
utils.timed_call(remaining_time_ms, function()
|
||||
propagate_route_state_change(route_id, data.reveal_type)
|
||||
return true
|
||||
end)
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
end)
|
||||
|
||||
--------------------------
|
||||
-- Main logic --
|
||||
--------------------------
|
||||
|
||||
function propagate_route_state_change(route_id, reveal_type)
|
||||
if not utils.valid_type{ caller = "propagate_route_state_change",
|
||||
"int", route_id } then return end
|
||||
|
||||
local route = route_manager.get_route(route_id)
|
||||
local levels, sender = route and route.connects
|
||||
|
||||
if math.random() > (opt.get("chance_special_character") / 100) then
|
||||
sender = find_random_stalker_on_level_pair(levels)
|
||||
else
|
||||
sender = find_special_character_on_level_pair(levels)
|
||||
end
|
||||
|
||||
local route_state_differs = route_manager.route_known_state_differs(route_id)
|
||||
local play_discovery_sound = opt.get("news_play_discovery_sound") and route_state_differs
|
||||
|
||||
if not utils.assert_failed(sender, "Sender for route #%s is nil!", route_id) then
|
||||
local anomaly_theme = route_manager.get_route_property(route_id, "anomaly_theme")
|
||||
send_route_news_tip(reveal_type, 0, sender, 10, levels, anomaly_theme, play_discovery_sound)
|
||||
end
|
||||
|
||||
-- TODO: Check, if still eligible to prevent log spamming at this point
|
||||
-- We still want to propagate the information, nonetheless.
|
||||
log("Revealing %s state of route #%s via news.", reveal_type, route_id)
|
||||
route_discovery.toggle_known_route_state(route_id, play_discovery_sound)
|
||||
|
||||
routes_to_reveal[route_id] = nil
|
||||
end
|
||||
|
||||
function queue_routes_for_propagation(selected_routes, percent_to_reveal, reveal_type)
|
||||
local indexed_routes = {}
|
||||
utils.index_keys(selected_routes, indexed_routes)
|
||||
|
||||
-- time before next emission when all reveal messages should be posted
|
||||
local next_emission_in_hours = surge_manager and surge_manager.get_surge_manager
|
||||
and (surge_manager.get_surge_manager()._delta / (60^2)) -- game time
|
||||
|
||||
if (not next_emission_in_hours) then
|
||||
next_emission_in_hours = ui_options.get("alife/event/emission_frequency")
|
||||
log_warn("Surge manager not available, using frequency set in options.")
|
||||
end
|
||||
|
||||
local revealed = 1
|
||||
local amount_to_reveal = math.floor(#indexed_routes * (percent_to_reveal/100))
|
||||
for id in utils.random_numbered_sequence(1, #indexed_routes) do
|
||||
if (revealed > amount_to_reveal) then goto continue end
|
||||
revealed = revealed +1
|
||||
|
||||
local minutes = 6 * get_random_time(3, next_emission_in_hours * 10)
|
||||
local hours = math.floor(minutes / 60)
|
||||
minutes = minutes % 60
|
||||
local days = math.floor(hours / 24)
|
||||
hours = hours % 24
|
||||
|
||||
local add_time = game.CTime()
|
||||
|
||||
-- days +1 so we get proper result (engine quirk)
|
||||
add_time:set(1, 1, (days +1), hours, minutes, math.random(0, 59), 0)
|
||||
|
||||
local selected_time = game.get_game_time()
|
||||
selected_time:add(add_time)
|
||||
|
||||
log("Queued route #%s for propagation (%s)", id, reveal_type)
|
||||
routes_to_reveal[indexed_routes[id]] = {
|
||||
route = selected_routes[indexed_routes[id]],
|
||||
reveal_time = ctime_to_tbl(selected_time), -- don't save userdata
|
||||
reveal_type = reveal_type,
|
||||
}
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
end
|
||||
|
||||
function find_random_stalker_on_level_pair(levels)
|
||||
if not utils.valid_type{ caller = "find_random_stalker_on_level_pair",
|
||||
"tbl", levels } then return end
|
||||
|
||||
for id in utils.random_numbered_sequence(1, 2^16 -2) do
|
||||
local se_obj = alife():object(id)
|
||||
if not (se_obj) then goto continue end
|
||||
|
||||
if IsStalker(nil, se_obj:clsid())
|
||||
and se_obj:alive()
|
||||
and se_obj:community() ~= "zombied"
|
||||
and se_obj:community() ~= "trader"
|
||||
and se_obj:community() ~= "greh"
|
||||
and se_obj:community() ~= "isg"
|
||||
and se_obj:community() ~= "renegade"
|
||||
and se_obj.group_id ~= 65535
|
||||
and (get_object_story_id(se_obj.group_id) == nil)
|
||||
and string.find(se_obj:name(),"sim_default_")
|
||||
and utils.table_has(levels, utils.get_mapname(se_obj)) then
|
||||
return id
|
||||
end
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
end
|
||||
|
||||
function find_special_character_on_level_pair(levels)
|
||||
if not utils.valid_type{ caller = "find_special_character_on_level_pair",
|
||||
"tbl", levels } then return end
|
||||
|
||||
local found_stalkers = {}
|
||||
for k, identifier in pairs(special_characters) do
|
||||
local id = get_story_object_id(k)
|
||||
local se_obj = id and alife_object(id)
|
||||
if not (se_obj) then goto continue end
|
||||
|
||||
local character_map = utils.get_mapname(se_obj)
|
||||
local connected_maps = character_map and utils.get_directly_connected_maps(character_map)
|
||||
|
||||
-- Route should at least be indirectly connected to the senders map
|
||||
if utils.table_contains(connected_maps, levels) then
|
||||
found_stalkers[#found_stalkers + 1] = se_obj.id
|
||||
end
|
||||
|
||||
:: continue ::
|
||||
end
|
||||
|
||||
return found_stalkers[math.random(1, #found_stalkers)]
|
||||
or find_random_stalker_on_level_pair(levels)
|
||||
end
|
||||
|
||||
function send_route_news_tip(news_reveal_type, timeout, sender_id, showtime, news_levels, news_anom_theme, suppress_pda_sound)
|
||||
timeout = timeout or 0
|
||||
showtime = showtime or 5
|
||||
|
||||
local npc = alife():object(sender_id)
|
||||
if (not npc) then return end
|
||||
|
||||
local actor = db.actor
|
||||
local texture = (npc.character_icon) and npc:character_icon() or CONST_NEWS_FALLBACK_ICON
|
||||
local special_character_nickname = special_characters[get_object_story_id(npc.id)]
|
||||
local npc_story_id = special_character_nickname or npc:community()
|
||||
local msg_to_translate = string.format("%s_%s_%s_", CONST_STRING_ID_PREFIX, npc_story_id, news_reveal_type)
|
||||
|
||||
local phrases = {}
|
||||
for i = 1, CONST_MESSAGE_MAX_STRINGS_TO_ITERATE do
|
||||
if (utils.has_translation(msg_to_translate .. i)) then
|
||||
phrases[#phrases +1] = i
|
||||
end
|
||||
end
|
||||
|
||||
if (utils.is_table_empty(phrases)) then
|
||||
log_warn("Undefined string group: '%s'. Using fallback.", msg_to_translate:sub(1, -2))
|
||||
msg_to_translate = CONST_STRING_ID_FALLBACK .. "_" .. news_reveal_type
|
||||
else
|
||||
msg_to_translate = msg_to_translate .. phrases[math.random(1, #phrases)]
|
||||
end
|
||||
|
||||
-- Make sure $FROM is the senders level, when route connects sender map
|
||||
local sender_level = utils.get_mapname(npc)
|
||||
local index = utils.index_of(news_levels, sender_level) or math.random(1, #news_levels)
|
||||
local level_from, level_to = news_levels[index], news_levels[#news_levels -index +1]
|
||||
utils.assert(level_from and level_to)
|
||||
|
||||
local character_name = (npc.character_name) and npc:character_name()
|
||||
local news_caption = character_name or game.translate_string("st_tip")
|
||||
|
||||
-- TODO: Maybe need a debug mode flag for cases such as this
|
||||
news_caption = (opt.get("verbose"))
|
||||
and string.format("%s (%s)", news_caption, utils.get_mapname(npc))
|
||||
or news_caption
|
||||
|
||||
local anomaly_theme_string = dynamic_zone_anomalies.get_theme_string(news_anom_theme) or ""
|
||||
local news_text = game.translate_string(msg_to_translate)
|
||||
|
||||
news_text = news_text:gsub("%$SENDER_NAME", character_name or CONST_MESSAGE_FALLBACK_NAME)
|
||||
news_text = news_text:gsub("%$FROM", game.translate_string(level_from))
|
||||
news_text = news_text:gsub("%$TO", game.translate_string(level_to))
|
||||
news_text = news_text:gsub("%$ANOM_THEME", game.translate_string(anomaly_theme_string))
|
||||
|
||||
actor:give_game_news(news_caption, news_text, texture, timeout *1000, showtime *1000, 0)
|
||||
if (not suppress_pda_sound) then
|
||||
xr_sound.set_sound_play(AC_ID, "pda_tips") -- play default sound
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,482 @@
|
||||
--[[
|
||||
DYNAMIC ZONE
|
||||
|
||||
Original Author(s)
|
||||
Singustromo <singustromo at disroot.org>
|
||||
|
||||
Edited by
|
||||
|
||||
License
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
|
||||
(https://creativecommons.org/licenses/by-nc-sa/4.0)
|
||||
|
||||
This file provides the rough structure for saving transitions and
|
||||
their pairs - called routes in this context.
|
||||
|
||||
Additional info:
|
||||
tables, functions, userdata and threads are passed around by reference,
|
||||
while numbers, booleans, and nil are passed by value
|
||||
Despite this we reference entries by their index because both tables
|
||||
are saved in m_data. Can't reference them.
|
||||
--]]
|
||||
|
||||
parent = _G["dynamic_zone"]
|
||||
if not (parent and parent.VERSION and parent.VERSION >= 20241224) then return end
|
||||
|
||||
--------------------------
|
||||
-- Dependencies --
|
||||
--------------------------
|
||||
|
||||
-- verified in main script
|
||||
local utils = dynamic_zone_utils
|
||||
local debug = dynamic_zone_debug
|
||||
|
||||
CONST_LOGGING_PREFIX = "Routes"
|
||||
local log = debug.log_register("info", CONST_LOGGING_PREFIX)
|
||||
local log_error = debug.log_register("error", CONST_LOGGING_PREFIX)
|
||||
|
||||
---------------------
|
||||
-- Structure --
|
||||
---------------------
|
||||
|
||||
registered_routes = {}
|
||||
registered_transitions = {
|
||||
--[[
|
||||
[size] = <int>,
|
||||
[transition_name] = {
|
||||
route = route_id, -- index in registeres_routes
|
||||
master = true, -- exception for 1 -> n routes (e.g. truck cemetary)
|
||||
spawned_anomalies = {}
|
||||
},
|
||||
...
|
||||
--]]
|
||||
}
|
||||
|
||||
function route_count()
|
||||
return ((utils.is_table_empty(registered_routes))
|
||||
and 0 or size_table(registered_routes))
|
||||
end
|
||||
|
||||
function transition_count()
|
||||
return (registered_transitions.size or 0)
|
||||
end
|
||||
|
||||
function clear()
|
||||
registered_routes = {}
|
||||
registered_transitions = {}
|
||||
end
|
||||
|
||||
-- first initialization of a route
|
||||
--- @returns id of route (index)
|
||||
function route_create()
|
||||
local id = #registered_routes +1
|
||||
|
||||
registered_routes[id] = {
|
||||
id = id, -- Needed in rare cases
|
||||
members = { }, -- transition names
|
||||
connects = { }, -- level names (game_levels.ltx)
|
||||
blacklisted = false,
|
||||
unlocked = true,
|
||||
blocked = false,
|
||||
block_discovered = false,
|
||||
recently_discovered = false, -- set during emission (controls blip)
|
||||
anomaly_theme = 0,
|
||||
}
|
||||
|
||||
return id
|
||||
end
|
||||
|
||||
function transition_register(transition_name, route_id)
|
||||
if not utils.valid_type{ caller = "transition_register", "str", transition_name,
|
||||
"tbl", registered_routes[route_id] } then return end
|
||||
|
||||
local id = get_story_object_id(transition_name)
|
||||
if (utils.assert_failed(id, "%s is not a Story-ID. Not registered.")) then return end
|
||||
|
||||
local route_members = registered_routes[route_id].members
|
||||
if utils.table_has(route_members, transition_name) then return end
|
||||
|
||||
registered_transitions[transition_name] = {
|
||||
route = route_id,
|
||||
master = false,
|
||||
spawned_anomalies = {}, -- holds the object ids
|
||||
}
|
||||
|
||||
table.insert(route_members, transition_name)
|
||||
|
||||
local index = (registered_transitions.size or 0) +1
|
||||
registered_transitions.size = index
|
||||
return index
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
function transition_table_register(tbl, route_id)
|
||||
local registered = {}
|
||||
|
||||
if not utils.valid_type{ caller = "transition_table_register",
|
||||
"tbl", tbl, "int", route_id } then return registered end
|
||||
|
||||
for _, transition_name in pairs(tbl) do
|
||||
if not transition_register(transition_name, route_id) then
|
||||
log_error("Transition '%s' has already been registered!", transition_name)
|
||||
else
|
||||
registered[#registered +1] = transition_name
|
||||
end
|
||||
end
|
||||
|
||||
return registered
|
||||
end
|
||||
|
||||
-- @returns mutable reference
|
||||
function get_route(route_id)
|
||||
return registered_routes[route_id]
|
||||
end
|
||||
|
||||
-- @returns mutable reference
|
||||
function get_transition(transition_name)
|
||||
return registered_transitions[transition_name]
|
||||
end
|
||||
|
||||
function route_exists(route_id)
|
||||
return (nil ~= registered_routes[route_id])
|
||||
end
|
||||
|
||||
function transition_exists(transition_name)
|
||||
return (nil ~= registered_transitions[transition_name])
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-- Wacky implementation
|
||||
-- TODO: Improve condition declaration and parsing
|
||||
-- Conditions are table entries (route flags) which are or'ed
|
||||
-- attributes are negated with an '!' or can be joined via an and '*'
|
||||
-- e.g. `get_routes{"blocked*!discovered", "!blocked*discovered"}`
|
||||
-- Should only be used with route flags (true, false)
|
||||
-- @returns <table> (route_ids)
|
||||
function get_routes(conditions)
|
||||
if not utils.valid_type{ caller = "get_routes", "tbl", conditions } then return end
|
||||
if utils.is_table_empty(conditions) then return end
|
||||
|
||||
local matches = {}
|
||||
|
||||
local func_body = ""
|
||||
for _, condition in pairs(conditions) do
|
||||
local parsed = condition
|
||||
parsed = parsed:gsub('*!', ' and not ref.')
|
||||
parsed = parsed:gsub('*', ' and ref.')
|
||||
parsed = parsed:gsub('!', 'not ref.')
|
||||
parsed = (string.find(parsed, "^not")) and parsed or "ref." .. parsed
|
||||
func_body = func_body .. "(" .. parsed .. ") or "
|
||||
end
|
||||
func_body = func_body:sub(1, -5) -- remove last ' or '
|
||||
|
||||
local to_eval = "return function(ref) return (" .. func_body .. ") end"
|
||||
local func, err = loadstring(to_eval)
|
||||
if (err) then
|
||||
log_error("Unable to evaluate '%s'\n! %s", to_eval, err)
|
||||
return
|
||||
end
|
||||
|
||||
-- loadstring encapsulates input into another function
|
||||
local check_func = func()
|
||||
|
||||
for route_id, route in iterate_routes(true) do
|
||||
local status, retval = pcall(check_func, route)
|
||||
|
||||
if (status and retval) then
|
||||
matches[#matches +1] = route_id
|
||||
end
|
||||
end
|
||||
|
||||
return matches
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-- Filters locked and blacklisted routes by default
|
||||
-- @returns name, modifiable route reference
|
||||
function iterate_routes(include_inactive)
|
||||
local index = 0
|
||||
|
||||
return function()
|
||||
index = index + 1
|
||||
|
||||
if not (include_inactive) then
|
||||
while (registered_routes[index] and route_inactive(index)) do
|
||||
index = index + 1
|
||||
end
|
||||
end
|
||||
|
||||
if (index <= #registered_routes) then
|
||||
return index, registered_routes[index]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- @returns name, mutable-reference
|
||||
function iterate_transitions(route_id)
|
||||
local route = get_route(route_id)
|
||||
local members = route and route.members
|
||||
if (not members) then return end
|
||||
|
||||
local index = 0
|
||||
return function()
|
||||
index = index + 1
|
||||
if (index > #members) then return end
|
||||
|
||||
return members[index], get_transition(members[index])
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
function set_transition_property(name, property, value)
|
||||
local transition = get_transition(name)
|
||||
if not utils.valid_type{ caller = "set_transition_property",
|
||||
"tbl", transition, "str", property} then return end
|
||||
|
||||
if (type(value) == 'nil') then return end
|
||||
if (type(transition[property]) == "nil") then return end
|
||||
|
||||
transition[property] = value
|
||||
return true
|
||||
end
|
||||
|
||||
-- @returns immutable reference
|
||||
function get_transition_property(name, property)
|
||||
local transition = get_transition(name)
|
||||
if (not transition) then return end
|
||||
|
||||
return transition[property]
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
function set_route_property(route_id, property, value)
|
||||
local route = get_route(route_id)
|
||||
if not utils.valid_type{ caller = "set_route_property",
|
||||
"tbl", route, "str", property} then return end
|
||||
|
||||
if (type(value) == 'nil') then return end
|
||||
if (type(route[property]) == "nil") then return end
|
||||
|
||||
route[property] = value
|
||||
return true
|
||||
end
|
||||
|
||||
-- @returns immutable reference
|
||||
function get_route_property(route_id, property)
|
||||
local route = get_route(route_id)
|
||||
if (not route) then return end
|
||||
|
||||
return route[property]
|
||||
end
|
||||
|
||||
function get_route_id(transition_name)
|
||||
local transition = get_transition(transition_name)
|
||||
return (transition and transition.route)
|
||||
end
|
||||
|
||||
-- used when we only have the transition name
|
||||
-- @returns modifyable route reference
|
||||
function get_route_by_transition(gizmo)
|
||||
local transition_name = (type(gizmo) == "number")
|
||||
and get_object_story_id(gizmo) or gizmo
|
||||
|
||||
if (utils.assert_failed(transition_name)) then return end
|
||||
|
||||
local transition = get_transition(transition_name)
|
||||
return (transition and get_route(transition.route))
|
||||
end
|
||||
has_route = get_route_by_transition -- alias for readability's sake
|
||||
|
||||
-- @returns immutable reference
|
||||
function get_route_members(route_id)
|
||||
local route = get_route(route_id)
|
||||
return (route and route.members)
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
function route_inactive(route_id)
|
||||
local route = get_route(route_id)
|
||||
if (not route) then return true end
|
||||
|
||||
return (route.blacklisted or (not route.unlocked))
|
||||
end
|
||||
|
||||
-- only really need this, if we do not replace accessible zone
|
||||
function route_unlock(route_id)
|
||||
local route = get_route(route_id)
|
||||
if (not route) then return end
|
||||
|
||||
route.unlocked = true
|
||||
return true
|
||||
end
|
||||
|
||||
function route_unlocked(route_id)
|
||||
local route = get_route(route_id)
|
||||
if (not route) then return end
|
||||
|
||||
return (route.unlocked)
|
||||
end
|
||||
|
||||
function route_locked(route_id)
|
||||
return (not route_unlocked(route_id))
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
function route_block(route_id)
|
||||
local route = get_route(route_id)
|
||||
if not (route and route.unlocked) then return end
|
||||
|
||||
route.blocked = true
|
||||
return true
|
||||
end
|
||||
|
||||
function route_unblock(route_id)
|
||||
local route = get_route(route_id)
|
||||
if not (route and route.unlocked and route.blocked) then return end
|
||||
|
||||
route.blocked = false
|
||||
return true
|
||||
end
|
||||
|
||||
function route_blocked(route_id)
|
||||
local route = get_route(route_id)
|
||||
if (not route) then return end
|
||||
|
||||
return (route.blocked)
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
-- We also pass an additional functor to be executed here
|
||||
function route_discover(route_id, functor, ...)
|
||||
local vararg = {...}
|
||||
|
||||
local route = get_route(route_id)
|
||||
if not (route and route.blocked) then return end
|
||||
|
||||
route.block_discovered = true
|
||||
|
||||
if (functor and type(functor) == "function") then
|
||||
functor(unpack(vararg))
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function route_discovered(route_id)
|
||||
local route = get_route(route_id)
|
||||
return (route and route.block_discovered)
|
||||
end
|
||||
|
||||
function route_known_state_differs(route_id)
|
||||
local route = get_route(route_id)
|
||||
return route and (route.blocked ~= route.block_discovered)
|
||||
end
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
function transition_tostring(transition_name, indentation, indent_str)
|
||||
indentation = (indentation) or 0
|
||||
indent_str = (indent_str) or " "
|
||||
local transition = get_transition(transition_name)
|
||||
local string = string.rep(indent_str, indentation)
|
||||
.. "[" .. transition_name .. "] = {\n"
|
||||
|
||||
for key, value in pairs(transition) do
|
||||
if (key == "route") then
|
||||
goto next_attribute
|
||||
end
|
||||
|
||||
string = string .. string.rep(indent_str, indentation +1)
|
||||
.. key .. " = "
|
||||
|
||||
if (type(value) == "table") then
|
||||
string = string .. "{" .. table.concat(value, ",") .. "}"
|
||||
else
|
||||
string = string .. tostring(value)
|
||||
end
|
||||
string = string .. "\n"
|
||||
|
||||
:: next_attribute ::
|
||||
end
|
||||
|
||||
return string .. string.rep(indent_str, indentation) .. "}\n"
|
||||
end
|
||||
|
||||
-- crude and specific string generation of attribute tables
|
||||
function route_tostring(route_id, include_member_attributes)
|
||||
local route = get_route(route_id)
|
||||
if (not route) then
|
||||
log_error("Printinfo No route with ID #%s !", route_id)
|
||||
return
|
||||
end
|
||||
|
||||
local indentation = " "
|
||||
local string = string.format("[%03d]\n", route_id)
|
||||
for key, value in pairs(route) do
|
||||
if (key == 'id') then goto next_attribute end
|
||||
|
||||
string = string .. indentation .. key .. " = "
|
||||
|
||||
if (type(value) ~= "table") then
|
||||
string = string .. tostring(value)
|
||||
goto continue
|
||||
end
|
||||
|
||||
if ((not include_member_attributes) or key ~= "members") then
|
||||
string = string .. "{" .. table.concat(value, ", ") .. "}"
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- lazy way to serialize member attributes
|
||||
string = string .. "{\n"
|
||||
for _, section in pairs(value) do
|
||||
string = string .. transition_tostring(section, 2, indentation)
|
||||
end
|
||||
string = string .. indentation .. "}"
|
||||
|
||||
:: continue ::
|
||||
string = string .. "\n"
|
||||
|
||||
:: next_attribute ::
|
||||
end
|
||||
|
||||
return string:sub(1, -2) -- remove last new line
|
||||
end
|
||||
|
||||
-- Prints all routes with the specified attributes
|
||||
-- @returns amount of all blocked routes
|
||||
function print_routes(conditions)
|
||||
local output_string = string.format("Route Attributes%s: ",
|
||||
(conditions) and " (" .. table.concat(conditions, ", ") .. ")" or "")
|
||||
|
||||
local routes = get_routes(conditions)
|
||||
if (not routes or utils.is_table_empty(routes)) then return end
|
||||
|
||||
for _, route_id in pairs(routes) do
|
||||
output_string = output_string .. "\n" .. route_tostring(route_id, true)
|
||||
end
|
||||
|
||||
log("%s", output_string)
|
||||
end
|
||||
|
||||
-- Prints all the information agnostic to what type the parameter is
|
||||
function printinfo(gizmo)
|
||||
local output_string = "Attributes of"
|
||||
if route_exists(gizmo) then
|
||||
output_string = string.format("%s route #%s:\n%s",
|
||||
output_string, gizmo, route_tostring(gizmo, true))
|
||||
elseif transition_exists(gizmo) then
|
||||
output_string = string.format("%s transition '%s':\n%s",
|
||||
output_string, gizmo, transition_tostring(gizmo))
|
||||
else return end
|
||||
|
||||
log("%s", output_string)
|
||||
end
|
||||
@@ -0,0 +1,641 @@
|
||||
--[[
|
||||
DYNAMIC ZONE
|
||||
|
||||
Original Author(s)
|
||||
Singustromo <singustromo at disroot.org>
|
||||
|
||||
Edited by
|
||||
|
||||
License
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
|
||||
(https://creativecommons.org/licenses/by-nc-sa/4.0)
|
||||
|
||||
This script contains general purpose utility functions and logic used
|
||||
to decouple the vanilla anomaly logic from the addon logic.
|
||||
|
||||
We've taken some inspiration from NLTP_ASHES' Western Goods.
|
||||
--]]
|
||||
|
||||
parent = _G["dynamic_zone"]
|
||||
if not (parent and parent.VERSION and parent.VERSION >= 20241224) then return end
|
||||
|
||||
CONST_GAMETICK_DURATION_MS = 16
|
||||
CONST_DEFAULT_PROXIMITY_DISTANCE = 2^16 -1
|
||||
CONST_MAX_VALID_LVID = 2^32 -2
|
||||
|
||||
-------------------------
|
||||
-- Dependencies
|
||||
-------------------------
|
||||
|
||||
local opt = dynamic_zone_mcm
|
||||
local debug = dynamic_zone_debug
|
||||
|
||||
CONST_LOGGING_PREFIX = "Utilities"
|
||||
local log = debug.log_register("info", CONST_LOGGING_PREFIX)
|
||||
local log_error = debug.log_register("error", CONST_LOGGING_PREFIX)
|
||||
|
||||
----------------------------
|
||||
-- Input Validation --
|
||||
----------------------------
|
||||
|
||||
-- Inspired by Western Goods
|
||||
-- Checks, if all necessary parameters are non-nil and have the correct type
|
||||
-- Use it like this: valid_type{ caller = "function_name", type, value, ... }
|
||||
-- The boolean type explicitely accepts only true|false as values
|
||||
-- @param tbl
|
||||
-- @returns boolean
|
||||
function valid_type(tbl)
|
||||
if (not opt.get("validate_parameter_types")) then return true end
|
||||
|
||||
local type_alias = {
|
||||
int = "number", str = "string", tbl = "table",
|
||||
fn = "function" , bool = "boolean", usr = "userdata",
|
||||
}
|
||||
|
||||
local caller = tbl.caller and string.format("[%s] ", tbl.caller) or ""
|
||||
local param_index, encountered_error = 1, false
|
||||
for i=1, #tbl, 2 do
|
||||
local typ, value, value_type = tbl[i], tbl[i +1]
|
||||
typ = type_alias[typ] or typ
|
||||
|
||||
if not (typ == "boolean" or value) then
|
||||
log_error("%sParameter no. %s is nil%s", caller, param_index, callstack(nil, true))
|
||||
encountered_error = true
|
||||
goto continue
|
||||
end
|
||||
|
||||
-- also accounts for nil values (it's own type)
|
||||
value_type = type(value)
|
||||
if value_type ~= typ then
|
||||
log_error("%sType mismatch for parameter no. %s (%s != %s)%s",
|
||||
caller, param_index, value_type, typ, callstack(nil, true))
|
||||
encountered_error = true
|
||||
end
|
||||
|
||||
:: continue ::
|
||||
param_index = param_index +1
|
||||
end
|
||||
|
||||
return (not encountered_error)
|
||||
end
|
||||
|
||||
-- Alternative to native assert that does not crash the game nor is intended to
|
||||
-- alter the control flow directly. It merely logs the message and a
|
||||
-- callstack via the default methods used by this addon
|
||||
-- @param condition (boolean)
|
||||
-- @param message (optional; format)
|
||||
-- @param vararg (format elements)
|
||||
-- @returns true when assertion failed
|
||||
function assert(condition, message, ...)
|
||||
condition = (type(condition) == nil) or condition -- nil-checking
|
||||
if (not opt.get("validate_parameter_types")) then return (not condition) end
|
||||
|
||||
if (not condition) then
|
||||
local args = {...}
|
||||
message = (message and type(message) == "string") and message or "failed!"
|
||||
|
||||
log_error("%s%s", string.format("Assertion: " .. message,
|
||||
unpack(args)), callstack(nil, true))
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- Just an alias for usage in if-statements
|
||||
assert_failed = assert
|
||||
|
||||
---------------------------
|
||||
-- Type Conversion --
|
||||
---------------------------
|
||||
|
||||
-- Only returns true, if string is "true" or of type bool
|
||||
function string_to_bool(str) return (str == "true" or s == true) end
|
||||
|
||||
-- Converts a comma delimited string into a 3 dimensional vector
|
||||
-- @returns vector on success
|
||||
function string_to_vector(str)
|
||||
if not valid_type{ caller = "string_to_vector", "str", str } then
|
||||
return
|
||||
end
|
||||
|
||||
local tbl = str_explode(str, ",") or {}
|
||||
if assert_failed(#tbl == 3, "Invalid posdata (not 3 elements)") then return end
|
||||
|
||||
for index, value in pairs(tbl) do
|
||||
tbl[index] = tonumber(value)
|
||||
if assert_failed(tbl[index], "Element is not a number") then return end
|
||||
end
|
||||
|
||||
return vector():set(tbl[1], tbl[2], tbl[3])
|
||||
end
|
||||
|
||||
-- Converts a comma delimited string in the form of
|
||||
-- `x, y, z, lvid, gvid` into positional data
|
||||
-- @returns position data as table (pos, lvid, gvid)
|
||||
function string_to_posdata(str)
|
||||
if not valid_type{ caller = "string_to_posdata", "str", str } then
|
||||
return
|
||||
end
|
||||
|
||||
local tbl = str_explode(str, ",")
|
||||
if assert_failed(#tbl == 5, "Invalid posdata (not 5 elements)") then return end
|
||||
|
||||
-- Convert all substrings to numbers
|
||||
for index, value in pairs(tbl) do
|
||||
tbl[index] = tonumber(value)
|
||||
if assert_failed(tbl[index], "Element is not a number") then return end
|
||||
end
|
||||
|
||||
local lvid, gvid = tbl[4], tbl[5]
|
||||
if assert_failed(lvid < CONST_MAX_VALID_LVID, "Invalid level vertex id") then return end
|
||||
|
||||
return {
|
||||
pos = vector():set(tbl[1], tbl[2], tbl[3]),
|
||||
lvid = lvid,
|
||||
gvid = gvid,
|
||||
}
|
||||
end
|
||||
|
||||
-- ARGB32 -> a byte for each channel
|
||||
-- @param pixelvalue e.g. return value from GetARGB(a,r,g,b)
|
||||
-- @returns table containing following keys: r, g, b, a
|
||||
function argb_convert_from_pixelvalue(pixelvalue)
|
||||
if not valid_type{ caller = "argb_convert_from_pixelvalue", "int", pixelvalue } then return end
|
||||
|
||||
local bitmask = 2^8 -1 -- 0xFF
|
||||
local alpha = bit.band(bit.rshift(pixelvalue, 24), bitmask)
|
||||
local red = bit.band(bit.rshift(pixelvalue, 16), bitmask)
|
||||
local green = bit.band(bit.rshift(pixelvalue, 8), bitmask)
|
||||
local blue = bit.band(pixelvalue, bitmask)
|
||||
|
||||
if assert_failed((red and green and blue and alpha), "Invalid pixelvalue (argb)!") then return end
|
||||
return { r = red, g = green, b = blue, a = alpha }
|
||||
end
|
||||
|
||||
-- Changes the weight of the alpha pixelvalue
|
||||
-- @param alpha 8-bit integer
|
||||
-- @returns pixelvalue (ARGB32)
|
||||
function argb_change_alpha(pixelvalue, alpha)
|
||||
if not valid_type{ caller = "argb_change_alpha",
|
||||
"int", pixelvalue, "int", alpha } then return end
|
||||
|
||||
local alphavalue = bit.lshift(bit.band(alpha, 2^8 -1), 24) -- make sure it's 1 byte
|
||||
local no_alpha = bit.band(pixelvalue, 2^24 -1)
|
||||
|
||||
if assert_failed((no_alpha and alphavalue), "Invalid pixelvalue (argb)!") then return end
|
||||
return bit.bor(no_alpha, alphavalue)
|
||||
end
|
||||
|
||||
---------------------------
|
||||
-- Table Functions --
|
||||
---------------------------
|
||||
|
||||
-- output of next() is nil when table is empty
|
||||
function is_table_empty(tbl)
|
||||
return not (tbl and next(tbl))
|
||||
end
|
||||
|
||||
function index_of(tbl, value)
|
||||
if not valid_type{ caller = "index_of", "tbl", tbl } then return end
|
||||
|
||||
for k, v in pairs(tbl) do
|
||||
if (v == value) then return k end
|
||||
end
|
||||
end
|
||||
|
||||
-- Checks, if table includes the value; Also checks subtables
|
||||
-- @returns true if value is in table
|
||||
function table_has(tbl, value)
|
||||
if not valid_type{ caller = "table_has", "tbl", tbl } then return end
|
||||
|
||||
for _, v in pairs(tbl) do
|
||||
if v == value then return true end
|
||||
|
||||
if type(v) == 'table' and table_has(v, value) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Checks, if all elements of tbl_in are contained in tbl
|
||||
-- @returns boolean
|
||||
function table_contains(tbl, tbl_in)
|
||||
for _, v in pairs(tbl_in) do
|
||||
if (not table_has(tbl, v)) then return end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Creates subtable with key, if needed, needs the type of the value
|
||||
-- @returns success state of insertion
|
||||
function table_safe_insert(tbl, key, value, value_type)
|
||||
if not valid_type{ caller = "table_safe_insert",
|
||||
"tbl", tbl, "str", key, value_type, value } then return end
|
||||
|
||||
if not tbl[key] then
|
||||
tbl[key] = { value }
|
||||
else
|
||||
table.insert(tbl[key], value)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function table_copy(tbl)
|
||||
if not valid_type{ caller = "table_copy", "tbl", tbl } then return end
|
||||
|
||||
local copy = {}
|
||||
for k,v in pairs(tbl) do
|
||||
copy[k] = (type(v) == "table") and table_copy(v) or v
|
||||
end
|
||||
|
||||
return copy
|
||||
end
|
||||
|
||||
-- recursively swaps values to keys into a one-dimensional dictionary
|
||||
function values_to_keys(tbl, result)
|
||||
if not valid_type{ caller = "values_to_keys",
|
||||
"tbl", tbl, "tbl", result } then return end
|
||||
|
||||
for k, v in pairs(tbl) do
|
||||
if type(v) == 'table' then
|
||||
values_to_keys(v, result)
|
||||
else
|
||||
result[v] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- lists all keys in tbl into an indexed table
|
||||
function index_keys(tbl, result)
|
||||
if not valid_type{ caller = "index_keys",
|
||||
"tbl", tbl, "tbl", result } then return end
|
||||
|
||||
for key, v in pairs(tbl) do
|
||||
result[#result +1] = key
|
||||
end
|
||||
end
|
||||
|
||||
------------------------
|
||||
-- Closures --
|
||||
------------------------
|
||||
|
||||
function random_numbered_sequence(from, to)
|
||||
if not valid_type{ caller = "random_numbered_sequence",
|
||||
"int", from, "int", to } then return end
|
||||
|
||||
local tbl = {}
|
||||
for i = from, to do
|
||||
tbl[#tbl + 1] = i
|
||||
end
|
||||
|
||||
for i = #tbl, 2, -1 do
|
||||
local j = math.random(i)
|
||||
tbl[i], tbl[j] = tbl[j], tbl[i]
|
||||
end
|
||||
|
||||
local index = 0
|
||||
return function()
|
||||
if index > #tbl then return end
|
||||
index = index + 1
|
||||
return tbl[index]
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------
|
||||
-- Timed Function Execution --
|
||||
------------------------------------
|
||||
|
||||
-- Taken from Western Goods
|
||||
-- executes functor once on next tick
|
||||
-- @author: demonized
|
||||
function next_tick(functor, ...)
|
||||
if not valid_type{ caller = "next_tick", "fn", functor } then return end
|
||||
|
||||
local args = {...}
|
||||
AddUniqueCall(function()
|
||||
functor(unpack(args))
|
||||
return true
|
||||
end)
|
||||
end
|
||||
|
||||
-- Wrapper to throttle function execution with time delay
|
||||
-- Modified derivative from modded exes
|
||||
-- @delay number (milliseconds)
|
||||
-- @delay_first_call boolean
|
||||
-- @func functor
|
||||
-- @vararg functor-parameters
|
||||
-- @returns functor
|
||||
function throttle(delay, delay_first_call, functor, ...)
|
||||
if not valid_type{ caller = "throttle",
|
||||
"int", delay, "bool", delay_first_call, "fn", functor } then return end
|
||||
|
||||
local args = {...}
|
||||
if not (delay and delay > (CONST_GAMETICK_DURATION_MS or 16)) then
|
||||
return function()
|
||||
return functor(unpack(args))
|
||||
end
|
||||
end
|
||||
|
||||
local TimeGlobal = time_global
|
||||
local tg_threshold = (delay_first_call)
|
||||
and TimeGlobal() + delay or 0
|
||||
|
||||
return function()
|
||||
local tg = TimeGlobal()
|
||||
if (tg_threshold +1) > tg then return end
|
||||
tg_threshold = tg + delay
|
||||
|
||||
return functor(unpack(args))
|
||||
end
|
||||
end
|
||||
|
||||
-- Repeatedly calls functor
|
||||
-- Time should be declared in milliseconds
|
||||
function timed_call(delay, functor, ...)
|
||||
if not valid_type{ caller = "timed_call", "int", delay, "fn", functor } then return end
|
||||
local args = {...}
|
||||
|
||||
-- We also delay first execution
|
||||
local throttled_func = throttle(delay, true, functor, unpack(args))
|
||||
if (not throttled_func) then return end
|
||||
|
||||
-- Calls functor every game tick (approx. 16-17 ms)
|
||||
AddUniqueCall(throttled_func)
|
||||
|
||||
if (opt.get("validate_parameter_types")) then
|
||||
log("timed_call | Added Unique Call for %s with delay of %sms%s",
|
||||
functor, delay, callstack(nil, true))
|
||||
end
|
||||
end
|
||||
|
||||
---------------------------
|
||||
-- Map Markers --
|
||||
---------------------------
|
||||
|
||||
function map_spot_exists(id, spot)
|
||||
if not valid_type{ caller = "map_spot_exists",
|
||||
"int", id, "str", spot } then return end
|
||||
|
||||
return (level.map_has_object_spot(id,spot) == 1)
|
||||
end
|
||||
|
||||
function map_spot_remove(id, spot)
|
||||
if assert_failed(map_spot_exists(id, spot), "Spot (%s, %s) does not exist!", id, spot) then
|
||||
return
|
||||
end
|
||||
|
||||
level.map_remove_object_spot(id, spot)
|
||||
return true
|
||||
end
|
||||
|
||||
function map_spot_add(id, spot, hint)
|
||||
if not valid_type{ caller = "map_spot_add", "str", hint } then return end
|
||||
|
||||
if assert_failed(not map_spot_exists(id, spot), "Spot (%s, %s) already exists!", id, spot) then
|
||||
return
|
||||
end
|
||||
|
||||
level.map_add_object_spot_ser(id, spot, hint)
|
||||
return true
|
||||
end
|
||||
|
||||
function map_spot_change_hint(id, spot, hint)
|
||||
if not valid_type{ caller = "map_spot_change_hint",
|
||||
"int", id, "str", spot, "str", hint } then return end
|
||||
|
||||
if assert_failed(map_spot_exists(id, spot), "Spot (%s, %s) does not exist!", id, spot) then
|
||||
return
|
||||
end
|
||||
|
||||
level.map_change_spot_hint(id, spot, hint)
|
||||
return true
|
||||
end
|
||||
|
||||
-- Read defined attributes from map_spots.xml for a given map spot
|
||||
-- Uses a cache to speed up subsequent calls
|
||||
-- @param spot (spot name; e.g. level_changer_right)
|
||||
-- @returns table || nil (table contains texture and color (pixelvalue), if read)
|
||||
local _cache_map_spot_texture_info = {}
|
||||
function map_spot_get_texture_info(spot, color_as_argb, default_alpha)
|
||||
if not valid_type{ caller = "get_mapspot_texture", "str", spot } then return end
|
||||
|
||||
local cached_result = _cache_map_spot_texture_info[spot]
|
||||
if (cached_result) then return (cached_result) end
|
||||
|
||||
default_alpha = default_alpha or 255
|
||||
spot = spot .. "_spot" -- that node got the actual info we need
|
||||
local attr_tex = "texture"
|
||||
|
||||
local xml = _cache_map_spot_texture_info.parser
|
||||
if (not xml) then
|
||||
xml = CScriptXmlInit()
|
||||
xml:ParseFile("map_spots.xml")
|
||||
_cache_map_spot_texture_info.parser = xml
|
||||
end
|
||||
|
||||
xml:NavigateToRoot() -- to <map_spots>
|
||||
if assert_failed(xml:NodeExist(spot, 0), "Node %s does not exist", spot) then return end
|
||||
xml:NavigateToNode(spot, 0)
|
||||
|
||||
local result = {}
|
||||
result.texture = xml:ReadValue(attr_tex, 0)
|
||||
if assert_failed(xml:NodeExist(attr_tex, 0), "Node %s/%s does not exist", spot, attr_tex) then return end
|
||||
|
||||
local color = {}
|
||||
color.r = tonumber(xml:ReadAttribute(attr_tex, 0, "r"))
|
||||
color.g = tonumber(xml:ReadAttribute(attr_tex, 0, "g"))
|
||||
color.b = tonumber(xml:ReadAttribute(attr_tex, 0, "b"))
|
||||
|
||||
if (not is_table_empty(color)) then
|
||||
color.a = tonumber(xml:ReadAttribute(attr_tex, 0, "a")) or default_alpha
|
||||
|
||||
result.color = color
|
||||
if (not color_as_argb) then
|
||||
result.color = GetARGB(color.a, color.r, color.g, color.b)
|
||||
end
|
||||
end
|
||||
|
||||
if (not is_table_empty(result)) then
|
||||
_cache_map_spot_texture_info[spot] = result
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
-- REQUIRES MODDED-EXES
|
||||
-- Changes the map spot texture of any given object (given that it already has a map spot)
|
||||
-- @param id object-id
|
||||
-- @param spot current texture
|
||||
-- @param texture new texture id
|
||||
-- @returns true on success
|
||||
function map_spot_change_texture(id, spot, texture)
|
||||
if not valid_type{ caller = "map_spot_change_spot", "str", texture } then return end
|
||||
|
||||
if assert_failed(map_spot_exists(id, spot), "Spot (%s, %s) does not exist!", id, spot) then
|
||||
return
|
||||
end
|
||||
|
||||
local spot_static = level.map_get_object_minimap_spot_static(id, spot)
|
||||
local mini_static = level.map_get_object_spot_static(id, spot)
|
||||
if assert_failed(spot_static and mini_static) then return end
|
||||
|
||||
spot_static:InitTexture(texture)
|
||||
mini_static:InitTexture(texture)
|
||||
return true
|
||||
end
|
||||
|
||||
-- REQUIRES MODDED-EXES
|
||||
-- @param color argb32 pixelvalue (e.g. GetARGB(a,r,g,b))
|
||||
-- @returns true on success
|
||||
function map_spot_change_color(id, spot, color)
|
||||
if not valid_type{ caller = "map_spot_change_color", "int", color } then return end
|
||||
|
||||
if assert_failed(map_spot_exists(id, spot), "Spot (%s, %s) does not exist!", id, spot) then
|
||||
return
|
||||
end
|
||||
|
||||
local main_mapspot = level.map_get_object_spot_static(id, spot)
|
||||
local mini_mapspot = level.map_get_object_minimap_spot_static(id, spot)
|
||||
main_mapspot:SetTextureColor(color)
|
||||
mini_mapspot:SetTextureColor(color)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- REQUIRES MODDED-EXES
|
||||
-- @returns pixelvalue (integer)
|
||||
function map_spot_get_color(id, spot, get_mini)
|
||||
if assert_failed(map_spot_exists(id, spot), "Spot (%s, %s) does not exist!", id, spot) then
|
||||
return
|
||||
end
|
||||
|
||||
local main_mapspot = level.map_get_object_spot_static(id, spot)
|
||||
local mini_mapspot = (get_mini) and level.map_get_object_minimap_spot_static(id, spot)
|
||||
|
||||
return (mini_mapspot and mini_mapspot:GetTextureColor())
|
||||
or (main_mapspot and main_mapspot:GetTextureColor())
|
||||
end
|
||||
|
||||
-----------------------------------
|
||||
-- Game related Checks --
|
||||
-----------------------------------
|
||||
|
||||
function has_translation(string)
|
||||
if not valid_type{ caller = "has_translation", "str", string } then return end
|
||||
return (game.translate_string(string) ~= string)
|
||||
end
|
||||
|
||||
------------------------------
|
||||
-- Object related --
|
||||
------------------------------
|
||||
|
||||
-- @param obj (optional)
|
||||
-- @returns level name
|
||||
function get_mapname(obj)
|
||||
if (not obj) then
|
||||
return level.name()
|
||||
elseif not valid_type{ caller = "get_mapname", "userdata", obj } then
|
||||
return
|
||||
end
|
||||
|
||||
local gvid
|
||||
if (obj.online and type(obj.id) == "function") then
|
||||
gvid = obj:game_vertex_id()
|
||||
elseif (obj.id) then
|
||||
gvid = obj.m_game_vertex_id
|
||||
end
|
||||
|
||||
return (alife():level_name(game_graph():vertex(gvid):level_id()))
|
||||
end
|
||||
|
||||
-- Only returns correct position for online objects as
|
||||
-- we only check for the proximity to the player
|
||||
-- @returns distance (to player)
|
||||
function get_proximity_by_id(id, default)
|
||||
if not valid_type{ caller = "get_proximity_by_id", "int", id } then return end
|
||||
|
||||
default = (default and type(default) == "number") or CONST_DEFAULT_PROXIMITY_DISTANCE
|
||||
|
||||
local se_obj = id and alife_object(id)
|
||||
if not (se_obj and se_obj.online) then return default end
|
||||
|
||||
local pos = se_obj.position
|
||||
return (pos and pos:distance_to(db.actor:position()) or default)
|
||||
end
|
||||
|
||||
function get_point_proximity_by_id(id, point, default)
|
||||
if not valid_type{ caller = "get_point_proximity_by_id", "int", id, "usr", point } then return end
|
||||
default = default or CONST_DEFAULT_PROXIMITY_DISTANCE
|
||||
|
||||
local se_obj = id and alife_object(id)
|
||||
if not (se_obj and se_obj.online) then return default end
|
||||
|
||||
local pos = se_obj.position
|
||||
return (pos and pos:distance_to(point) or default)
|
||||
end
|
||||
|
||||
-----------------------------
|
||||
-- Level related --
|
||||
-----------------------------
|
||||
|
||||
-- This works with and without modded exes
|
||||
-- @returns pos from the target in the center of the viewport
|
||||
function get_target_pos()
|
||||
return (level.get_target_pos) and level.get_target_pos() -- REQUIRES MODDED-EXES
|
||||
or device().cam_pos:add(device().cam_dir:mul(level.get_target_dist()))
|
||||
end
|
||||
|
||||
-- Returns the position of a level vertex in close proximity (determined in-engine)
|
||||
-- @param pos vector
|
||||
-- @returns vector (copy)
|
||||
function get_closest_vertex_pos(pos)
|
||||
if not valid_type{ caller = "get_closest_vertex_pos", "usr", pos } then return end
|
||||
local lvid = pos and pos.x and level.vertex_id(pos)
|
||||
|
||||
-- signed & last are reserved
|
||||
if not (lvid and (lvid < CONST_MAX_VALID_LVID)) then return end
|
||||
local pos = level.vertex_position(lvid)
|
||||
|
||||
return vector():set(pos.x, pos.y, pos.z)
|
||||
end
|
||||
|
||||
-- Taken from Catspaw's utilities
|
||||
function levelname_from_gvid(gvid)
|
||||
if not valid_type{ caller = "levelname_from_gvid", "int", gvid } then return end
|
||||
|
||||
local gv = game_graph():vertex(gvid)
|
||||
return alife():level_name(gv:level_id())
|
||||
end
|
||||
|
||||
-- Requires that the weather manager is running (just before actor_on_first_update)
|
||||
-- Additional levels need to be registered there to have working weather
|
||||
-- @param level_name
|
||||
-- @returns boolean
|
||||
function is_underground(level)
|
||||
if not valid_type{ caller = "is_underground", "str", level } then return end
|
||||
|
||||
return (not level_weathers.valid_levels[level])
|
||||
end
|
||||
|
||||
function debug_level_loaded()
|
||||
return (get_mapname() == "fake_start")
|
||||
end
|
||||
|
||||
-- Uses txr_routes to determine connected maps
|
||||
-- @param map
|
||||
-- @returns table
|
||||
function get_directly_connected_maps(map)
|
||||
local get_txr_section = txr_routes.get_section
|
||||
local get_txr_mapname = txr_routes.get_map
|
||||
local routes = txr_routes.routes
|
||||
|
||||
local txr_section = map and get_txr_mapname(map)
|
||||
if not (txr_section and routes[txr_section]) then return end
|
||||
|
||||
local connected = {}
|
||||
for map, _ in pairs(routes[txr_section]) do
|
||||
connected[#connected +1] = get_txr_section(map)
|
||||
end
|
||||
|
||||
return connected
|
||||
end
|
||||
@@ -0,0 +1,571 @@
|
||||
-- https://github.com/ubilabs/kd-tree-javascript
|
||||
-- k-d Tree Implementation for Lua for quick search in multidimensional tables
|
||||
-- k-d trees are a useful data structure for several applications, such as searches involving a multidimensional search key (e.g. range searches and nearest neighbor searches). k-d trees are a special case of binary space partitioning trees.
|
||||
-- Rewritten to pure Lua and adapted for usage in Anomaly by demonized
|
||||
|
||||
local math_floor = math.floor
|
||||
local math_log = math.log
|
||||
local math_max = math.max
|
||||
local math_min = math.min
|
||||
|
||||
local table_insert = table.insert
|
||||
local table_remove = table.remove
|
||||
local table_sort = table.sort
|
||||
|
||||
local empty_table = empty_table
|
||||
|
||||
local pairs = pairs
|
||||
|
||||
local function table_slice(t, first, last)
|
||||
local res = {}
|
||||
for i = first or 1, last and last - 1 or #t do
|
||||
res[#res + 1] = t[i]
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
-- http://lua-users.org/wiki/BinaryInsert
|
||||
local function binary_insert(t, value, fcomp)
|
||||
-- Initialise compare function
|
||||
local fcomp = fcomp or function(a, b) return a < b end
|
||||
|
||||
-- print_table(value)
|
||||
|
||||
-- Initialise numbers
|
||||
local iStart, iEnd, iMid, iState = 1, #t, 1, 0
|
||||
|
||||
if iEnd == 0 then
|
||||
t[1] = value
|
||||
-- printf("adding in beginning table empty")
|
||||
return 1
|
||||
end
|
||||
|
||||
if fcomp(value, t[1]) then
|
||||
-- printf("adding in beginning %s of %s", 1, iEnd)
|
||||
table_insert(t, 1, value)
|
||||
return 1
|
||||
end
|
||||
|
||||
if not fcomp(value, t[iEnd]) then
|
||||
-- printf("adding in end %s of %s", iEnd + 1, iEnd)
|
||||
local pos = iEnd + 1
|
||||
t[pos] = value
|
||||
return pos
|
||||
end
|
||||
|
||||
-- Get insert position
|
||||
while iStart <= iEnd do
|
||||
|
||||
-- calculate middle
|
||||
iMid = math_floor((iStart + iEnd) / 2)
|
||||
|
||||
-- compare
|
||||
if fcomp(value, t[iMid]) then
|
||||
iEnd, iState = iMid - 1, 0
|
||||
else
|
||||
iStart, iState = iMid + 1, 1
|
||||
end
|
||||
end
|
||||
|
||||
local pos = iMid + iState
|
||||
-- printf("adding in middle %s of %s", pos, iEnd)
|
||||
table_insert(t, pos, value)
|
||||
return pos
|
||||
end
|
||||
|
||||
function Node(obj, dimension, parent)
|
||||
local node = {}
|
||||
|
||||
node.obj = obj
|
||||
node.left = nil
|
||||
node.right = nil
|
||||
node.parent = parent
|
||||
node.dimension = dimension
|
||||
|
||||
return node
|
||||
end
|
||||
|
||||
function kdTree(points, metric, dimensions)
|
||||
local kd_tree = {}
|
||||
|
||||
kd_tree.points = points or {}
|
||||
kd_tree.metric = metric
|
||||
kd_tree.dimensions = dimensions
|
||||
|
||||
local function buildTree(new_points, depth, parent)
|
||||
local dim = (depth % #dimensions) + 1
|
||||
local median
|
||||
local node
|
||||
|
||||
if not new_points then
|
||||
return
|
||||
end
|
||||
|
||||
if #new_points == 0 then
|
||||
-- printf("buildTree #new_points == 0")
|
||||
return
|
||||
end
|
||||
|
||||
if #new_points == 1 then
|
||||
-- printf("buildTree #new_points == 1")
|
||||
return Node(new_points[1], dim, parent)
|
||||
end
|
||||
|
||||
table_sort(new_points, function(a, b)
|
||||
return a[dimensions[dim]] < b[dimensions[dim]]
|
||||
end)
|
||||
|
||||
median = math_floor(#new_points / 2) + 1
|
||||
node = Node(new_points[median], dim, parent)
|
||||
node.left = buildTree(table_slice(new_points, 1, median), depth + 1, node)
|
||||
node.right = buildTree(table_slice(new_points, median + 1), depth + 1, node)
|
||||
|
||||
return node
|
||||
end
|
||||
|
||||
kd_tree.root = buildTree(points, 0, nil)
|
||||
|
||||
kd_tree.insertAndRebuild = function(self, point)
|
||||
self.points[#self.points + 1] = point
|
||||
self.root = buildTree(self.points, 0, nil)
|
||||
return self
|
||||
end
|
||||
|
||||
kd_tree.insert = function(self, point)
|
||||
local function innerSearch(node, parent)
|
||||
|
||||
if node == nil then
|
||||
return parent
|
||||
end
|
||||
|
||||
local dimension = self.dimensions[node.dimension]
|
||||
if point[dimension] < node.obj[dimension] then
|
||||
return innerSearch(node.left, node)
|
||||
else
|
||||
return innerSearch(node.right, node)
|
||||
end
|
||||
end
|
||||
|
||||
local insertPosition = innerSearch(self.root, nil)
|
||||
local newNode
|
||||
local dimension
|
||||
|
||||
if insertPosition == nil then
|
||||
self.points[#self.points + 1] = point
|
||||
self.root = buildTree(self.points, 0, nil)
|
||||
return self
|
||||
end
|
||||
|
||||
newNode = Node(point, (insertPosition.dimension + 1) % #self.dimensions, insertPosition)
|
||||
dimension = self.dimensions[insertPosition.dimension]
|
||||
|
||||
if point[dimension] < insertPosition.obj[dimension] then
|
||||
insertPosition.left = newNode
|
||||
else
|
||||
insertPosition.right = newNode
|
||||
end
|
||||
|
||||
self.points[#self.points + 1] = point
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
kd_tree.remove = function(self, point)
|
||||
local node
|
||||
|
||||
local function nodeSearch(node)
|
||||
if node == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if node.obj == point then
|
||||
return node
|
||||
end
|
||||
|
||||
local dimension = self.dimensions[node.dimension]
|
||||
|
||||
if point[dimension] < node.obj[dimension] then
|
||||
return nodeSearch(node.left, node)
|
||||
else
|
||||
return nodeSearch(node.right, node)
|
||||
end
|
||||
end
|
||||
|
||||
local function removeNode(node)
|
||||
local nextNode
|
||||
local nextObj
|
||||
local pDimension
|
||||
|
||||
local function findMin(node, dim)
|
||||
local dimension
|
||||
local own
|
||||
local left
|
||||
local right
|
||||
local min
|
||||
|
||||
if node == nil then
|
||||
return
|
||||
end
|
||||
|
||||
dimension = self.dimensions[dim]
|
||||
|
||||
if node.dimension == dim then
|
||||
if node.left ~= nil then
|
||||
return findMin(node.left, dim)
|
||||
end
|
||||
return node
|
||||
end
|
||||
|
||||
own = node.obj[dimension]
|
||||
left = findMin(node.left, dim)
|
||||
right = findMin(node.right, dim)
|
||||
min = node
|
||||
|
||||
if left ~= nil and left.obj[dimension] < own then
|
||||
min = left
|
||||
end
|
||||
|
||||
if right ~= nil and right.obj[dimension] < min.obj[dimension] then
|
||||
min = right
|
||||
end
|
||||
|
||||
return min
|
||||
end
|
||||
|
||||
if node.left == nil and node.right == nil then
|
||||
if node.parent == nil then
|
||||
self.root = nil
|
||||
return
|
||||
end
|
||||
|
||||
pDimension = self.dimensions[node.parent.dimension]
|
||||
|
||||
if node.obj[pDimension] < node.parent.obj[pDimension] then
|
||||
node.parent.left = nil
|
||||
else
|
||||
node.parent.right = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- If the right subtree is not empty, swap with the minimum element on the
|
||||
-- node's dimension. If it is empty, we swap the left and right subtrees and
|
||||
-- do the same.
|
||||
if node.right ~= nil then
|
||||
nextNode = findMin(node.right, node.dimension)
|
||||
nextObj = nextNode.obj
|
||||
removeNode(nextNode)
|
||||
node.obj = nextObj
|
||||
else
|
||||
nextNode = findMin(node.left, node.dimension)
|
||||
nextObj = nextNode.obj
|
||||
removeNode(nextNode)
|
||||
node.right = node.left
|
||||
node.left = nil
|
||||
node.obj = nextObj
|
||||
end
|
||||
end
|
||||
|
||||
node = nodeSearch(self.root)
|
||||
|
||||
if node == nil then
|
||||
return
|
||||
end
|
||||
|
||||
removeNode(node)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
kd_tree.clearRoot = function(self)
|
||||
empty_table(self.root)
|
||||
return self
|
||||
end
|
||||
|
||||
-- Update positions of objects
|
||||
-- Points input must be same structure as existing in k-d Tree
|
||||
|
||||
kd_tree.updatePositions = function(self, points)
|
||||
self.points = points
|
||||
self:clearRoot()
|
||||
self.root = buildTree(points, 0, nil)
|
||||
return self
|
||||
end
|
||||
|
||||
-- get all points sorted by nearest
|
||||
kd_tree.nearestAll = function(self, point)
|
||||
local point = {
|
||||
x = point.x or point[1],
|
||||
y = point.y or point[2],
|
||||
z = point.z or point[3]
|
||||
}
|
||||
|
||||
local function comp_function(a, b)
|
||||
return a[2] < b[2]
|
||||
end
|
||||
|
||||
local res = {}
|
||||
for i = 1, #self.points do
|
||||
local v = self.points[i]
|
||||
res[i] = {
|
||||
[1] = {
|
||||
x = v.x,
|
||||
y = v.y,
|
||||
z = v.z,
|
||||
data = v.data
|
||||
},
|
||||
[2] = math.huge
|
||||
}
|
||||
res[i][2] = self.metric(res[i][1], point)
|
||||
end
|
||||
table_sort(res, comp_function)
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
-- Query the nearest *count* neighbours to a point, with an optional
|
||||
-- maximal search distance.
|
||||
-- Result is an array with *count* elements.
|
||||
-- Each element is an array with two components: the searched point and
|
||||
-- the distance to it.
|
||||
|
||||
kd_tree.nearest = function(self, point, maxNodes, maxDistance)
|
||||
local i
|
||||
local result
|
||||
local bestNodes
|
||||
|
||||
bestNodes = {}
|
||||
local passedNodes = {}
|
||||
|
||||
local maxNodes = maxNodes or 1
|
||||
|
||||
local function comp_function(a, b)
|
||||
return a[2] < b[2]
|
||||
end
|
||||
|
||||
local function saveNode(node, distance)
|
||||
binary_insert(bestNodes, {node, distance}, comp_function)
|
||||
if #bestNodes > maxNodes then
|
||||
table_remove(bestNodes)
|
||||
end
|
||||
end
|
||||
|
||||
local function nearestSearch(node)
|
||||
if passedNodes[node] then return end
|
||||
|
||||
local bestChild
|
||||
local dimension = self.dimensions[node.dimension]
|
||||
local ownDistance = self.metric(point, node.obj)
|
||||
local linearPoint = {}
|
||||
local linearDistance
|
||||
local otherChild
|
||||
local i
|
||||
|
||||
for i = 1, #self.dimensions do
|
||||
linearPoint[self.dimensions[i]] = i == node.dimension and point[self.dimensions[i]] or node.obj[self.dimensions[i]]
|
||||
end
|
||||
|
||||
linearDistance = self.metric(linearPoint, node.obj)
|
||||
|
||||
if node.right == nil and node.left == nil then
|
||||
if #bestNodes < maxNodes or ownDistance < bestNodes[#bestNodes][2] then
|
||||
saveNode(node, ownDistance)
|
||||
end
|
||||
passedNodes[node] = true
|
||||
return
|
||||
end
|
||||
|
||||
if node.right == nil then
|
||||
bestChild = node.left
|
||||
elseif node.left == nil then
|
||||
bestChild = node.right
|
||||
else
|
||||
bestChild = point[dimension] < node.obj[dimension] and node.left or node.right
|
||||
end
|
||||
|
||||
nearestSearch(bestChild)
|
||||
|
||||
if #bestNodes < maxNodes or ownDistance < bestNodes[#bestNodes][2] then
|
||||
saveNode(node, ownDistance)
|
||||
passedNodes[node] = true
|
||||
end
|
||||
|
||||
if #bestNodes < maxNodes or math.abs(linearDistance) < bestNodes[1][2] then
|
||||
otherChild = bestChild == node.left and node.right or node.left
|
||||
if (otherChild ~= nil) then
|
||||
nearestSearch(otherChild)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if maxDistance then
|
||||
for i = 1, maxNodes do
|
||||
bestNodes[i] = {nil, maxDistance}
|
||||
end
|
||||
end
|
||||
|
||||
if self.root then
|
||||
nearestSearch(self.root)
|
||||
end
|
||||
|
||||
result = {}
|
||||
for i = 1, math_min(maxNodes, #bestNodes) do
|
||||
if bestNodes[i][1] then
|
||||
result[#result + 1] = {
|
||||
bestNodes[i][1].obj,
|
||||
bestNodes[i][2]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- Get an approximation of how unbalanced the tree is.
|
||||
-- The higher this number, the worse query performance will be.
|
||||
-- It indicates how many times worse it is than the optimal tree.
|
||||
-- Minimum is 1. Unreliable for small trees.
|
||||
|
||||
kd_tree.balanceFactor = function(self)
|
||||
local function height(node)
|
||||
if node == nil then
|
||||
return 0
|
||||
end
|
||||
return math_max(height(node.left), height(node.right)) + 1
|
||||
end
|
||||
|
||||
local function count(node)
|
||||
if node == nil then
|
||||
return 0
|
||||
end
|
||||
return count(node.left) + count(node.right) + 1
|
||||
end
|
||||
|
||||
return height(self.root) / (math_log(count(self.root)) / math_log(2))
|
||||
end
|
||||
|
||||
return kd_tree
|
||||
end
|
||||
|
||||
local function distance_to(a, b)
|
||||
-- printf("distance_to fired")
|
||||
|
||||
local dist_x = a.x - b.x
|
||||
local dist_y = a.y - b.y
|
||||
local dist_z = a.z - b.z
|
||||
|
||||
return dist_x * dist_x + dist_y * dist_y + dist_z * dist_z
|
||||
end
|
||||
|
||||
-- Actual usage starts here
|
||||
--[[
|
||||
|
||||
When you build position tree, you can find nearest objects in relation to other objects
|
||||
Example, find nearest position to actor:
|
||||
local pos_tree = kd_tree.buildTreeObjectIds({45, 65, 23, 5353, 232})
|
||||
print_table(pos_tree:nearest(db.actor:position()))
|
||||
|
||||
will print position, distance and id of nearest object from given ids
|
||||
|
||||
--]]
|
||||
|
||||
-- Build k-d Tree by several inputs
|
||||
-- Input - Array of vectors (vector():set(x, y, z) or table with x, y, z keys or 1, 2, 3 keys)
|
||||
-- Data is an optional table where you can bind your data to your object, must have same amount of fields as vectors (#vectors == #data)
|
||||
function buildTreeVectors(vectors, data)
|
||||
local v = {}
|
||||
local data = data or {}
|
||||
local vectors = vectors or {}
|
||||
for k, t in pairs(vectors) do
|
||||
table_insert(v, {
|
||||
x = t.x or t[1],
|
||||
y = t.y or t[2],
|
||||
z = t.z or t[3],
|
||||
data = data[k]
|
||||
})
|
||||
end
|
||||
-- printf("vectors num %s", #v)
|
||||
return kdTree(v, distance_to, {"x", "y", "z"})
|
||||
end
|
||||
|
||||
-- Input - Array of game objects
|
||||
-- Vectors are binded to object ids automatically
|
||||
function buildTreeObjects(objects)
|
||||
local vectors = {}
|
||||
local data = {}
|
||||
for k, v in pairs(objects) do
|
||||
table_insert(vectors, v:position())
|
||||
table_insert(data, v:id())
|
||||
end
|
||||
return buildTreeVectors(vectors, data)
|
||||
end
|
||||
|
||||
-- Input - Array of server objects
|
||||
-- Vectors are binded to object ids automatically
|
||||
function buildTreeSeObjects(objects)
|
||||
local vectors = {}
|
||||
local data = {}
|
||||
for k, v in pairs(objects) do
|
||||
table_insert(vectors, v.position)
|
||||
table_insert(data, v.id)
|
||||
end
|
||||
return buildTreeVectors(vectors, data)
|
||||
end
|
||||
|
||||
-- Input - Array of game object ids
|
||||
-- Vectors are binded to object ids automatically
|
||||
function buildTreeObjectIds(ids)
|
||||
local vectors = {}
|
||||
local data = {}
|
||||
local level_object_by_id = level.object_by_id
|
||||
for k, v in pairs(ids) do
|
||||
local obj = level_object_by_id(v)
|
||||
if obj and obj ~= 0 and obj:id() ~= 0 then
|
||||
table_insert(vectors, obj:position())
|
||||
table_insert(data, v)
|
||||
end
|
||||
end
|
||||
return buildTreeVectors(vectors, data)
|
||||
end
|
||||
|
||||
-- Input - Array of server object ids
|
||||
-- Vectors are binded to object ids automatically
|
||||
function buildTreeSeObjectIds(ids)
|
||||
local vectors = {}
|
||||
local data = {}
|
||||
local sim = alife()
|
||||
local sim_object = sim.object
|
||||
for k, v in pairs(ids) do
|
||||
local obj = sim_object(sim, v)
|
||||
if obj and obj ~= 0 and obj.id ~= 0 then
|
||||
table_insert(vectors, obj.position)
|
||||
table_insert(data, v)
|
||||
end
|
||||
end
|
||||
return buildTreeVectors(vectors, data)
|
||||
end
|
||||
|
||||
-- If you build a tree using functions above
|
||||
-- You can use this function to update positions and rebuild the tree
|
||||
function updateObjPositions(kd_tree)
|
||||
local points = kd_tree.points
|
||||
local new_points = {}
|
||||
|
||||
local sim = alife()
|
||||
local sim_object = sim.object
|
||||
for i = 1, #points do
|
||||
local obj = sim_object(sim, points[i].data)
|
||||
if obj then
|
||||
local pos = obj.position
|
||||
table_insert(new_points, {
|
||||
x = pos.x,
|
||||
y = pos.y,
|
||||
z = pos.z,
|
||||
data = points[i].data
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
kd_tree:updatePositions(new_points)
|
||||
return kd_tree
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
-- A tiny library for picking random values between two numbers using bézier curves
|
||||
-- Written in Lua by Singustromo
|
||||
-- Based on and inspired by randomizing_functions from Demonized
|
||||
|
||||
-- Usage
|
||||
--[[
|
||||
local randomizer = libmath_bezier
|
||||
local a = randomizer.get_random_value(min, max, {p0, p1, p2, p3})
|
||||
--]]
|
||||
|
||||
-- Pick a random float between min_cond and max_cond
|
||||
-- random value will be picked according to the function graph
|
||||
function get_random_value(min_cond, max_cond, params)
|
||||
local min_cond = min_cond or 0
|
||||
local max_cond = max_cond or 1
|
||||
|
||||
if not params or type(params) ~= 'table' or #params < 3 then
|
||||
params = {0,0.5,1} -- linear interpolation
|
||||
end
|
||||
|
||||
local rand = 1
|
||||
if #params == 4 then
|
||||
rand = cubic_bezier(math.random(), params)
|
||||
elseif #params == 3 then
|
||||
rand = quadratic_bezier(math.random(), params)
|
||||
end
|
||||
if (not rand) then return end
|
||||
|
||||
if min_cond > max_cond then
|
||||
max_cond, min_cond = min_cond, max_cond
|
||||
end
|
||||
|
||||
local a = max_cond - min_cond
|
||||
local b = a * rand
|
||||
local c = min_cond + b
|
||||
return c
|
||||
end
|
||||
|
||||
function quadratic_bezier(x, p)
|
||||
-- linear interpolation: saving compute time
|
||||
if p[1] == 0 and p[2] == 0.5 and p[3] == 1 then return x end
|
||||
|
||||
return p[1]*(1-x)^2 + 2*p[2]*x*(1-x) + p[3]*x^2
|
||||
end
|
||||
|
||||
function cubic_bezier(x, p)
|
||||
-- linear interpolation: saving compute time
|
||||
if p[1] == 0 and p[2] == 1 and p[3] == 0 and p[4] == 1 then
|
||||
return x
|
||||
end
|
||||
|
||||
return p[1]*(1-x)^3 + 3*p[2]*(1-x)^2*x + 3*p[3]*(1-x)*x^2 + p[4]*x^3
|
||||
end
|
||||
@@ -0,0 +1,111 @@
|
||||
--[[
|
||||
DYNAMIC ZONE
|
||||
|
||||
Original Author(s)
|
||||
Singustromo <singustromo at disroot.org>
|
||||
|
||||
Edited by
|
||||
|
||||
License
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
|
||||
|
||||
Lazy monkey patches that overwrite those functions completely
|
||||
--]]
|
||||
|
||||
-- main script functors and variables are accessed through this variable
|
||||
parent = _G["dynamic_zone"]
|
||||
if not (parent and parent.VERSION and parent.VERSION >= 20240120) then return end
|
||||
|
||||
--------------------------
|
||||
-- Dependencies
|
||||
--------------------------
|
||||
|
||||
local opt = dynamic_zone_mcm
|
||||
local utils = dynamic_zone_utils
|
||||
local debug = dynamic_zone_debug
|
||||
local route_manager = dynamic_zone_routes
|
||||
local discovery = dynamic_zone_discovery
|
||||
|
||||
CONST_LOGGING_PREFIX = "TXR-Routes"
|
||||
local log = debug.log_register("info", CONST_LOGGING_PREFIX)
|
||||
local log_error = debug.log_register("error", CONST_LOGGING_PREFIX)
|
||||
|
||||
-------------------------------
|
||||
-- Patched Functions
|
||||
-------------------------------
|
||||
|
||||
ReloadRouteHints = txr_routes.reload_route_hints
|
||||
OpenRoute = txr_routes.open_route
|
||||
|
||||
-----------------------------
|
||||
-- Monkey patches
|
||||
-----------------------------
|
||||
|
||||
-- Let's make sure we update the transition mapspots and use our hint
|
||||
-- This is executed undermost on_game_load and is used
|
||||
-- to ensure that the spot hints adhere to set localization
|
||||
function txr_routes.reload_route_hints()
|
||||
if (utils.debug_level_loaded()) then return end
|
||||
log("Running patched txr_routes.reload_route_hints()")
|
||||
|
||||
-- This function is actually called before our parent.main_routine()
|
||||
-- We just do this for a new game, so that we can leverage our logic here
|
||||
if (route_manager.route_count() < 1) then
|
||||
log("reload_route_hints | Populating route list")
|
||||
parent.register_game_routes()
|
||||
parent.update_game_route_properties()
|
||||
end
|
||||
|
||||
log("reload_route_hints | Updating transition mapspots")
|
||||
local include_blacklisted = true
|
||||
for route_id, route in route_manager.iterate_routes(include_blacklisted) do
|
||||
for name, _ in route_manager.iterate_transitions(route_id) do
|
||||
discovery.update_transition_mapspot(name, nil,
|
||||
not route.block_discovered)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Prevents txr_routes from overwriting our map icons when a route is unlocked
|
||||
-- (e.g. Debug: Unlock all)
|
||||
-- Overwrote this in it's entirety because the changes needed, require it.
|
||||
function txr_routes.open_route(map_1, map_2, no_msg)
|
||||
local a = txr_routes.get_route(map_1, map_2) -- all space_restrictors between those two
|
||||
if (utils.assert_failed(a, "Got no route between %s and %s", map_1, map_2)) then return end
|
||||
|
||||
local msg
|
||||
for _, transition in pairs(a) do
|
||||
local id, spot, hint = txr_routes.get_route_info(transition)
|
||||
if utils.assert_failed(id and spot and hint) then
|
||||
log_error("Unknown restrictor: %s (Defined in txr_routes.routes)", transition)
|
||||
return -- prevent possible crash
|
||||
end
|
||||
|
||||
local route = route_manager.get_route_by_transition(transition)
|
||||
if utils.assert_failed(route, "Can not get a route ID with '%s' as a member!", transition) then
|
||||
goto continue
|
||||
elseif (route.block_discovered) then
|
||||
goto continue
|
||||
end
|
||||
|
||||
if utils.map_spot_exists(id, spot) then
|
||||
log("Map spot for transition '%s' already exists!", transition)
|
||||
goto continue
|
||||
end
|
||||
|
||||
hint = hint and game.translate_string(hint)
|
||||
level.map_add_object_spot_ser(id, spot, hint)
|
||||
|
||||
mlr_utils.save_var("routes_".. transition, true)
|
||||
txr_routes.register_map(map_1, map_2)
|
||||
msg = true
|
||||
|
||||
::continue::
|
||||
route.unlocked = true
|
||||
discovery.update_transition_mapspot(transition)
|
||||
end
|
||||
|
||||
if msg and (not no_msg) then
|
||||
txr_routes.msg_route(map_1, map_2)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user