Files
Bubberstation/code/modules/mapping/space_management/space_reservation.dm
Kyle Spier-Swenson 8703eac50d split area.contained_turfs up by zlevel, make init 10 seconds faster (#80941)
## About The Pull Request

Situation: areas have a list of all turfs in their area.

Problem: `/area/space` is an area and has a 6 to 7 digit count of turfs
that has to be traversed for every turf we need to remove from it. This
can take multiple byond ticks just to preform this action for a single
space rune

Solution: split the list by zlevel, and only search the right zlevel
list when removing turfs from areas.

replaces `area.get_contained_turfs()` with a few new procs:

* `get_highest_zlevel()` - returns the highest zlevel the area contains
turfs in. useful for use with `get_turfs_by_zlevel`
* `get_turfs_by_zlevel(zlevel)` - returns a list of turfs in the area in
a given zlevel. Useful for code that only cares about a specific zlevel
or changes behavior based on zlevel like lighting init.
* `get_turfs_from_all_zlevels()` - the replacement for
`get_contained_turfs()`, renamed as such so anybody copying/cargo
culting code gets a hint that a zlevel specific version might exist.
Still used in for loops that type checked so byond would do that all at
once
* `get_zlevel_turf_lists()` - returns the area's zlevel lists of lists
but only for non-empty zlevels. very useful for for loops.

The area contents unit test has been rewritten to ensure any improper
data triggers failures or runtimes by not having it use the helpers
above (some of which ensure a list is always returned) and access the
lists directly.
2024-01-18 12:16:12 -05:00

273 lines
9.4 KiB
Plaintext

//Yes, they can only be rectangular.
//Yes, I'm sorry.
/datum/turf_reservation
/// All turfs that we've reserved
var/list/reserved_turfs = list()
/// Turfs around the reservation for cordoning
var/list/cordon_turfs = list()
/// Area of turfs next to the cordon to fill with pre_cordon_area's
var/list/pre_cordon_turfs = list()
/// The width of the reservation
var/width = 0
/// The height of the reservation
var/height = 0
/// The z stack size of the reservation. Note that reservations are ALWAYS reserved from the bottom up
var/z_size = 0
/// List of the bottom left turfs. Indexed by what their z index for this reservation is
var/list/bottom_left_turfs = list()
/// List of the top right turfs. Indexed by what their z index for this reservation is
var/list/top_right_turfs = list()
/// The turf type the reservation is initially made with
var/turf_type = /turf/open/space
///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/open/space/transit
pre_cordon_distance = 7
/datum/turf_reservation/proc/Release()
bottom_left_turfs.Cut()
top_right_turfs.Cut()
var/list/reserved_copy = reserved_turfs.Copy()
SSmapping.used_turfs -= reserved_turfs
reserved_turfs = list()
var/list/cordon_copy = cordon_turfs.Copy()
SSmapping.used_turfs -= cordon_turfs
cordon_turfs = list()
var/release_turfs = reserved_copy + cordon_copy
for(var/turf/reserved_turf as anything in release_turfs)
SEND_SIGNAL(reserved_turf, COMSIG_TURF_RESERVATION_RELEASED, src)
// 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)
/// Attempts to calaculate and store a list of turfs around the reservation for cordoning. Returns whether a valid cordon was calculated
/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 // no space for a cordon here
var/list/possible_turfs = CORNER_OUTLINE(bottom_left, width, height)
// if they're our cordon turfs, accept them
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)
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
/// Actually generates the cordon around the reservation, and marking the cordon turfs as reserved
/datum/turf_reservation/proc/generate_cordon()
for(var/turf/cordon_turf as anything in cordon_turfs)
var/area/misc/cordon/cordon_area = GLOB.areas_by_type[/area/misc/cordon] || new
var/area/old_area = cordon_turf.loc
LISTASSERTLEN(old_area.turfs_to_uncontain_by_zlevel, cordon_turf.z, list())
LISTASSERTLEN(cordon_area.turfs_by_zlevel, cordon_turf.z, list())
old_area.turfs_to_uncontain_by_zlevel[cordon_turf.z] += cordon_turf
cordon_area.turfs_by_zlevel[cordon_turf.z] += cordon_turf
cordon_area.contents += cordon_turf
// Its no longer unused, but its also not "used"
cordon_turf.turf_flags &= ~UNUSED_RESERVATION_TURF
cordon_turf.ChangeTurf(/turf/cordon, /turf/cordon)
SSmapping.unused_turfs["[cordon_turf.z]"] -= cordon_turf
// still gets linked to us though
SSmapping.used_turfs[cordon_turf] = src
//swap the area with the pre-cordoning area
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)
..()
RegisterSignal(pre_cordon_turf, COMSIG_ATOM_ENTERED, PROC_REF(space_dump_soft))
/datum/turf_reservation/transit/stop_repel(turf/pre_cordon_turf)
..()
UnregisterSignal(pre_cordon_turf, COMSIG_ATOM_ENTERED)
/datum/turf_reservation/transit/proc/space_dump(atom/source, atom/movable/enterer)
SIGNAL_HANDLER
dump_in_space(enterer)
///Only dump if we don't have the hyperspace cordon movement exemption trait
/datum/turf_reservation/transit/proc/space_dump_soft(atom/source, atom/movable/enterer)
SIGNAL_HANDLER
if(!HAS_TRAIT(enterer, TRAIT_FREE_HYPERSPACE_SOFTCORDON_MOVEMENT))
space_dump(source, enterer)
/// 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/avail = SSmapping.unused_turfs["[zlevel]"]
var/turf/BL
var/turf/TR
var/list/turf/final = list()
var/passing = FALSE
for(var/i in avail)
CHECK_TICK
BL = i
if(!(BL.turf_flags & UNUSED_RESERVATION_TURF))
continue
if(BL.x + width > world.maxx || BL.y + height > world.maxy)
continue
TR = locate(BL.x + width - 1, BL.y + height - 1, BL.z)
if(!(TR.turf_flags & UNUSED_RESERVATION_TURF))
continue
final = block(BL, TR)
if(!final)
continue
passing = TRUE
for(var/I in final)
var/turf/checking = I
if(!(checking.turf_flags & UNUSED_RESERVATION_TURF))
passing = FALSE
break
if(passing) // found a potentially valid area, now try to calculate its cordon
passing = calculate_cordon_turfs(BL, TR)
if(!passing)
continue
break
if(!passing || !istype(BL) || !istype(TR))
return FALSE
for(var/i in final)
var/turf/T = i
reserved_turfs |= T
SSmapping.unused_turfs["[T.z]"] -= T
SSmapping.used_turfs[T] = src
T.turf_flags = (T.turf_flags | RESERVATION_TURF) & ~UNUSED_RESERVATION_TURF
T.ChangeTurf(turf_type, turf_type)
bottom_left_turfs += BL
top_right_turfs += TR
return TRUE
/datum/turf_reservation/proc/reserve(width, height, z_size, z_reservation)
src.z_size = z_size
var/failed_reservation = FALSE
for(var/_ in 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)
var/turf/bottom_left = bottom_left_turfs[z_idx]
var/turf/top_right = top_right_turfs[z_idx]
var/bl_x = bottom_left.x
var/bl_y = bottom_left.y
var/tr_x = top_right.x
var/tr_y = top_right.y
if(target.x < bl_x)
continue
if(target.y < bl_y)
continue
if(target.x > tr_x)
continue
if(target.y > tr_y)
continue
var/list/return_information = list()
return_information["z_idx"] = z_idx
return_information["offset_x"] = target.x - bl_x
return_information["offset_y"] = target.y - bl_y
return return_information
return null
/// Gets the turf below the given target. Returns null if there is no turf below the target
/datum/turf_reservation/proc/get_turf_below(turf/target)
var/list/bounds_info = calculate_turf_bounds_information(target)
if(isnull(bounds_info))
return null
var/z_idx = bounds_info["z_idx"]
// check what z level, if its the max, then there is no turf below
if(z_idx == z_size)
return null
var/offset_x = bounds_info["offset_x"]
var/offset_y = bounds_info["offset_y"]
var/turf/bottom_left = bottom_left_turfs[z_idx + 1]
return locate(bottom_left.x + offset_x, bottom_left.y + offset_y, bottom_left.z)
/// Gets the turf above the given target. Returns null if there is no turf above the target
/datum/turf_reservation/proc/get_turf_above(turf/target)
var/list/bounds_info = calculate_turf_bounds_information(target)
if(isnull(bounds_info))
return null
var/z_idx = bounds_info["z_idx"]
// check what z level, if its the min, then there is no turf above
if(z_idx == 1)
return null
var/offset_x = bounds_info["offset_x"]
var/offset_y = bounds_info["offset_y"]
var/turf/bottom_left = bottom_left_turfs[z_idx - 1]
return locate(bottom_left.x + offset_x, bottom_left.y + offset_y, bottom_left.z)
/datum/turf_reservation/New()
LAZYADD(SSmapping.turf_reservations, src)
/datum/turf_reservation/Destroy()
Release()
LAZYREMOVE(SSmapping.turf_reservations, src)
return ..()