Files
Bubberstation/code/controllers/subsystem/spatial_gridmap.dm
Kylerace 8f0df7816b (code bounty) The tram is now unstoppably powerful. it cannot be stopped, it cannot be slowed, it cannot be reasoned with. YOU HAVE NO IDEA HOW READY YOU ARE (#66657)
ever see the tram take 10 milliseconds per movement to move 2100 objects? now you have
https://user-images.githubusercontent.com/15794172/166198184-8bab93bd-f584-4269-9ed1-6aee746f8f3c.mp4
About The Pull Request

fixes #66887

done for the code bounty posted by @MMMiracles to optimize the tram so that it can be sped up. the tram is now twice as fast, firing every tick instead of every 2 ticks. and is now around 10x cheaper to move. also adds support for multiz trams, as in trams that span multiple z levels.

the tram on master takes around 10-15 milliseconds per movement with nothing on it other than its starting contents. why is this? because the tram is the canary in the coal mines when it comes to movement code, which is normally expensive as fuck. the tram does way more work than it needs to, and even finds new ways to slow the game down. I'll walk you through a few of the dumber things the tram currently does and how i fixed them.

    the tram, at absolute minimum, has to move 55 separate industrial_lift platforms once per movement. this means that the tram has to unregister its entered/exited signals 55 times when "the tram" as a singular object is only entering 5 new turfs and exiting 5 old turfs every movement, this means that each of the 55 platforms calculates their own destination turfs and checks their contents every movement. The biggest single optimization in this pr was that I made the tram into a single 5x11 multitile object and made it only do entering/exiting checks on the 5 new and 5 old turfs in each movement.
    way too many of the default tram contents are expensive to move for something that has to move a lot. fun fact, did you know that the walls on the tram have opacity? do you know what opacity does for movables? it makes them recalculate static lighting every time they move. did you know that the tram, this entire time, was taking JUST as much time spamming SSlighting updates as it was spending time in SStramprocess? well it is! now it doesnt do that, the walls are transparent. also, every window and every grille on the tram had the atmos_sensitive element applied to them which then added connect_loc to them, causing them to update signals every movement. that is also dumb and i got rid of that with snowflake overrides. Now we must take care to not add things that sneakily register to Moved() or the moved signal to the roundstart tram, because that is dumb, and the relative utility of simulating objects that should normally shatter due to heat and conduct heat from the atmosphere is far less than the cost of moving them, for this one object.
    all tram contents physically Entered() and Exited() their destination and old turfs every movement, even though because they are on a tram they literally do not interact with the turf, the tram does. also, any objects that use connect_loc or connect_loc behalf that are on the same point on the tram also interact with each other because of this. now all contents of the tram act as if theyre being abstract_move()'d to their destination so that (almost) nothing thats in the destination turf or the exit turf can react to the event of "something laying on the tram is moving over you". the rare things that DO need to know what is physically entering or exiting their turf regardless of whether theyre interacting with the ground can register to the abstract entered and exited signals which are now always sent.
    many of the things hooked into Moved(), whether it be overrides of Moved() itself, or handlers for the moved signal, add up to a LOT of processing time. especially for humans. now ive gotten rid of a lot of it, mostly for the tram but also for normal movement. i made footsteps (a significant portion of human movement cost) not do any work if the human themselves didnt do the movement. i optimized has_gravity() a fair amount, and then realized that since everything on the tram isnt changing momentum, i didnt actually need to check gravity for the purposes of drifting (newtonian_move() was taking a significant portion of the cost of movement at some points along the development process). so now it simply doesnt call newtonian_move() for movements that dont represent a change in momentum (by default all movements do).

also i put effort into 1. better organizing tram/lift code so that most of it is inside of a dedicated modules folder instead of scattered around 5 generic folders and 2. moved a lot of behavior from lift platforms themselves into their lift_master_datum since ideally the platforms would just handle moving themselves, while any behavior involving the entire lift such as "move to destination" and "blow up" would be handled by the lift_master_datum.

also
https://user-images.githubusercontent.com/15794172/166220129-ff2ea344-442f-4e3e-94f0-ec58ab438563.mp4
multiz tram (this just adds the capability to map it like this, no tram does this)
Actual Performance Differences

to benchmark this, i added a world.Profile(PROFILER_START) and world.Profile(PROFILER_START) to the tram moving, so that it generates a profiler output of all tram movement without any unrelated procs being recorded (except for world.Profile() overhead). this made it a lot easier to quantify what was slowing down both the tram and movement in general. and i did 3 types of tests on both master and my branch.

also i should note that i sped up the "master" tram test to move once per tick as well, simply because the normal movement speed seems unbearably slow now. so all recorded videos are done at twice the speed of the real tram on master. this doesnt affect the main thing i was trying to measure: cost for each movement.

the first test was the base tram, containing only my player mob and the movables starting on the tram roundstart. on master, this takes around 13 milliseconds or so on my computer (which is pretty close to what it takes on the servers), on this branch, it takes between 0.9-1.3 milliseconds.

ALSO in these benchmarks youll see that tram/proc/travel() will vary significantly between the master and optimized branches. this is 100% because there are 55 times more platforms moving on master compared to the master branch, and thus 55x more calls to this proc. every test was recorded with the exact same amount of distance moved

here are the master and optimized benchmark text files:
master
master base tram.txt
https://user-images.githubusercontent.com/15794172/166210149-f118683d-6f6d-4dfb-b9e4-14f17b26aad8.mp4
also this shows the increased SSlighting usage resulting from the tram on master spamming updates, which doesnt happen on the optimized branch

optimized
optimization base tram.txt
https://user-images.githubusercontent.com/15794172/166206280-cd849aaa-ed3b-4e2f-b741-b8a5726091a9.mp4

the second test is meant to benchmark the best case scaling cost of moving objects, where nothing extra is registered to movement besides the bare minimum stuff on the /atom/movable level. Each of the open tiles of the tram had 1 bluespace rped filled with parts dumped onto it, to the point that the tram in total was moving 2100 objects. the vast majority of these objects did nothing special in movement so they serve as a good base case. only slightly off due to the rped's registering to movement.

on master, this test takes over 100 milliseconds per movement
master 2000 obj's.txt
https://user-images.githubusercontent.com/15794172/166210560-f4de620d-7dc6-4dbd-8b61-4a48149af707.mp4

when optimized, about 10 milliseconds per movement
https://user-images.githubusercontent.com/15794172/166208654-bc10086b-bbfc-49fa-9987-d7558109cc1d.mp4
optimization 2000 obj's.txt

the third test is 300 humans spawned onto the tram, meant to test all the shit added on to movement cost for humans/carbons. in retrospect this test is actually way too biased in favor of my optimizations since the humans are all in only 3 tiles, so all 100 humans on a tile are reacting to the other 99 humans movements, which wouldnt be as bad if they were distributed across 20 tiles like in the second test. so dont read into this one too hard.

on master, this test takes 200 milliseconds
master 300 catgirls.txt

when optimized, this takes about 13-14 milliseconds.
optimization 300 catgirls on ram ranch.txt
Why It's Good For The Game

the tram is literally 10x cheaper to move. and the code is better organized.
currently on master the tram is as fast as running speed, meaning it has no real relative utility compared to just running the tracks (except for the added safety of not having to risk being ran over by the tram). now the tram of which we have an entire map based around can be used to its full potential.

also, has some fixes to things on the tram reacting to movement. for example on master if you are standing on a tram tile that contains a banana and the TRAM moves, you will slip if the banana was in that spot before you (not if you were there first however). this is because the banana has no concept of relative movement, you and it are in the same reference frame but the banana, which failed highschool physics, believes you to have moved onto it and thus subjected you to the humiliation of an unjust slipping. now since tram contents that dont register to abstract entered/exited cannot know about other tram contents on the same tile during a movement, this cannot happen.

also, you no longer make footstep sounds when the tram moves you over a floor
TODO

mainly opened it now so i can create a stopping point and attend to my other now staling prs, we're at a state of functionality far enough to start testmerging it anyways.

add a better way for admins to be notified of the tram overloading the server if someone purposefully stuffs it with as much shit as they can, and for admins to clear said shit.
automatically slow down the tram if SStramprocess takes over like, 10 milliseconds complete. the tram still cant really check tick and yield without introducing logic holes, so making sure it doesnt take half of the tick every tick is important
go over my code to catch dumb shit i forgot about, there always is for these kinds of refactors because im very messy
remove the area based forced_gravity optimization its not worth figuring out why it doesnt work
fix the inevitable merge conflict with master lol
create an icon for the tram_tunnel area type i made so that objects on the tram dont have to enter and exit areas twice in a cross-station traversal

    add an easy way to vv tram lethality for mobs/things being hit by it. its an easy target in another thing i already wanted to do: a reinforced concept of shared variables from any particular tram platform and the entire tram itself. admins should be able to slow down the tram by vv'ing one platform and have it apply to the entire tram for example.

Changelog

cl
balance: the tram is now twice as fast, pray it doesnt get any faster (it cant without raising world fps)
performance: the tram is now about 10 times cheaper to move for the server
add: mappers can now create trams with multiple z levels
code: industrial_lift's now have more of their behavior pertaining to "the entire lift" being handled by their lift_master_datum as opposed to belonging to a random platform on the lift.
/cl
2022-06-24 13:42:09 +12:00

760 lines
35 KiB
Plaintext

///the subsystem creates this many [/mob/oranges_ear] mob instances during init. allocations that require more than this create more.
#define NUMBER_OF_PREGENERATED_ORANGES_EARS 2500
/**
* # Spatial Grid Cell
*
* used by [/datum/controller/subsystem/spatial_grid] to cover every z level so that the coordinates of every turf in the world corresponds to one of these in
* the subsystems list of grid cells by z level. each one of these contains content lists holding all atoms meeting a certain criteria that is in our borders.
* these datums shouldnt have significant behavior, they should just hold data. the lists are filled and emptied by the subsystem.
*/
/datum/spatial_grid_cell
///our x index in the list of cells. this is our index inside of our row list
var/cell_x
///our y index in the list of cells. this is the index of our row list inside of our z level grid
var/cell_y
///which z level we belong to, corresponding to the index of our gridmap in SSspatial_grid.grids_by_z_level
var/cell_z
//every data point in a grid cell is separated by usecase
//when empty, the contents lists of these grid cell datums are just references to a dummy list from SSspatial_grid
//this is meant to allow a great compromise between memory usage and speed.
//now orthogonal_range_search() doesnt need to check if the list is null and each empty list is taking 12 bytes instead of 24
//the only downside is that it needs to be switched over to a new list when it goes from 0 contents to > 0 contents and switched back on the opposite case
///every hearing sensitive movable inside this cell
var/list/hearing_contents
///every client possessed mob inside this cell
var/list/client_contents
///every atmos machine inside this cell
var/list/atmos_contents
/datum/spatial_grid_cell/New(cell_x, cell_y, cell_z)
. = ..()
src.cell_x = cell_x
src.cell_y = cell_y
src.cell_z = cell_z
//cache for sanic speed (lists are references anyways)
var/list/dummy_list = SSspatial_grid.dummy_list
if(length(dummy_list))
dummy_list.Cut()
stack_trace("SSspatial_grid.dummy_list had something inserted into it at some point! this is a problem as it is supposed to stay empty")
hearing_contents = dummy_list
client_contents = dummy_list
atmos_contents = dummy_list
/datum/spatial_grid_cell/Destroy(force, ...)
if(force)//the response to someone trying to qdel this is a right proper fuck you
stack_trace("dont try to destroy spatial grid cells without a good reason. if you need to do it use force")
return
. = ..()
/**
* # Spatial Grid
*
* a gamewide grid of spatial_grid_cell datums, each "covering" [SPATIAL_GRID_CELLSIZE] ^ 2 turfs.
* each spatial_grid_cell datum stores information about what is inside its covered area, so that searches through that area dont have to literally search
* through all turfs themselves to know what is within it since view() calls are expensive, and so is iterating through stuff you dont want.
* this allows you to only go through lists of what you want very cheaply.
*
* you can also register to objects entering and leaving a spatial cell, this allows you to do things like stay idle until a player enters, so you wont
* have to use expensive view() calls or iteratite over the global list of players and call get_dist() on every one. which is fineish for a few things, but is
* k * n operations for k objects iterating through n players.
*
* currently this system is only designed for searching for relatively uncommon things, small subsets of /atom/movable.
* dont add stupid shit to the cells please, keep the information that the cells store to things that need to be searched for often
*
* The system currently implements two different "classes" of spatial type
*
* The first exists to support important_recursive_contents.
* So if a client is inside a locker and the locker crosses a boundary, you'll still get a signal from the spatial grid.
* These types are [SPATIAL_GRID_CONTENTS_TYPE_HEARING] and [SPATIAL_GRID_CONTENTS_TYPE_CLIENTS]
*
* The second pattern is more paired down, and supports more wide use.
* Rather then the object and anything the object is in being sensitive, it's limited to just the object itself
* Currently only [SPATIAL_GRID_CONTENTS_TYPE_ATMOS] uses this pattern. This is because it's far more common, and so worth optimizing
*
*/
SUBSYSTEM_DEF(spatial_grid)
can_fire = FALSE
init_order = INIT_ORDER_SPATIAL_GRID
name = "Spatial Grid"
///list of the spatial_grid_cell datums per z level, arranged in the order of y index then x index
var/list/grids_by_z_level = list()
///everything that spawns before us is added to this list until we initialize
var/list/waiting_to_add_by_type = list(SPATIAL_GRID_CONTENTS_TYPE_HEARING = list(), SPATIAL_GRID_CONTENTS_TYPE_CLIENTS = list(), SPATIAL_GRID_CONTENTS_TYPE_ATMOS = list())
///associative list of the form: movable.spatial_grid_key (string) -> inner list of spatial grid types for that key.
///inner lists contain contents channel types such as SPATIAL_GRID_CONTENTS_TYPE_HEARING etc.
///we use this to make adding to a cell static cost, and to save on memory
var/list/spatial_grid_categories = list()
var/cells_on_x_axis = 0
var/cells_on_y_axis = 0
///empty spatial grid cell content lists are just a reference to this instead of a standalone list to save memory without needed to check if its null when iterating
var/list/dummy_list = list()
///list of all of /mob/oranges_ear instances we have pregenerated for view() iteration speedup
var/list/mob/oranges_ear/pregenerated_oranges_ears = list()
///how many pregenerated /mob/oranges_ear instances currently exist. this should hopefully never exceed its starting value
var/number_of_oranges_ears = NUMBER_OF_PREGENERATED_ORANGES_EARS
/datum/controller/subsystem/spatial_grid/Initialize(start_timeofday)
. = ..()
cells_on_x_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxx)
cells_on_y_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxy)
for(var/datum/space_level/z_level as anything in SSmapping.z_list)
propogate_spatial_grid_to_new_z(null, z_level)
CHECK_TICK
//go through the pre init queue for anything waiting to be let in the grid
for(var/channel_type in waiting_to_add_by_type)
for(var/atom/movable/movable as anything in waiting_to_add_by_type[channel_type])
var/turf/movable_turf = get_turf(movable)
if(movable_turf)
enter_cell(movable, movable_turf)
UnregisterSignal(movable, COMSIG_PARENT_QDELETING)
waiting_to_add_by_type[channel_type] -= movable
pregenerate_more_oranges_ears(NUMBER_OF_PREGENERATED_ORANGES_EARS)
RegisterSignal(SSdcs, COMSIG_GLOB_NEW_Z, .proc/propogate_spatial_grid_to_new_z)
RegisterSignal(SSdcs, COMSIG_GLOB_EXPANDED_WORLD_BOUNDS, .proc/after_world_bounds_expanded)
///add a movable to the pre init queue for whichever type is specified so that when the subsystem initializes they get added to the grid
/datum/controller/subsystem/spatial_grid/proc/enter_pre_init_queue(atom/movable/waiting_movable, type)
RegisterSignal(waiting_movable, COMSIG_PARENT_QDELETING, .proc/queued_item_deleted, override = TRUE)
//override because something can enter the queue for two different types but that is done through unrelated procs that shouldnt know about eachother
waiting_to_add_by_type[type] += waiting_movable
///removes an initialized and probably deleted movable from our pre init queue before we're initialized
/datum/controller/subsystem/spatial_grid/proc/remove_from_pre_init_queue(atom/movable/movable_to_remove, exclusive_type)
if(exclusive_type)
waiting_to_add_by_type[exclusive_type] -= movable_to_remove
var/waiting_movable_is_in_other_queues = FALSE//we need to check if this movable is inside the other queues
for(var/type in waiting_to_add_by_type)
if(movable_to_remove in waiting_to_add_by_type[type])
waiting_movable_is_in_other_queues = TRUE
if(!waiting_movable_is_in_other_queues)
UnregisterSignal(movable_to_remove, COMSIG_PARENT_QDELETING)
return
UnregisterSignal(movable_to_remove, COMSIG_PARENT_QDELETING)
for(var/type in waiting_to_add_by_type)
waiting_to_add_by_type[type] -= movable_to_remove
///if a movable is inside our pre init queue before we're initialized and it gets deleted we need to remove that reference with this proc
/datum/controller/subsystem/spatial_grid/proc/queued_item_deleted(atom/movable/movable_being_deleted)
SIGNAL_HANDLER
remove_from_pre_init_queue(movable_being_deleted, null)
///creates the spatial grid for a new z level
/datum/controller/subsystem/spatial_grid/proc/propogate_spatial_grid_to_new_z(datum/controller/subsystem/processing/dcs/fucking_dcs, datum/space_level/z_level)
SIGNAL_HANDLER
var/list/new_cell_grid = list()
grids_by_z_level += list(new_cell_grid)
for(var/y in 1 to cells_on_y_axis)
new_cell_grid += list(list())
for(var/x in 1 to cells_on_x_axis)
var/datum/spatial_grid_cell/cell = new(x, y, z_level.z_value)
new_cell_grid[y] += cell
///adds cells to the grid for every z level when world.maxx or world.maxy is expanded after this subsystem is initialized. hopefully this is never needed.
///because i never tested this.
/datum/controller/subsystem/spatial_grid/proc/after_world_bounds_expanded(datum/controller/subsystem/processing/dcs/fucking_dcs, has_expanded_world_maxx, has_expanded_world_maxy)
SIGNAL_HANDLER
var/old_x_axis = cells_on_x_axis
var/old_y_axis = cells_on_y_axis
cells_on_x_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxx)
cells_on_y_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxy)
for(var/z_level in 1 to length(grids_by_z_level))
var/list/z_level_gridmap = grids_by_z_level[z_level]
for(var/cell_row_for_expanded_y_axis in 1 to cells_on_y_axis)
if(cell_row_for_expanded_y_axis > old_y_axis)//we are past the old length of the number of rows, so add to the list
z_level_gridmap += list(list())
//now we know theres a row at this position, so add cells to it that need to be added and update the ones that already exist
var/list/cell_row = z_level_gridmap[cell_row_for_expanded_y_axis]
for(var/grid_cell_for_expanded_x_axis in 1 to cells_on_x_axis)
if(grid_cell_for_expanded_x_axis > old_x_axis)
var/datum/spatial_grid_cell/new_cell_inserted = new(grid_cell_for_expanded_x_axis, cell_row_for_expanded_y_axis, z_level)
cell_row += new_cell_inserted
continue
//now we know the cell index we're at contains an already existing cell that needs its x and y values updated
var/datum/spatial_grid_cell/old_cell_that_needs_updating = cell_row[grid_cell_for_expanded_x_axis]
old_cell_that_needs_updating.cell_x = grid_cell_for_expanded_x_axis
old_cell_that_needs_updating.cell_y = cell_row_for_expanded_y_axis
///the left or bottom side index of a box composed of spatial grid cells with the given actual center x or y coordinate
#define BOUNDING_BOX_MIN(center_coord) max(GET_SPATIAL_INDEX(center_coord - range), 1)
///the right or upper side index of a box composed of spatial grid cells with the given center x or y coordinate.
///outputted value cant exceed the number of cells on that axis
#define BOUNDING_BOX_MAX(center_coord, axis_size) min(GET_SPATIAL_INDEX(center_coord + range), axis_size)
/**
* https://en.wikipedia.org/wiki/Range_searching#Orthogonal_range_searching
*
* searches through the grid cells intersecting a rectangular search space (with sides of length 2 * range) then returns all contents of type inside them.
* much faster than iterating through view() to find all of what you want.
*
* this does NOT return things only in range distance from center! the search space is a square not a circle, if you want only things in a certain distance
* then you need to filter that yourself
*
* * center - the atom that is the center of the searched circle
* * type - the type of grid contents you are looking for, see __DEFINES/spatial_grid.dm
* * range - the bigger this is, the more spatial grid cells the search space intersects
*/
/datum/controller/subsystem/spatial_grid/proc/orthogonal_range_search(atom/center, type, range)
var/turf/center_turf = get_turf(center)
var/center_x = center_turf.x//used inside the macros
var/center_y = center_turf.y
. = list()
//technically THIS list only contains lists, but inside those lists are grid cell datums and we can go without a SINGLE var init if we do this
var/list/datum/spatial_grid_cell/grid_level = grids_by_z_level[center_turf.z]
switch(type)
if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS)
for(var/row in BOUNDING_BOX_MIN(center_y) to BOUNDING_BOX_MAX(center_y, cells_on_y_axis))
for(var/x_index in BOUNDING_BOX_MIN(center_x) to BOUNDING_BOX_MAX(center_x, cells_on_x_axis))
. += grid_level[row][x_index].client_contents
if(SPATIAL_GRID_CONTENTS_TYPE_HEARING)
for(var/row in BOUNDING_BOX_MIN(center_y) to BOUNDING_BOX_MAX(center_y, cells_on_y_axis))
for(var/x_index in BOUNDING_BOX_MIN(center_x) to BOUNDING_BOX_MAX(center_x, cells_on_x_axis))
. += grid_level[row][x_index].hearing_contents
if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS)
for(var/row in BOUNDING_BOX_MIN(center_y) to BOUNDING_BOX_MAX(center_y, cells_on_y_axis))
for(var/x_index in BOUNDING_BOX_MIN(center_x) to BOUNDING_BOX_MAX(center_x, cells_on_x_axis))
. += grid_level[row][x_index].atmos_contents
return .
///get the grid cell encomapassing targets coordinates
/datum/controller/subsystem/spatial_grid/proc/get_cell_of(atom/target)
var/turf/target_turf = get_turf(target)
if(!target_turf)
return
return grids_by_z_level[target_turf.z][GET_SPATIAL_INDEX(target_turf.y)][GET_SPATIAL_INDEX(target_turf.x)]
///get all grid cells intersecting the bounding box around center with sides of length 2 * range
/datum/controller/subsystem/spatial_grid/proc/get_cells_in_range(atom/center, range)
return get_cells_in_bounds(center, range, range)
///get all grid cells intersecting the bounding box around center with sides of length (2 * range_x, 2 * range_y)
/datum/controller/subsystem/spatial_grid/proc/get_cells_in_bounds(atom/center, range_x, range_y)
var/turf/center_turf = get_turf(center)
var/center_x = center_turf.x
var/center_y = center_turf.y
var/list/intersecting_grid_cells = list()
//the minimum x and y cell indexes to test
var/min_x = max(GET_SPATIAL_INDEX(center_x - range_x), 1)
var/min_y = max(GET_SPATIAL_INDEX(center_y - range_y), 1)//calculating these indices only takes around 2 microseconds
//the maximum x and y cell indexes to test
var/max_x = min(GET_SPATIAL_INDEX(center_x + range_x), cells_on_x_axis)
var/max_y = min(GET_SPATIAL_INDEX(center_y + range_y), cells_on_y_axis)
var/list/grid_level = grids_by_z_level[center_turf.z]
for(var/row in min_y to max_y)
var/list/grid_row = grid_level[row]
for(var/x_index in min_x to max_x)
intersecting_grid_cells += grid_row[x_index]
return intersecting_grid_cells
/// Adds grid awareness to the passed in atom, of the passed in type
/// Basically, when this atom moves between grids, it wants to have enter/exit cell called on it
/datum/controller/subsystem/spatial_grid/proc/add_grid_awareness(atom/movable/add_to, type)
// We need to ensure we have a new list reference, to build our new key out of
var/list/current_list = spatial_grid_categories[add_to.spatial_grid_key]
if(current_list)
current_list = current_list.Copy()
else
current_list = list()
// Now we do a binary insert, to ensure it's sorted (don't wanna overcache)
BINARY_INSERT_DEFINE(type, current_list, SORT_VAR_NO_TYPE, type, SORT_COMPARE_DIRECTLY, COMPARE_KEY)
update_grid_awareness(add_to, current_list)
/// Removes grid awareness from the passed in atom, of the passed in type
/datum/controller/subsystem/spatial_grid/proc/remove_grid_awareness(atom/movable/remove_from, type)
// We need to ensure we have a new list reference, to build our new key out of
var/list/current_list = spatial_grid_categories[remove_from.spatial_grid_key]
if(current_list)
current_list = current_list.Copy()
else
current_list = list()
current_list -= type
update_grid_awareness(remove_from, current_list)
/// Alerts the atom's current cell that it wishes to be treated as a member
/// This functionally amounts to "hey, I was recently made aware by [add_grid_awareness], please insert me into my current cell"
/datum/controller/subsystem/spatial_grid/proc/add_grid_membership(atom/movable/add_to, turf/target_turf, type)
if(!target_turf)
return
if(initialized)
add_single_type(add_to, target_turf, type)
else //SSspatial_grid isnt init'd yet, add ourselves to the queue
enter_pre_init_queue(add_to, type)
/// Removes grid membership from the passed in atom, of the passed in type
/datum/controller/subsystem/spatial_grid/proc/remove_grid_membership(atom/movable/remove_from, turf/target_turf, type)
if(!target_turf)
return
if(initialized)
remove_single_type(remove_from, target_turf, type)
else //SSspatial_grid isnt init'd yet, remove ourselves from the queue
remove_from_pre_init_queue(remove_from, type)
/// Updates the string that atoms hold that stores their grid awareness
/// We will use it to key into their spatial grid categories later
/datum/controller/subsystem/spatial_grid/proc/update_grid_awareness(atom/movable/update, list/new_list)
// We locally store a stringified version of the list, to prevent people trying to mutate it
update.spatial_grid_key = new_list.Join("-")
// Ensure the global representation is cached
if(!spatial_grid_categories[update.spatial_grid_key])
spatial_grid_categories[update.spatial_grid_key] = new_list
///find the spatial map cell that target belongs to, then add the target to it, as its type prefers.
///make sure to provide the turf new_target is "in"
/datum/controller/subsystem/spatial_grid/proc/enter_cell(atom/movable/new_target, turf/target_turf)
if(!initialized)
return
if(QDELETED(new_target))
CRASH("qdeleted or null target trying to enter the spatial grid!")
if(!target_turf || !new_target.spatial_grid_key)
CRASH("null turf loc or a new_target that doesn't support it trying to enter the spatial grid!")
var/x_index = GET_SPATIAL_INDEX(target_turf.x)
var/y_index = GET_SPATIAL_INDEX(target_turf.y)
var/z_index = target_turf.z
var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index]
for(var/type in spatial_grid_categories[new_target.spatial_grid_key])
switch(type)
if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS)
var/list/new_target_contents = new_target.important_recursive_contents //cache for sanic speeds (lists are references anyways)
GRID_CELL_SET(intersecting_cell.client_contents, new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS])
SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS])
if(SPATIAL_GRID_CONTENTS_TYPE_HEARING)
var/list/new_target_contents = new_target.important_recursive_contents
GRID_CELL_SET(intersecting_cell.hearing_contents, new_target.important_recursive_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING])
SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_HEARING), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING])
if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS)
GRID_CELL_SET(intersecting_cell.atmos_contents, new_target)
SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_ATMOS), new_target)
///acts like enter_cell() but only adds the target to a specified type of grid cell contents list
/datum/controller/subsystem/spatial_grid/proc/add_single_type(atom/movable/new_target, turf/target_turf, exclusive_type)
if(!initialized)
return
if(QDELETED(new_target))
CRASH("qdeleted or null target trying to enter the spatial grid!")
if(!target_turf || !(exclusive_type in spatial_grid_categories[new_target.spatial_grid_key]))
CRASH("null turf loc or a new_target that doesn't support it trying to enter the spatial grid as a [exclusive_type]!")
var/x_index = GET_SPATIAL_INDEX(target_turf.x)
var/y_index = GET_SPATIAL_INDEX(target_turf.y)
var/z_index = target_turf.z
var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index]
switch(exclusive_type)
if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS)
var/list/new_target_contents = new_target.important_recursive_contents //cache for sanic speeds (lists are references anyways)
GRID_CELL_SET(intersecting_cell.client_contents, new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS])
SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS])
if(SPATIAL_GRID_CONTENTS_TYPE_HEARING)
var/list/new_target_contents = new_target.important_recursive_contents
GRID_CELL_SET(intersecting_cell.hearing_contents, new_target.important_recursive_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING])
SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_HEARING), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING])
if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS)
GRID_CELL_SET(intersecting_cell.atmos_contents, new_target)
SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_ATMOS), new_target)
return intersecting_cell
/**
* find the spatial map cell that target used to belong to, then remove the target (and sometimes it's important_recusive_contents) from it.
* make sure to provide the turf old_target used to be "in"
*
* * old_target - the thing we want to remove from the spatial grid cell
* * target_turf - the turf we use to determine the cell we're removing from
* * exclusive_type - either null or a valid contents channel. if you just want to remove a single type from the grid cell then use this
*/
/datum/controller/subsystem/spatial_grid/proc/exit_cell(atom/movable/old_target, turf/target_turf, exclusive_type)
if(!initialized)
return
if(!target_turf || !old_target.spatial_grid_key)
stack_trace("/datum/controller/subsystem/spatial_grid/proc/exit_cell() was given null arguments or a old_target that doesn't use the spatial grid!")
return FALSE
var/x_index = GET_SPATIAL_INDEX(target_turf.x)
var/y_index = GET_SPATIAL_INDEX(target_turf.y)
var/z_index = target_turf.z
var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index]
for(var/type in spatial_grid_categories[old_target.spatial_grid_key])
switch(type)
if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS)
var/list/old_target_contents = old_target.important_recursive_contents //cache for sanic speeds (lists are references anyways)
GRID_CELL_REMOVE(intersecting_cell.client_contents, old_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS])
SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), old_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS])
if(SPATIAL_GRID_CONTENTS_TYPE_HEARING)
var/list/old_target_contents = old_target.important_recursive_contents //cache for sanic speeds (lists are references anyways)
GRID_CELL_REMOVE(intersecting_cell.hearing_contents, old_target_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING])
SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_HEARING), old_target_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING])
if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS)
GRID_CELL_REMOVE(intersecting_cell.atmos_contents, old_target)
SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_ATMOS), old_target)
return TRUE
///acts like exit_cell() but only removes the target from the specified type of grid cell contents list
/datum/controller/subsystem/spatial_grid/proc/remove_single_type(atom/movable/old_target, turf/target_turf, exclusive_type)
if(!target_turf || !exclusive_type || !old_target.spatial_grid_key)
stack_trace("/datum/controller/subsystem/spatial_grid/proc/remove_single_type() was given null arguments or an old_target that doesn't use the spatial grid!")
return FALSE
if(!(exclusive_type in spatial_grid_categories[old_target.spatial_grid_key]))
return FALSE
var/x_index = GET_SPATIAL_INDEX(target_turf.x)
var/y_index = GET_SPATIAL_INDEX(target_turf.y)
var/z_index = target_turf.z
var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index]
switch(exclusive_type)
if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS)
var/list/old_target_contents = old_target.important_recursive_contents //cache for sanic speeds (lists are references anyways)
GRID_CELL_REMOVE(intersecting_cell.client_contents, old_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS])
SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(exclusive_type), old_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS])
if(SPATIAL_GRID_CONTENTS_TYPE_HEARING)
var/list/old_target_contents = old_target.important_recursive_contents
GRID_CELL_REMOVE(intersecting_cell.hearing_contents, old_target_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING])
SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(exclusive_type), old_target_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING])
if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS)
GRID_CELL_REMOVE(intersecting_cell.atmos_contents, old_target)
SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(exclusive_type), old_target)
return TRUE
///find the cell this movable is associated with and removes it from all lists
/datum/controller/subsystem/spatial_grid/proc/force_remove_from_cell(atom/movable/to_remove, datum/spatial_grid_cell/input_cell)
if(!initialized)
remove_from_pre_init_queue(to_remove)//the spatial grid doesnt exist yet, so just take it out of the queue
return
if(!input_cell)
input_cell = get_cell_of(to_remove)
if(!input_cell)
find_hanging_cell_refs_for_movable(to_remove, TRUE)
return
GRID_CELL_REMOVE(input_cell.client_contents, to_remove)
GRID_CELL_REMOVE(input_cell.hearing_contents, to_remove)
GRID_CELL_REMOVE(input_cell.atmos_contents, to_remove)
///if shit goes south, this will find hanging references for qdeleting movables inside the spatial grid
/datum/controller/subsystem/spatial_grid/proc/find_hanging_cell_refs_for_movable(atom/movable/to_remove, remove_from_cells = TRUE)
var/list/queues_containing_movable = list()
for(var/queue_channel in waiting_to_add_by_type)
var/list/queue_list = waiting_to_add_by_type[queue_channel]
if(to_remove in queue_list)
queues_containing_movable += queue_channel//just add the associative key
if(remove_from_cells)
queue_list -= to_remove
if(!initialized)
return queues_containing_movable
var/list/containing_cells = list()
for(var/list/z_level_grid as anything in grids_by_z_level)
for(var/list/cell_row as anything in z_level_grid)
for(var/datum/spatial_grid_cell/cell as anything in cell_row)
if(to_remove in (cell.hearing_contents | cell.client_contents | cell.atmos_contents))
containing_cells += cell
if(remove_from_cells)
force_remove_from_cell(to_remove, cell)
return containing_cells
///debug proc for checking if a movable is in multiple cells when it shouldnt be (ie always unless multitile entering is implemented)
/atom/proc/find_all_cells_containing(remove_from_cells = FALSE)
var/datum/spatial_grid_cell/real_cell = SSspatial_grid.get_cell_of(src)
var/list/containing_cells = SSspatial_grid.find_hanging_cell_refs_for_movable(src, FALSE, remove_from_cells)
message_admins("[src] is located in the contents of [length(containing_cells)] spatial grid cells")
var/cell_coords = "the following cells contain [src]: "
for(var/datum/spatial_grid_cell/cell as anything in containing_cells)
cell_coords += "([cell.cell_x], [cell.cell_y], [cell.cell_z]), "
message_admins(cell_coords)
message_admins("[src] is supposed to only be contained in the cell at indexes ([real_cell.cell_x], [real_cell.cell_y], [real_cell.cell_z]). but is contained at the cells at [cell_coords]")
///creates number_to_generate new oranges_ear's and adds them to the subsystems list of ears.
///i really fucking hope this never gets called after init :clueless:
/datum/controller/subsystem/spatial_grid/proc/pregenerate_more_oranges_ears(number_to_generate)
for(var/new_ear in 1 to number_to_generate)
pregenerated_oranges_ears += new/mob/oranges_ear(null)
number_of_oranges_ears = length(pregenerated_oranges_ears)
///allocate one [/mob/oranges_ear] mob per turf containing atoms_that_need_ears and give them a reference to every listed atom in their turf.
///if an oranges_ear is allocated to a turf that already has an oranges_ear then the second one fails to allocate (and gives the existing one the atom it was assigned to)
/datum/controller/subsystem/spatial_grid/proc/assign_oranges_ears(list/atoms_that_need_ears)
var/input_length = length(atoms_that_need_ears)
if(input_length > number_of_oranges_ears)
stack_trace("somehow, for some reason, more than the preset generated number of oranges ears was requested. thats fucking [number_of_oranges_ears]. this is not good that should literally never happen")
pregenerate_more_oranges_ears(input_length - number_of_oranges_ears)//im still gonna DO IT but ill complain about it
. = list()
///the next unallocated /mob/oranges_ear that we try to allocate to assigned_atom's turf
var/mob/oranges_ear/current_ear
///the next atom in atoms_that_need_ears an ear assigned to it
var/atom/assigned_atom
///the turf loc of the current assigned_atom. turfs are used to track oranges_ears already assigned to one location so we dont allocate more than one
///because allocating more than one oranges_ear to a given loc wastes view iterations
var/turf/turf_loc
for(var/current_ear_index in 1 to input_length)
assigned_atom = atoms_that_need_ears[current_ear_index]
turf_loc = get_turf(assigned_atom)
if(!turf_loc)
continue
current_ear = pregenerated_oranges_ears[current_ear_index]
if(turf_loc.assigned_oranges_ear)
turf_loc.assigned_oranges_ear.references += assigned_atom
continue //if theres already an oranges_ear mob at assigned_movable's turf we give assigned_movable to it instead and dont allocate ourselves
current_ear.references += assigned_atom
current_ear.loc = turf_loc //normally this is bad, but since this is meant to be as fast as possible we literally just need to exist there for view() to see us
turf_loc.assigned_oranges_ear = current_ear
. += current_ear
///debug proc for finding how full the cells of src's z level are
/atom/proc/find_grid_statistics_for_z_level(insert_clients = 0)
var/raw_clients = 0
var/raw_hearables = 0
var/raw_atmos = 0
var/cells_with_clients = 0
var/cells_with_hearables = 0
var/cells_with_atmos = 0
var/list/client_list = list()
var/list/hearable_list = list()
var/list/atmos_list = list()
var/x_cell_count = world.maxx / SPATIAL_GRID_CELLSIZE
var/y_cell_count = world.maxy / SPATIAL_GRID_CELLSIZE
var/total_cells = x_cell_count ** 2
var/average_clients_per_cell = 0
var/average_hearables_per_cell = 0
var/average_atmos_mech_per_call = 0
var/hearable_min_x = x_cell_count
var/hearable_max_x = 1
var/hearable_min_y = y_cell_count
var/hearable_max_y = 1
var/client_min_x = x_cell_count
var/client_max_x = 1
var/client_min_y = y_cell_count
var/client_max_y = 1
var/atmos_min_x = x_cell_count
var/atmos_max_x = 1
var/atmos_min_y = y_cell_count
var/atmos_max_y = 1
var/list/inserted_clients = list()
if(insert_clients)
var/list/turfs
var/level = SSmapping.get_level(z)
if(is_station_level(level))
turfs = GLOB.station_turfs
else
turfs = block(locate(1,1,z), locate(world.maxx, world.maxy, z))
for(var/client_to_insert in 0 to insert_clients)
var/turf/random_turf = pick(turfs)
var/mob/fake_client = new()
fake_client.important_recursive_contents = list(SPATIAL_GRID_CONTENTS_TYPE_HEARING = list(fake_client), SPATIAL_GRID_CONTENTS_TYPE_CLIENTS = list(fake_client))
fake_client.forceMove(random_turf)
inserted_clients += fake_client
var/list/all_z_level_cells = SSspatial_grid.get_cells_in_range(src, 1000)
for(var/datum/spatial_grid_cell/cell as anything in all_z_level_cells)
var/client_length = length(cell.client_contents)
var/hearable_length = length(cell.hearing_contents)
var/atmos_length = length(cell.atmos_contents)
raw_clients += client_length
raw_hearables += hearable_length
raw_atmos += atmos_length
if(client_length)
cells_with_clients++
client_list += cell.client_contents
if(cell.cell_x < client_min_x)
client_min_x = cell.cell_x
if(cell.cell_x > client_max_x)
client_max_x = cell.cell_x
if(cell.cell_y < client_min_y)
client_min_y = cell.cell_y
if(cell.cell_y > client_max_y)
client_max_y = cell.cell_y
if(hearable_length)
cells_with_hearables++
hearable_list += cell.hearing_contents
if(cell.cell_x < hearable_min_x)
hearable_min_x = cell.cell_x
if(cell.cell_x > hearable_max_x)
hearable_max_x = cell.cell_x
if(cell.cell_y < hearable_min_y)
hearable_min_y = cell.cell_y
if(cell.cell_y > hearable_max_y)
hearable_max_y = cell.cell_y
if(raw_atmos)
cells_with_atmos++
atmos_list += cell.atmos_contents
if(cell.cell_x < atmos_min_x)
atmos_min_x = cell.cell_x
if(cell.cell_x > atmos_max_x)
atmos_max_x = cell.cell_x
if(cell.cell_y < atmos_min_y)
atmos_min_y = cell.cell_y
if(cell.cell_y > atmos_max_y)
atmos_max_y = cell.cell_y
var/total_client_distance = 0
var/total_hearable_distance = 0
var/total_atmos_distance = 0
var/average_client_distance = 0
var/average_hearable_distance = 0
var/average_atmos_distance = 0
for(var/hearable in hearable_list)//n^2 btw
for(var/other_hearable in hearable_list)
if(hearable == other_hearable)
continue
total_hearable_distance += get_dist(hearable, other_hearable)
for(var/client in client_list)//n^2 btw
for(var/other_client in client_list)
if(client == other_client)
continue
total_client_distance += get_dist(client, other_client)
for(var/atmos in atmos_list)//n^2 btw
for(var/other_atmos in atmos_list)
if(atmos == other_atmos)
continue
total_atmos_distance += get_dist(atmos, other_atmos)
if(length(hearable_list))
average_hearable_distance = total_hearable_distance / length(hearable_list)
if(length(client_list))
average_client_distance = total_client_distance / length(client_list)
if(length(atmos_list))
average_atmos_distance = total_atmos_distance / length(atmos_list)
average_clients_per_cell = raw_clients / total_cells
average_hearables_per_cell = raw_hearables / total_cells
average_atmos_mech_per_call = raw_atmos / total_cells
for(var/mob/inserted_client as anything in inserted_clients)
qdel(inserted_client)
message_admins("on z level [z] there are [raw_clients] clients ([insert_clients] of whom are fakes inserted to random station turfs)\
, [raw_hearables] hearables, and [raw_atmos] atmos machines. all of whom are inside the bounding box given by \
clients: ([client_min_x], [client_min_y]) x ([client_max_x], [client_max_y]), \
hearables: ([hearable_min_x], [hearable_min_y]) x ([hearable_max_x], [hearable_max_y]) \
and atmos machines: ([atmos_min_x], [atmos_min_y]) x ([atmos_max_x], [atmos_max_y]), \
on average there are [average_clients_per_cell] clients per cell, [average_hearables_per_cell] hearables per cell, \
and [average_atmos_mech_per_call] per cell, \
[cells_with_clients] cells have clients, [cells_with_hearables] have hearables, and [cells_with_atmos] have atmos machines \
the average client distance is: [average_client_distance], the average hearable_distance is [average_hearable_distance], \
and the average atmos distance is [average_atmos_distance] ")
#undef GRID_CELL_ADD
#undef GRID_CELL_REMOVE
#undef GRID_CELL_SET