mirror of
https://github.com/Citadel-Station-13/Citadel-Station-13-RP.git
synced 2026-07-22 22:13:30 +01:00
4ab234f10c
tl;dr level collections that can easily be logically restructured/moved as necessary will eventually be used for radio and overmaps location resolution. this is the last part of https://github.com/Citadel-Station-13/Citadel-Station-13-RP/pull/4841 that has not yet been integrated. this will be a draft for a while. --------- Co-authored-by: LetterN <24603524+LetterN@users.noreply.github.com>
461 lines
17 KiB
Plaintext
461 lines
17 KiB
Plaintext
//* This file is explicitly licensed under the MIT license. *//
|
|
//* Copyright (c) 2025 Citadel Station Developers *//
|
|
|
|
/**
|
|
* # /datum/map
|
|
*
|
|
* * clusters of zlevels, basically.
|
|
* * when maps are loaded, areas are cached together to preserve byond-like behavior.
|
|
* * A map can make a /datum/map_struct. This is used to initialize its overmaps,
|
|
* if an overmap initializer is provided.
|
|
* * While multiple map structs can be made per map with some modifications, this
|
|
* functionality is not included as there is little reason to support such
|
|
* behavior. Maps should use the dependencies system to chain-load other maps,
|
|
* not have multiple planets in one map datum.
|
|
* * Loading more than one instance of a map is not supported at this time,
|
|
* and will likely never be supported. An upcoming system will be added to handle
|
|
* instanced planets and similar things.
|
|
*
|
|
* ## Loading
|
|
*
|
|
* Generally, the load process is like so;
|
|
* * call ready() to initialize anything lazy-loaded
|
|
* * call validate() to check for soundness of the map
|
|
* * call construct() to set all variables as needed. this will recheck all current levels
|
|
* and set needed struct vars on ourselves and them, computing things like sparse
|
|
* size as needed. if the levels already exist, they'll be updated.
|
|
*/
|
|
/datum/map
|
|
abstract_type = /datum/map
|
|
|
|
//* Core *//
|
|
/// id - must be unique
|
|
var/id
|
|
/// mangling id override
|
|
var/mangling_id
|
|
/// override map id for persistence so two maps are considered the same
|
|
/// two maps should **never** be loaded at the same time with the same persistence ID!
|
|
var/persistence_id
|
|
/// Is this registered in SSmapping? Once registered, our reference belongs
|
|
/// to the mapping system, and we can no longer be deleted.
|
|
var/tmp/registered = FALSE
|
|
/// are we modified from our prototype?
|
|
var/tmp/modified = FALSE
|
|
|
|
//* Chainload *//
|
|
/// dependencies by id or path of other maps - these are critical maps to always load in
|
|
/// * resolved during ready()
|
|
var/list/dependencies
|
|
/// lateload by id or path of other maps - these are non-critical maps to always load in
|
|
/// * resolved during ready()
|
|
var/list/lateload
|
|
|
|
//* Config *//
|
|
/// Allow gateway mission to load?
|
|
var/conf_load_gateway_mission = TRUE
|
|
|
|
//* Identity *//
|
|
/// in-code name
|
|
var/name = "Unknown Map"
|
|
/// in-code category
|
|
var/category = "Misc"
|
|
|
|
//* Levels *//
|
|
/// /datum/map_level datums. starts off as paths, inits later.
|
|
/// * Automatic ordering will be enforced unless specifically overridden, but we still prefer you order it properly
|
|
/// for style / review reasons.
|
|
var/list/datum/map_level/levels
|
|
/// force mangling ids of levels to be the same
|
|
/// you usually want this to be on!
|
|
var/levels_match_mangling_id = TRUE
|
|
|
|
//* Properties *//
|
|
/// declared width = must match all levels
|
|
var/width
|
|
/// declared height - must match all levels
|
|
var/height
|
|
|
|
//* Overmaps *//
|
|
/// our overmap initializer
|
|
///
|
|
/// * if specified, our overmap location will be a /datum/overmap_location/map
|
|
/// * will not be re-fired if overmaps side is what caused us to be loaded. remember,
|
|
/// /datum/overmap_initializer is a bi-directional binding to and from /datum/map!
|
|
var/datum/overmap_initializer/map/overmap_initializer
|
|
|
|
//* Load Options *//
|
|
/// orientation - defaults to south
|
|
var/load_orientation = SOUTH
|
|
/// crop if too big, instead of panic
|
|
var/load_auto_crop = FALSE
|
|
/// center us if we're smaller than world size
|
|
var/load_auto_center = TRUE
|
|
/// use map-wide area cache instead of individual level area caches; has no effect on submap loading, only level loading.
|
|
/// * don't touch this unless you know what you're doing.
|
|
var/load_shared_area_cache = TRUE
|
|
|
|
//* Simulation *//
|
|
/// Ceiling heights for levels that don't specify it, as well as the blank space between levels
|
|
var/ceiling_height_default = 5
|
|
|
|
//* World State *//
|
|
/// Are we loaded in?
|
|
var/tmp/loaded = FALSE
|
|
/// Quick access - loaded indices
|
|
/// * This should be in the same order as [levels].
|
|
var/tmp/list/loaded_z_indices
|
|
/// list of stringified z coordinates to the level datum
|
|
///
|
|
/// * "x,y,z" is the format
|
|
var/tmp/list/loaded_z_grid
|
|
/// total width of all levels
|
|
///
|
|
/// * if a level is at 0,0,0 and another is at 5,0,0 with nothing in between, our width is 5
|
|
var/tmp/loaded_sparse_size_x
|
|
/// total height of all levels
|
|
///
|
|
/// * if a level is at 0,0,0 and another is at 0,5,0 with nothing in between, our height is 5
|
|
var/tmp/loaded_sparse_size_y
|
|
/// total depth of all levels
|
|
///
|
|
/// * if a level is at 0,0,0 and another is at 0,0,5 with nothing in between, our depth is 5
|
|
var/tmp/loaded_sparse_size_z
|
|
/// z-stacks, addressed by string key "[x],[y]"
|
|
var/tmp/list/loaded_z_stacks
|
|
/// z-planes, addressed by string key "[z]"
|
|
var/tmp/list/loaded_z_planes
|
|
|
|
//* Bindings - Overmaps *//
|
|
/// the overmap location that is binding to us
|
|
///
|
|
/// * If this exists, it will be deleted if we are somehow deleted. This can result
|
|
/// in some very weird things.
|
|
var/datum/overmap_location/map/overmap_binding
|
|
|
|
//! legacy : spawn these shuttle datums on load
|
|
var/list/legacy_assert_shuttle_datums
|
|
|
|
/datum/map/New()
|
|
// resolve overmap initializer
|
|
if(ispath(overmap_initializer) || IS_ANONYMOUS_TYPEPATH(overmap_initializer))
|
|
overmap_initializer = new overmap_initializer
|
|
|
|
/datum/map/Destroy()
|
|
if(loaded)
|
|
. = QDEL_HINT_LETMELIVE
|
|
CRASH("UH OH, SOMETHING TRIED TO DELETE AN INSTANTIATED MAP.")
|
|
for(var/datum/map_level/level in levels)
|
|
if(level.parent_map != src)
|
|
stack_trace("how?")
|
|
continue
|
|
level.parent_map = null
|
|
levels = null
|
|
return ..()
|
|
|
|
/datum/map/serialize()
|
|
. = ..()
|
|
// TODO: verify all variables are in here
|
|
.["id"] = id
|
|
.["name"] = name
|
|
var/list/serialized_levels = (.["levels"] = list())
|
|
for(var/datum/map_level/level as anything in levels)
|
|
if(!istype(level))
|
|
serialized_levels += level // isn't init'd, probably path or id
|
|
continue
|
|
serialized_levels += json_encode(level.serialize())
|
|
.["dependencies"] = dependencies
|
|
.["lateload"] = lateload
|
|
.["width"] = width
|
|
.["height"] = height
|
|
.["load_auto_crop"] = load_auto_crop
|
|
.["load_auto_center"] = load_auto_center
|
|
.["load_orientation"] = load_orientation
|
|
.["load_shared_area_cache"] = load_shared_area_cache
|
|
|
|
/datum/map/deserialize(list/data)
|
|
if(loaded)
|
|
CRASH("attempted deserialize while loaded")
|
|
. = ..()
|
|
// TODO: verify all variables are in here
|
|
id = data["id"]
|
|
name = data["name"]
|
|
levels = list()
|
|
for(var/serialized_level in data["levels"])
|
|
var/is_it_a_path = text2path(serialized_level)
|
|
// path
|
|
if(ispath(is_it_a_path, /datum/map_level))
|
|
levels += is_it_a_path
|
|
continue
|
|
// json
|
|
if(serialized_level[1] == "{")
|
|
var/datum/map_level/level = new
|
|
level.deserialize(json_decode(serialized_level))
|
|
levels += level
|
|
continue
|
|
dependencies = data["dependencies"]
|
|
lateload = data["lateload"]
|
|
width = data["width"]
|
|
height = data["height"]
|
|
if(!isnull(data["load_auto_crop"]))
|
|
load_auto_crop = data["load_auto_crop"]
|
|
if(!isnull(data["load_auto_center"]))
|
|
load_auto_center = data["load_auto_center"]
|
|
if(!isnull(data["load_orientation"]))
|
|
load_orientation = data["load_orientation"]
|
|
if(!isnull(data["load_shared_area_cache"]))
|
|
load_shared_area_cache = data["load_shared_area_cache"]
|
|
|
|
// todo: implement clone()
|
|
|
|
/**
|
|
* called before we load in
|
|
* * should be called before validate()
|
|
* * instances any levels not instanced
|
|
*
|
|
* @return TRUE / FALSE
|
|
*/
|
|
/datum/map/proc/ready()
|
|
. = TRUE
|
|
for(var/i in 1 to length(levels))
|
|
if(ispath(levels[i]))
|
|
var/datum/map_level/level_path = levels[i]
|
|
var/datum/map_level/level_instance = new level_path(src)
|
|
levels[i] = level_instance
|
|
if(levels_match_mangling_id)
|
|
level_instance.mangling_id = mangling_id || id
|
|
for(var/i in 1 to length(dependencies))
|
|
if(ispath(dependencies[i]))
|
|
var/datum/map/resolving = dependencies[i]
|
|
dependencies[i] = initial(resolving.id)
|
|
for(var/i in 1 to length(lateload))
|
|
if(ispath(lateload[i]))
|
|
var/datum/map/resolving = lateload[i]
|
|
lateload[i] = initial(resolving.id)
|
|
|
|
/**
|
|
* validates that everything works
|
|
*
|
|
* @params
|
|
* * for_load - (optional) validate for loading, not just that it makes semantic sense
|
|
* * out_errors - (optional) human readable errors get added to this list if provided
|
|
*
|
|
* @return TRUE / FALSE
|
|
*/
|
|
/datum/map/proc/validate(for_load, list/out_errors)
|
|
. = TRUE
|
|
var/list/struct_position_strs = list()
|
|
var/list/keyed_levels = list()
|
|
var/list/levels_in_plane_by_xy = list()
|
|
// validate levels individually
|
|
for(var/level_idx in 1 to length(levels))
|
|
var/datum/map_level/level = levels[level_idx]
|
|
if(!istype(level))
|
|
out_errors?.Add("level: index [level_idx] is not a valid map level datum.")
|
|
. = FALSE
|
|
continue
|
|
|
|
if(level.id)
|
|
if(keyed_levels[level.id])
|
|
out_errors?.Add("level: index [level_idx] collides with index [levels.Find(keyed_levels[level.id])] on level id '[level.id]'")
|
|
. = FALSE
|
|
else
|
|
keyed_levels[level.id] = level
|
|
|
|
if(level.has_map_path())
|
|
var/datum/dmm_parsed/parsed = level.parse_map_path()
|
|
if(!parsed)
|
|
out_errors?.Add("level: index [level_idx] couldn't find its map file.")
|
|
. = FALSE
|
|
else if(!parsed.parsed)
|
|
out_errors?.Add("level: index [level_idx] parse failed. is the .dmm malformed?")
|
|
. = FALSE
|
|
else
|
|
if(parsed.width > world.maxx || parsed.height > world.maxy)
|
|
out_errors?.Add("level: index [level_idx] dim [parsed.width]x[parsed.height] > [world.maxx]x[world.maxy]")
|
|
. = FALSE
|
|
|
|
var/level_struct_enabled = level.struct_x && level.struct_y && level.struct_z
|
|
if(level_struct_enabled)
|
|
if(!level.id)
|
|
out_errors?.Add("level: index [level_idx] has no id but is in a struct; struct-levels must have IDs.")
|
|
. = FALSE
|
|
var/level_struct_valid = TRUE
|
|
if(!isnum(level.struct_x))
|
|
out_errors?.Add("level: index [level_idx] struct_x not num")
|
|
level_struct_valid = FALSE
|
|
else if(!ISINRANGE(level.struct_x, -(SHORT_REAL_LIMIT * 0.5), (SHORT_REAL_LIMIT * 0.5)) || (round(level.struct_x) != level.struct_x))
|
|
out_errors?.Add("level: index [level_idx] struct_x out of bounds or fractional")
|
|
level_struct_valid = FALSE
|
|
if(!isnum(level.struct_y))
|
|
out_errors?.Add("level: index [level_idx] struct_y not num")
|
|
level_struct_valid = FALSE
|
|
else if(!ISINRANGE(level.struct_y, -(SHORT_REAL_LIMIT * 0.5), (SHORT_REAL_LIMIT * 0.5)) || (round(level.struct_y) != level.struct_y))
|
|
out_errors?.Add("level: index [level_idx] struct_y out of bounds or fractional")
|
|
level_struct_valid = FALSE
|
|
if(!isnum(level.struct_z))
|
|
out_errors?.Add("level: index [level_idx] struct_z not num")
|
|
level_struct_valid = FALSE
|
|
else if(!ISINRANGE(level.struct_z, -(SHORT_REAL_LIMIT * 0.5), (SHORT_REAL_LIMIT * 0.5)) || (round(level.struct_z) != level.struct_z))
|
|
out_errors?.Add("level: index [level_idx] struct_z out of bounds or fractional")
|
|
level_struct_valid = FALSE
|
|
if(level_struct_valid)
|
|
var/level_struct_position_str = "[level.struct_x],[level.struct_y],[level.struct_z]"
|
|
var/level_struct_plane_xy = "[level.struct_x],[level.struct_y]"
|
|
if(struct_position_strs[level_struct_position_str])
|
|
out_errors?.Add("level: index [level_idx] collides with level index [levels.Find(struct_position_strs[level_struct_position_str])] on struct position '[level_struct_position_str]'")
|
|
. = FALSE
|
|
else
|
|
struct_position_strs[level_struct_position_str] = level
|
|
if(!levels_in_plane_by_xy[level_struct_plane_xy])
|
|
levels_in_plane_by_xy[level_struct_plane_xy] = list()
|
|
levels_in_plane_by_xy[level_struct_plane_xy] += level
|
|
|
|
// TODO: invoke level datum validation, including for base turf/area validity (see spawn flags & area not `special`)
|
|
|
|
// can't run overall checks if any levels are individually invalid
|
|
if(!.)
|
|
return
|
|
|
|
for(var/plane_str in levels_in_plane_by_xy)
|
|
var/list/datum/map_level/plane_levels = levels_in_plane_by_xy[plane_str]
|
|
var/found_ceiling_height
|
|
for(var/datum/map_level/plane_level as anything in plane_levels)
|
|
if(isnull(plane_level.ceiling_height))
|
|
continue
|
|
if(plane_level.ceiling_height == 0)
|
|
out_errors?.Add("Plane [plane_str] has a zero ceiling height level.")
|
|
. = FALSE
|
|
break
|
|
if(!isnull(found_ceiling_height) && found_ceiling_height != plane_level.ceiling_height)
|
|
out_errors?.Add("Plane [plane_str] has mismatching ceiling heights.")
|
|
. = FALSE
|
|
break
|
|
else
|
|
found_ceiling_height = plane_level.ceiling_height
|
|
|
|
if(for_load && !validate_load(out_errors, .))
|
|
. = FALSE
|
|
|
|
/**
|
|
* validate that we can currently load
|
|
* * this checks for things like collisions with current loaded maps, levels, world state, etc
|
|
*
|
|
* @params
|
|
* * out_errors - (optional) human readable errors get added to this list if provided
|
|
* * structure_validity - (optional) pass in if structure is valid; this makes it so we don't run
|
|
* future tests that would always fail if the map's contents for whatever reason
|
|
* are considered invalid.
|
|
*
|
|
* @return TRUE / FALSE
|
|
*/
|
|
/datum/map/proc/validate_load(list/out_errors, structure_validity)
|
|
. = TRUE
|
|
for(var/level_idx in 1 to length(levels))
|
|
var/datum/map_level/level = levels[level_idx]
|
|
if(SSmapping.keyed_levels[level.id])
|
|
. = FALSE
|
|
out_errors?.Add("level: index [level_idx] has id of '[level.id]' which is already taken by a currently loaded level.")
|
|
|
|
/**
|
|
* rebuilds state, setting variables as needed on ourselves and our level
|
|
* * should be called after validate(); validate() doesn't require this to work
|
|
* * if loaded maps have their multiz things change, this'll have ssmapping rebuild its cache
|
|
* and also rebuild the transitions
|
|
* * this will trample any custom linkages set on levels if they're not set to LINKAGE_FORCED!
|
|
* * this will **not** trample reciprocal custom linkages. if another level links to a level in us,
|
|
* that level will not be unbound despite our level now being unbound.
|
|
*
|
|
* TODO: mode to only re-assert internal linkages, without disrupting linkages to other levels outside of ours.
|
|
*
|
|
* @params
|
|
* * skip_validation - skip data validation. this should only be done if you already validate()'d before construct().
|
|
* * skip_loaded_rebuild - do not rebuild loaded zlevels immediately.
|
|
*/
|
|
/datum/map/proc/construct(skip_validation, skip_loaded_rebuild)
|
|
var/list/validation_errors = list()
|
|
if(!validate(TRUE, validation_errors))
|
|
CRASH("validation failed at construct() with errors: [english_list(validation_errors)]; this shouldn't be failing this far in the pipeline, please remember to validate() maps yourself!")
|
|
|
|
var/list/datum/map_level/loaded_levels_requiring_immediate_rebuild_to_dirs = list()
|
|
|
|
var/z_grid = list()
|
|
var/z_stacks = list()
|
|
var/z_planes = list()
|
|
|
|
// collect data
|
|
for(var/datum/map_level/level as anything in levels)
|
|
level.struct_active = level.is_in_struct()
|
|
if(!level.struct_active)
|
|
continue
|
|
|
|
z_grid["[level.struct_x],[level.struct_y],[level.struct_z]"] = level
|
|
|
|
var/x_y_str = "[level.struct_x],[level.struct_y]"
|
|
if(!z_stacks[x_y_str])
|
|
z_stacks[x_y_str] = list()
|
|
z_stacks[x_y_str] += level
|
|
|
|
var/z_str = "[level.struct_z]"
|
|
if(!z_planes[z_str])
|
|
z_planes[z_str] = list()
|
|
z_planes[z_str] += level
|
|
|
|
// sweep levels
|
|
for(var/datum/map_level/level as anything in levels)
|
|
level.load_orientation = load_orientation
|
|
level.load_center = load_auto_center
|
|
level.load_crop = load_auto_crop
|
|
|
|
if(level.struct_active)
|
|
var/level_multiz_changed_dirs = NONE
|
|
switch(level.linkage)
|
|
if(Z_LINKAGE_FORCED)
|
|
if(Z_LINKAGE_NORMAL)
|
|
var/datum/map_level/level_partner
|
|
level_partner = z_grid["[level.struct_x+1],[level.struct_y],[level.struct_z]"]
|
|
if(level.link_east_id != level_partner?.id)
|
|
level.link_east_id = level_partner?.id
|
|
level_multiz_changed_dirs |= EAST
|
|
level_partner = z_grid["[level.struct_x-1],[level.struct_y],[level.struct_z]"]
|
|
if(level.link_west_id != level_partner?.id)
|
|
level.link_west_id = level_partner?.id
|
|
level_multiz_changed_dirs |= WEST
|
|
level_partner = z_grid["[level.struct_x],[level.struct_y+1],[level.struct_z]"]
|
|
if(level.link_north_id != level_partner?.id)
|
|
level.link_north_id = level_partner?.id
|
|
level_multiz_changed_dirs |= NORTH
|
|
level_partner = z_grid["[level.struct_x],[level.struct_y-1],[level.struct_z]"]
|
|
if(level.link_south_id != level_partner?.id)
|
|
level.link_south_id = level_partner?.id
|
|
level_multiz_changed_dirs |= SOUTH
|
|
level_partner = z_grid["[level.struct_x],[level.struct_y],[level.struct_z+1]"]
|
|
if(level.link_above_id != level_partner?.id)
|
|
level.link_above_id = level_partner?.id
|
|
level_multiz_changed_dirs |= UP
|
|
level_partner = z_grid["[level.struct_x],[level.struct_y],[level.struct_z-1]"]
|
|
if(level.link_below_id != level_partner?.id)
|
|
level.link_below_id = level_partner?.id
|
|
level_multiz_changed_dirs |= DOWN
|
|
|
|
if(level.loaded && level_multiz_changed_dirs)
|
|
loaded_levels_requiring_immediate_rebuild_to_dirs[level] = level_multiz_changed_dirs
|
|
|
|
for(var/datum/map_level/rebuilding_level as anything in loaded_levels_requiring_immediate_rebuild_to_dirs)
|
|
var/rebuild_dirs = loaded_levels_requiring_immediate_rebuild_to_dirs[rebuilding_level]
|
|
SSmapping.rebuild_multiz_lookup(rebuilding_level.z_index, rebuild_dirs)
|
|
if(!skip_loaded_rebuild && rebuilding_level.loaded)
|
|
if(rebuild_dirs & (NORTH|SOUTH|EAST|WEST))
|
|
rebuilding_level.rebuild_multiz_horizontal()
|
|
else
|
|
rebuilding_level.rebuild_multiz_vertical()
|
|
|
|
return TRUE
|
|
|
|
/**
|
|
* Get levels sorted into z-loading order
|
|
*/
|
|
/datum/map/proc/get_sorted_levels()
|
|
// this only works because tim_sort is stable;
|
|
// cmp_map_level_load_sequence doesn't touch order if structs aren't active.
|
|
return tim_sort(levels.Copy(), /proc/cmp_map_level_load_sequence)
|