From d005d76f0bd201060b6ee515678a4b6950d9f0eb Mon Sep 17 00:00:00 2001 From: Kylerace Date: Thu, 16 Dec 2021 19:49:27 -0800 Subject: [PATCH] Fixes Massive Radio Overtime, Implements a Spatial Grid System for Faster Searching Over Areas (#61422) a month or two ago i realized that on master the reason why get_hearers_in_view() overtimes so much (ie one of our highest overtiming procs at highpop) is because when you transmit a radio signal over the common channel, it can take ~20 MILLISECONDS, which isnt good when 1. player verbs and commands usually execute after SendMaps processes for that tick, meaning they can execute AFTER the tick was supposed to start if master is overloaded and theres a lot of maptick 2. each of our server ticks are only 50 ms, so i started on optimizing this. the main optimization was SSspatial_grid which allows searching through 15x15 spatial_grid_cell datums (one set for each z level) far faster than iterating over movables in view() to look for what you want. now all hearing sensitive movables in the 5x5 areas associated with each spatial_grid_cell datum are stored in the datum (so are client mobs). when you search for one of the stored "types" (hearable or client mob) in a radius around a center, it just needs to iterate over the cell datums in range add the content type you want from the datums to a list subtract contents that arent in range, then contents not in line of sight return the list from benchmarks, this makes short range searches like what is used with radio code (it goes over every radio connected to a radio channel that can hear the signal then calls get_hearers_in_view() to search in the radios canhear_range which is at most 3) about 3-10 times faster depending on workload. the line of sight algorithm scales well with range but not very well if it has to check LOS to > 100 objects, which seems incredibly rare for this workload, the largest range any radio in the game searches through is only 3 tiles the second optimization is to enforce complex setter vars for radios that removes them from the global radio list if they couldnt actually receive any radio transmissions from a given frequency in the first place. the third optimization i did was massively reduce the number of hearables on the station by making hologram projectors not hear if dont have an active call/anything that would make them need hearing. so one of hte most common non player hearables that require view iteration to find is crossed out. also implements a variation of an idea oranges had on how to speed up get_hearers_in_view() now that ive realized that view() cant be replicated by a raycasting algorithm. it distributes pregenerated abstract /mob/oranges_ear instances to all hearables in range such that theres at max one per turf and then iterates through only those mobs to take advantage of type-specific view() optimizations and just adds up the references in each one to create the list of hearing atoms, then puts the oranges_ear mobs back into nullspace. this is about 2x as fast as the get_hearers_in_view() on master holy FUCK its fast. like really fucking fast. the only costly part of the radio transmission pipeline i dont touch is mob/living/Hear() which takes ~100 microseconds on live but searching through every radio in the world with get_hearers_in_radio_ranges() -> get_hearers_in_view() is much faster, as well as the filtering radios step the spatial grid searching proc is about 36 microseconds/call at 10 range and 16 microseconds at 3 range in the captains office (relatively many hearables in view), the new get_hearers_in_view() was 4.16 times faster than get_hearers_in_view_old() at 10 range and 4.59 times faster at 3 range SSspatial_grid could be used for a lot more things other than just radio and say code, i just didnt implement it. for example since the cells are datums you could get all cells in a radius then register for new objects entering them then activate when a player enters your radius. this is something that would require either very expensive view() calls or iterating over every player in the global list and calling get_dist() on them which isnt that expensive but is still worse than it needs to be on normal get_hearers_in_view cost the new version that uses /mob/oranges_ear instances is about 2x faster than the old version, especially since the number of hearing sensitive movables has been brought down dramatically. with get_hearers_in_view_oranges_ear() being the benchmark proc that implements this system and get_hearers_in_view() being a slightly optimized version of the version we have on master, get_hearers_in_view_as() being a more optimized version of the one we have on master, and get_hearers_in_LOS() being the raycasting version currently only used for radios because it cant replicate view()'s behavior perfectly. --- .github/CODEOWNERS | 4 + code/__DEFINES/dcs/signals/signals_global.dm | 2 + .../dcs/signals/signals_spatial_grid.dm | 6 + .../__DEFINES/important_recursive_contents.dm | 3 + code/__DEFINES/maths.dm | 2 + code/__DEFINES/radio.dm | 3 + code/__DEFINES/spatial_gridmap.dm | 16 + code/__DEFINES/subsystems.dm | 1 + code/__DEFINES/traits.dm | 4 +- code/__HELPERS/atoms.dm | 1 + code/__HELPERS/game.dm | 221 ------- code/__HELPERS/maths.dm | 64 +- code/__HELPERS/spatial_info.dm | 380 +++++++++++ code/_onclick/hud/parallax.dm | 44 +- code/controllers/subsystem/parallax.dm | 4 + code/controllers/subsystem/spatial_gridmap.dm | 593 ++++++++++++++++++ code/datums/components/mood.dm | 3 +- code/datums/datum.dm | 13 +- code/datums/elements/beauty.dm | 8 +- code/datums/holocall.dm | 102 +-- code/datums/wires/_wires.dm | 2 +- code/datums/wires/radio.dm | 8 +- code/game/atoms_movable.dm | 108 +++- code/game/communications.dm | 1 + code/game/machinery/_machinery.dm | 2 +- code/game/machinery/bank_machine.dm | 1 + code/game/machinery/civilian_bounties.dm | 2 +- code/game/machinery/computer/arcade/orion.dm | 2 +- .../computer/chef_orders/chef_order.dm | 2 +- code/game/machinery/doors/brigdoors.dm | 2 +- code/game/machinery/hologram.dm | 96 ++- code/game/machinery/requests_console.dm | 2 +- code/game/machinery/telecomms/broadcasting.dm | 52 +- .../telecomms/machine_interactions.dm | 53 +- .../telecomms/machines/broadcaster.dm | 1 + code/game/machinery/telecomms/machines/bus.dm | 1 + code/game/machinery/telecomms/machines/hub.dm | 1 + .../telecomms/machines/message_server.dm | 1 + .../machinery/telecomms/machines/processor.dm | 1 + .../machinery/telecomms/machines/receiver.dm | 1 + .../machinery/telecomms/machines/relay.dm | 4 +- .../machinery/telecomms/machines/server.dm | 1 + .../machinery/telecomms/telecomunications.dm | 89 ++- .../objects/items/devices/radio/headset.dm | 39 +- .../objects/items/devices/radio/intercom.dm | 25 +- .../game/objects/items/devices/radio/radio.dm | 497 +++++++++------ code/game/say.dm | 11 +- code/game/sound.dm | 14 +- code/game/turfs/turf.dm | 1 - .../abductor/equipment/abduction_gear.dm | 10 +- .../machinery/components/fusion/hfr_core.dm | 2 +- code/modules/client/client_defines.dm | 1 + code/modules/client/client_procs.dm | 5 +- code/modules/holodeck/area_copy.dm | 2 +- .../jobs/job_types/security_officer.dm | 2 +- code/modules/mapping/reader.dm | 15 +- .../space_management/zlevel_manager.dm | 2 +- code/modules/mining/laborcamp/laborstacker.dm | 2 +- code/modules/mob/living/brain/MMI.dm | 16 +- .../carbon/human/species_types/dullahan.dm | 6 +- code/modules/mob/living/living_say.dm | 17 +- code/modules/mob/living/silicon/ai/ai.dm | 12 +- code/modules/mob/living/silicon/silicon.dm | 2 +- code/modules/mob/login.dm | 3 +- code/modules/mob/logout.dm | 2 + code/modules/mob/mob.dm | 70 +-- code/modules/power/supermatter/supermatter.dm | 2 +- code/modules/shuttle/shuttle.dm | 2 +- code/modules/vehicles/mecha/mecha_topic.dm | 18 +- code/modules/vending/_vending.dm | 2 +- tgstation.dme | 4 + 71 files changed, 1907 insertions(+), 784 deletions(-) create mode 100644 code/__DEFINES/dcs/signals/signals_spatial_grid.dm create mode 100644 code/__DEFINES/spatial_gridmap.dm create mode 100644 code/__HELPERS/spatial_info.dm create mode 100644 code/controllers/subsystem/spatial_gridmap.dm 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"