From 7b5c263ae5bf6ed36112aa962afd9616cbef7fd3 Mon Sep 17 00:00:00 2001 From: silicons <2003111+silicons@users.noreply.github.com> Date: Thu, 20 Jun 2024 19:26:53 -0400 Subject: [PATCH] fast turf reservation lookups & aligned spatial grid lookups (#6532) # why? non-player facing we're going to need fast lookups for "is this thing in range of this other thing" at some point, for things like telecomms this current solution consumes about 16KB per reservation level we also add the capability to use spatial grids to quickly get atoms on a level. spatial grids are also at 16KB per grid, per world level all in all worth it in my opinion. --- code/__DEFINES/mapping/system.dm | 8 +- .../subsystem/mapping/allocation.dm | 5 ++ code/controllers/subsystem/mapping/boot.dm | 26 ++++++- code/controllers/subsystem/mapping/levels.dm | 45 ++++++----- .../subsystem/mapping/reservations.dm | 39 +++++++++- code/controllers/subsystem/spatial_grids.dm | 76 +++++++++++++++---- .../datums/components/movable/spatial_grid.dm | 7 +- code/modules/mapping/turf_reservation.dm | 65 ++++++++++++++-- 8 files changed, 211 insertions(+), 60 deletions(-) diff --git a/code/__DEFINES/mapping/system.dm b/code/__DEFINES/mapping/system.dm index b713e17b37c4..ca87cd22d3fa 100644 --- a/code/__DEFINES/mapping/system.dm +++ b/code/__DEFINES/mapping/system.dm @@ -2,7 +2,9 @@ #define RESERVED_TURF_TYPE /turf/space/basic /// reserved area type #define RESERVED_AREA_TYPE /area/space -/// reservation resolution - this drastically decreases the amount of time used to search for a spot for reservation. -/// set this to a value that's slightly above average of a common multiplier for reservation sizes. -#define RESERVED_TURF_RESOLUTION 8 +/// Turf chunk resolution +/// +/// * this is the most granular a turf reservation alloc can be (e.g. 8x8 for '8') +/// * this is the resolution of spatial gridmaps. why? this way spatial queries are aligned and super fast. +#define TURF_CHUNK_RESOLUTION 8 diff --git a/code/controllers/subsystem/mapping/allocation.dm b/code/controllers/subsystem/mapping/allocation.dm index 7a99d909631c..af4adad50094 100644 --- a/code/controllers/subsystem/mapping/allocation.dm +++ b/code/controllers/subsystem/mapping/allocation.dm @@ -22,6 +22,11 @@ // todo: recover() // todo: zclear system will be in this later, for now, this is just a wrapper +/datum/controller/subsystem/mapping/on_max_z_changed(old_z_count, new_z_count) + . = ..() + // just to make sure order of ops / assumptions are right + ASSERT(length(ordered_levels) == world.maxz) + /** * gets an reusable level, or increments world.maxz * WARNING: AFTER THIS, YOU NEED TO USE THE LEVEL, OR READD TO REUSABLE, OR THIS IS A MEMORY LEAK! diff --git a/code/controllers/subsystem/mapping/boot.dm b/code/controllers/subsystem/mapping/boot.dm index a51e2eb4004b..516ddad82c7a 100644 --- a/code/controllers/subsystem/mapping/boot.dm +++ b/code/controllers/subsystem/mapping/boot.dm @@ -7,13 +7,31 @@ * so we snowflaked it by compiling a single empty space level * this is called during initial world loading to expand that space level to the size of the world, * and init it as our first reserved level. + * + * width and height exist to init the world's dimensions based on the map being loaded + * this must be done before anything like spatial hashes are made, as those depend on world dimensions! + * + * this proc is hilariously, hilariously unstable and changes as backend changes + * why? + * + * because the backend is generally extremely tightly coupled + * as an example, the backend API assumes all level allocs are done through SSmapping, + * so it doesn't even allow for the existnece of an unmanaged level already being there; + * such a thing is impossible outside of severe bugs + * + * so in this proc, we're basically hard-setting variables - with potential issues, because + * this can get desynced with the rest of the subsystem's code - to 'fake' such a proper init cycle. + * + * at some point, SSmapping will be better coded, but for now, it's pretty messy. */ /datum/controller/subsystem/mapping/proc/load_server_initial_reservation_area(width, height) ASSERT(world.maxz == 1) - ASSERT(length(reserve_levels) == 0) - reserved_level_count = 1 - reserve_levels = list(1) world.maxx = width world.maxy = height - ordered_levels = list(new /datum/map_level/reserved) + ASSERT(length(reserve_levels) == 0) + // basically makes allocate_level() grab the first one + reusable_levels += 1 + ordered_levels += null world.max_z_changed(0, 1) + synchronize_datastructures() + allocate_reserved_level() diff --git a/code/controllers/subsystem/mapping/levels.dm b/code/controllers/subsystem/mapping/levels.dm index 69184c8f2e41..2440f7692b1c 100644 --- a/code/controllers/subsystem/mapping/levels.dm +++ b/code/controllers/subsystem/mapping/levels.dm @@ -233,27 +233,30 @@ SSplanets.legacy_planet_assert(z_index, level_or_path.planet_path) //! LEGACY - if((level_or_path.flags & LEGACY_LEVEL_STATION) || level_or_path.has_trait(ZTRAIT_STATION)) - loaded_station.station_levels += z_index - if((level_or_path.flags & LEGACY_LEVEL_ADMIN) || level_or_path.has_trait(ZTRAIT_ADMIN)) - loaded_station.admin_levels += z_index - if((level_or_path.flags & LEGACY_LEVEL_CONTACT) || level_or_path.has_trait(ZTRAIT_STATION)) - loaded_station.contact_levels += z_index - if((level_or_path.flags & LEGACY_LEVEL_SEALED)) - loaded_station.sealed_levels += z_index - if((level_or_path.flags & LEGACY_LEVEL_CONSOLES) || level_or_path.has_trait(ZTRAIT_STATION)) - loaded_station.map_levels += z_index - // Holomaps - // Auto-center the map if needed (Guess based on maxx/maxy) - if (level_or_path.holomap_offset_x < 0) - level_or_path.holomap_offset_x = ((HOLOMAP_ICON_SIZE - world.maxx) / 2) - if (level_or_path.holomap_offset_x < 0) - level_or_path.holomap_offset_y = ((HOLOMAP_ICON_SIZE - world.maxy) / 2) - // Assign them to the map lists - LIST_NUMERIC_SET(loaded_station.holomap_offset_x, z_index, level_or_path.holomap_offset_x) - LIST_NUMERIC_SET(loaded_station.holomap_offset_y, z_index, level_or_path.holomap_offset_y) - LIST_NUMERIC_SET(loaded_station.holomap_legend_x, z_index, level_or_path.holomap_legend_x) - LIST_NUMERIC_SET(loaded_station.holomap_legend_y, z_index, level_or_path.holomap_legend_y) + // the fact that this exists is stupid but this check + // make sure we're not loading system maps like reserved levels before station loads. + if(loaded_station) + if((level_or_path.flags & LEGACY_LEVEL_STATION) || level_or_path.has_trait(ZTRAIT_STATION)) + loaded_station.station_levels += z_index + if((level_or_path.flags & LEGACY_LEVEL_ADMIN) || level_or_path.has_trait(ZTRAIT_ADMIN)) + loaded_station.admin_levels += z_index + if((level_or_path.flags & LEGACY_LEVEL_CONTACT) || level_or_path.has_trait(ZTRAIT_STATION)) + loaded_station.contact_levels += z_index + if((level_or_path.flags & LEGACY_LEVEL_SEALED)) + loaded_station.sealed_levels += z_index + if((level_or_path.flags & LEGACY_LEVEL_CONSOLES) || level_or_path.has_trait(ZTRAIT_STATION)) + loaded_station.map_levels += z_index + // Holomaps + // Auto-center the map if needed (Guess based on maxx/maxy) + if (level_or_path.holomap_offset_x < 0) + level_or_path.holomap_offset_x = ((HOLOMAP_ICON_SIZE - world.maxx) / 2) + if (level_or_path.holomap_offset_x < 0) + level_or_path.holomap_offset_y = ((HOLOMAP_ICON_SIZE - world.maxy) / 2) + // Assign them to the map lists + LIST_NUMERIC_SET(loaded_station.holomap_offset_x, z_index, level_or_path.holomap_offset_x) + LIST_NUMERIC_SET(loaded_station.holomap_offset_y, z_index, level_or_path.holomap_offset_y) + LIST_NUMERIC_SET(loaded_station.holomap_legend_x, z_index, level_or_path.holomap_legend_x) + LIST_NUMERIC_SET(loaded_station.holomap_legend_y, z_index, level_or_path.holomap_legend_y) //! END /** diff --git a/code/controllers/subsystem/mapping/reservations.dm b/code/controllers/subsystem/mapping/reservations.dm index 24891485a7f7..640cbcaa0ff3 100644 --- a/code/controllers/subsystem/mapping/reservations.dm +++ b/code/controllers/subsystem/mapping/reservations.dm @@ -16,7 +16,22 @@ /// doing some blocking op on reservation system var/static/reservation_blocking_op = FALSE /// singleton area holding all free reservation turfs - var/static/area/unused_reservation_area/unallocated_reserve_area = new + var/static/area/unused_reservation_area/reservation_unallocated_area = new + /// spatial grid of turf reservations. the owner of a chunk is the bottom left tile's owner. + /// + /// this is a list with length of world.maxz, with the actual spatial grid list being at the index of the z + /// e.g. to grab a reserved level's lookup, do `reservation_spatia_lookups[z_index]` + /// + /// * null means that a level isn't a reservation level + /// * this also means that we can't zclear / 'free' reserved levels; they're effectively immovable due to this datastructure + /// * if it is a reserved level, it returns the spatial grid + /// * to get a chunk, do `spatial_lookup[ceil(where.x / TURF_CHUNK_RESOLUTION) + (ceil(where.y / TURF_CHUNK_RESOLUTION) - 1) * ceil(world.maxx / TURF_CHUNK_RESOLUTION)]` + var/static/list/reservation_spatial_lookups = list() + +/datum/controller/subsystem/mapping/on_max_z_changed(old_z_count, new_z_count) + . = ..() + if(length(reservation_spatial_lookups) < new_z_count) + reservation_spatial_lookups.len = new_z_count /datum/controller/subsystem/mapping/Recover() . = ..() @@ -29,12 +44,15 @@ if(reserved_level_count && ((world.maxx * world.maxy * (reserved_level_count + 1)) > reserved_turfs_max)) log_and_message_admins(SPAN_USERDANGER("Out of dynamic reservation allocations. Is there a memory leak with turf reservations?")) return FALSE - log_and_message_admins(SPAN_USERDANGER("Allocating new reserved level. Now at [reserved_level_count]. This is probably not a good thing if the server is not at high load right now.")) + if(reserved_level_count) + log_and_message_admins(SPAN_USERDANGER("Allocating new reserved level. Now at [reserved_level_count + 1]. This is probably not a good thing if the server is not at high load right now.")) + reserved_level_count++ var/datum/map_level/reserved/level_struct = new ASSERT(allocate_level(level_struct)) - reserved_level_count++ initialize_reserved_level(level_struct.z_index) reserve_levels |= level_struct.z_index + // make a list with a predetermined size for the lookup + reservation_spatial_lookups[level_struct.z_index] = new /list(ceil(world.maxx / TURF_CHUNK_RESOLUTION) * ceil(world.maxy / TURF_CHUNK_RESOLUTION)) return level_struct.z_index /** @@ -71,7 +89,20 @@ T.turf_flags |= UNUSED_RESERVATION_TURF CHECK_TICK // todo: area.assimilate_turfs? - unallocated_reserve_area.contents.Add(turfs) + reservation_unallocated_area.contents.Add(turfs) + +/** + * @return turf reservation someone's in, or null if they're not in a reservation + */ +/datum/controller/subsystem/mapping/proc/get_turf_reservation(atom/where) + where = get_turf(where) + if(!where) + return + // this doubles as 'is this a reserved level' + var/list/spatial_lookup = reservation_spatial_lookups[where.z] + if(!spatial_lookup) + return + return spatial_lookup[ceil(where.x / TURF_CHUNK_RESOLUTION) + (ceil(where.y / TURF_CHUNK_RESOLUTION) - 1) * ceil(world.maxx / TURF_CHUNK_RESOLUTION)] /area/unused_reservation_area name = "Unused Reservation Area" diff --git a/code/controllers/subsystem/spatial_grids.dm b/code/controllers/subsystem/spatial_grids.dm index 9d738f97bb37..fdfe31813e94 100644 --- a/code/controllers/subsystem/spatial_grids.dm +++ b/code/controllers/subsystem/spatial_grids.dm @@ -27,7 +27,9 @@ SUBSYSTEM_DEF(spatial_grids) living.sync_world_z(new_z_count) /** - * index = ceil(x / resolution) + width * ceil(y / resolution) + * index = ceil(x / resolution) + width * (ceil(y / resolution) - 1) + * + * * at time of writing, spatial grids are intentionally aligned with turf reservation resolution. this is intentional so looking stuff up in grids on a reservation is lightning-fast. */ /datum/spatial_grid /// our grid; list[z] = grid: list() @@ -36,21 +38,14 @@ SUBSYSTEM_DEF(spatial_grids) var/width /// our grid height var/height - /// our grid resolution in tiles - var/resolution = 16 /// expected type var/expected_type = /atom/movable -/datum/spatial_grid/New(expected_type, resolution) - // make sure resolution is reasonable - if(resolution <= 8 || resolution >= 128) - stack_trace("invalid resolution: [resolution]") - resolution = 16 +/datum/spatial_grid/New(expected_type) // initialize grid - src.width = ceil(world.maxx / resolution) - src.height = ceil(world.maxy / resolution) + src.width = ceil(world.maxx / TURF_CHUNK_RESOLUTION) + src.height = ceil(world.maxy / TURF_CHUNK_RESOLUTION) src.grids = list() - src.resolution = resolution src.expected_type = expected_type sync_world_z(world.maxz) @@ -105,10 +100,10 @@ SUBSYSTEM_DEF(spatial_grids) */ /datum/spatial_grid/proc/range_query(turf/epicenter, distance) . = list() - var/min_x = ceil((epicenter.x - distance) / src.resolution) - var/min_y = floor((epicenter.y - distance) / src.resolution) - var/max_x = ceil((epicenter.x + distance) / src.resolution) - var/max_y = floor((epicenter.y + distance) / src.resolution) + var/min_x = ceil((epicenter.x - distance) / TURF_CHUNK_RESOLUTION) + var/min_y = ceil((epicenter.y - distance) / TURF_CHUNK_RESOLUTION) + var/max_x = ceil((epicenter.x + distance) / TURF_CHUNK_RESOLUTION) + var/max_y = ceil((epicenter.y + distance) / TURF_CHUNK_RESOLUTION) var/list/grid = src.grids[epicenter.z] for(var/x in max(1, min_x) to min(src.width, max_x)) for(var/y in max(1, min_y) to min(src.height, max_y)) @@ -142,3 +137,54 @@ SUBSYSTEM_DEF(spatial_grids) if(!grid[i]) continue . += grid[i] + +//* basically the above but only within a certain turf reservation *// + +/datum/spatial_grid/proc/reservation_range_query(datum/turf_reservation/reservation, turf/epicenter, distance) + ASSERT(reservation.spatial_z == epicenter.z) + . = list() + var/min_x = ceil((epicenter.x - distance) / TURF_CHUNK_RESOLUTION) + var/min_y = ceil((epicenter.y - distance) / TURF_CHUNK_RESOLUTION) + var/max_x = ceil((epicenter.x + distance) / TURF_CHUNK_RESOLUTION) + var/max_y = ceil((epicenter.y + distance) / TURF_CHUNK_RESOLUTION) + var/list/grid = src.grids[epicenter.z] + for(var/x in max(1, min_x, reservation.spatial_bl_x) to min(src.width, max_x, reservation.spatial_tr_x)) + for(var/y in max(1, min_y, reservation.spatial_bl_y) to min(src.height, max_y, reservation.spatial_tr_y)) + var/index = x + src.width * (y - 1) + if(grid[index]) + var/entry = grid[index] + if(islist(entry)) + for(var/atom/movable/AM as anything in entry) + if(get_dist(AM, epicenter) <= distance) + . += AM + else if(get_dist(entry, epicenter) <= distance) + . += entry + +/datum/spatial_grid/proc/reservation_all_atoms(datum/turf_reservation/reservation) + . = list() + var/list/grid = src.grids[reservation.spatial_z] + for(var/x in reservation.spatial_bl_x to reservation.spatial_tr_x) + for(var/y in reservation.spatial_bl_y to reservation.spatial_tr_y) + var/index = x + src.width * (y - 1) + if(!grid[index]) + continue + . += grid[index] + +//* basically the above but only within a certain turf reservation, if reservation exists; otherwise, proceed as normal *// +//* if on a reservation level, but no reservation, we return nothing. *// + +/datum/spatial_grid/proc/automatic_range_query(turf/epicenter, distance) + // check if we're on a reserved level + var/list/spatial_lookup = SSmapping.reservation_spatial_lookups[epicenter.z] + if(!spatial_lookup) + // we're not on a reserved level, use normal + return range_query(epicenter, distance) + // we're on a reserve level + var/datum/turf_reservation/reservation = spatial_lookup[ceil(epicenter.x / TURF_CHUNK_RESOLUTION) + (ceil(epicenter.y / TURF_CHUNK_RESOLUTION) - 1) * ceil(world.maxx / TURF_CHUNK_RESOLUTION)] + // check if reservation exists + if(reservation) + // it does, get stuff in reservation + return reservation_range_query(reservation, epicenter, distance) + else + // it doesn't, return nothing + return list() diff --git a/code/datums/components/movable/spatial_grid.dm b/code/datums/components/movable/spatial_grid.dm index eb64267d8be6..a549c642d1ae 100644 --- a/code/datums/components/movable/spatial_grid.dm +++ b/code/datums/components/movable/spatial_grid.dm @@ -9,8 +9,6 @@ registered_type = /datum/component/spatial_grid /// target spatial grid var/datum/spatial_grid/grid - /// target grid resolution - var/grid_resolution /// target grid width var/grid_width /// last grid index @@ -24,7 +22,6 @@ return COMPONENT_INCOMPATIBLE src.grid = grid - src.grid_resolution = grid.resolution src.grid_width = grid.width /datum/component/spatial_grid/RegisterWithParent() @@ -43,7 +40,7 @@ RegisterSignal(root, COMSIG_MOVABLE_MOVED, PROC_REF(update)) root = root.loc if(isturf(root)) - var/idx = ceil(root.x / grid_resolution) + grid_width * floor(root.y / grid_resolution) + var/idx = ceil(root.x / TURF_CHUNK_RESOLUTION) + grid_width * (ceil(root.y / TURF_CHUNK_RESOLUTION) - 1) grid.direct_insert(parent, root.z, idx) current_index = idx @@ -61,7 +58,7 @@ return // turf --> turf, try to do an optimized, lazy update if(isturf(oldloc) && isturf(newloc) && (oldloc.z == newloc.z)) - var/new_index = ceil(newloc.x / grid_resolution) + grid_width * floor(newloc.y / grid_resolution) + var/new_index = ceil(newloc.x / TURF_CHUNK_RESOLUTION) + grid_width * (ceil(newloc.y / TURF_CHUNK_RESOLUTION) - 1) var/z = oldloc.z if(new_index != current_index) grid.direct_remove(parent, z, current_index) diff --git a/code/modules/mapping/turf_reservation.dm b/code/modules/mapping/turf_reservation.dm index 0e043d4ffd3d..e13a0da17f4d 100644 --- a/code/modules/mapping/turf_reservation.dm +++ b/code/modules/mapping/turf_reservation.dm @@ -27,6 +27,13 @@ /// type of our area - null for default var/area_type + //* spatial lookup *// + var/spatial_bl_x + var/spatial_bl_y + var/spatial_tr_x + var/spatial_tr_y + var/spatial_z + /datum/turf_reservation/New() if(isnull(turf_type)) turf_type = RESERVED_TURF_TYPE @@ -43,6 +50,21 @@ allocated = FALSE SSmapping.reservations -= src + // unegister from lookup + var/list/spatial_lookup = SSmapping.reservation_spatial_lookups[spatial_z] + var/spatial_width = ceil(world.maxx / TURF_CHUNK_RESOLUTION) + for(var/spatial_x in spatial_bl_x to spatial_tr_x) + for(var/spatial_y in spatial_bl_y to spatial_tr_y) + var/index = spatial_x + (spatial_y - 1) * spatial_width + if(spatial_lookup[index] != src) + stack_trace("index [index] wasn't self, what happened?") + continue + spatial_lookup[index] = null + + spatial_bl_x = spatial_tr_x = spatial_bl_y = spatial_tr_y = spatial_z = null + + return TRUE + /datum/turf_reservation/proc/reserve(width, height, z_override) if(width > world.maxx || height > world.maxy || width < 1 || height < 1) CRASH("invalid request") @@ -58,26 +80,35 @@ var/list/turf/final var/area/area_path = area_type var/area/area_instance = initial(area_path.unique)? (GLOB.areas_by_type[area_path] || new area_path) : new area_path + var/found_a_spot = FALSE - var/how_many_wide = FLOOR(width / RESERVED_TURF_RESOLUTION, 1) - var/how_many_high = FLOOR(height / RESERVED_TURF_RESOLUTION, 1) - var/total_many_wide = FLOOR(world.maxx / RESERVED_TURF_RESOLUTION, 1) - var/total_many_high = FLOOR(world.maxy / RESERVED_TURF_RESOLUTION, 1) + + var/how_many_wide = ceil(width / TURF_CHUNK_RESOLUTION) + var/how_many_high = ceil(height / TURF_CHUNK_RESOLUTION) + var/total_many_wide = floor(world.maxx / TURF_CHUNK_RESOLUTION) + var/total_many_high = floor(world.maxy / TURF_CHUNK_RESOLUTION) + // the dreaded 5 deep for loop while((level_index = z_override) || (level_index = pick_n_take(possible_levels))) /** * here's the magic - * because reservations are aligned to RESERVED_TURF_RESOLUTION, + * because reservations are aligned to TURF_CHUNK_RESOLUTION, * we just have to check the start spots, since we always align to them. * - * bottom-right turfs on reservations align to 0 * RESERVED_TURF_RESOLUTION + 1, 1 * RESERVED_TURF_RESOLUTION + 1, 2 * RESERVED_TURF_RESOLUTION + 1, ... + * bottom-left turfs on reservations align to + * (chunk_x - 1) * TURF_CHUNK_RESOLUTION + 1 + * (chunk_y - 1) * TURF_CHUNK_RESOLUTION + 1 */ for(var/outer_x in 1 to (total_many_wide - how_many_wide + 1)) for(var/outer_y in 1 to (total_many_high - how_many_high + 1)) var/passing = TRUE for(var/inner_x in outer_x to outer_x + how_many_wide - 1) for(var/inner_y in outer_y to outer_y + how_many_high - 1) - var/turf/checking = locate(1 + RESERVED_TURF_RESOLUTION * (inner_x - 1), 1 + RESERVED_TURF_RESOLUTION * (inner_y - 1), level_index) + var/turf/checking = locate( + 1 + (inner_x - 1) * TURF_CHUNK_RESOLUTION, + 1 + (inner_y - 1) * TURF_CHUNK_RESOLUTION, + level_index, + ) if(!(checking.turf_flags & UNUSED_RESERVATION_TURF)) passing = FALSE break @@ -85,7 +116,7 @@ break if(!passing) continue - BL = locate(1 + RESERVED_TURF_RESOLUTION * (outer_x - 1), 1 + RESERVED_TURF_RESOLUTION * (outer_y - 1), level_index) + BL = locate(1 + TURF_CHUNK_RESOLUTION * (outer_x - 1), 1 + TURF_CHUNK_RESOLUTION * (outer_y - 1), level_index) TR = locate(BL.x + width - 1, BL.y + height - 1, BL.z) final = block(BL, TR) found_a_spot = TRUE @@ -116,4 +147,22 @@ allocated = TRUE SSmapping.reservation_blocking_op = FALSE SSmapping.reservations += src + + // register in lookup + ASSERT(bottom_left_coords[3] == top_right_coords[3]) // just to make sure assumptions made at time of writing are still true + src.spatial_bl_x = ceil(bottom_left_coords[1] / TURF_CHUNK_RESOLUTION) + src.spatial_bl_y = ceil(bottom_left_coords[2] / TURF_CHUNK_RESOLUTION) + src.spatial_tr_x = ceil(top_right_coords[1] / TURF_CHUNK_RESOLUTION) + src.spatial_tr_y = ceil(top_right_coords[2] / TURF_CHUNK_RESOLUTION) + src.spatial_z = bottom_left_coords[3] + var/spatial_width = ceil(world.maxx / TURF_CHUNK_RESOLUTION) + var/list/spatial_lookup = SSmapping.reservation_spatial_lookups[spatial_z] + for(var/spatial_x in src.spatial_bl_x to src.spatial_tr_x) + for(var/spatial_y in src.spatial_bl_y to src.spatial_tr_y) + var/index = spatial_x + (spatial_y - 1) * spatial_width + if(spatial_lookup[index]) + stack_trace("index [index] wasn't null, what happened?") + continue + spatial_lookup[index] = src + return TRUE