diff --git a/code/__DEFINES/mapping/system.dm b/code/__DEFINES/mapping/system.dm index b713e17b37c..ca87cd22d3f 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 7a99d909631..af4adad5009 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 a51e2eb4004..516ddad82c7 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 69184c8f2e4..2440f7692b1 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 24891485a7f..640cbcaa0ff 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 9d738f97bb3..fdfe31813e9 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 eb64267d8be..a549c642d1a 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 0e043d4ffd3..e13a0da17f4 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