From acd8c6d9d5c85618db1569051fb7050e9c07c6d3 Mon Sep 17 00:00:00 2001 From: Batrachophreno Date: Sun, 12 Jul 2026 16:45:26 -0400 Subject: [PATCH] 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. --- code/__DEFINES/dcs/signals/signals_turf.dm | 3 + code/__DEFINES/maps.dm | 3 + code/__DEFINES/shuttle.dm | 2 + code/controllers/subsystems/mapping.dm | 135 +++++++++++- .../admin/verbs/map_template_loadverb.dm | 32 ++- code/modules/mapping/map_template.dm | 119 ++++++++++- code/modules/mapping/reader.dm | 62 +++++- .../space_management/space_reservation.dm | 195 +++++++++++++++++- html/changelogs/Bat-TurfReservations.yml | 4 + 9 files changed, 526 insertions(+), 29 deletions(-) create mode 100644 html/changelogs/Bat-TurfReservations.yml diff --git a/code/__DEFINES/dcs/signals/signals_turf.dm b/code/__DEFINES/dcs/signals/signals_turf.dm index d1d7148268c..4d190ffa0e3 100644 --- a/code/__DEFINES/dcs/signals/signals_turf.dm +++ b/code/__DEFINES/dcs/signals/signals_turf.dm @@ -7,3 +7,6 @@ ///from base of datum/thrownthing/finalize(): (turf/turf, atom/movable/thrownthing) when something is thrown and lands on us #define COMSIG_TURF_MOVABLE_THROW_LANDED "turf_movable_throw_landed" + +///from base of /datum/turf_reservation/proc/Release: (datum/turf_reservation/reservation) +#define COMSIG_TURF_RESERVATION_RELEASED "turf_reservation_released" diff --git a/code/__DEFINES/maps.dm b/code/__DEFINES/maps.dm index 0d39f3da3c7..020326ef7e1 100644 --- a/code/__DEFINES/maps.dm +++ b/code/__DEFINES/maps.dm @@ -43,3 +43,6 @@ #define DEFAULT_MAP_TRAITS list(\ DECLARE_LEVEL("CentCom", ZTRAITS_CENTCOM),\ ) + +//Reserved/Transit turf type +#define RESERVED_TURF_TYPE /turf/space //What the turf is when not being used diff --git a/code/__DEFINES/shuttle.dm b/code/__DEFINES/shuttle.dm index d6729701a32..d3439fc3d4c 100644 --- a/code/__DEFINES/shuttle.dm +++ b/code/__DEFINES/shuttle.dm @@ -10,6 +10,8 @@ #define SHIP_STATUS_TRANSIT 2 #define SHIP_STATUS_OVERMAP 3 +#define SHUTTLE_TRANSIT_BORDER 16 + //Landmarks. #define SLANDMARK_FLAG_AUTOSET 1 // If set, will set base area and turf type to same as where it was spawned at #define SLANDMARK_FLAG_ZERO_G 2 // Zero-G shuttles moved here will lose gravity unless the area has ambient gravity. diff --git a/code/controllers/subsystems/mapping.dm b/code/controllers/subsystems/mapping.dm index 42aeb5da30e..c779e8cf4bc 100644 --- a/code/controllers/subsystems/mapping.dm +++ b/code/controllers/subsystems/mapping.dm @@ -9,7 +9,15 @@ SUBSYSTEM_DEF(mapping) var/list/away_sites_templates = list() var/list/submaps = list() - var/list/used_turfs = list() //list of turf = datum/turf_reservation -- Currently unused + /// Not actually unused turfs they're unused but reserved for use for whatever requests them. "[zlevel_of_turf]" = list(turfs) + var/list/unused_turfs = list() + /// List of actual datum/turf_reservations - reservation-centric + var/list/turf_reservations = list() + /// List of turfs that are currently reserved and in use - turf-centric, used to get the reservations from turfs + var/list/used_turfs = list() + var/list/reservation_ready = list() + var/clearing_reserved_turfs = FALSE + var/num_of_res_levels = 0 /// List of z level (as number) -> list of all z levels vertically connected to ours /// Useful for fast grouping lookups and such @@ -47,6 +55,12 @@ SUBSYSTEM_DEF(mapping) space_ruins_templates = SSmapping.space_ruins_templates exoplanet_ruins_templates = SSmapping.exoplanet_ruins_templates away_sites_templates = SSmapping.away_sites_templates + unused_turfs = SSmapping.unused_turfs + turf_reservations = SSmapping.turf_reservations + used_turfs = SSmapping.used_turfs + reservation_ready = SSmapping.reservation_ready + clearing_reserved_turfs = SSmapping.clearing_reserved_turfs + num_of_res_levels = SSmapping.num_of_res_levels /datum/controller/subsystem/mapping/proc/preloadTemplates(path = "maps/templates/") //see master controller setup var/list/filelist = flist(path) @@ -115,11 +129,128 @@ SUBSYSTEM_DEF(mapping) // Bare minimum we have ourselves z_level_to_stack[z_value] = list(z_value) -//Placeholder for now /datum/controller/subsystem/mapping/proc/get_reservation_from_turf(turf/T) RETURN_TYPE(/datum/turf_reservation) return used_turfs[T] +/// Adds a new reservation z level. A bit of space that can be handed out on request +/// Of note, reservations default to transit turfs, to make their most common use, shuttles, faster +/datum/controller/subsystem/mapping/proc/add_reservation_zlevel(for_shuttles) + num_of_res_levels++ + return add_new_zlevel("Transit/Reserved #[num_of_res_levels]", list(ZTRAIT_RESERVED = TRUE), contain_turfs = FALSE) + +/// Sets up a z level as reserved +/// This is not for wiping reserved levels, use wipe_reservations() for that. +/// If this is called after SSatom init, it will call Initialize on all turfs on the passed z, as its name promises +/datum/controller/subsystem/mapping/proc/initialize_reserved_level(z) + UNTIL(!clearing_reserved_turfs) //regardless, lets add a check just in case. + clearing_reserved_turfs = TRUE //This operation will likely clear any existing reservations, so lets make sure nothing tries to make one while we're doing it. + + if(!level_trait(z, ZTRAIT_RESERVED)) + clearing_reserved_turfs = FALSE + CRASH("Invalid z level prepared for reservations.") + + var/min_x = SHUTTLE_TRANSIT_BORDER + var/min_y = SHUTTLE_TRANSIT_BORDER + var/max_x = world.maxx - SHUTTLE_TRANSIT_BORDER + var/max_y = world.maxy - SHUTTLE_TRANSIT_BORDER + if(min_x > max_x || min_y > max_y) + clearing_reserved_turfs = FALSE + CRASH("World bounds are too small for dynamic reservations with SHUTTLE_TRANSIT_BORDER [SHUTTLE_TRANSIT_BORDER].") + + var/list/reserved_block = block(locate(min_x, min_y, z), locate(max_x, max_y, z)) + var/list/prepared_block = list() + var/area/space/space_area = locate(/area/space) + for(var/turf/T as anything in reserved_block) + var/turf/reservation_turf = reset_turf_for_reservation(T, space_area) + if(!reservation_turf) + continue + reservation_turf.turf_flags = (reservation_turf.turf_flags | UNUSED_RESERVATION_TURF) & ~RESERVATION_TURF + prepared_block += reservation_turf + CHECK_TICK + + // Gotta create these suckers if we've not done so already + if(SSatoms.initialized) + SSatoms.InitializeAtoms(Z_TURFS(z)) + + unused_turfs["[z]"] = prepared_block + reservation_ready["[z]"] = TRUE + clearing_reserved_turfs = FALSE + +/// Requests a /datum/turf_reservation based on the given width, height, and z_size. You can specify a z_reservation to use a specific z level, or leave it null to use any z level. +/datum/controller/subsystem/mapping/proc/request_turf_block_reservation( + width, + height, + z_size = 1, + z_reservation = null, + reservation_type = /datum/turf_reservation, + turf_type_override = null, +) + UNTIL(!clearing_reserved_turfs) + + var/datum/turf_reservation/reserve = new reservation_type + if(!isnull(turf_type_override)) + reserve.turf_type = turf_type_override + + if(!z_reservation) + for(var/reserved_z in levels_by_trait(ZTRAIT_RESERVED)) + if(!reservation_ready["[reserved_z]"]) + continue + if(reserve.reserve(width, height, z_size, reserved_z)) + return reserve + + // If we didn't return at this point, theres a good chance we ran out of room on the existing reserved z levels, so lets try a new one + var/datum/space_level/newReserved = add_reservation_zlevel() + initialize_reserved_level(newReserved.z_value) + if(reserve.reserve(width, height, z_size, newReserved.z_value)) + return reserve + else + if(!level_trait(z_reservation, ZTRAIT_RESERVED)) + qdel(reserve) + return + if(!reservation_ready["[z_reservation]"]) + initialize_reserved_level(z_reservation) + if(reserve.reserve(width, height, z_size, z_reservation)) + return reserve + + QDEL_NULL(reserve) + +/// Schedules a group of turfs to be handed back to the reservation system's control +/// If await is true, will sleep until the turfs are finished work +/datum/controller/subsystem/mapping/proc/reserve_turfs(list/turfs, await = FALSE) + if(!islist(turfs) || !length(turfs)) + return + if(await) + return reset_turf_reservation_list(turfs) + INVOKE_ASYNC(src, PROC_REF(reset_turf_reservation_list), turfs) + +/datum/controller/subsystem/mapping/proc/reset_turf_reservation_list(list/turfs) + var/area/space/space_area = locate(/area/space) + while(length(turfs)) + var/turf/T = turfs[length(turfs)] + turfs.len-- + if(!istype(T)) + continue + var/turf/reservation_turf = reset_turf_for_reservation(T, space_area) + if(!reservation_turf) + continue + reservation_turf.turf_flags = (reservation_turf.turf_flags | UNUSED_RESERVATION_TURF) & ~RESERVATION_TURF + LAZYINITLIST(unused_turfs["[reservation_turf.z]"]) + unused_turfs["[reservation_turf.z]"] |= reservation_turf + CHECK_TICK + +/datum/controller/subsystem/mapping/proc/reset_turf_for_reservation(turf/T, area/target_area) + RETURN_TYPE(/turf) + if(!istype(T)) + return + if(T.type != RESERVED_TURF_TYPE) + T = T.ChangeTurf(RESERVED_TURF_TYPE, TRUE, FALSE, TRUE) + if(target_area && T.loc != target_area) + T.change_area(T.loc, target_area) + T.baseturf = RESERVED_TURF_TYPE + T.blocks_air = TRUE + return T + /proc/generateMapList(filename) var/list/potentialMaps = list() diff --git a/code/modules/admin/verbs/map_template_loadverb.dm b/code/modules/admin/verbs/map_template_loadverb.dm index 6e9ef3bfed7..29679159d37 100644 --- a/code/modules/admin/verbs/map_template_loadverb.dm +++ b/code/modules/admin/verbs/map_template_loadverb.dm @@ -27,8 +27,9 @@ log_and_message_admins("has placed a map template [log_name].") to_chat(usr, "Successfully placed map template [log_name].") else - log_and_message_admins("has failed to place a map template [log_name].") - to_chat(usr, "Failed to place map template [log_name].") + var/failure_reason = template.last_load_error ? " Reason: [template.last_load_error]" : "" + log_and_message_admins("has failed to place a map template [log_name].[failure_reason]") + to_chat(usr, "Failed to place map template [log_name].[failure_reason]") usr.client.images -= preview /datum/admins/proc/map_template_load_new_z() @@ -64,8 +65,9 @@ log_and_message_admins("has placed a map template [log_name] on Z bounds [bounds[MAP_MINZ]] - [bounds[MAP_MAXZ]].") to_chat(usr, "Successfully place map template [log_name].") else - log_and_message_admins("has failed to place a map template [log_name].") - to_chat(usr, "Failed to place map template [log_name].") + var/failure_reason = template.last_load_error ? " Reason: [template.last_load_error]" : "" + log_and_message_admins("has failed to place a map template [log_name].[failure_reason]") + to_chat(usr, "Failed to place map template [log_name].[failure_reason]") SSair.can_fire = TRUE /datum/admins/proc/map_template_upload() @@ -82,17 +84,25 @@ to_chat(usr, "Bad map file: [map]") return + var/map_text = file2text(map) + if(!map_text) + var/failure_reason = " Reason: uploaded file could not be read or was empty." + log_and_message_admins("failed to upload map template '[map]'.[failure_reason]") + to_chat(usr, "Map template '[map]' failed to load properly.[failure_reason]") + return + var/rounded_wtime = round(world.time) - rustg_file_write(file2text(map), "data/logs/[GLOB.diary_date_string]_[rounded_wtime]_[map]") + rustg_file_write(map_text, "data/logs/[GLOB.diary_date_string]_[rounded_wtime]_[map]") - var/datum/map_template/M = new(list(map), "[map]") + var/datum/map_template/M = new(list(map_text), "[map]") - log_and_message_admins("is attempting to upload a map template '[map]''.") - to_chat(usr, "Attempting to upload map template '[map]''.") + log_and_message_admins("is attempting to upload a map template '[map]'.") + to_chat(usr, "Attempting to upload map template '[map]'.") if(M.preload_size()) to_chat(usr, "Map template '[map]' ready to place ([M.width]x[M.height]).") SSmapping.map_templates[M.name] = M - log_and_message_admins("has uploaded map template '[map]''.") + log_and_message_admins("has uploaded map template '[map]'.") else - log_and_message_admins("failed to upload map template '[map]''.") - to_chat(usr, "Map template '[map]' failed to load properly") + var/failure_reason = M.last_load_error ? " Reason: [M.last_load_error]" : "" + log_and_message_admins("failed to upload map template '[map]'.[failure_reason]") + to_chat(usr, "Map template '[map]' failed to load properly.[failure_reason]") diff --git a/code/modules/mapping/map_template.dm b/code/modules/mapping/map_template.dm index b49b730ca7f..66c4b022782 100644 --- a/code/modules/mapping/map_template.dm +++ b/code/modules/mapping/map_template.dm @@ -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) diff --git a/code/modules/mapping/reader.dm b/code/modules/mapping/reader.dm index f52208b7ece..bfa3778a85c 100644 --- a/code/modules/mapping/reader.dm +++ b/code/modules/mapping/reader.dm @@ -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 /** diff --git a/code/modules/mapping/space_management/space_reservation.dm b/code/modules/mapping/space_management/space_reservation.dm index 77bc1cfebb2..da520e10cbf 100644 --- a/code/modules/mapping/space_management/space_reservation.dm +++ b/code/modules/mapping/space_management/space_reservation.dm @@ -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) diff --git a/html/changelogs/Bat-TurfReservations.yml b/html/changelogs/Bat-TurfReservations.yml new file mode 100644 index 00000000000..2744b26cee8 --- /dev/null +++ b/html/changelogs/Bat-TurfReservations.yml @@ -0,0 +1,4 @@ +author: Batrachophrenoboocosmomachia +delete-after: True +changes: + - rscadd: "Ports tg-style dynamic turf reservations."