Dynamic Turf Reservations Port (#22798)

Ports https://github.com/tgstation/tgstation/pull/38098 and
https://github.com/tgstation/tgstation/pull/77786

Prereq for shuttle rotation. No player-facing changes.
This commit is contained in:
Batrachophreno
2026-07-12 20:45:26 +00:00
committed by GitHub
parent e117ef3f87
commit acd8c6d9d5
9 changed files with 526 additions and 29 deletions
+110 -9
View File
@@ -19,10 +19,14 @@
var/loaded = 0 // Times loaded this round
var/static/dmm_suite/maploader = new
var/datum/parsed_map/cached_map
var/keep_cached_map = FALSE
var/list/shuttles_to_initialise = list()
var/base_turf_for_zs = null
var/accessibility_weight = 0
var/template_flags = TEMPLATE_FLAG_ALLOW_DUPLICATES
/// Last admin-facing map loader failure reason.
var/last_load_error = null
///A list of groups, as strings, that this template belongs to. When adding new map templates, try to keep this balanced on the CI execution time, or consider adding a new one
///ONLY IF IT'S THE LONGEST RUNNING CI POD AND THEY ARE ALREADY BALANCED
@@ -38,26 +42,107 @@
if(rename)
name = rename
/datum/map_template/proc/preload_size(path)
var/list/bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF)
/datum/map_template/proc/preload_size(path = null, cache = FALSE)
last_load_error = null
if(path)
mappath = path
if(!mappath)
last_load_error = "no map source assigned"
return FALSE
var/datum/map_load_metadata/M = maploader.load_map_impl(file(mappath), 1, 1, cropMap=FALSE, measureOnly=TRUE, no_changeturf=TRUE)
var/list/bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF)
var/map_source = get_map_source()
if(!map_source)
last_load_error = "unable to normalize map source ([describe_map_source()])"
return FALSE
var/datum/map_load_metadata/M = maploader.load_map_impl(map_source, 1, 1, 1, cropMap = FALSE, measureOnly = TRUE, no_changeturf = TRUE, cache_metadata = cache)
if(M)
bounds = extend_bounds_if_needed(bounds, M.bounds)
if(cache)
cached_map = M.parsed_map
else
last_load_error = "DMM parser returned no measured bounds for [describe_map_source()]"
return FALSE
width = bounds[MAP_MAXX] - bounds[MAP_MINX] + 1
height = bounds[MAP_MAXY] - bounds[MAP_MINX] + 1
height = bounds[MAP_MAXY] - bounds[MAP_MINY] + 1
return bounds
/datum/map_template/proc/get_map_source()
if(islist(mappath))
var/list/map_sources = mappath
if(!length(map_sources))
return null
return map_sources[1]
if(isfile(mappath))
return mappath
return file(mappath)
/datum/map_template/proc/describe_map_source()
if(islist(mappath))
var/list/map_sources = mappath
if(!length(map_sources))
return "empty source list"
var/source = map_sources[1]
if(isfile(source))
return "source list with file [source]"
if(istext(source))
return "source list with text length [length(source)]"
return "source list with [source]"
if(isfile(mappath))
return "file [mappath]"
if(istext(mappath))
return "path [mappath]"
return "[mappath]"
/// Takes in a type path, locates an instance of that type in the cached map, and calculates its offset from the origin of the map, returns this offset in the form list(x, y).
/datum/map_template/proc/discover_offset(obj/marker)
if(!cached_map && mappath)
preload_size(mappath, TRUE)
if(!cached_map || !cached_map.key_len)
return null
var/key
var/found_marker = FALSE
var/list/models = cached_map.grid_models
for(key in models)
if(findtext(models[key], "[marker]")) // Yay compile time checks
found_marker = TRUE
break // This works by assuming there will ever only be one mobile dock in a template at most
if(!found_marker)
return null
for(var/datum/grid_set/gset as anything in cached_map.gridSets)
var/ycrd = gset.ycrd
for(var/line in gset.gridLines)
var/xcrd = gset.xcrd
for(var/j in 1 to length(line) step cached_map.key_len)
if(key == copytext(line, j, j + cached_map.key_len))
return list(xcrd, ycrd)
++xcrd
--ycrd
return null
/datum/map_template/proc/post_load()
return
/datum/map_template/proc/update_blacklist(turf/T, list/input_blacklist)
return
/datum/map_template/proc/load_new_z(var/no_changeturf = TRUE)
last_load_error = null
var/x = round((world.maxx - width)/2)
var/y = round((world.maxy - height)/2)
if (x < 1) x = 1
if (y < 1) y = 1
var/map_source = get_map_source()
if(!map_source)
last_load_error = "unable to normalize map source ([describe_map_source()])"
return FALSE
var/list/bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF)
var/list/atoms_to_initialise = list()
var/shuttle_state = pre_init_shuttles()
@@ -72,13 +157,15 @@
base_level = level
GLOB.map_templates["[level.z_value]"] = src
var/datum/map_load_metadata/M = maploader.load_map(file(mappath), x, y, base_level.z_value, no_changeturf = no_changeturf)
var/datum/map_load_metadata/M = maploader.load_map(map_source, x, y, base_level.z_value, no_changeturf = no_changeturf)
if(M)
bounds = extend_bounds_if_needed(bounds, M.bounds)
atoms_to_initialise += M.atoms_to_initialise
else
SSicon_smooth.can_fire = TRUE
SSshuttle.block_queue = shuttle_state
last_load_error = "DMM loader failed while loading new z from [describe_map_source()]"
return FALSE
for (var/z_index = bounds[MAP_MINZ]; z_index <= bounds[MAP_MAXZ]; z_index++)
@@ -173,26 +260,39 @@
T.static_lighting_build_overlay()
/datum/map_template/proc/load(turf/T, centered = FALSE)
last_load_error = null
if(centered)
T = locate(T.x - round(width / 2) , T.y - round(height / 2) , T.z)
if(!T)
last_load_error = "invalid placement turf"
return
if(T.x + width > world.maxx)
if(T.x + width - 1 > world.maxx)
last_load_error = "template would exceed world maxx ([T.x] + [width] - 1 > [world.maxx])"
return
if(T.y + height > world.maxy)
if(T.y + height - 1 > world.maxy)
last_load_error = "template would exceed world maxy ([T.y] + [height] - 1 > [world.maxy])"
return
var/list/atoms_to_initialise = list()
var/list/bounds
var/map_source = get_map_source()
if(!map_source)
last_load_error = "unable to normalize map source ([describe_map_source()])"
return FALSE
var/shuttle_state = pre_init_shuttles()
//Since SSicon_smooth.add_to_queue() manually wakes the subsystem, we have to use enable/disable.
SSicon_smooth.can_fire = FALSE
var/datum/map_load_metadata/M = maploader.load_map(file(mappath), T.x, T.y, T.z, cropMap=TRUE)
var/datum/map_load_metadata/M = maploader.load_map(map_source, T.x, T.y, T.z, cropMap=TRUE)
if(M)
atoms_to_initialise += M.atoms_to_initialise
bounds = M.bounds
else
SSicon_smooth.can_fire = TRUE
SSshuttle.block_queue = shuttle_state
last_load_error = "DMM loader failed while placing from [describe_map_source()]"
return FALSE
//initialize things that are normally initialized after map load
@@ -202,7 +302,8 @@
SSicon_smooth.can_fire = TRUE
message_admins("[name] loaded at [T.x], [T.y], [T.z]")
log_game("[name] loaded at [T.x], [T.y], [T.z]")
return TRUE
cached_map = keep_cached_map ? cached_map : null
return bounds
/datum/map_template/proc/get_affected_turfs(turf/T, centered = FALSE)
SHOULD_NOT_SLEEP(TRUE)
+58 -4
View File
@@ -6,9 +6,29 @@
GLOBAL_VAR_INIT(use_preloader, FALSE)
GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
/datum/grid_set
var/xcrd
var/ycrd
var/zcrd
var/gridLines
/datum/parsed_map
var/original_path
/// The length of a key in this file. This is promised by the standard to be static.
var/key_len = 0
/// The length of a line in this file. Not promised by dmm but standard dmm uses it, so we can trust it.
var/line_len = 0
var/list/grid_models = list()
var/list/gridSets = list()
/// Unoffset bounds. Null on parse failure.
var/list/parsed_bounds
/// Offset bounds. Same as parsed_bounds until load().
var/list/bounds
/datum/map_load_metadata
var/bounds
var/list/atoms_to_initialise
var/datum/parsed_map/parsed_map
/dmm_suite
// /"([a-zA-Z]+)" = \(((?:.|\n)*?)\)\n(?!\t)|\((\d+),(\d+),(\d+)\) = \{"([a-zA-Z\n]*)"\}/g
@@ -57,8 +77,11 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
. = load_map_impl(dmm_file, x_offset, y_offset, z_offset, cropMap, measureOnly, no_changeturf, lower_crop_x, upper_crop_x, lower_crop_y, upper_crop_y)
Master.StopLoadingMap()
/dmm_suite/proc/load_map_impl(dmm_file, x_offset, y_offset, z_offset, cropMap, measureOnly, no_changeturf, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper = INFINITY)
/dmm_suite/proc/load_map_impl(dmm_file, x_offset, y_offset, z_offset, cropMap, measureOnly, no_changeturf, x_lower = -INFINITY, x_upper = INFINITY, y_lower = -INFINITY, y_upper = INFINITY, cache_metadata = FALSE)
var/tfile = dmm_file//the map file we're creating
var/datum/parsed_map/parsed_map
if(cache_metadata)
parsed_map = new
/*#### WARNING AURORA SNOWFLAKE SECTION ####*/
@@ -66,6 +89,8 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
// name/path of dmm file, new var so as to not rename the `tfile` var
// to maybe maintain compatibility with other codebases
var/tfilepath = "[tfile]"
if(parsed_map)
parsed_map.original_path = tfilepath
tfile = null
// use bapi to read, parse, process, mapmanip etc
// this will "crash"/stacktrace on fail
@@ -91,6 +116,9 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
var/list/bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF)
var/list/grid_models = list()
var/key_len = 0
var/line_len = 0
if(parsed_map)
parsed_map.grid_models = grid_models
var/stored_index = 1
@@ -114,7 +142,7 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
key_len = length(key)
else
CRASH("Inconsistent key length in DMM")
if(!measureOnly)
if(!measureOnly || cache_metadata)
grid_models[key] = regexOutput[2]
// (1,1,1) = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
@@ -123,15 +151,24 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
CRASH("Coords before model definition in DMM")
var/curr_x = text2num(regexOutput[3])
var/curr_y = text2num(regexOutput[4])
var/curr_z = text2num(regexOutput[5])
if(curr_x < x_lower || curr_x > x_upper)
continue
var/datum/grid_set/gridSet
if(parsed_map)
gridSet = new
gridSet.xcrd = curr_x
gridSet.ycrd = curr_y
gridSet.zcrd = curr_z
var/xcrdStart = curr_x + x_offset - 1
//position of the currently processed square
var/xcrd
var/ycrd = text2num(regexOutput[4]) + y_offset - 1
var/zcrd = text2num(regexOutput[5]) + z_offset - 1
var/ycrd = curr_y + y_offset - 1
var/zcrd = curr_z + z_offset - 1
var/zexpansion = zcrd > world.maxz
if(zexpansion && !measureOnly) // don't actually expand the world if we're only measuring bounds
@@ -149,6 +186,8 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
bounds[MAP_MAXZ] = max(bounds[MAP_MAXZ], zcrd)
var/list/gridLines = splittext(regexOutput[6], "\n")
if(parsed_map)
gridSet.gridLines = gridLines
var/leadingBlanks = 0
while(leadingBlanks < length(gridLines) && gridLines[++leadingBlanks] == "")
@@ -161,8 +200,13 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
if(gridLines[length(gridLines)] == "")
gridLines.Cut(length(gridLines)) // Remove only one blank line at the end.
if(parsed_map)
parsed_map.gridSets += gridSet
bounds[MAP_MINY] = min(bounds[MAP_MINY], clamp(ycrd, y_lower, y_upper))
ycrd += length(gridLines) - 1 // Start at the top and work down
if(parsed_map)
gridSet.ycrd += length(gridLines) - 1
if(!cropMap && ycrd > world.maxy)
if(!measureOnly)
@@ -172,6 +216,9 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
else
bounds[MAP_MAXY] = max(bounds[MAP_MAXY], clamp(min(ycrd, world.maxy), y_lower, y_upper))
if(!line_len && length(gridLines))
line_len = length(gridLines[1])
var/maxx = xcrdStart
if(measureOnly)
for(var/line in gridLines)
@@ -236,6 +283,13 @@ GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
var/datum/map_load_metadata/M = new
M.bounds = bounds
M.atoms_to_initialise = atoms_to_initialise
if(parsed_map)
parsed_map.key_len = key_len
parsed_map.line_len = line_len
parsed_map.grid_models = grid_models.Copy()
parsed_map.parsed_bounds = bounds.Copy()
parsed_map.bounds = bounds.Copy()
M.parsed_map = parsed_map
return M
/**
@@ -1,5 +1,3 @@
/* THIS IS ONLY A PLACEHOLDER AND DOESN'T WORK */
//Yes, they can only be rectangular.
//Yes, I'm sorry.
/datum/turf_reservation
@@ -28,11 +26,202 @@
var/list/top_right_turfs = list()
/// The turf type the reservation is initially made with
var/turf_type = /turf/space
var/turf_type = RESERVED_TURF_TYPE
/// Do we override baseturfs with turf_type?
var/turf_type_is_baseturf = TRUE
///Distance away from the cordon where we can put a "sort-cordon" and run some extra code (see make_repel). 0 makes nothing happen
var/pre_cordon_distance = 0
/datum/turf_reservation/transit
turf_type = /turf/space/transit
pre_cordon_distance = 7
/datum/turf_reservation/New()
LAZYADD(SSmapping.turf_reservations, src)
/datum/turf_reservation/Destroy()
Release()
LAZYREMOVE(SSmapping.turf_reservations, src)
return ..()
/datum/turf_reservation/proc/Release()
bottom_left_turfs.Cut()
top_right_turfs.Cut()
var/list/release_turfs = list()
release_turfs |= reserved_turfs
release_turfs |= cordon_turfs
reserved_turfs.Cut()
cordon_turfs.Cut()
for(var/turf/pre_cordon_turf as anything in pre_cordon_turfs)
stop_repel(pre_cordon_turf)
pre_cordon_turfs.Cut()
for(var/turf/released_turf as anything in release_turfs)
SSmapping.used_turfs -= released_turf
SEND_SIGNAL(released_turf, COMSIG_TURF_RESERVATION_RELEASED, src)
released_turf.blocks_air = TRUE
// Makes the linter happy, even tho we don't await this
INVOKE_ASYNC(SSmapping, TYPE_PROC_REF(/datum/controller/subsystem/mapping, reserve_turfs), release_turfs)
/datum/turf_reservation/proc/calculate_cordon_turfs(turf/bottom_left, turf/top_right)
if(bottom_left.x < 2 || bottom_left.y < 2 || top_right.x > (world.maxx - 2) || top_right.y > (world.maxy - 2))
return FALSE
var/list/possible_turfs = CORNER_OUTLINE(bottom_left, width, height)
possible_turfs -= cordon_turfs
for(var/turf/cordon_turf as anything in possible_turfs)
if(!(cordon_turf.turf_flags & UNUSED_RESERVATION_TURF))
return FALSE
cordon_turfs |= possible_turfs
if(pre_cordon_distance && width > pre_cordon_distance * 2 && height > pre_cordon_distance * 2)
var/turf/offset_turf = locate(bottom_left.x + pre_cordon_distance, bottom_left.y + pre_cordon_distance, bottom_left.z)
var/list/to_add = CORNER_OUTLINE(offset_turf, width - pre_cordon_distance * 2, height - pre_cordon_distance * 2) //we step-by-stop move inwards from the outer cordon
for(var/turf/turf_being_added as anything in to_add)
pre_cordon_turfs |= turf_being_added //add one by one so we can filter out duplicates
return TRUE
/datum/turf_reservation/proc/generate_cordon()
var/area/space/space_area = locate(/area/space)
var/list/prepared_cordon_turfs = list()
for(var/turf/cordon_turf as anything in cordon_turfs)
SSmapping.unused_turfs["[cordon_turf.z]"] -= cordon_turf
cordon_turf = SSmapping.reset_turf_for_reservation(cordon_turf, space_area)
if(!cordon_turf)
continue
cordon_turf.turf_flags &= ~UNUSED_RESERVATION_TURF
SSmapping.used_turfs[cordon_turf] = src
prepared_cordon_turfs += cordon_turf
cordon_turfs = prepared_cordon_turfs
for(var/turf/pre_cordon_turf as anything in pre_cordon_turfs)
make_repel(pre_cordon_turf)
/// Register signals in the cordon "danger zone" to do something with whoever trespasses
/datum/turf_reservation/proc/make_repel(turf/pre_cordon_turf)
SHOULD_CALL_PARENT(TRUE)
//Okay so hear me out. If we place a special turf IN the reserved area, it will be overwritten, so we can't do that
//But signals are preserved even between turf changes, so even if we register a signal now it will stay even if that turf is overriden by the template
RegisterSignals(pre_cordon_turf, list(COMSIG_QDELETING, COMSIG_TURF_RESERVATION_RELEASED), PROC_REF(on_stop_repel))
/datum/turf_reservation/proc/on_stop_repel(turf/pre_cordon_turf)
SHOULD_CALL_PARENT(TRUE)
SIGNAL_HANDLER
stop_repel(pre_cordon_turf)
/// Unregister all the signals we added in RegisterRepelSignals
/datum/turf_reservation/proc/stop_repel(turf/pre_cordon_turf)
UnregisterSignal(pre_cordon_turf, list(COMSIG_QDELETING, COMSIG_TURF_RESERVATION_RELEASED))
/datum/turf_reservation/transit/make_repel(turf/pre_cordon_turf)
..()
// this does dump_in_space stuff in tg
/datum/turf_reservation/proc/prepare_reserved_turf(turf/T)
RETURN_TYPE(/turf)
if(!istype(T))
return
if(T.type != turf_type)
T = T.ChangeTurf(turf_type, TRUE, FALSE, TRUE)
if(!T)
return
if(turf_type_is_baseturf)
T.baseturf = turf_type
T.blocks_air = TRUE
return T
/datum/turf_reservation/turf_not_baseturf
turf_type_is_baseturf = FALSE
/// Internal proc which handles reserving the area for the reservation.
/datum/turf_reservation/proc/reserve_area(width, height, zlevel)
src.width = width
src.height = height
if(width > world.maxx || height > world.maxy || width < 1 || height < 1)
return FALSE
var/list/available_turfs = SSmapping.unused_turfs["[zlevel]"]
if(!length(available_turfs))
return FALSE
var/turf/bottom_left
var/turf/top_right
var/list/final_turfs = list()
var/passing = FALSE
for(var/candidate in available_turfs)
CHECK_TICK
bottom_left = candidate
if(!(bottom_left.turf_flags & UNUSED_RESERVATION_TURF))
continue
if(bottom_left.x + width > world.maxx || bottom_left.y + height > world.maxy)
continue
top_right = locate(bottom_left.x + width - 1, bottom_left.y + height - 1, bottom_left.z)
if(!(top_right.turf_flags & UNUSED_RESERVATION_TURF))
continue
final_turfs = block(bottom_left, top_right)
if(!length(final_turfs))
continue
passing = TRUE
for(var/turf/checking as anything in final_turfs)
if(!(checking.turf_flags & UNUSED_RESERVATION_TURF))
passing = FALSE
break
if(passing)
passing = calculate_cordon_turfs(bottom_left, top_right)
if(passing)
break
if(!passing || !istype(bottom_left) || !istype(top_right))
cordon_turfs.Cut()
pre_cordon_turfs.Cut()
return FALSE
for(var/turf/T as anything in final_turfs)
SSmapping.unused_turfs["[T.z]"] -= T
var/turf/reserved_turf = prepare_reserved_turf(T)
if(!reserved_turf)
continue
reserved_turfs |= reserved_turf
SSmapping.used_turfs[reserved_turf] = src
reserved_turf.turf_flags = (reserved_turf.turf_flags | RESERVATION_TURF) & ~UNUSED_RESERVATION_TURF
if(T == bottom_left)
bottom_left = reserved_turf
if(T == top_right)
top_right = reserved_turf
bottom_left_turfs += bottom_left
top_right_turfs += top_right
return TRUE
/datum/turf_reservation/proc/reserve(width, height, z_size, z_reservation)
src.z_size = z_size
var/failed_reservation = FALSE
for(var/i = 1 to z_size)
if(!reserve_area(width, height, z_reservation))
failed_reservation = TRUE
break
if(failed_reservation)
Release()
return FALSE
generate_cordon()
return TRUE
/// Calculates the effective bounds information for the given turf. Returns a list of the information, or null if not applicable.
/datum/turf_reservation/proc/calculate_turf_bounds_information(turf/target)
for(var/z_idx in 1 to z_size)