From cc74d7dcc895e04d34f6f21bad3738ff43d60807 Mon Sep 17 00:00:00 2001 From: Fluffy <65877598+FluffyGhoster@users.noreply.github.com> Date: Mon, 22 Jan 2024 00:29:42 +0100 Subject: [PATCH] Updated spatial gridmap (#18244) * sadf * jhbkhbh * hope * gsda --- aurorastation.dme | 2 +- code/__DEFINES/dcs/signals.dm | 7 - .../dcs/signals/signals_spatial_grid.dm | 6 + code/__DEFINES/lists.dm | 69 -- code/__DEFINES/spatial_gridmap.dm | 93 +-- code/__DEFINES/subsystems.dm | 1 + code/__DEFINES/turfs.dm | 3 + code/__HELPERS/_lists.dm | 236 +++++++ code/__HELPERS/spatial_info.dm | 217 +++--- .../controllers/subsystems/spatial_gridmap.dm | 623 +++++++++++------- code/game/atoms_movable.dm | 179 +++-- code/game/gamemodes/cult/structures/pylon.dm | 4 +- code/game/machinery/hologram.dm | 6 +- code/game/machinery/portable_turret.dm | 2 +- code/game/machinery/stargazer.dm | 2 +- .../game/objects/items/weapons/neutralizer.dm | 2 +- code/game/turfs/turf.dm | 4 + code/modules/mob/death.dm | 3 +- code/modules/mob/living/carbon/brain/brain.dm | 1 - .../living/simple_animal/hostile/hostile.dm | 6 +- code/modules/psionics/abilities/grip.dm | 8 +- code/unit_tests/ss_test.dm | 6 +- .../fluffyghost-updatespatialgrid.yml | 45 ++ 23 files changed, 1004 insertions(+), 521 deletions(-) create mode 100644 code/__DEFINES/dcs/signals/signals_spatial_grid.dm delete mode 100644 code/__DEFINES/lists.dm create mode 100644 html/changelogs/fluffyghost-updatespatialgrid.yml diff --git a/aurorastation.dme b/aurorastation.dme index cb50f227de0..6b33985f82e 100644 --- a/aurorastation.dme +++ b/aurorastation.dme @@ -71,7 +71,6 @@ #include "code\__DEFINES\items_clothing.dm" #include "code\__DEFINES\jobs.dm" #include "code\__DEFINES\lighting.dm" -#include "code\__DEFINES\lists.dm" #include "code\__DEFINES\logging.dm" #include "code\__DEFINES\machinery.dm" #include "code\__DEFINES\materials.dm" @@ -136,6 +135,7 @@ #include "code\__DEFINES\dcs\signals\signals_datum.dm" #include "code\__DEFINES\dcs\signals\signals_global.dm" #include "code\__DEFINES\dcs\signals\signals_record.dm" +#include "code\__DEFINES\dcs\signals\signals_spatial_grid.dm" #include "code\__DEFINES\dcs\signals\signals_subsystem.dm" #include "code\__DEFINES\dcs\signals\signals_atom\signals_atom_main.dm" #include "code\__DEFINES\dcs\signals\signals_atom\signals_atom_x_act.dm" diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index 54c77295201..1cccb694f9e 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -29,13 +29,6 @@ #define HEARING_LANGUAGE 3 #define HEARING_RAW_MESSAGE 4 -//spatial grid signals - -///Called from base of /datum/controller/subsystem/spatial_grid/proc/enter_cell: (/atom/movable) -#define SPATIAL_GRID_CELL_ENTERED(contents_type) "spatial_grid_cell_entered_[contents_type]" -///Called from base of /datum/controller/subsystem/spatial_grid/proc/exit_cell: (/atom/movable) -#define SPATIAL_GRID_CELL_EXITED(contents_type) "spatial_grid_cell_exited_[contents_type]" - // /mob signals #define COMSIG_MOB_EXAMINATE "mob_examinate" #define COMSIG_MOB_FACEDIR "mob_facedir" diff --git a/code/__DEFINES/dcs/signals/signals_spatial_grid.dm b/code/__DEFINES/dcs/signals/signals_spatial_grid.dm new file mode 100644 index 00000000000..82e69dfcdf8 --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_spatial_grid.dm @@ -0,0 +1,6 @@ +//spatial grid signals + +///Called from base of /datum/controller/subsystem/spatial_grid/proc/enter_cell: (/atom/movable) +#define SPATIAL_GRID_CELL_ENTERED(contents_type) "spatial_grid_cell_entered_[contents_type]" +///Called from base of /datum/controller/subsystem/spatial_grid/proc/exit_cell: (/atom/movable) +#define SPATIAL_GRID_CELL_EXITED(contents_type) "spatial_grid_cell_exited_[contents_type]" diff --git a/code/__DEFINES/lists.dm b/code/__DEFINES/lists.dm deleted file mode 100644 index b2ddc928ae0..00000000000 --- a/code/__DEFINES/lists.dm +++ /dev/null @@ -1,69 +0,0 @@ -#define LAZYINITLIST(L) if (!L) L = list() - -#define UNSETEMPTY(L) if (L && !L.len) L = null -#define LAZYREMOVE(L, I) if(L) { L -= I; if(!length(L)) { L = null; } } -#define LAZYADD(L, I) if(!L) { L = list(); } L += I; -#define LAZYACCESS(L, I) (L ? (isnum(I) ? (I > 0 && I <= L.len ? L[I] : null) : L[I]) : null) -#define LAZYINSERT(L, I, X) if(!L) { L = list(); } L.Insert(X, I); -#define LAZYLEN(L) length(L) -#define LAZYCLEARLIST(L) if(L) L.Cut() -#define LAZYSET(L, K, V) if (!L) { L = list(); } L[K] = V; -#define LAZYPICK(L,DEFAULT) (LAZYLEN(L) ? pick(L) : DEFAULT) -#define LAZYISIN(L, I) (L ? (I in L) : FALSE) -#define LAZYDISTINCTADD(L, I) if(!L) { L = list(); } L |= I; -#define LAZYADDASSOCLIST(L, K, V) if(!L) { L = list(); } L[K] += list(V); -#define LAZYREMOVEASSOC(L, K, V) if(L) { if(L[K]) { L[K] -= V; if(!length(L[K])) L -= K; } if(!length(L)) L = null; } -#define LAZYACCESSASSOC(L, I, K) L ? L[I] ? L[I][K] ? L[I][K] : null : null : null -#define LAZYREPLACEKEY(L, K, NK) if(L) { if(L[K]) { L[NK] = L[K] } else {L += NK} L -= K; } - -/// Explicitly set the length of L to NEWLEN, adding nulls or dropping entries. Is the same value as NEWLEN. -#define LIST_RESIZE(L, NEWLEN) ((L).len = (NEWLEN)) - -/// Performs an insertion on the given lazy list with the given key and value. If the value already exists, a new one will not be made. -#define LAZYORASSOCLIST(lazy_list, key, value) \ - LAZYINITLIST(lazy_list); \ - LAZYINITLIST(lazy_list[key]); \ - lazy_list[key] |= value; - -// Shims for some list procs in lists.dm. -#define isemptylist(L) (!LAZYLEN(L)) - -/// Passed into BINARY_INSERT to compare keys -#define COMPARE_KEY __BIN_LIST[__BIN_MID] -/// Passed into BINARY_INSERT to compare values -#define COMPARE_VALUE __BIN_LIST[__BIN_LIST[__BIN_MID]] - -/** - * Binary search sorted insert from TG - * INPUT: Object to be inserted - * LIST: List to insert object into - * TYPECONT: The typepath of the contents of the list - * COMPARE: The object to compare against, usualy the same as INPUT - * COMPARISON: The variable on the objects to compare - * COMPTYPE: How should the values be compared? Either COMPARE_KEY or COMPARE_VALUE. - */ -#define BINARY_INSERT(INPUT, LIST, TYPECONT, COMPARE, COMPARISON, COMPTYPE) \ - do {\ - var/list/__BIN_LIST = LIST;\ - var/__BIN_CTTL = length(__BIN_LIST);\ - if(!__BIN_CTTL) {\ - __BIN_LIST += INPUT;\ - } else {\ - var/__BIN_LEFT = 1;\ - var/__BIN_RIGHT = __BIN_CTTL;\ - var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ - var ##TYPECONT/__BIN_ITEM;\ - while(__BIN_LEFT < __BIN_RIGHT) {\ - __BIN_ITEM = COMPTYPE;\ - if(__BIN_ITEM.##COMPARISON <= COMPARE.##COMPARISON) {\ - __BIN_LEFT = __BIN_MID + 1;\ - } else {\ - __BIN_RIGHT = __BIN_MID;\ - };\ - __BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ - };\ - __BIN_ITEM = COMPTYPE;\ - __BIN_MID = __BIN_ITEM.##COMPARISON > COMPARE.##COMPARISON ? __BIN_MID : __BIN_MID + 1;\ - __BIN_LIST.Insert(__BIN_MID, INPUT);\ - };\ - } while(FALSE) diff --git a/code/__DEFINES/spatial_gridmap.dm b/code/__DEFINES/spatial_gridmap.dm index 04d2c4c5d76..97a6f991539 100644 --- a/code/__DEFINES/spatial_gridmap.dm +++ b/code/__DEFINES/spatial_gridmap.dm @@ -1,54 +1,55 @@ -// Each cell in a spatial_grid is this many turfs in length and width +/// each cell in a spatial_grid is this many turfs in length and width (with world.max(x or y) being 255, 15 of these fit on each side of a z level) #define SPATIAL_GRID_CELLSIZE 17 +/// Takes a coordinate, and spits out the spatial grid index (x or y) it's inside +#define GET_SPATIAL_INDEX(coord) ROUND_UP((coord) / SPATIAL_GRID_CELLSIZE) +/// changes the cell_(x or y) vars on /datum/spatial_grid_cell to the x or y coordinate on the map for the LOWER LEFT CORNER of the grid cell. +/// index is from 1 to SPATIAL_GRID_CELLS_PER_SIDE +#define GRID_INDEX_TO_COORDS(index) ((((index) - 1) * SPATIAL_GRID_CELLSIZE) + 1) +/// number of grid cells per x or y side of all z levels. pass in world.maxx or world.maxy +#define SPATIAL_GRID_CELLS_PER_SIDE(world_bounds) GET_SPATIAL_INDEX(world_bounds) -#define SPATIAL_GRID_CELLS_PER_SIDE(world_bounds) ROUND_UP((world_bounds) / SPATIAL_GRID_CELLSIZE) +//grid contents channels -#define SPATIAL_GRID_CHANNELS 2 - -// grid contents channels - -// Everything that is hearing sensitive is stored in this channel +///everything that is hearing sensitive is stored in this channel #define SPATIAL_GRID_CONTENTS_TYPE_HEARING RECURSIVE_CONTENTS_HEARING_SENSITIVE -// Every movable that has a client in it is stored in this channel +///every movable that has a client in it is stored in this channel #define SPATIAL_GRID_CONTENTS_TYPE_CLIENTS RECURSIVE_CONTENTS_CLIENT_MOBS -// Every /mob is stored in this channel -#define SPATIAL_GRID_CONTENTS_TYPE_TARGETS RECURSIVE_CONTENTS_AI_TARGETS +///all atmos machines are stored in this channel (I'm sorry kyler) +#define SPATIAL_GRID_CONTENTS_TYPE_ATMOS "spatial_grid_contents_type_atmos" -// Whether movable is itself or containing something which should be in one of the spatial grid channels -#define HAS_SPATIAL_GRID_CONTENTS(movable) (movable.important_recursive_contents && \ - (movable.important_recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE] || movable.important_recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS] || movable.important_recursive_contents[RECURSIVE_CONTENTS_AI_TARGETS])) +#define ALL_CONTENTS_OF_CELL(cell) (cell.hearing_contents | cell.client_contents | cell.atmos_contents) +///whether movable is itself or containing something which should be in one of the spatial grid channels. +#define HAS_SPATIAL_GRID_CONTENTS(movable) (movable.spatial_grid_key) -/** - * Checks if two atoms are in line of sight (can see each other) - * - * * RETURN_VALUE - Where to store the result of this check (TRUE if LoS, FALSE otherwise) - * * source - Source atom - * * target - Target atom - * * view_radius - How far the atom can see, usually you want to use `world.view` for the default value - */ -#define SPATIAL_CHECK_LOS(RETURN_VALUE, source, target, view_radius)\ - do{\ - var/turf/source_turf = get_turf(source);\ - var/turf/target_turf = get_turf(target);\ - var/distance = get_dist(source_turf, target_turf);\ - if(distance > view_radius){\ - RETURN_VALUE = FALSE;\ - break;\ - }\ - if(distance < 2){\ - RETURN_VALUE = TRUE;\ - break;\ - }\ - for(var/step_counter in 1 to distance){\ - source_turf = get_step_towards(source_turf, target_turf);\ - if(source_turf == target_turf){\ - RETURN_VALUE = TRUE;\ - break;\ - }\ - if(IS_OPAQUE_TURF(source_turf)){\ - RETURN_VALUE = FALSE;\ - break;\ - }\ - }\ - } while(FALSE) +// macros meant specifically to add/remove movables from the internal lists of /datum/spatial_grid_cell, +// when empty they become references to a single list in SSspatial_grid and when filled they become their own list +// this is to save memory without making them lazylists as that slows down iteration through them +#define GRID_CELL_ADD(cell_contents_list, movable_or_list) \ + if(!length(cell_contents_list)) { \ + cell_contents_list = list(); \ + cell_contents_list += movable_or_list; \ + } else { \ + cell_contents_list += movable_or_list; \ + }; + +#define GRID_CELL_SET(cell_contents_list, movable_or_list) \ + if(!length(cell_contents_list)) { \ + cell_contents_list = list(); \ + cell_contents_list += movable_or_list; \ + } else { \ + cell_contents_list |= movable_or_list; \ + }; + +//dont use these outside of SSspatial_grid's scope use the procs it has for this purpose +#define GRID_CELL_REMOVE(cell_contents_list, movable_or_list) \ + cell_contents_list -= movable_or_list; \ + if(!length(cell_contents_list)) {\ + cell_contents_list = dummy_list; \ + }; + +///remove from every list +#define GRID_CELL_REMOVE_ALL(cell, movable) \ + GRID_CELL_REMOVE(cell.hearing_contents, movable) \ + GRID_CELL_REMOVE(cell.client_contents, movable) \ + GRID_CELL_REMOVE(cell.atmos_contents, movable) diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 08180ee8bbe..7d5a74000a2 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -198,6 +198,7 @@ #define INIT_ORDER_PROFILER 101 #define INIT_ORDER_GARBAGE 99 +#define INIT_ORDER_SPATIAL_GRID 43 // Subsystem fire priority, from lowest to highest priority // If the subsystem isn't listed here it's either DEFAULT or PROCESS (if it's a processing subsystem child) diff --git a/code/__DEFINES/turfs.dm b/code/__DEFINES/turfs.dm index 491fb4fdb49..71b117745a4 100644 --- a/code/__DEFINES/turfs.dm +++ b/code/__DEFINES/turfs.dm @@ -1,5 +1,8 @@ #define IS_OPAQUE_TURF(turf) (turf.opacity || turf.has_opaque_atom) +///Returns all turfs in a zlevel +#define Z_TURFS(ZLEVEL) block(locate(1,1,ZLEVEL), locate(world.maxx, world.maxy, ZLEVEL)) + #define TURF_REMOVE_CROWBAR BITFLAG(1) #define TURF_REMOVE_SCREWDRIVER BITFLAG(2) #define TURF_REMOVE_SHOVEL BITFLAG(3) diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm index 5a23a9fb74b..94cfb4eb50d 100644 --- a/code/__HELPERS/_lists.dm +++ b/code/__HELPERS/_lists.dm @@ -1,3 +1,239 @@ +/* + * Holds procs to help with list operations + * Contains groups: + * Misc + * Sorting + */ + +/* + * Misc + */ + +// Generic listoflist safe add and removal macros: +///If value is a list, wrap it in a list so it can be used with list add/remove operations +#define LIST_VALUE_WRAP_LISTS(value) (islist(value) ? list(value) : value) +///Add an untyped item to a list, taking care to handle list items by wrapping them in a list to remove the footgun +#define UNTYPED_LIST_ADD(list, item) (list += LIST_VALUE_WRAP_LISTS(item)) +///Remove an untyped item to a list, taking care to handle list items by wrapping them in a list to remove the footgun +#define UNTYPED_LIST_REMOVE(list, item) (list -= LIST_VALUE_WRAP_LISTS(item)) + +/* + * ## Lazylists + * + * * What is a lazylist? + * + * True to its name a lazylist is a lazy instantiated list. + * It is a list that is only created when necessary (when it has elements) and is null when empty. + * + * * Why use a lazylist? + * + * Lazylists save memory - an empty list that is never used takes up more memory than just `null`. + * + * * When to use a lazylist? + * + * Lazylists are best used on hot types when making lists that are not always used. + * + * For example, if you were adding a list to all atoms that tracks the names of people who touched it, + * you would want to use a lazylist because most atoms will never be touched by anyone. + * + * * How do I use a lazylist? + * + * A lazylist is just a list you defined as `null` rather than `list()`. + * Then, you use the LAZY* macros to interact with it, which are essentially null-safe ways to interact with a list. + * + * Note that you probably should not be using these macros if your list is not a lazylist. + * This will obfuscate the code and make it a bit harder to read and debug. + * + * Generally speaking you shouldn't be checking if your lazylist is `null` yourself, the macros will do that for you. + * Remember that LAZYLEN (and by extension, length) will return 0 if the list is null. + */ + +///Initialize the lazylist +#define LAZYINITLIST(L) if (!L) { L = list(); } +///If the provided list is empty, set it to null +#define UNSETEMPTY(L) if (L && !length(L)) L = null +///If the provided key -> list is empty, remove it from the list +#define ASSOC_UNSETEMPTY(L, K) if (!length(L[K])) L -= K; +///Like LAZYCOPY - copies an input list if the list has entries, If it doesn't the assigned list is nulled +#define LAZYLISTDUPLICATE(L) (L ? L.Copy() : null ) +///Remove an item from the list, set the list to null if empty +#define LAZYREMOVE(L, I) if(L) { L -= I; if(!length(L)) { L = null; } } +///Add an item to the list, if the list is null it will initialize it +#define LAZYADD(L, I) if(!L) { L = list(); } L += I; +///Add an item to the list if not already present, if the list is null it will initialize it +#define LAZYOR(L, I) if(!L) { L = list(); } L |= I; +///Returns the key of the submitted item in the list +#define LAZYFIND(L, V) (L ? L.Find(V) : 0) +///returns L[I] if L exists and I is a valid index of L, runtimes if L is not a list +#define LAZYACCESS(L, I) (L ? (isnum(I) ? (I > 0 && I <= length(L) ? L[I] : null) : L[I]) : null) +///Sets the item K to the value V, if the list is null it will initialize it +#define LAZYSET(L, K, V) if(!L) { L = list(); } L[K] = V; +///Sets the length of a lazylist +#define LAZYSETLEN(L, V) if (!L) { L = list(); } L.len = V; +///Returns the length of the list +#define LAZYLEN(L) length(L) +///Sets a list to null +#define LAZYNULL(L) L = null +///Adds to the item K the value V, if the list is null it will initialize it +#define LAZYADDASSOC(L, K, V) if(!L) { L = list(); } L[K] += V; +///This is used to add onto lazy assoc list when the value you're adding is a /list/. This one has extra safety over lazyaddassoc because the value could be null (and thus cant be used to += objects) +#define LAZYADDASSOCLIST(L, K, V) if(!L) { L = list(); } L[K] += list(V); +///Removes the value V from the item K, if the item K is empty will remove it from the list, if the list is empty will set the list to null +#define LAZYREMOVEASSOC(L, K, V) if(L) { if(L[K]) { L[K] -= V; if(!length(L[K])) L -= K; } if(!length(L)) L = null; } +///Accesses an associative list, returns null if nothing is found +#define LAZYACCESSASSOC(L, I, K) L ? L[I] ? L[I][K] ? L[I][K] : null : null : null +///Qdel every item in the list before setting the list to null +#define QDEL_LAZYLIST(L) for(var/I in L) qdel(I); L = null; +//These methods don't null the list +///Use LAZYLISTDUPLICATE instead if you want it to null with no entries +#define LAZYCOPY(L) (L ? L.Copy() : list() ) +/// Consider LAZYNULL instead +#define LAZYCLEARLIST(L) if(L) L.Cut() +///Returns the list if it's actually a valid list, otherwise will initialize it +#define SANITIZE_LIST(L) ( islist(L) ? L : list() ) +/// Performs an insertion on the given lazy list with the given key and value. If the value already exists, a new one will not be made. +#define LAZYORASSOCLIST(lazy_list, key, value) \ + LAZYINITLIST(lazy_list); \ + LAZYINITLIST(lazy_list[key]); \ + lazy_list[key] |= value; + +/* + Aurora Snowflake +*/ +#define LAZYPICK(L,DEFAULT) (LAZYLEN(L) ? pick(L) : DEFAULT) +#define LAZYISIN(L, I) (L ? (I in L) : FALSE) +#define LAZYDISTINCTADD(L, I) if(!L) { L = list(); } L |= I; +#define LAZYREPLACEKEY(L, K, NK) if(L) { if(L[K]) { L[NK] = L[K] } else {L += NK} L -= K; } + +// Shims for some list procs in lists.dm. +#define isemptylist(L) (!LAZYLEN(L)) + +/** + * Removes any null entries from the list + * Returns TRUE if the list had nulls, FALSE otherwise +**/ +/proc/list_clear_nulls(list/list_to_clear) +//Sorry, OpenDream doesn't have this proc yet for lists +#if !defined(OPENDREAM) + return (list_to_clear.RemoveAll(null) > 0) +#else + return FALSE +#endif + + +/// Passed into BINARY_INSERT to compare keys +#define COMPARE_KEY __BIN_LIST[__BIN_MID] +/// Passed into BINARY_INSERT to compare values +#define COMPARE_VALUE __BIN_LIST[__BIN_LIST[__BIN_MID]] + +/**** + * Binary search sorted insert + * INPUT: Object to be inserted + * LIST: List to insert object into + * TYPECONT: The typepath of the contents of the list + * COMPARE: The object to compare against, usualy the same as INPUT + * COMPARISON: The variable on the objects to compare + * COMPTYPE: How should the values be compared? Either COMPARE_KEY or COMPARE_VALUE. + */ +#define BINARY_INSERT(INPUT, LIST, TYPECONT, COMPARE, COMPARISON, COMPTYPE) \ + do {\ + var/list/__BIN_LIST = LIST;\ + var/__BIN_CTTL = length(__BIN_LIST);\ + if(!__BIN_CTTL) {\ + __BIN_LIST += INPUT;\ + } else {\ + var/__BIN_LEFT = 1;\ + var/__BIN_RIGHT = __BIN_CTTL;\ + var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + var ##TYPECONT/__BIN_ITEM;\ + while(__BIN_LEFT < __BIN_RIGHT) {\ + __BIN_ITEM = COMPTYPE;\ + if(__BIN_ITEM.##COMPARISON <= COMPARE.##COMPARISON) {\ + __BIN_LEFT = __BIN_MID + 1;\ + } else {\ + __BIN_RIGHT = __BIN_MID;\ + };\ + __BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + };\ + __BIN_ITEM = COMPTYPE;\ + __BIN_MID = __BIN_ITEM.##COMPARISON > COMPARE.##COMPARISON ? __BIN_MID : __BIN_MID + 1;\ + __BIN_LIST.Insert(__BIN_MID, INPUT);\ + };\ + } while(FALSE) + +/** + * Custom binary search sorted insert utilising comparison procs instead of vars. + * INPUT: Object to be inserted + * LIST: List to insert object into + * TYPECONT: The typepath of the contents of the list + * COMPARE: The object to compare against, usualy the same as INPUT + * COMPARISON: The plaintext name of a proc on INPUT that takes a single argument to accept a single element from LIST and returns a positive, negative or zero number to perform a comparison. + * COMPTYPE: How should the values be compared? Either COMPARE_KEY or COMPARE_VALUE. + */ +#define BINARY_INSERT_PROC_COMPARE(INPUT, LIST, TYPECONT, COMPARE, COMPARISON, COMPTYPE) \ + do {\ + var/list/__BIN_LIST = LIST;\ + var/__BIN_CTTL = length(__BIN_LIST);\ + if(!__BIN_CTTL) {\ + __BIN_LIST += INPUT;\ + } else {\ + var/__BIN_LEFT = 1;\ + var/__BIN_RIGHT = __BIN_CTTL;\ + var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + var ##TYPECONT/__BIN_ITEM;\ + while(__BIN_LEFT < __BIN_RIGHT) {\ + __BIN_ITEM = COMPTYPE;\ + if(__BIN_ITEM.##COMPARISON(COMPARE) <= 0) {\ + __BIN_LEFT = __BIN_MID + 1;\ + } else {\ + __BIN_RIGHT = __BIN_MID;\ + };\ + __BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + };\ + __BIN_ITEM = COMPTYPE;\ + __BIN_MID = __BIN_ITEM.##COMPARISON(COMPARE) > 0 ? __BIN_MID : __BIN_MID + 1;\ + __BIN_LIST.Insert(__BIN_MID, INPUT);\ + };\ + } while(FALSE) + +#define SORT_FIRST_INDEX(list) (list[1]) +#define SORT_COMPARE_DIRECTLY(thing) (thing) +#define SORT_VAR_NO_TYPE(varname) var/varname +/**** + * Even more custom binary search sorted insert, using defines instead of vars + * INPUT: Item to be inserted + * LIST: List to insert INPUT into + * TYPECONT: A define setting the var to the typepath of the contents of the list + * COMPARE: The item to compare against, usualy the same as INPUT + * COMPARISON: A define that takes an item to compare as input, and returns their comparable value + * COMPTYPE: How should the list be compared? Either COMPARE_KEY or COMPARE_VALUE. + */ +#define BINARY_INSERT_DEFINE(INPUT, LIST, TYPECONT, COMPARE, COMPARISON, COMPTYPE) \ + do {\ + var/list/__BIN_LIST = LIST;\ + var/__BIN_CTTL = length(__BIN_LIST);\ + if(!__BIN_CTTL) {\ + __BIN_LIST += INPUT;\ + } else {\ + var/__BIN_LEFT = 1;\ + var/__BIN_RIGHT = __BIN_CTTL;\ + var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + ##TYPECONT(__BIN_ITEM);\ + while(__BIN_LEFT < __BIN_RIGHT) {\ + __BIN_ITEM = COMPTYPE;\ + if(##COMPARISON(__BIN_ITEM) <= ##COMPARISON(COMPARE)) {\ + __BIN_LEFT = __BIN_MID + 1;\ + } else {\ + __BIN_RIGHT = __BIN_MID;\ + };\ + __BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + };\ + __BIN_ITEM = COMPTYPE;\ + __BIN_MID = ##COMPARISON(__BIN_ITEM) > ##COMPARISON(COMPARE) ? __BIN_MID : __BIN_MID + 1;\ + __BIN_LIST.Insert(__BIN_MID, INPUT);\ + };\ + } while(FALSE) + ///replaces reverseList ~Carnie /proc/reverse_range(list/inserted_list, start = 1, end = 0) if(inserted_list.len) diff --git a/code/__HELPERS/spatial_info.dm b/code/__HELPERS/spatial_info.dm index 5566004ca36..81d58260f6e 100644 --- a/code/__HELPERS/spatial_info.dm +++ b/code/__HELPERS/spatial_info.dm @@ -1,44 +1,36 @@ -/turf - ///what /mob/abstract/oranges_ear instance is already assigned to us as there should only ever be one. - ///used for guaranteeing there is only one oranges_ear per turf when assigned, speeds up view() iteration - var/mob/abstract/oranges_ear/assigned_oranges_ear - /** # Oranges Ear * * turns out view() spends a significant portion of its processing time generating lists of contents of viewable turfs which includes EVERYTHING on it visible - * and the turf itself. there is an optimization to view() which makes it only generate lists of a certain atom type - this system takes advantage of that. + * and the turf itself. there is an optimization to view() which makes it only iterate through either /obj or /mob contents, as well as normal list typechecking filters + * * a fuckton of these are generated as part of its SS's init and stored in a list, when requested for a list of movables returned by the spatial grid or by some - * superset of the final output that must be narrowed down by view() one of these gets put on every turf that contains the movables that need filtering - * and each is given references to the movables they represent. that way you can do for(var/mob/abstract/oranges_ear/ear in view(...)) and check what they reference + * superset of the final output that must be narrowed down by view(), one of these gets put on every turf that contains the movables that need filtering + * and each is given references to the movables they represent. that way you can do for(var/mob/oranges_ear/ear in view(...)) and check what they reference * as opposed to for(var/atom/movable/target in view(...)) and checking if they have the properties you want which leads to much larger lists generated by view() * and also leads to iterating through more movables to filter them. * - * TLDR: iterating through just mobs is much faster than all movables when iterating through view(), this system leverages that to boost speed + * TLDR: iterating through just mobs is much faster than all movables when iterating through view() on average, this system leverages that to boost speed * enough to offset the cost of allocating the mobs * * named because the idea was first made by oranges and i didnt know what else to call it (note that this system was originally made for get_hearers_in_view()) */ -/mob/abstract/oranges_ear +/mob/oranges_ear icon_state = null - invisibility = 0 + density = FALSE mouse_opacity = MOUSE_OPACITY_TRANSPARENT - faction = null screens = null - /// references to everything "on" the turf we are assigned to, that we care about. populated in assign() and cleared in unassign(). /// movables iside of other movables count as being "on" if they have get_turf(them) == our turf. intentionally not a lazylist var/list/references = list() -/mob/abstract/oranges_ear/Initialize(mapload) +/mob/oranges_ear/Initialize(mapload) SHOULD_CALL_PARENT(FALSE) - if(flags_1 & INITIALIZED_1) stack_trace("Warning: [src]([type]) initialized multiple times!") flags_1 |= INITIALIZED_1 - return INITIALIZE_HINT_NORMAL -/mob/abstract/oranges_ear/Destroy(force) +/mob/oranges_ear/Destroy(force) var/old_length = length(SSspatial_grid.pregenerated_oranges_ears) SSspatial_grid.pregenerated_oranges_ears -= src if(length(SSspatial_grid.pregenerated_oranges_ears) < old_length) @@ -50,22 +42,22 @@ . = ..() -/mob/abstract/oranges_ear/Move() +/mob/oranges_ear/Move() SHOULD_CALL_PARENT(FALSE) - crash_with("SOMEHOW A /mob/abstract/oranges_ear MOVED") + stack_trace("SOMEHOW A /mob/oranges_ear MOVED") return FALSE -/mob/abstract/oranges_ear/forceMove(atom/destination) - SHOULD_CALL_PARENT(FALSE) - crash_with("SOMEHOW A /mob/abstract/oranges_ear MOVED") - return FALSE +// /mob/oranges_ear/abstract_move(atom/destination) +// SHOULD_CALL_PARENT(FALSE) +// stack_trace("SOMEHOW A /mob/oranges_ear MOVED") +// return FALSE -/mob/abstract/oranges_ear/Bump() +/mob/oranges_ear/Bump() SHOULD_CALL_PARENT(FALSE) return FALSE ///clean this oranges_ear up for future use -/mob/abstract/oranges_ear/proc/unassign() +/mob/oranges_ear/proc/unassign() var/turf/turf_loc = loc turf_loc.assigned_oranges_ear = null//trollface. our loc should ALWAYS be a turf, no exceptions. if it isnt then this doubles as an error message ;) loc = null @@ -74,7 +66,7 @@ /** * returns every hearaing movable in view to the turf of source not taking into account lighting * useful when you need to maintain always being able to hear something if a sound is emitted from it and you can see it (and youre in range). - * otherwise this is just a more expensive version of get_hearers_in_LOS() + * otherwise this is just a more expensive version of get_hearers_in_LOS(). * * * view_radius - what radius search circle we are using, worse performance as this increases * * source - object at the center of our search area. everything in get_turf(source) is guaranteed to be part of the search area @@ -100,23 +92,56 @@ var/list/assigned_oranges_ears = SSspatial_grid.assign_oranges_ears(hearables_from_grid) - var/old_luminosity = center_turf.luminosity - center_turf.luminosity = 6 //man if only we had an inbuilt dview() - - //this is the ENTIRE reason all this shit is worth it due to how view() works and can be optimized - //view() constructs lists of viewed atoms by default and specifying a specific type of atom to look for limits the lists it constructs to those of that - //primitive type and then when the view operation is completed the output is then typechecked to only iterate through objects in view with the same - //typepath. by assigning one /mob/abstract/oranges_ear to every turf with hearable atoms on it and giving them references to each one means that: - //1. view() only constructs lists of atoms with the mob primitive type and - //2. the mobs returned by view are fast typechecked to only iterate through /mob/abstract/oranges_ear mobs, which guarantees at most one per turf + //this is the ENTIRE reason all this shit is worth it due to how view()-like procs and the contents list works and can be optimized + //internally, the contents list is secretly two linked lists, one for /obj's and one for /mob's (/atom/movable counts as /obj here) + //by default, for(var/atom/name in view()) iterates through both the /obj linked list then the /mob linked list of each turf + //but because what we want are only a tiny proportion of all movables, most of the things in the /obj contents list are not what we're looking for + //while every mob can hear. for this case view() and similar procs have an optimization to only look through 1 of these lists if it can (eg youre only looking for mobs) + //so by representing every hearing contents on a turf with a single /mob/oranges_ear containing references to all of them, we are: + //1. making view() only go through the smallest of the two linked lists per turf, which contains the type we're looking for at the end + //2. typechecking all mobs in the output to only actually return mobs of type /mob/oranges_ear //on a whole this can outperform iterating through all movables in view() by ~2x especially when hearables are a tiny percentage of movables in view - for(var/mob/abstract/oranges_ear/ear in view(view_radius, center_turf)) + //using hearers is a further optimization of that because for our purposes its the same as view except we dont have to set center's luminosity to 6 and then unset it + for(var/mob/oranges_ear/ear in hearers(view_radius, center_turf)) . += ear.references - for(var/mob/abstract/oranges_ear/remaining_ear as anything in assigned_oranges_ears)//we need to clean up our mess + for(var/mob/oranges_ear/remaining_ear as anything in assigned_oranges_ears)//we need to clean up our mess remaining_ear.unassign() - center_turf.luminosity = old_luminosity + return . + +/** + * The exact same as get_hearers_in_view, but not limited by visibility. Does no filtering for traits, line of sight, or any other such criteria. + * Filtering is intended to be done by whatever calls this function. + * + * This function exists to allow for mobs to hear speech without line of sight, if such a thing is needed. + * + * * radius - what radius search circle we are using, worse performance as this increases + * * source - object at the center of our search area. everything in get_turf(source) is guaranteed to be part of the search area + */ +/proc/get_hearers_in_range(range, atom/source) + var/turf/center_turf = get_turf(source) + if(!center_turf) + return + + . = list() + + if(range <= 0)//special case for if only source cares + for(var/atom/movable/target as anything in center_turf) + var/list/recursive_contents = target.important_recursive_contents?[RECURSIVE_CONTENTS_HEARING_SENSITIVE] + if(recursive_contents) + . += recursive_contents + return . + + var/list/hearables_from_grid = SSspatial_grid.orthogonal_range_search(source, RECURSIVE_CONTENTS_HEARING_SENSITIVE, range) + + if(!length(hearables_from_grid))//we know that something is returned by the grid, but we dont know if we need to actually filter down the output + return . + + for(var/atom/movable/hearable as anything in hearables_from_grid) + if (get_dist(center_turf, hearable) <= range) + . += hearable + return . /** @@ -168,51 +193,28 @@ if(inbetween_turf == target_turf)//we've gotten to target's turf without returning due to turf opacity, so we must be able to see target break - if(inbetween_turf.opacity)//this turf or something on it is opaque so we cant see through it + if(IS_OPAQUE_TURF(inbetween_turf))//this turf or something on it is opaque so we cant see through it . -= target break -/proc/get_hearers_in_radio_ranges(list/obj/item/device/radio/radios) +/proc/get_hearers_in_radio_ranges(list/obj/item/device/radios) . = list() // Returns a list of mobs who can hear any of the radios given in @radios - for(var/obj/item/device/radio/radio in radios) + for(var/obj/item/device/radio/radio as anything in radios) . |= get_hearers_in_LOS(radio.canhear_range, radio, FALSE) -// get_hearers_in_LOS but for anything AIs care to target -/proc/get_targets_in_LOS(view_radius, atom/source) - var/turf/center_turf = get_turf(source) - if(!center_turf) - return - - if(view_radius <= 0) - . = list() - for(var/atom/movable/target as anything in center_turf) - var/list/tgt_contents = target.important_recursive_contents?[RECURSIVE_CONTENTS_AI_TARGETS] - if(tgt_contents) - . += tgt_contents - return - - . = SSspatial_grid.orthogonal_range_search(source, SPATIAL_GRID_CONTENTS_TYPE_TARGETS, view_radius) - - for(var/mob/target as anything in .) - var/los = null - SPATIAL_CHECK_LOS(los, source, target, view_radius) - if(!los) - . -= target - continue - ///Calculate if two atoms are in sight, returns TRUE or FALSE /proc/inLineOfSight(X1,Y1,X2,Y2,Z=1,PX1=16.5,PY1=16.5,PX2=16.5,PY2=16.5) var/turf/T - if(X1==X2) - if(Y1==Y2) + if(X1 == X2) + if(Y1 == Y2) return TRUE //Light cannot be blocked on same tile else var/s = SIGN(Y2-Y1) Y1+=s - while(Y1!=Y2) + while(Y1 != Y2) T=locate(X1,Y1,Z) - if(T.opacity) + if(IS_OPAQUE_TURF(T)) return FALSE Y1+=s else @@ -222,18 +224,18 @@ var/signY = SIGN(Y2-Y1) if(X1 outer_angle) + continue + sliced_turfs += checked_turf + return sliced_turfs + /** * Get a bounding box of a list of atoms. * @@ -340,26 +366,35 @@ ///Returns the open turf next to the center in a specific direction /proc/get_open_turf_in_dir(atom/center, dir) - var/turf/T = get_ranged_target_turf(center, dir, 1) - if(istype(T) && !T.density) - return T + var/turf/get_turf = get_step(center, dir) + if(istype(get_turf) && !get_turf.density) //This differs from how TG does it, they have a dedicated turf type for open turf. Thanks Aurora, always be special. + return get_turf +//This differs from how TG does it, they have a dedicated turf type for open turf, we have to check the density. Thanks Aurora, always be special. ///Returns a list with all the adjacent open turfs. Clears the list of nulls in the end. /proc/get_adjacent_open_turfs(atom/center) - . = list( - get_open_turf_in_dir(center, NORTH), - get_open_turf_in_dir(center, SOUTH), - get_open_turf_in_dir(center, EAST), - get_open_turf_in_dir(center, WEST) - ) - listclearnulls(.) + var/list/hand_back = list() + // Inlined get_open_turf_in_dir, just to be fast + var/turf/new_turf = get_step(center, NORTH) + if(istype(new_turf) && !new_turf.density) + hand_back += new_turf + new_turf = get_step(center, SOUTH) + if(istype(new_turf) && !new_turf.density) + hand_back += new_turf + new_turf = get_step(center, EAST) + if(istype(new_turf) && !new_turf.density) + hand_back += new_turf + new_turf = get_step(center, WEST) + if(istype(new_turf) && !new_turf.density) + hand_back += new_turf + return hand_back -///Returns a list with all the adjacent areas by getting the adjacent open turfs -/proc/get_adjacent_open_areas(atom/center) - . = list() - var/list/adjacent_turfs = get_adjacent_open_turfs(center) - for(var/near_turf in adjacent_turfs) - . |= get_area(near_turf) +// ///Returns a list with all the adjacent areas by getting the adjacent open turfs +// /proc/get_adjacent_open_areas(atom/center) +// . = list() +// var/list/adjacent_turfs = get_adjacent_open_turfs(center) +// for(var/near_turf in adjacent_turfs) +// . |= get_area(near_turf) /** * Returns a list with the names of the areas around a center at a certain distance @@ -387,12 +422,12 @@ get_area(get_ranged_target_turf(center, EAST, 1)), get_area(get_ranged_target_turf(center, WEST, 1)) ) - listclearnulls(.) + list_clear_nulls(.) ///Checks if the mob provided (must_be_alone) is alone in an area /proc/alone_in_area(area/the_area, mob/must_be_alone, check_type = /mob/living/carbon) var/area/our_area = get_area(the_area) - for(var/carbon in GLOB.living_mob_list) + for(var/carbon in GLOB.mob_list) if(!istype(carbon, check_type)) continue if(carbon == must_be_alone) diff --git a/code/controllers/subsystems/spatial_gridmap.dm b/code/controllers/subsystems/spatial_gridmap.dm index 00a101c62ff..85e5bfebc20 100644 --- a/code/controllers/subsystems/spatial_gridmap.dm +++ b/code/controllers/subsystems/spatial_gridmap.dm @@ -1,32 +1,6 @@ -///the subsystem creates this many [/mob/abstract/oranges_ear] mob instances during init. allocations that require more than this create more. +///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 -// macros meant specifically to add/remove movables from the hearing_contents and client_contents lists of -// /datum/spatial_grid_cell, when empty they become references to a single list in SSspatial_grid and when filled they become their own list -// this is to save memory without making them lazylists as that slows down iteration through them -#define GRID_CELL_ADD(cell_contents_list, movable_or_list) \ - if(!length(cell_contents_list)) { \ - cell_contents_list = list(); \ - cell_contents_list += movable_or_list; \ - } else { \ - cell_contents_list += movable_or_list; \ - }; - -#define GRID_CELL_SET(cell_contents_list, movable_or_list) \ - if(!length(cell_contents_list)) { \ - cell_contents_list = list(); \ - cell_contents_list += movable_or_list; \ - } else { \ - cell_contents_list |= movable_or_list; \ - }; - -//dont use these outside of SSspatial_grid's scope use the procs it has for this purpose -#define GRID_CELL_REMOVE(cell_contents_list, movable_or_list) \ - cell_contents_list -= movable_or_list; \ - if(!length(cell_contents_list)) {\ - cell_contents_list = dummy_list; \ - }; - /** * # Spatial Grid Cell * @@ -52,8 +26,8 @@ var/list/hearing_contents ///every client possessed mob inside this cell var/list/client_contents - ///every mob inside this cell - var/list/tgt_contents + ///every atmos machine inside this cell + var/list/atmos_contents /datum/spatial_grid_cell/New(cell_x, cell_y, cell_z) . = ..() @@ -65,15 +39,14 @@ if(length(dummy_list)) dummy_list.Cut() - crash_with("SSspatial_grid.dummy_list had something inserted into it at some point! this is a problem as it is supposed to stay empty") - + 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 - tgt_contents = dummy_list + atmos_contents = dummy_list -/datum/spatial_grid_cell/Destroy(force, ...) +/datum/spatial_grid_cell/Destroy(force) if(force)//the response to someone trying to qdel this is a right proper fuck you - crash_with("dont try to destroy spatial grid cells without a good reason. if you need to do it use force") + stack_trace("dont try to destroy spatial grid cells without a good reason. if you need to do it use force") return . = ..() @@ -93,17 +66,30 @@ * 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 * - * as of right now this system operates on a subset of the important_recursive_contents list for atom/movable, specifically - * [RECURSIVE_CONTENTS_HEARING_SENSITIVE] and [RECURSIVE_CONTENTS_CLIENT_MOBS] because both are those are both 1. important and 2. commonly searched for + * 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" - flags = SS_NO_FIRE ///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(RECURSIVE_CONTENTS_HEARING_SENSITIVE = list(), RECURSIVE_CONTENTS_CLIENT_MOBS = list(), RECURSIVE_CONTENTS_AI_TARGETS = list()) + 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 @@ -111,15 +97,18 @@ SUBSYSTEM_DEF(spatial_grid) ///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/abstract/oranges_ear instances we have pregenerated for view() iteration speedup - var/list/mob/abstract/oranges_ear/pregenerated_oranges_ears = list() - ///how many pregenerated /mob/abstract/oranges_ear instances currently exist. this should hopefully never exceed its starting value + ///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) +/datum/controller/subsystem/spatial_grid/Initialize() cells_on_x_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxx) cells_on_y_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxy) + // enter_cell only runs if 'initialized' + initialized = TRUE + for(var/z_level in 1 to world.maxz) propogate_spatial_grid_to_new_z(null, z_level) CHECK_TICK @@ -131,19 +120,18 @@ SUBSYSTEM_DEF(spatial_grid) if(movable_turf) enter_cell(movable, movable_turf) - UnregisterSignal(movable, COMSIG_PREQDELETED) + UnregisterSignal(movable, COMSIG_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_REF(propogate_spatial_grid_to_new_z)) RegisterSignal(SSdcs, COMSIG_GLOB_EXPANDED_WORLD_BOUNDS, PROC_REF(after_world_bounds_expanded)) - return SS_INIT_SUCCESS ///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_PREQDELETED, PROC_REF(queued_item_deleted), override = TRUE) + RegisterSignal(waiting_movable, COMSIG_QDELETING, PROC_REF(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 @@ -158,11 +146,11 @@ SUBSYSTEM_DEF(spatial_grid) waiting_movable_is_in_other_queues = TRUE if(!waiting_movable_is_in_other_queues) - UnregisterSignal(movable_to_remove, COMSIG_PREQDELETED) + UnregisterSignal(movable_to_remove, COMSIG_QDELETING) return - UnregisterSignal(movable_to_remove, COMSIG_PREQDELETED) + UnregisterSignal(movable_to_remove, COMSIG_QDELETING) for(var/type in waiting_to_add_by_type) waiting_to_add_by_type[type] -= movable_to_remove @@ -185,53 +173,6 @@ SUBSYSTEM_DEF(spatial_grid) var/datum/spatial_grid_cell/cell = new(x, y, z_level) new_cell_grid[y] += cell -///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/abstract/oranges_ear(null) - - number_of_oranges_ears = length(pregenerated_oranges_ears) - -///allocate one [/mob/abstract/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) - crash_with("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/abstract/oranges_ear that we try to allocate to assigned_atom's turf - var/mob/abstract/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 - ///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) @@ -260,16 +201,19 @@ SUBSYSTEM_DEF(spatial_grid) cell_row += new_cell_inserted continue + if(!length(cell_row)) + 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(ROUND_UP((center_coord - range) / SPATIAL_GRID_CELLSIZE), 1) +#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(ROUND_UP((center_coord + range) / SPATIAL_GRID_CELLSIZE), axis_size) +#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 @@ -292,15 +236,9 @@ SUBSYSTEM_DEF(spatial_grid) . = list() - //cache for sanic speeds - var/cells_on_y_axis = src.cells_on_y_axis - var/cells_on_x_axis = src.cells_on_x_axis - - if(grids_by_z_level.len < center_turf.z) - return - //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)) @@ -314,11 +252,10 @@ SUBSYSTEM_DEF(spatial_grid) . += grid_level[row][x_index].hearing_contents - if(SPATIAL_GRID_CONTENTS_TYPE_TARGETS) + 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].tgt_contents + . += grid_level[row][x_index].atmos_contents return . @@ -328,10 +265,14 @@ SUBSYSTEM_DEF(spatial_grid) if(!target_turf) return - return grids_by_z_level[target_turf.z][ROUND_UP(target_turf.y / SPATIAL_GRID_CELLSIZE)][ROUND_UP(target_turf.x / SPATIAL_GRID_CELLSIZE)] + 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 @@ -340,12 +281,12 @@ SUBSYSTEM_DEF(spatial_grid) var/list/intersecting_grid_cells = list() //the minimum x and y cell indexes to test - var/min_x = max(ROUND_UP((center_x - range) / SPATIAL_GRID_CELLSIZE), 1) - var/min_y = max(ROUND_UP((center_y - range) / SPATIAL_GRID_CELLSIZE), 1)//calculating these indices only takes around 2 microseconds + 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(ROUND_UP((center_x + range) / SPATIAL_GRID_CELLSIZE), cells_on_x_axis) - var/max_y = min(ROUND_UP((center_y + range) / SPATIAL_GRID_CELLSIZE), cells_on_y_axis) + 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] @@ -357,46 +298,124 @@ SUBSYSTEM_DEF(spatial_grid) return intersecting_grid_cells -///find the spatial map cell that target belongs to, then add target's important_recusive_contents to it. +/// 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) - - // This contraption only applies to unit tests, as during the destroy phase some have an MMI machine that is being deleted - #ifdef UNIT_TEST - if(QDELETED(new_target) && istype(new_target, /obj/item/organ/internal/mmi_holder)) + if(!initialized) return - #endif - if(QDELETED(new_target)) CRASH("qdeleted or null target trying to enter the spatial grid!") - if(!target_turf || !new_target?.important_recursive_contents) - CRASH("/datum/controller/subsystem/spatial_grid/proc/enter_cell() was given null arguments or a new_target without important_recursive_contents!") + 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 = ROUND_UP(target_turf.x / SPATIAL_GRID_CELLSIZE) - var/y_index = ROUND_UP(target_turf.y / SPATIAL_GRID_CELLSIZE) + 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(new_target.important_recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS]) - GRID_CELL_SET(intersecting_cell.client_contents, new_target.important_recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS]) + 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]) - SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(RECURSIVE_CONTENTS_CLIENT_MOBS), new_target) + 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) - if(new_target.important_recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE]) - GRID_CELL_SET(intersecting_cell.hearing_contents, new_target.important_recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE]) +///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!") - SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(RECURSIVE_CONTENTS_HEARING_SENSITIVE), new_target) + 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]!") - if(new_target.important_recursive_contents[RECURSIVE_CONTENTS_AI_TARGETS]) - GRID_CELL_SET(intersecting_cell.tgt_contents, new_target.important_recursive_contents[RECURSIVE_CONTENTS_AI_TARGETS]) + var/x_index = GET_SPATIAL_INDEX(target_turf.x) + var/y_index = GET_SPATIAL_INDEX(target_turf.y) + var/z_index = target_turf.z - SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(RECURSIVE_CONTENTS_AI_TARGETS), new_target) + 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 subtract target's important_recusive_contents from it. + * 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 @@ -407,60 +426,165 @@ SUBSYSTEM_DEF(spatial_grid) if(!initialized) return - if(!target_turf || !old_target?.important_recursive_contents) - CRASH("/datum/controller/subsystem/spatial_grid/proc/exit_cell() was given null arguments or a new_target without important_recursive_contents!") + 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 = ROUND_UP(target_turf.x / SPATIAL_GRID_CELLSIZE) - var/y_index = ROUND_UP(target_turf.y / SPATIAL_GRID_CELLSIZE) + 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/list/grid = grids_by_z_level[z_index] - var/datum/spatial_grid_cell/intersecting_cell = grid[y_index][x_index] + 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?[type] || old_target + GRID_CELL_REMOVE(intersecting_cell.client_contents, old_target_contents) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(type), old_target_contents) - if(exclusive_type && old_target.important_recursive_contents[exclusive_type]) - switch(exclusive_type) - if(RECURSIVE_CONTENTS_CLIENT_MOBS) - GRID_CELL_REMOVE(intersecting_cell.client_contents, old_target.important_recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS]) + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + var/list/old_target_contents = old_target.important_recursive_contents?[type] || old_target + GRID_CELL_REMOVE(intersecting_cell.hearing_contents, old_target_contents) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(type), old_target_contents) - if(RECURSIVE_CONTENTS_HEARING_SENSITIVE) - GRID_CELL_REMOVE(intersecting_cell.hearing_contents, old_target.important_recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE]) + if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS) + GRID_CELL_REMOVE(intersecting_cell.atmos_contents, old_target) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(type), old_target) - if(RECURSIVE_CONTENTS_AI_TARGETS) - GRID_CELL_REMOVE(intersecting_cell.tgt_contents, old_target.important_recursive_contents[RECURSIVE_CONTENTS_AI_TARGETS]) + return TRUE - SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(exclusive_type), old_target) +///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?[exclusive_type] || old_target //cache for sanic speeds (lists are references anyways) + GRID_CELL_REMOVE(intersecting_cell.client_contents, old_target_contents) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(exclusive_type), old_target_contents) + + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + var/list/old_target_contents = old_target.important_recursive_contents?[exclusive_type] || old_target + GRID_CELL_REMOVE(intersecting_cell.hearing_contents, old_target_contents) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(exclusive_type), old_target_contents) + + 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 + +/// if for whatever reason this movable is "untracked" e.g. it breaks the assumption that a movable is only inside the contents of any grid cell associated with its loc, +/// this will error. this checks every grid cell in the world so dont call this on live unless you have to. +/// returns TRUE if this movable is untracked, FALSE otherwise +/datum/controller/subsystem/spatial_grid/proc/untracked_movable_error(atom/movable/movable_to_check) + if(!movable_to_check?.spatial_grid_key) + return FALSE + + if(!initialized) + return FALSE + + var/datum/spatial_grid_cell/loc_cell = get_cell_of(movable_to_check) + var/list/containing_cells = find_hanging_cell_refs_for_movable(movable_to_check, remove_from_cells=FALSE) + //if we're in multiple cells, throw an error. + //if we're in 1 cell but it cant be deduced by our location, throw an error. + if(length(containing_cells) > 1 || (length(containing_cells) == 1 && loc_cell && containing_cells[1] != loc_cell && containing_cells[1] != null)) + var/error_data = "" + + var/location_string = "which is in nullspace, and thus not be within the contents of any spatial grid cell" + if(loc_cell) + location_string = "which is supposed to only be in the contents of a spatial grid cell at coords: ([GRID_INDEX_TO_COORDS(loc_cell.cell_x)], [GRID_INDEX_TO_COORDS(loc_cell.cell_y)], [loc_cell.cell_z])" + + var/error_explanation = "was in the contents of [length(containing_cells)] spatial grid cells when it was only supposed to be in one!" + if(length(containing_cells) == 1) + error_explanation = "was in the contents of 1 spatial grid cell but it was inside the area handled by another grid cell!" + var/datum/spatial_grid_cell/bad_cell = containing_cells[1] + + error_data = "within the contents of a cell at coords: ([GRID_INDEX_TO_COORDS(bad_cell.cell_x)], [GRID_INDEX_TO_COORDS(bad_cell.cell_y)], [bad_cell.cell_z])" + + if(!error_data) + for(var/datum/spatial_grid_cell/cell in containing_cells) + var/coords = "([GRID_INDEX_TO_COORDS(cell.cell_x)], [GRID_INDEX_TO_COORDS(cell.cell_y)], [cell.cell_z])" + var/contents = "" + + if(movable_to_check in cell.hearing_contents) + contents = "hearing" + + if(movable_to_check in cell.client_contents) + if(length(contents) > 0) + contents = "[contents], client" + else + contents = "client" + + if(movable_to_check in cell.atmos_contents) + if(length(contents) > 0) + contents = "[contents], atmos" + else + contents = "atmos" + + if(length(error_data) > 0) + error_data = "[error_data], {coords: [coords], within channels: [contents]}" + else + error_data = "within the contents of the following cells: {coords: [coords], within channels: [contents]}" + + /** + * example: + * + * /mob/living/trolls_the_maintainer instance, which is supposed to only be in the contents of a spatial grid cell at coords: (136, 136, 14), + * was in the contents of 3 spatial grid cells when it was only supposed to be in one! within the contents of the following cells: + * {(68, 153, 2), within channels: hearing}, + * {coords: (221, 170, 3), within channels: hearing}, + * {coords: (255, 153, 11), within channels: hearing}, + * {coords: (136, 136, 14), within channels: hearing}. + */ + stack_trace("[movable_to_check.type] instance, [location_string], [error_explanation] [error_data].") + + return TRUE + + return FALSE + +/** + * remove this movable from the grid by finding the grid cell its in and removing it from that. + * if it cant infer a grid cell its located in (e.g. if its in nullspace but it can happen if the grid isnt expanded to a z level), search every grid cell. + */ +/datum/controller/subsystem/spatial_grid/proc/force_remove_from_grid(atom/movable/to_remove) + if(!to_remove?.spatial_grid_key) return - if(old_target.important_recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS]) - GRID_CELL_REMOVE(intersecting_cell.client_contents, old_target.important_recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS]) - - SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), old_target) - - if(old_target.important_recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE]) - GRID_CELL_REMOVE(intersecting_cell.hearing_contents, old_target.important_recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE]) - - SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(RECURSIVE_CONTENTS_HEARING_SENSITIVE), old_target) - - if(old_target.important_recursive_contents[RECURSIVE_CONTENTS_AI_TARGETS]) - GRID_CELL_REMOVE(intersecting_cell.tgt_contents, old_target.important_recursive_contents[RECURSIVE_CONTENTS_AI_TARGETS]) - - SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(RECURSIVE_CONTENTS_AI_TARGETS), old_target) - -///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 +#ifdef UNIT_TESTS + if(untracked_movable_error(to_remove)) + find_hanging_cell_refs_for_movable(to_remove, remove_from_cells=FALSE) //dont remove from cells because we should be able to see 2 errors + return +#endif - GRID_CELL_REMOVE(input_cell.client_contents, to_remove) - GRID_CELL_REMOVE(input_cell.hearing_contents, to_remove) - GRID_CELL_REMOVE(input_cell.tgt_contents, to_remove) + var/datum/spatial_grid_cell/loc_cell = get_cell_of(to_remove) + + if(loc_cell) + GRID_CELL_REMOVE_ALL(loc_cell, to_remove) + else + find_hanging_cell_refs_for_movable(to_remove, remove_from_cells=TRUE) + +///remove this movable from the given spatial_grid_cell +/datum/controller/subsystem/spatial_grid/proc/force_remove_from_cell(atom/movable/to_remove, datum/spatial_grid_cell/input_cell) + if(!input_cell) + return + + GRID_CELL_REMOVE_ALL(input_cell, 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) @@ -480,7 +604,7 @@ SUBSYSTEM_DEF(spatial_grid) 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.tgt_contents)) + 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) @@ -490,7 +614,7 @@ SUBSYSTEM_DEF(spatial_grid) ///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) + var/list/containing_cells = SSspatial_grid.find_hanging_cell_refs_for_movable(src, remove_from_cells) message_admins("[src] is located in the contents of [length(containing_cells)] spatial grid cells") @@ -501,58 +625,109 @@ SUBSYSTEM_DEF(spatial_grid) 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_targets = 0 + var/raw_atmos = 0 var/cells_with_clients = 0 var/cells_with_hearables = 0 - var/cells_with_targets = 0 + var/cells_with_atmos = 0 var/list/client_list = list() var/list/hearable_list = list() - var/list/target_list = list() + var/list/atmos_list = list() - var/total_cells = (world.maxx / SPATIAL_GRID_CELLSIZE) ** 2 + 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_targets_per_cell = 0 + var/average_atmos_mech_per_call = 0 - var/hearable_min_x = (world.maxx / SPATIAL_GRID_CELLSIZE) + var/hearable_min_x = x_cell_count var/hearable_max_x = 1 - var/hearable_min_y = (world.maxy / SPATIAL_GRID_CELLSIZE) + var/hearable_min_y = y_cell_count var/hearable_max_y = 1 - var/client_min_x = (world.maxx / SPATIAL_GRID_CELLSIZE) + var/client_min_x = x_cell_count var/client_max_x = 1 - var/client_min_y = (world.maxy / SPATIAL_GRID_CELLSIZE) + var/client_min_y = y_cell_count var/client_max_y = 1 - var/target_min_x = (world.maxx / SPATIAL_GRID_CELLSIZE) - var/target_max_x = 1 + var/atmos_min_x = x_cell_count + var/atmos_max_x = 1 - var/target_min_y = (world.maxy / SPATIAL_GRID_CELLSIZE) - var/target_max_y = 1 + var/atmos_min_y = y_cell_count + var/atmos_max_y = 1 var/list/inserted_clients = list() if(insert_clients) var/list/turfs - if(isStationLevel(z)) + var/level = z + if(isStationLevel(level)) turfs = GLOB.station_turfs else - turfs = block(locate(1,1,z), locate(world.maxx, world.maxy, z)) + turfs = Z_TURFS(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), SPATIAL_GRID_CONTENTS_TYPE_TARGETS = list(fake_client)) + 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 @@ -561,11 +736,11 @@ SUBSYSTEM_DEF(spatial_grid) 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/target_length = length(cell.tgt_contents) + var/atmos_length = length(cell.atmos_contents) raw_clients += client_length raw_hearables += hearable_length - raw_targets += target_length + raw_atmos += atmos_length if(client_length) cells_with_clients++ @@ -601,30 +776,30 @@ SUBSYSTEM_DEF(spatial_grid) if(cell.cell_y > hearable_max_y) hearable_max_y = cell.cell_y - if(target_length) - cells_with_targets++ + if(raw_atmos) + cells_with_atmos++ - target_list += cell.tgt_contents + atmos_list += cell.atmos_contents - if(cell.cell_x < target_min_x) - target_min_x = cell.cell_x + if(cell.cell_x < atmos_min_x) + atmos_min_x = cell.cell_x - if(cell.cell_x > target_max_x) - target_max_x = cell.cell_x + if(cell.cell_x > atmos_max_x) + atmos_max_x = cell.cell_x - if(cell.cell_y < target_min_y) - target_min_y = cell.cell_y + if(cell.cell_y < atmos_min_y) + atmos_min_y = cell.cell_y - if(cell.cell_y > target_max_y) - target_max_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_target_distance = 0 + var/total_atmos_distance = 0 var/average_client_distance = 0 var/average_hearable_distance = 0 - var/average_target_distance = 0 + var/average_atmos_distance = 0 for(var/hearable in hearable_list)//n^2 btw for(var/other_hearable in hearable_list) @@ -638,36 +813,38 @@ SUBSYSTEM_DEF(spatial_grid) continue total_client_distance += get_dist(client, other_client) - for(var/target in target_list) - for(var/other_target in target_list) - if(target == other_target) + for(var/atmos in atmos_list)//n^2 btw + for(var/other_atmos in atmos_list) + if(atmos == other_atmos) continue - total_target_distance += get_dist(target, other_target) + 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(target_list)) - average_target_distance = total_target_distance / length(target_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_targets_per_cell = raw_targets / 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_targets] targets. all of whom are inside the bounding box given by \ + 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 targets: ([target_min_x], [target_min_y]) x ([target_max_x], [target_max_y]). \ - on average there are [average_clients_per_cell] clients per cell, [average_hearables_per_cell] hearables per cell, and \ - [average_targets_per_cell] targets per cell. [cells_with_clients] cells have clients, [cells_with_hearables] have hearables, \ - and [cells_with_targets] have targets. the average client distance is: [average_client_distance], the average hearable_distance \ - is [average_hearable_distance], and the average target distance is [average_target_distance].") + 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 +#undef BOUNDING_BOX_MAX +#undef BOUNDING_BOX_MIN + +#undef NUMBER_OF_PREGENERATED_ORANGES_EARS diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 77c60033175..58c0a149d7f 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -46,27 +46,33 @@ */ var/list/important_recursive_contents -// We don't really need this, and apparently defining it slows down GC. -/*/atom/movable/Del() - if(!QDELING(src) && loc) - testing("GC: -- [type] was deleted via del() rather than qdel() --") - crash_with("GC: -- [type] was deleted via del() rather than qdel() --") // stick a stack trace in the runtime logs - ..()*/ + /// String representing the spatial grid groups we want to be held in. + /// acts as a key to the list of spatial grid contents types we exist in via SSspatial_grid.spatial_grid_categories. + /// We do it like this to prevent people trying to mutate them and to save memory on holding the lists ourselves + var/spatial_grid_key /atom/movable/Destroy() - if (HAS_SPATIAL_GRID_CONTENTS(src)) - SSspatial_grid.force_remove_from_cell(src) - - LAZYCLEARLIST(contained_mobs) - LAZYCLEARLIST(important_recursive_contents) - GLOB.moved_event.unregister_all_movement(loc, src) . = ..() - for(var/atom/movable/AM in contents) - qdel(AM) + if(spatial_grid_key) + SSspatial_grid.force_remove_from_grid(src) + + for(var/movable_content in contents) + qdel(movable_content) + + QDEL_LAZYLIST(contained_mobs) + + //Pretend this is moveToNullspace() + moveToNullspace() loc = null + + //This absolutely must be after moveToNullspace() + //We rely on Entered and Exited to manage this list, and the copy of this list that is on any /atom/movable "Containers" + //If we clear this before the nullspace move, a ref to this object will be hung in any of its movable containers + LAZYNULL(important_recursive_contents) + screen_loc = null if(ismob(pulledby)) var/mob/M = pulledby @@ -77,6 +83,25 @@ if (bound_overlay) QDEL_NULL(bound_overlay) +/atom/movable/proc/moveToNullspace() + . = TRUE + + var/atom/oldloc = loc + + if (oldloc) + loc = null + var/area/old_area = get_area(oldloc) + if(isturf(oldloc)) //This checked if it's a multitile, which i have no clue what would be here, so just check for turf + for(var/atom/old_loc as anything in locs) + old_loc.Exited(src, NONE) + else + oldloc.Exited(src, NONE) + + if(old_area) + old_area.Exited(src, NONE) + + Moved(oldloc, TRUE) + // This is called when this atom is prevented from moving by atom/A. /atom/movable/proc/Collide(atom/A) if(airflow_speed > 0 && airflow_dest) @@ -401,11 +426,23 @@ /atom/movable/Exited(atom/movable/gone, direction) . = ..() - if (LAZYLEN(gone.important_recursive_contents)) - var/list/nested_locs = get_nested_locs(src) + src - for (var/channel in gone.important_recursive_contents) - for (var/atom/movable/location as anything in nested_locs) - LAZYREMOVEASSOC(location.important_recursive_contents, channel, gone.important_recursive_contents[channel]) + if(!LAZYLEN(gone.important_recursive_contents)) + return + var/list/nested_locs = get_nested_locs(src) + src + for(var/channel in gone.important_recursive_contents) + for(var/atom/movable/location as anything in nested_locs) + LAZYINITLIST(location.important_recursive_contents) + var/list/recursive_contents = location.important_recursive_contents // blue hedgehog velocity + LAZYINITLIST(recursive_contents[channel]) + recursive_contents[channel] -= gone.important_recursive_contents[channel] + switch(channel) + if(RECURSIVE_CONTENTS_CLIENT_MOBS, RECURSIVE_CONTENTS_HEARING_SENSITIVE) + if(!length(recursive_contents[channel])) + // This relies on a nice property of the linked recursive and gridmap types + // They're defined in relation to each other, so they have the same value + SSspatial_grid.remove_grid_awareness(location, channel) + ASSOC_UNSETEMPTY(recursive_contents, channel) + UNSETEMPTY(location.important_recursive_contents) if(LAZYLEN(gone.stored_chat_text)) return_floating_text(gone) @@ -413,28 +450,38 @@ /atom/movable/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs) . = ..() - if (LAZYLEN(arrived.important_recursive_contents)) - var/list/nested_locs = get_nested_locs(src) + src - for (var/channel in arrived.important_recursive_contents) - for (var/atom/movable/location as anything in nested_locs) - LAZYORASSOCLIST(location.important_recursive_contents, channel, arrived.important_recursive_contents[channel]) + if(!LAZYLEN(arrived.important_recursive_contents)) + return + var/list/nested_locs = get_nested_locs(src) + src + for(var/channel in arrived.important_recursive_contents) + for(var/atom/movable/location as anything in nested_locs) + LAZYINITLIST(location.important_recursive_contents) + var/list/recursive_contents = location.important_recursive_contents // blue hedgehog velocity + LAZYINITLIST(recursive_contents[channel]) + switch(channel) + if(RECURSIVE_CONTENTS_CLIENT_MOBS, RECURSIVE_CONTENTS_HEARING_SENSITIVE) + if(!length(recursive_contents[channel])) + SSspatial_grid.add_grid_awareness(location, channel) + recursive_contents[channel] |= arrived.important_recursive_contents[channel] if (LAZYLEN(arrived.stored_chat_text)) give_floating_text(arrived) -//allows this movable to hear and adds itself to the important_recursive_contents list of itself and every movable loc its in +///allows this movable to hear and adds itself to the important_recursive_contents list of itself and every movable loc its in /atom/movable/proc/become_hearing_sensitive(trait_source = TRAIT_GENERIC) - if(!HAS_TRAIT(src, TRAIT_HEARING_SENSITIVE)) - for (var/atom/movable/location as anything in get_nested_locs(src) + src) - LAZYADDASSOCLIST(location.important_recursive_contents, RECURSIVE_CONTENTS_HEARING_SENSITIVE, src) - - var/turf/our_turf = get_turf(src) - if(our_turf && SSspatial_grid.initialized) - SSspatial_grid.enter_cell(src, our_turf) - - else if(our_turf && !SSspatial_grid.initialized)//SSspatial_grid isnt init'd yet, add ourselves to the queue - SSspatial_grid.enter_pre_init_queue(src, RECURSIVE_CONTENTS_HEARING_SENSITIVE) ADD_TRAIT(src, TRAIT_HEARING_SENSITIVE, trait_source) + if(!HAS_TRAIT(src, TRAIT_HEARING_SENSITIVE)) + return + + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYINITLIST(location.important_recursive_contents) + var/list/recursive_contents = location.important_recursive_contents // blue hedgehog velocity + if(!length(recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE])) + SSspatial_grid.add_grid_awareness(location, SPATIAL_GRID_CONTENTS_TYPE_HEARING) + recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE] += list(src) + + var/turf/our_turf = get_turf(src) + SSspatial_grid.add_grid_membership(src, our_turf, SPATIAL_GRID_CONTENTS_TYPE_HEARING) /** * removes the hearing sensitivity channel from the important_recursive_contents list of this and all nested locs containing us if there are no more sources of the trait left @@ -450,18 +497,21 @@ return var/turf/our_turf = get_turf(src) - if(our_turf && SSspatial_grid.initialized) - SSspatial_grid.exit_cell(src, our_turf, RECURSIVE_CONTENTS_HEARING_SENSITIVE) - else if(our_turf && !SSspatial_grid.initialized) - SSspatial_grid.remove_from_pre_init_queue(src, RECURSIVE_CONTENTS_HEARING_SENSITIVE) + /// We get our awareness updated by the important recursive contents stuff, here we remove our membership + SSspatial_grid.remove_grid_membership(src, our_turf, SPATIAL_GRID_CONTENTS_TYPE_HEARING) for(var/atom/movable/location as anything in get_nested_locs(src) + src) - LAZYREMOVEASSOC(location.important_recursive_contents, RECURSIVE_CONTENTS_HEARING_SENSITIVE, src) + var/list/recursive_contents = location.important_recursive_contents // blue hedgehog velocity + recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE] -= src + if(!length(recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE])) + SSspatial_grid.remove_grid_awareness(location, SPATIAL_GRID_CONTENTS_TYPE_HEARING) + ASSOC_UNSETEMPTY(recursive_contents, RECURSIVE_CONTENTS_HEARING_SENSITIVE) + UNSETEMPTY(location.important_recursive_contents) ///allows this movable to know when it has "entered" another area no matter how many movable atoms its stuffed into, uses important_recursive_contents /atom/movable/proc/become_area_sensitive(trait_source = TRAIT_GENERIC) if(!HAS_TRAIT(src, TRAIT_AREA_SENSITIVE)) - for (var/atom/movable/location as anything in get_nested_locs(src) + src) + for(var/atom/movable/location as anything in get_nested_locs(src) + src) LAZYADDASSOCLIST(location.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE, src) ADD_TRAIT(src, TRAIT_AREA_SENSITIVE, trait_source) @@ -473,34 +523,38 @@ if(HAS_TRAIT(src, TRAIT_AREA_SENSITIVE)) return - for (var/atom/movable/location as anything in get_nested_locs(src) + src) - LAZYREMOVE(location.important_recursive_contents[RECURSIVE_CONTENTS_AREA_SENSITIVE], src) + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYREMOVEASSOC(location.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE, src) -///propogates new_client's mob through our nested contents, similar to other important_recursive_contents procs +///propogates ourselves through our nested contents, similar to other important_recursive_contents procs ///main difference is that client contents need to possibly duplicate recursive contents for the clients mob AND its eye -/atom/movable/proc/enable_client_mobs_in_contents(client/new_client) - var/turf/our_turf = get_turf(src) - - if(our_turf && SSspatial_grid.initialized) - SSspatial_grid.enter_cell(src, our_turf, RECURSIVE_CONTENTS_CLIENT_MOBS) - else if(our_turf && !SSspatial_grid.initialized) - SSspatial_grid.enter_pre_init_queue(src, RECURSIVE_CONTENTS_CLIENT_MOBS) - +/mob/proc/enable_client_mobs_in_contents() for(var/atom/movable/movable_loc as anything in get_nested_locs(src) + src) - LAZYORASSOCLIST(movable_loc.important_recursive_contents, RECURSIVE_CONTENTS_CLIENT_MOBS, new_client.mob) - -///Clears the clients channel of this movables important_recursive_contents list and all nested locs -/atom/movable/proc/clear_important_client_contents(client/former_client) + LAZYINITLIST(movable_loc.important_recursive_contents) + var/list/recursive_contents = movable_loc.important_recursive_contents // blue hedgehog velocity + if(!length(recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS])) + SSspatial_grid.add_grid_awareness(movable_loc, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + LAZYINITLIST(recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS]) + recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS] |= src var/turf/our_turf = get_turf(src) + /// We got our awareness updated by the important recursive contents stuff, now we add our membership + SSspatial_grid.add_grid_membership(src, our_turf, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) - if(our_turf && SSspatial_grid.initialized) - SSspatial_grid.exit_cell(src, our_turf, RECURSIVE_CONTENTS_CLIENT_MOBS) - else if(our_turf && !SSspatial_grid.initialized) - SSspatial_grid.remove_from_pre_init_queue(src, RECURSIVE_CONTENTS_CLIENT_MOBS) +///Clears the clients channel of this mob +/mob/proc/clear_important_client_contents() + var/turf/our_turf = get_turf(src) + SSspatial_grid.remove_grid_membership(src, our_turf, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) for(var/atom/movable/movable_loc as anything in get_nested_locs(src) + src) - LAZYREMOVEASSOC(movable_loc.important_recursive_contents, RECURSIVE_CONTENTS_CLIENT_MOBS, former_client.mob) + LAZYINITLIST(movable_loc.important_recursive_contents) + var/list/recursive_contents = movable_loc.important_recursive_contents // blue hedgehog velocity + LAZYINITLIST(recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS]) + recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS] -= src + if(!length(recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS])) + SSspatial_grid.remove_grid_awareness(movable_loc, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + ASSOC_UNSETEMPTY(recursive_contents, RECURSIVE_CONTENTS_CLIENT_MOBS) + UNSETEMPTY(movable_loc.important_recursive_contents) // This proc adds atom/movables to the AI targetable list, i.e. things that the AI (turrets, hostile animals) will attempt to target /atom/movable/proc/add_to_target_grid() @@ -509,7 +563,8 @@ var/turf/our_turf = get_turf(src) if(our_turf && SSspatial_grid.initialized) - SSspatial_grid.enter_cell(src, our_turf) + SSspatial_grid.add_grid_awareness(src, RECURSIVE_CONTENTS_AI_TARGETS) + SSspatial_grid.add_grid_membership(src, our_turf, RECURSIVE_CONTENTS_AI_TARGETS) else if(our_turf && !SSspatial_grid.initialized)//SSspatial_grid isnt init'd yet, add ourselves to the queue SSspatial_grid.enter_pre_init_queue(src, RECURSIVE_CONTENTS_AI_TARGETS) diff --git a/code/game/gamemodes/cult/structures/pylon.dm b/code/game/gamemodes/cult/structures/pylon.dm index 9c9dcb4b0de..9984075df48 100644 --- a/code/game/gamemodes/cult/structures/pylon.dm +++ b/code/game/gamemodes/cult/structures/pylon.dm @@ -267,7 +267,7 @@ return else stuffcache = mobs_in_view(9, src) - if((target in stuffcache) && isInSight(src, target)) + if((target in stuffcache) && is_in_sight(src, target)) fire_at(target) return else @@ -286,7 +286,7 @@ continue if(iscultist(L)) //Pylon wont shoot at cultists or constructs continue - if(!isInSight(src, L)) + if(!is_in_sight(src, L)) continue if(ismech(L)) var/mob/living/heavy_vehicle/mech = L diff --git a/code/game/machinery/hologram.dm b/code/game/machinery/hologram.dm index eeac9f3186c..c1fa360c116 100644 --- a/code/game/machinery/hologram.dm +++ b/code/game/machinery/hologram.dm @@ -370,7 +370,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ H.x = src.x - (connected_pad.x - M.x) H.y = src.y - (connected_pad.y - M.y) set_can_hear_flags(CAN_HEAR_ACTIVE_HOLOCALLS) - if(!isInSight(H, src)) + if(!is_in_sight(H, src)) qdel(H) return H.assume_form(M, long_range) @@ -387,7 +387,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ if(connected_pad) H.x = src.x - (connected_pad.x - M.x) H.y = src.y - (connected_pad.y - M.y) - if(get_dist(H, src) > world.view || !isInSight(H, src)) + if(get_dist(H, src) > world.view || !is_in_sight(H, src)) clear_holo(M) return H.assume_form(M, long_range) @@ -457,7 +457,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ clear_holo(user) return 0 - if(get_dist(user.eyeobj, src) > holo_range || !isInSight(H, src)) + if(get_dist(user.eyeobj, src) > holo_range || !is_in_sight(H, src)) user.holo = null clear_holo(user) return TRUE diff --git a/code/game/machinery/portable_turret.dm b/code/game/machinery/portable_turret.dm index 3934da36a59..d78de3369dc 100644 --- a/code/game/machinery/portable_turret.dm +++ b/code/game/machinery/portable_turret.dm @@ -519,7 +519,7 @@ targets = list() secondarytargets = list() - var/list/potentials = get_targets_in_LOS(world.view, src) + var/list/potentials = get_hearers_in_LOS(world.view, src) if(potentials.len) for(var/mob/living/L in potentials) diff --git a/code/game/machinery/stargazer.dm b/code/game/machinery/stargazer.dm index 3c44e82fa21..872dcae64aa 100644 --- a/code/game/machinery/stargazer.dm +++ b/code/game/machinery/stargazer.dm @@ -40,7 +40,7 @@ /obj/machinery/stargazer/Topic(href, href_list, datum/ui_state/state) if((stat & BROKEN) || (stat & NOPOWER)) return TRUE - if(!isInSight(usr, src)) + if(!is_in_sight(usr, src)) return TRUE if(usr.incapacitated(INCAPACITATION_KNOCKOUT)) return TRUE diff --git a/code/game/objects/items/weapons/neutralizer.dm b/code/game/objects/items/weapons/neutralizer.dm index c08af8d93ac..dc7fb45f82c 100644 --- a/code/game/objects/items/weapons/neutralizer.dm +++ b/code/game/objects/items/weapons/neutralizer.dm @@ -34,7 +34,7 @@ toggle() return var/turf/our_turf = get_turf(src) - if(!(our_turf.z == revenants.revenant_rift.z && get_dist(src, revenants.revenant_rift) < 5 && isInSight(src, revenants.revenant_rift))) + if(!(our_turf.z == revenants.revenant_rift.z && get_dist(src, revenants.revenant_rift) < 5 && is_in_sight(src, revenants.revenant_rift))) tethered = FALSE return if(!tethered) diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index c636c7ad022..6cac5350149 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -54,6 +54,10 @@ var/last_clean //for clean log spam. + ///what /mob/oranges_ear instance is already assigned to us as there should only ever be one. + ///used for guaranteeing there is only one oranges_ear per turf when assigned, speeds up view() iteration + var/mob/oranges_ear/assigned_oranges_ear + // Parent code is duplicated in here instead of ..() for performance reasons. // There's ALSO a copy of this in mine_turfs.dm! /turf/Initialize(mapload, ...) diff --git a/code/modules/mob/death.dm b/code/modules/mob/death.dm index 4b7e75ce029..2c2fccc5c67 100644 --- a/code/modules/mob/death.dm +++ b/code/modules/mob/death.dm @@ -97,7 +97,8 @@ //This might seems like an useless computation to the programmer of the future, why would we do this? //Easy! That's because otherwise, the hostile AI will keep us referenced, leading to an harddel - for(var/mob/living/simple_animal/hostile/hostile_in_sight in get_targets_in_LOS(world.view)) + //(Make this shit a weakref whenever convenient) + for(var/mob/living/simple_animal/hostile/hostile_in_sight in get_hearers_in_LOS(world.view)) hostile_in_sight.targets.Remove(src) if(hostile_in_sight.target_mob == src) diff --git a/code/modules/mob/living/carbon/brain/brain.dm b/code/modules/mob/living/carbon/brain/brain.dm index e128026ef30..fff0ffd9dff 100644 --- a/code/modules/mob/living/carbon/brain/brain.dm +++ b/code/modules/mob/living/carbon/brain/brain.dm @@ -22,7 +22,6 @@ ghostize() //Ghostize checks for key so nothing else is necessary. container = null . = ..() - GC_TEMPORARY_HARDDEL /mob/living/carbon/brain/IsAdvancedToolUser() // to be able to use weapons when piloting a hardsuit return TRUE diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm index a78363e93d8..c1ee7ab80e6 100644 --- a/code/modules/mob/living/simple_animal/hostile/hostile.dm +++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm @@ -155,9 +155,7 @@ return /mob/living/simple_animal/hostile/proc/see_target() - var/los = null - SPATIAL_CHECK_LOS(los, src, target_mob, world.view) - return los + return is_in_sight(src, target_mob) /mob/living/simple_animal/hostile/proc/MoveToTarget() stop_automated_movement = 1 @@ -271,7 +269,7 @@ return /mob/living/simple_animal/hostile/proc/get_targets(dist = world.view) - return get_targets_in_LOS(dist, src) + return get_hearers_in_LOS(dist, src) /mob/living/simple_animal/hostile/death() ..() diff --git a/code/modules/psionics/abilities/grip.dm b/code/modules/psionics/abilities/grip.dm index 81ac4a9ce85..10d70d57f69 100644 --- a/code/modules/psionics/abilities/grip.dm +++ b/code/modules/psionics/abilities/grip.dm @@ -39,9 +39,7 @@ if(!.) return - var/has_line_of_sight = FALSE - SPATIAL_CHECK_LOS(has_line_of_sight, user, victim, world.view) - if(!has_line_of_sight) + if(!is_in_sight(user, victim)) to_chat(user, SPAN_WARNING("You don't have a direct line of sight to \the [victim]!")) return @@ -65,9 +63,7 @@ var/mob/living/M = hit_atom - var/has_line_of_sight = FALSE - SPATIAL_CHECK_LOS(has_line_of_sight, user, M, world.view) - if(!has_line_of_sight) + if(!is_in_sight(user, M)) to_chat(user, SPAN_WARNING("You don't have a direct line of sight to \the [M]!")) return diff --git a/code/unit_tests/ss_test.dm b/code/unit_tests/ss_test.dm index 7553cd69008..65928dd1a34 100644 --- a/code/unit_tests/ss_test.dm +++ b/code/unit_tests/ss_test.dm @@ -15,7 +15,7 @@ SUBSYSTEM_DEF(unit_tests_config) name = "Unit Test Config" init_order = SS_INIT_PERSISTENT_CONFIG - flags = SS_NO_FIRE | SS_NO_INIT + flags = SS_NO_FIRE var/datum/unit_test/UT // Logging/output, use this to log things from outside where a specific unit_test is defined @@ -31,7 +31,7 @@ SUBSYSTEM_DEF(unit_tests_config) ///How many times can the pod retries before the unit test is considered failed var/retries = 0 -/datum/controller/subsystem/unit_tests_config/PreInit() +/datum/controller/subsystem/unit_tests_config/Initialize() UT = new //Acquire our identifier, or enter Hopper mode if failing to do so @@ -81,6 +81,8 @@ SUBSYSTEM_DEF(unit_tests_config) refresh_retries(FALSE) refresh_fail_fast() + return SS_INIT_SUCCESS + /** * Refresh the `retries` variable from the environment variables diff --git a/html/changelogs/fluffyghost-updatespatialgrid.yml b/html/changelogs/fluffyghost-updatespatialgrid.yml new file mode 100644 index 00000000000..aabf2fdf5d2 --- /dev/null +++ b/html/changelogs/fluffyghost-updatespatialgrid.yml @@ -0,0 +1,45 @@ +################################ +# Example Changelog File +# +# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb. +# +# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.) +# When it is, any changes listed below will disappear. +# +# Valid Prefixes: +# bugfix +# wip (For works in progress) +# tweak +# soundadd +# sounddel +# rscadd (general adding of nice things) +# rscdel (general deleting of nice things) +# imageadd +# imagedel +# maptweak +# spellcheck (typo fixes) +# experiment +# balance +# admin +# backend +# security +# refactor +################################# + +# Your name. +author: FluffyGhost + +# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again. +delete-after: True + +# Any changes you've made. See valid prefix list above. +# INDENT WITH TWO SPACES. NOT TABS. SPACES. +# SCREW THIS UP AND IT WON'T WORK. +# Also, all entries are changed into a single [] after a master changelog generation. Just remove the brackets when you add new entries. +# Please surround your changes in double quotes ("), as certain characters otherwise screws up compiling. The quotes will not show up in the changelog. +changes: + - backend: "Updated spatial gridmap to TG version." + - backend: "Some file and file contents reorderings." + - backend: "Altered handling of /atom/movable destruction." + - backend: "Updated how SSunittest_config initializes." + - bugfix: "Resolved an harddel with carbon/brain mobs."