483 lines
15 KiB
Plaintext
483 lines
15 KiB
Plaintext
|
--[[
|
||
|
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
|