--[[ DYNAMIC ZONE Original Author(s) Singustromo 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] = , [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 (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