diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm index 49907949dd9b..3e85f7cce22f 100644 --- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm +++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_movable.dm @@ -111,5 +111,8 @@ #define MOVABLE_SAY_QUOTE_MESSAGE_SPANS 2 #define MOVABLE_SAY_QUOTE_MESSAGE_MODS 3 +/// From base of area/Exited(): (area/left, direction) +#define COMSIG_MOVABLE_EXITED_AREA "movable_exited_area" + /// from /datum/thrownthing/tick() #define COMSIG_MOVABLE_THROW_TICK "movable_throw_tick" diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob.dm index 632acf561ab3..2f88057640f7 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob.dm @@ -44,10 +44,11 @@ #define MOVE_ARG_DIRECTION 2 /// From base of /client/Move() #define COMSIG_MOB_CLIENT_MOVED "mob_client_moved" -/// From base of /client/proc/change_view() (mob/source, new_size) -#define COMSIG_MOB_CLIENT_CHANGE_VIEW "mob_client_change_view" /// From base of /mob/proc/reset_perspective() : () #define COMSIG_MOB_RESET_PERSPECTIVE "mob_reset_perspective" + +/// From base of /client/proc/change_view() (mob/source, new_size) +#define COMSIG_MOB_CLIENT_CHANGE_VIEW "mob_client_change_view" /// from base of /client/proc/set_eye() : (atom/old_eye, atom/new_eye) #define COMSIG_CLIENT_SET_EYE "client_set_eye" /// from base of /datum/view_data/proc/afterViewChange() : (view) 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 000000000000..82e69dfcdf8d --- /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/important_recursive_contents.dm b/code/__DEFINES/important_recursive_contents.dm new file mode 100644 index 000000000000..79abb67d1836 --- /dev/null +++ b/code/__DEFINES/important_recursive_contents.dm @@ -0,0 +1,9 @@ +///the area channel of the important_recursive_contents list, everything in here will be sent a signal when their last holding object changes areas +#define RECURSIVE_CONTENTS_AREA_SENSITIVE "recursive_contents_area_sensitive" +///the hearing channel of the important_recursive_contents list, everything in here will count as a hearing atom +#define RECURSIVE_CONTENTS_HEARING_SENSITIVE "recursive_contents_hearing_sensitive" +///the client mobs channel of the important_recursive_contents list, everything in here will be a mob with an attached client +///this is given to both a clients mob, and a clients eye, both point to the clients mob +#define RECURSIVE_CONTENTS_CLIENT_MOBS "recursive_contents_client_mobs" +///the parent of storage components currently shown to some client mob get this. gets removed when nothing is viewing the parent +#define RECURSIVE_CONTENTS_ACTIVE_STORAGE "recursive_contents_active_storage" diff --git a/code/__DEFINES/radio.dm b/code/__DEFINES/radio.dm index d3190b8fa20f..8b1757271719 100644 --- a/code/__DEFINES/radio.dm +++ b/code/__DEFINES/radio.dm @@ -114,3 +114,6 @@ #define REQ_DEP_TYPE_ASSISTANCE (1<<0) #define REQ_DEP_TYPE_SUPPLIES (1<<1) #define REQ_DEP_TYPE_INFORMATION (1<<2) + +///give this to can_receive to specify that there is no restriction on what z level this signal is sent to +#define RADIO_NO_Z_LEVEL_RESTRICTION 0 diff --git a/code/__DEFINES/spatial_gridmap.dm b/code/__DEFINES/spatial_gridmap.dm new file mode 100644 index 000000000000..97a6f9915399 --- /dev/null +++ b/code/__DEFINES/spatial_gridmap.dm @@ -0,0 +1,55 @@ +/// 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) + +//grid contents channels + +///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 +#define SPATIAL_GRID_CONTENTS_TYPE_CLIENTS RECURSIVE_CONTENTS_CLIENT_MOBS +///all atmos machines are stored in this channel (I'm sorry kyler) +#define SPATIAL_GRID_CONTENTS_TYPE_ATMOS "spatial_grid_contents_type_atmos" + +#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) + +// 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 bc7b4b05d6f4..90167cc6f0e4 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -137,7 +137,7 @@ #define INIT_ORDER_INPUT 85 #define INIT_ORDER_SOUNDS 83 #define INIT_ORDER_INSTRUMENTS 82 -#define INIT_ORDER_GREYSCALE 81 +#define INIT_ORDER_GREYSCALE 81 #define INIT_ORDER_VIS 80 #define INIT_ORDER_SECURITY_LEVEL 79 #define INIT_ORDER_MATERIALS 76 @@ -149,6 +149,7 @@ #define INIT_ORDER_TICKER 55 #define INIT_ORDER_MAPPING 50 #define INIT_ORDER_EARLY_ASSETS 48 +#define INIT_ORDER_SPATIAL_GRID 43 #define INIT_ORDER_ECONOMY 40 #define INIT_ORDER_OUTPUTS 35 #define INIT_ORDER_ATOMS 30 diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm index df8ec8bd64b6..3cbb7e7e482c 100644 --- a/code/__HELPERS/_lists.dm +++ b/code/__HELPERS/_lists.dm @@ -52,6 +52,8 @@ #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; ///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 diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm index 8490f613572b..9d74b8f0ef08 100644 --- a/code/__HELPERS/game.dm +++ b/code/__HELPERS/game.dm @@ -13,103 +13,6 @@ name = Gibberish(name, TRUE, 90) return format_text ? format_text(name) : name -/proc/get_areas_in_range(dist=0, atom/center=usr) - if(!dist) - var/turf/T = get_turf(center) - return T ? list(T.loc) : list() - if(!center) - return list() - - var/list/turfs = RANGE_TURFS(dist, center) - var/list/areas = list() - for(var/V in turfs) - var/turf/T = V - areas |= T.loc - return areas - -/proc/get_adjacent_areas(atom/center) - . = list(get_area(get_ranged_target_turf(center, NORTH, 1)), - get_area(get_ranged_target_turf(center, SOUTH, 1)), - get_area(get_ranged_target_turf(center, EAST, 1)), - get_area(get_ranged_target_turf(center, WEST, 1))) - listclearnulls(.) - -///Returns the open turf next to the center in a specific direction -/proc/get_open_turf_in_dir(atom/center, dir) - var/turf/open/get_turf = get_step(center, dir) - if(istype(get_turf)) - return get_turf - -///Returns a list with all the adjacent open turfs. Clears the list of nulls in the end. -/proc/get_adjacent_open_turfs(atom/center) - var/list/hand_back = list() - // Inlined get_open_turf_in_dir, just to be fast - var/turf/open/new_turf = get_step(center, NORTH) - if(istype(new_turf)) - hand_back += new_turf - new_turf = get_step(center, SOUTH) - if(istype(new_turf)) - hand_back += new_turf - new_turf = get_step(center, EAST) - if(istype(new_turf)) - hand_back += new_turf - new_turf = get_step(center, WEST) - if(istype(new_turf)) - hand_back += new_turf - return hand_back - - -/proc/get_adjacent_open_areas(atom/center) - . = list() - var/list/adjacent_turfs = get_adjacent_open_turfs(center) - for(var/I in adjacent_turfs) - . |= get_area(I) - -/** - * Get a bounding box of a list of atoms. - * - * Arguments: - * - atoms - List of atoms. Can accept output of view() and range() procs. - * - * Returns: list(x1, y1, x2, y2) - */ -/proc/get_bbox_of_atoms(list/atoms) - var/list/list_x = list() - var/list/list_y = list() - for(var/_a in atoms) - var/atom/a = _a - list_x += a.x - list_y += a.y - return list( - min(list_x), - min(list_y), - max(list_x), - max(list_y)) - - -// Like view but bypasses luminosity check - -/proc/get_hear(range, atom/source) - - var/lum = source.luminosity - source.luminosity = 6 - - var/list/heard = view(range, source) - source.luminosity = lum - - return heard - -/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/C in GLOB.alive_mob_list) - if(!istype(C, check_type)) - continue - if(C == must_be_alone) - continue - if(our_area == get_area(C)) - return 0 - return 1 - //We used to use linear regression to approximate the answer, but Mloc realized this was actually faster. //And lo and behold, it is, and it's more accurate to boot. /proc/cheap_hypotenuse(Ax,Ay,Bx,By) @@ -145,27 +48,6 @@ //turfs += centerturf return atoms -/proc/get_dist_euclidian(atom/Loc1 as turf|mob|obj,atom/Loc2 as turf|mob|obj) - var/dx = Loc1.x - Loc2.x - var/dy = Loc1.y - Loc2.y - - var/dist = sqrt(dx**2 + dy**2) - - return dist - -///Returns a list of turfs around a center based on RANGE_TURFS() -/proc/circle_range_turfs(center = usr, radius = 3) - - var/turf/center_turf = get_turf(center) - var/list/turfs = new/list() - var/rsq = radius * (radius + 0.5) - - for(var/turf/checked_turf as anything in RANGE_TURFS(radius, center_turf)) - var/dx = checked_turf.x - center_turf.x - var/dy = checked_turf.y - center_turf.y - if(dx * dx + dy * dy <= rsq) - turfs += checked_turf - return turfs /proc/circleviewturfs(center=usr,radius=3) //Is there even a diffrence between this proc and circle_range_turfs()? // Yes @@ -180,7 +62,6 @@ turfs += T return turfs - //This is the new version of recursive_mob_check, used for say(). //The other proc was left intact because morgue trays use it. //Sped this up again for real this time @@ -231,129 +112,6 @@ return -// Better recursive loop, technically sort of not actually recursive cause that shit is stupid, enjoy. -//No need for a recursive limit either -/proc/recursive_mob_check(atom/O,client_check=1,sight_check=1,include_radio=1) - - var/list/processing_list = list(O) - var/list/processed_list = list() - var/list/found_mobs = list() - - while(processing_list.len) - - var/atom/A = processing_list[1] - var/passed = 0 - - if(ismob(A)) - var/mob/A_tmp = A - passed=1 - - if(client_check && !A_tmp.client) - passed=0 - - if(sight_check && !isInSight(A_tmp, O)) - passed=0 - - else if(include_radio && istype(A, /obj/item/radio)) - passed=1 - - if(sight_check && !isInSight(A, O)) - passed=0 - - if(passed) - found_mobs |= A - - for(var/atom/B in A) - if(!processed_list[B]) - processing_list |= B - - processing_list.Cut(1, 2) - processed_list[A] = A - - return found_mobs - - -/proc/get_hearers_in_view(R, atom/source) - // Returns a list of hearers in view(R) from source (ignoring luminosity). Used in saycode. - var/turf/T = get_turf(source) - . = list() - if(!T) - return - var/list/processing_list = list() - if (R == 0) // if the range is zero, we know exactly where to look for, we can skip view - processing_list += T.contents // We can shave off one iteration by assuming turfs cannot hear - else // A variation of get_hear inlined here to take advantage of the compiler's fastpath for obj/mob in view - var/lum = T.luminosity - T.luminosity = 6 // This is the maximum luminosity - for(var/mob/M in view(R, T)) - processing_list += M - for(var/obj/O in view(R, T)) - processing_list += O - T.luminosity = lum - - var/i = 0 - while(i < length(processing_list)) // recursive_hear_check inlined here - var/atom/A = processing_list[++i] - if(A.flags_1 & HEAR_1) - . += A - processing_list += A.contents - -/proc/get_mobs_in_radio_ranges(list/obj/item/radio/radios) - . = list() - // Returns a list of mobs who can hear any of the radios given in @radios - for(var/obj/item/radio/R in radios) - if(R) - . |= get_hearers_in_view(R.canhear_range, R) - - -#define SIGNV(X) ((X<0)?-1:1) - -/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) - return 1 //Light cannot be blocked on same tile - else - var/s = SIGN(Y2-Y1) - Y1+=s - while(Y1!=Y2) - T=locate(X1,Y1,Z) - if(IS_OPAQUE_TURF(T)) - return 0 - Y1+=s - else - var/m=(32*(Y2-Y1)+(PY2-PY1))/(32*(X2-X1)+(PX2-PX1)) - var/b=(Y1+PY1/32-0.015625)-m*(X1+PX1/32-0.015625) //In tiles - var/signX = SIGN(X2-X1) - var/signY = SIGN(Y2-Y1) - if(X1 0 + * because view() isnt a raycasting algorithm, this does not hold symmetry to it. something in view might not be hearable with this. + * if you want that use get_hearers_in_view() - however thats significantly more expensive + * + * * view_radius - what radius search circle we are using, worse performance as this increases but not as much as it used to + * * 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_LOS(view_radius, atom/source) + var/turf/center_turf = get_turf(source) + if(!center_turf) + return + + if(view_radius <= 0)//special case for if only source cares + . = list() + for(var/atom/movable/target as anything in center_turf) + var/list/hearing_contents = target.important_recursive_contents?[RECURSIVE_CONTENTS_HEARING_SENSITIVE] + if(hearing_contents) + . += hearing_contents + return + + . = SSspatial_grid.orthogonal_range_search(source, SPATIAL_GRID_CONTENTS_TYPE_HEARING, view_radius) + + for(var/atom/movable/target as anything in .) + var/turf/target_turf = get_turf(target) + + var/distance = get_dist(center_turf, target_turf) + + if(distance > view_radius) + . -= target + continue + + else if(distance < 2) //we should always be able to see something 0 or 1 tiles away + continue + + //this turf search algorithm is the worst scaling part of this proc, scaling worse than view() for small-moderate ranges and > 50 length contents_to_return + //luckily its significantly faster than view for large ranges in large spaces and/or relatively few contents_to_return + //i can do things that would scale better, but they would be slower for low volume searches which is the vast majority of the current workload + //maybe in the future a high volume algorithm would be worth it + var/turf/inbetween_turf = center_turf + + //this is the lowest overhead way of doing a loop in dm other than a goto. distance is guaranteed to be >= steps taken to target by this algorithm + for(var/step_counter in 1 to distance) + inbetween_turf = get_step_towards(inbetween_turf, target_turf) + + 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(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/radio/radios) + . = list() + // Returns a list of mobs who can hear any of the radios given in @radios + for(var/obj/item/radio/radio as anything in radios) + . |= get_hearers_in_LOS(radio.canhear_range, radio, FALSE) + +///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) + return TRUE //Light cannot be blocked on same tile + else + var/s = SIGN(Y2-Y1) + Y1+=s + while(Y1!=Y2) + T=locate(X1,Y1,Z) + if(IS_OPAQUE_TURF(T)) + return FALSE + Y1+=s + else + var/m=(32*(Y2-Y1)+(PY2-PY1))/(32*(X2-X1)+(PX2-PX1)) + var/b=(Y1+PY1/32-0.015625)-m*(X1+PX1/32-0.015625) //In tiles + var/signX = SIGN(X2-X1) + var/signY = SIGN(Y2-Y1) + if(X1>1 //Counters for steps taken, setting to distance/2 - var/y=dyabs>>1 //Bit-shifting makes me l33t. It also makes getline() unnessecarrily fast. - var/j //Generic integer for counting - if(dxabs>=dyabs) //x distance is greater than y - for(j=0;j=dxabs) //Every dyabs steps, step once in y direction - y-=dxabs - py+=sdy - px+=sdx //Step on in x direction - line+=locate(px,py,M.z)//Add the turf to the list - else - for(j=0;j=dyabs) - x-=dyabs - px+=sdx - py+=sdy - line+=locate(px,py,M.z) +/proc/getline(atom/M,atom/N)//Ultra-Fast Bresenham Line-Drawing Algorithm + var/px=M.x //starting x + var/py=M.y + var/line[] = list(locate(px,py,M.z)) + var/dx=N.x-px //x distance + var/dy=N.y-py + var/dxabs = abs(dx)//Absolute value of x distance + var/dyabs = abs(dy) + var/sdx = SIGN(dx) //Sign of x distance (+ or -) + var/sdy = SIGN(dy) + var/x=dxabs>>1 //Counters for steps taken, setting to distance/2 + var/y=dyabs>>1 //Bit-shifting makes me l33t. It also makes getline() unnessecarrily fast. + var/j //Generic integer for counting + if(dxabs>=dyabs) //x distance is greater than y + for(j=0;j=dxabs) //Every dyabs steps, step once in y direction + y-=dxabs + py+=sdy + px+=sdx //Step on in x direction + line+=locate(px,py,M.z)//Add the turf to the list + else + for(j=0;j=dyabs) + x-=dyabs + px+=sdx + py+=sdy + line+=locate(px,py,M.z) return line //Returns whether or not a player is a guest using their ckey as an input diff --git a/code/controllers/subsystem/spatial_gridmap.dm b/code/controllers/subsystem/spatial_gridmap.dm new file mode 100644 index 000000000000..65bcb0ec365b --- /dev/null +++ b/code/controllers/subsystem/spatial_gridmap.dm @@ -0,0 +1,847 @@ +///the subsystem creates this many [/mob/oranges_ear] mob instances during init. allocations that require more than this create more. +#define NUMBER_OF_PREGENERATED_ORANGES_EARS 2500 + +/** + * # Spatial Grid Cell + * + * used by [/datum/controller/subsystem/spatial_grid] to cover every z level so that the coordinates of every turf in the world corresponds to one of these in + * the subsystems list of grid cells by z level. each one of these contains content lists holding all atoms meeting a certain criteria that is in our borders. + * these datums shouldnt have significant behavior, they should just hold data. the lists are filled and emptied by the subsystem. + */ +/datum/spatial_grid_cell + ///our x index in the list of cells. this is our index inside of our row list + var/cell_x + ///our y index in the list of cells. this is the index of our row list inside of our z level grid + var/cell_y + ///which z level we belong to, corresponding to the index of our gridmap in SSspatial_grid.grids_by_z_level + var/cell_z + //every data point in a grid cell is separated by usecase + + //when empty, the contents lists of these grid cell datums are just references to a dummy list from SSspatial_grid + //this is meant to allow a great compromise between memory usage and speed. + //now orthogonal_range_search() doesnt need to check if the list is null and each empty list is taking 12 bytes instead of 24 + //the only downside is that it needs to be switched over to a new list when it goes from 0 contents to > 0 contents and switched back on the opposite case + + ///every hearing sensitive movable inside this cell + var/list/hearing_contents + ///every client possessed mob inside this cell + var/list/client_contents + ///every atmos machine inside this cell + var/list/atmos_contents + +/datum/spatial_grid_cell/New(cell_x, cell_y, cell_z) + . = ..() + src.cell_x = cell_x + src.cell_y = cell_y + src.cell_z = cell_z + //cache for sanic speed (lists are references anyways) + var/list/dummy_list = SSspatial_grid.dummy_list + + if(length(dummy_list)) + dummy_list.Cut() + stack_trace("SSspatial_grid.dummy_list had something inserted into it at some point! this is a problem as it is supposed to stay empty") + hearing_contents = dummy_list + client_contents = dummy_list + atmos_contents = dummy_list + +/datum/spatial_grid_cell/Destroy(force) + if(force)//the response to someone trying to qdel this is a right proper fuck you + stack_trace("dont try to destroy spatial grid cells without a good reason. if you need to do it use force") + return + + . = ..() + +/** + * # Spatial Grid + * + * a gamewide grid of spatial_grid_cell datums, each "covering" [SPATIAL_GRID_CELLSIZE] ^ 2 turfs. + * each spatial_grid_cell datum stores information about what is inside its covered area, so that searches through that area dont have to literally search + * through all turfs themselves to know what is within it since view() calls are expensive, and so is iterating through stuff you dont want. + * this allows you to only go through lists of what you want very cheaply. + * + * you can also register to objects entering and leaving a spatial cell, this allows you to do things like stay idle until a player enters, so you wont + * have to use expensive view() calls or iteratite over the global list of players and call get_dist() on every one. which is fineish for a few things, but is + * k * n operations for k objects iterating through n players. + * + * currently this system is only designed for searching for relatively uncommon things, small subsets of /atom/movable. + * dont add stupid shit to the cells please, keep the information that the cells store to things that need to be searched for often + * + * The system currently implements two different "classes" of spatial type + * + * The first exists to support important_recursive_contents. + * So if a client is inside a locker and the locker crosses a boundary, you'll still get a signal from the spatial grid. + * These types are [SPATIAL_GRID_CONTENTS_TYPE_HEARING] and [SPATIAL_GRID_CONTENTS_TYPE_CLIENTS] + * + * The second pattern is more paired down, and supports more wide use. + * Rather then the object and anything the object is in being sensitive, it's limited to just the object itself + * Currently only [SPATIAL_GRID_CONTENTS_TYPE_ATMOS] uses this pattern. This is because it's far more common, and so worth optimizing + * + */ +SUBSYSTEM_DEF(spatial_grid) + can_fire = FALSE + init_order = INIT_ORDER_SPATIAL_GRID + name = "Spatial Grid" + + ///list of the spatial_grid_cell datums per z level, arranged in the order of y index then x index + var/list/grids_by_z_level = list() + ///everything that spawns before us is added to this list until we initialize + var/list/waiting_to_add_by_type = list(SPATIAL_GRID_CONTENTS_TYPE_HEARING = list(), SPATIAL_GRID_CONTENTS_TYPE_CLIENTS = list(), SPATIAL_GRID_CONTENTS_TYPE_ATMOS = list()) + ///associative list of the form: movable.spatial_grid_key (string) -> inner list of spatial grid types for that key. + ///inner lists contain contents channel types such as SPATIAL_GRID_CONTENTS_TYPE_HEARING etc. + ///we use this to make adding to a cell static cost, and to save on memory + var/list/spatial_grid_categories = list() + + var/cells_on_x_axis = 0 + var/cells_on_y_axis = 0 + + ///empty spatial grid cell content lists are just a reference to this instead of a standalone list to save memory without needed to check if its null when iterating + var/list/dummy_list = list() + + ///list of all of /mob/oranges_ear instances we have pregenerated for view() iteration speedup + var/list/mob/oranges_ear/pregenerated_oranges_ears = list() + ///how many pregenerated /mob/oranges_ear instances currently exist. this should hopefully never exceed its starting value + var/number_of_oranges_ears = NUMBER_OF_PREGENERATED_ORANGES_EARS + +/datum/controller/subsystem/spatial_grid/Initialize() + 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/datum/space_level/z_level as anything in SSmapping.z_list) + propogate_spatial_grid_to_new_z(null, z_level) + CHECK_TICK + + //go through the pre init queue for anything waiting to be let in the grid + for(var/channel_type in waiting_to_add_by_type) + for(var/atom/movable/movable as anything in waiting_to_add_by_type[channel_type]) + var/turf/movable_turf = get_turf(movable) + if(movable_turf) + enter_cell(movable, movable_turf) + + UnregisterSignal(movable, COMSIG_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_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 + +///removes an initialized and probably deleted movable from our pre init queue before we're initialized +/datum/controller/subsystem/spatial_grid/proc/remove_from_pre_init_queue(atom/movable/movable_to_remove, exclusive_type) + if(exclusive_type) + waiting_to_add_by_type[exclusive_type] -= movable_to_remove + + var/waiting_movable_is_in_other_queues = FALSE//we need to check if this movable is inside the other queues + for(var/type in waiting_to_add_by_type) + if(movable_to_remove in waiting_to_add_by_type[type]) + waiting_movable_is_in_other_queues = TRUE + + if(!waiting_movable_is_in_other_queues) + UnregisterSignal(movable_to_remove, COMSIG_QDELETING) + + return + + UnregisterSignal(movable_to_remove, COMSIG_QDELETING) + for(var/type in waiting_to_add_by_type) + waiting_to_add_by_type[type] -= movable_to_remove + +///if a movable is inside our pre init queue before we're initialized and it gets deleted we need to remove that reference with this proc +/datum/controller/subsystem/spatial_grid/proc/queued_item_deleted(atom/movable/movable_being_deleted) + SIGNAL_HANDLER + remove_from_pre_init_queue(movable_being_deleted, null) + +///creates the spatial grid for a new z level +/datum/controller/subsystem/spatial_grid/proc/propogate_spatial_grid_to_new_z(datum/controller/subsystem/processing/dcs/fucking_dcs, datum/space_level/z_level) + SIGNAL_HANDLER + + var/list/new_cell_grid = list() + + grids_by_z_level += list(new_cell_grid) + + for(var/y in 1 to cells_on_y_axis) + new_cell_grid += list(list()) + for(var/x in 1 to cells_on_x_axis) + var/datum/spatial_grid_cell/cell = new(x, y, z_level.z_value) + new_cell_grid[y] += cell + +///adds cells to the grid for every z level when world.maxx or world.maxy is expanded after this subsystem is initialized. hopefully this is never needed. +///because i never tested this. +/datum/controller/subsystem/spatial_grid/proc/after_world_bounds_expanded(datum/controller/subsystem/processing/dcs/fucking_dcs, has_expanded_world_maxx, has_expanded_world_maxy) + SIGNAL_HANDLER + var/old_x_axis = cells_on_x_axis + var/old_y_axis = cells_on_y_axis + + cells_on_x_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxx) + cells_on_y_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxy) + + for(var/z_level in 1 to length(grids_by_z_level)) + var/list/z_level_gridmap = grids_by_z_level[z_level] + + for(var/cell_row_for_expanded_y_axis in 1 to cells_on_y_axis) + + if(cell_row_for_expanded_y_axis > old_y_axis)//we are past the old length of the number of rows, so add to the list + z_level_gridmap += list(list()) + + //now we know theres a row at this position, so add cells to it that need to be added and update the ones that already exist + var/list/cell_row = z_level_gridmap[cell_row_for_expanded_y_axis] + + for(var/grid_cell_for_expanded_x_axis in 1 to cells_on_x_axis) + + if(grid_cell_for_expanded_x_axis > old_x_axis) + var/datum/spatial_grid_cell/new_cell_inserted = new(grid_cell_for_expanded_x_axis, cell_row_for_expanded_y_axis, z_level) + cell_row += new_cell_inserted + continue + + //now we know the cell index we're at contains an already existing cell that needs its x and y values updated + var/datum/spatial_grid_cell/old_cell_that_needs_updating = cell_row[grid_cell_for_expanded_x_axis] + old_cell_that_needs_updating.cell_x = grid_cell_for_expanded_x_axis + old_cell_that_needs_updating.cell_y = cell_row_for_expanded_y_axis + +///the left or bottom side index of a box composed of spatial grid cells with the given actual center x or y coordinate +#define BOUNDING_BOX_MIN(center_coord) max(GET_SPATIAL_INDEX(center_coord - range), 1) +///the right or upper side index of a box composed of spatial grid cells with the given center x or y coordinate. +///outputted value cant exceed the number of cells on that axis +#define BOUNDING_BOX_MAX(center_coord, axis_size) min(GET_SPATIAL_INDEX(center_coord + range), axis_size) + +/** + * https://en.wikipedia.org/wiki/Range_searching#Orthogonal_range_searching + * + * searches through the grid cells intersecting a rectangular search space (with sides of length 2 * range) then returns all contents of type inside them. + * much faster than iterating through view() to find all of what you want. + * + * this does NOT return things only in range distance from center! the search space is a square not a circle, if you want only things in a certain distance + * then you need to filter that yourself + * + * * center - the atom that is the center of the searched circle + * * type - the type of grid contents you are looking for, see __DEFINES/spatial_grid.dm + * * range - the bigger this is, the more spatial grid cells the search space intersects + */ +/datum/controller/subsystem/spatial_grid/proc/orthogonal_range_search(atom/center, type, range) + var/turf/center_turf = get_turf(center) + + var/center_x = center_turf.x//used inside the macros + var/center_y = center_turf.y + + . = list() + + //technically THIS list only contains lists, but inside those lists are grid cell datums and we can go without a SINGLE var init if we do this + var/list/list/datum/spatial_grid_cell/grid_level = grids_by_z_level[center_turf.z] + + switch(type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + for(var/row in BOUNDING_BOX_MIN(center_y) to BOUNDING_BOX_MAX(center_y, cells_on_y_axis)) + for(var/x_index in BOUNDING_BOX_MIN(center_x) to BOUNDING_BOX_MAX(center_x, cells_on_x_axis)) + + . += grid_level[row][x_index].client_contents + + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + for(var/row in BOUNDING_BOX_MIN(center_y) to BOUNDING_BOX_MAX(center_y, cells_on_y_axis)) + for(var/x_index in BOUNDING_BOX_MIN(center_x) to BOUNDING_BOX_MAX(center_x, cells_on_x_axis)) + + . += grid_level[row][x_index].hearing_contents + + if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS) + for(var/row in BOUNDING_BOX_MIN(center_y) to BOUNDING_BOX_MAX(center_y, cells_on_y_axis)) + for(var/x_index in BOUNDING_BOX_MIN(center_x) to BOUNDING_BOX_MAX(center_x, cells_on_x_axis)) + . += grid_level[row][x_index].atmos_contents + + return . + +///get the grid cell encomapassing targets coordinates +/datum/controller/subsystem/spatial_grid/proc/get_cell_of(atom/target) + var/turf/target_turf = get_turf(target) + if(!target_turf) + return + + return grids_by_z_level[target_turf.z][GET_SPATIAL_INDEX(target_turf.y)][GET_SPATIAL_INDEX(target_turf.x)] + +///get all grid cells intersecting the bounding box around center with sides of length 2 * range +/datum/controller/subsystem/spatial_grid/proc/get_cells_in_range(atom/center, range) + return get_cells_in_bounds(center, range, range) + +///get all grid cells intersecting the bounding box around center with sides of length (2 * range_x, 2 * range_y) +/datum/controller/subsystem/spatial_grid/proc/get_cells_in_bounds(atom/center, range_x, range_y) + var/turf/center_turf = get_turf(center) + + var/center_x = center_turf.x + var/center_y = center_turf.y + + var/list/intersecting_grid_cells = list() + + //the minimum x and y cell indexes to test + var/min_x = max(GET_SPATIAL_INDEX(center_x - range_x), 1) + var/min_y = max(GET_SPATIAL_INDEX(center_y - range_y), 1)//calculating these indices only takes around 2 microseconds + + //the maximum x and y cell indexes to test + var/max_x = min(GET_SPATIAL_INDEX(center_x + range_x), cells_on_x_axis) + var/max_y = min(GET_SPATIAL_INDEX(center_y + range_y), cells_on_y_axis) + + var/list/grid_level = grids_by_z_level[center_turf.z] + + for(var/row in min_y to max_y) + var/list/grid_row = grid_level[row] + + for(var/x_index in min_x to max_x) + intersecting_grid_cells += grid_row[x_index] + + return intersecting_grid_cells + +/// Adds grid awareness to the passed in atom, of the passed in type +/// Basically, when this atom moves between grids, it wants to have enter/exit cell called on it +/datum/controller/subsystem/spatial_grid/proc/add_grid_awareness(atom/movable/add_to, type) + // We need to ensure we have a new list reference, to build our new key out of + var/list/current_list = spatial_grid_categories[add_to.spatial_grid_key] + if(current_list) + current_list = current_list.Copy() + else + current_list = list() + // Now we do a binary insert, to ensure it's sorted (don't wanna overcache) + BINARY_INSERT_DEFINE(type, current_list, SORT_VAR_NO_TYPE, type, SORT_COMPARE_DIRECTLY, COMPARE_KEY) + update_grid_awareness(add_to, current_list) + +/// Removes grid awareness from the passed in atom, of the passed in type +/datum/controller/subsystem/spatial_grid/proc/remove_grid_awareness(atom/movable/remove_from, type) + // We need to ensure we have a new list reference, to build our new key out of + var/list/current_list = spatial_grid_categories[remove_from.spatial_grid_key] + if(current_list) + current_list = current_list.Copy() + else + current_list = list() + current_list -= type + update_grid_awareness(remove_from, current_list) + +/// Alerts the atom's current cell that it wishes to be treated as a member +/// This functionally amounts to "hey, I was recently made aware by [add_grid_awareness], please insert me into my current cell" +/datum/controller/subsystem/spatial_grid/proc/add_grid_membership(atom/movable/add_to, turf/target_turf, type) + if(!target_turf) + return + if(initialized) + add_single_type(add_to, target_turf, type) + else //SSspatial_grid isnt init'd yet, add ourselves to the queue + enter_pre_init_queue(add_to, type) + +/// Removes grid membership from the passed in atom, of the passed in type +/datum/controller/subsystem/spatial_grid/proc/remove_grid_membership(atom/movable/remove_from, turf/target_turf, type) + if(!target_turf) + return + if(initialized) + remove_single_type(remove_from, target_turf, type) + else //SSspatial_grid isnt init'd yet, remove ourselves from the queue + remove_from_pre_init_queue(remove_from, type) + +/// Updates the string that atoms hold that stores their grid awareness +/// We will use it to key into their spatial grid categories later +/datum/controller/subsystem/spatial_grid/proc/update_grid_awareness(atom/movable/update, list/new_list) + // We locally store a stringified version of the list, to prevent people trying to mutate it + update.spatial_grid_key = new_list.Join("-") + // Ensure the global representation is cached + if(!spatial_grid_categories[update.spatial_grid_key]) + spatial_grid_categories[update.spatial_grid_key] = new_list + +///find the spatial map cell that target belongs to, then add the target to it, as its type prefers. +///make sure to provide the turf new_target is "in" +/datum/controller/subsystem/spatial_grid/proc/enter_cell(atom/movable/new_target, turf/target_turf) + if(!initialized) + return + if(QDELETED(new_target)) + CRASH("qdeleted or null target trying to enter the spatial grid!") + + if(!target_turf || !new_target.spatial_grid_key) + CRASH("null turf loc or a new_target that doesn't support it trying to enter the spatial grid!") + + var/x_index = GET_SPATIAL_INDEX(target_turf.x) + var/y_index = GET_SPATIAL_INDEX(target_turf.y) + var/z_index = target_turf.z + + var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index] + for(var/type in spatial_grid_categories[new_target.spatial_grid_key]) + switch(type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + var/list/new_target_contents = new_target.important_recursive_contents //cache for sanic speeds (lists are references anyways) + GRID_CELL_SET(intersecting_cell.client_contents, new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS]) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS]) + + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + var/list/new_target_contents = new_target.important_recursive_contents + GRID_CELL_SET(intersecting_cell.hearing_contents, new_target.important_recursive_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING]) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_HEARING), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING]) + + if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS) + GRID_CELL_SET(intersecting_cell.atmos_contents, new_target) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_ATMOS), new_target) + +///acts like enter_cell() but only adds the target to a specified type of grid cell contents list +/datum/controller/subsystem/spatial_grid/proc/add_single_type(atom/movable/new_target, turf/target_turf, exclusive_type) + if(!initialized) + return + if(QDELETED(new_target)) + CRASH("qdeleted or null target trying to enter the spatial grid!") + + if(!target_turf || !(exclusive_type in spatial_grid_categories[new_target.spatial_grid_key])) + CRASH("null turf loc or a new_target that doesn't support it trying to enter the spatial grid as a [exclusive_type]!") + + var/x_index = GET_SPATIAL_INDEX(target_turf.x) + var/y_index = GET_SPATIAL_INDEX(target_turf.y) + var/z_index = target_turf.z + + var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index] + switch(exclusive_type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + var/list/new_target_contents = new_target.important_recursive_contents //cache for sanic speeds (lists are references anyways) + GRID_CELL_SET(intersecting_cell.client_contents, new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS]) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS]) + + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + var/list/new_target_contents = new_target.important_recursive_contents + GRID_CELL_SET(intersecting_cell.hearing_contents, new_target.important_recursive_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING]) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_HEARING), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING]) + + if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS) + GRID_CELL_SET(intersecting_cell.atmos_contents, new_target) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_ATMOS), new_target) + + return intersecting_cell + +/** + * find the spatial map cell that target used to belong to, then remove the target (and sometimes its important_recusive_contents) from it. + * make sure to provide the turf old_target used to be "in" + * + * * old_target - the thing we want to remove from the spatial grid cell + * * target_turf - the turf we use to determine the cell we're removing from + * * exclusive_type - either null or a valid contents channel. if you just want to remove a single type from the grid cell then use this + */ +/datum/controller/subsystem/spatial_grid/proc/exit_cell(atom/movable/old_target, turf/target_turf, exclusive_type) + if(!initialized) + return + + if(!target_turf || !old_target.spatial_grid_key) + stack_trace("/datum/controller/subsystem/spatial_grid/proc/exit_cell() was given null arguments or a old_target that doesn't use the spatial grid!") + return FALSE + + var/x_index = GET_SPATIAL_INDEX(target_turf.x) + var/y_index = GET_SPATIAL_INDEX(target_turf.y) + var/z_index = target_turf.z + + var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index] + for(var/type in spatial_grid_categories[old_target.spatial_grid_key]) + switch(type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + var/list/old_target_contents = old_target.important_recursive_contents?[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(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(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) + + return TRUE + +///acts like exit_cell() but only removes the target from the specified type of grid cell contents list +/datum/controller/subsystem/spatial_grid/proc/remove_single_type(atom/movable/old_target, turf/target_turf, exclusive_type) + if(!target_turf || !exclusive_type || !old_target.spatial_grid_key) + stack_trace("/datum/controller/subsystem/spatial_grid/proc/remove_single_type() was given null arguments or an old_target that doesn't use the spatial grid!") + return FALSE + + if(!(exclusive_type in spatial_grid_categories[old_target.spatial_grid_key])) + return FALSE + + var/x_index = GET_SPATIAL_INDEX(target_turf.x) + var/y_index = GET_SPATIAL_INDEX(target_turf.y) + var/z_index = target_turf.z + + var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index] + + switch(exclusive_type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + var/list/old_target_contents = old_target.important_recursive_contents?[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(!initialized) + remove_from_pre_init_queue(to_remove)//the spatial grid doesnt exist yet, so just take it out of the queue + 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 + + 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) + + var/list/queues_containing_movable = list() + for(var/queue_channel in waiting_to_add_by_type) + var/list/queue_list = waiting_to_add_by_type[queue_channel] + if(to_remove in queue_list) + queues_containing_movable += queue_channel//just add the associative key + if(remove_from_cells) + queue_list -= to_remove + + if(!initialized) + return queues_containing_movable + + var/list/containing_cells = list() + for(var/list/z_level_grid as anything in grids_by_z_level) + for(var/list/cell_row as anything in z_level_grid) + for(var/datum/spatial_grid_cell/cell as anything in cell_row) + if(to_remove in (cell.hearing_contents | cell.client_contents | cell.atmos_contents)) + containing_cells += cell + if(remove_from_cells) + force_remove_from_cell(to_remove, cell) + + return containing_cells + +///debug proc for checking if a movable is in multiple cells when it shouldnt be (ie always unless multitile entering is implemented) +/atom/proc/find_all_cells_containing(remove_from_cells = FALSE) + var/datum/spatial_grid_cell/real_cell = SSspatial_grid.get_cell_of(src) + var/list/containing_cells = SSspatial_grid.find_hanging_cell_refs_for_movable(src, remove_from_cells) + + message_admins("[src] is located in the contents of [length(containing_cells)] spatial grid cells") + + var/cell_coords = "the following cells contain [src]: " + for(var/datum/spatial_grid_cell/cell as anything in containing_cells) + cell_coords += "([cell.cell_x], [cell.cell_y], [cell.cell_z]), " + + message_admins(cell_coords) + message_admins("[src] is supposed to only be contained in the cell at indexes ([real_cell.cell_x], [real_cell.cell_y], [real_cell.cell_z]). but is contained at the cells at [cell_coords]") + +///creates number_to_generate new oranges_ear's and adds them to the subsystems list of ears. +///i really fucking hope this never gets called after init :clueless: +/datum/controller/subsystem/spatial_grid/proc/pregenerate_more_oranges_ears(number_to_generate) + for(var/new_ear in 1 to number_to_generate) + pregenerated_oranges_ears += new/mob/oranges_ear(null) + + number_of_oranges_ears = length(pregenerated_oranges_ears) + +///allocate one [/mob/oranges_ear] mob per turf containing atoms_that_need_ears and give them a reference to every listed atom in their turf. +///if an oranges_ear is allocated to a turf that already has an oranges_ear then the second one fails to allocate (and gives the existing one the atom it was assigned to) +/datum/controller/subsystem/spatial_grid/proc/assign_oranges_ears(list/atoms_that_need_ears) + var/input_length = length(atoms_that_need_ears) + + if(input_length > number_of_oranges_ears) + stack_trace("somehow, for some reason, more than the preset generated number of oranges ears was requested. thats fucking [number_of_oranges_ears]. this is not good that should literally never happen") + pregenerate_more_oranges_ears(input_length - number_of_oranges_ears)//im still gonna DO IT but ill complain about it + + . = list() + + ///the next unallocated /mob/oranges_ear that we try to allocate to assigned_atom's turf + var/mob/oranges_ear/current_ear + ///the next atom in atoms_that_need_ears an ear assigned to it + var/atom/assigned_atom + ///the turf loc of the current assigned_atom. turfs are used to track oranges_ears already assigned to one location so we dont allocate more than one + ///because allocating more than one oranges_ear to a given loc wastes view iterations + var/turf/turf_loc + + for(var/current_ear_index in 1 to input_length) + assigned_atom = atoms_that_need_ears[current_ear_index] + + turf_loc = get_turf(assigned_atom) + if(!turf_loc) + continue + + current_ear = pregenerated_oranges_ears[current_ear_index] + + if(turf_loc.assigned_oranges_ear) + turf_loc.assigned_oranges_ear.references += assigned_atom + continue //if theres already an oranges_ear mob at assigned_movable's turf we give assigned_movable to it instead and dont allocate ourselves + + current_ear.references += assigned_atom + + current_ear.loc = turf_loc //normally this is bad, but since this is meant to be as fast as possible we literally just need to exist there for view() to see us + turf_loc.assigned_oranges_ear = current_ear + + . += current_ear + +///debug proc for finding how full the cells of src's z level are +/atom/proc/find_grid_statistics_for_z_level(insert_clients = 0) + var/raw_clients = 0 + var/raw_hearables = 0 + var/raw_atmos = 0 + + var/cells_with_clients = 0 + var/cells_with_hearables = 0 + var/cells_with_atmos = 0 + + var/list/client_list = list() + var/list/hearable_list = list() + var/list/atmos_list = list() + + var/x_cell_count = world.maxx / SPATIAL_GRID_CELLSIZE + var/y_cell_count = world.maxy / SPATIAL_GRID_CELLSIZE + + var/total_cells = x_cell_count ** 2 + + var/average_clients_per_cell = 0 + var/average_hearables_per_cell = 0 + var/average_atmos_mech_per_call = 0 + + var/hearable_min_x = x_cell_count + var/hearable_max_x = 1 + + var/hearable_min_y = y_cell_count + var/hearable_max_y = 1 + + var/client_min_x = x_cell_count + var/client_max_x = 1 + + var/client_min_y = y_cell_count + var/client_max_y = 1 + + var/atmos_min_x = x_cell_count + var/atmos_max_x = 1 + + var/atmos_min_y = y_cell_count + var/atmos_max_y = 1 + + var/list/inserted_clients = list() + + if(insert_clients) + var/list/turfs + var/level = SSmapping.get_level(z) + if(is_station_level(level)) + turfs = GLOB.station_turfs + + else + turfs = 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)) + fake_client.forceMove(random_turf) + inserted_clients += fake_client + + var/list/all_z_level_cells = SSspatial_grid.get_cells_in_range(src, 1000) + + for(var/datum/spatial_grid_cell/cell as anything in all_z_level_cells) + var/client_length = length(cell.client_contents) + var/hearable_length = length(cell.hearing_contents) + var/atmos_length = length(cell.atmos_contents) + + raw_clients += client_length + raw_hearables += hearable_length + raw_atmos += atmos_length + + if(client_length) + cells_with_clients++ + + client_list += cell.client_contents + + if(cell.cell_x < client_min_x) + client_min_x = cell.cell_x + + if(cell.cell_x > client_max_x) + client_max_x = cell.cell_x + + if(cell.cell_y < client_min_y) + client_min_y = cell.cell_y + + if(cell.cell_y > client_max_y) + client_max_y = cell.cell_y + + if(hearable_length) + cells_with_hearables++ + + hearable_list += cell.hearing_contents + + if(cell.cell_x < hearable_min_x) + hearable_min_x = cell.cell_x + + if(cell.cell_x > hearable_max_x) + hearable_max_x = cell.cell_x + + if(cell.cell_y < hearable_min_y) + hearable_min_y = cell.cell_y + + if(cell.cell_y > hearable_max_y) + hearable_max_y = cell.cell_y + + if(raw_atmos) + cells_with_atmos++ + + atmos_list += cell.atmos_contents + + if(cell.cell_x < atmos_min_x) + atmos_min_x = cell.cell_x + + if(cell.cell_x > atmos_max_x) + atmos_max_x = cell.cell_x + + if(cell.cell_y < atmos_min_y) + atmos_min_y = cell.cell_y + + if(cell.cell_y > atmos_max_y) + atmos_max_y = cell.cell_y + + var/total_client_distance = 0 + var/total_hearable_distance = 0 + var/total_atmos_distance = 0 + + var/average_client_distance = 0 + var/average_hearable_distance = 0 + var/average_atmos_distance = 0 + + for(var/hearable in hearable_list)//n^2 btw + for(var/other_hearable in hearable_list) + if(hearable == other_hearable) + continue + total_hearable_distance += get_dist(hearable, other_hearable) + + for(var/client in client_list)//n^2 btw + for(var/other_client in client_list) + if(client == other_client) + continue + total_client_distance += get_dist(client, other_client) + + for(var/atmos in atmos_list)//n^2 btw + for(var/other_atmos in atmos_list) + if(atmos == other_atmos) + continue + total_atmos_distance += get_dist(atmos, other_atmos) + + if(length(hearable_list)) + average_hearable_distance = total_hearable_distance / length(hearable_list) + if(length(client_list)) + average_client_distance = total_client_distance / length(client_list) + if(length(atmos_list)) + average_atmos_distance = total_atmos_distance / length(atmos_list) + + average_clients_per_cell = raw_clients / total_cells + average_hearables_per_cell = raw_hearables / total_cells + average_atmos_mech_per_call = raw_atmos / total_cells + + for(var/mob/inserted_client as anything in inserted_clients) + qdel(inserted_client) + + message_admins("on z level [z] there are [raw_clients] clients ([insert_clients] of whom are fakes inserted to random station turfs)\ + , [raw_hearables] hearables, and [raw_atmos] atmos machines. all of whom are inside the bounding box given by \ + clients: ([client_min_x], [client_min_y]) x ([client_max_x], [client_max_y]), \ + hearables: ([hearable_min_x], [hearable_min_y]) x ([hearable_max_x], [hearable_max_y]) \ + and atmos machines: ([atmos_min_x], [atmos_min_y]) x ([atmos_max_x], [atmos_max_y]), \ + on average there are [average_clients_per_cell] clients per cell, [average_hearables_per_cell] hearables per cell, \ + and [average_atmos_mech_per_call] per cell, \ + [cells_with_clients] cells have clients, [cells_with_hearables] have hearables, and [cells_with_atmos] have atmos machines \ + the average client distance is: [average_client_distance], the average hearable_distance is [average_hearable_distance], \ + and the average atmos distance is [average_atmos_distance] ") + +#undef BOUNDING_BOX_MAX +#undef BOUNDING_BOX_MIN + +#undef NUMBER_OF_PREGENERATED_ORANGES_EARS diff --git a/code/datums/components/mood.dm b/code/datums/components/mood.dm index 588d0d85b154..6b876a60021c 100644 --- a/code/datums/components/mood.dm +++ b/code/datums/components/mood.dm @@ -28,6 +28,8 @@ /datum/component/mood/Destroy() STOP_PROCESSING(SSmood, src) + var/atom/movable/movable_parent = parent + movable_parent.lose_area_sensitivity(MOOD_DATUM_TRAIT) unmodify_hud() return ..() diff --git a/code/datums/holocall.dm b/code/datums/holocall.dm index a1f4156126f7..c0b8792d0511 100644 --- a/code/datums/holocall.dm +++ b/code/datums/holocall.dm @@ -25,14 +25,21 @@ //this datum manages it's own references /datum/holocall - var/mob/living/user //the one that called - var/obj/machinery/holopad/calling_holopad //the one that sent the call - var/obj/machinery/holopad/connected_holopad //the one that answered the call (may be null) - var/list/dialed_holopads //all things called, will be cleared out to just connected_holopad once answered - - var/mob/camera/ai_eye/remote/holo/eye //user's eye, once connected - var/obj/effect/overlay/holo_pad_hologram/hologram //user's hologram, once connected - var/datum/action/innate/end_holocall/hangup //hangup action + ///the one that called + var/mob/living/user + ///the holopad that sent the call to another holopad + var/obj/machinery/holopad/calling_holopad + ///the one that answered the call (may be null) + var/obj/machinery/holopad/connected_holopad + ///populated with all holopads that are either being dialed or have that have answered us, will be cleared out to just connected_holopad once answered + var/list/dialed_holopads + + ///user's eye, once connected + var/mob/camera/ai_eye/remote/holo/eye + ///user's hologram, once connected + var/obj/effect/overlay/holo_pad_hologram/hologram + ///hangup action + var/datum/action/innate/end_holocall/hangup var/call_start_time var/head_call = FALSE //calls from a head of staff autoconnect, if the recieving pad is not secure. @@ -46,19 +53,11 @@ head_call = elevated_access dialed_holopads = list() - for(var/I in callees) - var/obj/machinery/holopad/H = I - if(!QDELETED(H) && H.is_operational()) - dialed_holopads += H - if(head_call) - if(H.secure) - calling_pad.say("Auto-connection refused, falling back to call mode.") - H.say("Incoming call.") - else - H.say("Incoming connection.") - else - H.say("Incoming call.") - LAZYADD(H.holo_calls, src) + for(var/obj/machinery/holopad/connected_holopad as anything in callees) + if(!QDELETED(connected_holopad) && connected_holopad.is_operational()) + dialed_holopads += connected_holopad + connected_holopad.say("Incoming call.") + connected_holopad.set_holocall(src) if(!dialed_holopads.len) calling_pad.say("Connection failure.") @@ -85,12 +84,11 @@ hologram.HC = null QDEL_NULL(hologram) - for(var/I in dialed_holopads) - var/obj/machinery/holopad/H = I - LAZYREMOVE(H.holo_calls, src) + for(var/obj/machinery/holopad/dialed_holopad as anything in dialed_holopads) + dialed_holopad.set_holocall(src, FALSE) dialed_holopads.Cut() - if(calling_holopad) + if(calling_holopad)//if the call is answered, then calling_holopad wont be in dialed_holopads and thus wont have set_holocall(src, FALSE) called calling_holopad.calling = FALSE calling_holopad.outgoing_call = null calling_holopad.SetLightsAndPower() @@ -114,63 +112,60 @@ ConnectionFailure(H, TRUE) -//Forcefully disconnects a holopad `H` from a call. Pads not in the call are ignored. -/datum/holocall/proc/ConnectionFailure(obj/machinery/holopad/H, graceful = FALSE) +//Forcefully disconnects disconnected_holopad from a call. Pads not in the call are ignored. +/datum/holocall/proc/ConnectionFailure(obj/machinery/holopad/disconnected_holopad, graceful = FALSE) testing("Holocall connection failure: graceful [graceful]") - if(H == connected_holopad || H == calling_holopad) - if(!graceful && H != calling_holopad) + if(disconnected_holopad == connected_holopad || disconnected_holopad == calling_holopad) + if(!graceful && disconnected_holopad != calling_holopad) calling_holopad.say("Connection failure.") qdel(src) return - LAZYREMOVE(H.holo_calls, src) - dialed_holopads -= H + disconnected_holopad.set_holocall(src, FALSE) + + dialed_holopads -= disconnected_holopad if(!dialed_holopads.len) if(graceful) calling_holopad.say("Call rejected.") testing("No recipients, terminating") qdel(src) -//Answers a call made to a holopad `H` which cannot be the calling holopad. Pads not in the call are ignored -/datum/holocall/proc/Answer(obj/machinery/holopad/H) +///Answers a call made to answering_holopad which cannot be the calling holopad. Pads not in the call are ignored +/datum/holocall/proc/Answer(obj/machinery/holopad/answering_holopad) testing("Holocall answer") - if(H == calling_holopad) + if(answering_holopad == calling_holopad) CRASH("How cute, a holopad tried to answer itself.") - if(!(H in dialed_holopads)) + if(!(answering_holopad in dialed_holopads)) return - if(connected_holopad) - CRASH("Multi-connection holocall") - - for(var/I in dialed_holopads) - if(I == H) + for(var/obj/machinery/holopad/other_dialed_holopad as anything in dialed_holopads) + if(other_dialed_holopad == answering_holopad) continue - Disconnect(I) + Disconnect(other_dialed_holopad) - for(var/I in H.holo_calls) - var/datum/holocall/HC = I - if(HC != src) - HC.Disconnect(H) + for(var/datum/holocall/previously_answered_holocall as anything in answering_holopad.holo_calls)//disconnect the other holocalls answering_holopad is occupied with + if(previously_answered_holocall != src) + previously_answered_holocall.Disconnect(answering_holopad) - connected_holopad = H + connected_holopad = answering_holopad if(!Check()) return calling_holopad.calling = FALSE - hologram = H.activate_holo(user) + hologram = answering_holopad.activate_holo(user) hologram.HC = src //eyeobj code is horrid, this is the best copypasta I could make eye = new - eye.origin = H + eye.origin = answering_holopad eye.eye_initialized = TRUE eye.eye_user = user eye.name = "Camera Eye ([user.name])" user.remote_control = eye user.reset_perspective(eye) - eye.setLoc(H.loc) + eye.setLoc(answering_holopad.loc) hangup = new(eye, src) hangup.Grant(user) @@ -179,10 +174,9 @@ //Checks the validity of a holocall and qdels itself if it's not. Returns TRUE if valid, FALSE otherwise /datum/holocall/proc/Check() - for(var/I in dialed_holopads) - var/obj/machinery/holopad/H = I - if(!H.is_operational()) - ConnectionFailure(H) + for(var/obj/machinery/holopad/dialed_holopad as anything in dialed_holopads) + if(!dialed_holopad.is_operational()) + ConnectionFailure(dialed_holopad) if(QDELETED(src)) return FALSE diff --git a/code/datums/wires/radio.dm b/code/datums/wires/radio.dm index a1118da6d73c..e2b4192020f0 100644 --- a/code/datums/wires/radio.dm +++ b/code/datums/wires/radio.dm @@ -17,9 +17,9 @@ var/obj/item/radio/R = holder switch(index) if(WIRE_SIGNAL) - R.listening = !R.listening - R.broadcasting = R.listening + R.set_listening(!R.get_listening()) + R.set_broadcasting(R.get_listening()) if(WIRE_RX) - R.listening = !R.listening + R.set_listening(!R.get_listening()) if(WIRE_TX) - R.broadcasting = !R.broadcasting + R.set_broadcasting(!R.get_broadcasting()) diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm index 8413dc598875..744636619ba8 100644 --- a/code/game/area/areas.dm +++ b/code/game/area/areas.dm @@ -772,25 +772,44 @@ GLOBAL_LIST_EMPTY(teleportlocs) used_environ += amount /** - * Call back when an atom enters an area - * - * Sends signals COMSIG_AREA_ENTERED and COMSIG_ENTER_AREA (to the atom) - * - * If the area has ambience, then it plays some ambience music to the ambience channel - */ -/area/Entered(atom/movable/M) + * Call back when an atom enters an area + * + * Sends signals COMSIG_AREA_ENTERED and COMSIG_ENTER_AREA (to a list of atoms) + * + * If the area has ambience, then it plays some ambience music to the ambience channel + */ +/area/Entered(atom/movable/arrived, area/old_area) set waitfor = FALSE - SEND_SIGNAL(src, COMSIG_AREA_ENTERED, M) - SEND_SIGNAL(M, COMSIG_ENTER_AREA, src) //The atom that enters the area + SEND_SIGNAL(src, COMSIG_AREA_ENTERED, arrived, old_area) + + if(!arrived.important_recursive_contents?[RECURSIVE_CONTENTS_AREA_SENSITIVE]) + return + for(var/atom/movable/recipient as anything in arrived.important_recursive_contents[RECURSIVE_CONTENTS_AREA_SENSITIVE]) + SEND_SIGNAL(recipient, COMSIG_ENTER_AREA, src) + + if(!isliving(arrived)) + return + + var/mob/living/L = arrived + if(!L.ckey) + return + + if(ambient_buzz != old_area.ambient_buzz) + L.refresh_looping_ambience() /** - * Called when an atom exits an area - * - * Sends signals COMSIG_AREA_EXITED and COMSIG_EXIT_AREA (to the atom) - */ -/area/Exited(atom/movable/M) - SEND_SIGNAL(src, COMSIG_AREA_EXITED, M) - SEND_SIGNAL(M, COMSIG_EXIT_AREA, src) //The atom that exits the area + * Called when an atom exits an area + * + * Sends signals COMSIG_AREA_EXITED and COMSIG_EXIT_AREA (to a list of atoms) + */ +/area/Exited(atom/movable/gone, direction) + SEND_SIGNAL(src, COMSIG_AREA_EXITED, gone, direction) + SEND_SIGNAL(gone, COMSIG_MOVABLE_EXITED_AREA, src, direction) + + if(!gone.important_recursive_contents?[RECURSIVE_CONTENTS_AREA_SENSITIVE]) + return + for(var/atom/movable/recipient as anything in gone.important_recursive_contents[RECURSIVE_CONTENTS_AREA_SENSITIVE]) + SEND_SIGNAL(recipient, COMSIG_EXIT_AREA, src) /** * Returns true if this atom has gravity for the passed in turf diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index f758158da8e4..8809b5630d09 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -32,7 +32,6 @@ var/generic_canpass = TRUE var/moving_diagonally = 0 //0: not doing a diagonal move. 1 and 2: doing the first/second step of the diagonal move var/atom/movable/moving_from_pull //attempt to resume grab after moving instead of before. - var/list/client_mobs_in_contents // This contains all the client mobs within this container var/list/acted_explosions //for explosion dodging var/datum/forced_movement/force_moving = null //handled soley by forced_movement.dm @@ -72,6 +71,21 @@ /// Whether this atom should have its dir automatically changed when it moves. Setting this to FALSE allows for things such as directional windows to retain dir on moving without snowflake code all of the place. var/set_dir_on_move = TRUE + /** + * an associative lazylist of relevant nested contents by "channel", the list is of the form: list(channel = list(important nested contents of that type)) + * each channel has a specific purpose and is meant to replace potentially expensive nested contents iteration. + * do NOT add channels to this for little reason as it can add considerable memory usage. + */ + var/list/important_recursive_contents + ///contains every client mob corresponding to every client eye in this container. lazily updated by SSparallax and is sparse: + ///only the last container of a client eye has this list assuming no movement since SSparallax's last fire + var/list/client_mobs_in_contents + + /// 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 + /mutable_appearance/emissive_blocker /mutable_appearance/emissive_blocker/New() @@ -169,6 +183,9 @@ orbiting.end_orbit(src) orbiting = null + if(important_recursive_contents && (important_recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS] || important_recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE])) + SSspatial_grid.force_remove_from_cell(src) + LAZYCLEARLIST(client_mobs_in_contents) . = ..() @@ -178,6 +195,11 @@ moveToNullspace() + //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) + vis_locs = null //clears this atom out of all viscontents // Checking length(vis_contents) before cutting has significant speed benefits @@ -769,6 +791,20 @@ var/same_z_layer = (GET_TURF_PLANE_OFFSET(old_turf) == GET_TURF_PLANE_OFFSET(new_turf)) on_changed_z_level(old_turf, new_turf, same_z_layer) + if(HAS_SPATIAL_GRID_CONTENTS(src)) + if(old_turf && new_turf && (old_turf.z != new_turf.z \ + || ROUND_UP(old_turf.x / SPATIAL_GRID_CELLSIZE) != ROUND_UP(new_turf.x / SPATIAL_GRID_CELLSIZE) \ + || ROUND_UP(old_turf.y / SPATIAL_GRID_CELLSIZE) != ROUND_UP(new_turf.y / SPATIAL_GRID_CELLSIZE))) + + SSspatial_grid.exit_cell(src, old_turf) + SSspatial_grid.enter_cell(src, new_turf) + + else if(old_turf && !new_turf) + SSspatial_grid.exit_cell(src, old_turf) + + else if(new_turf && !old_turf) + SSspatial_grid.enter_cell(src, new_turf) + SSdemo.mark_dirty(src) return TRUE @@ -807,6 +843,44 @@ return bumped_atom.Bumped(src) +/atom/movable/Exited(atom/movable/gone, direction) + . = ..() + + 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) + +/atom/movable/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs) + . = ..() + + 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] + /atom/movable/proc/forceMove(atom/destination) . = FALSE if(destination) @@ -869,6 +943,154 @@ old_area.Exited(src, null) loc = null + +///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) + +/** + * 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 + * since RECURSIVE_CONTENTS_HEARING_SENSITIVE is also a spatial grid content type, removes us from the spatial grid if the trait is removed + * + * * trait_source - trait source define or ALL, if ALL, force removes hearing sensitivity. if a trait source define, removes hearing sensitivity only if the trait is removed + */ +/atom/movable/proc/lose_hearing_sensitivity(trait_source = TRAIT_GENERIC) + if(!HAS_TRAIT(src, TRAIT_HEARING_SENSITIVE)) + return + REMOVE_TRAIT(src, TRAIT_HEARING_SENSITIVE, trait_source) + if(HAS_TRAIT(src, TRAIT_HEARING_SENSITIVE)) + return + + var/turf/our_turf = get_turf(src) + if(our_turf && SSspatial_grid.initialized) + SSspatial_grid.exit_cell(src, our_turf) + else if(our_turf && !SSspatial_grid.initialized) + SSspatial_grid.remove_from_pre_init_queue(src, RECURSIVE_CONTENTS_HEARING_SENSITIVE) + + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYREMOVEASSOC(location.important_recursive_contents, RECURSIVE_CONTENTS_HEARING_SENSITIVE, src) + +///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) + LAZYADDASSOCLIST(location.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE, src) + ADD_TRAIT(src, TRAIT_AREA_SENSITIVE, trait_source) + +///removes the area sensitive channel from the important_recursive_contents list of this and all nested locs containing us if there are no more source of the trait left +/atom/movable/proc/lose_area_sensitivity(trait_source = TRAIT_GENERIC) + if(!HAS_TRAIT(src, TRAIT_AREA_SENSITIVE)) + return + REMOVE_TRAIT(src, TRAIT_AREA_SENSITIVE, trait_source) + if(HAS_TRAIT(src, TRAIT_AREA_SENSITIVE)) + return + + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYREMOVEASSOC(location.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE, src) + +///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 +/mob/proc/enable_client_mobs_in_contents() + 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) + + for(var/atom/movable/movable_loc as anything in get_nested_locs(src) + src) + LAZYORASSOCLIST(movable_loc.important_recursive_contents, RECURSIVE_CONTENTS_CLIENT_MOBS, src) + +///Clears the clients channel of this mob +/mob/proc/clear_important_client_contents() + var/turf/our_turf = get_turf(src) + + 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) + + for(var/atom/movable/movable_loc as anything in get_nested_locs(src) + src) + LAZYREMOVEASSOC(movable_loc.important_recursive_contents, RECURSIVE_CONTENTS_CLIENT_MOBS, src) + +///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) + LAZYADDASSOCLIST(location.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE, src) + ADD_TRAIT(src, TRAIT_AREA_SENSITIVE, trait_source) + +///removes the area sensitive channel from the important_recursive_contents list of this and all nested locs containing us if there are no more source of the trait left +/atom/movable/proc/lose_area_sensitivity(trait_source = TRAIT_GENERIC) + if(!HAS_TRAIT(src, TRAIT_AREA_SENSITIVE)) + return + REMOVE_TRAIT(src, TRAIT_AREA_SENSITIVE, trait_source) + if(HAS_TRAIT(src, TRAIT_AREA_SENSITIVE)) + return + + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYREMOVEASSOC(location.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE, src) + + +///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 +/mob/proc/enable_client_mobs_in_contents() + for(var/atom/movable/movable_loc as anything in get_nested_locs(src) + src) + 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) + +///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) + 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) + +///called when this movable becomes the parent of a storage component that is currently being viewed by a player. uses important_recursive_contents +/atom/movable/proc/become_active_storage(datum/storage/source) + if(!HAS_TRAIT(src, TRAIT_ACTIVE_STORAGE)) + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYADDASSOCLIST(location.important_recursive_contents, RECURSIVE_CONTENTS_ACTIVE_STORAGE, src) + ADD_TRAIT(src, TRAIT_ACTIVE_STORAGE, REF(source)) + +///called when this movable's storage component is no longer viewed by any players, unsets important_recursive_contents +/atom/movable/proc/lose_active_storage(datum/storage/source) + if(!HAS_TRAIT(src, TRAIT_ACTIVE_STORAGE)) + return + REMOVE_TRAIT(src, TRAIT_ACTIVE_STORAGE, REF(source)) + if(HAS_TRAIT(src, TRAIT_ACTIVE_STORAGE)) + return + + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYREMOVEASSOC(location.important_recursive_contents, RECURSIVE_CONTENTS_ACTIVE_STORAGE, src) + /** * Called when a movable changes z-levels. * diff --git a/code/game/machinery/bank_machine.dm b/code/game/machinery/bank_machine.dm index 43270c40efc2..0b2db0380df9 100644 --- a/code/game/machinery/bank_machine.dm +++ b/code/game/machinery/bank_machine.dm @@ -16,6 +16,7 @@ radio = new(src) radio.subspace_transmission = TRUE radio.canhear_range = 0 + radio.set_listening(FALSE) radio.recalculateChannels() /obj/machinery/computer/bank_machine/Destroy() diff --git a/code/game/machinery/doors/brigdoors.dm b/code/game/machinery/doors/brigdoors.dm index fe12396a1ff7..4871b2ab8cef 100644 --- a/code/game/machinery/doors/brigdoors.dm +++ b/code/game/machinery/doors/brigdoors.dm @@ -83,7 +83,7 @@ . = ..() Radio = new/obj/item/radio(src) - Radio.listening = 0 + Radio.set_listening(FALSE) /obj/machinery/door_timer/Initialize(mapload) . = ..() diff --git a/code/game/machinery/hologram.dm b/code/game/machinery/hologram.dm index 2df3aadcc8a4..cb39b0f1a074 100644 --- a/code/game/machinery/hologram.dm +++ b/code/game/machinery/hologram.dm @@ -1,3 +1,8 @@ +#define CAN_HEAR_MASTERS (1<<0) +#define CAN_HEAR_ACTIVE_HOLOCALLS (1<<1) +#define CAN_HEAR_RECORD_MODE (1<<2) +#define CAN_HEAR_ALL_FLAGS (CAN_HEAR_MASTERS|CAN_HEAR_ACTIVE_HOLOCALLS|CAN_HEAR_RECORD_MODE) + /* Holograms! * Contains: * Holopad @@ -84,6 +89,8 @@ GLOBAL_LIST_EMPTY(holopads) var/padname = null /// Holopad Harassment Cooldown var/holopad_cooldown = 20 SECONDS + ///bitfield. used to turn on and off hearing sensitivity depending on if we can act on Hear() at all - meant for lowering the number of unessesary hearable atoms + var/can_hear_flags = NONE /obj/machinery/holopad/secure name = "secure holopad" @@ -153,9 +160,8 @@ obj/machinery/holopad/secure/Initialize(mapload) if(outgoing_call) outgoing_call.ConnectionFailure(src) - for(var/I in holo_calls) - var/datum/holocall/HC = I - HC.ConnectionFailure(src) + for(var/datum/holocall/holocall_to_disconnect as anything in holo_calls) + holocall_to_disconnect.ConnectionFailure(src) for (var/I in masters) clear_holo(I) @@ -357,13 +363,58 @@ obj/machinery/holopad/secure/Initialize(mapload) outgoing_call.Disconnect(src) return TRUE + +//setters +/** + * setter for can_hear_flags. handles adding or removing the given flag on can_hear_flags and then adding hearing sensitivity or removing it depending on the final state + * this is necessary because holopads are a significant fraction of the hearable atoms on station which increases the cost of procs that iterate through hearables + * so we need holopads to not be hearable until it is needed + * + * * flag - one of the can_hear_flags flag defines + * * set_flag - boolean, if TRUE sets can_hear_flags to that flag and might add hearing sensitivity if can_hear_flags was NONE before, + * if FALSE unsets the flag and possibly removes hearing sensitivity + */ +/obj/machinery/holopad/proc/set_can_hear_flags(flag, set_flag = TRUE) + if(!(flag & CAN_HEAR_ALL_FLAGS)) + return FALSE //the given flag doesnt exist + + if(set_flag) + if(can_hear_flags == NONE)//we couldnt hear before, so become hearing sensitive + become_hearing_sensitive() + + can_hear_flags |= flag + return TRUE + + else + can_hear_flags &= ~flag + if(can_hear_flags == NONE) + lose_hearing_sensitivity() + + return TRUE + +///setter for adding/removing holocalls to this holopad. used to update the holo_calls list and can_hear_flags +///adds the given holocall if add_holocall is TRUE, removes if FALSE +/obj/machinery/holopad/proc/set_holocall(datum/holocall/holocall_to_update, add_holocall = TRUE) + if(!istype(holocall_to_update)) + return FALSE + + if(add_holocall) + set_can_hear_flags(CAN_HEAR_ACTIVE_HOLOCALLS) + LAZYADD(holo_calls, holocall_to_update) + + else + LAZYREMOVE(holo_calls, holocall_to_update) + if(!LAZYLEN(holo_calls)) + set_can_hear_flags(CAN_HEAR_ACTIVE_HOLOCALLS, FALSE) + + return TRUE + /** * hangup_all_calls: Disconnects all current holocalls from the holopad */ /obj/machinery/holopad/proc/hangup_all_calls() - for(var/I in holo_calls) - var/datum/holocall/HC = I - HC.Disconnect(src) + for(var/datum/holocall/holocall_to_disconnect as anything in holo_calls) + holocall_to_disconnect.Disconnect(src) //do not allow AIs to answer calls or people will use it to meta the AI sattelite /obj/machinery/holopad/attack_ai(mob/living/silicon/ai/user) @@ -461,10 +512,12 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ if(masters[master] && speaker != master) master.relay_speech(message, speaker, message_language, raw_message, radio_freq, spans, message_mods) - for(var/I in holo_calls) - var/datum/holocall/HC = I - if(HC.connected_holopad == src && speaker != HC.hologram) - HC.user.Hear(message, speaker, message_language, raw_message, radio_freq, spans, message_mods) + for(var/datum/holocall/holocall_to_update as anything in holo_calls) + if(holocall_to_update.connected_holopad == src)//if we answered this call originating from another holopad + if(speaker == holocall_to_update.hologram && holocall_to_update.user.client?.prefs.read_preference(/datum/preference/toggle/enable_runechat)) + holocall_to_update.user.create_chat_message(speaker, message_language, raw_message, spans) + else + holocall_to_update.user.Hear(message, speaker, message_language, raw_message, radio_freq, spans, message_mods) if(outgoing_call && speaker == outgoing_call.user) outgoing_call.hologram.say(raw_message) @@ -495,6 +548,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ /obj/machinery/holopad/proc/set_holo(mob/living/user, obj/effect/overlay/holo_pad_hologram/h) LAZYSET(masters, user, h) LAZYSET(holorays, user, new /obj/effect/overlay/holoray(loc)) + set_can_hear_flags(CAN_HEAR_MASTERS) var/mob/living/silicon/ai/AI = user if(istype(AI)) AI.current = src @@ -515,6 +569,8 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ if(istype(AI) && AI.current == src) AI.current = null LAZYREMOVE(masters, user) // Discard AI from the list of those who use holopad + if(!LAZYLEN(masters)) + set_can_hear_flags(CAN_HEAR_MASTERS, set_flag = FALSE) qdel(holorays[user]) LAZYREMOVE(holorays, user) SetLightsAndPower() @@ -628,12 +684,14 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ offset = FALSE QDEL_NULL(replay_holo) SetLightsAndPower() + set_can_hear_flags(CAN_HEAR_RECORD_MODE, FALSE) /obj/machinery/holopad/proc/record_start(mob/living/user) if(!user || !disk || disk.record) return disk.record = new record_mode = TRUE + set_can_hear_flags(CAN_HEAR_RECORD_MODE) record_start = world.time record_user = user disk.record.set_caller_image(user) @@ -745,3 +803,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ #undef HOLOPAD_PASSIVE_POWER_USAGE #undef HOLOGRAM_POWER_USAGE +#undef CAN_HEAR_MASTERS +#undef CAN_HEAR_ACTIVE_HOLOCALLS +#undef CAN_HEAR_RECORD_MODE +#undef CAN_HEAR_ALL_FLAGS diff --git a/code/game/machinery/requests_console.dm b/code/game/machinery/requests_console.dm index 94b7c67392f8..c5b683c8ce37 100644 --- a/code/game/machinery/requests_console.dm +++ b/code/game/machinery/requests_console.dm @@ -115,7 +115,7 @@ GLOBAL_LIST_EMPTY(req_console_ckey_departments) GLOB.req_console_ckey_departments[ckey(department)] = department Radio = new /obj/item/radio(src) - Radio.listening = 0 + Radio.set_listening(FALSE) /obj/machinery/requests_console/Destroy() QDEL_NULL(Radio) diff --git a/code/game/machinery/telecomms/broadcasting.dm b/code/game/machinery/telecomms/broadcasting.dm index 134b526c9ed1..a96a8fd0d9c2 100644 --- a/code/game/machinery/telecomms/broadcasting.dm +++ b/code/game/machinery/telecomms/broadcasting.dm @@ -146,40 +146,50 @@ if(compression > 0) message = Gibberish(message, compression + 40) + var/list/signal_reaches_every_z_level = levels + + if(0 in levels) + signal_reaches_every_z_level = RADIO_NO_Z_LEVEL_RESTRICTION + + // Assemble the list of radios var/list/radios = list() switch (transmission_method) if (TRANSMISSION_SUBSPACE) // Reaches any radios on the levels - for(var/obj/item/radio/R in GLOB.all_radios["[frequency]"]) - if(R.can_receive(frequency, levels)) - radios += R + var/list/all_radios_of_our_frequency = GLOB.all_radios["[frequency]"] + radios = all_radios_of_our_frequency.Copy() + + for(var/obj/item/radio/subspace_radio in radios) + if(!subspace_radio.can_receive(frequency, signal_reaches_every_z_level)) + radios -= subspace_radio // Syndicate radios can hear all well-known radio channels if (num2text(frequency) in GLOB.reverseradiochannels) - for(var/obj/item/radio/R in GLOB.all_radios["[FREQ_SYNDICATE]"]) - if(R.can_receive(FREQ_SYNDICATE, list(R.z))) - radios |= R + for(var/obj/item/radio/syndicate_radios in GLOB.all_radios["[FREQ_SYNDICATE]"]) + if(syndicate_radios.can_receive(FREQ_SYNDICATE, RADIO_NO_Z_LEVEL_RESTRICTION)) + radios |= syndicate_radios if (TRANSMISSION_RADIO) // Only radios not currently in subspace mode - for(var/obj/item/radio/R in GLOB.all_radios["[frequency]"]) - if(!R.subspace_transmission && R.can_receive(frequency, levels)) - radios += R + for(var/obj/item/radio/non_subspace_radio in GLOB.all_radios["[frequency]"]) + if(!non_subspace_radio.subspace_transmission && non_subspace_radio.can_receive(frequency, signal_reaches_every_z_level)) + radios += non_subspace_radio if (TRANSMISSION_SUPERSPACE) // Only radios which are independent - for(var/obj/item/radio/R in GLOB.all_radios["[frequency]"]) - if(R.independent && R.can_receive(frequency, levels)) - radios += R + for(var/obj/item/radio/independent_radio in GLOB.all_radios["[frequency]"]) + if(independent_radio.independent && independent_radio.can_receive(frequency, signal_reaches_every_z_level)) + radios += independent_radio // From the list of radios, find all mobs who can hear those. - var/list/receive = get_mobs_in_radio_ranges(radios) + var/list/receive = get_hearers_in_radio_ranges(radios) // Cut out mobs with clients who are admins and have radio chatter disabled. - for(var/mob/R in receive) - if (R.client && R.client.holder && !(R.client.prefs.chat_toggles & CHAT_RADIO)) - receive -= R + for(var/atom/movable/hearer as anything in receive) + if(!hearer) + stack_trace("null found in the hearers list returned by the spatial grid. this is bad") + continue // Add observers who have ghost radio enabled. for(var/mob/dead/observer/M in GLOB.player_list) @@ -191,8 +201,13 @@ var/spans = data["spans"] var/list/message_mods = data["mods"] var/rendered = virt.compose_message(virt, language, message, frequency, spans) - for(var/atom/movable/hearer in receive) - hearer.Hear(rendered, virt, language, message, frequency, spans, message_mods) + + for(var/atom/movable/hearer as anything in receive) + if(!hearer) + stack_trace("null found in the hearers list returned by the spatial grid. this is bad") + continue + + hearer.Hear(rendered, virt, language, message, frequency, spans, message_mods, message_range = INFINITY) // This following recording is intended for research and feedback in the use of department radio channels if(length(receive)) @@ -209,7 +224,7 @@ var/log_text = "\[[get_radio_name(frequency)]\] [spans_part]\"[message]\" (language: [lang_name])" var/mob/source_mob = virt.source - if(istype(source_mob)) + if(ismob(source_mob)) source_mob.log_message(log_text, LOG_TELECOMMS) else log_telecomms("[virt.source] [log_text] [loc_name(get_turf(virt.source))]") diff --git a/code/game/machinery/telecomms/machine_interactions.dm b/code/game/machinery/telecomms/machine_interactions.dm index 73ceb93dc91e..df3557c48ada 100644 --- a/code/game/machinery/telecomms/machine_interactions.dm +++ b/code/game/machinery/telecomms/machine_interactions.dm @@ -116,10 +116,10 @@ return else for(var/obj/machinery/telecomms/T in links) - T.links.Remove(src) + remove_link(T) network = params["value"] links = list() - log_game("[key_name(operator)] has changed the network for [src] at [AREACOORD(src)] to [network].") + log_game("[key_name(usr)] has changed the network for [src] at [AREACOORD(src)] to [network].") . = TRUE if("tempfreq") if(params["value"]) @@ -139,24 +139,13 @@ log_game("[key_name(operator)] added removed frequency [params["value"]] for [src] at [AREACOORD(src)].") . = TRUE if("unlink") - var/obj/machinery/telecomms/T = links[text2num(params["value"])] + var/obj/machinery/telecomms/machine_to_unlink = links[text2num(params["value"])] if(T) - // Remove link entries from both T and src. - if(T.links) - T.links.Remove(src) - links.Remove(T) - log_game("[key_name(operator)] unlinked [src] and [T] at [AREACOORD(src)].") - . = TRUE + . = remove_link(T, usr) if("link") if(heldmultitool) - var/obj/machinery/telecomms/tcomms_machine = multitool_get_buffer(src, heldmultitool) - if(istype(tcomms_machine) && tcomms_machine != src) - if(!(src in tcomms_machine.links)) - tcomms_machine.links += src - if(!(tcomms_machine in links)) - links += tcomms_machine - log_game("[key_name(operator)] linked [src] for [tcomms_machine] at [AREACOORD(src)].") - . = TRUE + var/obj/machinery/telecomms/T = heldmultitool.target + . = add_new_link(T, usr) if("buffer") // Yogs start -- holotool support if(heldmultitool) multitool_set_buffer(usr, heldmultitool, src) @@ -169,6 +158,46 @@ add_act(action, params) . = TRUE +///adds new_connection to src's links list AND vice versa. also updates links_by_telecomms_type +/obj/machinery/telecomms/proc/add_new_link(obj/machinery/telecomms/new_connection, mob/user) + if(!istype(new_connection) || new_connection == src) + return FALSE + + if((new_connection in links) && (src in new_connection.links)) + return FALSE + + links |= new_connection + new_connection.links |= src + new_connection.ui_update() + + LAZYADDASSOCLIST(links_by_telecomms_type, new_connection.telecomms_type, new_connection) + LAZYADDASSOCLIST(new_connection.links_by_telecomms_type, telecomms_type, src) + + if(user) + log_game("[key_name(user)] linked [src] for [new_connection] at [AREACOORD(src)].") + return TRUE + +///removes old_connection from src's links list AND vice versa. also updates links_by_telecomms_type +/obj/machinery/telecomms/proc/remove_link(obj/machinery/telecomms/old_connection, mob/user) + if(!istype(old_connection) || old_connection == src) + return FALSE + + if(old_connection in links) + links -= old_connection + LAZYREMOVEASSOC(links_by_telecomms_type, old_connection.telecomms_type, old_connection) + + + if(src in old_connection.links) + old_connection.links -= src + LAZYREMOVEASSOC(old_connection.links_by_telecomms_type, telecomms_type, src) + + old_connection.ui_update() + + if(user) + log_game("[key_name(user)] unlinked [src] and [old_connection] at [AREACOORD(src)].") + + return TRUE + /obj/machinery/telecomms/proc/add_option() return diff --git a/code/game/machinery/telecomms/machines/broadcaster.dm b/code/game/machinery/telecomms/machines/broadcaster.dm index 5b34915ff5a1..7b50a38604d0 100644 --- a/code/game/machinery/telecomms/machines/broadcaster.dm +++ b/code/game/machinery/telecomms/machines/broadcaster.dm @@ -13,6 +13,7 @@ GLOBAL_VAR_INIT(message_delay, 0) // To make sure restarting the recentmessages icon_state = "caster" desc = "A dish-shaped machine used to broadcast processed subspace signals." density = TRUE + telecomms_type = /obj/machinery/telecomms/broadcaster use_power = IDLE_POWER_USE idle_power_usage = 25 circuit = /obj/item/circuitboard/machine/telecomms/broadcaster diff --git a/code/game/machinery/telecomms/machines/bus.dm b/code/game/machinery/telecomms/machines/bus.dm index 69e59ab1ca38..a4c298fa913f 100644 --- a/code/game/machinery/telecomms/machines/bus.dm +++ b/code/game/machinery/telecomms/machines/bus.dm @@ -13,6 +13,7 @@ icon_state = "bus" desc = "A mighty piece of hardware used to send massive amounts of data quickly." density = TRUE + telecomms_type = /obj/machinery/telecomms/bus use_power = IDLE_POWER_USE idle_power_usage = 50 netspeed = 40 diff --git a/code/game/machinery/telecomms/machines/hub.dm b/code/game/machinery/telecomms/machines/hub.dm index dedf7c7f3a77..13a352464983 100644 --- a/code/game/machinery/telecomms/machines/hub.dm +++ b/code/game/machinery/telecomms/machines/hub.dm @@ -13,6 +13,7 @@ icon_state = "hub" desc = "A mighty piece of hardware used to send/receive massive amounts of data." density = TRUE + telecomms_type = /obj/machinery/telecomms/hub use_power = IDLE_POWER_USE idle_power_usage = 80 long_range_link = TRUE diff --git a/code/game/machinery/telecomms/machines/message_server.dm b/code/game/machinery/telecomms/machines/message_server.dm index b3103415f3c5..d3eb33297233 100644 --- a/code/game/machinery/telecomms/machines/message_server.dm +++ b/code/game/machinery/telecomms/machines/message_server.dm @@ -11,6 +11,7 @@ icon_state = "blackbox" name = "Blackbox Recorder" density = TRUE + telecomms_type = /obj/machinery/telecomms/message_server use_power = IDLE_POWER_USE idle_power_usage = 10 active_power_usage = 100 diff --git a/code/game/machinery/telecomms/machines/processor.dm b/code/game/machinery/telecomms/machines/processor.dm index 2362273469a0..e3cf0e5de6ab 100644 --- a/code/game/machinery/telecomms/machines/processor.dm +++ b/code/game/machinery/telecomms/machines/processor.dm @@ -11,6 +11,7 @@ icon_state = "processor" desc = "This machine is used to process large quantities of information." density = TRUE + telecomms_type = /obj/machinery/telecomms/processor use_power = IDLE_POWER_USE idle_power_usage = 30 circuit = /obj/item/circuitboard/machine/telecomms/processor diff --git a/code/game/machinery/telecomms/machines/receiver.dm b/code/game/machinery/telecomms/machines/receiver.dm index ea01e720e84f..6d3e71865d2d 100644 --- a/code/game/machinery/telecomms/machines/receiver.dm +++ b/code/game/machinery/telecomms/machines/receiver.dm @@ -11,6 +11,7 @@ icon_state = "caster" desc = "This machine has a dish-like shape and green lights. It is designed to detect and process subspace radio activity." density = TRUE + telecomms_type = /obj/machinery/telecomms/receiver use_power = IDLE_POWER_USE idle_power_usage = 30 circuit = /obj/item/circuitboard/machine/telecomms/receiver diff --git a/code/game/machinery/telecomms/machines/relay.dm b/code/game/machinery/telecomms/machines/relay.dm index 49747a37587f..071b3127444d 100644 --- a/code/game/machinery/telecomms/machines/relay.dm +++ b/code/game/machinery/telecomms/machines/relay.dm @@ -11,6 +11,7 @@ icon_state = "relay" desc = "A mighty piece of hardware used to send massive amounts of data far away." density = TRUE + telecomms_type = /obj/machinery/telecomms/relay use_power = IDLE_POWER_USE idle_power_usage = 30 netspeed = 5 diff --git a/code/game/machinery/telecomms/machines/server.dm b/code/game/machinery/telecomms/machines/server.dm index e173da01235c..f99dcd908694 100644 --- a/code/game/machinery/telecomms/machines/server.dm +++ b/code/game/machinery/telecomms/machines/server.dm @@ -10,6 +10,7 @@ icon_state = "server" desc = "A machine used to store data and network statistics." density = TRUE + telecomms_type = /obj/machinery/telecomms/server use_power = IDLE_POWER_USE idle_power_usage = 15 circuit = /obj/item/circuitboard/machine/telecomms/server diff --git a/code/game/machinery/telecomms/telecomunications.dm b/code/game/machinery/telecomms/telecomunications.dm index a9d31fb3f18b..f13e9f5945d5 100644 --- a/code/game/machinery/telecomms/telecomunications.dm +++ b/code/game/machinery/telecomms/telecomunications.dm @@ -18,36 +18,45 @@ GLOBAL_LIST_EMPTY(telecomms_list) name = "telecommunications machine" icon = 'icons/obj/machines/telecomms.dmi' critical_machine = TRUE - var/list/links = list() // list of machines this machine is linked to - var/traffic = 0 // value increases as traffic increases - var/netspeed = 2.5 // how much traffic to lose per second (50 gigabytes/second * netspeed) - var/net_efective = 100 //yogs percentage of netspeed aplied - var/list/autolinkers = list() // list of text/number values to link with - var/id = "NULL" // identification string - var/network = "NULL" // the network of the machinery - - var/list/freq_listening = list() // list of frequencies to tune into: if none, will listen to all +/// list of machines this machine is linked to + var/list/links = list() + /** + * associative lazylist list of the telecomms_type of linked telecomms machines and a list of said machines. + * eg list(telecomms_type1 = list(everything linked to us with that type), telecomms_type2 = list(everything linked to us with THAT type)...) + */ + var/list/links_by_telecomms_type + /// value increases as traffic increases + var/traffic = 0 + /// how much traffic to lose per second (50 gigabytes/second * netspeed) + var/netspeed = 2.5 + /// list of text/number values to link with + var/list/autolinkers = list() + /// identification string + var/id = "NULL" + /// the relevant type path of this telecomms machine eg /obj/machinery/telecomms/server but not server/preset. used for links_by_telecomms_type + var/telecomms_type = null + /// the network of the machinery + var/network = "NULL" + + // list of frequencies to tune into: if none, will listen to all + var/list/freq_listening = list() var/on = TRUE - var/toggled = TRUE // Is it toggled on - var/long_range_link = FALSE // Can you link it across Z levels or on the otherside of the map? (Relay & Hub) + /// Is it toggled on + var/toggled = TRUE + /// Can you link it across Z levels or on the otherside of the map? (Relay & Hub) + var/long_range_link = FALSE /// Is it a hidden machine? var/hide = FALSE - var/generates_heat = TRUE //yogs turn off tcomms generating heat - var/heatoutput = 2500 //yogs modify power output per trafic removed(usual heat capacity of the air in server room is 1600J/K) - - var/on_icon = "" // used for special cases broadcaster/reciever - - +/// relay signal to all linked machinery that are of type [filter]. If signal has been sent [amount] times, stop sending /obj/machinery/telecomms/proc/relay_information(datum/signal/subspace/signal, filter, copysig, amount = 20) - // relay signal to all linked machinery that are of type [filter]. If signal has been sent [amount] times, stop sending if(!on) return - if(filter && !ispath(filter)) // Yogs -- for debugging telecomms later when I soop up NTSL some more - CRASH("relay_information() was given a path filter that wasn't actually a path!") + if(!filter || !ispath(filter, /obj/machinery/telecomms)) + CRASH("null or non /obj/machinery/telecomms typepath given as the filter argument! given typepath: [filter]") var/send_count = 0 // Apply some lag based on traffic rates @@ -58,39 +67,39 @@ GLOBAL_LIST_EMPTY(telecomms_list) // Loop through all linked machines and send the signal or copy. for(var/m_typeless in links) // Yogs -- God bless typeless for-loops var/obj/machinery/telecomms/machine = m_typeless - if(filter && !istype(machine, filter) ) - continue - if(!machine.on) + for(var/obj/machinery/telecomms/filtered_machine in links_by_telecomms_type?[filter]) + if(!filtered_machine.on) continue if(amount && send_count >= amount) break - if(z != machine.loc.z && !long_range_link && !machine.long_range_link) + if(get_virtual_z_level() != filtered_machine.loc.get_virtual_z_level() && !long_range_link && !filtered_machine.long_range_link) continue send_count++ - if(machine.is_freq_listening(signal)) - machine.traffic++ + if(filtered_machine.is_freq_listening(signal)) + filtered_machine.traffic++ if(copysig) - machine.receive_information(signal.copy(), src) + filtered_machine.receive_information(signal.copy(), src) else - machine.receive_information(signal, src) + filtured_machine.receive_information(signal, src) if(send_count > 0 && is_freq_listening(signal)) traffic++ return send_count +// send signal directly to a machine /obj/machinery/telecomms/proc/relay_direct_information(datum/signal/signal, obj/machinery/telecomms/machine) - // send signal directly to a machine machine.receive_information(signal, src) +//receive information from linked machinery /obj/machinery/telecomms/proc/receive_information(datum/signal/signal, obj/machinery/telecomms/machine_from) - // receive information from linked machinery + return /obj/machinery/telecomms/proc/is_freq_listening(datum/signal/signal) // return TRUE if found, FALSE if not found - return signal && (!freq_listening.len || (signal.frequency in freq_listening)) + return signal && (!length(freq_listening) || (signal.frequency in freq_listening)) /obj/machinery/telecomms/Initialize(mapload) . = ..() @@ -101,25 +110,26 @@ GLOBAL_LIST_EMPTY(telecomms_list) /obj/machinery/telecomms/LateInitialize() ..() for(var/obj/machinery/telecomms/T in (long_range_link ? GLOB.telecomms_list : urange(20, src, 1))) - add_link(T) + add_automatic_link(T) /obj/machinery/telecomms/Destroy() GLOB.telecomms_list -= src for(var/obj/machinery/telecomms/comm in GLOB.telecomms_list) - comm.links -= src + remove_link(comm) links = list() return ..() // Used in auto linking -/obj/machinery/telecomms/proc/add_link(obj/machinery/telecomms/T) +/obj/machinery/telecomms/proc/add_automatic_link(obj/machinery/telecomms/T) var/turf/position = get_turf(src) var/turf/T_position = get_turf(T) if((position.z == T_position.z) || (long_range_link && T.long_range_link)) - if(src != T) - for(var/x in autolinkers) - if(x in T.autolinkers) - links |= T - T.links |= src + if(src == T) + return + for(var/autolinker_id in autolinkers) + if(autolinker_id in T.autolinkers) + add_new_link(T) + return /obj/machinery/telecomms/update_icon_state() diff --git a/code/game/objects/items/devices/radio/headset.dm b/code/game/objects/items/devices/radio/headset.dm index c7906d086a7a..9357eb11f096 100644 --- a/code/game/objects/items/devices/radio/headset.dm +++ b/code/game/objects/items/devices/radio/headset.dm @@ -9,6 +9,8 @@ slot_flags = ITEM_SLOT_EARS dog_fashion = null + var/obj/item/encryptionkey/keyslot2 = null + var/obj/item/encryptionkey/keyslot = null /obj/item/radio/headset/suicide_act(mob/living/carbon/user) user.visible_message(span_suicide("[user] begins putting \the [src]'s antenna up [user.p_their()] nose! It looks like [user.p_theyre()] trying to give [user.p_them()]self cancer!")) @@ -16,21 +18,19 @@ /obj/item/radio/headset/Initialize(mapload) . = ..() + set_listening(TRUE) recalculateChannels() + possibly_deactivate_in_loc() -/obj/item/radio/headset/talk_into(mob/living/M, message, channel, list/spans, datum/language/language, list/message_mods) - if (!listening) - return ITALICS | REDUCE_RANGE - return ..() - -/obj/item/radio/headset/can_receive(freq, level, AIuser) - if(ishuman(src.loc)) - var/mob/living/carbon/human/H = src.loc - if(H.ears == src) - return ..(freq, level) - else if(AIuser) - return ..(freq, level) - return FALSE +/obj/item/radio/headset/proc/possibly_deactivate_in_loc() + if(ismob(loc)) + set_listening(should_be_listening) + else + set_listening(FALSE, actual_setting = FALSE) + +/obj/item/radio/headset/Moved(atom/OldLoc, Dir) + . = ..() + possibly_deactivate_in_loc() /obj/item/radio/headset/ui_data(mob/user) . = ..() @@ -266,8 +266,6 @@ keyslot2 = new /obj/item/encryptionkey/ai command = TRUE -/obj/item/radio/headset/silicon/can_receive(freq, level) - return ..(freq, level, TRUE) /obj/item/radio/headset/attackby(obj/item/W, mob/user, params) user.set_machine(src) @@ -312,6 +310,25 @@ else return ..() +/obj/item/radio/headset/recalculateChannels() + . = ..() + if(keyslot2) + for(var/ch_name in keyslot2.channels) + if(!(ch_name in src.channels)) + LAZYSET(channels, ch_name, keyslot2.channels[ch_name]) + + if(keyslot2.translate_binary) + translate_binary = TRUE + if(keyslot2.syndie) + syndie = TRUE + if (keyslot2.independent) + independent = TRUE + if (keyslot2.amplification) + command = TRUE + + for(var/ch_name in channels) + secure_radio_connections[ch_name] = add_radio(src, GLOB.radiochannels[ch_name]) + /obj/item/radio/headset/AltClick(mob/living/user) if(!istype(user) || !Adjacent(user) || user.incapacitated()) return diff --git a/code/game/objects/items/devices/radio/intercom.dm b/code/game/objects/items/devices/radio/intercom.dm index 5291fe6b1157..cf615127c85a 100644 --- a/code/game/objects/items/devices/radio/intercom.dm +++ b/code/game/objects/items/devices/radio/intercom.dm @@ -97,24 +97,16 @@ /obj/item/radio/intercom/ui_state(mob/user) return GLOB.default_state -/obj/item/radio/intercom/can_receive(freq, level) - if(!on) - return FALSE - if(wires.is_cut(WIRE_RX)) - return FALSE - if(!(0 in level)) +/obj/item/radio/intercom/can_receive(freq, list/levels) + if(levels != RADIO_NO_Z_LEVEL_RESTRICTION) var/turf/position = get_turf(src) - if(isnull(position) || !(position.z in level)) + if(isnull(position) || !(position.get_virtual_z_level() in levels)) return FALSE - if(!src.listening) - return FALSE if(freq == FREQ_SYNDICATE) - if(!(src.syndie)) + if(!(syndie)) return FALSE//Prevents broadcast of messages over devices lacking the encryption - return TRUE - /obj/item/radio/intercom/Hear(message, atom/movable/speaker, message_langs, raw_message, radio_freq, list/spans, list/message_mods = list()) if(message_mods[RADIO_EXTENSION] == MODE_INTERCOM) return // Avoid hearing the same thing twice @@ -155,3 +147,8 @@ pixel_shift = 29 inverse = TRUE materials = list(/datum/material/iron = 75, /datum/material/glass = 25) + +/obj/item/radio/intercom/chapel/Initialize(mapload, ndir, building) + . = ..() + set_frequency(1481) + set_broadcasting(TRUE) diff --git a/code/game/objects/items/devices/radio/radio.dm b/code/game/objects/items/devices/radio/radio.dm index edbce19ab02b..ca29208c4e51 100644 --- a/code/game/objects/items/devices/radio/radio.dm +++ b/code/game/objects/items/devices/radio/radio.dm @@ -30,52 +30,105 @@ GLOBAL_LIST_INIT(channel_tokens, list( materials = list(/datum/material/iron=75, /datum/material/glass=25) obj_flags = USES_TGUI - var/on = TRUE - var/frequency = FREQ_COMMON - var/canhear_range = 3 // The range around the radio in which mobs can hear what it receives. - var/emped = 0 // Tracks the number of EMPs currently stacked. - - var/broadcasting = FALSE // Whether the radio will transmit dialogue it hears nearby. - var/listening = TRUE // Whether the radio is currently receiving. - var/prison_radio = FALSE // If true, the transmit wire starts cut. - var/unscrewed = FALSE // Whether wires are accessible. Toggleable by screwdrivering. - var/freerange = FALSE // If true, the radio has access to the full spectrum. - var/subspace_transmission = FALSE // If true, the radio transmits and receives on subspace exclusively. - var/subspace_switchable = FALSE // If true, subspace_transmission can be toggled at will. - var/freqlock = FALSE // Frequency lock to stop the user from untuning specialist radios. - var/use_command = FALSE // If true, broadcasts will be large and BOLD. - var/command = FALSE // If true, use_command can be toggled at will. - - // Encryption key handling - var/obj/item/encryptionkey/keyslot - var/obj/item/encryptionkey/keyslot2 - var/translate_binary = FALSE // If true, can hear the special binary channel. - var/independent = FALSE // If true, can say/hear on the special CentCom channel. - var/syndie = FALSE // If true, hears all well-known channels automatically, and can say/hear on the Syndicate channel. - var/list/channels = list() // Map from name (see communications.dm) to on/off. First entry is current department (:h). + ///if FALSE, broadcasting and listening dont matter and this radio shouldnt do anything + VAR_PRIVATE/on = TRUE + ///the "default" radio frequency this radio is set to, listens and transmits to this frequency by default. wont work if the channel is encrypted + VAR_PRIVATE/frequency = FREQ_COMMON + + /// Whether the radio will transmit dialogue it hears nearby into its radio channel. + VAR_PRIVATE/broadcasting = FALSE + /// Whether the radio is currently receiving radio messages from its radio frequencies. + VAR_PRIVATE/listening = TRUE + + //the below three vars are used to track listening and broadcasting should they be forced off for whatever reason but "supposed" to be active + //eg player sets the radio to listening, but an emp or whatever turns it off, its still supposed to be activated but was forced off, + //when it wears off it sets listening to should_be_listening + + ///used for tracking what broadcasting should be in the absence of things forcing it off, eg its set to broadcast but gets emp'd temporarily + var/should_be_broadcasting = FALSE + ///used for tracking what listening should be in the absence of things forcing it off, eg its set to listen but gets emp'd temporarily + var/should_be_listening = TRUE + + /// Both the range around the radio in which mobs can hear what it receives and the range the radio can hear + var/canhear_range = 3 + /// Tracks the number of EMPs currently stacked. + var/emped = 0 + + /// If true, the transmit wire starts cut. + var/prison_radio = FALSE + /// Whether wires are accessible. Toggleable by screwdrivering. + var/unscrewed = FALSE + /// If true, the radio has access to the full spectrum. + var/freerange = FALSE + /// If true, the radio transmits and receives on subspace exclusively. + var/subspace_transmission = FALSE + /// If true, subspace_transmission can be toggled at will. + var/subspace_switchable = FALSE + /// Frequency lock to stop the user from untuning specialist radios. + var/freqlock = FALSE + /// If true, broadcasts will be large and BOLD. + var/use_command = FALSE + /// If true, use_command can be toggled at will. + var/command = FALSE + + var/headset = FALSE + + ///makes anyone who is talking through this anonymous. + var/anonymize = FALSE + + /// If true, can hear the special binary channel. + var/translate_binary = FALSE + /// If true, can say/hear on the special CentCom channel. + var/independent = FALSE + /// If true, hears all well-known channels automatically, and can say/hear on the Syndicate channel. + var/syndie = FALSE + /// associative list of the encrypted radio channels this radio is currently set to listen/broadcast to, of the form: list(channel name = TRUE or FALSE) + var/list/channels + /// associative list of the encrypted radio channels this radio can listen/broadcast to, of the form: list(channel name = channel frequency) var/list/secure_radio_connections + // If true, radio doesn't make sound effects (ie for Syndicate internal radio implants) + var/radio_silent = FALSE var/list/radio_sounds = list('yogstation/sound/effects/radio1.ogg','yogstation/sound/effects/radio2.ogg','yogstation/sound/effects/radio3.ogg') var/const/FREQ_LISTENING = 1 //FREQ_BROADCASTING = 2 -/obj/item/radio/suicide_act(mob/living/user) - talk_into(user, pick_list_replacements(BRAIN_DAMAGE_FILE, "brain_damage"), null, SPAN_COMMAND) - use_command = TRUE // converts the radio in to use LOUD per poll. - return OXYLOSS // you die from oxygen loss by yelling the brain damage line at full volume +/obj/item/radio/Initialize(mapload) + wires = new /datum/wires/radio(src) + if(prison_radio) + wires.cut(WIRE_TX, null) // OH GOD WHY + secure_radio_connections = list() + . = ..() + + for(var/ch_name in channels) + secure_radio_connections[ch_name] = add_radio(src, GLOB.radiochannels[ch_name]) + + set_listening(listening) + set_broadcasting(broadcasting) + set_frequency(sanitize_frequency(frequency, freerange)) + set_on(on) + + AddElement(/datum/element/empprotection, EMP_PROTECT_SELF) + +/obj/item/radio/Destroy() + remove_radio_all(src) //Just to be sure + QDEL_NULL(wires) + QDEL_NULL(keyslot) + return ..() /obj/item/radio/proc/set_frequency(new_frequency) SEND_SIGNAL(src, COMSIG_RADIO_NEW_FREQUENCY, args) remove_radio(src, frequency) frequency = add_radio(src, new_frequency) + /obj/item/radio/proc/recalculateChannels() resetChannels() if(keyslot) - for(var/ch_name in keyslot.channels) - if(!(ch_name in channels)) - channels[ch_name] = keyslot.channels[ch_name] + for(var/channel_name in keyslot.channels) + if(!(channel_name in channels)) + channels[channel_name] = keyslot.channels[channel_name] if(keyslot.translate_binary) translate_binary = TRUE @@ -84,54 +137,30 @@ GLOBAL_LIST_INIT(channel_tokens, list( if(keyslot.independent) independent = TRUE - if(keyslot2) - for(var/ch_name in keyslot2.channels) - if(!(ch_name in channels)) - channels[ch_name] = keyslot2.channels[ch_name] - - if(keyslot2.translate_binary) - translate_binary = TRUE - if(keyslot2.syndie) - syndie = TRUE - if(keyslot2.independent) - independent = TRUE - for(var/ch_name in channels) secure_radio_connections[ch_name] = add_radio(src, GLOB.radiochannels[ch_name]) // Used for cyborg override /obj/item/radio/proc/resetChannels() channels = list() + secure_radio_connections = list() translate_binary = FALSE syndie = FALSE independent = FALSE +///goes through all radio channels we should be listening for and readds them to the global list +/obj/item/radio/proc/readd_listening_radio_channels() + for(var/channel_name in channels) + add_radio(src, GLOB.radiochannels[channel_name]) + + add_radio(src, FREQ_COMMON) + /obj/item/radio/proc/make_syndie() // Turns normal radios into Syndicate radios! qdel(keyslot) keyslot = new /obj/item/encryptionkey/syndicate - syndie = 1 + syndie = TRUE recalculateChannels() -/obj/item/radio/Destroy() - remove_radio_all(src) //Just to be sure - QDEL_NULL(wires) - QDEL_NULL(keyslot) - QDEL_NULL(keyslot2) - return ..() - -/obj/item/radio/Initialize(mapload) - wires = new /datum/wires/radio(src) - if(prison_radio) - wires.cut(WIRE_TX) // OH GOD WHY - secure_radio_connections = new - . = ..() - ADD_TRAIT(src, TRAIT_EMPPROOF_CONTENTS, "innate_empproof") - frequency = sanitize_frequency(frequency, freerange) - set_frequency(frequency) - - for(var/ch_name in channels) - secure_radio_connections[ch_name] = add_radio(src, GLOB.radiochannels[ch_name]) - /obj/item/radio/interact(mob/user) if(unscrewed && !isAI(user)) wires.interact(user) @@ -139,103 +168,102 @@ GLOBAL_LIST_INIT(channel_tokens, list( else ..() -/obj/item/radio/ui_state(mob/user) - return GLOB.inventory_state - -/obj/item/radio/ui_interact(mob/user, datum/tgui/ui, datum/ui_state/state) - ui = SStgui.try_update_ui(user, src, ui) - if(!ui) - ui = new(user, src, "Radio", name) - if(state) - ui.set_state(state) - ui.open() - -/obj/item/radio/ui_data(mob/user) - var/list/data = list() - - data["broadcasting"] = broadcasting - data["listening"] = listening - data["frequency"] = frequency - data["minFrequency"] = freerange ? MIN_FREE_FREQ : MIN_FREQ - data["maxFrequency"] = freerange ? MAX_FREE_FREQ : MAX_FREQ - data["freqlock"] = freqlock - data["channels"] = list() - for(var/channel in channels) - data["channels"][channel] = channels[channel] & FREQ_LISTENING - data["command"] = command - data["useCommand"] = use_command - data["subspace"] = subspace_transmission - data["subspaceSwitchable"] = subspace_switchable - data["headset"] = FALSE - - return data - -/obj/item/radio/ui_act(action, params, datum/tgui/ui) - if(..()) - return - switch(action) - if("frequency") - if(freqlock) - return - var/tune - var/adjust = text2num(params["adjust"]) - adjust -= frequency / 10 - - if(adjust) - tune = frequency + adjust * 10 - . = TRUE - else if(text2num(tune) != null) - tune = tune * 10 - . = TRUE - if(.) - set_frequency(sanitize_frequency(tune, freerange)) - if("listen") - listening = !listening - . = TRUE - if("broadcast") - broadcasting = !broadcasting - . = TRUE - if("channel") - var/channel = params["channel"] - if(!(channel in channels)) - return - if(channels[channel] & FREQ_LISTENING) - channels[channel] &= ~FREQ_LISTENING - else - channels[channel] |= FREQ_LISTENING - . = TRUE - if("command") - use_command = !use_command - . = TRUE - if("subspace") - if(subspace_switchable) - subspace_transmission = !subspace_transmission - if(!subspace_transmission) - channels = list() - else - recalculateChannels() - . = TRUE - -/obj/item/radio/talk_into(atom/movable/M, message, channel, list/spans, datum/language/language, list/message_mods) +//simple getters only because i NEED to enforce complex setter use for these vars for caching purposes but VAR_PROTECTED requires getter usage as well. +//if another decorator is made that doesnt require getters feel free to nuke these and change these vars over to that + +///simple getter for the on variable. necessary due to VAR_PROTECTED +/obj/item/radio/proc/is_on() + return on + +///simple getter for the frequency variable. necessary due to VAR_PROTECTED +/obj/item/radio/proc/get_frequency() + return frequency + +///simple getter for the broadcasting variable. necessary due to VAR_PROTECTED +/obj/item/radio/proc/get_broadcasting() + return broadcasting + +///simple getter for the listening variable. necessary due to VAR_PROTECTED +/obj/item/radio/proc/get_listening() + return listening + +//now for setters for the above protected vars + +/** + * setter for the listener var, adds or removes this radio from the global radio list if we are also on + * + * * new_listening - the new value we want to set listening to + * * actual_setting - whether or not the radio is supposed to be listening, sets should_be_listening to the new listening value if true, otherwise just changes listening + */ +/obj/item/radio/proc/set_listening(new_listening, actual_setting = TRUE) + + listening = new_listening + if(actual_setting) + should_be_listening = listening + + if(listening && on) + readd_listening_radio_channels() + else if(!listening) + remove_radio_all(src) + +/** + * setter for broadcasting that makes us not hearing sensitive if not broadcasting and hearing sensitive if broadcasting + * hearing sensitive in this case only matters for the purposes of listening for words said in nearby tiles, talking into us directly bypasses hearing + * + * * new_broadcasting- the new value we want to set broadcasting to + * * actual_setting - whether or not the radio is supposed to be broadcasting, sets should_be_broadcasting to the new value if true, otherwise just changes broadcasting + */ +/obj/item/radio/proc/set_broadcasting(new_broadcasting, actual_setting = TRUE) + + broadcasting = new_broadcasting + if(actual_setting) + should_be_broadcasting = broadcasting + + if(broadcasting && on) //we dont need hearing sensitivity if we arent broadcasting, because talk_into doesnt care about hearing + become_hearing_sensitive(INNATE_TRAIT) + else if(!broadcasting) + lose_hearing_sensitivity(INNATE_TRAIT) + +///setter for the on var that sets both broadcasting and listening to off or whatever they were supposed to be +/obj/item/radio/proc/set_on(new_on) + + on = new_on + + if(on) + set_broadcasting(should_be_broadcasting)//set them to whatever theyre supposed to be + set_listening(should_be_listening) + else + set_broadcasting(FALSE, actual_setting = FALSE)//fake set them to off + set_listening(FALSE, actual_setting = FALSE) + +/obj/item/radio/talk_into(atom/movable/talking_movable, message, channel, list/spans, datum/language/language, list/message_mods) + if(HAS_TRAIT(talking_movable, TRAIT_SIGN_LANG)) //Forces Sign Language users to wear the translation gloves to speak over radios + var/mob/living/carbon/mute = talking_movable + if(istype(mute)) + var/obj/item/clothing/gloves/radio/G = mute.get_item_by_slot(ITEM_SLOT_GLOVES) + if(!istype(G)) + return FALSE + switch(mute.check_signables_state()) + if(SIGN_ONE_HAND) // One hand full + message = stars(message) + if(SIGN_HANDS_FULL to SIGN_CUFFED) + return FALSE if(!spans) - spans = list(M.speech_span) + spans = list(talking_movable.speech_span) if(!language) - language = M.get_selected_language() - INVOKE_ASYNC(src, PROC_REF(talk_into_impl), M, message, channel, spans.Copy(), language, message_mods) + language = talking_movable.get_selected_language() + INVOKE_ASYNC(src, .proc/talk_into_impl, talking_movable, message, channel, spans.Copy(), language, message_mods) return ITALICS | REDUCE_RANGE -/obj/item/radio/proc/talk_into_impl(atom/movable/M, message, channel, list/spans, datum/language/language, list/message_mods) +/obj/item/radio/proc/talk_into_impl(atom/movable/talking_movable, message, channel, list/spans, datum/language/language, list/message_mods) if(!on) return // the device has to be on - if(!M || !message) + if(!talking_movable || !message) return if(wires.is_cut(WIRE_TX)) // Permacell and otherwise tampered-with radios return - if(!M.IsVocal()) + if(!talking_movable.IsVocal()) return - if(radio_sounds.len) //Sephora - Radios make small static sounds now. - var/sound/radio_sound = pick(radio_sounds) - playsound(M.loc, radio_sound, 50, 1) if(use_command) spans |= SPAN_COMMAND @@ -264,22 +292,22 @@ GLOBAL_LIST_INIT(channel_tokens, list( // Nearby active jammers prevent the message from transmitting var/turf/position = get_turf(src) - for(var/obj/item/jammer/jammer in GLOB.active_jammers) + for(var/obj/item/jammer/jammer as anything in GLOB.active_jammers) var/turf/jammer_turf = get_turf(jammer) - if(position.z == jammer_turf.z && (get_dist(position, jammer_turf) <= jammer.range)) + if(position?.z == jammer_turf.z && (get_dist(position, jammer_turf) <= jammer.range)) return // Determine the identity information which will be attached to the signal. - var/atom/movable/virtualspeaker/speaker = new(null, M, src) + var/atom/movable/virtualspeaker/speaker = new(null, talking_movable, src) // Construct the signal var/datum/signal/subspace/vocal/signal = new(src, freq, speaker, language, message, spans, message_mods) // Independent radios, on the CentCom frequency, reach all independent radios - if (independent && (freq == FREQ_CENTCOM || freq == FREQ_CTF_RED || freq == FREQ_CTF_BLUE)) + if (independent && (freq == FREQ_CENTCOM || freq == FREQ_CTF_RED || freq == FREQ_CTF_BLUE || freq == FREQ_CTF_GREEN || freq == FREQ_CTF_YELLOW)) signal.data["compression"] = 0 signal.transmission_method = TRANSMISSION_SUPERSPACE - signal.levels = list(0) // reaches all Z-levels + signal.levels = list(0) signal.broadcast() return @@ -292,84 +320,147 @@ GLOBAL_LIST_INIT(channel_tokens, list( // Non-subspace radios will check in a couple of seconds, and if the signal // was never received, send a mundane broadcast (no headsets). - addtimer(CALLBACK(src, PROC_REF(backup_transmission), signal), 20) + addtimer(CALLBACK(src, .proc/backup_transmission, signal), 20) /obj/item/radio/proc/backup_transmission(datum/signal/subspace/vocal/signal) - var/turf/T = get_turf_global(src) // yogs - get_turf_global instead of get_turf + var/turf/T = get_turf(src) if (signal.data["done"] && (T.z in signal.levels)) return // Okay, the signal was never processed, send a mundane broadcast. signal.data["compression"] = 0 signal.transmission_method = TRANSMISSION_RADIO - signal.levels = SSmapping.get_connected_levels(T) + signal.levels = list(T.z) signal.broadcast() /obj/item/radio/Hear(message, atom/movable/speaker, message_language, raw_message, radio_freq, list/spans, list/message_mods = list()) . = ..() if(radio_freq || !broadcasting || get_dist(src, speaker) > canhear_range) return + var/filtered_mods = list() + if (message_mods[MODE_CUSTOM_SAY_EMOTE]) + filtered_mods[MODE_CUSTOM_SAY_EMOTE] = message_mods[MODE_CUSTOM_SAY_EMOTE] + filtered_mods[MODE_CUSTOM_SAY_ERASE_INPUT] = message_mods[MODE_CUSTOM_SAY_ERASE_INPUT] + if(message_mods[RADIO_EXTENSION] == MODE_L_HAND || message_mods[RADIO_EXTENSION] == MODE_R_HAND) + // try to avoid being heard double + if (loc == speaker && ismob(speaker)) + var/mob/M = speaker + var/idx = M.get_held_index_of_item(src) + // left hands are odd slots + if (idx && (idx % 2) == (message_mods[RADIO_EXTENSION] == MODE_L_HAND)) + return - // try to avoid being heard double - if(loc == speaker && ismob(speaker)) - var/mob/M = speaker - if(M.is_holding(src) && message_mods[RADIO_EXTENSION] == MODE_RADIO) - return - - talk_into(speaker, raw_message, , spans, language=message_language) + talk_into(speaker, raw_message, , spans, language=message_language, message_mods=filtered_mods) // Checks if this radio can receive on the given frequency. -/obj/item/radio/proc/can_receive(freq, level) +/obj/item/radio/proc/can_receive(input_frequency, list/levels) // deny checks - if (!on || !listening || wires.is_cut(WIRE_RX)) - return FALSE - if (freq == FREQ_SYNDICATE && !syndie) - return FALSE - if (freq == FREQ_CENTCOM) - return independent // hard-ignores the z-level check - if (!(0 in level)) - var/turf/position = get_turf_global(src) // yogs - get_turf_global instead of get_turf - if(!position || !(position.z in level)) + if (levels != RADIO_NO_Z_LEVEL_RESTRICTION) + var/turf/position = get_turf(src) + if(!position || !(position.get_virtual_z_level() in levels)) return FALSE + if (input_frequency == FREQ_SYNDICATE && !syndie) + return FALSE + // allow checks: are we listening on that frequency? - if (freq == frequency) + if (input_frequency == frequency) return TRUE for(var/ch_name in channels) if(channels[ch_name] & FREQ_LISTENING) - //the GLOB.radiochannels list is located in communications.dm - if(GLOB.radiochannels[ch_name] == text2num(freq) || syndie) + if(GLOB.radiochannels[ch_name] == text2num(input_frequency) || syndie) return TRUE return FALSE +/obj/item/radio/ui_state(mob/user) + return GLOB.inventory_state + +/obj/item/radio/ui_interact(mob/user, datum/tgui/ui, datum/ui_state/state) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Radio", name) + if(state) + ui.set_state(state) + ui.open() + +/obj/item/radio/ui_data(mob/user) + var/list/data = list() + + data["broadcasting"] = broadcasting + data["listening"] = listening + data["frequency"] = frequency + data["minFrequency"] = freerange ? MIN_FREE_FREQ : MIN_FREQ + data["maxFrequency"] = freerange ? MAX_FREE_FREQ : MAX_FREQ + data["freqlock"] = freqlock + data["channels"] = list() + for(var/channel in channels) + data["channels"][channel] = channels[channel] & FREQ_LISTENING + data["command"] = command + data["useCommand"] = use_command + data["subspace"] = subspace_transmission + data["subspaceSwitchable"] = subspace_switchable + data["headset"] = FALSE + + return data + +/obj/item/radio/ui_act(action, params, datum/tgui/ui) + . = ..() + if(.) + return + switch(action) + if("frequency") + if(freqlock) + return + var/tune = params["tune"] + var/adjust = text2num(params["adjust"]) + if(adjust) + tune = frequency + adjust * 10 + . = TRUE + else if(text2num(tune) != null) + tune = tune * 10 + . = TRUE + if(.) + set_frequency(sanitize_frequency(tune, freerange)) + if("listen") + set_listening(!listening) + . = TRUE + if("broadcast") + set_broadcasting(!broadcasting) + . = TRUE + if("channel") + var/channel = params["channel"] + if(!(channel in channels)) + return + if(channels[channel] & FREQ_LISTENING) + channels[channel] &= ~FREQ_LISTENING + else + channels[channel] |= FREQ_LISTENING + . = TRUE + if("command") + use_command = !use_command + . = TRUE + if("subspace") + if(subspace_switchable) + subspace_transmission = !subspace_transmission + if(!subspace_transmission) + channels = list() + else + recalculateChannels() + . = TRUE + +/obj/item/radio/suicide_act(mob/living/user) + user.visible_message("[user] starts bouncing [src] off [user.p_their()] head! It looks like [user.p_theyre()] trying to commit suicide!") + return BRUTELOSS /obj/item/radio/examine(mob/user) . = ..() if (frequency && in_range(src, user)) . += span_notice("It is set to broadcast over the [frequency/10] frequency.") if (unscrewed) - . += span_notice("It can be attached and modified. [(keyslot || keyslot2)? "Altclick to remove encryption key." : ""]") + . += span_notice("It can be attached and modified.") else . += span_notice("It cannot be modified or attached.") - if((item_flags & IN_INVENTORY && loc == user) || (src in view(1, user))) - // construction of frequency description - var/list/avail_chans = list("Use [RADIO_KEY_COMMON] for the currently tuned frequency") - if(translate_binary) - avail_chans += "use [MODE_TOKEN_BINARY] for [MODE_BINARY]" - if(length(channels)) - for(var/i in 1 to length(channels)) - if(i == 1) - avail_chans += "use [MODE_TOKEN_DEPARTMENT] or [GLOB.channel_tokens[channels[i]]] for [lowertext(channels[i])]" - else - avail_chans += "use [GLOB.channel_tokens[channels[i]]] for [lowertext(channels[i])]" - . += span_notice("A small screen on the headset displays the following available frequencies:\n[english_list(avail_chans)].") - - if(command) - . += span_info("Alt-click to toggle the high-volume mode.") - else - . += span_notice("A small screen on the [src] flashes, it's too small to read without going near the [src].") - /obj/item/radio/attackby(obj/item/W, mob/user, params) add_fingerprint(user) if(W.tool_behaviour == TOOL_SCREWDRIVER) @@ -378,66 +469,37 @@ GLOBAL_LIST_INIT(channel_tokens, list( to_chat(user, span_notice("The radio can now be attached and modified!")) else to_chat(user, span_notice("The radio can no longer be modified or attached!")) - - else if(istype(W, /obj/item/encryptionkey/)) - if(keyslot && keyslot2) - to_chat(user, span_warning("The radio can't hold another key!")) - return - - if(!keyslot) - if(!user.transferItemToLoc(W, src)) - return - keyslot = W - - else - if(!user.transferItemToLoc(W, src)) - return - keyslot2 = W - - recalculateChannels() - else return ..() -/obj/item/radio/AltClick(mob/user) - . = ..() - if(keyslot || keyslot2) - for(var/ch_name in channels) - SSradio.remove_object(src, GLOB.radiochannels[ch_name]) - secure_radio_connections[ch_name] = null - - - if(keyslot) - user.put_in_hands(keyslot) - keyslot = null - if(keyslot2) - user.put_in_hands(keyslot2) - keyslot2 = null - - recalculateChannels() - to_chat(user, span_notice("You pop out the encryption key in the radio.")) - - else - to_chat(user, span_warning("This radio doesn't have any encryption keys!")) - - /obj/item/radio/emp_act(severity) . = ..() if (. & EMP_PROTECT_SELF) return emped++ //There's been an EMP; better count it - if (listening && ismob(loc)) // if the radio is turned on and on someone's person they notice + var/curremp = emped //Remember which EMP this was + if (listening && ismob(loc)) // if the radio is turned on and on someone's person they notice to_chat(loc, span_warning("\The [src] overloads.")) - broadcasting = FALSE - listening = FALSE for (var/ch_name in channels) channels[ch_name] = 0 - on = FALSE - addtimer(CALLBACK(src, PROC_REF(end_emp_effect)), 20 * severity, TIMER_UNIQUE | TIMER_OVERRIDE) + set_on(FALSE) + addtimer(CALLBACK(src, .proc/end_emp_effect, curremp), 200) -/obj/item/radio/proc/end_emp_effect() +/obj/item/radio/suicide_act(mob/living/user) + user.visible_message(span_suicide("[user] starts bouncing [src] off [user.p_their()] head! It looks like [user.p_theyre()] trying to commit suicide!")) + return BRUTELOSS + +/obj/item/radio/Destroy() + remove_radio_all(src) //Just to be sure + QDEL_NULL(wires) + QDEL_NULL(keyslot) + return ..() + +/obj/item/radio/proc/end_emp_effect(curremp) + if(emped != curremp) //Don't fix it if it's been EMP'd again + return FALSE emped = FALSE - on = TRUE + set_on(TRUE) return TRUE /////////////////////////////// @@ -451,16 +513,8 @@ GLOBAL_LIST_INIT(channel_tokens, list( subspace_switchable = TRUE dog_fashion = null -/obj/item/radio/borg/resetChannels() - . = ..() - - var/mob/living/silicon/robot/R = loc - if(istype(R)) - for(var/ch_name in R.module.radio_channels) - channels[ch_name] = 1 - /obj/item/radio/borg/syndicate - syndie = 1 + syndie = TRUE keyslot = new /obj/item/encryptionkey/syndicate /obj/item/radio/borg/syndicate/Initialize(mapload) @@ -501,10 +555,14 @@ GLOBAL_LIST_INIT(channel_tokens, list( recalculateChannels() -/obj/item/radio/off // Station bounced radios, their only difference is spawning with the speakers off, this was made to help the lag. - listening = 0 // And it's nice to have a subtype too for future features. +/obj/item/radio/off // Station bounced radios, their only difference is spawning with the speakers off, this was made to help the lag. dog_fashion = /datum/dog_fashion/back +/obj/item/radio/off/Initialize() + . = ..() + set_listening(FALSE) + + /obj/item/radio/off/makeshift // Makeshift SBR, limited use cases but could be useful. icon = 'icons/obj/improvised.dmi' icon_state = "radio_makeshift" diff --git a/code/game/say.dm b/code/game/say.dm index 45410e3f1656..f844d36d7723 100644 --- a/code/game/say.dm +++ b/code/game/say.dm @@ -37,11 +37,20 @@ GLOBAL_LIST_INIT(freqtospan, list( /atom/movable/proc/can_speak() return 1 -/atom/movable/proc/send_speech(message, range = 7, obj/source = src, bubble_type, list/spans, datum/language/message_language = null, list/message_mods = list()) +/atom/movable/proc/send_speech(message, range = 7, obj/source = src, bubble_type, list/spans, datum/language/message_language, list/message_mods = list()) var/rendered = compose_message(src, message_language, message, , spans, message_mods) - for(var/_AM in get_hearers_in_view(range, source)) - var/atom/movable/AM = _AM - AM.Hear(rendered, src, message_language, message, , spans, message_mods) + for(var/atom/movable/hearing_movable as anything in get_hearers_in_view(range, source, SEE_INVISIBLE_MAXIMUM)) + if(!hearing_movable)//theoretically this should use as anything because it shouldnt be able to get nulls but there are reports that it does. + stack_trace("somehow theres a null returned from get_hearers_in_view() in send_speech!") + continue + if(ismob(hearing_movable)) + var/mob/M = hearing_movable + if(M.should_show_chat_message(source, message_language, FALSE, is_heard = TRUE)) + show_overhead_message_to += M + hearing_movable.Hear(rendered, src, message_language, message, , spans, message_mods) + if(length(show_overhead_message_to)) + create_chat_message(src, message_language, show_overhead_message_to, message, spans, message_mods) + /atom/movable/proc/compose_message(atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, list/spans, list/message_mods = list(), face_name = FALSE) //This proc uses text() because it is faster than appending strings. Thanks BYOND. diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index 56d31bbf5903..8782c42a0ddf 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -26,6 +26,7 @@ GLOBAL_LIST_EMPTY(station_turfs) /// Does this turf block air from existing on it var/blocks_air = FALSE + /// If there's a tile over a basic floor that can be ripped out var/overfloor_placed = FALSE /// How accessible underfloor pieces such as wires, pipes, etc are on this turf. Can be HIDDEN, VISIBLE, or INTERACTABLE. diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 8a9c3875e8e5..8d165b9bb55a 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -488,6 +488,11 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( ////////////// /client/Del() + if(!gc_destroyed) + Destroy() //Clean up signals and timers. + return ..() + +/client/Destroy() log_access("Logout: [key_name(src)]") if(holder) adminGreet(1) diff --git a/code/modules/jobs/job_types/security_officer.dm b/code/modules/jobs/job_types/security_officer.dm index fed23668d65b..7250b30019a6 100644 --- a/code/modules/jobs/job_types/security_officer.dm +++ b/code/modules/jobs/job_types/security_officer.dm @@ -187,7 +187,7 @@ GLOBAL_LIST_INIT(available_depts_sec, list(SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICA /obj/item/radio/headset/headset_sec/alt/department/Initialize(mapload) . = ..() wires = new/datum/wires/radio(src) - secure_radio_connections = new + secure_radio_connections = list() recalculateChannels() /obj/item/radio/headset/headset_sec/alt/department/engi diff --git a/code/modules/jobs/job_types/station_engineer.dm b/code/modules/jobs/job_types/station_engineer.dm index aab47082a52a..c865b0f74c87 100644 --- a/code/modules/jobs/job_types/station_engineer.dm +++ b/code/modules/jobs/job_types/station_engineer.dm @@ -134,7 +134,7 @@ GLOBAL_LIST_INIT(available_depts_eng, list(ENG_DEPT_MEDICAL, ENG_DEPT_SCIENCE, E /obj/item/radio/headset/headset_eng/department/Initialize(mapload) . = ..() wires = new/datum/wires/radio(src) - secure_radio_connections = new + secure_radio_connections = list() recalculateChannels() /obj/item/radio/headset/headset_eng/department/supply diff --git a/code/modules/mining/laborcamp/laborstacker.dm b/code/modules/mining/laborcamp/laborstacker.dm index d8a2392268fb..5e1d0f2c611a 100644 --- a/code/modules/mining/laborcamp/laborstacker.dm +++ b/code/modules/mining/laborcamp/laborstacker.dm @@ -14,12 +14,12 @@ GLOBAL_LIST(labor_sheet_values) var/obj/machinery/door/airlock/release_door var/door_tag = "prisonshuttle" /// Needed to send messages to sec radio - var/obj/item/radio/Radio + var/obj/item/radio/integrated_radio /obj/machinery/mineral/labor_claim_console/Initialize(mapload) . = ..() - Radio = new/obj/item/radio(src) - Radio.listening = FALSE + integrated_radio = new /obj/item/radio(src) + integrated_radio.set_listening(FALSE) locate_stacking_machine() if(!GLOB.labor_sheet_values) diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index b70a272befac..774a0ee740b9 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -536,3 +536,6 @@ return TRUE #undef RESET_HUD_INTERVAL + +/mob/dead/new_player/say(message, bubble_type, var/list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null) + return diff --git a/code/modules/mob/living/brain/MMI.dm b/code/modules/mob/living/brain/MMI.dm index 71a46b48d508..4b68b15d6eac 100644 --- a/code/modules/mob/living/brain/MMI.dm +++ b/code/modules/mob/living/brain/MMI.dm @@ -54,7 +54,7 @@ /obj/item/mmi/Initialize(mapload) . = ..() radio = new(src) //Spawns a radio inside the MMI. - radio.broadcasting = FALSE //researching radio mmis turned the robofabs into radios because this didnt start as 0. + radio.set_broadcasting(FALSE) //researching radio mmis turned the robofabs into radios because this didnt start as 0. laws.set_laws_config() /obj/item/mmi/attackby(obj/item/O, mob/user, params) @@ -144,8 +144,8 @@ reboot_timer = null /obj/item/mmi/AltClick(mob/user) - radio.on = !radio.on - to_chat(user, span_notice("You toggle [src]'s radio system [radio.on==1 ? "on" : "off"].")) + radio.set_on(!radio.is_on()) + to_chat(user, "You toggle [src]'s radio system [radio.is_on() == TRUE ? "on" : "off"].") /obj/item/mmi/proc/eject_brain(mob/user) if(brainmob) @@ -211,12 +211,12 @@ if(brainmob.stat) to_chat(brainmob, span_warning("Can't do that while incapacitated or dead!")) - if(!radio.on) + if(!radio.is_on()) to_chat(brainmob, span_warning("Your radio is disabled!")) return - radio.listening = !radio.listening - to_chat(brainmob, span_notice("Radio is [radio.listening ? "now" : "no longer"] receiving broadcast.")) + radio.set_listening(!radio.get_listening()) + to_chat(brainmob, "Radio is [radio.get_listening() ? "now" : "no longer"] receiving broadcast.") /obj/item/mmi/emp_act(severity) . = ..() @@ -258,7 +258,7 @@ /obj/item/mmi/examine(mob/user) . = ..() - . += span_notice("There is a switch to toggle the radio system [radio.on ? "off" : "on"].[brain ? " It is currently being covered by [brain]." : null]") + . += "There is a switch to toggle the radio system [radio.is_on() ? "off" : "on"].[brain ? " It is currently being covered by [brain]." : null]" if(brainmob) var/mob/living/brain/B = brainmob if(!B.key || !B.mind || B.stat == DEAD) diff --git a/code/modules/mob/living/carbon/human/species_types/dullahan.dm b/code/modules/mob/living/carbon/human/species_types/dullahan.dm index f9101b53130b..060c97550dfb 100644 --- a/code/modules/mob/living/carbon/human/species_types/dullahan.dm +++ b/code/modules/mob/living/carbon/human/species_types/dullahan.dm @@ -24,7 +24,7 @@ /datum/species/dullahan/on_species_gain(mob/living/carbon/human/H, datum/species/old_species) . = ..() - H.flags_1 &= ~HEAR_1 + H.lose_hearing_sensitivity(TRAIT_GENERIC) var/obj/item/bodypart/head/head = H.get_bodypart(BODY_ZONE_HEAD) if(head) head.drop_limb() @@ -150,9 +150,8 @@ var/datum/species/dullahan/D = H.dna.species D.update_vision_perspective(H) -/obj/item/dullahan_relay - var/mob/living/owner - flags_1 = HEAR_1 +/obj/item/dullahan_relay/Hear(message, atom/movable/speaker, message_language, raw_message, radio_freq, list/spans, list/message_mods = list()) + owner.Hear(arglist(args)) /obj/item/dullahan_relay/Initialize(mapload,new_owner) . = ..() diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 64d837b981f6..7beaccada56b 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -696,8 +696,11 @@ var/mob/living/L = pulledby L.set_pull_offsets(src, pulledby.grab_state) - if(active_storage && !(CanReach(active_storage.parent,view_only = TRUE))) - active_storage.close(src) + if(active_storage) + var/storage_is_important_recurisve = (active_storage.parent in important_recursive_contents?[RECURSIVE_CONTENTS_ACTIVE_STORAGE]) + var/can_reach_active_storage = CanReach(active_storage.parent, view_only = TRUE) + if(!storage_is_important_recurisve && !can_reach_active_storage) + active_storage.close(src) if(!(mobility_flags & MOBILITY_STAND) && !buckled && prob(getBruteLoss()*200/maxHealth)) makeTrail(newloc, T, old_direction) diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm index ce518c162584..8157a0229127 100644 --- a/code/modules/mob/living/say.dm +++ b/code/modules/mob/living/say.dm @@ -288,7 +288,7 @@ GLOBAL_LIST_INIT(special_radio_keys, list( continue if(!M.client || !client) //client is so that ghosts don't have to listen to mice continue - if(get_dist(M, src) > 7 || M.z != z) //they're out of range of normal hearing + if(M.get_virtual_z_level() != get_virtual_z_level() || get_dist(M, src) > 7 ) //they're out of range of normal hearing if(eavesdrop_range && !(M.client.prefs.chat_toggles & CHAT_GHOSTWHISPER)) //they're whispering and we have hearing whispers at any range off continue if(!(M.client.prefs.chat_toggles & CHAT_GHOSTEARS)) //they're talking normally and we have hearing at any range off @@ -312,8 +312,10 @@ GLOBAL_LIST_INIT(special_radio_keys, list( //yogs change end var/rendered = compose_message(src, message_language, message, , spans, message_mods) - for(var/_AM in listening) - var/atom/movable/AM = _AM + for(var/atom/movable/AM as anything in listening) + if(!AM) + stack_trace("somehow theres a null returned from get_hearers_in_view() in send_speech!") + continue if(eavesdrop_range && get_dist(source, AM) > message_range && !(the_dead[AM])) AM.Hear(eavesrendered, src, message_language, eavesdropping, , spans, message_mods) else @@ -385,7 +387,7 @@ GLOBAL_LIST_INIT(special_radio_keys, list( /mob/living/proc/radio(message, list/message_mods = list(), list/spans, language) var/obj/item/implant/radio/imp = locate() in src - if(imp && imp.radio.on) + if(imp?.radio.is_on()) if(message_mods[MODE_HEADSET]) imp.radio.talk_into(src, message, null, spans, language, message_mods) return ITALICS | REDUCE_RANGE diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm index 3ceba75e89d3..80b80362c265 100644 --- a/code/modules/mob/login.dm +++ b/code/modules/mob/login.dm @@ -68,6 +68,9 @@ if(!client) return FALSE + //We do this here to prevent hanging refs from ghostize or whatever, since if we were in another mob before this'll take care of it + clear_important_client_contents(client) + enable_client_mobs_in_contents(client) SEND_SIGNAL(src, COMSIG_MOB_LOGIN) if (client && key != client.key) diff --git a/code/modules/mob/logout.dm b/code/modules/mob/logout.dm index 258c77c61664..71d1682847a0 100644 --- a/code/modules/mob/logout.dm +++ b/code/modules/mob/logout.dm @@ -15,4 +15,5 @@ var/datum/callback/CB = foo CB.Invoke() + clear_important_client_contents(client) return TRUE diff --git a/code/modules/power/supermatter/supermatter.dm b/code/modules/power/supermatter/supermatter.dm index 80d0c0e6efbc..b28172cd10ca 100644 --- a/code/modules/power/supermatter/supermatter.dm +++ b/code/modules/power/supermatter/supermatter.dm @@ -246,7 +246,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) GLOB.poi_list |= src radio = new(src) radio.keyslot = new radio_key - radio.listening = 0 + radio.set_listening(FALSE) radio.recalculateChannels() distort = new(src) add_emitter(/obj/emitter/sparkle, "supermatter_sparkle") diff --git a/code/modules/vending/_vending.dm b/code/modules/vending/_vending.dm index f8ea167a3e9c..016e642c91e2 100644 --- a/code/modules/vending/_vending.dm +++ b/code/modules/vending/_vending.dm @@ -247,7 +247,7 @@ IF YOU MODIFY THE PRODUCTS LIST OF A MACHINE, MAKE SURE TO UPDATE ITS RESUPPLY C onstation = circuit.onstation //if it was constructed outside mapload, sync the vendor up with the circuit's var so you can't bypass price requirements by moving / reconstructing it off station. if(isnull(alertradio)) alertradio = new(src) - alertradio.listening = 0 + alertradio.set_listening(FALSE) alertradio.set_frequency(FREQ_SECURITY) /obj/machinery/vending/Destroy() diff --git a/yogstation.dme b/yogstation.dme index 76b0a58d9339..71ecddb4842a 100644 --- a/yogstation.dme +++ b/yogstation.dme @@ -80,6 +80,7 @@ #include "code\__DEFINES\html_assistant.dm" #include "code\__DEFINES\hud.dm" #include "code\__DEFINES\icon_smoothing.dm" +#include "code\__DEFINES\important_recursive_contents.dm" #include "code\__DEFINES\instruments.dm" #include "code\__DEFINES\interaction_flags.dm" #include "code\__DEFINES\inventory.dm" @@ -144,6 +145,7 @@ #include "code\__DEFINES\space.dm" #include "code\__DEFINES\spaceman_dmm.dm" #include "code\__DEFINES\span.dm" +#include "code\__DEFINES\spatial_gridmap.dm" #include "code\__DEFINES\species_clothing_paths.dm" #include "code\__DEFINES\speech_channels.dm" #include "code\__DEFINES\stat.dm" @@ -204,6 +206,7 @@ #include "code\__DEFINES\dcs\signals\signals_object.dm" #include "code\__DEFINES\dcs\signals\signals_plane_master_group.dm" #include "code\__DEFINES\dcs\signals\signals_song.dm" +#include "code\__DEFINES\dcs\signals\signals_spatial_grid.dm" #include "code\__DEFINES\dcs\signals\signals_spell.dm" #include "code\__DEFINES\dcs\signals\signals_storage.dm" #include "code\__DEFINES\dcs\signals\signals_subsystem.dm" @@ -302,6 +305,7 @@ #include "code\__HELPERS\screen_objs.dm" #include "code\__HELPERS\see_through_maps.dm" #include "code\__HELPERS\shell.dm" +#include "code\__HELPERS\spatial_info.dm" #include "code\__HELPERS\stat_tracking.dm" #include "code\__HELPERS\string_assoc_lists.dm" #include "code\__HELPERS\text.dm" @@ -474,6 +478,7 @@ #include "code\controllers\subsystem\shuttle.dm" #include "code\controllers\subsystem\sounds.dm" #include "code\controllers\subsystem\spacedrift.dm" +#include "code\controllers\subsystem\spatial_gridmap.dm" #include "code\controllers\subsystem\statpanel.dm" #include "code\controllers\subsystem\stickyban.dm" #include "code\controllers\subsystem\sun.dm"