mirror of
https://github.com/Citadel-Station-13/Citadel-Station-13-RP.git
synced 2026-08-28 03:16:48 +01:00
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:
@@ -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!
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user