mirror of
https://github.com/Citadel-Station-13/Citadel-Station-13-RP.git
synced 2026-08-21 07:26:31 +01:00
468 lines
17 KiB
Plaintext
468 lines
17 KiB
Plaintext
//* This file is explicitly licensed under the MIT license. *//
|
|
//* Copyright (c) 2025 Citadel Station Developers *//
|
|
|
|
/**
|
|
* Z-Level Management System
|
|
*
|
|
* All adds/removes should go through this. Directly modifying zlevel amount/whatever is forbidden.
|
|
*/
|
|
|
|
/**
|
|
* Rebuilds multiz lookup.
|
|
*
|
|
* * Please specify a specific index/dir, doing this without that is extremely expensive.
|
|
* * This proc reserves the right to eagerly rebuild something you didn't specify, but will never
|
|
* skip rebuilding something you did specify. As an example, telling it rebuild z-1's NORTH
|
|
* might result in all of z-1's lookups being updated, but NORTH will always be updated no matter
|
|
* what.
|
|
* * This proc does not sleep, as it's used in some critical blocking contexts. That's why
|
|
* it's important to not rebuild everything unless it's actually needed, as that's very slow.
|
|
*
|
|
* @params
|
|
* * for_index - for level index
|
|
* * for_dir - for dir bits; this is ignored if for_index is null.
|
|
* * for_reciprocal - do the same for the other level towards the target index; this is ignored if for_index is null
|
|
*/
|
|
/datum/controller/subsystem/mapping/proc/rebuild_multiz_lookup(for_index, for_dir, for_reciprocal)
|
|
z_stack_dirty = TRUE
|
|
|
|
if(!for_index)
|
|
cached_level_up = new /list(world.maxz)
|
|
cached_level_down = new /list(world.maxz)
|
|
cached_level_east = new /list(world.maxz)
|
|
cached_level_west = new /list(world.maxz)
|
|
cached_level_north = new /list(world.maxz)
|
|
cached_level_south = new /list(world.maxz)
|
|
|
|
if(for_index)
|
|
var/datum/map_level/level = ordered_levels[for_index]
|
|
var/level_partner_index
|
|
level_partner_index = level.get_level_in_dir(UP)?.z_index
|
|
cached_level_up[for_index] = level_partner_index
|
|
if(level_partner_index && for_reciprocal)
|
|
rebuild_multiz_lookup(level_partner_index, DOWN, FALSE)
|
|
level_partner_index = level.get_level_in_dir(DOWN)?.z_index
|
|
cached_level_down[for_index] = level_partner_index
|
|
if(level_partner_index && for_reciprocal)
|
|
rebuild_multiz_lookup(level_partner_index, UP, FALSE)
|
|
level_partner_index = level.get_level_in_dir(NORTH)?.z_index
|
|
cached_level_north[for_index] = level_partner_index
|
|
if(level_partner_index && for_reciprocal)
|
|
rebuild_multiz_lookup(level_partner_index, SOUTH, FALSE)
|
|
level_partner_index = level.get_level_in_dir(SOUTH)?.z_index
|
|
cached_level_south[for_index] = level_partner_index
|
|
if(level_partner_index && for_reciprocal)
|
|
rebuild_multiz_lookup(level_partner_index, NORTH, FALSE)
|
|
level_partner_index = level.get_level_in_dir(EAST)?.z_index
|
|
cached_level_east[for_index] = level_partner_index
|
|
if(level_partner_index && for_reciprocal)
|
|
rebuild_multiz_lookup(level_partner_index, EAST, FALSE)
|
|
level_partner_index = level.get_level_in_dir(WEST)?.z_index
|
|
cached_level_west[for_index] = level_partner_index
|
|
if(level_partner_index && for_reciprocal)
|
|
rebuild_multiz_lookup(level_partner_index, WEST, FALSE)
|
|
else
|
|
for(var/i in 1 to world.maxz)
|
|
var/datum/map_level/level = ordered_levels[i]
|
|
cached_level_up[i] = level.get_level_in_dir(UP)?.z_index
|
|
cached_level_down[i] = level.get_level_in_dir(DOWN)?.z_index
|
|
cached_level_north[i] = level.get_level_in_dir(NORTH)?.z_index
|
|
cached_level_south[i] = level.get_level_in_dir(SOUTH)?.z_index
|
|
cached_level_east[i] = level.get_level_in_dir(EAST)?.z_index
|
|
cached_level_west[i] = level.get_level_in_dir(WEST)?.z_index
|
|
|
|
/**
|
|
* Performs full multiz (including turf / transition) rebuild of an active level.
|
|
*
|
|
* * Implies [rebuild_multiz_lookup]
|
|
* * Please specify a specific index/dir, doing this without that is extremely expensive.
|
|
* * This proc reserves the right to eagerly rebuild something you didn't specify, but will never
|
|
* skip rebuilding something you did specify. As an example, telling it rebuild z-1's NORTH
|
|
* might result in all of z-1's lookups being updated, but NORTH will always be updated no matter
|
|
* what.
|
|
*
|
|
* @params
|
|
* * for_index - for level index
|
|
* * for_dir - for dir bits; this is ignored if for_index is null.
|
|
* * for_reciprocal - do the same for the other level towards the target index; this is ignored if for_index is null
|
|
*/
|
|
/datum/controller/subsystem/mapping/proc/rebuild_multiz(for_index, for_dir, for_reciprocal)
|
|
if(for_index)
|
|
rebuild_multiz_lookup(for_index, for_dir, for_reciprocal)
|
|
var/datum/map_level/level = ordered_levels[for_index]
|
|
// TODO: how do we only rebuild in a specific direction?
|
|
level.rebuild_multiz(for_reciprocal)
|
|
else
|
|
rebuild_multiz_lookup()
|
|
spawn(0)
|
|
for(var/i in 1 to world.maxz)
|
|
var/datum/map_level/level = ordered_levels[for_index]
|
|
level.rebuild_multiz()
|
|
|
|
//* Allocations & Deallocations * //
|
|
|
|
/**
|
|
* allocates a raw map level using the given datum type.
|
|
*
|
|
* * This does not perform **any** generation or processing on the level, including replacing baseturfs!
|
|
* * This **will** call the level's on_loaded_immediate and on_loaded_finalize.
|
|
* * You usually want load_level() instead, this is instead the equivalent of just incrementing world.maxz.
|
|
* * Any level passed in or returned from this belongs to SSmapping; you are not allowed to keep direct references to it
|
|
* in other datastructures.
|
|
* * This is a very low level proc, please be careful what you do with it.
|
|
*
|
|
* @params
|
|
* * level - a path, or instance.
|
|
*
|
|
* @#return the instance of /datum/map_level created / used, null on failure
|
|
*/
|
|
/datum/controller/subsystem/mapping/proc/allocate_level(datum/map_level/level) as /datum/map_level
|
|
RETURN_TYPE(/datum/map_level)
|
|
UNTIL(!map_system_mutex)
|
|
map_system_mutex = TRUE
|
|
. = allocate_level_impl(ispath(level) ? new level : level)
|
|
map_system_mutex = FALSE
|
|
|
|
/**
|
|
* Special internal helper. Used to allocate a level's z-index and set system variables.
|
|
*/
|
|
/datum/controller/subsystem/mapping/proc/allocate_level_impl(datum/map_level/level) as /datum/map_level
|
|
PRIVATE_PROC(TRUE)
|
|
RETURN_TYPE(/datum/map_level)
|
|
|
|
// run safety checks
|
|
ASSERT(!level.loaded)
|
|
|
|
// register level, generating an ID if it has none
|
|
if(level.id)
|
|
if(!isnull(keyed_levels[level.id]))
|
|
CRASH("ID collision on '[level.id]' during level allocation.")
|
|
else
|
|
do
|
|
// TODO: persistence-stable ID
|
|
level.id = "gen-[copytext(md5("[rand(1, 1024 ** 2)]"), 1, 5)]"
|
|
while(keyed_levels[level.id])
|
|
keyed_levels[level.id] = level
|
|
|
|
// allocate a zlevel
|
|
var/allocated_z_level = allocate_z_index()
|
|
ASSERT(allocated_z_level)
|
|
ASSERT(isnull(ordered_levels[allocated_z_level]))
|
|
|
|
// assign z-level and set it as loaded
|
|
level.z_index = allocated_z_level
|
|
level.loaded = TRUE
|
|
ordered_levels[allocated_z_level] = level
|
|
. = level
|
|
|
|
// generate names if they're not there
|
|
if(isnull(level.display_id))
|
|
level.display_id = generate_fluff_level_id()
|
|
if(isnull(level.display_name))
|
|
level.display_name = "Sector [level.display_id]"
|
|
|
|
//! LEGACY
|
|
if(!isnull(level.planet_path))
|
|
SSplanets.legacy_planet_assert(allocated_z_level, level.planet_path)
|
|
if(loaded_station)
|
|
if((level.flags & LEGACY_LEVEL_STATION) || level.has_trait(ZTRAIT_STATION))
|
|
loaded_station.station_levels += allocated_z_level
|
|
if((level.flags & LEGACY_LEVEL_ADMIN) || level.has_trait(ZTRAIT_ADMIN))
|
|
loaded_station.admin_levels += allocated_z_level
|
|
if((level.flags & LEGACY_LEVEL_CONTACT) || level.has_trait(ZTRAIT_STATION))
|
|
loaded_station.contact_levels += allocated_z_level
|
|
if((level.flags & LEGACY_LEVEL_CONSOLES) || level.has_trait(ZTRAIT_STATION))
|
|
loaded_station.map_levels += allocated_z_level
|
|
// Holomaps
|
|
// Auto-center the map if needed (Guess based on maxx/maxy)
|
|
if (level.holomap_offset_x < 0)
|
|
level.holomap_offset_x = ((HOLOMAP_ICON_SIZE - world.maxx) / 2)
|
|
if (level.holomap_offset_x < 0)
|
|
level.holomap_offset_y = ((HOLOMAP_ICON_SIZE - world.maxy) / 2)
|
|
// Assign them to the map lists
|
|
LIST_NUMERIC_SET(loaded_station.holomap_offset_x, allocated_z_level, level.holomap_offset_x)
|
|
LIST_NUMERIC_SET(loaded_station.holomap_offset_y, allocated_z_level, level.holomap_offset_y)
|
|
LIST_NUMERIC_SET(loaded_station.holomap_legend_x, allocated_z_level, level.holomap_legend_x)
|
|
LIST_NUMERIC_SET(loaded_station.holomap_legend_y, allocated_z_level, level.holomap_legend_y)
|
|
//! END
|
|
|
|
/**
|
|
* loads a map level.
|
|
*
|
|
* if it doesn't have a file, we'll change all the turfs to the given baseturf and set atmos/whatever.
|
|
*
|
|
* @params
|
|
* * instance - level to load
|
|
* * use_area_cache - use the given list as the area cache for the DMM loader. if none is provided, one will be created.
|
|
* * use_dmm_context - use the given dmm_context for the loader. if none is provided, one will be created.
|
|
* * defer_for_group_load - defer things like generation callbacks. should generally only be internally used by SSmapping; the API can change at any time.
|
|
* * out_generation_callbacks - generation callbacks emitted by on_load_immediate are put in here instead of executed, if we are deferring due to group load
|
|
*
|
|
* @return loaded context, or null on fail
|
|
*/
|
|
/datum/controller/subsystem/mapping/proc/load_level(
|
|
datum/map_level/instance,
|
|
list/use_area_cache,
|
|
datum/dmm_context/use_dmm_context,
|
|
defer_for_group_load,
|
|
list/datum/callback/out_generation_callbacks,
|
|
)
|
|
UNTIL(!map_system_mutex)
|
|
map_system_mutex = TRUE
|
|
. = load_level_impl(
|
|
instance,
|
|
use_area_cache,
|
|
use_dmm_context,
|
|
defer_for_group_load,
|
|
out_generation_callbacks,
|
|
)
|
|
map_system_mutex = FALSE
|
|
|
|
/datum/controller/subsystem/mapping/proc/load_level_impl(
|
|
datum/map_level/instance,
|
|
list/use_area_cache,
|
|
datum/dmm_context/use_dmm_context,
|
|
defer_for_group_load,
|
|
list/datum/callback/out_generation_callbacks,
|
|
)
|
|
PRIVATE_PROC(TRUE)
|
|
|
|
var/datum/map_level/allocated_instance = allocate_level_impl(instance, FALSE)
|
|
ASSERT(instance.z_index)
|
|
ASSERT(instance.id)
|
|
ASSERT(instance == allocated_instance)
|
|
|
|
if(isnull(use_dmm_context))
|
|
use_dmm_context = create_dmm_context()
|
|
if(isnull(use_dmm_context.mangling_id))
|
|
use_dmm_context.mangling_id = "level-[instance.mangling_id || instance.id]"
|
|
|
|
var/datum/dmm_context/loaded_context
|
|
|
|
if(instance.base_area)
|
|
var/area/level_area_type = instance.base_area
|
|
var/area/level_area_instance
|
|
if(level_area_type.unique)
|
|
level_area_instance = GLOB.areas_by_type[level_area_type]
|
|
if(!level_area_instance)
|
|
level_area_instance = new(null)
|
|
level_area_instance.take_turfs(Z_TURFS(instance.z_index))
|
|
CHECK_TICK
|
|
if(instance.base_turf)
|
|
var/list/turf/to_change = Z_TURFS(instance.z_index)
|
|
for(var/turf/T as anything in to_change)
|
|
T.ChangeTurf(instance.base_turf, list(/turf/baseturf_bottom))
|
|
CHECK_TICK
|
|
|
|
if(instance.has_map_path())
|
|
var/datum/dmm_parsed/parsed = instance.parse_map_path()
|
|
if(!parsed)
|
|
CRASH("level [instance] ([instance.type]) couldn't parse its map because its map path was missing or an unknown error occurred.")
|
|
if(!parsed.parsed)
|
|
CRASH("failed to parse a level being loaded. is the map malformed?")
|
|
|
|
var/real_x = instance.load_center ? 1 + round((world.maxx - parsed.width) / 2) : 1
|
|
var/real_y = instance.load_center ? 1 + round((world.maxy - parsed.height) / 2) : 1
|
|
var/real_z = instance.z_index
|
|
|
|
if(!instance.load_crop && ((parsed.width + real_x - 1) > world.maxx || (parsed.height + real_y - 1) > world.maxy))
|
|
CRASH("tried to load a map that would overrun the world bounds")
|
|
|
|
loaded_context = parsed.load(
|
|
real_x,
|
|
real_y,
|
|
real_z,
|
|
no_changeturf = TRUE,
|
|
place_on_top = FALSE,
|
|
orientation = instance.load_orientation,
|
|
area_cache = use_area_cache,
|
|
context = use_dmm_context,
|
|
)
|
|
|
|
ASSERT(loaded_context.success)
|
|
else
|
|
// this is not a real map path
|
|
loaded_context = use_dmm_context
|
|
loaded_context.mark_used()
|
|
loaded_context.set_empty_load()
|
|
loaded_context.success = TRUE
|
|
|
|
// immediately update munltiz cache
|
|
rebuild_multiz_lookup(instance.z_index)
|
|
|
|
// fire context immediately
|
|
loaded_context.execute_postload()
|
|
|
|
// fire instance on load hooks
|
|
var/list/datum/callback/generation_callbacks = list()
|
|
instance.on_loaded_immediate(instance.z_index, generation_callbacks)
|
|
|
|
// fire finalize and generation callbacks if not deferred
|
|
if(!defer_for_group_load)
|
|
for(var/datum/callback/generation_callback as anything in generation_callbacks)
|
|
generation_callback.Invoke()
|
|
if(!SSatoms.initialized)
|
|
SSatoms.init_map_bounds(loaded_context.loaded_bounds)
|
|
instance.on_loaded_finalize(instance.z_index)
|
|
instance.rebuild_multiz()
|
|
else
|
|
out_generation_callbacks += generation_callbacks
|
|
|
|
return loaded_context
|
|
|
|
/**
|
|
* destroys a loaded level and frees it for later usage
|
|
*
|
|
* @return TRUE / FALSE based on success / fail
|
|
*/
|
|
/datum/controller/subsystem/mapping/proc/unload_level(datum/map_level/instance)
|
|
if(instance.parent_map)
|
|
CRASH("attempted to unload_level on a level with a parent map. the map must be unloaded, not the level individually.")
|
|
CRASH("unimplemented")
|
|
|
|
/**
|
|
* immediately de-allocates a loaded level and frees its z-index.
|
|
*
|
|
* **Do not use this directly unless you absolutely know what you are doing.**
|
|
* This does not perform any cleanup, and calling this on a loaded zlevel can have
|
|
* severe consequences.
|
|
*
|
|
* @return TRUE / FALSE based on success / fail
|
|
*/
|
|
/datum/controller/subsystem/mapping/proc/unallocate_level(datum/map_level/instance)
|
|
if(instance.parent_map)
|
|
CRASH("attempted to unallocate_level on a level with a parent map. the map must be unloaded, not the level individually.")
|
|
CRASH("unimplemented")
|
|
|
|
//* Traits, Attributes, and IDs *//
|
|
|
|
/**
|
|
* called when a trait is added to a loaded level
|
|
*
|
|
* if a level is loading with traits included, this is called per trait after load.
|
|
*/
|
|
/datum/controller/subsystem/mapping/proc/on_level_trait_add(datum/map_level/level, trait)
|
|
return
|
|
|
|
/**
|
|
* called when a trait is removed from a loaded level
|
|
*
|
|
* if a level is being deleted with traits on it, this is called per trait prior to delete.
|
|
*/
|
|
/datum/controller/subsystem/mapping/proc/on_level_trait_del(datum/map_level/level, trait)
|
|
return
|
|
|
|
/**
|
|
* called when an attribute is set ton a level
|
|
*
|
|
* if a level is loading with attribute included, this is called per attribute after load with an old_value of null.
|
|
*/
|
|
/datum/controller/subsystem/mapping/proc/on_level_attribute_set(datum/map_level/level, attribute, old_value, new_value)
|
|
return
|
|
|
|
/**
|
|
* generates random hex fluff level id
|
|
*/
|
|
/datum/controller/subsystem/mapping/proc/generate_fluff_level_id()
|
|
// todo: needs to be persistence-stable..?
|
|
// todo: this looks ugly, fix it!!!
|
|
var/discriminator = GLOB.round_id? "[num2hex(text2num(GLOB.round_id), 6)]-" : ""
|
|
var/safety = 500
|
|
do
|
|
if(!--safety)
|
|
CRASH("ran out of safety somehow, wtf")
|
|
. = "[discriminator][num2hex(rand(1, 16 ** 4 - 1))]"
|
|
while(. in random_fluff_level_hashes)
|
|
random_fluff_level_hashes += .
|
|
|
|
//* Z-stacks *//
|
|
|
|
/**
|
|
* Recalculates Z stack
|
|
*
|
|
* **Warning**: rebuild_verticality must be called to recalculate up/down lookups,
|
|
* as this proc uses them for speed!
|
|
*/
|
|
/datum/controller/subsystem/mapping/proc/recalculate_z_stack()
|
|
validate_no_loops()
|
|
z_stack_lookup = new /list(world.maxz)
|
|
// let's sing the bottom song
|
|
var/list/datum/map_level/bottoms = list()
|
|
for(var/z in 1 to world.maxz)
|
|
if(cached_level_down[z])
|
|
continue
|
|
bottoms += ordered_levels[z]
|
|
for(var/datum/map_level/bottom as anything in bottoms)
|
|
var/datum/map_level/iterating = bottom
|
|
var/datum/map_level/last = null
|
|
var/list/stack = list()
|
|
var/list/levels_in_stack = list()
|
|
var/discontinuity_found = FALSE
|
|
do
|
|
if(last && last != iterating.get_level_in_dir(DOWN))
|
|
discontinuity_found = TRUE
|
|
stack += iterating.z_index
|
|
levels_in_stack += iterating.z_index
|
|
last = iterating
|
|
iterating = ordered_levels[cached_level_up[iterating.z_index]]
|
|
while(iterating)
|
|
if(discontinuity_found)
|
|
stack_trace("z-levels [english_list(levels_in_stack)] had at least one discontinuity where going down isn't the same as going up. \
|
|
non-euclidean verticality is not supported.")
|
|
stack = list()
|
|
for(var/z in levels_in_stack)
|
|
z_stack_lookup[z] = stack
|
|
for(var/i in 1 to world.maxz)
|
|
if(length(z_stack_lookup[i]) >= 1)
|
|
else
|
|
z_stack_lookup[i] = list()
|
|
stack_trace("z-level [i] ([ordered_levels[i]?.name]) had no z-stack. did someone mess up their up/down configs?")
|
|
z_stack_dirty = FALSE
|
|
|
|
/**
|
|
* Ensures there's no up/down infinite loops
|
|
*/
|
|
/datum/controller/subsystem/mapping/proc/validate_no_loops()
|
|
var/list/loops = list()
|
|
for(var/z in 1 to world.maxz)
|
|
// already in a loop don't bother
|
|
if(z in loops)
|
|
continue
|
|
var/list/found
|
|
var/next
|
|
// scan up
|
|
next = z
|
|
found = list(z)
|
|
while(next)
|
|
next = cached_level_up[next]
|
|
if(next in found)
|
|
loops += found
|
|
break
|
|
if(next)
|
|
found += next
|
|
// scan down
|
|
next = z
|
|
found = list(z)
|
|
while(next)
|
|
next = cached_level_down[next]
|
|
if(next in found)
|
|
loops += found
|
|
break
|
|
if(next)
|
|
found += next
|
|
if(!loops.len)
|
|
return
|
|
for(var/z in loops)
|
|
var/datum/map_level/level = ordered_levels[z]
|
|
level.link_above_id = null
|
|
level.link_below_id = null
|
|
stack_trace("WARNING: Up/Down loops found in zlevels [english_list(loops)]. This is not allowed and will cause both falling and zcopy to infinitely loop. All zlevels involved have been disconnected, and any structs involved have been destroyed.")
|
|
rebuild_multiz_lookup()
|
|
for(var/z in loops)
|
|
var/datum/map_level/level = ordered_levels[z]
|
|
if(level.link_above_id || level.link_below_id)
|
|
stack_trace("WARNING: level [z] ([level.name]) wasn't unlinked after having a loop detected.")
|
|
continue
|
|
spawn(0)
|
|
level.rebuild_multiz_vertical()
|