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.
This commit is contained in:
silicons
2024-06-20 17:26:53 -06:00
committed by GitHub
parent 83bc9a7ebf
commit 7b5c263ae5
8 changed files with 211 additions and 60 deletions
@@ -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!
+22 -4
View File
@@ -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()
+24 -21
View File
@@ -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
/**
@@ -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"
+61 -15
View File
@@ -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()