diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0ca0a119161..a1eb540c1d9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -76,6 +76,10 @@ /code/modules/wiremod/ @Watermelon914 +# Kylerace +/code/controllers/subsystem/spatial_gridmap.dm @Kylerace +/code/__DEFINES/spatial_gridmap.dm @Kylerace + # CONTRIBUTORS # Ghilker diff --git a/code/__DEFINES/dcs/signals/signals_global.dm b/code/__DEFINES/dcs/signals/signals_global.dm index 32fd115c89b..0a771e756dc 100644 --- a/code/__DEFINES/dcs/signals/signals_global.dm +++ b/code/__DEFINES/dcs/signals/signals_global.dm @@ -8,6 +8,8 @@ ///from base of datum/controller/subsystem/mapping/proc/add_new_zlevel(): (list/args) #define COMSIG_GLOB_NEW_Z "!new_z" +/// sent after world.maxx and/or world.maxy are expanded: (has_exapnded_world_maxx, has_expanded_world_maxy) +#define COMSIG_GLOB_EXPANDED_WORLD_BOUNDS "!expanded_world_bounds" /// called after a successful var edit somewhere in the world: (list/args) #define COMSIG_GLOB_VAR_EDIT "!var_edit" /// called after an explosion happened : (epicenter, devastation_range, heavy_impact_range, light_impact_range, took, orig_dev_range, orig_heavy_range, orig_light_range) diff --git a/code/__DEFINES/dcs/signals/signals_spatial_grid.dm b/code/__DEFINES/dcs/signals/signals_spatial_grid.dm new file mode 100644 index 00000000000..82e69dfcdf8 --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_spatial_grid.dm @@ -0,0 +1,6 @@ +//spatial grid signals + +///Called from base of /datum/controller/subsystem/spatial_grid/proc/enter_cell: (/atom/movable) +#define SPATIAL_GRID_CELL_ENTERED(contents_type) "spatial_grid_cell_entered_[contents_type]" +///Called from base of /datum/controller/subsystem/spatial_grid/proc/exit_cell: (/atom/movable) +#define SPATIAL_GRID_CELL_EXITED(contents_type) "spatial_grid_cell_exited_[contents_type]" diff --git a/code/__DEFINES/important_recursive_contents.dm b/code/__DEFINES/important_recursive_contents.dm index f1dc0bd3ea1..24cca713c65 100644 --- a/code/__DEFINES/important_recursive_contents.dm +++ b/code/__DEFINES/important_recursive_contents.dm @@ -2,3 +2,6 @@ #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" diff --git a/code/__DEFINES/maths.dm b/code/__DEFINES/maths.dm index 81349a59bf5..f67b1b772a9 100644 --- a/code/__DEFINES/maths.dm +++ b/code/__DEFINES/maths.dm @@ -30,6 +30,8 @@ #define CEILING(x, y) ( -round(-(x) / (y)) * (y) ) +#define ROUND_UP(x) ( -round(-(x))) + // round() acts like floor(x, 1) by default but can't handle other values #define FLOOR(x, y) ( round((x) / (y)) * (y) ) diff --git a/code/__DEFINES/radio.dm b/code/__DEFINES/radio.dm index 9cd9ee5f0cb..a598703047c 100644 --- a/code/__DEFINES/radio.dm +++ b/code/__DEFINES/radio.dm @@ -118,3 +118,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 00000000000..858abc9d55b --- /dev/null +++ b/code/__DEFINES/spatial_gridmap.dm @@ -0,0 +1,16 @@ +///each cell in a spatial_grid is this many turfs in length and width +#define SPATIAL_GRID_CELLSIZE 17 + +#define SPATIAL_GRID_CELLS_PER_SIDE(world_bounds) ROUND_UP((world_bounds) / SPATIAL_GRID_CELLSIZE) + +#define SPATIAL_GRID_CHANNELS 2 + +//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 + +///whether movable is itself or containing something which should be in one of the spatial grid channels. +#define HAS_SPATIAL_GRID_CONTENTS(movable) (movable.important_recursive_contents && (movable.important_recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE] || movable.important_recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS])) diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 00c31e5f887..21414bee02f 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -135,6 +135,7 @@ #define INIT_ORDER_EARLY_ASSETS 48 #define INIT_ORDER_TIMETRACK 47 #define INIT_ORDER_NETWORKS 45 +#define INIT_ORDER_SPATIAL_GRID 43 #define INIT_ORDER_ECONOMY 40 #define INIT_ORDER_OUTPUTS 35 #define INIT_ORDER_RESTAURANT 34 diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index dbbb81fa8e0..e6bfadcefc7 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -427,12 +427,14 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// Used for limbs. #define TRAIT_DISABLED_BY_WOUND "disabled-by-wound" + +//important_recursive_contents traits /* * Used for movables that need to be updated, via COMSIG_ENTER_AREA and COMSIG_EXIT_AREA, when transitioning areas. * Use [/atom/movable/proc/become_area_sensitive(trait_source)] to properly enable it. How you remove it isn't as important. */ #define TRAIT_AREA_SENSITIVE "area-sensitive" - +///every hearing sensitive atom has this trait #define TRAIT_HEARING_SENSITIVE "hearing_sensitive" /// Climbable trait, given and taken by the climbable element when added or removed. Exists to be easily checked via HAS_TRAIT(). diff --git a/code/__HELPERS/atoms.dm b/code/__HELPERS/atoms.dm index 34f42951501..0e6b916830f 100644 --- a/code/__HELPERS/atoms.dm +++ b/code/__HELPERS/atoms.dm @@ -34,6 +34,7 @@ . += checked_atom ///Step-towards method of determining whether one atom can see another. Similar to viewers() +///note: this is a line of sight algorithm, view() does not do any sort of raycasting and cannot be emulated by it accurately /proc/can_see(atom/source, atom/target, length=5) // I couldnt be arsed to do actual raycasting :I This is horribly inaccurate. var/turf/current = get_turf(source) var/turf/target_turf = get_turf(target) diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm index b5e4109db6d..4c4d001ce4c 100644 --- a/code/__HELPERS/game.dm +++ b/code/__HELPERS/game.dm @@ -11,171 +11,12 @@ return null return format_text ? format_text(checked_area.name) : checked_area.name -/** - * Returns a list with the names of the areas around a center at a certain distance - * Returns the local area if no distance is indicated - * Returns an empty list if the center is null -**/ -/proc/get_areas_in_range(distance = 0, atom/center = usr) - if(!distance) - var/turf/center_turf = get_turf(center) - return center_turf ? list(center_turf.loc) : list() - if(!center) - return list() - - var/list/turfs = RANGE_TURFS(distance, center) - var/list/areas = list() - for(var/turf/checked_turf as anything in turfs) - areas |= checked_turf.loc - return areas - -///Returns a list of all areas that are adjacent to the center atom's area, clear the list of nulls at the end. -/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)) - ) - list_clear_nulls(.) - -///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_ranged_target_turf(center, dir, 1) - 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) - . = list( - get_open_turf_in_dir(center, NORTH), - get_open_turf_in_dir(center, SOUTH), - get_open_turf_in_dir(center, EAST), - get_open_turf_in_dir(center, WEST) - ) - list_clear_nulls(.) - -///Returns a list with all the adjacent areas by getting the adjacent open turfs -/proc/get_adjacent_open_areas(atom/center) - . = list() - var/list/adjacent_turfs = get_adjacent_open_turfs(center) - for(var/near_turf in adjacent_turfs) - . |= get_area(near_turf) - -/** - * 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 - - . = view(range, source) - source.luminosity = lum - -///Checks if the mob provided (must_be_alone) is alone in an area -/proc/alone_in_area(area/the_area, mob/must_be_alone, check_type = /mob/living/carbon) - var/area/our_area = get_area(the_area) - for(var/carbon in GLOB.alive_mob_list) - if(!istype(carbon, check_type)) - continue - if(carbon == must_be_alone) - continue - if(our_area == get_area(carbon)) - return FALSE - return TRUE - //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. ///Calculate the hypotenuse cheaply (this should be in maths.dm) /proc/cheap_hypotenuse(Ax, Ay, Bx, By) return sqrt(abs(Ax - Bx) ** 2 + abs(Ay - By) ** 2) //A squared + B squared = C squared -///Returns all atoms present in a circle around the center -/proc/circle_range(center = usr,radius = 3) - - var/turf/center_turf = get_turf(center) - var/list/atoms = new/list() - var/rsq = radius * (radius + 0.5) - - for(var/atom/checked_atom as anything in range(radius, center_turf)) - var/dx = checked_atom.x - center_turf.x - var/dy = checked_atom.y - center_turf.y - if(dx * dx + dy * dy <= rsq) - atoms += checked_atom - - return atoms - -///Returns all atoms present in a circle around the center but uses view() instead of range() (Currently not used) -/proc/circle_view(center=usr,radius=3) - - var/turf/center_turf = get_turf(center) - var/list/atoms = new/list() - var/rsq = radius * (radius + 0.5) - - for(var/atom/checked_atom as anything in view(radius, center_turf)) - var/dx = checked_atom.x - center_turf.x - var/dy = checked_atom.y - center_turf.y - if(dx * dx + dy * dy <= rsq) - atoms += checked_atom - - return atoms - -///Returns the distance between two atoms -/proc/get_dist_euclidian(atom/first_location as turf|mob|obj, atom/second_location as turf|mob|obj) - var/dx = first_location.x - second_location.x - var/dy = first_location.y - second_location.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 - -///Returns a list of turfs around a center based on view() -/proc/circle_view_turfs(center=usr,radius=3) //Is there even a diffrence between this proc and circle_range_turfs()? - - var/turf/center_turf = get_turf(center) - var/list/turfs = new/list() - var/rsq = radius * (radius + 0.5) - - for(var/turf/checked_turf in view(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 - /** recursive_organ_check * inputs: first_object (object to start with) * outputs: @@ -213,68 +54,6 @@ return -/// Returns a list of hearers in view(view_radius) from source (ignoring luminosity). uses important_recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE] -/proc/get_hearers_in_view(view_radius, atom/source) - var/turf/center_turf = get_turf(source) - . = list() - if(!center_turf) - return - var/lum = center_turf.luminosity - center_turf.luminosity = 6 // This is the maximum luminosity - for(var/atom/movable/movable in view(view_radius, center_turf)) - var/list/recursive_contents = LAZYACCESS(movable.important_recursive_contents, RECURSIVE_CONTENTS_HEARING_SENSITIVE) - if(recursive_contents) - . += recursive_contents - SEND_SIGNAL(movable, COMSIG_ATOM_HEARER_IN_VIEW, .) - center_turf.luminosity = lum - -/// Returns a list of mobs who can hear any of the radios given in @radios -/proc/get_mobs_in_radio_ranges(list/obj/item/radio/radios) - . = list() - for(var/obj/item/radio/this_radio in radios) - . |= get_hearers_in_view(this_radio.canhear_range, this_radio) - -///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 get_line() unnessecarrily fast. - var/j //Generic integer for counting - if(dxabs >= dyabs) //x distance is greater than y - for(j = 0; j < dxabs; j++)//It'll take dxabs steps to get there - y += dyabs - if(y >= dxabs) //Every dyabs steps, step once in y direction - y -= dxabs - py += sdy - px += sdx //Step on in x direction - line += locate(px, py, starting_atom.z)//Add the turf to the list + var/current_x_step = starting_atom.x//start at x and y, then add 1 or -1 to these to get every turf from starting_atom to ending_atom + var/current_y_step = starting_atom.y + var/starting_z = starting_atom.z + + var/list/line = list(get_turf(starting_atom))//get_turf(atom) is faster than locate(x, y, z) + + var/x_distance = ending_atom.x - current_x_step //x distance + var/y_distance = ending_atom.y - current_y_step + + var/abs_x_distance = abs(x_distance)//Absolute value of x distance + var/abs_y_distance = abs(y_distance) + + var/x_distance_sign = SIGN(x_distance) //Sign of x distance (+ or -) + var/y_distance_sign = SIGN(y_distance) + + var/x = abs_x_distance >> 1 //Counters for steps taken, setting to distance/2 + var/y = abs_y_distance >> 1 //Bit-shifting makes me l33t. It also makes get_line() unnessecarrily fast. + + if(abs_x_distance >= abs_y_distance) //x distance is greater than y + for(var/distance_counter in 0 to (abs_x_distance - 1))//It'll take abs_x_distance steps to get there + y += abs_y_distance + + if(y >= abs_x_distance) //Every abs_y_distance steps, step once in y direction + y -= abs_x_distance + current_y_step += y_distance_sign + + current_x_step += x_distance_sign //Step on in x direction + line += locate(current_x_step, current_y_step, starting_z)//Add the turf to the list else - for(j=0 ;j < dyabs; j++) - x += dxabs - if(x >= dyabs) - x -= dyabs - px += sdx - py += sdy - line += locate(px, py, starting_atom.z) + for(var/distance_counter in 0 to (abs_y_distance - 1)) + x += abs_x_distance + + if(x >= abs_y_distance) + x -= abs_y_distance + current_x_step += x_distance_sign + + current_y_step += y_distance_sign + line += locate(current_x_step, current_y_step, starting_z) return line ///Format a power value in W, kW, MW, or GW. diff --git a/code/__HELPERS/spatial_info.dm b/code/__HELPERS/spatial_info.dm new file mode 100644 index 00000000000..99849c71040 --- /dev/null +++ b/code/__HELPERS/spatial_info.dm @@ -0,0 +1,380 @@ +/turf + ///what /mob/oranges_ear instance is already assigned to us as there should only ever be one. + ///used for guaranteeing there is only one oranges_ear per turf when assigned, speeds up view() iteration + var/mob/oranges_ear/assigned_oranges_ear + +/** # Oranges Ear + * + * turns out view() spends a significant portion of its processing time generating lists of contents of viewable turfs which includes EVERYTHING on it visible + * and the turf itself. there is an optimization to view() which makes it only generate lists of a certain atom type - this system takes advantage of that. + * a fuckton of these are generated as part of its SS's init and stored in a list, when requested for a list of movables returned by the spatial grid or by some + * superset of the final output that must be narrowed down by view() one of these gets put on every turf that contains the movables that need filtering + * and each is given references to the movables they represent. that way you can do for(var/mob/oranges_ear/ear in view(...)) and check what they reference + * as opposed to for(var/atom/movable/target in view(...)) and checking if they have the properties you want which leads to much larger lists generated by view() + * and also leads to iterating through more movables to filter them. + * + * TLDR: iterating through just mobs is much faster than all movables when iterating through view(), this system leverages that to boost speed + * enough to offset the cost of allocating the mobs + * + * named because the idea was first made by oranges and i didnt know what else to call it (note that this system was originally made for get_hearers_in_view()) + */ +/mob/oranges_ear + icon_state = null + density = FALSE + move_resist = INFINITY + invisibility = 0 + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + logging = null + held_items = null //all of these are list objects that should not exist for something like us + faction = null + alerts = null + screens = null + client_colours = null + hud_possible = null + /// references to everything "on" the turf we are assigned to, that we care about. populated in assign() and cleared in unassign(). + /// movables iside of other movables count as being "on" if they have get_turf(them) == our turf. intentionally not a lazylist + var/list/references = list() + +/mob/oranges_ear/Initialize(mapload) + SHOULD_CALL_PARENT(FALSE) + return INITIALIZE_HINT_NORMAL + +/mob/oranges_ear/Destroy(force) + var/old_length = length(SSspatial_grid.pregenerated_oranges_ears) + SSspatial_grid.pregenerated_oranges_ears -= src + if(length(SSspatial_grid.pregenerated_oranges_ears) < old_length) + SSspatial_grid.number_of_oranges_ears -= 1 + + var/turf/our_loc = get_turf(src) + if(our_loc && our_loc.assigned_oranges_ear == src) + our_loc.assigned_oranges_ear = null + + . = ..() + +/mob/oranges_ear/Move() + SHOULD_CALL_PARENT(FALSE) + stack_trace("SOMEHOW A /mob/oranges_ear MOVED") + return FALSE + +/mob/oranges_ear/abstract_move(atom/destination) + SHOULD_CALL_PARENT(FALSE) + stack_trace("SOMEHOW A /mob/oranges_ear MOVED") + return FALSE + +/mob/oranges_ear/Bump() + SHOULD_CALL_PARENT(FALSE) + return FALSE + +///clean this oranges_ear up for future use +/mob/oranges_ear/proc/unassign() + var/turf/turf_loc = loc + turf_loc.assigned_oranges_ear = null//trollface. our loc should ALWAYS be a turf, no exceptions. if it isnt then this doubles as an error message ;) + loc = null + references.Cut() + +/** + * returns every hearaing movable in view to the turf of source not taking into account lighting + * useful when you need to maintain always being able to hear something if a sound is emitted from it and you can see it (and youre in range). + * otherwise this is just a more expensive version of get_hearers_in_LOS() + * + * * view_radius - what radius search circle we are using, worse performance as this increases + * * source - object at the center of our search area. everything in get_turf(source) is guaranteed to be part of the search area + */ +/proc/get_hearers_in_view(view_radius, atom/source) + var/turf/center_turf = get_turf(source) + if(!center_turf) + return + + . = list() + + if(view_radius <= 0)//special case for if only source cares + for(var/atom/movable/target as anything in center_turf) + var/list/recursive_contents = target.important_recursive_contents?[RECURSIVE_CONTENTS_HEARING_SENSITIVE] + if(recursive_contents) + . += recursive_contents + return . + + var/list/hearables_from_grid = SSspatial_grid.orthogonal_range_search(source, RECURSIVE_CONTENTS_HEARING_SENSITIVE, view_radius) + + if(!length(hearables_from_grid))//we know that something is returned by the grid, but we dont know if we need to actually filter down the output + return . + + var/list/assigned_oranges_ears = SSspatial_grid.assign_oranges_ears(hearables_from_grid) + + var/old_luminosity = center_turf.luminosity + center_turf.luminosity = 6 //man if only we had an inbuilt dview() + + //this is the ENTIRE reason all this shit is worth it due to how view() works and can be optimized + //view() constructs lists of viewed atoms by default and specifying a specific type of atom to look for limits the lists it constructs to those of that + //primitive type and then when the view operation is completed the output is then typechecked to only iterate through objects in view with the same + //typepath. by assigning one /mob/oranges_ear to every turf with hearable atoms on it and giving them references to each one means that: + //1. view() only constructs lists of atoms with the mob primitive type and + //2. the mobs returned by view are fast typechecked to only iterate through /mob/oranges_ear mobs, which guarantees at most one per turf + //on a whole this can outperform iterating through all movables in view() by ~2x especially when hearables are a tiny percentage of movables in view + for(var/mob/oranges_ear/ear in view(view_radius, center_turf)) + . += ear.references + + for(var/mob/oranges_ear/remaining_ear as anything in assigned_oranges_ears)//we need to clean up our mess + remaining_ear.unassign() + + center_turf.luminosity = old_luminosity + return . + +/** + * Returns a list of movable atoms that are hearing sensitive in view_radius and line of sight to source + * the majority of the work is passed off to the spatial grid if view_radius > 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 240) - L.offset_x -= 480 - if(L.offset_x < -240) - L.offset_x += 480 - if(L.offset_y > 240) - L.offset_y -= 480 - if(L.offset_y < -240) - L.offset_y += 480 + if(parallax_layer.offset_x > 240) + parallax_layer.offset_x -= 480 + if(parallax_layer.offset_x < -240) + parallax_layer.offset_x += 480 + if(parallax_layer.offset_y > 240) + parallax_layer.offset_y -= 480 + if(parallax_layer.offset_y < -240) + parallax_layer.offset_y += 480 - L.screen_loc = "CENTER-7:[round(L.offset_x,1)],CENTER-7:[round(L.offset_y,1)]" + parallax_layer.screen_loc = "CENTER-7:[round(parallax_layer.offset_x,1)],CENTER-7:[round(parallax_layer.offset_y,1)]" /atom/movable/proc/update_parallax_contents() - if(length(client_mobs_in_contents)) - for(var/mob/client_mob as anything in client_mobs_in_contents) - if(length(client_mob?.client?.parallax_layers) && client_mob.hud_used) - client_mob.hud_used.update_parallax() + for(var/mob/client_mob as anything in client_mobs_in_contents) + if(length(client_mob?.client?.parallax_layers) && client_mob.hud_used) + client_mob.hud_used.update_parallax() /mob/proc/update_parallax_teleport() //used for arrivals shuttle if(client?.eye && hud_used && length(client.parallax_layers)) diff --git a/code/controllers/subsystem/parallax.dm b/code/controllers/subsystem/parallax.dm index c9573fbaa57..9d42217fbb4 100644 --- a/code/controllers/subsystem/parallax.dm +++ b/code/controllers/subsystem/parallax.dm @@ -41,14 +41,18 @@ SUBSYSTEM_DEF(parallax) continue for (movable_eye; isloc(movable_eye.loc) && !isturf(movable_eye.loc); movable_eye = movable_eye.loc); + //get the last movable holding the mobs eye if(movable_eye == processing_client.movingmob) if (MC_TICK_CHECK) return continue + + //eye and the last recorded eye are different, and the last recorded eye isnt just the clients mob if(!isnull(processing_client.movingmob)) LAZYREMOVE(processing_client.movingmob.client_mobs_in_contents, processing_client.mob) LAZYADD(movable_eye.client_mobs_in_contents, processing_client.mob) + processing_client.movingmob = movable_eye if (MC_TICK_CHECK) return diff --git a/code/controllers/subsystem/spatial_gridmap.dm b/code/controllers/subsystem/spatial_gridmap.dm new file mode 100644 index 00000000000..a3cf7aa5953 --- /dev/null +++ b/code/controllers/subsystem/spatial_gridmap.dm @@ -0,0 +1,593 @@ +///the subsystem creates this many [/mob/oranges_ear] mob instances during init. allocations that require more than this create more. +#define NUMBER_OF_PREGENERATED_ORANGES_EARS 2500 + +// macros meant specifically to add/remove movables from the hearing_contents and client_contents lists of +// /datum/spatial_grid_cell, when empty they become references to a single list in SSspatial_grid and when filled they become their own list +// this is to save memory without making them lazylists as that slows down iteration through them +#define GRID_CELL_ADD(cell_contents_list, movable_or_list) \ + if(!length(cell_contents_list)) { \ + cell_contents_list = list(); \ + cell_contents_list += movable_or_list; \ + } else { \ + cell_contents_list += movable_or_list; \ + }; + +#define GRID_CELL_SET(cell_contents_list, movable_or_list) \ + if(!length(cell_contents_list)) { \ + cell_contents_list = list(); \ + cell_contents_list += movable_or_list; \ + } else { \ + cell_contents_list |= movable_or_list; \ + }; + +//dont use these outside of SSspatial_grid's scope use the procs it has for this purpose +#define GRID_CELL_REMOVE(cell_contents_list, movable_or_list) \ + cell_contents_list -= movable_or_list; \ + if(!length(cell_contents_list)) {\ + cell_contents_list = dummy_list; \ + }; + +/** + * # Spatial Grid Cell + * + * 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 + +/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 + +/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 + * + * as of right now this system operates on a subset of the important_recursive_contents list for atom/movable, specifically + * [RECURSIVE_CONTENTS_HEARING_SENSITIVE] and [RECURSIVE_CONTENTS_CLIENT_MOBS] because both are those are both 1. important and 2. commonly searched for + */ +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(RECURSIVE_CONTENTS_HEARING_SENSITIVE = list(), RECURSIVE_CONTENTS_CLIENT_MOBS = list()) + + var/cells_on_x_axis = 0 + var/cells_on_y_axis = 0 + + ///empty spatial grid cell content lists are just a reference to this instead of a standalone list to save memory without needed to check if its null when iterating + var/list/dummy_list = list() + + ///list of all of /mob/oranges_ear instances we have pregenerated for view() iteration speedup + var/list/mob/oranges_ear/pregenerated_oranges_ears = list() + ///how many pregenerated /mob/oranges_ear instances currently exist. this should hopefully never exceed its starting value + var/number_of_oranges_ears = NUMBER_OF_PREGENERATED_ORANGES_EARS + +/datum/controller/subsystem/spatial_grid/Initialize(start_timeofday) + . = ..() + + cells_on_x_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxx) + cells_on_y_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxy) + + for(var/datum/space_level/z_level as anything in SSmapping.z_list) + propogate_spatial_grid_to_new_z(null, z_level) + CHECK_TICK_HIGH_PRIORITY + + //go through the pre init queue for anything waiting to be let in the grid + for(var/channel_type in waiting_to_add_by_type) + for(var/atom/movable/movable as anything in waiting_to_add_by_type[channel_type]) + var/turf/movable_turf = get_turf(movable) + if(movable_turf) + enter_cell(movable, movable_turf) + + UnregisterSignal(movable, COMSIG_PARENT_PREQDELETED) + waiting_to_add_by_type[channel_type] -= movable + + pregenerate_more_oranges_ears(NUMBER_OF_PREGENERATED_ORANGES_EARS) + + RegisterSignal(SSdcs, COMSIG_GLOB_NEW_Z, .proc/propogate_spatial_grid_to_new_z) + RegisterSignal(SSdcs, COMSIG_GLOB_EXPANDED_WORLD_BOUNDS, .proc/after_world_bounds_expanded) + +///add a movable to the pre init queue for whichever type is specified so that when the subsystem initializes they get added to the grid +/datum/controller/subsystem/spatial_grid/proc/enter_pre_init_queue(atom/movable/waiting_movable, type) + RegisterSignal(waiting_movable, COMSIG_PARENT_PREQDELETED, .proc/queued_item_deleted, override = TRUE) + //override because something can enter the queue for two different types but that is done through unrelated procs that shouldnt know about eachother + waiting_to_add_by_type[type] += waiting_movable + +///removes an initialized and probably deleted movable from our pre init queue before we're initialized +/datum/controller/subsystem/spatial_grid/proc/remove_from_pre_init_queue(atom/movable/movable_to_remove, exclusive_type) + if(exclusive_type) + waiting_to_add_by_type[exclusive_type] -= movable_to_remove + + var/waiting_movable_is_in_other_queues = FALSE//we need to check if this movable is inside the other queues + for(var/type in waiting_to_add_by_type) + if(movable_to_remove in waiting_to_add_by_type[type]) + waiting_movable_is_in_other_queues = TRUE + + if(!waiting_movable_is_in_other_queues) + UnregisterSignal(movable_to_remove, COMSIG_PARENT_PREQDELETED) + + return + + UnregisterSignal(movable_to_remove, COMSIG_PARENT_PREQDELETED) + 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 + +///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 + +///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(ROUND_UP((center_coord - range) / SPATIAL_GRID_CELLSIZE), 1) +///the right or upper side index of a box composed of spatial grid cells with the given center x or y coordinate. +///outputted value cant exceed the number of cells on that axis +#define BOUNDING_BOX_MAX(center_coord, axis_size) min(ROUND_UP((center_coord + range) / SPATIAL_GRID_CELLSIZE), axis_size) + +/** + * 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() + + //cache for sanic speeds + var/cells_on_y_axis = src.cells_on_y_axis + var/cells_on_x_axis = src.cells_on_x_axis + + //technically THIS list only contains lists, but inside those lists are grid cell datums and we can go without a SINGLE var init if we do this + var/list/datum/spatial_grid_cell/grid_level = grids_by_z_level[center_turf.z] + switch(type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + for(var/row in BOUNDING_BOX_MIN(center_y) to BOUNDING_BOX_MAX(center_y, cells_on_y_axis)) + for(var/x_index in BOUNDING_BOX_MIN(center_x) to BOUNDING_BOX_MAX(center_x, cells_on_x_axis)) + + . += grid_level[row][x_index].client_contents + + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + for(var/row in BOUNDING_BOX_MIN(center_y) to BOUNDING_BOX_MAX(center_y, cells_on_y_axis)) + for(var/x_index in BOUNDING_BOX_MIN(center_x) to BOUNDING_BOX_MAX(center_x, cells_on_x_axis)) + + . += grid_level[row][x_index].hearing_contents + + 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][ROUND_UP(target_turf.y / SPATIAL_GRID_CELLSIZE)][ROUND_UP(target_turf.x / SPATIAL_GRID_CELLSIZE)] + +///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) + 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(ROUND_UP((center_x - range) / SPATIAL_GRID_CELLSIZE), 1) + var/min_y = max(ROUND_UP((center_y - range) / SPATIAL_GRID_CELLSIZE), 1)//calculating these indices only takes around 2 microseconds + + //the maximum x and y cell indexes to test + var/max_x = min(ROUND_UP((center_x + range) / SPATIAL_GRID_CELLSIZE), cells_on_x_axis) + var/max_y = min(ROUND_UP((center_y + range) / SPATIAL_GRID_CELLSIZE), cells_on_y_axis) + + var/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 + +///find the spatial map cell that target belongs to, then add target's important_recusive_contents to it. +///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(!target_turf || !new_target?.important_recursive_contents) + CRASH("/datum/controller/subsystem/spatial_grid/proc/enter_cell() was given null arguments or a new_target without important_recursive_contents!") + + var/x_index = ROUND_UP(target_turf.x / SPATIAL_GRID_CELLSIZE) + var/y_index = ROUND_UP(target_turf.y / SPATIAL_GRID_CELLSIZE) + var/z_index = target_turf.z + + var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index] + + if(new_target.important_recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS]) + GRID_CELL_SET(intersecting_cell.client_contents, new_target.important_recursive_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS]) + + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(RECURSIVE_CONTENTS_CLIENT_MOBS), new_target) + + if(new_target.important_recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE]) + GRID_CELL_SET(intersecting_cell.hearing_contents, new_target.important_recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE]) + + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(RECURSIVE_CONTENTS_HEARING_SENSITIVE), new_target) + +/** + * find the spatial map cell that target used to belong to, then subtract target's important_recusive_contents from it. + * make sure to provide the turf old_target used to be "in" + * + * * old_target - the thing we want to remove from the spatial grid cell + * * target_turf - the turf we use to determine the cell we're removing from + * * exclusive_type - either null or a valid contents channel. if you just want to remove a single type from the grid cell then use this + */ +/datum/controller/subsystem/spatial_grid/proc/exit_cell(atom/movable/old_target, turf/target_turf, exclusive_type) + if(!target_turf || !old_target?.important_recursive_contents) + CRASH("/datum/controller/subsystem/spatial_grid/proc/exit_cell() was given null arguments or a new_target without important_recursive_contents!") + + var/x_index = ROUND_UP(target_turf.x / SPATIAL_GRID_CELLSIZE) + var/y_index = ROUND_UP(target_turf.y / SPATIAL_GRID_CELLSIZE) + var/z_index = target_turf.z + + var/list/grid = grids_by_z_level[z_index] + var/datum/spatial_grid_cell/intersecting_cell = grid[y_index][x_index] + + if(exclusive_type && old_target.important_recursive_contents[exclusive_type]) + switch(exclusive_type) + if(RECURSIVE_CONTENTS_CLIENT_MOBS) + GRID_CELL_REMOVE(intersecting_cell.client_contents, old_target.important_recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS]) + + if(RECURSIVE_CONTENTS_HEARING_SENSITIVE) + GRID_CELL_REMOVE(intersecting_cell.hearing_contents, old_target.important_recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE]) + + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(exclusive_type), old_target) + return + + if(old_target.important_recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS]) + GRID_CELL_REMOVE(intersecting_cell.client_contents, old_target.important_recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS]) + + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), old_target) + + if(old_target.important_recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE]) + GRID_CELL_REMOVE(intersecting_cell.hearing_contents, old_target.important_recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE]) + + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(RECURSIVE_CONTENTS_HEARING_SENSITIVE), old_target) + +///find the cell this movable is associated with and removes it from all lists +/datum/controller/subsystem/spatial_grid/proc/force_remove_from_cell(atom/movable/to_remove, datum/spatial_grid_cell/input_cell) + if(!initialized) + remove_from_pre_init_queue(to_remove)//the spatial grid doesnt exist yet, so just take it out of the queue + return + + if(!input_cell) + input_cell = get_cell_of(to_remove) + if(!input_cell) + find_hanging_cell_refs_for_movable(to_remove, TRUE) + return + + GRID_CELL_REMOVE(input_cell.client_contents, to_remove) + GRID_CELL_REMOVE(input_cell.hearing_contents, to_remove) + +///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)) + containing_cells += cell + if(remove_from_cells) + force_remove_from_cell(to_remove, cell) + + return containing_cells + +///debug proc for checking if a movable is in multiple cells when it shouldnt be (ie always unless multitile entering is implemented) +/atom/proc/find_all_cells_containing(remove_from_cells = FALSE) + var/datum/spatial_grid_cell/real_cell = SSspatial_grid.get_cell_of(src) + var/list/containing_cells = SSspatial_grid.find_hanging_cell_refs_for_movable(src, FALSE, remove_from_cells) + + message_admins("[src] is located in the contents of [length(containing_cells)] spatial grid cells") + + var/cell_coords = "the following cells contain [src]: " + for(var/datum/spatial_grid_cell/cell as anything in containing_cells) + cell_coords += "([cell.cell_x], [cell.cell_y], [cell.cell_z]), " + + message_admins(cell_coords) + message_admins("[src] is supposed to only be contained in the cell at indexes ([real_cell.cell_x], [real_cell.cell_y], [real_cell.cell_z]). but is contained at the cells at [cell_coords]") + +///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/cells_with_clients = 0 + var/cells_with_hearables = 0 + + var/list/client_list = list() + var/list/hearable_list = list() + + var/total_cells = (world.maxx / SPATIAL_GRID_CELLSIZE) ** 2 + + var/average_clients_per_cell = 0 + var/average_hearables_per_cell = 0 + + var/hearable_min_x = (world.maxx / SPATIAL_GRID_CELLSIZE) + var/hearable_max_x = 1 + + var/hearable_min_y = (world.maxy / SPATIAL_GRID_CELLSIZE) + var/hearable_max_y = 1 + + var/client_min_x = (world.maxx / SPATIAL_GRID_CELLSIZE) + var/client_max_x = 1 + + var/client_min_y = (world.maxy / SPATIAL_GRID_CELLSIZE) + var/client_max_y = 1 + + var/list/inserted_clients = list() + + if(insert_clients) + var/list/turfs + var/level = SSmapping.get_level(z) + if(is_station_level(level)) + turfs = GLOB.station_turfs + + else + turfs = block(locate(1,1,z), locate(world.maxx, world.maxy, z)) + + for(var/client_to_insert in 0 to insert_clients) + var/turf/random_turf = pick(turfs) + var/mob/fake_client = new() + fake_client.important_recursive_contents = list(SPATIAL_GRID_CONTENTS_TYPE_HEARING = list(fake_client), SPATIAL_GRID_CONTENTS_TYPE_CLIENTS = list(fake_client)) + fake_client.forceMove(random_turf) + inserted_clients += fake_client + + var/list/all_z_level_cells = SSspatial_grid.get_cells_in_range(src, 1000) + + for(var/datum/spatial_grid_cell/cell as anything in all_z_level_cells) + var/client_length = length(cell.client_contents) + var/hearable_length = length(cell.hearing_contents) + + raw_clients += client_length + raw_hearables += hearable_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 + + var/total_client_distance = 0 + var/total_hearable_distance = 0 + + var/average_client_distance = 0 + var/average_hearable_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) + + 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) + + average_clients_per_cell = raw_clients / total_cells + average_hearables_per_cell = raw_hearables / 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) \ + and [raw_hearables] hearables. all of whom are inside the bounding box given by \ + clients: ([client_min_x], [client_min_y]) x ([client_max_x], [client_max_y]) \ + and hearables: ([hearable_min_x], [hearable_min_y]) x ([hearable_max_x], [hearable_max_y]) \ + on average there are [average_clients_per_cell] clients per cell and [average_hearables_per_cell] hearables per cell. \ + [cells_with_clients] cells have clients and [cells_with_hearables] have hearables, \ + the average client distance is: [average_client_distance] and the average hearable_distance is [average_hearable_distance].") + +#undef GRID_CELL_ADD +#undef GRID_CELL_REMOVE +#undef GRID_CELL_SET diff --git a/code/datums/components/mood.dm b/code/datums/components/mood.dm index cb571449f47..e7dfaee399e 100644 --- a/code/datums/components/mood.dm +++ b/code/datums/components/mood.dm @@ -36,7 +36,8 @@ /datum/component/mood/Destroy() STOP_PROCESSING(SSmood, src) - REMOVE_TRAIT(parent, TRAIT_AREA_SENSITIVE, MOOD_COMPONENT_TRAIT) + var/atom/movable/movable_parent = parent + movable_parent.lose_area_sensitivity(MOOD_COMPONENT_TRAIT) unmodify_hud() return ..() diff --git a/code/datums/datum.dm b/code/datums/datum.dm index 8fbd5fa1476..e8391a7116e 100644 --- a/code/datums/datum.dm +++ b/code/datums/datum.dm @@ -19,7 +19,7 @@ /// Active timers with this datum as the target var/list/active_timers - /// Status traits attached to this datum + /// Status traits attached to this datum. associative list of the form: list(trait name (string) = list(source1, source2, source3,...)) var/list/status_traits /** @@ -98,8 +98,7 @@ var/list/timers = active_timers active_timers = null - for(var/thing in timers) - var/datum/timedevent/timer = thing + for(var/datum/timedevent/timer as anything in timers) if (timer.spent && !(timer.flags & TIMER_DELETE_ME)) continue qdel(timer) @@ -115,9 +114,8 @@ if(dc) var/all_components = dc[/datum/component] if(length(all_components)) - for(var/I in all_components) - var/datum/component/C = I - qdel(C, FALSE, TRUE) + for(var/datum/component/component as anything in all_components) + qdel(component, FALSE, TRUE) else var/datum/component/C = all_components qdel(C, FALSE, TRUE) @@ -136,8 +134,7 @@ for(var/sig in lookup) var/list/comps = lookup[sig] if(length(comps)) - for(var/i in comps) - var/datum/component/comp = i + for(var/datum/component/comp as anything in comps) comp.UnregisterSignal(src, sig) else var/datum/component/comp = comps diff --git a/code/datums/elements/beauty.dm b/code/datums/elements/beauty.dm index 937800876e6..06cabb96d47 100644 --- a/code/datums/elements/beauty.dm +++ b/code/datums/elements/beauty.dm @@ -59,7 +59,9 @@ if(current_area) exit_area(source, current_area) beauty_counter -= source - REMOVE_TRAIT(source, TRAIT_AREA_SENSITIVE, BEAUTY_ELEMENT_TRAIT) + var/atom/movable/movable_source = source + if(istype(movable_source)) + movable_source.lose_area_sensitivity(BEAUTY_ELEMENT_TRAIT) else //lower the 'counter' down by one, update the area, and call parent if it's reached zero. beauty_counter[source]-- if(current_area && !current_area.outdoors) @@ -69,4 +71,6 @@ . = ..() UnregisterSignal(source, list(COMSIG_ENTER_AREA, COMSIG_EXIT_AREA)) beauty_counter -= source - REMOVE_TRAIT(source, TRAIT_AREA_SENSITIVE, BEAUTY_ELEMENT_TRAIT) + var/atom/movable/movable_source = source + if(istype(movable_source)) + movable_source.lose_area_sensitivity(BEAUTY_ELEMENT_TRAIT) diff --git a/code/datums/holocall.dm b/code/datums/holocall.dm index 189a32baa6e..0ab1c620dbe 100644 --- a/code/datums/holocall.dm +++ b/code/datums/holocall.dm @@ -22,17 +22,25 @@ //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 + ///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 - 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 + ///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 receiving pad is not secure. + ///calls from a head of staff autoconnect, if the receiving pad is not secure. + var/head_call = FALSE //creates a holocall made by `caller` from `calling_pad` to `callees` /datum/holocall/New(mob/living/caller, obj/machinery/holopad/calling_pad, list/callees, elevated_access = FALSE) @@ -43,19 +51,18 @@ 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 + for(var/obj/machinery/holopad/connected_holopad as anything in callees) + if(!QDELETED(connected_holopad) && connected_holopad.is_operational) + dialed_holopads += connected_holopad if(head_call) - if(H.secure) + if(connected_holopad.secure) calling_pad.say("Auto-connection refused, falling back to call mode.") - H.say("Incoming call.") + connected_holopad.say("Incoming call.") else - H.say("Incoming connection.") + connected_holopad.say("Incoming connection.") else - H.say("Incoming call.") - LAZYADD(H.holo_calls, src) + connected_holopad.say("Incoming call.") + connected_holopad.set_holocall(src) if(!dialed_holopads.len) calling_pad.say("Connection failure.") @@ -83,12 +90,12 @@ QDEL_NULL(hologram) hologram = null - 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() @@ -112,75 +119,74 @@ 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) - playsound(H, 'sound/machines/ping.ogg', 100) - H.say("Connection established.") + playsound(answering_holopad, 'sound/machines/ping.ogg', 100) + answering_holopad.say("Connection established.") //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/_wires.dm b/code/datums/wires/_wires.dm index e4633988496..3bf5f345dcd 100644 --- a/code/datums/wires/_wires.dm +++ b/code/datums/wires/_wires.dm @@ -110,7 +110,7 @@ randomize() /datum/wires/proc/repair() - cut_wires.Cut() + cut_wires.Cut()//a negative times a negative equals a positive /datum/wires/proc/get_wire(color) return colors[color] diff --git a/code/datums/wires/radio.dm b/code/datums/wires/radio.dm index 538685aca3a..8f1b1d4ce07 100644 --- a/code/datums/wires/radio.dm +++ b/code/datums/wires/radio.dm @@ -19,9 +19,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/atoms_movable.dm b/code/game/atoms_movable.dm index 2c777dc51cf..a7a67e0c1a7 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -36,14 +36,16 @@ 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/datum/forced_movement/force_moving = null //handled soley by forced_movement.dm /** * 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 + * 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 /** * In case you have multiple types, you automatically use the most useful one. @@ -81,7 +83,6 @@ /// The degree of pressure protection that mobs in list/contents have from the external environment, between 0 and 1 var/contents_pressure_protection = 0 - /atom/movable/Initialize(mapload) . = ..() switch(blocks_emissive) @@ -132,19 +133,20 @@ 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) + + LAZYCLEARLIST(important_recursive_contents)//has to be before moveToNullspace() so that we can exit our spatial_grid cell if we're in it + . = ..() for(var/movable_content in contents) qdel(movable_content) - LAZYCLEARLIST(client_mobs_in_contents) - moveToNullspace() - //We add ourselves to this list, best to clear it out - //DO it after moveToNullspace so memes can be had - LAZYCLEARLIST(important_recursive_contents) - vis_locs = null //clears this atom out of all viscontents vis_contents.Cut() @@ -645,7 +647,7 @@ if (!inertia_moving) inertia_next_move = world.time + inertia_move_delay newtonian_move(movement_dir) - if (length(client_mobs_in_contents)) + if (client_mobs_in_contents) update_parallax_contents() move_stacks-- @@ -663,6 +665,20 @@ if (old_turf?.z != new_turf?.z) on_changed_z_level(old_turf, new_turf) + 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) + return TRUE // Make sure you know what you're doing if you call this, this is intended to only be called by byond directly. @@ -745,33 +761,85 @@ ///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)) - RegisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_HEARING_SENSITIVE), .proc/on_hearing_sensitive_trait_loss) + //RegisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_HEARING_SENSITIVE), .proc/on_hearing_sensitive_trait_loss) 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)) - RegisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_AREA_SENSITIVE), .proc/on_area_sensitive_trait_loss) + //RegisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_AREA_SENSITIVE), .proc/on_area_sensitive_trait_loss) 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) -/atom/movable/proc/on_area_sensitive_trait_loss() - SIGNAL_HANDLER +///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 - UnregisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_AREA_SENSITIVE)) for(var/atom/movable/location as anything in get_nested_locs(src) + src) LAZYREMOVEASSOC(location.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE, src) -/atom/movable/proc/on_hearing_sensitive_trait_loss() - SIGNAL_HANDLER +///propogates new_client's mob through our nested contents, similar to other important_recursive_contents procs +///main difference is that client contents need to possibly duplicate recursive contents for the clients mob AND its eye +/atom/movable/proc/enable_client_mobs_in_contents(client/new_client) + var/turf/our_turf = get_turf(src) - UnregisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_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) + 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, new_client.mob) + +///Clears the clients channel of this movables important_recursive_contents list and all nested locs +/atom/movable/proc/clear_important_client_contents(client/former_client) + + 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, former_client.mob) ///Sets the anchored var and returns if it was sucessfully changed or not. /atom/movable/proc/set_anchored(anchorvalue) diff --git a/code/game/communications.dm b/code/game/communications.dm index dd2b5d57cb9..e4517f4f644 100644 --- a/code/game/communications.dm +++ b/code/game/communications.dm @@ -64,6 +64,7 @@ so i made radios not use the radio controller. */ GLOBAL_LIST_EMPTY(all_radios) + /proc/add_radio(obj/item/radio, freq) if(!freq || !radio) return diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm index 2cdaae24724..a08d90e2b91 100644 --- a/code/game/machinery/_machinery.dm +++ b/code/game/machinery/_machinery.dm @@ -208,7 +208,7 @@ if(our_area) UnregisterSignal(our_area, COMSIG_AREA_POWER_CHANGE) - REMOVE_TRAIT(src, TRAIT_AREA_SENSITIVE, INNATE_TRAIT) + lose_area_sensitivity(INNATE_TRAIT) UnregisterSignal(src, COMSIG_ENTER_AREA) UnregisterSignal(src, COMSIG_EXIT_AREA) diff --git a/code/game/machinery/bank_machine.dm b/code/game/machinery/bank_machine.dm index ea79f56ddc5..abc6a2a187d 100644 --- a/code/game/machinery/bank_machine.dm +++ b/code/game/machinery/bank_machine.dm @@ -17,6 +17,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/civilian_bounties.dm b/code/game/machinery/civilian_bounties.dm index 3f4881a1f54..e7d167b73be 100644 --- a/code/game/machinery/civilian_bounties.dm +++ b/code/game/machinery/civilian_bounties.dm @@ -277,7 +277,7 @@ . = ..() radio = new(src) radio.keyslot = new radio_key - radio.listening = FALSE + radio.set_listening(FALSE) radio.recalculateChannels() /obj/item/bounty_cube/Destroy() diff --git a/code/game/machinery/computer/arcade/orion.dm b/code/game/machinery/computer/arcade/orion.dm index c3f65d1e6c8..d773828cf7e 100644 --- a/code/game/machinery/computer/arcade/orion.dm +++ b/code/game/machinery/computer/arcade/orion.dm @@ -45,7 +45,7 @@ GLOBAL_LIST_INIT(orion_events, generate_orion_events()) /obj/machinery/computer/arcade/orion_trail/Initialize(mapload) . = ..() radio = new /obj/item/radio(src) - radio.listening = 0 + radio.set_listening(FALSE) setup_events() /obj/machinery/computer/arcade/orion_trail/proc/setup_events() diff --git a/code/game/machinery/computer/chef_orders/chef_order.dm b/code/game/machinery/computer/chef_orders/chef_order.dm index 3f553afe8e8..303ae382d8e 100644 --- a/code/game/machinery/computer/chef_orders/chef_order.dm +++ b/code/game/machinery/computer/chef_orders/chef_order.dm @@ -17,7 +17,7 @@ /obj/machinery/computer/chef_order/Initialize(mapload) . = ..() radio = new(src) - radio.frequency = FREQ_SUPPLY + radio.set_frequency(FREQ_SUPPLY) radio.subspace_transmission = TRUE radio.canhear_range = 0 radio.recalculateChannels() diff --git a/code/game/machinery/doors/brigdoors.dm b/code/game/machinery/doors/brigdoors.dm index ade37ef3006..a2f2c810301 100644 --- a/code/game/machinery/doors/brigdoors.dm +++ b/code/game/machinery/doors/brigdoors.dm @@ -48,7 +48,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 50628158c52..ac8b70675f5 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 @@ -41,7 +46,8 @@ Possible to do for anyone motivated enough: max_integrity = 300 armor = list(MELEE = 50, BULLET = 20, LASER = 20, ENERGY = 20, BOMB = 0, BIO = 0, FIRE = 50, ACID = 0) circuit = /obj/item/circuitboard/machine/holopad - /// List of living mobs that use the holopad + /// associative lazylist of the form: list(mob calling us = hologram representing that mob). + /// this is only populated for holopads answering calls from another holopad var/list/masters /// Holoray-mob link var/list/holorays @@ -49,9 +55,10 @@ Possible to do for anyone motivated enough: var/last_request = 0 /// Change to change how far the AI can move away from the holopad before deactivating var/holo_range = 5 - /// Array of /datum/holocalls + /// Array of /datum/holocalls that are calling US. this is only filled for holopads answering calls from another holopad var/list/holo_calls - /// Currently outgoing holocall, do not modify the datums only check and call the public procs + /// Currently outgoing holocall, cannot call any other holopads unless this is null. + /// creating a new holocall from us to another holopad sets this var to that holocall datum var/datum/holocall/outgoing_call /// Record disk var/obj/item/disk/holodisk/disk @@ -78,10 +85,8 @@ Possible to do for anyone motivated enough: var/secure = FALSE /// If we are currently calling another holopad var/calling = FALSE - -/obj/machinery/holopad/Initialize(mapload) - . = ..() - become_hearing_sensitive() + ///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" @@ -156,9 +161,8 @@ Possible to do for anyone motivated enough: 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) @@ -363,13 +367,57 @@ Possible to do for anyone motivated enough: 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) /obj/machinery/holopad/attack_ai_secondary(mob/living/silicon/ai/user) if (!istype(user)) @@ -400,6 +448,8 @@ Possible to do for anyone motivated enough: clear_holo(user) return +//this really should not be processing by default with how common holopads are +//everything in here can start processing if need be once first set and stop processing after being unset /obj/machinery/holopad/process() if(LAZYLEN(masters)) for(var/mob/living/master as anything in masters) @@ -475,13 +525,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) - if(speaker == HC.hologram && HC.user.client?.prefs.read_preference(/datum/preference/toggle/enable_runechat)) - HC.user.create_chat_message(speaker, message_language, raw_message, spans) + 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 - HC.user.Hear(message, speaker, message_language, raw_message, radio_freq, spans, message_mods) + holocall_to_update.user.Hear(message, speaker, message_language, raw_message, radio_freq, spans, message_mods) if(outgoing_call?.hologram && speaker == outgoing_call.user) outgoing_call.hologram.say(raw_message, sanitize = FALSE) @@ -510,6 +559,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 @@ -527,6 +577,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() @@ -639,6 +691,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ 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) @@ -708,6 +761,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ if(record_mode) record_mode = FALSE record_user = null + set_can_hear_flags(CAN_HEAR_RECORD_MODE, FALSE) /obj/machinery/holopad/proc/record_clear() if(disk?.record) @@ -747,3 +801,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 cae2f1c7678..59531d4a45e 100644 --- a/code/game/machinery/requests_console.dm +++ b/code/game/machinery/requests_console.dm @@ -118,7 +118,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/requests_console, 30) 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 7c4a19f542a..c0eecb9c732 100644 --- a/code/game/machinery/telecomms/broadcasting.dm +++ b/code/game/machinery/telecomms/broadcasting.dm @@ -128,7 +128,7 @@ copy.levels = levels return copy -// This is the meat function for making radios hear vocal transmissions. +/// This is the meat function for making radios hear vocal transmissions. /datum/signal/subspace/vocal/broadcast() set waitfor = FALSE @@ -140,47 +140,60 @@ if(compression > 0) message = Gibberish(message, compression >= 30) + 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, levels)) + 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, levels)) + 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) // Add observers who have ghost radio enabled. - for(var/mob/dead/observer/M in GLOB.player_list) - if(M.client?.prefs.chat_toggles & CHAT_GHOSTRADIO) - receive |= M + for(var/mob/dead/observer/ghost in GLOB.player_list) + if(ghost.client.prefs?.chat_toggles & CHAT_GHOSTRADIO) + receive |= ghost // Render the message and have everybody hear it. // Always call this on the virtualspeaker to avoid issues. 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) + + 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) // This following recording is intended for research and feedback in the use of department radio channels @@ -198,7 +211,8 @@ 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 90b353fa3d4..6b50a29d732 100644 --- a/code/game/machinery/telecomms/machine_interactions.dm +++ b/code/game/machinery/telecomms/machine_interactions.dm @@ -114,7 +114,7 @@ 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].") @@ -138,22 +138,11 @@ if("unlink") var/obj/machinery/telecomms/T = 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, operator) if("link") if(heldmultitool) var/obj/machinery/telecomms/T = heldmultitool.buffer - if(istype(T) && T != src) - if(!(src in T.links)) - T.links += src - if(!(T in links)) - links += T - log_game("[key_name(operator)] linked [src] for [T] at [AREACOORD(src)].") - . = TRUE + . = add_new_link(T, operator) if("buffer") heldmultitool.buffer = src . = TRUE @@ -164,6 +153,42 @@ 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 + + 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) + + 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 554021553d6..6f0a2fcf7c1 100644 --- a/code/game/machinery/telecomms/machines/broadcaster.dm +++ b/code/game/machinery/telecomms/machines/broadcaster.dm @@ -12,6 +12,7 @@ GLOBAL_VAR_INIT(message_delay, 0) // To make sure restarting the recentmessages name = "subspace broadcaster" icon_state = "broadcaster" desc = "A dish-shaped machine used to broadcast processed subspace signals." + telecomms_type = /obj/machinery/telecomms/broadcaster density = TRUE use_power = IDLE_POWER_USE idle_power_usage = 25 diff --git a/code/game/machinery/telecomms/machines/bus.dm b/code/game/machinery/telecomms/machines/bus.dm index 761392af4ca..76c89063dfd 100644 --- a/code/game/machinery/telecomms/machines/bus.dm +++ b/code/game/machinery/telecomms/machines/bus.dm @@ -12,6 +12,7 @@ name = "bus mainframe" icon_state = "bus" desc = "A mighty piece of hardware used to send massive amounts of data quickly." + telecomms_type = /obj/machinery/telecomms/bus density = TRUE use_power = IDLE_POWER_USE idle_power_usage = 50 diff --git a/code/game/machinery/telecomms/machines/hub.dm b/code/game/machinery/telecomms/machines/hub.dm index dedf7c7f3a7..290cb094e7d 100644 --- a/code/game/machinery/telecomms/machines/hub.dm +++ b/code/game/machinery/telecomms/machines/hub.dm @@ -12,6 +12,7 @@ name = "telecommunication hub" icon_state = "hub" desc = "A mighty piece of hardware used to send/receive massive amounts of data." + telecomms_type = /obj/machinery/telecomms/hub density = TRUE use_power = IDLE_POWER_USE idle_power_usage = 80 diff --git a/code/game/machinery/telecomms/machines/message_server.dm b/code/game/machinery/telecomms/machines/message_server.dm index 737680ea4f6..e188640677c 100644 --- a/code/game/machinery/telecomms/machines/message_server.dm +++ b/code/game/machinery/telecomms/machines/message_server.dm @@ -75,6 +75,7 @@ icon_state = "message_server" name = "Messaging Server" desc = "A machine that processes and routes PDA and request console messages." + telecomms_type = /obj/machinery/telecomms/message_server density = TRUE use_power = IDLE_POWER_USE idle_power_usage = 10 diff --git a/code/game/machinery/telecomms/machines/processor.dm b/code/game/machinery/telecomms/machines/processor.dm index 2362273469a..02db6748971 100644 --- a/code/game/machinery/telecomms/machines/processor.dm +++ b/code/game/machinery/telecomms/machines/processor.dm @@ -10,6 +10,7 @@ name = "processor unit" icon_state = "processor" desc = "This machine is used to process large quantities of information." + telecomms_type = /obj/machinery/telecomms/processor density = TRUE use_power = IDLE_POWER_USE idle_power_usage = 30 diff --git a/code/game/machinery/telecomms/machines/receiver.dm b/code/game/machinery/telecomms/machines/receiver.dm index 66ce5e62117..f9abd5972db 100644 --- a/code/game/machinery/telecomms/machines/receiver.dm +++ b/code/game/machinery/telecomms/machines/receiver.dm @@ -10,6 +10,7 @@ name = "subspace receiver" icon_state = "broadcast receiver" desc = "This machine has a dish-like shape and green lights. It is designed to detect and process subspace radio activity." + telecomms_type = /obj/machinery/telecomms/receiver density = TRUE use_power = IDLE_POWER_USE idle_power_usage = 30 diff --git a/code/game/machinery/telecomms/machines/relay.dm b/code/game/machinery/telecomms/machines/relay.dm index 7b7084491f3..5744c8086fa 100644 --- a/code/game/machinery/telecomms/machines/relay.dm +++ b/code/game/machinery/telecomms/machines/relay.dm @@ -10,6 +10,7 @@ name = "telecommunication relay" icon_state = "relay" desc = "A mighty piece of hardware used to send massive amounts of data far away." + telecomms_type = /obj/machinery/telecomms/relay density = TRUE use_power = IDLE_POWER_USE idle_power_usage = 30 @@ -30,8 +31,7 @@ else signal.levels |= relay_turf.z -// Checks to see if it can send/receive. - +/// Checks to see if it can send/receive. /obj/machinery/telecomms/relay/proc/can(datum/signal/signal) if(!on) return FALSE diff --git a/code/game/machinery/telecomms/machines/server.dm b/code/game/machinery/telecomms/machines/server.dm index 7fe38294c58..e38c8c6bb85 100644 --- a/code/game/machinery/telecomms/machines/server.dm +++ b/code/game/machinery/telecomms/machines/server.dm @@ -9,6 +9,7 @@ name = "telecommunication server" icon_state = "comm_server" desc = "A machine used to store data and network statistics." + telecomms_type = /obj/machinery/telecomms/server density = TRUE use_power = IDLE_POWER_USE idle_power_usage = 15 diff --git a/code/game/machinery/telecomms/telecomunications.dm b/code/game/machinery/telecomms/telecomunications.dm index 2dee715a23d..845b7392c62 100644 --- a/code/game/machinery/telecomms/telecomunications.dm +++ b/code/game/machinery/telecomms/telecomunications.dm @@ -17,28 +17,48 @@ GLOBAL_LIST_EMPTY(telecomms_list) /obj/machinery/telecomms 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/list/autolinkers = list() // list of text/number values to link with - var/id = "NULL" // identification string - var/network = "NULL" // the network of the machinery + /// 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" - var/list/freq_listening = list() // list of frequencies to tune into: if none, will listen to all + // 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) - var/hide = FALSE // Is it a hidden machine? + /// 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 ///Looping sounds for any servers var/datum/looping_sound/server/soundloop +/// 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, /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 @@ -47,24 +67,23 @@ GLOBAL_LIST_EMPTY(telecomms_list) signal.data["slow"] = netlag // Loop through all linked machines and send the signal or copy. - for(var/obj/machinery/telecomms/machine in links) - 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(z != filtered_machine.loc.z && !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) + filtered_machine.receive_information(signal, src) if(send_count > 0 && is_freq_listening(signal)) traffic++ @@ -75,12 +94,13 @@ GLOBAL_LIST_EMPTY(telecomms_list) // 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) . = ..() @@ -92,27 +112,28 @@ 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 QDEL_NULL(soundloop) 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) +/// Used in auto linking +/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((position.z != T_position.z) && !(long_range_link && T.long_range_link)) + return + 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() icon_state = "[initial(icon_state)][panel_open ? "_o" : null][on ? null : "_off"]" diff --git a/code/game/objects/items/devices/radio/headset.dm b/code/game/objects/items/devices/radio/headset.dm index 53856cccb3a..136b453ade3 100644 --- a/code/game/objects/items/devices/radio/headset.dm +++ b/code/game/objects/items/devices/radio/headset.dm @@ -25,8 +25,8 @@ GLOBAL_LIST_INIT(channel_tokens, list( canhear_range = 0 // can't hear headsets from very far away slot_flags = ITEM_SLOT_EARS - var/obj/item/encryptionkey/keyslot2 = null dog_fashion = null + var/obj/item/encryptionkey/keyslot2 = 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!")) @@ -55,26 +55,24 @@ GLOBAL_LIST_INIT(channel_tokens, list( /obj/item/radio/headset/Initialize(mapload) . = ..() + set_listening(TRUE) recalculateChannels() + possibly_deactivate_in_loc() + +/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/Destroy() QDEL_NULL(keyslot2) return ..() -/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/ui_data(mob/user) . = ..() .["headset"] = TRUE @@ -288,9 +286,6 @@ GLOBAL_LIST_INIT(channel_tokens, list( 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) @@ -335,11 +330,11 @@ GLOBAL_LIST_INIT(channel_tokens, list( /obj/item/radio/headset/recalculateChannels() - ..() + . = ..() if(keyslot2) for(var/ch_name in keyslot2.channels) if(!(ch_name in src.channels)) - channels[ch_name] = keyslot2.channels[ch_name] + LAZYSET(channels, ch_name, keyslot2.channels[ch_name]) if(keyslot2.translate_binary) translate_binary = TRUE @@ -348,8 +343,8 @@ GLOBAL_LIST_INIT(channel_tokens, list( if (keyslot2.independent) independent = TRUE - for(var/ch_name in channels) - secure_radio_connections[ch_name] = add_radio(src, GLOB.radiochannels[ch_name]) + 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()) diff --git a/code/game/objects/items/devices/radio/intercom.dm b/code/game/objects/items/devices/radio/intercom.dm index f08bbb8a2c4..3df550dc64d 100644 --- a/code/game/objects/items/devices/radio/intercom.dm +++ b/code/game/objects/items/devices/radio/intercom.dm @@ -80,24 +80,18 @@ /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.z in levels)) return FALSE - if(!listening) - return FALSE + if(freq == FREQ_SYNDICATE) 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 @@ -126,9 +120,9 @@ SIGNAL_HANDLER var/area/current_area = get_area(src) if(!current_area) - on = FALSE + set_on(FALSE) else - on = current_area.powered(AREA_USAGE_EQUIP) // set "on" to the equipment power status of our area. + set_on(current_area.powered(AREA_USAGE_EQUIP)) // set "on" to the equipment power status of our area. update_appearance() /obj/item/radio/intercom/add_blood_DNA(list/blood_dna) @@ -148,8 +142,11 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/item/radio/intercom, 26) /obj/item/radio/intercom/chapel name = "Confessional intercom" anonymize = TRUE - frequency = 1481 - broadcasting = TRUE + +/obj/item/radio/intercom/chapel/Initialize(mapload, ndir, building) + . = ..() + set_frequency(1481) + set_broadcasting(TRUE) MAPPING_DIRECTIONAL_HELPERS(/obj/item/radio/intercom/prison, 26) MAPPING_DIRECTIONAL_HELPERS(/obj/item/radio/intercom/chapel, 26) diff --git a/code/game/objects/items/devices/radio/radio.dm b/code/game/objects/items/devices/radio/radio.dm index 8a3410cd076..13304777d12 100644 --- a/code/game/objects/items/devices/radio/radio.dm +++ b/code/game/objects/items/devices/radio/radio.dm @@ -17,36 +17,85 @@ custom_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. + ///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 - 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. + /// 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 ///makes anyone who is talking through this anonymous. var/anonymize = FALSE - // Encryption key handling + /// Encryption key handling var/obj/item/encryptionkey/keyslot - 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 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 -/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/Initialize(mapload) + wires = new /datum/wires/radio(src) + if(prison_radio) + wires.cut(WIRE_TX) // 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_WIRES) + +/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) @@ -74,6 +123,7 @@ // Used for cyborg override /obj/item/radio/proc/resetChannels() channels = list() + secure_radio_connections = list() translate_binary = FALSE syndie = FALSE independent = FALSE @@ -81,33 +131,9 @@ /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) - 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 - . = ..() - 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]) - - become_hearing_sensitive(ROUNDSTART_TRAIT) - -/obj/item/radio/ComponentInitialize() - . = ..() - AddElement(/datum/element/empprotection, EMP_PROTECT_WIRES) - /obj/item/radio/interact(mob/user) if(unscrewed && !isAI(user)) wires.interact(user) @@ -115,6 +141,211 @@ else ..() +//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) + recalculateChannels() + add_radio(src, frequency) + 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(talking_movable.speech_span) + if(!language) + 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/talking_movable, message, channel, list/spans, datum/language/language, list/message_mods) + if(!on) + return // the device has to be on + if(!talking_movable || !message) + return + if(wires.is_cut(WIRE_TX)) // Permacell and otherwise tampered-with radios + return + if(!talking_movable.IsVocal()) + return + + if(use_command) + spans |= SPAN_COMMAND + + /* + Roughly speaking, radios attempt to make a subspace transmission (which + is received, processed, and rebroadcast by the telecomms satellite) and + if that fails, they send a mundane radio transmission. + + Headsets cannot send/receive mundane transmissions, only subspace. + Syndicate radios can hear transmissions on all well-known frequencies. + CentCom radios can hear the CentCom frequency no matter what. + */ + + // From the channel, determine the frequency and get a reference to it. + var/freq + if(channel && channels && channels.len > 0) + if(channel == MODE_DEPARTMENT) + channel = channels[1] + freq = secure_radio_connections[channel] + if (!channels[channel]) // if the channel is turned off, don't broadcast + return + else + freq = frequency + channel = null + + // Nearby active jammers prevent the message from transmitting + var/turf/position = get_turf(src) + 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)) + return + + // Determine the identity information which will be attached to the signal. + 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 || freq == FREQ_CTF_GREEN || freq == FREQ_CTF_YELLOW)) + signal.data["compression"] = 0 + signal.transmission_method = TRANSMISSION_SUPERSPACE + signal.levels = list(0) + signal.broadcast() + return + + // All radios make an attempt to use the subspace system first + signal.send_to_receivers() + + // If the radio is subspace-only, that's all it can do + if (subspace_transmission) + return + + // 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/backup_transmission, signal), 20) + +/obj/item/radio/proc/backup_transmission(datum/signal/subspace/vocal/signal) + 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 = 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 + + 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(input_frequency, list/levels) + // deny checks + if (levels != RADIO_NO_Z_LEVEL_RESTRICTION) + var/turf/position = get_turf(src) + if(!position || !(position.z in levels)) + return FALSE + + if (input_frequency == FREQ_SYNDICATE && !syndie) + return FALSE + + // allow checks: are we listening on that frequency? + if (input_frequency == frequency) + return TRUE + for(var/ch_name in channels) + if(channels[ch_name] & FREQ_LISTENING) + if(GLOB.radiochannels[ch_name] == text2num(input_frequency) || syndie) + return TRUE + return FALSE + /obj/item/radio/ui_state(mob/user) return GLOB.inventory_state @@ -165,10 +396,10 @@ if(.) set_frequency(sanitize_frequency(tune, freerange)) if("listen") - listening = !listening + set_listening(!listening) . = TRUE if("broadcast") - broadcasting = !broadcasting + set_broadcasting(!broadcasting) . = TRUE if("channel") var/channel = params["channel"] @@ -191,146 +422,9 @@ recalculateChannels() . = TRUE -/obj/item/radio/talk_into(atom/movable/M, message, channel, list/spans, datum/language/language, list/message_mods) - if(HAS_TRAIT(M, TRAIT_SIGN_LANG)) //Forces Sign Language users to wear the translation gloves to speak over radios - var/mob/living/carbon/mute = M - 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) - if(!language) - language = M.get_selected_language() - INVOKE_ASYNC(src, .proc/talk_into_impl, M, 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) - if(!on) - return // the device has to be on - if(!M || !message) - return - if(wires.is_cut(WIRE_TX)) // Permacell and otherwise tampered-with radios - return - if(!M.IsVocal()) - return - - if(use_command) - spans |= SPAN_COMMAND - - /* - Roughly speaking, radios attempt to make a subspace transmission (which - is received, processed, and rebroadcast by the telecomms satellite) and - if that fails, they send a mundane radio transmission. - - Headsets cannot send/receive mundane transmissions, only subspace. - Syndicate radios can hear transmissions on all well-known frequencies. - CentCom radios can hear the CentCom frequency no matter what. - */ - - // From the channel, determine the frequency and get a reference to it. - var/freq - if(channel && channels && channels.len > 0) - if(channel == MODE_DEPARTMENT) - channel = channels[1] - freq = secure_radio_connections[channel] - if (!channels[channel]) // if the channel is turned off, don't broadcast - return - else - freq = frequency - channel = null - - // Nearby active jammers prevent the message from transmitting - var/turf/position = get_turf(src) - for(var/obj/item/jammer/jammer in GLOB.active_jammers) - var/turf/jammer_turf = get_turf(jammer) - 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) - - // 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 || 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.broadcast() - return - - // All radios make an attempt to use the subspace system first - signal.send_to_receivers() - - // If the radio is subspace-only, that's all it can do - if (subspace_transmission) - return - - // 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/backup_transmission, signal), 20) - -/obj/item/radio/proc/backup_transmission(datum/signal/subspace/vocal/signal) - 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 = 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 - - 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) - // 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(src) - if(!position || !(position.z in level)) - return FALSE - - // allow checks: are we listening on that frequency? - if (freq == 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) - return TRUE - return FALSE - +/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/examine(mob/user) . = ..() @@ -360,18 +454,26 @@ 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 + set_on(FALSE) addtimer(CALLBACK(src, .proc/end_emp_effect, curremp), 200) +/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 /////////////////////////////// @@ -391,10 +493,10 @@ var/mob/living/silicon/robot/R = loc if(istype(R)) for(var/ch_name in R.model.radio_channels) - channels[ch_name] = 1 + channels[ch_name] = TRUE /obj/item/radio/borg/syndicate - syndie = 1 + syndie = TRUE keyslot = new /obj/item/encryptionkey/syndicate /obj/item/radio/borg/syndicate/Initialize(mapload) @@ -436,5 +538,8 @@ /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. dog_fashion = /datum/dog_fashion/back + +/obj/item/radio/off/Initialize() + . = ..() + set_listening(FALSE) diff --git a/code/game/say.dm b/code/game/say.dm index fc994a2a8b0..6e7b1cc0392 100644 --- a/code/game/say.dm +++ b/code/game/say.dm @@ -39,10 +39,13 @@ GLOBAL_LIST_INIT(freqtospan, list( //SHOULD_BE_PURE(TRUE) return TRUE -/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/atom/movable/AM as anything in get_hearers_in_view(range, source)) - 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)) + 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 + hearing_movable.Hear(rendered, src, message_language, 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. @@ -170,7 +173,7 @@ GLOBAL_LIST_INIT(freqtospan, list( return "[src]" //Returns the atom's name, prepended with 'The' if it's not a proper noun /atom/movable/proc/IsVocal() - return 1 + return TRUE /atom/movable/proc/get_alt_name() diff --git a/code/game/sound.dm b/code/game/sound.dm index 428396e9c0b..14429a8cf06 100644 --- a/code/game/sound.dm +++ b/code/game/sound.dm @@ -78,14 +78,12 @@ falloff_distance - Distance at which falloff begins. Sound is at peak volume (in if(below_turf && istransparentturf(turf_source)) listeners += SSmobs.clients_by_zlevel[below_turf.z] - for(var/P in listeners) - var/mob/M = P - if(get_dist(M, turf_source) <= maxdistance) - M.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb) - for(var/P in SSmobs.dead_players_by_zlevel[source_z]) - var/mob/M = P - if(get_dist(M, turf_source) <= maxdistance) - M.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb) + for(var/mob/listening_mob as anything in listeners) + if(get_dist(listening_mob, turf_source) <= maxdistance) + listening_mob.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb) + for(var/mob/listening_mob as anything in SSmobs.dead_players_by_zlevel[source_z]) + if(get_dist(listening_mob, turf_source) <= maxdistance) + listening_mob.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb) /*! playsound diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index 4c7d3fc16ed..f0bb5ddc6ef 100755 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -377,7 +377,6 @@ GLOBAL_LIST_EMPTY(station_turfs) return (mover.movement_type & PHASING) return TRUE - /turf/open/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs) . = ..() //melting diff --git a/code/modules/antagonists/abductor/equipment/abduction_gear.dm b/code/modules/antagonists/abductor/equipment/abduction_gear.dm index 1fc6c79f0dd..2fab327af31 100644 --- a/code/modules/antagonists/abductor/equipment/abduction_gear.dm +++ b/code/modules/antagonists/abductor/equipment/abduction_gear.dm @@ -280,12 +280,10 @@ /obj/item/abductor/silencer/proc/radio_off_mob(mob/living/carbon/human/M) var/list/all_items = M.get_all_contents() - for(var/obj/I in all_items) - if(istype(I, /obj/item/radio/)) - var/obj/item/radio/r = I - r.listening = 0 - if(!istype(I, /obj/item/radio/headset)) - r.broadcasting = 0 //goddamned headset hacks + for(var/obj/item/radio/radio in all_items) + radio.set_listening(FALSE) + if(!istype(radio, /obj/item/radio/headset)) + radio.set_broadcasting(FALSE) //goddamned headset hacks /obj/item/abductor/mind_device name = "mental interface device" diff --git a/code/modules/atmospherics/machinery/components/fusion/hfr_core.dm b/code/modules/atmospherics/machinery/components/fusion/hfr_core.dm index 2412e05baa8..cde55b10a35 100644 --- a/code/modules/atmospherics/machinery/components/fusion/hfr_core.dm +++ b/code/modules/atmospherics/machinery/components/fusion/hfr_core.dm @@ -168,7 +168,7 @@ radio = new(src) radio.keyslot = new radio_key - radio.listening = 0 + radio.set_listening(FALSE) radio.recalculateChannels() investigate_log("has been created.", INVESTIGATE_HYPERTORUS) diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index 00253cdedaa..4953f50a6c4 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -164,6 +164,7 @@ var/list/parallax_layers var/list/parallax_layers_cached + ///this is the last recorded client eye by SSparallax/fire() var/atom/movable/movingmob var/turf/previous_turf ///world.time of when we can state animate()ing parallax again diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 50e0cdc72d0..a7721cdebd5 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -532,10 +532,11 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( send2adminchat("Server", "[cheesy_message] (No admins online)") QDEL_LIST_ASSOC_VAL(char_render_holders) + if(movingmob != null) - movingmob.client_mobs_in_contents -= mob - UNSETEMPTY(movingmob.client_mobs_in_contents) + LAZYREMOVE(movingmob.client_mobs_in_contents, mob) movingmob = null + active_mousedown_item = null SSambience.remove_ambience_client(src) QDEL_NULL(view_size) diff --git a/code/modules/holodeck/area_copy.dm b/code/modules/holodeck/area_copy.dm index 5eb040b760c..054f5d59abc 100644 --- a/code/modules/holodeck/area_copy.dm +++ b/code/modules/holodeck/area_copy.dm @@ -2,7 +2,7 @@ GLOBAL_LIST_INIT(duplicate_forbidden_vars,list( "tag", "datum_components", "area", "type", "loc", "locs", "vars", "parent", "parent_type", "verbs", "ckey", "key", "power_supply", "contents", "reagents", "stat", "x", "y", "z", "group", "atmos_adjacent_turfs", "comp_lookup", - "client_mobs_in_contents", "bodyparts", "internal_organs", "hand_bodyparts", "overlays_standing", "hud_list", + "important_recursive_contents", "bodyparts", "internal_organs", "hand_bodyparts", "overlays_standing", "hud_list", "actions", "AIStatus", "appearance", "managed_overlays", "managed_vis_overlays", "computer_id", "lastKnownIP", "implants", "tgui_shared_states" )) diff --git a/code/modules/jobs/job_types/security_officer.dm b/code/modules/jobs/job_types/security_officer.dm index 1e3f055f475..5d1d383c8fc 100644 --- a/code/modules/jobs/job_types/security_officer.dm +++ b/code/modules/jobs/job_types/security_officer.dm @@ -228,7 +228,7 @@ GLOBAL_LIST_EMPTY(security_officer_distribution) /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/mapping/reader.dm b/code/modules/mapping/reader.dm index 8c21f9256a7..69a53fae535 100644 --- a/code/modules/mapping/reader.dm +++ b/code/modules/mapping/reader.dm @@ -148,12 +148,16 @@ var/list/bounds src.bounds = bounds = list(1.#INF, 1.#INF, 1.#INF, -1.#INF, -1.#INF, -1.#INF) - for(var/I in gridSets) - var/datum/grid_set/gset = I + //used for sending the maxx and maxy expanded global signals at the end of this proc + var/has_expanded_world_maxx = FALSE + var/has_expanded_world_maxy = FALSE + + for(var/datum/grid_set/gset as anything in gridSets) var/ycrd = gset.ycrd + y_offset - 1 var/zcrd = gset.zcrd + z_offset - 1 if(!cropMap && ycrd > world.maxy) world.maxy = ycrd // Expand Y here. X is expanded in the loop below + has_expanded_world_maxy = TRUE var/zexpansion = zcrd > world.maxz if(zexpansion) if(cropMap) @@ -179,6 +183,7 @@ break else world.maxx = xcrd + has_expanded_world_maxx = TRUE if(xcrd >= 1) var/model_key = copytext(line, tpos, tpos + key_len) @@ -207,11 +212,13 @@ CHECK_TICK if(!no_changeturf) - for(var/t in block(locate(bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ]), locate(bounds[MAP_MAXX], bounds[MAP_MAXY], bounds[MAP_MAXZ]))) - var/turf/T = t + for(var/turf/T as anything in block(locate(bounds[MAP_MINX], bounds[MAP_MINY], bounds[MAP_MINZ]), locate(bounds[MAP_MAXX], bounds[MAP_MAXY], bounds[MAP_MAXZ]))) //we do this after we load everything in. if we don't; we'll have weird atmos bugs regarding atmos adjacent turfs T.AfterChange(CHANGETURF_IGNORE_AIR) + if(has_expanded_world_maxx || has_expanded_world_maxy) + SEND_GLOBAL_SIGNAL(COMSIG_GLOB_EXPANDED_WORLD_BOUNDS, has_expanded_world_maxx, has_expanded_world_maxy) + #ifdef TESTING if(turfsSkipped) testing("Skipped loading [turfsSkipped] default turfs") diff --git a/code/modules/mapping/space_management/zlevel_manager.dm b/code/modules/mapping/space_management/zlevel_manager.dm index 35235579a44..9d6d0a37709 100644 --- a/code/modules/mapping/space_management/zlevel_manager.dm +++ b/code/modules/mapping/space_management/zlevel_manager.dm @@ -19,7 +19,6 @@ /datum/controller/subsystem/mapping/proc/add_new_zlevel(name, traits = list(), z_type = /datum/space_level) UNTIL(!adding_new_zlevel) adding_new_zlevel = TRUE - SEND_GLOBAL_SIGNAL(COMSIG_GLOB_NEW_Z, args) var/new_z = z_list.len + 1 if (world.maxz < new_z) world.incrementMaxZ() @@ -28,6 +27,7 @@ var/datum/space_level/S = new z_type(new_z, name, traits) z_list += S adding_new_zlevel = FALSE + SEND_GLOBAL_SIGNAL(COMSIG_GLOB_NEW_Z, S) return S /datum/controller/subsystem/mapping/proc/get_level(z) diff --git a/code/modules/mining/laborcamp/laborstacker.dm b/code/modules/mining/laborcamp/laborstacker.dm index 79b1c200aa4..31c1e4869f4 100644 --- a/code/modules/mining/laborcamp/laborstacker.dm +++ b/code/modules/mining/laborcamp/laborstacker.dm @@ -18,7 +18,7 @@ GLOBAL_LIST(labor_sheet_values) /obj/machinery/mineral/labor_claim_console/Initialize(mapload) . = ..() Radio = new /obj/item/radio(src) - Radio.listening = FALSE + Radio.set_listening(FALSE) locate_stacking_machine() //If we can't find a stacking machine end it all ok? if(!stacking_machine) diff --git a/code/modules/mob/living/brain/MMI.dm b/code/modules/mob/living/brain/MMI.dm index 9d8ccd46269..e65541efb58 100644 --- a/code/modules/mob/living/brain/MMI.dm +++ b/code/modules/mob/living/brain/MMI.dm @@ -18,7 +18,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/Destroy() @@ -104,8 +104,8 @@ /obj/item/mmi/attack_self(mob/user) if(!brain) - 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, span_notice("You toggle [src]'s radio system [radio.is_on() == TRUE ? "on" : "off"].")) else eject_brain(user) update_appearance() @@ -204,12 +204,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, span_notice("Radio is [radio.get_listening() ? "now" : "no longer"] receiving broadcast.")) /obj/item/mmi/emp_act(severity) . = ..() @@ -235,7 +235,7 @@ /obj/item/mmi/examine(mob/user) . = ..() if(radio) - . += span_notice("There is a switch to toggle the radio system [radio.on ? "off" : "on"].[brain ? " It is currently being covered by [brain]." : null]") + . += span_notice("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) @@ -284,4 +284,4 @@ /obj/item/mmi/syndie/Initialize(mapload) . = ..() laws = new /datum/ai_laws/syndicate_override() - radio.on = FALSE + radio.set_on(FALSE) 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 fed7c57c205..2c24525ea88 100644 --- a/code/modules/mob/living/carbon/human/species_types/dullahan.dm +++ b/code/modules/mob/living/carbon/human/species_types/dullahan.dm @@ -30,7 +30,7 @@ /datum/species/dullahan/on_species_gain(mob/living/carbon/human/H, datum/species/old_species) . = ..() - REMOVE_TRAIT(src, TRAIT_HEARING_SENSITIVE, TRAIT_GENERIC) + H.lose_hearing_sensitivity(TRAIT_GENERIC) var/obj/item/bodypart/head/head = H.get_bodypart(BODY_ZONE_HEAD) if(head) head.drop_limb() @@ -129,11 +129,13 @@ owner = new_owner START_PROCESSING(SSobj, src) RegisterSignal(owner, COMSIG_CLICK_SHIFT, .proc/examinate_check) - RegisterSignal(src, COMSIG_ATOM_HEARER_IN_VIEW, .proc/include_owner) RegisterSignal(owner, COMSIG_LIVING_REGENERATE_LIMBS, .proc/unlist_head) RegisterSignal(owner, COMSIG_LIVING_REVIVE, .proc/retrieve_head) become_hearing_sensitive(ROUNDSTART_TRAIT) +/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/process() if(!istype(loc, /obj/item/bodypart/head) || QDELETED(owner)) . = PROCESS_KILL diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm index 65e3ad114f8..6d0be610386 100644 --- a/code/modules/mob/living/living_say.dm +++ b/code/modules/mob/living/living_say.dm @@ -240,7 +240,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( // radios don't pick up whispers very well radio_message = stars(radio_message) spans |= SPAN_ITALICS - var/radio_return = radio(radio_message, message_mods, spans, language) + var/radio_return = radio(radio_message, message_mods, spans, language)//roughly 27% of living/say()'s total cost if(radio_return & ITALICS) spans |= SPAN_ITALICS if(radio_return & REDUCE_RANGE) @@ -248,7 +248,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( if(!message_mods[WHISPER_MODE]) message_mods[WHISPER_MODE] = MODE_WHISPER if(radio_return & NOPASS) - return 1 + return TRUE //No screams in space, unless you're next to someone. var/turf/T = get_turf(src) @@ -260,13 +260,13 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( if(pressure < ONE_ATMOSPHERE*0.4) //Thin air, let's italicise the message spans |= SPAN_ITALICS - send_speech(message, message_range, src, bubble_type, spans, language, message_mods) + send_speech(message, message_range, src, bubble_type, spans, language, message_mods)//roughly 58% of living/say()'s total cost if(succumbed) succumb(1) to_chat(src, compose_message(src, language, message, , spans, message_mods)) - return 1 + return TRUE /mob/living/Hear(message, atom/movable/speaker, datum/language/message_language, raw_message, radio_freq, list/spans, list/message_mods = list()) SEND_SIGNAL(src, COMSIG_MOVABLE_HEAR, args) @@ -315,7 +315,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( deaf_type = 2 // Since you should be able to hear yourself without looking // Create map text prior to modifying message for goonchat - if (client?.prefs.read_preference(/datum/preference/toggle/enable_runechat) && !(stat == UNCONSCIOUS || stat == HARD_CRIT) && (client.prefs.read_preference(/datum/preference/toggle/enable_runechat_non_mobs) || ismob(speaker)) && can_hear()) + if (client?.prefs.read_preference(/datum/preference/toggle/enable_runechat) && !(stat == UNCONSCIOUS || stat == HARD_CRIT) && (ismob(speaker) || client.prefs.read_preference(/datum/preference/toggle/enable_runechat_non_mobs)) && can_hear()) if (message_mods[MODE_CUSTOM_SAY_ERASE_INPUT]) create_chat_message(speaker, null, message_mods[MODE_CUSTOM_SAY_EMOTE], spans, EMOTE_MESSAGE) else @@ -357,7 +357,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( continue //Remove if underlying cause (likely byond issue) is fixed. See TG PR #49004. if(player_mob.stat != DEAD) //not dead, not important continue - if(get_dist(player_mob, src) > 7 || player_mob.z != z) //they're out of range of normal hearing + if(player_mob.z != z || get_dist(player_mob, src) > 7) //they're out of range of normal hearing if(eavesdrop_range) if(!(player_mob.client?.prefs.chat_toggles & CHAT_GHOSTWHISPER)) //they're whispering and we have hearing whispers at any range off continue @@ -374,6 +374,9 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( var/rendered = compose_message(src, message_language, message, , spans, message_mods) for(var/atom/movable/listening_movable as anything in listening) + if(!listening_movable) + stack_trace("somehow theres a null returned from get_hearers_in_view() in send_speech!") + continue if(eavesdrop_range && get_dist(source, listening_movable) > message_range && !(the_dead[listening_movable])) listening_movable.Hear(eavesrendered, src, message_language, eavesdropping, , spans, message_mods) else @@ -444,7 +447,7 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list( /mob/living/proc/radio(message, list/message_mods = list(), list/spans, language) var/obj/item/implant/radio/imp = locate() in src - if(imp?.radio.on) + if(imp?.radio.is_on()) if(message_mods[MODE_HEADSET]) imp.radio.talk_into(src, message, , spans, language, message_mods) return ITALICS | REDUCE_RANGE diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm index 3313ffdac56..bcb644d3b89 100644 --- a/code/modules/mob/living/silicon/ai/ai.dm +++ b/code/modules/mob/living/silicon/ai/ai.dm @@ -849,17 +849,17 @@ modules_action = new(malf_picker) modules_action.Grant(src) -/mob/living/silicon/ai/reset_perspective(atom/A) +/mob/living/silicon/ai/reset_perspective(atom/new_eye) if(camera_light_on) light_cameras() - if(istype(A, /obj/machinery/camera)) - current = A + if(istype(new_eye, /obj/machinery/camera)) + current = new_eye if(client) - if(ismovable(A)) - if(A != GLOB.ai_camera_room_landmark) + if(ismovable(new_eye)) + if(new_eye != GLOB.ai_camera_room_landmark) end_multicam() client.perspective = EYE_PERSPECTIVE - client.eye = A + client.eye = new_eye else end_multicam() if(isturf(loc)) diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm index 42c7f223245..6446d84ac40 100644 --- a/code/modules/mob/living/silicon/silicon.dm +++ b/code/modules/mob/living/silicon/silicon.dm @@ -318,7 +318,7 @@ return if(Autochan == "Default") //Autospeak on whatever frequency to which the radio is set, usually Common. radiomod = ";" - Autochan += " ([radio.frequency])" + Autochan += " ([radio.get_frequency()])" else if(Autochan == "None") //Prevents use of the radio for automatic annoucements. radiomod = "" else //For department channels, if any, given by the internal radio. diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm index c0bffa95335..5a941f06e49 100644 --- a/code/modules/mob/login.dm +++ b/code/modules/mob/login.dm @@ -62,7 +62,8 @@ 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_client_in_contents() + clear_important_client_contents(client) + enable_client_mobs_in_contents(client) SEND_SIGNAL(src, COMSIG_MOB_LOGIN) diff --git a/code/modules/mob/logout.dm b/code/modules/mob/logout.dm index ae087188081..25f202e8a14 100644 --- a/code/modules/mob/logout.dm +++ b/code/modules/mob/logout.dm @@ -15,4 +15,6 @@ var/datum/callback/CB = foo CB.Invoke() + clear_important_client_contents(client) + return TRUE diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 9b151234572..638ab8c18aa 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -36,8 +36,7 @@ for (var/alert in alerts) clear_alert(alert, TRUE) if(observers?.len) - for(var/M in observers) - var/mob/dead/observe = M + for(var/mob/dead/observe as anything in observers) observe.reset_perspective(null) qdel(hud_used) QDEL_LIST(client_colours) @@ -404,40 +403,43 @@ /** * Reset the attached clients perspective (viewpoint) * - * reset_perspective() set eye to common default : mob on turf, loc otherwise + * reset_perspective(null) set eye to common default : mob on turf, loc otherwise * reset_perspective(thing) set the eye to the thing (if it's equal to current default reset to mob perspective) */ -/mob/proc/reset_perspective(atom/A) - if(client) - if(A) - if(ismovable(A)) - //Set the the thing unless it's us - if(A != src) - client.perspective = EYE_PERSPECTIVE - client.eye = A - else - client.eye = client.mob - client.perspective = MOB_PERSPECTIVE - else if(isturf(A)) - //Set to the turf unless it's our current turf - if(A != loc) - client.perspective = EYE_PERSPECTIVE - client.eye = A - else - client.eye = client.mob - client.perspective = MOB_PERSPECTIVE +/mob/proc/reset_perspective(atom/new_eye) + SEND_SIGNAL(src, COMSIG_MOB_RESET_PERSPECTIVE) + if(!client) + return + + if(new_eye) + if(ismovable(new_eye)) + //Set the new eye unless it's us + if(new_eye != src) + client.perspective = EYE_PERSPECTIVE + client.eye = new_eye else - //Do nothing - else - //Reset to common defaults: mob if on turf, otherwise current loc - if(isturf(loc)) client.eye = client.mob client.perspective = MOB_PERSPECTIVE - else + + else if(isturf(new_eye)) + //Set to the turf unless it's our current turf + if(new_eye != loc) client.perspective = EYE_PERSPECTIVE - client.eye = loc - return 1 - SEND_SIGNAL(src, COMSIG_MOB_RESET_PERSPECTIVE) + client.eye = new_eye + else + client.eye = client.mob + client.perspective = MOB_PERSPECTIVE + else + return TRUE //no setting eye to stupid things like areas or whatever + else + //Reset to common defaults: mob if on turf, otherwise current loc + if(isturf(loc)) + client.eye = client.mob + client.perspective = MOB_PERSPECTIVE + else + client.perspective = EYE_PERSPECTIVE + client.eye = loc + return TRUE /** * Examine a mob @@ -1314,14 +1316,12 @@ SIGNAL_HANDLER set_active_storage(null) -///Clears the client in contents list of our current "eye". Prevents hard deletes +///clears the client mob in our client_mobs_in_contents list /mob/proc/clear_client_in_contents() - if(client?.movingmob) //In the case the client was transferred to another mob and not deleted. - client.movingmob.client_mobs_in_contents -= src - UNSETEMPTY(client.movingmob.client_mobs_in_contents) + if(client?.movingmob) + LAZYREMOVE(client.movingmob.client_mobs_in_contents, src) client.movingmob = null - ///Shows a tgui window with memories /mob/verb/memory() set name = "Memories" diff --git a/code/modules/power/supermatter/supermatter.dm b/code/modules/power/supermatter/supermatter.dm index 1f04545cb7d..38fb78397f4 100644 --- a/code/modules/power/supermatter/supermatter.dm +++ b/code/modules/power/supermatter/supermatter.dm @@ -255,7 +255,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) SSpoints_of_interest.make_point_of_interest(src) radio = new(src) radio.keyslot = new radio_key - radio.listening = 0 + radio.set_listening(FALSE) radio.recalculateChannels() investigate_log("has been created.", INVESTIGATE_SUPERMATTER) if(is_main_engine) diff --git a/code/modules/shuttle/shuttle.dm b/code/modules/shuttle/shuttle.dm index c35065d0996..73d9cf3a874 100644 --- a/code/modules/shuttle/shuttle.dm +++ b/code/modules/shuttle/shuttle.dm @@ -718,7 +718,7 @@ if(!T || !istype(T.loc, area_type)) continue for (var/atom/movable/movable as anything in T) - if (length(movable.client_mobs_in_contents)) + if (movable.client_mobs_in_contents) movable.update_parallax_contents() /obj/docking_port/mobile/proc/check_transit_zone() diff --git a/code/modules/vehicles/mecha/mecha_topic.dm b/code/modules/vehicles/mecha/mecha_topic.dm index 565a7aceeca..1038c685cf3 100644 --- a/code/modules/vehicles/mecha/mecha_topic.dm +++ b/code/modules/vehicles/mecha/mecha_topic.dm @@ -128,14 +128,14 @@ Radio settings:
Microphone: [radio? "\ - [radio.broadcasting?"Engaged":"Disengaged"]":"Error"]
+ [radio.get_broadcasting()?"Engaged":"Disengaged"]":"Error"]
Speaker: [radio? "\ - [radio.listening?"Engaged":"Disengaged"]":"Error"]
+ [radio.get_listening()?"Engaged":"Disengaged"]":"Error"]
Frequency: [radio? "-":"-"] [radio? "-":"-"] - [radio?"[format_frequency(radio.frequency)]":"Error"] + [radio?"[format_frequency(radio.get_frequency())]":"Error"] [radio? "+":"+"] [radio? "+":"+"]
@@ -336,21 +336,21 @@ //Toggles radio broadcasting if(href_list["rmictoggle"]) - radio.broadcasting = !radio.broadcasting - send_byjax(usr,"exosuit.browser","rmicstate",(radio.broadcasting?"Engaged":"Disengaged")) + radio.set_broadcasting(!radio.get_broadcasting()) + send_byjax(usr,"exosuit.browser","rmicstate",(radio.get_broadcasting()?"Engaged":"Disengaged")) return //Toggles radio listening if(href_list["rspktoggle"]) - radio.listening = !radio.listening - send_byjax(usr,"exosuit.browser","rspkstate",(radio.listening?"Engaged":"Disengaged")) + radio.set_listening(!radio.get_listening()) + send_byjax(usr,"exosuit.browser","rspkstate",(radio.get_listening()?"Engaged":"Disengaged")) return //Changes radio freqency. if(href_list["rfreq"]) - var/new_frequency = radio.frequency + text2num(href_list["rfreq"]) + var/new_frequency = radio.get_frequency() + text2num(href_list["rfreq"]) radio.set_frequency(sanitize_frequency(new_frequency, radio.freerange)) - send_byjax(usr,"exosuit.browser","rfreq","[format_frequency(radio.frequency)]") + send_byjax(usr,"exosuit.browser","rfreq","[format_frequency(radio.get_frequency())]") return //Changes the exosuit name. diff --git a/code/modules/vending/_vending.dm b/code/modules/vending/_vending.dm index 6a216b5f347..4a16cb629d5 100644 --- a/code/modules/vending/_vending.dm +++ b/code/modules/vending/_vending.dm @@ -211,7 +211,7 @@ else if(circuit && (circuit.onstation != onstation)) //check if they're not the same to minimize the amount of edited values. 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. Radio = new /obj/item/radio(src) - Radio.listening = 0 + Radio.set_listening(FALSE) /obj/machinery/vending/Destroy() QDEL_NULL(wires) diff --git a/tgstation.dme b/tgstation.dme index a9a244742a3..6ff390ad30b 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -143,6 +143,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\stat.dm" #include "code\__DEFINES\stat_tracking.dm" #include "code\__DEFINES\station.dm" @@ -206,6 +207,7 @@ #include "code\__DEFINES\dcs\signals\signals_reagent.dm" #include "code\__DEFINES\dcs\signals\signals_restaurant.dm" #include "code\__DEFINES\dcs\signals\signals_scangate.dm" +#include "code\__DEFINES\dcs\signals\signals_spatial_grid.dm" #include "code\__DEFINES\dcs\signals\signals_specie.dm" #include "code\__DEFINES\dcs\signals\signals_storage.dm" #include "code\__DEFINES\dcs\signals\signals_subsystem.dm" @@ -282,6 +284,7 @@ #include "code\__HELPERS\roundend.dm" #include "code\__HELPERS\sanitize_values.dm" #include "code\__HELPERS\shell.dm" +#include "code\__HELPERS\spatial_info.dm" #include "code\__HELPERS\spawns.dm" #include "code\__HELPERS\stack_trace.dm" #include "code\__HELPERS\stat_tracking.dm" @@ -457,6 +460,7 @@ #include "code\controllers\subsystem\sound_loops.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"