diff --git a/code/__DEFINES/_helpers.dm b/code/__DEFINES/_helpers.dm index 68ab30cace23..f8d4763dd978 100644 --- a/code/__DEFINES/_helpers.dm +++ b/code/__DEFINES/_helpers.dm @@ -1,3 +1,18 @@ +// Stuff that is relatively "core" and is used in other defines/helpers + +/** + * The game's world.icon_size. \ + * Ideally divisible by 16. \ + * Ideally a number, but it + * can be a string ("32x32"), so more exotic coders + * will be sad if you use this in math. + */ +#define ICON_SIZE_ALL 32 +/// The X/Width dimension of ICON_SIZE. This will more than likely be the bigger axis. +#define ICON_SIZE_X 32 +/// The Y/Height dimension of ICON_SIZE. This will more than likely be the smaller axis. +#define ICON_SIZE_Y 32 + /// Takes a datum as input, returns its ref string #define text_ref(datum) ref(datum) diff --git a/code/__DEFINES/dcs/signals/signals_global.dm b/code/__DEFINES/dcs/signals/signals_global.dm index a44b2b528cc3..044ee1547fe3 100644 --- a/code/__DEFINES/dcs/signals/signals_global.dm +++ b/code/__DEFINES/dcs/signals/signals_global.dm @@ -82,3 +82,4 @@ #define COMSIG_DARKSPAWN_ASCENSION "!darkspawn_ascension" /// Global signal sent when the backrooms finishes initailizing: (No arguments) #define COMSIG_BACKROOMS_INITIALIZE "!backrooms_initialize" + diff --git a/code/__DEFINES/dcs/signals/signals_spatial_grid.dm b/code/__DEFINES/dcs/signals/signals_spatial_grid.dm new file mode 100644 index 000000000000..82e69dfcdf8d --- /dev/null +++ b/code/__DEFINES/dcs/signals/signals_spatial_grid.dm @@ -0,0 +1,6 @@ +//spatial grid signals + +///Called from base of /datum/controller/subsystem/spatial_grid/proc/enter_cell: (/atom/movable) +#define SPATIAL_GRID_CELL_ENTERED(contents_type) "spatial_grid_cell_entered_[contents_type]" +///Called from base of /datum/controller/subsystem/spatial_grid/proc/exit_cell: (/atom/movable) +#define SPATIAL_GRID_CELL_EXITED(contents_type) "spatial_grid_cell_exited_[contents_type]" diff --git a/code/__DEFINES/flags.dm b/code/__DEFINES/flags.dm index a87ad394b632..feb33f5b954b 100644 --- a/code/__DEFINES/flags.dm +++ b/code/__DEFINES/flags.dm @@ -28,45 +28,43 @@ GLOBAL_LIST_INIT(bitflags, list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 204 /// item has priority to check when entering or leaving #define ON_BORDER_1 (1<<1) -/// This flag is what recursive_hear_check() uses to determine wether to add an item to the hearer list or not. -#define HEAR_1 (1<<2) /// Projectiels will check ricochet on things impacted that have this. -#define CHECK_RICOCHET_1 (1<<3) +#define CHECK_RICOCHET_1 (1<<2) ///specifies that this atom is a hologram that isnt real -#define HOLOGRAM_1 (1<<4) +#define HOLOGRAM_1 (1<<3) ///Whether /atom/Initialize(mapload) has already run for the object -#define INITIALIZED_1 (1<<5) +#define INITIALIZED_1 (1<<4) /// was this spawned by an admin? used for stat tracking stuff. -#define ADMIN_SPAWNED_1 (1<<6) +#define ADMIN_SPAWNED_1 (1<<5) /// For machines and structures that should not break into parts, eg, holodeck stuff -#define NODECONSTRUCT_1 (1<<7) +#define NODECONSTRUCT_1 (1<<6) /// Prevent clicking things below it on the same turf eg. doors/ fulltile windows -#define PREVENT_CLICK_UNDER_1 (1<<9) +#define PREVENT_CLICK_UNDER_1 (1<<7) /// Can players recolor this in-game via vendors (and maybe more if support is added)? -#define IS_PLAYER_COLORABLE_1 (1<<10) +#define IS_PLAYER_COLORABLE_1 (1<<8) /// TESLA_IGNORE grants immunity from being targeted by tesla-style electricity -#define TESLA_IGNORE_1 (1<<11) +#define TESLA_IGNORE_1 (1<<9) /// If a turf can be made dirty at roundstart. This is also used in areas. -#define CAN_BE_DIRTY_1 (1<<12) +#define CAN_BE_DIRTY_1 (1<<10) /// Should we use the initial icon for display? Mostly used by overlay only objects -#define HTML_USE_INITAL_ICON_1 (1<<13) +#define HTML_USE_INITAL_ICON_1 (1<<11) /// conducts electricity (metal etc.) -#define CONDUCT_1 (1<<14) +#define CONDUCT_1 (1<<12) /// should not get harmed if this gets caught by an explosion? -#define PREVENT_CONTENTS_EXPLOSION_1 (1<<15) +#define PREVENT_CONTENTS_EXPLOSION_1 (1<<13) /// should the contents of this atom be acted upon -#define RAD_PROTECT_CONTENTS_1 (1<<16) +#define RAD_PROTECT_CONTENTS_1 (1<<14) /// should this object be allowed to be contaminated -#define RAD_NO_CONTAMINATE_1 (1<<17) +#define RAD_NO_CONTAMINATE_1 (1<<15) /// Prevents most radiation on this turf from leaving it -#define RAD_CONTAIN_CONTENTS (1<<18) +#define RAD_CONTAIN_CONTENTS (1<<16) /// Is the thing currently spinning? -#define IS_SPINNING_1 (1<<19) +#define IS_SPINNING_1 (1<<17) /// If this atom has experienced a decal element "init finished" sourced appearance update /// We use this to ensure stacked decals don't double up appearance updates for no rasin /// Flag as an optimization, don't make this a trait without profiling /// Yes I know this is a stupid flag, no you can't take him from me -#define DECAL_INIT_UPDATE_EXPERIENCED_1 (1<<20) +#define DECAL_INIT_UPDATE_EXPERIENCED_1 (1<<18) //TURF FLAGS /// If a turf cant be jaunted through. diff --git a/code/__DEFINES/important_recursive_contents.dm b/code/__DEFINES/important_recursive_contents.dm new file mode 100644 index 000000000000..79abb67d1836 --- /dev/null +++ b/code/__DEFINES/important_recursive_contents.dm @@ -0,0 +1,9 @@ +///the area channel of the important_recursive_contents list, everything in here will be sent a signal when their last holding object changes areas +#define RECURSIVE_CONTENTS_AREA_SENSITIVE "recursive_contents_area_sensitive" +///the hearing channel of the important_recursive_contents list, everything in here will count as a hearing atom +#define RECURSIVE_CONTENTS_HEARING_SENSITIVE "recursive_contents_hearing_sensitive" +///the client mobs channel of the important_recursive_contents list, everything in here will be a mob with an attached client +///this is given to both a clients mob, and a clients eye, both point to the clients mob +#define RECURSIVE_CONTENTS_CLIENT_MOBS "recursive_contents_client_mobs" +///the parent of storage components currently shown to some client mob get this. gets removed when nothing is viewing the parent +#define RECURSIVE_CONTENTS_ACTIVE_STORAGE "recursive_contents_active_storage" diff --git a/code/__DEFINES/radio.dm b/code/__DEFINES/radio.dm index d3190b8fa20f..efa9dedaa5fe 100644 --- a/code/__DEFINES/radio.dm +++ b/code/__DEFINES/radio.dm @@ -114,3 +114,20 @@ #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 + +/// Radio frequency is unlocked and can be ajusted by anyone +#define RADIO_FREQENCY_UNLOCKED 0 +/// Radio frequency is locked, unchangeable by players +#define RADIO_FREQENCY_LOCKED 1 +/// Radio frequency is locked and unchangeable, but can be unlocked by an emag +#define RADIO_FREQENCY_EMAGGABLE_LOCK 2 + +///Bitflag for if a headset can use the syndicate radio channel +#define RADIO_SPECIAL_SYNDIE (1<<0) +///Bitflag for if a headset can use the centcom radio channel +#define RADIO_SPECIAL_CENTCOM (1<<1) +///Bitflag for if a headset can use the binary radio channel +#define RADIO_SPECIAL_BINARY (1<<2) diff --git a/code/__DEFINES/spatial_gridmap.dm b/code/__DEFINES/spatial_gridmap.dm new file mode 100644 index 000000000000..97a6f9915399 --- /dev/null +++ b/code/__DEFINES/spatial_gridmap.dm @@ -0,0 +1,55 @@ +/// each cell in a spatial_grid is this many turfs in length and width (with world.max(x or y) being 255, 15 of these fit on each side of a z level) +#define SPATIAL_GRID_CELLSIZE 17 +/// Takes a coordinate, and spits out the spatial grid index (x or y) it's inside +#define GET_SPATIAL_INDEX(coord) ROUND_UP((coord) / SPATIAL_GRID_CELLSIZE) +/// changes the cell_(x or y) vars on /datum/spatial_grid_cell to the x or y coordinate on the map for the LOWER LEFT CORNER of the grid cell. +/// index is from 1 to SPATIAL_GRID_CELLS_PER_SIDE +#define GRID_INDEX_TO_COORDS(index) ((((index) - 1) * SPATIAL_GRID_CELLSIZE) + 1) +/// number of grid cells per x or y side of all z levels. pass in world.maxx or world.maxy +#define SPATIAL_GRID_CELLS_PER_SIDE(world_bounds) GET_SPATIAL_INDEX(world_bounds) + +//grid contents channels + +///everything that is hearing sensitive is stored in this channel +#define SPATIAL_GRID_CONTENTS_TYPE_HEARING RECURSIVE_CONTENTS_HEARING_SENSITIVE +///every movable that has a client in it is stored in this channel +#define SPATIAL_GRID_CONTENTS_TYPE_CLIENTS RECURSIVE_CONTENTS_CLIENT_MOBS +///all atmos machines are stored in this channel (I'm sorry kyler) +#define SPATIAL_GRID_CONTENTS_TYPE_ATMOS "spatial_grid_contents_type_atmos" + +#define ALL_CONTENTS_OF_CELL(cell) (cell.hearing_contents | cell.client_contents | cell.atmos_contents) + +///whether movable is itself or containing something which should be in one of the spatial grid channels. +#define HAS_SPATIAL_GRID_CONTENTS(movable) (movable.spatial_grid_key) + +// macros meant specifically to add/remove movables from the internal lists of /datum/spatial_grid_cell, +// when empty they become references to a single list in SSspatial_grid and when filled they become their own list +// this is to save memory without making them lazylists as that slows down iteration through them +#define GRID_CELL_ADD(cell_contents_list, movable_or_list) \ + if(!length(cell_contents_list)) { \ + cell_contents_list = list(); \ + cell_contents_list += movable_or_list; \ + } else { \ + cell_contents_list += movable_or_list; \ + }; + +#define GRID_CELL_SET(cell_contents_list, movable_or_list) \ + if(!length(cell_contents_list)) { \ + cell_contents_list = list(); \ + cell_contents_list += movable_or_list; \ + } else { \ + cell_contents_list |= movable_or_list; \ + }; + +//dont use these outside of SSspatial_grid's scope use the procs it has for this purpose +#define GRID_CELL_REMOVE(cell_contents_list, movable_or_list) \ + cell_contents_list -= movable_or_list; \ + if(!length(cell_contents_list)) {\ + cell_contents_list = dummy_list; \ + }; + +///remove from every list +#define GRID_CELL_REMOVE_ALL(cell, movable) \ + GRID_CELL_REMOVE(cell.hearing_contents, movable) \ + GRID_CELL_REMOVE(cell.client_contents, movable) \ + GRID_CELL_REMOVE(cell.atmos_contents, movable) diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index bc7b4b05d6f4..90167cc6f0e4 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -137,7 +137,7 @@ #define INIT_ORDER_INPUT 85 #define INIT_ORDER_SOUNDS 83 #define INIT_ORDER_INSTRUMENTS 82 -#define INIT_ORDER_GREYSCALE 81 +#define INIT_ORDER_GREYSCALE 81 #define INIT_ORDER_VIS 80 #define INIT_ORDER_SECURITY_LEVEL 79 #define INIT_ORDER_MATERIALS 76 @@ -149,6 +149,7 @@ #define INIT_ORDER_TICKER 55 #define INIT_ORDER_MAPPING 50 #define INIT_ORDER_EARLY_ASSETS 48 +#define INIT_ORDER_SPATIAL_GRID 43 #define INIT_ORDER_ECONOMY 40 #define INIT_ORDER_OUTPUTS 35 #define INIT_ORDER_ATOMS 30 diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm index df8ec8bd64b6..a0b7ac3fddb5 100644 --- a/code/__HELPERS/_lists.dm +++ b/code/__HELPERS/_lists.dm @@ -52,6 +52,10 @@ #define LAZYINITLIST(L) if (!L) { L = list(); } ///If the provided list is empty, set it to null #define UNSETEMPTY(L) if (L && !length(L)) L = null +///If the provided key -> list is empty, remove it from the list +#define ASSOC_UNSETEMPTY(L, K) if (!length(L[K])) L -= K; +///Like LAZYCOPY - copies an input list if the list has entries, If it doesn't the assigned list is nulled +#define LAZYLISTDUPLICATE(L) (L ? L.Copy() : null ) ///Remove an item from the list, set the list to null if empty #define LAZYREMOVE(L, I) if(L) { L -= I; if(!length(L)) { L = null; } } ///Add an item to the list, if the list is null it will initialize it diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm index 8490f613572b..e517e738188b 100644 --- a/code/__HELPERS/game.dm +++ b/code/__HELPERS/game.dm @@ -1,115 +1,12 @@ +// moved some logic to a different folder +// check spatial_info.dm + ///Time before being allowed to select a new cult leader again #define CULT_POLL_WAIT (240 SECONDS) /// Returns either the error landmark or the location of the room. Needless to say, if this is used, it means things have gone awry. #define GET_ERROR_ROOM ((locate(/obj/effect/landmark/error) in GLOB.landmarks_list) || locate(4,4,1)) -/proc/get_area_name(atom/X, format_text = FALSE, is_sensor = FALSE) - var/area/A = isarea(X) ? X : get_area(X) - if(!A) - return null - var/name = A.name - if (is_sensor && !A.show_on_sensors) - name = Gibberish(name, TRUE, 90) - return format_text ? format_text(name) : name - -/proc/get_areas_in_range(dist=0, atom/center=usr) - if(!dist) - var/turf/T = get_turf(center) - return T ? list(T.loc) : list() - if(!center) - return list() - - var/list/turfs = RANGE_TURFS(dist, center) - var/list/areas = list() - for(var/V in turfs) - var/turf/T = V - areas |= T.loc - return areas - -/proc/get_adjacent_areas(atom/center) - . = list(get_area(get_ranged_target_turf(center, NORTH, 1)), - get_area(get_ranged_target_turf(center, SOUTH, 1)), - get_area(get_ranged_target_turf(center, EAST, 1)), - get_area(get_ranged_target_turf(center, WEST, 1))) - listclearnulls(.) - -///Returns the open turf next to the center in a specific direction -/proc/get_open_turf_in_dir(atom/center, dir) - var/turf/open/get_turf = get_step(center, dir) - if(istype(get_turf)) - return get_turf - -///Returns a list with all the adjacent open turfs. Clears the list of nulls in the end. -/proc/get_adjacent_open_turfs(atom/center) - var/list/hand_back = list() - // Inlined get_open_turf_in_dir, just to be fast - var/turf/open/new_turf = get_step(center, NORTH) - if(istype(new_turf)) - hand_back += new_turf - new_turf = get_step(center, SOUTH) - if(istype(new_turf)) - hand_back += new_turf - new_turf = get_step(center, EAST) - if(istype(new_turf)) - hand_back += new_turf - new_turf = get_step(center, WEST) - if(istype(new_turf)) - hand_back += new_turf - return hand_back - - -/proc/get_adjacent_open_areas(atom/center) - . = list() - var/list/adjacent_turfs = get_adjacent_open_turfs(center) - for(var/I in adjacent_turfs) - . |= get_area(I) - -/** - * Get a bounding box of a list of atoms. - * - * Arguments: - * - atoms - List of atoms. Can accept output of view() and range() procs. - * - * Returns: list(x1, y1, x2, y2) - */ -/proc/get_bbox_of_atoms(list/atoms) - var/list/list_x = list() - var/list/list_y = list() - for(var/_a in atoms) - var/atom/a = _a - list_x += a.x - list_y += a.y - return list( - min(list_x), - min(list_y), - max(list_x), - max(list_y)) - - -// Like view but bypasses luminosity check - -/proc/get_hear(range, atom/source) - - var/lum = source.luminosity - source.luminosity = 6 - - var/list/heard = view(range, source) - source.luminosity = lum - - return heard - -/proc/alone_in_area(area/the_area, mob/must_be_alone, check_type = /mob/living/carbon) - var/area/our_area = get_area(the_area) - for(var/C in GLOB.alive_mob_list) - if(!istype(C, check_type)) - continue - if(C == must_be_alone) - continue - if(our_area == get_area(C)) - return 0 - return 1 - //We used to use linear regression to approximate the answer, but Mloc realized this was actually faster. //And lo and behold, it is, and it's more accurate to boot. /proc/cheap_hypotenuse(Ax,Ay,Bx,By) @@ -130,69 +27,7 @@ //turfs += centerturf return turfs -/proc/circleview(center=usr,radius=3) - var/turf/centerturf = get_turf(center) - var/list/atoms = new/list() - var/rsq = radius * (radius+0.5) - - for(var/atom/A in view(radius, centerturf)) - var/dx = A.x - centerturf.x - var/dy = A.y - centerturf.y - if(dx*dx + dy*dy <= rsq) - atoms += A - - //turfs += centerturf - return atoms - -/proc/get_dist_euclidian(atom/Loc1 as turf|mob|obj,atom/Loc2 as turf|mob|obj) - var/dx = Loc1.x - Loc2.x - var/dy = Loc1.y - Loc2.y - - var/dist = sqrt(dx**2 + dy**2) - - return dist - -///Returns a list of turfs around a center based on RANGE_TURFS() -/proc/circle_range_turfs(center = usr, radius = 3) - - var/turf/center_turf = get_turf(center) - var/list/turfs = new/list() - var/rsq = radius * (radius + 0.5) - - for(var/turf/checked_turf as anything in RANGE_TURFS(radius, center_turf)) - var/dx = checked_turf.x - center_turf.x - var/dy = checked_turf.y - center_turf.y - if(dx * dx + dy * dy <= rsq) - turfs += checked_turf - return turfs - -/proc/circleviewturfs(center=usr,radius=3) //Is there even a diffrence between this proc and circle_range_turfs()? // Yes - - var/turf/centerturf = get_turf(center) - var/list/turfs = new/list() - var/rsq = radius * (radius+0.5) - - for(var/turf/T in view(radius, centerturf)) - var/dx = T.x - centerturf.x - var/dy = T.y - centerturf.y - if(dx*dx + dy*dy <= rsq) - turfs += T - return turfs - - -//This is the new version of recursive_mob_check, used for say(). -//The other proc was left intact because morgue trays use it. -//Sped this up again for real this time -/proc/recursive_hear_check(O) - var/list/processing_list = list(O) - . = list() - var/i = 0 - while(i < length(processing_list)) - var/atom/A = processing_list[++i] - if(A.flags_1 & HEAR_1) - . += A - processing_list += A.contents /** recursive_organ_check * inputs: O (object to start with) @@ -231,128 +66,15 @@ return -// Better recursive loop, technically sort of not actually recursive cause that shit is stupid, enjoy. -//No need for a recursive limit either -/proc/recursive_mob_check(atom/O,client_check=1,sight_check=1,include_radio=1) - - var/list/processing_list = list(O) - var/list/processed_list = list() - var/list/found_mobs = list() - - while(processing_list.len) - - var/atom/A = processing_list[1] - var/passed = 0 - - if(ismob(A)) - var/mob/A_tmp = A - passed=1 - - if(client_check && !A_tmp.client) - passed=0 - - if(sight_check && !isInSight(A_tmp, O)) - passed=0 - - else if(include_radio && istype(A, /obj/item/radio)) - passed=1 - - if(sight_check && !isInSight(A, O)) - passed=0 - - if(passed) - found_mobs |= A - - for(var/atom/B in A) - if(!processed_list[B]) - processing_list |= B - - processing_list.Cut(1, 2) - processed_list[A] = A - - return found_mobs - - -/proc/get_hearers_in_view(R, atom/source) - // Returns a list of hearers in view(R) from source (ignoring luminosity). Used in saycode. - var/turf/T = get_turf(source) - . = list() - if(!T) - return - var/list/processing_list = list() - if (R == 0) // if the range is zero, we know exactly where to look for, we can skip view - processing_list += T.contents // We can shave off one iteration by assuming turfs cannot hear - else // A variation of get_hear inlined here to take advantage of the compiler's fastpath for obj/mob in view - var/lum = T.luminosity - T.luminosity = 6 // This is the maximum luminosity - for(var/mob/M in view(R, T)) - processing_list += M - for(var/obj/O in view(R, T)) - processing_list += O - T.luminosity = lum - - var/i = 0 - while(i < length(processing_list)) // recursive_hear_check inlined here - var/atom/A = processing_list[++i] - if(A.flags_1 & HEAR_1) - . += A - processing_list += A.contents - -/proc/get_mobs_in_radio_ranges(list/obj/item/radio/radios) - . = list() - // Returns a list of mobs who can hear any of the radios given in @radios - for(var/obj/item/radio/R in radios) - if(R) - . |= get_hearers_in_view(R.canhear_range, R) - - -#define SIGNV(X) ((X<0)?-1:1) - -/proc/inLineOfSight(X1,Y1,X2,Y2,Z=1,PX1=16.5,PY1=16.5,PX2=16.5,PY2=16.5) - var/turf/T - if(X1==X2) - if(Y1==Y2) - return 1 //Light cannot be blocked on same tile - else - var/s = SIGN(Y2-Y1) - Y1+=s - while(Y1!=Y2) - T=locate(X1,Y1,Z) - if(IS_OPAQUE_TURF(T)) - return 0 - Y1+=s - else - var/m=(32*(Y2-Y1)+(PY2-PY1))/(32*(X2-X1)+(PX2-PX1)) - var/b=(Y1+PY1/32-0.015625)-m*(X1+PX1/32-0.015625) //In tiles - var/signX = SIGN(X2-X1) - var/signY = SIGN(Y2-Y1) - if(X1 0 + * because view() isnt a raycasting algorithm, this does not hold symmetry to it. something in view might not be hearable with this. + * if you want that use get_hearers_in_view() - however thats significantly more expensive + * + * * view_radius - what radius search circle we are using, worse performance as this increases but not as much as it used to + * * source - object at the center of our search area. everything in get_turf(source) is guaranteed to be part of the search area + */ +/proc/get_hearers_in_LOS(view_radius, atom/source) + var/turf/center_turf = get_turf(source) + if(!center_turf) + return + + if(view_radius <= 0)//special case for if only source cares + . = list() + for(var/atom/movable/target as anything in center_turf) + var/list/hearing_contents = target.important_recursive_contents?[RECURSIVE_CONTENTS_HEARING_SENSITIVE] + if(hearing_contents) + . += hearing_contents + return + + . = SSspatial_grid.orthogonal_range_search(source, SPATIAL_GRID_CONTENTS_TYPE_HEARING, view_radius) + + for(var/atom/movable/target as anything in .) + var/turf/target_turf = get_turf(target) + + var/distance = get_dist(center_turf, target_turf) + + if(distance > view_radius) + . -= target + continue + + else if(distance < 2) //we should always be able to see something 0 or 1 tiles away + continue + + //this turf search algorithm is the worst scaling part of this proc, scaling worse than view() for small-moderate ranges and > 50 length contents_to_return + //luckily its significantly faster than view for large ranges in large spaces and/or relatively few contents_to_return + //i can do things that would scale better, but they would be slower for low volume searches which is the vast majority of the current workload + //maybe in the future a high volume algorithm would be worth it + var/turf/inbetween_turf = center_turf + + //this is the lowest overhead way of doing a loop in dm other than a goto. distance is guaranteed to be >= steps taken to target by this algorithm + for(var/step_counter in 1 to distance) + inbetween_turf = get_step_towards(inbetween_turf, target_turf) + + if(inbetween_turf == target_turf)//we've gotten to target's turf without returning due to turf opacity, so we must be able to see target + break + + if(IS_OPAQUE_TURF(inbetween_turf))//this turf or something on it is opaque so we cant see through it + . -= target + break + +/proc/get_hearers_in_radio_ranges(list/obj/item/radio/radios) + . = list() + // Returns a list of mobs who can hear any of the radios given in @radios + for(var/obj/item/radio/radio as anything in radios) + . |= get_hearers_in_LOS(radio.canhear_range, radio, FALSE) + +//Used when converting pixels to tiles to make them accurate +#define OFFSET_X (0.5 / ICON_SIZE_X) +#define OFFSET_Y (0.5 / ICON_SIZE_Y) + +///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/current_turf + if(X1 == X2) + if(Y1 == Y2) + return TRUE //Light cannot be blocked on same tile + else + var/sign = SIGN(Y2-Y1) + Y1 += sign + while(Y1 != Y2) + current_turf = locate(X1, Y1, Z) + if(IS_OPAQUE_TURF(current_turf)) + return FALSE + Y1 += sign + else + //This looks scary but we're just calculating a linear function (y = mx + b) + + //m = y/x + var/m = (ICON_SIZE_Y*(Y2-Y1) + (PY2-PY1)) / (ICON_SIZE_X*(X2-X1) + (PX2-PX1))//In pixels + + //b = y - mx + var/b = (Y1 + PY1/ICON_SIZE_Y - OFFSET_Y) - m*(X1 + PX1/ICON_SIZE_X - OFFSET_X)//In tiles + + var/signX = SIGN(X2-X1) + var/signY = SIGN(Y2-Y1) + if(X1 < X2) + b += m + while(X1 != X2 || Y1 != Y2) + if(round(m*X1 + b - Y1)) // Basically, if y >= mx+b + Y1 += signY //Line exits tile vertically + else + X1 += signX //Line exits tile horizontally + current_turf = locate(X1, Y1, Z) + if(IS_OPAQUE_TURF(current_turf)) + return FALSE + return TRUE + +#undef OFFSET_X +#undef OFFSET_Y + +/proc/is_in_sight(atom/first_atom, atom/second_atom) + var/turf/first_turf = get_turf(first_atom) + var/turf/second_turf = get_turf(second_atom) + + if(!first_turf || !second_turf) + return FALSE + + return inLineOfSight(first_turf.x, first_turf.y, second_turf.x, second_turf.y, first_turf.z) + +///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/Loc1 as turf|mob|obj,atom/Loc2 as turf|mob|obj) + var/dx = Loc1.x - Loc2.x + var/dy = Loc1.y - Loc2.y + + var/dist = sqrt(dx**2 + dy**2) + + return dist + +///Returns a list of turfs around a center based on RANGE_TURFS() +/proc/circle_range_turfs(center = usr, radius = 3) + + var/turf/center_turf = get_turf(center) + var/list/turfs = new/list() + var/rsq = radius * (radius + 0.5) + + for(var/turf/checked_turf as anything in RANGE_TURFS(radius, center_turf)) + var/dx = checked_turf.x - center_turf.x + var/dy = checked_turf.y - center_turf.y + if(dx * dx + dy * dy <= rsq) + turfs += checked_turf + return turfs + +///Returns a list of turfs around a center based on view() +/proc/circleviewturfs(center=usr,radius=3) //Is there even a diffrence between this proc and circle_range_turfs()? // Yes + 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 + +///Returns the list of turfs around the outside of a center based on RANGE_TURFS() +/proc/border_diamond_range_turfs(atom/center = usr, radius = 3) + var/turf/center_turf = get_turf(center) + var/list/turfs = list() + + 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 + var/abs_sum = abs(dx) + abs(dy) + if(abs_sum == radius) + turfs += checked_turf + return turfs + +///Returns a slice of a list of turfs, defined by the ones that are inside the inner/outer angle's bounds +/proc/slice_off_turfs(atom/center, list/turf/turfs, inner_angle, outer_angle) + var/turf/center_turf = get_turf(center) + var/list/sliced_turfs = list() + + for(var/turf/checked_turf as anything in turfs) + var/angle_to = get_angle(center_turf, checked_turf) + if(angle_to < inner_angle || angle_to > outer_angle) + continue + sliced_turfs += checked_turf + return sliced_turfs + +/** + * 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 + +///Returns the open turf next to the center in a specific direction +/proc/get_open_turf_in_dir(atom/center, dir) + var/turf/open/get_turf = get_step(center, dir) + if(istype(get_turf)) + return get_turf + +///Returns a list with all the adjacent open turfs. Clears the list of nulls in the end. +/proc/get_adjacent_open_turfs(atom/center) + var/list/hand_back = list() + // Inlined get_open_turf_in_dir, just to be fast + var/turf/open/new_turf = get_step(center, NORTH) + if(istype(new_turf)) + hand_back += new_turf + new_turf = get_step(center, SOUTH) + if(istype(new_turf)) + hand_back += new_turf + new_turf = get_step(center, EAST) + if(istype(new_turf)) + hand_back += new_turf + new_turf = get_step(center, WEST) + if(istype(new_turf)) + hand_back += new_turf + return hand_back + +///Returns a list with all the adjacent areas by getting the adjacent open turfs +/proc/get_adjacent_open_areas(atom/center) + . = list() + var/list/adjacent_turfs = get_adjacent_open_turfs(center) + for(var/near_turf in adjacent_turfs) + . |= get_area(near_turf) + +/** + * Returns a list with the names of the areas around a center at a certain distance + * 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)) + ) + listclearnulls(.) + +///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 + +/** + * Behaves like the orange() proc, but only looks in the outer range of the function (The "peel" of the orange). + * This is useful for things like checking if a mob is in a certain range, but not within a smaller range. + * + * @params outer_range - The outer range of the cicle to pull from. + * @params inner_range - The inner range of the circle to NOT pull from. + * @params center - The center of the circle to pull from, can be an atom (we'll apply get_turf() to it within circle_x_turfs procs.) + * @params view_based - If TRUE, we'll use circleviewturfs instead of circle_range_turfs procs. + */ +/proc/turf_peel(outer_range, inner_range, center, view_based = FALSE) + if(inner_range > outer_range) // If the inner range is larger than the outer range, you're using this wrong. + CRASH("Turf peel inner range is larger than outer range!") + var/list/peel = list() + var/list/outer + var/list/inner + if(view_based) + outer = circleviewturfs(center, outer_range) + inner = circleviewturfs(center, inner_range) + else + outer = circle_range_turfs(center, outer_range) + inner = circle_range_turfs(center, inner_range) + for(var/turf/possible_spawn as anything in outer) + if(possible_spawn in inner) + continue + peel += possible_spawn + + if(!length(peel)) + return center //Offer the center only as a default case when we don't have a valid circle. + return peel + diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm index df9e0a4ab2e0..b9eb2b916c3c 100644 --- a/code/_globalvars/bitfields.dm +++ b/code/_globalvars/bitfields.dm @@ -163,7 +163,6 @@ DEFINE_BITFIELD(resistance_flags, list( )) DEFINE_BITFIELD(flags_1, list( - "HEAR_1" = HEAR_1, "CHECK_RICOCHET_1" = CHECK_RICOCHET_1, "CONDUCT_1" = CONDUCT_1, "NODECONSTRUCT_1" = NODECONSTRUCT_1, diff --git a/code/controllers/subsystem/explosions.dm b/code/controllers/subsystem/explosions.dm index 16547126d4bc..e73f3dd230c5 100644 --- a/code/controllers/subsystem/explosions.dm +++ b/code/controllers/subsystem/explosions.dm @@ -279,18 +279,18 @@ SUBSYSTEM_DEF(explosions) baseshakeamount = sqrt((orig_max_distance - dist)*0.1) // If inside the blast radius + world.view - 2 if(dist <= round(max_range + world.view - 2, 1)) - M.playsound_local(epicenter, null, 100, 1, frequency, S = explosion_sound) + M.playsound_local(epicenter, null, 100, 1, frequency, sound_to_use = explosion_sound) if(baseshakeamount > 0) shake_camera(M, 25, clamp(baseshakeamount, 0, 10)) // You hear a far explosion if you're outside the blast radius. Small bombs shouldn't be heard all over the station. else if(dist <= far_dist) var/far_volume = clamp(far_dist/2, FAR_LOWER, FAR_UPPER) // Volume is based on explosion size and dist if(creaking_explosion) - M.playsound_local(epicenter, null, far_volume, 1, frequency, S = creaking_explosion_sound, distance_multiplier = 0) + M.playsound_local(epicenter, null, far_volume, 1, frequency, sound_to_use = creaking_explosion_sound, distance_multiplier = 0) else if(prob(PROB_SOUND)) // Sound variety during meteor storm/tesloose/other bad event - M.playsound_local(epicenter, null, far_volume, 1, frequency, S = far_explosion_sound, distance_multiplier = 0) // Far sound + M.playsound_local(epicenter, null, far_volume, 1, frequency, sound_to_use = far_explosion_sound, distance_multiplier = 0) // Far sound else - M.playsound_local(epicenter, null, far_volume, 1, frequency, S = explosion_echo_sound, distance_multiplier = 0) // Echo sound + M.playsound_local(epicenter, null, far_volume, 1, frequency, sound_to_use = explosion_echo_sound, distance_multiplier = 0) // Echo sound if(baseshakeamount > 0 || devastation_range) if(!baseshakeamount) // Devastating explosions rock the station and ground @@ -302,7 +302,7 @@ SUBSYSTEM_DEF(explosions) baseshakeamount = devastation_range shake_camera(M, 10, clamp(baseshakeamount*0.25, 0, SHAKE_CLAMP)) echo_volume = 60 - M.playsound_local(epicenter, null, echo_volume, 1, frequency, S = explosion_echo_sound, distance_multiplier = 0) + M.playsound_local(epicenter, null, echo_volume, 1, frequency, sound_to_use = explosion_echo_sound, distance_multiplier = 0) if(creaking_explosion) // 5 seconds after the bang, the station begins to creak addtimer(CALLBACK(M, TYPE_PROC_REF(/mob, playsound_local), epicenter, null, rand(FREQ_LOWER, FREQ_UPPER), 1, frequency, null, null, FALSE, hull_creaking_sound, 0), CREAK_DELAY) diff --git a/code/controllers/subsystem/spatial_gridmap.dm b/code/controllers/subsystem/spatial_gridmap.dm new file mode 100644 index 000000000000..65bcb0ec365b --- /dev/null +++ b/code/controllers/subsystem/spatial_gridmap.dm @@ -0,0 +1,847 @@ +///the subsystem creates this many [/mob/oranges_ear] mob instances during init. allocations that require more than this create more. +#define NUMBER_OF_PREGENERATED_ORANGES_EARS 2500 + +/** + * # Spatial Grid Cell + * + * used by [/datum/controller/subsystem/spatial_grid] to cover every z level so that the coordinates of every turf in the world corresponds to one of these in + * the subsystems list of grid cells by z level. each one of these contains content lists holding all atoms meeting a certain criteria that is in our borders. + * these datums shouldnt have significant behavior, they should just hold data. the lists are filled and emptied by the subsystem. + */ +/datum/spatial_grid_cell + ///our x index in the list of cells. this is our index inside of our row list + var/cell_x + ///our y index in the list of cells. this is the index of our row list inside of our z level grid + var/cell_y + ///which z level we belong to, corresponding to the index of our gridmap in SSspatial_grid.grids_by_z_level + var/cell_z + //every data point in a grid cell is separated by usecase + + //when empty, the contents lists of these grid cell datums are just references to a dummy list from SSspatial_grid + //this is meant to allow a great compromise between memory usage and speed. + //now orthogonal_range_search() doesnt need to check if the list is null and each empty list is taking 12 bytes instead of 24 + //the only downside is that it needs to be switched over to a new list when it goes from 0 contents to > 0 contents and switched back on the opposite case + + ///every hearing sensitive movable inside this cell + var/list/hearing_contents + ///every client possessed mob inside this cell + var/list/client_contents + ///every atmos machine inside this cell + var/list/atmos_contents + +/datum/spatial_grid_cell/New(cell_x, cell_y, cell_z) + . = ..() + src.cell_x = cell_x + src.cell_y = cell_y + src.cell_z = cell_z + //cache for sanic speed (lists are references anyways) + var/list/dummy_list = SSspatial_grid.dummy_list + + if(length(dummy_list)) + dummy_list.Cut() + stack_trace("SSspatial_grid.dummy_list had something inserted into it at some point! this is a problem as it is supposed to stay empty") + hearing_contents = dummy_list + client_contents = dummy_list + atmos_contents = dummy_list + +/datum/spatial_grid_cell/Destroy(force) + if(force)//the response to someone trying to qdel this is a right proper fuck you + stack_trace("dont try to destroy spatial grid cells without a good reason. if you need to do it use force") + return + + . = ..() + +/** + * # Spatial Grid + * + * a gamewide grid of spatial_grid_cell datums, each "covering" [SPATIAL_GRID_CELLSIZE] ^ 2 turfs. + * each spatial_grid_cell datum stores information about what is inside its covered area, so that searches through that area dont have to literally search + * through all turfs themselves to know what is within it since view() calls are expensive, and so is iterating through stuff you dont want. + * this allows you to only go through lists of what you want very cheaply. + * + * you can also register to objects entering and leaving a spatial cell, this allows you to do things like stay idle until a player enters, so you wont + * have to use expensive view() calls or iteratite over the global list of players and call get_dist() on every one. which is fineish for a few things, but is + * k * n operations for k objects iterating through n players. + * + * currently this system is only designed for searching for relatively uncommon things, small subsets of /atom/movable. + * dont add stupid shit to the cells please, keep the information that the cells store to things that need to be searched for often + * + * The system currently implements two different "classes" of spatial type + * + * The first exists to support important_recursive_contents. + * So if a client is inside a locker and the locker crosses a boundary, you'll still get a signal from the spatial grid. + * These types are [SPATIAL_GRID_CONTENTS_TYPE_HEARING] and [SPATIAL_GRID_CONTENTS_TYPE_CLIENTS] + * + * The second pattern is more paired down, and supports more wide use. + * Rather then the object and anything the object is in being sensitive, it's limited to just the object itself + * Currently only [SPATIAL_GRID_CONTENTS_TYPE_ATMOS] uses this pattern. This is because it's far more common, and so worth optimizing + * + */ +SUBSYSTEM_DEF(spatial_grid) + can_fire = FALSE + init_order = INIT_ORDER_SPATIAL_GRID + name = "Spatial Grid" + + ///list of the spatial_grid_cell datums per z level, arranged in the order of y index then x index + var/list/grids_by_z_level = list() + ///everything that spawns before us is added to this list until we initialize + var/list/waiting_to_add_by_type = list(SPATIAL_GRID_CONTENTS_TYPE_HEARING = list(), SPATIAL_GRID_CONTENTS_TYPE_CLIENTS = list(), SPATIAL_GRID_CONTENTS_TYPE_ATMOS = list()) + ///associative list of the form: movable.spatial_grid_key (string) -> inner list of spatial grid types for that key. + ///inner lists contain contents channel types such as SPATIAL_GRID_CONTENTS_TYPE_HEARING etc. + ///we use this to make adding to a cell static cost, and to save on memory + var/list/spatial_grid_categories = list() + + var/cells_on_x_axis = 0 + var/cells_on_y_axis = 0 + + ///empty spatial grid cell content lists are just a reference to this instead of a standalone list to save memory without needed to check if its null when iterating + var/list/dummy_list = list() + + ///list of all of /mob/oranges_ear instances we have pregenerated for view() iteration speedup + var/list/mob/oranges_ear/pregenerated_oranges_ears = list() + ///how many pregenerated /mob/oranges_ear instances currently exist. this should hopefully never exceed its starting value + var/number_of_oranges_ears = NUMBER_OF_PREGENERATED_ORANGES_EARS + +/datum/controller/subsystem/spatial_grid/Initialize() + cells_on_x_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxx) + cells_on_y_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxy) + + // enter_cell only runs if 'initialized' + initialized = TRUE + + for(var/datum/space_level/z_level as anything in SSmapping.z_list) + propogate_spatial_grid_to_new_z(null, z_level) + CHECK_TICK + + //go through the pre init queue for anything waiting to be let in the grid + for(var/channel_type in waiting_to_add_by_type) + for(var/atom/movable/movable as anything in waiting_to_add_by_type[channel_type]) + var/turf/movable_turf = get_turf(movable) + if(movable_turf) + enter_cell(movable, movable_turf) + + UnregisterSignal(movable, COMSIG_QDELETING) + waiting_to_add_by_type[channel_type] -= movable + + pregenerate_more_oranges_ears(NUMBER_OF_PREGENERATED_ORANGES_EARS) + + RegisterSignal(SSdcs, COMSIG_GLOB_NEW_Z, PROC_REF(propogate_spatial_grid_to_new_z)) + RegisterSignal(SSdcs, COMSIG_GLOB_EXPANDED_WORLD_BOUNDS, PROC_REF(after_world_bounds_expanded)) + return SS_INIT_SUCCESS + +///add a movable to the pre init queue for whichever type is specified so that when the subsystem initializes they get added to the grid +/datum/controller/subsystem/spatial_grid/proc/enter_pre_init_queue(atom/movable/waiting_movable, type) + RegisterSignal(waiting_movable, COMSIG_QDELETING, PROC_REF(queued_item_deleted), override = TRUE) + //override because something can enter the queue for two different types but that is done through unrelated procs that shouldnt know about eachother + waiting_to_add_by_type[type] += waiting_movable + +///removes an initialized and probably deleted movable from our pre init queue before we're initialized +/datum/controller/subsystem/spatial_grid/proc/remove_from_pre_init_queue(atom/movable/movable_to_remove, exclusive_type) + if(exclusive_type) + waiting_to_add_by_type[exclusive_type] -= movable_to_remove + + var/waiting_movable_is_in_other_queues = FALSE//we need to check if this movable is inside the other queues + for(var/type in waiting_to_add_by_type) + if(movable_to_remove in waiting_to_add_by_type[type]) + waiting_movable_is_in_other_queues = TRUE + + if(!waiting_movable_is_in_other_queues) + UnregisterSignal(movable_to_remove, COMSIG_QDELETING) + + return + + UnregisterSignal(movable_to_remove, COMSIG_QDELETING) + for(var/type in waiting_to_add_by_type) + waiting_to_add_by_type[type] -= movable_to_remove + +///if a movable is inside our pre init queue before we're initialized and it gets deleted we need to remove that reference with this proc +/datum/controller/subsystem/spatial_grid/proc/queued_item_deleted(atom/movable/movable_being_deleted) + SIGNAL_HANDLER + remove_from_pre_init_queue(movable_being_deleted, null) + +///creates the spatial grid for a new z level +/datum/controller/subsystem/spatial_grid/proc/propogate_spatial_grid_to_new_z(datum/controller/subsystem/processing/dcs/fucking_dcs, datum/space_level/z_level) + SIGNAL_HANDLER + + var/list/new_cell_grid = list() + + grids_by_z_level += list(new_cell_grid) + + for(var/y in 1 to cells_on_y_axis) + new_cell_grid += list(list()) + for(var/x in 1 to cells_on_x_axis) + var/datum/spatial_grid_cell/cell = new(x, y, z_level.z_value) + new_cell_grid[y] += cell + +///adds cells to the grid for every z level when world.maxx or world.maxy is expanded after this subsystem is initialized. hopefully this is never needed. +///because i never tested this. +/datum/controller/subsystem/spatial_grid/proc/after_world_bounds_expanded(datum/controller/subsystem/processing/dcs/fucking_dcs, has_expanded_world_maxx, has_expanded_world_maxy) + SIGNAL_HANDLER + var/old_x_axis = cells_on_x_axis + var/old_y_axis = cells_on_y_axis + + cells_on_x_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxx) + cells_on_y_axis = SPATIAL_GRID_CELLS_PER_SIDE(world.maxy) + + for(var/z_level in 1 to length(grids_by_z_level)) + var/list/z_level_gridmap = grids_by_z_level[z_level] + + for(var/cell_row_for_expanded_y_axis in 1 to cells_on_y_axis) + + if(cell_row_for_expanded_y_axis > old_y_axis)//we are past the old length of the number of rows, so add to the list + z_level_gridmap += list(list()) + + //now we know theres a row at this position, so add cells to it that need to be added and update the ones that already exist + var/list/cell_row = z_level_gridmap[cell_row_for_expanded_y_axis] + + for(var/grid_cell_for_expanded_x_axis in 1 to cells_on_x_axis) + + if(grid_cell_for_expanded_x_axis > old_x_axis) + var/datum/spatial_grid_cell/new_cell_inserted = new(grid_cell_for_expanded_x_axis, cell_row_for_expanded_y_axis, z_level) + cell_row += new_cell_inserted + continue + + //now we know the cell index we're at contains an already existing cell that needs its x and y values updated + var/datum/spatial_grid_cell/old_cell_that_needs_updating = cell_row[grid_cell_for_expanded_x_axis] + old_cell_that_needs_updating.cell_x = grid_cell_for_expanded_x_axis + old_cell_that_needs_updating.cell_y = cell_row_for_expanded_y_axis + +///the left or bottom side index of a box composed of spatial grid cells with the given actual center x or y coordinate +#define BOUNDING_BOX_MIN(center_coord) max(GET_SPATIAL_INDEX(center_coord - range), 1) +///the right or upper side index of a box composed of spatial grid cells with the given center x or y coordinate. +///outputted value cant exceed the number of cells on that axis +#define BOUNDING_BOX_MAX(center_coord, axis_size) min(GET_SPATIAL_INDEX(center_coord + range), axis_size) + +/** + * https://en.wikipedia.org/wiki/Range_searching#Orthogonal_range_searching + * + * searches through the grid cells intersecting a rectangular search space (with sides of length 2 * range) then returns all contents of type inside them. + * much faster than iterating through view() to find all of what you want. + * + * this does NOT return things only in range distance from center! the search space is a square not a circle, if you want only things in a certain distance + * then you need to filter that yourself + * + * * center - the atom that is the center of the searched circle + * * type - the type of grid contents you are looking for, see __DEFINES/spatial_grid.dm + * * range - the bigger this is, the more spatial grid cells the search space intersects + */ +/datum/controller/subsystem/spatial_grid/proc/orthogonal_range_search(atom/center, type, range) + var/turf/center_turf = get_turf(center) + + var/center_x = center_turf.x//used inside the macros + var/center_y = center_turf.y + + . = list() + + //technically THIS list only contains lists, but inside those lists are grid cell datums and we can go without a SINGLE var init if we do this + var/list/list/datum/spatial_grid_cell/grid_level = grids_by_z_level[center_turf.z] + + switch(type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + for(var/row in BOUNDING_BOX_MIN(center_y) to BOUNDING_BOX_MAX(center_y, cells_on_y_axis)) + for(var/x_index in BOUNDING_BOX_MIN(center_x) to BOUNDING_BOX_MAX(center_x, cells_on_x_axis)) + + . += grid_level[row][x_index].client_contents + + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + for(var/row in BOUNDING_BOX_MIN(center_y) to BOUNDING_BOX_MAX(center_y, cells_on_y_axis)) + for(var/x_index in BOUNDING_BOX_MIN(center_x) to BOUNDING_BOX_MAX(center_x, cells_on_x_axis)) + + . += grid_level[row][x_index].hearing_contents + + if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS) + for(var/row in BOUNDING_BOX_MIN(center_y) to BOUNDING_BOX_MAX(center_y, cells_on_y_axis)) + for(var/x_index in BOUNDING_BOX_MIN(center_x) to BOUNDING_BOX_MAX(center_x, cells_on_x_axis)) + . += grid_level[row][x_index].atmos_contents + + return . + +///get the grid cell encomapassing targets coordinates +/datum/controller/subsystem/spatial_grid/proc/get_cell_of(atom/target) + var/turf/target_turf = get_turf(target) + if(!target_turf) + return + + return grids_by_z_level[target_turf.z][GET_SPATIAL_INDEX(target_turf.y)][GET_SPATIAL_INDEX(target_turf.x)] + +///get all grid cells intersecting the bounding box around center with sides of length 2 * range +/datum/controller/subsystem/spatial_grid/proc/get_cells_in_range(atom/center, range) + return get_cells_in_bounds(center, range, range) + +///get all grid cells intersecting the bounding box around center with sides of length (2 * range_x, 2 * range_y) +/datum/controller/subsystem/spatial_grid/proc/get_cells_in_bounds(atom/center, range_x, range_y) + var/turf/center_turf = get_turf(center) + + var/center_x = center_turf.x + var/center_y = center_turf.y + + var/list/intersecting_grid_cells = list() + + //the minimum x and y cell indexes to test + var/min_x = max(GET_SPATIAL_INDEX(center_x - range_x), 1) + var/min_y = max(GET_SPATIAL_INDEX(center_y - range_y), 1)//calculating these indices only takes around 2 microseconds + + //the maximum x and y cell indexes to test + var/max_x = min(GET_SPATIAL_INDEX(center_x + range_x), cells_on_x_axis) + var/max_y = min(GET_SPATIAL_INDEX(center_y + range_y), cells_on_y_axis) + + var/list/grid_level = grids_by_z_level[center_turf.z] + + for(var/row in min_y to max_y) + var/list/grid_row = grid_level[row] + + for(var/x_index in min_x to max_x) + intersecting_grid_cells += grid_row[x_index] + + return intersecting_grid_cells + +/// Adds grid awareness to the passed in atom, of the passed in type +/// Basically, when this atom moves between grids, it wants to have enter/exit cell called on it +/datum/controller/subsystem/spatial_grid/proc/add_grid_awareness(atom/movable/add_to, type) + // We need to ensure we have a new list reference, to build our new key out of + var/list/current_list = spatial_grid_categories[add_to.spatial_grid_key] + if(current_list) + current_list = current_list.Copy() + else + current_list = list() + // Now we do a binary insert, to ensure it's sorted (don't wanna overcache) + BINARY_INSERT_DEFINE(type, current_list, SORT_VAR_NO_TYPE, type, SORT_COMPARE_DIRECTLY, COMPARE_KEY) + update_grid_awareness(add_to, current_list) + +/// Removes grid awareness from the passed in atom, of the passed in type +/datum/controller/subsystem/spatial_grid/proc/remove_grid_awareness(atom/movable/remove_from, type) + // We need to ensure we have a new list reference, to build our new key out of + var/list/current_list = spatial_grid_categories[remove_from.spatial_grid_key] + if(current_list) + current_list = current_list.Copy() + else + current_list = list() + current_list -= type + update_grid_awareness(remove_from, current_list) + +/// Alerts the atom's current cell that it wishes to be treated as a member +/// This functionally amounts to "hey, I was recently made aware by [add_grid_awareness], please insert me into my current cell" +/datum/controller/subsystem/spatial_grid/proc/add_grid_membership(atom/movable/add_to, turf/target_turf, type) + if(!target_turf) + return + if(initialized) + add_single_type(add_to, target_turf, type) + else //SSspatial_grid isnt init'd yet, add ourselves to the queue + enter_pre_init_queue(add_to, type) + +/// Removes grid membership from the passed in atom, of the passed in type +/datum/controller/subsystem/spatial_grid/proc/remove_grid_membership(atom/movable/remove_from, turf/target_turf, type) + if(!target_turf) + return + if(initialized) + remove_single_type(remove_from, target_turf, type) + else //SSspatial_grid isnt init'd yet, remove ourselves from the queue + remove_from_pre_init_queue(remove_from, type) + +/// Updates the string that atoms hold that stores their grid awareness +/// We will use it to key into their spatial grid categories later +/datum/controller/subsystem/spatial_grid/proc/update_grid_awareness(atom/movable/update, list/new_list) + // We locally store a stringified version of the list, to prevent people trying to mutate it + update.spatial_grid_key = new_list.Join("-") + // Ensure the global representation is cached + if(!spatial_grid_categories[update.spatial_grid_key]) + spatial_grid_categories[update.spatial_grid_key] = new_list + +///find the spatial map cell that target belongs to, then add the target to it, as its type prefers. +///make sure to provide the turf new_target is "in" +/datum/controller/subsystem/spatial_grid/proc/enter_cell(atom/movable/new_target, turf/target_turf) + if(!initialized) + return + if(QDELETED(new_target)) + CRASH("qdeleted or null target trying to enter the spatial grid!") + + if(!target_turf || !new_target.spatial_grid_key) + CRASH("null turf loc or a new_target that doesn't support it trying to enter the spatial grid!") + + var/x_index = GET_SPATIAL_INDEX(target_turf.x) + var/y_index = GET_SPATIAL_INDEX(target_turf.y) + var/z_index = target_turf.z + + var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index] + for(var/type in spatial_grid_categories[new_target.spatial_grid_key]) + switch(type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + var/list/new_target_contents = new_target.important_recursive_contents //cache for sanic speeds (lists are references anyways) + GRID_CELL_SET(intersecting_cell.client_contents, new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS]) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS]) + + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + var/list/new_target_contents = new_target.important_recursive_contents + GRID_CELL_SET(intersecting_cell.hearing_contents, new_target.important_recursive_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING]) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_HEARING), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING]) + + if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS) + GRID_CELL_SET(intersecting_cell.atmos_contents, new_target) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_ATMOS), new_target) + +///acts like enter_cell() but only adds the target to a specified type of grid cell contents list +/datum/controller/subsystem/spatial_grid/proc/add_single_type(atom/movable/new_target, turf/target_turf, exclusive_type) + if(!initialized) + return + if(QDELETED(new_target)) + CRASH("qdeleted or null target trying to enter the spatial grid!") + + if(!target_turf || !(exclusive_type in spatial_grid_categories[new_target.spatial_grid_key])) + CRASH("null turf loc or a new_target that doesn't support it trying to enter the spatial grid as a [exclusive_type]!") + + var/x_index = GET_SPATIAL_INDEX(target_turf.x) + var/y_index = GET_SPATIAL_INDEX(target_turf.y) + var/z_index = target_turf.z + + var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index] + switch(exclusive_type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + var/list/new_target_contents = new_target.important_recursive_contents //cache for sanic speeds (lists are references anyways) + GRID_CELL_SET(intersecting_cell.client_contents, new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS]) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_CLIENTS]) + + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + var/list/new_target_contents = new_target.important_recursive_contents + GRID_CELL_SET(intersecting_cell.hearing_contents, new_target.important_recursive_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING]) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_HEARING), new_target_contents[SPATIAL_GRID_CONTENTS_TYPE_HEARING]) + + if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS) + GRID_CELL_SET(intersecting_cell.atmos_contents, new_target) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_ATMOS), new_target) + + return intersecting_cell + +/** + * find the spatial map cell that target used to belong to, then remove the target (and sometimes its important_recusive_contents) from it. + * make sure to provide the turf old_target used to be "in" + * + * * old_target - the thing we want to remove from the spatial grid cell + * * target_turf - the turf we use to determine the cell we're removing from + * * exclusive_type - either null or a valid contents channel. if you just want to remove a single type from the grid cell then use this + */ +/datum/controller/subsystem/spatial_grid/proc/exit_cell(atom/movable/old_target, turf/target_turf, exclusive_type) + if(!initialized) + return + + if(!target_turf || !old_target.spatial_grid_key) + stack_trace("/datum/controller/subsystem/spatial_grid/proc/exit_cell() was given null arguments or a old_target that doesn't use the spatial grid!") + return FALSE + + var/x_index = GET_SPATIAL_INDEX(target_turf.x) + var/y_index = GET_SPATIAL_INDEX(target_turf.y) + var/z_index = target_turf.z + + var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index] + for(var/type in spatial_grid_categories[old_target.spatial_grid_key]) + switch(type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + var/list/old_target_contents = old_target.important_recursive_contents?[type] || old_target + GRID_CELL_REMOVE(intersecting_cell.client_contents, old_target_contents) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(type), old_target_contents) + + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + var/list/old_target_contents = old_target.important_recursive_contents?[type] || old_target + GRID_CELL_REMOVE(intersecting_cell.hearing_contents, old_target_contents) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(type), old_target_contents) + + if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS) + GRID_CELL_REMOVE(intersecting_cell.atmos_contents, old_target) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(type), old_target) + + return TRUE + +///acts like exit_cell() but only removes the target from the specified type of grid cell contents list +/datum/controller/subsystem/spatial_grid/proc/remove_single_type(atom/movable/old_target, turf/target_turf, exclusive_type) + if(!target_turf || !exclusive_type || !old_target.spatial_grid_key) + stack_trace("/datum/controller/subsystem/spatial_grid/proc/remove_single_type() was given null arguments or an old_target that doesn't use the spatial grid!") + return FALSE + + if(!(exclusive_type in spatial_grid_categories[old_target.spatial_grid_key])) + return FALSE + + var/x_index = GET_SPATIAL_INDEX(target_turf.x) + var/y_index = GET_SPATIAL_INDEX(target_turf.y) + var/z_index = target_turf.z + + var/datum/spatial_grid_cell/intersecting_cell = grids_by_z_level[z_index][y_index][x_index] + + switch(exclusive_type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + var/list/old_target_contents = old_target.important_recursive_contents?[exclusive_type] || old_target //cache for sanic speeds (lists are references anyways) + GRID_CELL_REMOVE(intersecting_cell.client_contents, old_target_contents) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(exclusive_type), old_target_contents) + + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + var/list/old_target_contents = old_target.important_recursive_contents?[exclusive_type] || old_target + GRID_CELL_REMOVE(intersecting_cell.hearing_contents, old_target_contents) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(exclusive_type), old_target_contents) + + if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS) + GRID_CELL_REMOVE(intersecting_cell.atmos_contents, old_target) + SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_EXITED(exclusive_type), old_target) + + return TRUE + +/// if for whatever reason this movable is "untracked" e.g. it breaks the assumption that a movable is only inside the contents of any grid cell associated with its loc, +/// this will error. this checks every grid cell in the world so dont call this on live unless you have to. +/// returns TRUE if this movable is untracked, FALSE otherwise +/datum/controller/subsystem/spatial_grid/proc/untracked_movable_error(atom/movable/movable_to_check) + if(!movable_to_check?.spatial_grid_key) + return FALSE + + if(!initialized) + return FALSE + + var/datum/spatial_grid_cell/loc_cell = get_cell_of(movable_to_check) + var/list/containing_cells = find_hanging_cell_refs_for_movable(movable_to_check, remove_from_cells=FALSE) + //if we're in multiple cells, throw an error. + //if we're in 1 cell but it cant be deduced by our location, throw an error. + if(length(containing_cells) > 1 || (length(containing_cells) == 1 && loc_cell && containing_cells[1] != loc_cell && containing_cells[1] != null)) + var/error_data = "" + + var/location_string = "which is in nullspace, and thus not be within the contents of any spatial grid cell" + if(loc_cell) + location_string = "which is supposed to only be in the contents of a spatial grid cell at coords: ([GRID_INDEX_TO_COORDS(loc_cell.cell_x)], [GRID_INDEX_TO_COORDS(loc_cell.cell_y)], [loc_cell.cell_z])" + + var/error_explanation = "was in the contents of [length(containing_cells)] spatial grid cells when it was only supposed to be in one!" + if(length(containing_cells) == 1) + error_explanation = "was in the contents of 1 spatial grid cell but it was inside the area handled by another grid cell!" + var/datum/spatial_grid_cell/bad_cell = containing_cells[1] + + error_data = "within the contents of a cell at coords: ([GRID_INDEX_TO_COORDS(bad_cell.cell_x)], [GRID_INDEX_TO_COORDS(bad_cell.cell_y)], [bad_cell.cell_z])" + + if(!error_data) + for(var/datum/spatial_grid_cell/cell in containing_cells) + var/coords = "([GRID_INDEX_TO_COORDS(cell.cell_x)], [GRID_INDEX_TO_COORDS(cell.cell_y)], [cell.cell_z])" + var/contents = "" + + if(movable_to_check in cell.hearing_contents) + contents = "hearing" + + if(movable_to_check in cell.client_contents) + if(length(contents) > 0) + contents = "[contents], client" + else + contents = "client" + + if(movable_to_check in cell.atmos_contents) + if(length(contents) > 0) + contents = "[contents], atmos" + else + contents = "atmos" + + if(length(error_data) > 0) + error_data = "[error_data], {coords: [coords], within channels: [contents]}" + else + error_data = "within the contents of the following cells: {coords: [coords], within channels: [contents]}" + + /** + * example: + * + * /mob/living/trolls_the_maintainer instance, which is supposed to only be in the contents of a spatial grid cell at coords: (136, 136, 14), + * was in the contents of 3 spatial grid cells when it was only supposed to be in one! within the contents of the following cells: + * {(68, 153, 2), within channels: hearing}, + * {coords: (221, 170, 3), within channels: hearing}, + * {coords: (255, 153, 11), within channels: hearing}, + * {coords: (136, 136, 14), within channels: hearing}. + */ + stack_trace("[movable_to_check.type] instance, [location_string], [error_explanation] [error_data].") + + return TRUE + + return FALSE + +/** + * remove this movable from the grid by finding the grid cell its in and removing it from that. + * if it cant infer a grid cell its located in (e.g. if its in nullspace but it can happen if the grid isnt expanded to a z level), search every grid cell. + */ +/datum/controller/subsystem/spatial_grid/proc/force_remove_from_grid(atom/movable/to_remove) + if(!to_remove?.spatial_grid_key) + return + + if(!initialized) + remove_from_pre_init_queue(to_remove)//the spatial grid doesnt exist yet, so just take it out of the queue + return + +#ifdef UNIT_TESTS + if(untracked_movable_error(to_remove)) + find_hanging_cell_refs_for_movable(to_remove, remove_from_cells=FALSE) //dont remove from cells because we should be able to see 2 errors + return +#endif + + var/datum/spatial_grid_cell/loc_cell = get_cell_of(to_remove) + + if(loc_cell) + GRID_CELL_REMOVE_ALL(loc_cell, to_remove) + else + find_hanging_cell_refs_for_movable(to_remove, remove_from_cells=TRUE) + +///remove this movable from the given spatial_grid_cell +/datum/controller/subsystem/spatial_grid/proc/force_remove_from_cell(atom/movable/to_remove, datum/spatial_grid_cell/input_cell) + if(!input_cell) + return + + GRID_CELL_REMOVE_ALL(input_cell, to_remove) + +///if shit goes south, this will find hanging references for qdeleting movables inside the spatial grid +/datum/controller/subsystem/spatial_grid/proc/find_hanging_cell_refs_for_movable(atom/movable/to_remove, remove_from_cells = TRUE) + + var/list/queues_containing_movable = list() + for(var/queue_channel in waiting_to_add_by_type) + var/list/queue_list = waiting_to_add_by_type[queue_channel] + if(to_remove in queue_list) + queues_containing_movable += queue_channel//just add the associative key + if(remove_from_cells) + queue_list -= to_remove + + if(!initialized) + return queues_containing_movable + + var/list/containing_cells = list() + for(var/list/z_level_grid as anything in grids_by_z_level) + for(var/list/cell_row as anything in z_level_grid) + for(var/datum/spatial_grid_cell/cell as anything in cell_row) + if(to_remove in (cell.hearing_contents | cell.client_contents | cell.atmos_contents)) + containing_cells += cell + if(remove_from_cells) + force_remove_from_cell(to_remove, cell) + + return containing_cells + +///debug proc for checking if a movable is in multiple cells when it shouldnt be (ie always unless multitile entering is implemented) +/atom/proc/find_all_cells_containing(remove_from_cells = FALSE) + var/datum/spatial_grid_cell/real_cell = SSspatial_grid.get_cell_of(src) + var/list/containing_cells = SSspatial_grid.find_hanging_cell_refs_for_movable(src, remove_from_cells) + + message_admins("[src] is located in the contents of [length(containing_cells)] spatial grid cells") + + var/cell_coords = "the following cells contain [src]: " + for(var/datum/spatial_grid_cell/cell as anything in containing_cells) + cell_coords += "([cell.cell_x], [cell.cell_y], [cell.cell_z]), " + + message_admins(cell_coords) + message_admins("[src] is supposed to only be contained in the cell at indexes ([real_cell.cell_x], [real_cell.cell_y], [real_cell.cell_z]). but is contained at the cells at [cell_coords]") + +///creates number_to_generate new oranges_ear's and adds them to the subsystems list of ears. +///i really fucking hope this never gets called after init :clueless: +/datum/controller/subsystem/spatial_grid/proc/pregenerate_more_oranges_ears(number_to_generate) + for(var/new_ear in 1 to number_to_generate) + pregenerated_oranges_ears += new/mob/oranges_ear(null) + + number_of_oranges_ears = length(pregenerated_oranges_ears) + +///allocate one [/mob/oranges_ear] mob per turf containing atoms_that_need_ears and give them a reference to every listed atom in their turf. +///if an oranges_ear is allocated to a turf that already has an oranges_ear then the second one fails to allocate (and gives the existing one the atom it was assigned to) +/datum/controller/subsystem/spatial_grid/proc/assign_oranges_ears(list/atoms_that_need_ears) + var/input_length = length(atoms_that_need_ears) + + if(input_length > number_of_oranges_ears) + stack_trace("somehow, for some reason, more than the preset generated number of oranges ears was requested. thats fucking [number_of_oranges_ears]. this is not good that should literally never happen") + pregenerate_more_oranges_ears(input_length - number_of_oranges_ears)//im still gonna DO IT but ill complain about it + + . = list() + + ///the next unallocated /mob/oranges_ear that we try to allocate to assigned_atom's turf + var/mob/oranges_ear/current_ear + ///the next atom in atoms_that_need_ears an ear assigned to it + var/atom/assigned_atom + ///the turf loc of the current assigned_atom. turfs are used to track oranges_ears already assigned to one location so we dont allocate more than one + ///because allocating more than one oranges_ear to a given loc wastes view iterations + var/turf/turf_loc + + for(var/current_ear_index in 1 to input_length) + assigned_atom = atoms_that_need_ears[current_ear_index] + + turf_loc = get_turf(assigned_atom) + if(!turf_loc) + continue + + current_ear = pregenerated_oranges_ears[current_ear_index] + + if(turf_loc.assigned_oranges_ear) + turf_loc.assigned_oranges_ear.references += assigned_atom + continue //if theres already an oranges_ear mob at assigned_movable's turf we give assigned_movable to it instead and dont allocate ourselves + + current_ear.references += assigned_atom + + current_ear.loc = turf_loc //normally this is bad, but since this is meant to be as fast as possible we literally just need to exist there for view() to see us + turf_loc.assigned_oranges_ear = current_ear + + . += current_ear + +///debug proc for finding how full the cells of src's z level are +/atom/proc/find_grid_statistics_for_z_level(insert_clients = 0) + var/raw_clients = 0 + var/raw_hearables = 0 + var/raw_atmos = 0 + + var/cells_with_clients = 0 + var/cells_with_hearables = 0 + var/cells_with_atmos = 0 + + var/list/client_list = list() + var/list/hearable_list = list() + var/list/atmos_list = list() + + var/x_cell_count = world.maxx / SPATIAL_GRID_CELLSIZE + var/y_cell_count = world.maxy / SPATIAL_GRID_CELLSIZE + + var/total_cells = x_cell_count ** 2 + + var/average_clients_per_cell = 0 + var/average_hearables_per_cell = 0 + var/average_atmos_mech_per_call = 0 + + var/hearable_min_x = x_cell_count + var/hearable_max_x = 1 + + var/hearable_min_y = y_cell_count + var/hearable_max_y = 1 + + var/client_min_x = x_cell_count + var/client_max_x = 1 + + var/client_min_y = y_cell_count + var/client_max_y = 1 + + var/atmos_min_x = x_cell_count + var/atmos_max_x = 1 + + var/atmos_min_y = y_cell_count + var/atmos_max_y = 1 + + var/list/inserted_clients = list() + + if(insert_clients) + var/list/turfs + var/level = SSmapping.get_level(z) + if(is_station_level(level)) + turfs = GLOB.station_turfs + + else + turfs = Z_TURFS(z) + + for(var/client_to_insert in 0 to insert_clients) + var/turf/random_turf = pick(turfs) + var/mob/fake_client = new() + fake_client.important_recursive_contents = list(SPATIAL_GRID_CONTENTS_TYPE_HEARING = list(fake_client), SPATIAL_GRID_CONTENTS_TYPE_CLIENTS = list(fake_client)) + fake_client.forceMove(random_turf) + inserted_clients += fake_client + + var/list/all_z_level_cells = SSspatial_grid.get_cells_in_range(src, 1000) + + for(var/datum/spatial_grid_cell/cell as anything in all_z_level_cells) + var/client_length = length(cell.client_contents) + var/hearable_length = length(cell.hearing_contents) + var/atmos_length = length(cell.atmos_contents) + + raw_clients += client_length + raw_hearables += hearable_length + raw_atmos += atmos_length + + if(client_length) + cells_with_clients++ + + client_list += cell.client_contents + + if(cell.cell_x < client_min_x) + client_min_x = cell.cell_x + + if(cell.cell_x > client_max_x) + client_max_x = cell.cell_x + + if(cell.cell_y < client_min_y) + client_min_y = cell.cell_y + + if(cell.cell_y > client_max_y) + client_max_y = cell.cell_y + + if(hearable_length) + cells_with_hearables++ + + hearable_list += cell.hearing_contents + + if(cell.cell_x < hearable_min_x) + hearable_min_x = cell.cell_x + + if(cell.cell_x > hearable_max_x) + hearable_max_x = cell.cell_x + + if(cell.cell_y < hearable_min_y) + hearable_min_y = cell.cell_y + + if(cell.cell_y > hearable_max_y) + hearable_max_y = cell.cell_y + + if(raw_atmos) + cells_with_atmos++ + + atmos_list += cell.atmos_contents + + if(cell.cell_x < atmos_min_x) + atmos_min_x = cell.cell_x + + if(cell.cell_x > atmos_max_x) + atmos_max_x = cell.cell_x + + if(cell.cell_y < atmos_min_y) + atmos_min_y = cell.cell_y + + if(cell.cell_y > atmos_max_y) + atmos_max_y = cell.cell_y + + var/total_client_distance = 0 + var/total_hearable_distance = 0 + var/total_atmos_distance = 0 + + var/average_client_distance = 0 + var/average_hearable_distance = 0 + var/average_atmos_distance = 0 + + for(var/hearable in hearable_list)//n^2 btw + for(var/other_hearable in hearable_list) + if(hearable == other_hearable) + continue + total_hearable_distance += get_dist(hearable, other_hearable) + + for(var/client in client_list)//n^2 btw + for(var/other_client in client_list) + if(client == other_client) + continue + total_client_distance += get_dist(client, other_client) + + for(var/atmos in atmos_list)//n^2 btw + for(var/other_atmos in atmos_list) + if(atmos == other_atmos) + continue + total_atmos_distance += get_dist(atmos, other_atmos) + + if(length(hearable_list)) + average_hearable_distance = total_hearable_distance / length(hearable_list) + if(length(client_list)) + average_client_distance = total_client_distance / length(client_list) + if(length(atmos_list)) + average_atmos_distance = total_atmos_distance / length(atmos_list) + + average_clients_per_cell = raw_clients / total_cells + average_hearables_per_cell = raw_hearables / total_cells + average_atmos_mech_per_call = raw_atmos / total_cells + + for(var/mob/inserted_client as anything in inserted_clients) + qdel(inserted_client) + + message_admins("on z level [z] there are [raw_clients] clients ([insert_clients] of whom are fakes inserted to random station turfs)\ + , [raw_hearables] hearables, and [raw_atmos] atmos machines. all of whom are inside the bounding box given by \ + clients: ([client_min_x], [client_min_y]) x ([client_max_x], [client_max_y]), \ + hearables: ([hearable_min_x], [hearable_min_y]) x ([hearable_max_x], [hearable_max_y]) \ + and atmos machines: ([atmos_min_x], [atmos_min_y]) x ([atmos_max_x], [atmos_max_y]), \ + on average there are [average_clients_per_cell] clients per cell, [average_hearables_per_cell] hearables per cell, \ + and [average_atmos_mech_per_call] per cell, \ + [cells_with_clients] cells have clients, [cells_with_hearables] have hearables, and [cells_with_atmos] have atmos machines \ + the average client distance is: [average_client_distance], the average hearable_distance is [average_hearable_distance], \ + and the average atmos distance is [average_atmos_distance] ") + +#undef BOUNDING_BOX_MAX +#undef BOUNDING_BOX_MIN + +#undef NUMBER_OF_PREGENERATED_ORANGES_EARS diff --git a/code/datums/wires/radio.dm b/code/datums/wires/radio.dm index a1118da6d73c..e2b4192020f0 100644 --- a/code/datums/wires/radio.dm +++ b/code/datums/wires/radio.dm @@ -17,9 +17,9 @@ var/obj/item/radio/R = holder switch(index) if(WIRE_SIGNAL) - R.listening = !R.listening - R.broadcasting = R.listening + R.set_listening(!R.get_listening()) + R.set_broadcasting(R.get_listening()) if(WIRE_RX) - R.listening = !R.listening + R.set_listening(!R.get_listening()) if(WIRE_TX) - R.broadcasting = !R.broadcasting + R.set_broadcasting(!R.get_broadcasting()) diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm index 8413dc598875..051821d36043 100644 --- a/code/game/area/areas.dm +++ b/code/game/area/areas.dm @@ -778,19 +778,29 @@ GLOBAL_LIST_EMPTY(teleportlocs) * * If the area has ambience, then it plays some ambience music to the ambience channel */ -/area/Entered(atom/movable/M) +/area/Entered(atom/movable/arrived, area/old_area) set waitfor = FALSE - SEND_SIGNAL(src, COMSIG_AREA_ENTERED, M) - SEND_SIGNAL(M, COMSIG_ENTER_AREA, src) //The atom that enters the area + SEND_SIGNAL(src, COMSIG_AREA_ENTERED, arrived, old_area) + + if(!arrived.important_recursive_contents?[RECURSIVE_CONTENTS_AREA_SENSITIVE]) + return + for(var/atom/movable/recipient as anything in arrived.important_recursive_contents[RECURSIVE_CONTENTS_AREA_SENSITIVE]) + SEND_SIGNAL(recipient, COMSIG_ENTER_AREA, src) /** * Called when an atom exits an area * * Sends signals COMSIG_AREA_EXITED and COMSIG_EXIT_AREA (to the atom) */ -/area/Exited(atom/movable/M) - SEND_SIGNAL(src, COMSIG_AREA_EXITED, M) - SEND_SIGNAL(M, COMSIG_EXIT_AREA, src) //The atom that exits the area +/area/Exited(atom/movable/gone, direction) + SEND_SIGNAL(src, COMSIG_AREA_EXITED,gone, direction) + SEND_SIGNAL(gone, COMSIG_EXIT_AREA, src, direction) //The atom that exits the area + + if(!gone.important_recursive_contents?[RECURSIVE_CONTENTS_AREA_SENSITIVE]) + return + for(var/atom/movable/recipient as anything in gone.important_recursive_contents[RECURSIVE_CONTENTS_AREA_SENSITIVE]) + SEND_SIGNAL(recipient, COMSIG_EXIT_AREA, src) + /** * Returns true if this atom has gravity for the passed in turf diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index f758158da8e4..79a9777f3664 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -32,7 +32,6 @@ var/generic_canpass = TRUE var/moving_diagonally = 0 //0: not doing a diagonal move. 1 and 2: doing the first/second step of the diagonal move var/atom/movable/moving_from_pull //attempt to resume grab after moving instead of before. - var/list/client_mobs_in_contents // This contains all the client mobs within this container var/list/acted_explosions //for explosion dodging var/datum/forced_movement/force_moving = null //handled soley by forced_movement.dm @@ -72,6 +71,21 @@ /// Whether this atom should have its dir automatically changed when it moves. Setting this to FALSE allows for things such as directional windows to retain dir on moving without snowflake code all of the place. var/set_dir_on_move = TRUE + /** + * an associative lazylist of relevant nested contents by "channel", the list is of the form: list(channel = list(important nested contents of that type)) + * each channel has a specific purpose and is meant to replace potentially expensive nested contents iteration. + * do NOT add channels to this for little reason as it can add considerable memory usage. + */ + var/list/important_recursive_contents + ///contains every client mob corresponding to every client eye in this container. lazily updated by SSparallax and is sparse: + ///only the last container of a client eye has this list assuming no movement since SSparallax's last fire + var/list/client_mobs_in_contents + + /// String representing the spatial grid groups we want to be held in. + /// acts as a key to the list of spatial grid contents types we exist in via SSspatial_grid.spatial_grid_categories. + /// We do it like this to prevent people trying to mutate them and to save memory on holding the lists ourselves + var/spatial_grid_key + /mutable_appearance/emissive_blocker /mutable_appearance/emissive_blocker/New() @@ -169,7 +183,10 @@ orbiting.end_orbit(src) orbiting = null - LAZYCLEARLIST(client_mobs_in_contents) + if(spatial_grid_key) + SSspatial_grid.force_remove_from_grid(src) + + LAZYNULL(client_mobs_in_contents) . = ..() @@ -178,6 +195,11 @@ moveToNullspace() + //This absolutely must be after moveToNullspace() + //We rely on Entered and Exited to manage this list, and the copy of this list that is on any /atom/movable "Containers" + //If we clear this before the nullspace move, a ref to this object will be hung in any of its movable containers + LAZYNULL(important_recursive_contents) + vis_locs = null //clears this atom out of all viscontents // Checking length(vis_contents) before cutting has significant speed benefits @@ -770,6 +792,21 @@ on_changed_z_level(old_turf, new_turf, same_z_layer) SSdemo.mark_dirty(src) + + if(HAS_SPATIAL_GRID_CONTENTS(src)) + if(old_turf && new_turf && (old_turf.z != new_turf.z \ + || GET_SPATIAL_INDEX(old_turf.x) != GET_SPATIAL_INDEX(new_turf.x) \ + || GET_SPATIAL_INDEX(old_turf.y) != GET_SPATIAL_INDEX(new_turf.y))) + + 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. @@ -795,6 +832,156 @@ /atom/movable/Uncrossed(atom/movable/AM) SEND_SIGNAL(src, COMSIG_MOVABLE_UNCROSSED, AM) +/////////////////important-recursive-contents/////////////////////////// +/////////////////and spatial grid stuff///////////////////////////////// + +/atom/movable/Exited(atom/movable/gone, direction) + . = ..() + + if(!LAZYLEN(gone.important_recursive_contents)) + return + var/list/nested_locs = get_nested_locs(src) + src + for(var/channel in gone.important_recursive_contents) + for(var/atom/movable/location as anything in nested_locs) + LAZYINITLIST(location.important_recursive_contents) + var/list/recursive_contents = location.important_recursive_contents // blue hedgehog velocity + LAZYINITLIST(recursive_contents[channel]) + recursive_contents[channel] -= gone.important_recursive_contents[channel] + switch(channel) + if(RECURSIVE_CONTENTS_CLIENT_MOBS, RECURSIVE_CONTENTS_HEARING_SENSITIVE) + if(!length(recursive_contents[channel])) + // This relies on a nice property of the linked recursive and gridmap types + // They're defined in relation to each other, so they have the same value + SSspatial_grid.remove_grid_awareness(location, channel) + ASSOC_UNSETEMPTY(recursive_contents, channel) + UNSETEMPTY(location.important_recursive_contents) + +/atom/movable/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs) + . = ..() + + if(!LAZYLEN(arrived.important_recursive_contents)) + return + var/list/nested_locs = get_nested_locs(src) + src + for(var/channel in arrived.important_recursive_contents) + for(var/atom/movable/location as anything in nested_locs) + LAZYINITLIST(location.important_recursive_contents) + var/list/recursive_contents = location.important_recursive_contents // blue hedgehog velocity + LAZYINITLIST(recursive_contents[channel]) + switch(channel) + if(RECURSIVE_CONTENTS_CLIENT_MOBS, RECURSIVE_CONTENTS_HEARING_SENSITIVE) + if(!length(recursive_contents[channel])) + SSspatial_grid.add_grid_awareness(location, channel) + recursive_contents[channel] |= arrived.important_recursive_contents[channel] + +///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) + ADD_TRAIT(src, TRAIT_HEARING_SENSITIVE, trait_source) + if(!HAS_TRAIT(src, TRAIT_HEARING_SENSITIVE)) + return + + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYINITLIST(location.important_recursive_contents) + var/list/recursive_contents = location.important_recursive_contents // blue hedgehog velocity + if(!length(recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE])) + SSspatial_grid.add_grid_awareness(location, SPATIAL_GRID_CONTENTS_TYPE_HEARING) + recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE] += list(src) + + var/turf/our_turf = get_turf(src) + SSspatial_grid.add_grid_membership(src, our_turf, SPATIAL_GRID_CONTENTS_TYPE_HEARING) + +/** + * removes the hearing sensitivity channel from the important_recursive_contents list of this and all nested locs containing us if there are no more sources of the trait left + * 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) + /// We get our awareness updated by the important recursive contents stuff, here we remove our membership + SSspatial_grid.remove_grid_membership(src, our_turf, SPATIAL_GRID_CONTENTS_TYPE_HEARING) + + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + var/list/recursive_contents = location.important_recursive_contents // blue hedgehog velocity + recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE] -= src + if(!length(recursive_contents[RECURSIVE_CONTENTS_HEARING_SENSITIVE])) + SSspatial_grid.remove_grid_awareness(location, SPATIAL_GRID_CONTENTS_TYPE_HEARING) + ASSOC_UNSETEMPTY(recursive_contents, RECURSIVE_CONTENTS_HEARING_SENSITIVE) + UNSETEMPTY(location.important_recursive_contents) + +///allows this movable to know when it has "entered" another area no matter how many movable atoms its stuffed into, uses important_recursive_contents +/atom/movable/proc/become_area_sensitive(trait_source = TRAIT_GENERIC) + if(!HAS_TRAIT(src, TRAIT_AREA_SENSITIVE)) + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYADDASSOCLIST(location.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE, src) + ADD_TRAIT(src, TRAIT_AREA_SENSITIVE, trait_source) + +///removes the area sensitive channel from the important_recursive_contents list of this and all nested locs containing us if there are no more source of the trait left +/atom/movable/proc/lose_area_sensitivity(trait_source = TRAIT_GENERIC) + if(!HAS_TRAIT(src, TRAIT_AREA_SENSITIVE)) + return + REMOVE_TRAIT(src, TRAIT_AREA_SENSITIVE, trait_source) + if(HAS_TRAIT(src, TRAIT_AREA_SENSITIVE)) + return + + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYREMOVEASSOC(location.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE, src) + +///propogates ourselves through our nested contents, similar to other important_recursive_contents procs +///main difference is that client contents need to possibly duplicate recursive contents for the clients mob AND its eye +/mob/proc/enable_client_mobs_in_contents() + for(var/atom/movable/movable_loc as anything in get_nested_locs(src) + src) + LAZYINITLIST(movable_loc.important_recursive_contents) + var/list/recursive_contents = movable_loc.important_recursive_contents // blue hedgehog velocity + if(!length(recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS])) + SSspatial_grid.add_grid_awareness(movable_loc, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + LAZYINITLIST(recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS]) + recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS] |= src + + var/turf/our_turf = get_turf(src) + /// We got our awareness updated by the important recursive contents stuff, now we add our membership + SSspatial_grid.add_grid_membership(src, our_turf, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + +///Clears the clients channel of this mob +/mob/proc/clear_important_client_contents() + var/turf/our_turf = get_turf(src) + SSspatial_grid.remove_grid_membership(src, our_turf, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + + for(var/atom/movable/movable_loc as anything in get_nested_locs(src) + src) + LAZYINITLIST(movable_loc.important_recursive_contents) + var/list/recursive_contents = movable_loc.important_recursive_contents // blue hedgehog velocity + LAZYINITLIST(recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS]) + recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS] -= src + if(!length(recursive_contents[RECURSIVE_CONTENTS_CLIENT_MOBS])) + SSspatial_grid.remove_grid_awareness(movable_loc, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + ASSOC_UNSETEMPTY(recursive_contents, RECURSIVE_CONTENTS_CLIENT_MOBS) + UNSETEMPTY(movable_loc.important_recursive_contents) + +///called when this movable becomes the parent of a storage component that is currently being viewed by a player. uses important_recursive_contents +/atom/movable/proc/become_active_storage(datum/component/storage/source) + if(!HAS_TRAIT(src, TRAIT_ACTIVE_STORAGE)) + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYADDASSOCLIST(location.important_recursive_contents, RECURSIVE_CONTENTS_ACTIVE_STORAGE, src) + ADD_TRAIT(src, TRAIT_ACTIVE_STORAGE, REF(source)) + +///called when this movable's storage component is no longer viewed by any players, unsets important_recursive_contents +/atom/movable/proc/lose_active_storage(datum/component/storage/source) + if(!HAS_TRAIT(src, TRAIT_ACTIVE_STORAGE)) + return + REMOVE_TRAIT(src, TRAIT_ACTIVE_STORAGE, REF(source)) + if(HAS_TRAIT(src, TRAIT_ACTIVE_STORAGE)) + return + + for(var/atom/movable/location as anything in get_nested_locs(src) + src) + LAZYREMOVEASSOC(location.important_recursive_contents, RECURSIVE_CONTENTS_ACTIVE_STORAGE, src) + +/////////////////////// + /atom/movable/Bump(atom/bumped_atom) if(!bumped_atom) CRASH("Bump was called with no argument.") diff --git a/code/game/machinery/bank_machine.dm b/code/game/machinery/bank_machine.dm index 43270c40efc2..0b2db0380df9 100644 --- a/code/game/machinery/bank_machine.dm +++ b/code/game/machinery/bank_machine.dm @@ -16,6 +16,7 @@ radio = new(src) radio.subspace_transmission = TRUE radio.canhear_range = 0 + radio.set_listening(FALSE) radio.recalculateChannels() /obj/machinery/computer/bank_machine/Destroy() diff --git a/code/game/machinery/computer/security.dm b/code/game/machinery/computer/security.dm index d9c82157eaf8..e82d4404c8e6 100644 --- a/code/game/machinery/computer/security.dm +++ b/code/game/machinery/computer/security.dm @@ -37,7 +37,7 @@ radio = new(src) radio.keyslot = new radio_key radio.subspace_transmission = TRUE - radio.listening = FALSE + radio.set_listening(FALSE) radio.use_command = TRUE radio.independent = TRUE radio.recalculateChannels() diff --git a/code/game/machinery/dance_machine.dm b/code/game/machinery/dance_machine.dm index 0216151d09fb..9410bf95f213 100644 --- a/code/game/machinery/dance_machine.dm +++ b/code/game/machinery/dance_machine.dm @@ -183,7 +183,7 @@ if(!L || !L.client) continue // it doesn't send at 0 volume so you get 0.001 volume on init - L.playsound_local(get_turf(L), null, 0.001, channel = CHANNEL_JUKEBOX, S = song_played) + L.playsound_local(get_turf(L), null, 0.001, channel = CHANNEL_JUKEBOX, sound_to_use = song_played) if(L in close && L.client.prefs.toggles & SOUND_JUKEBOX) L.set_sound_channel_volume(CHANNEL_JUKEBOX, volume) // TURN THAT SHIT UP!!!! else diff --git a/code/game/machinery/doors/brigdoors.dm b/code/game/machinery/doors/brigdoors.dm index fe12396a1ff7..4871b2ab8cef 100644 --- a/code/game/machinery/doors/brigdoors.dm +++ b/code/game/machinery/doors/brigdoors.dm @@ -83,7 +83,7 @@ . = ..() Radio = new/obj/item/radio(src) - Radio.listening = 0 + Radio.set_listening(FALSE) /obj/machinery/door_timer/Initialize(mapload) . = ..() diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm index 04711dc951be..7f0a6682a15d 100644 --- a/code/game/machinery/doors/door.dm +++ b/code/game/machinery/doors/door.dm @@ -120,6 +120,7 @@ qdel(spark_system) spark_system = null air_update_turf() + lose_hearing_sensitivity() // used by password door, hopefully clears return ..() /obj/machinery/door/Bumped(atom/movable/AM) diff --git a/code/game/machinery/doors/passworddoor.dm b/code/game/machinery/doors/passworddoor.dm index 29e5deab4a64..385976d0e402 100644 --- a/code/game/machinery/doors/passworddoor.dm +++ b/code/game/machinery/doors/passworddoor.dm @@ -69,9 +69,9 @@ /obj/machinery/door/password/Initialize(mapload) . = ..() if(voice_activated) - flags_1 |= HEAR_1 + become_hearing_sensitive() -/obj/machinery/door/password/Hear(message, atom/movable/speaker, message_language, raw_message, radio_freq, list/spans, list/message_mods = list()) +/obj/machinery/door/password/Hear(message, atom/movable/speaker, message_language, raw_message, radio_freq, list/spans, list/message_mods = list(), message_range) . = ..() if(!density || !voice_activated || radio_freq) return @@ -104,13 +104,13 @@ switch(animation) if("opening") flick("opening", src) - playsound(src, 'sound/machines/blastdoor.ogg', 30, 1) + playsound(src, 'sound/machines/blastdoor.ogg', 30, TRUE) if("closing") flick("closing", src) - playsound(src, 'sound/machines/blastdoor.ogg', 30, 1) + playsound(src, 'sound/machines/blastdoor.ogg', 30, TRUE) if("deny") //Deny animation would be nice to have. - playsound(src, 'sound/machines/buzz-sigh.ogg', 30, 1) + playsound(src, 'sound/machines/buzz-sigh.ogg', 30, TRUE) /obj/machinery/door/password/proc/ask_for_pass(mob/user) var/guess = stripped_input(user,"Enter the password:", "Password", "") diff --git a/code/game/machinery/hologram.dm b/code/game/machinery/hologram.dm index 2df3aadcc8a4..f3b0257ada65 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 @@ -35,7 +40,6 @@ GLOBAL_LIST_EMPTY(holopads) icon_state = "holopad0" layer = LOW_OBJ_LAYER plane = FLOOR_PLANE - flags_1 = HEAR_1 req_access = list(ACCESS_KEYCARD_AUTH) //Used to allow for forced connecting to other (not secure) holopads. Anyone can make a call, though. use_power = IDLE_POWER_USE idle_power_usage = 5 @@ -43,7 +47,8 @@ GLOBAL_LIST_EMPTY(holopads) max_integrity = 300 armor = list(MELEE = 50, BULLET = 20, LASER = 20, ENERGY = 20, BOMB = 0, BIO = 0, RAD = 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 @@ -84,6 +89,8 @@ GLOBAL_LIST_EMPTY(holopads) var/padname = null /// Holopad Harassment Cooldown var/holopad_cooldown = 20 SECONDS + ///bitfield. used to turn on and off hearing sensitivity depending on if we can act on Hear() at all - meant for lowering the number of unessesary hearable atoms + var/can_hear_flags = NONE /obj/machinery/holopad/secure name = "secure holopad" @@ -153,9 +160,8 @@ obj/machinery/holopad/secure/Initialize(mapload) if(outgoing_call) outgoing_call.ConnectionFailure(src) - for(var/I in holo_calls) - var/datum/holocall/HC = I - HC.ConnectionFailure(src) + for(var/datum/holocall/holocall_to_disconnect as anything in holo_calls) + holocall_to_disconnect.ConnectionFailure(src) for (var/I in masters) clear_holo(I) @@ -357,13 +363,59 @@ obj/machinery/holopad/secure/Initialize(mapload) outgoing_call.Disconnect(src) return TRUE + +//setters +/** + * setter for can_hear_flags. handles adding or removing the given flag on can_hear_flags and then adding hearing sensitivity or removing it depending on the final state + * this is necessary because holopads are a significant fraction of the hearable atoms on station which increases the cost of procs that iterate through hearables + * so we need holopads to not be hearable until it is needed + * + * * flag - one of the can_hear_flags flag defines + * * set_flag - boolean, if TRUE sets can_hear_flags to that flag and might add hearing sensitivity if can_hear_flags was NONE before, + * if FALSE unsets the flag and possibly removes hearing sensitivity + */ +/obj/machinery/holopad/proc/set_can_hear_flags(flag, set_flag = TRUE) + if(!(flag & CAN_HEAR_ALL_FLAGS)) + return FALSE //the given flag doesnt exist + + if(set_flag) + if(can_hear_flags == NONE)//we couldnt hear before, so become hearing sensitive + become_hearing_sensitive() + + can_hear_flags |= flag + return TRUE + + else + can_hear_flags &= ~flag + if(can_hear_flags == NONE) + lose_hearing_sensitivity() + + return TRUE + +///setter for adding/removing holocalls to this holopad. used to update the holo_calls list and can_hear_flags +///adds the given holocall if add_holocall is TRUE, removes if FALSE +/obj/machinery/holopad/proc/set_holocall(datum/holocall/holocall_to_update, add_holocall = TRUE) + if(!istype(holocall_to_update)) + return FALSE + + if(add_holocall) + set_can_hear_flags(CAN_HEAR_ACTIVE_HOLOCALLS) + LAZYADD(holo_calls, holocall_to_update) + + else + LAZYREMOVE(holo_calls, holocall_to_update) + if(!LAZYLEN(holo_calls)) + set_can_hear_flags(CAN_HEAR_ACTIVE_HOLOCALLS, FALSE) + + return TRUE + + /** * hangup_all_calls: Disconnects all current holocalls from the holopad */ /obj/machinery/holopad/proc/hangup_all_calls() - for(var/I in holo_calls) - var/datum/holocall/HC = I - HC.Disconnect(src) + for(var/datum/holocall/holocall_to_disconnect as anything in holo_calls) + holocall_to_disconnect.Disconnect(src) //do not allow AIs to answer calls or people will use it to meta the AI sattelite /obj/machinery/holopad/attack_ai(mob/living/silicon/ai/user) @@ -461,10 +513,12 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ if(masters[master] && speaker != master) master.relay_speech(message, speaker, message_language, raw_message, radio_freq, spans, message_mods) - for(var/I in holo_calls) - var/datum/holocall/HC = I - if(HC.connected_holopad == src && speaker != HC.hologram) - HC.user.Hear(message, speaker, message_language, raw_message, radio_freq, spans, message_mods) + for(var/datum/holocall/holocall_to_update as anything in holo_calls) + if(holocall_to_update.connected_holopad == src)//if we answered this call originating from another holopad + if(speaker == holocall_to_update.hologram && holocall_to_update.user.client?.prefs.read_preference(/datum/preference/toggle/enable_runechat)) + holocall_to_update.user.create_chat_message(speaker, message_language, raw_message, spans) + else + holocall_to_update.user.Hear(message, speaker, message_language, raw_message, radio_freq, spans, message_mods) if(outgoing_call && speaker == outgoing_call.user) outgoing_call.hologram.say(raw_message) @@ -495,6 +549,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ /obj/machinery/holopad/proc/set_holo(mob/living/user, obj/effect/overlay/holo_pad_hologram/h) LAZYSET(masters, user, h) LAZYSET(holorays, user, new /obj/effect/overlay/holoray(loc)) + set_can_hear_flags(CAN_HEAR_MASTERS) var/mob/living/silicon/ai/AI = user if(istype(AI)) AI.current = src @@ -515,6 +570,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() @@ -634,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) @@ -703,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 && disk.record) @@ -745,3 +804,7 @@ For the other part of the code, check silicon say.dm. Particularly robot talk.*/ #undef HOLOPAD_PASSIVE_POWER_USAGE #undef HOLOGRAM_POWER_USAGE +#undef CAN_HEAR_MASTERS +#undef CAN_HEAR_ACTIVE_HOLOCALLS +#undef CAN_HEAR_RECORD_MODE +#undef CAN_HEAR_ALL_FLAGS diff --git a/code/game/machinery/requests_console.dm b/code/game/machinery/requests_console.dm index 94b7c67392f8..c5b683c8ce37 100644 --- a/code/game/machinery/requests_console.dm +++ b/code/game/machinery/requests_console.dm @@ -115,7 +115,7 @@ GLOBAL_LIST_EMPTY(req_console_ckey_departments) GLOB.req_console_ckey_departments[ckey(department)] = department Radio = new /obj/item/radio(src) - Radio.listening = 0 + Radio.set_listening(FALSE) /obj/machinery/requests_console/Destroy() QDEL_NULL(Radio) diff --git a/code/game/machinery/sci_bombardment.dm b/code/game/machinery/sci_bombardment.dm index e7f49447f8a9..fa6adcef310f 100644 --- a/code/game/machinery/sci_bombardment.dm +++ b/code/game/machinery/sci_bombardment.dm @@ -52,7 +52,7 @@ lavaland = Z break radio = new /obj/item/radio/(src) - radio.frequency = radio_freq + radio.set_frequency(radio_freq) update_appearance() /obj/machinery/sci_bombardment/Destroy() diff --git a/code/game/machinery/telecomms/broadcasting.dm b/code/game/machinery/telecomms/broadcasting.dm index 134b526c9ed1..20bf6e70594f 100644 --- a/code/game/machinery/telecomms/broadcasting.dm +++ b/code/game/machinery/telecomms/broadcasting.dm @@ -146,40 +146,48 @@ if(compression > 0) message = Gibberish(message, compression + 40) + var/list/signal_reaches_every_z_level = levels + + if(0 in levels) + signal_reaches_every_z_level = RADIO_NO_Z_LEVEL_RESTRICTION + // Assemble the list of radios var/list/radios = list() switch (transmission_method) if (TRANSMISSION_SUBSPACE) // Reaches any radios on the levels - for(var/obj/item/radio/R in GLOB.all_radios["[frequency]"]) - if(R.can_receive(frequency, levels)) - radios += R + var/list/all_radios_of_our_frequency = GLOB.all_radios["[frequency]"] + radios = all_radios_of_our_frequency.Copy() + + for(var/obj/item/radio/subspace_radio in radios) + if(!subspace_radio.can_receive(frequency, signal_reaches_every_z_level)) + radios -= subspace_radio // Syndicate radios can hear all well-known radio channels if (num2text(frequency) in GLOB.reverseradiochannels) - for(var/obj/item/radio/R in GLOB.all_radios["[FREQ_SYNDICATE]"]) - if(R.can_receive(FREQ_SYNDICATE, list(R.z))) - radios |= R + for(var/obj/item/radio/syndicate_radios in GLOB.all_radios["[FREQ_SYNDICATE]"]) + if(syndicate_radios.can_receive(FREQ_SYNDICATE, RADIO_NO_Z_LEVEL_RESTRICTION)) + radios |= syndicate_radios if (TRANSMISSION_RADIO) // Only radios not currently in subspace mode - for(var/obj/item/radio/R in GLOB.all_radios["[frequency]"]) - if(!R.subspace_transmission && R.can_receive(frequency, levels)) - radios += R + for(var/obj/item/radio/non_subspace_radio in GLOB.all_radios["[frequency]"]) + if(!non_subspace_radio.subspace_transmission && non_subspace_radio.can_receive(frequency, 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) // Cut out mobs with clients who are admins and have radio chatter disabled. - for(var/mob/R in receive) - if (R.client && R.client.holder && !(R.client.prefs.chat_toggles & CHAT_RADIO)) - receive -= R + for(var/mob/dead/observer/ghost in GLOB.player_list) + if(ghost.client.prefs?.chat_toggles & CHAT_GHOSTRADIO) + receive |= ghost // Add observers who have ghost radio enabled. for(var/mob/dead/observer/M in GLOB.player_list) @@ -191,7 +199,10 @@ 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 @@ -209,7 +220,7 @@ var/log_text = "\[[get_radio_name(frequency)]\] [spans_part]\"[message]\" (language: [lang_name])" var/mob/source_mob = virt.source - if(istype(source_mob)) + if(ismob(source_mob)) source_mob.log_message(log_text, LOG_TELECOMMS) else log_telecomms("[virt.source] [log_text] [loc_name(get_turf(virt.source))]") diff --git a/code/game/machinery/telecomms/machine_interactions.dm b/code/game/machinery/telecomms/machine_interactions.dm index 73ceb93dc91e..3c8b6f942b2a 100644 --- a/code/game/machinery/telecomms/machine_interactions.dm +++ b/code/game/machinery/telecomms/machine_interactions.dm @@ -116,7 +116,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].") @@ -141,12 +141,8 @@ 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/tcomms_machine = multitool_get_buffer(src, heldmultitool) @@ -156,7 +152,7 @@ if(!(tcomms_machine in links)) links += tcomms_machine log_game("[key_name(operator)] linked [src] for [tcomms_machine] at [AREACOORD(src)].") - . = TRUE + if("buffer") // Yogs start -- holotool support if(heldmultitool) multitool_set_buffer(usr, heldmultitool, src) @@ -169,6 +165,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 5b34915ff5a1..8c44ca9a2067 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 = "caster" 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 69e59ab1ca38..27873982abca 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 dedf7c7f3a77..290cb094e7dd 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 b3103415f3c5..7411966f64cb 100644 --- a/code/game/machinery/telecomms/machines/message_server.dm +++ b/code/game/machinery/telecomms/machines/message_server.dm @@ -77,6 +77,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 2362273469a0..02db67489711 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 ea01e720e84f..25a0c29b120d 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 = "caster" 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 49747a37587f..4a7d4c40b4c8 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 diff --git a/code/game/machinery/telecomms/machines/server.dm b/code/game/machinery/telecomms/machines/server.dm index e173da01235c..2e5d629e83f7 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 = "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 a9d31fb3f18b..f511db60f4ab 100644 --- a/code/game/machinery/telecomms/telecomunications.dm +++ b/code/game/machinery/telecomms/telecomunications.dm @@ -18,36 +18,53 @@ GLOBAL_LIST_EMPTY(telecomms_list) name = "telecommunications machine" icon = 'icons/obj/machines/telecomms.dmi' critical_machine = TRUE - var/list/links = list() // list of machines this machine is linked to - var/traffic = 0 // value increases as traffic increases - var/netspeed = 2.5 // how much traffic to lose per second (50 gigabytes/second * netspeed) - var/net_efective = 100 //yogs percentage of netspeed aplied - var/list/autolinkers = list() // list of text/number values to link with - var/id = "NULL" // identification string - var/network = "NULL" // the network of the machinery - - var/list/freq_listening = list() // list of frequencies to tune into: if none, will listen to all + /// list of machines this machine is linked to + var/list/links = list() + /** + * associative lazylist list of the telecomms_type of linked telecomms machines and a list of said machines. + * eg list(telecomms_type1 = list(everything linked to us with that type), telecomms_type2 = list(everything linked to us with THAT type)...) + */ + var/list/links_by_telecomms_type + /// value increases as traffic increases + var/traffic = 0 + /// how much traffic to lose per second (50 gigabytes/second * netspeed) + var/netspeed = 2.5 + /// list of text/number values to link with + var/list/autolinkers = list() + /// identification string + var/id = "NULL" + /// the relevant type path of this telecomms machine eg /obj/machinery/telecomms/server but not server/preset. used for links_by_telecomms_type + var/telecomms_type = null + /// the network of the machinery + var/network = "NULL" + + // list of frequencies to tune into: if none, will listen to all + var/list/freq_listening = list() var/on = TRUE - var/toggled = TRUE // Is it toggled on - var/long_range_link = FALSE // Can you link it across Z levels or on the otherside of the map? (Relay & Hub) + /// Is it toggled on + var/toggled = TRUE + /// Can you link it across Z levels or on the otherside of the map? (Relay & Hub) + var/long_range_link = FALSE /// Is it a hidden machine? var/hide = FALSE + var/net_efective = 100 //yogs percentage of netspeed aplied var/generates_heat = TRUE //yogs turn off tcomms generating heat var/heatoutput = 2500 //yogs modify power output per trafic removed(usual heat capacity of the air in server room is 1600J/K) var/on_icon = "" // used for special cases broadcaster/reciever + ///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)) // Yogs -- for debugging telecomms later when I soop up NTSL some more - CRASH("relay_information() was given a path filter that wasn't actually a path!") + if(!filter || !ispath(filter, /obj/machinery/telecomms)) + CRASH("null or non /obj/machinery/telecomms typepath given as the filter argument! given typepath: [filter]") var/send_count = 0 // Apply some lag based on traffic rates @@ -56,25 +73,22 @@ GLOBAL_LIST_EMPTY(telecomms_list) signal.data["slow"] = netlag // Loop through all linked machines and send the signal or copy. - for(var/m_typeless in links) // Yogs -- God bless typeless for-loops - var/obj/machinery/telecomms/machine = m_typeless - if(filter && !istype(machine, filter) ) - continue - if(!machine.on) + for(var/obj/machinery/telecomms/filtered_machine in links_by_telecomms_type?[filter]) + if(!filtered_machine.on) continue if(amount && send_count >= amount) break - if(z != machine.loc.z && !long_range_link && !machine.long_range_link) + if(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++ @@ -85,12 +99,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) . = ..() @@ -101,25 +116,27 @@ GLOBAL_LIST_EMPTY(telecomms_list) /obj/machinery/telecomms/LateInitialize() ..() for(var/obj/machinery/telecomms/T in (long_range_link ? GLOB.telecomms_list : urange(20, src, 1))) - add_link(T) + add_automatic_link(T) /obj/machinery/telecomms/Destroy() GLOB.telecomms_list -= src for(var/obj/machinery/telecomms/comm in GLOB.telecomms_list) - comm.links -= src + remove_link(comm) links = list() return ..() // Used in auto linking -/obj/machinery/telecomms/proc/add_link(obj/machinery/telecomms/T) +/obj/machinery/telecomms/proc/add_automatic_link(obj/machinery/telecomms/T) var/turf/position = get_turf(src) var/turf/T_position = get_turf(T) - if((position.z == T_position.z) || (long_range_link && T.long_range_link)) - if(src != T) - for(var/x in autolinkers) - if(x in T.autolinkers) - links |= T - T.links |= src + if((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() diff --git a/code/game/mecha/mecha.dm b/code/game/mecha/mecha.dm index d412c0c1adfe..342a99c0b0c2 100644 --- a/code/game/mecha/mecha.dm +++ b/code/game/mecha/mecha.dm @@ -12,7 +12,6 @@ light_system = MOVABLE_LIGHT_DIRECTIONAL light_range = 8 light_on = FALSE - flags_1 = HEAR_1 demolition_mod = 3 // mech punch go brr var/weather_protection = WEATHER_STORM var/ruin_mecha = FALSE //if the mecha starts on a ruin, don't automatically give it a tracking beacon to prevent metagaming. @@ -171,6 +170,7 @@ diag_hud_set_mechoverheat() RegisterSignal(src, COMSIG_LIGHT_EATER_ACT, PROC_REF(on_light_eater)) ADD_TRAIT(src, TRAIT_SHIELDBUSTER, INNATE_TRAIT) // previously it didn't even check shields at all, now it still doesn't but does some fun stuff in the process + become_hearing_sensitive() /// Special light eater handling /obj/mecha/proc/on_light_eater(obj/vehicle/sealed/source, datum/light_eater) @@ -203,6 +203,7 @@ else M.forceMove(loc) force_eject_occupant() + lose_hearing_sensitivity() for(var/obj/item/mecha_parts/mecha_equipment/E in equipment) E.detach(loc) qdel(E) @@ -537,7 +538,7 @@ /obj/mecha/Hear(message, atom/movable/speaker, message_language, raw_message, radio_freq, list/spans, list/message_mods = list()) . = ..() if(speaker == occupant) - if(radio?.broadcasting) + if(radio?.get_broadcasting()) radio.talk_into(speaker, text, , spans, message_language, message_mods) //flick speech bubble var/list/speech_bubble_recipients = list() diff --git a/code/game/mecha/mecha_topic.dm b/code/game/mecha/mecha_topic.dm index 820894348fe4..76ebca8ca3dc 100644 --- a/code/game/mecha/mecha_topic.dm +++ b/code/game/mecha/mecha_topic.dm @@ -116,12 +116,12 @@
Electronics
@@ -332,19 +332,22 @@ send_byjax(usr,"exosuit.browser","eq_list",src.get_equipment_list()) 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 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 if(href_list["rfreq"]) - var/new_frequency = (radio.frequency + text2num(href_list["rfreq"])) - if (!radio.freerange || (radio.frequency < MIN_FREE_FREQ || radio.frequency > MAX_FREE_FREQ)) + var/new_frequency = (radio.get_frequency() + text2num(href_list["rfreq"])) + if (!radio.freerange || (radio.get_frequency() < MIN_FREE_FREQ || radio.get_frequency() > MAX_FREE_FREQ)) new_frequency = sanitize_frequency(new_frequency) radio.set_frequency(new_frequency) - send_byjax(usr,"exosuit.browser","rfreq","[format_frequency(radio.frequency)]") + send_byjax(usr,"exosuit.browser","rfreq","[format_frequency(radio.get_frequency())]") + return if (href_list["change_name"]) var/userinput = stripped_input(occupant, "Choose new exosuit name", "Rename exosuit", "", MAX_NAME_LEN) diff --git a/code/game/mecha/working/clarke.dm b/code/game/mecha/working/clarke.dm index 9268b98d2ec8..cf49ac27e116 100644 --- a/code/game/mecha/working/clarke.dm +++ b/code/game/mecha/working/clarke.dm @@ -14,7 +14,7 @@ facing_modifiers = list(FRONT_ARMOUR = 1.2, SIDE_ARMOUR = 1, BACK_ARMOUR = 0.8) // omnidirectional, less significant difference between attack directions light_power = 7 deflect_chance = 10 - flags_1 = HEAR_1 | RAD_PROTECT_CONTENTS_1 | RAD_NO_CONTAMINATE_1 + flags_1 = RAD_PROTECT_CONTENTS_1 | RAD_NO_CONTAMINATE_1 armor = list(MELEE = 25, BULLET = 10, LASER = 20, ENERGY = 0, BOMB = 60, BIO = 0, RAD = 100, FIRE = 100, ACID = 100) max_equip = 7 enter_delay = 40 @@ -29,9 +29,11 @@ var/obj/item/mecha_parts/mecha_equipment/orebox_manager/ME = new(src) ME.attach(src) AddComponent(/datum/component/armor_plate, 5, /obj/item/stack/sheet/animalhide/weaver_chitin, list(MELEE = 5, BULLET = 2, LASER = 2)) + become_hearing_sensitive() /obj/mecha/working/clarke/Destroy() box.dump_box_contents() + lose_hearing_sensitivity() return ..() /obj/mecha/working/clarke/moved_inside(mob/living/carbon/human/H) diff --git a/code/game/mecha/working/ripley.dm b/code/game/mecha/working/ripley.dm index fad104b44617..7237e4fa1e83 100644 --- a/code/game/mecha/working/ripley.dm +++ b/code/game/mecha/working/ripley.dm @@ -32,6 +32,7 @@ A.forceMove(drop_location()) step_rand(A) cargo.Cut() + lose_hearing_sensitivity() return ..() /obj/mecha/working/ripley/go_out() @@ -61,6 +62,7 @@ /obj/mecha/working/ripley/Initialize(mapload) . = ..() AddComponent(/datum/component/armor_plate,3,/obj/item/stack/sheet/animalhide/goliath_hide,list(MELEE = 10, BULLET = 5, LASER = 5)) + become_hearing_sensitive() /obj/mecha/working/ripley/firefighter desc = "Autonomous Power Loader Unit MK-II. This model is fitted with a pressurized cabin and thermal protection." @@ -74,7 +76,7 @@ step_in = 3 resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF weather_protection = WEATHER_LAVA|WEATHER_STORM - flags_1 = HEAR_1 | RAD_PROTECT_CONTENTS_1 | RAD_NO_CONTAMINATE_1 + flags_1 = RAD_PROTECT_CONTENTS_1 | RAD_NO_CONTAMINATE_1 armor = list(MELEE = 40, BULLET = 30, LASER = 30, ENERGY = 0, BOMB = 60, BIO = 100, RAD = 100, FIRE = 100, ACID = 100) enclosed = TRUE enter_delay = 40 diff --git a/code/game/objects/items/devices/radio/headset.dm b/code/game/objects/items/devices/radio/headset.dm index c7906d086a7a..d6b809f0299f 100644 --- a/code/game/objects/items/devices/radio/headset.dm +++ b/code/game/objects/items/devices/radio/headset.dm @@ -16,21 +16,19 @@ /obj/item/radio/headset/Initialize(mapload) . = ..() + set_listening(TRUE) recalculateChannels() + possibly_deactivate_in_loc() -/obj/item/radio/headset/talk_into(mob/living/M, message, channel, list/spans, datum/language/language, list/message_mods) - if (!listening) - return ITALICS | REDUCE_RANGE - return ..() - -/obj/item/radio/headset/can_receive(freq, level, AIuser) - if(ishuman(src.loc)) - var/mob/living/carbon/human/H = src.loc - if(H.ears == src) - return ..(freq, level) - else if(AIuser) - return ..(freq, level) - return FALSE +/obj/item/radio/headset/proc/possibly_deactivate_in_loc() + if(ismob(loc)) + set_listening(should_be_listening) + else + set_listening(FALSE, actual_setting = FALSE) + +/obj/item/radio/headset/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE) + . = ..() + possibly_deactivate_in_loc() /obj/item/radio/headset/ui_data(mob/user) . = ..() diff --git a/code/game/objects/items/devices/radio/intercom.dm b/code/game/objects/items/devices/radio/intercom.dm index 5291fe6b1157..b965c327d4f8 100644 --- a/code/game/objects/items/devices/radio/intercom.dm +++ b/code/game/objects/items/devices/radio/intercom.dm @@ -97,17 +97,11 @@ /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(!src.listening) - return FALSE if(freq == FREQ_SYNDICATE) if(!(src.syndie)) return FALSE//Prevents broadcast of messages over devices lacking the encryption @@ -155,3 +149,12 @@ pixel_shift = 29 inverse = TRUE materials = list(/datum/material/iron = 75, /datum/material/glass = 25) + +/obj/item/radio/intercom/chapel + name = "Confessional intercom" + anonymize = TRUE + +/obj/item/radio/intercom/chapel/Initialize(mapload, ndir, building) + . = ..() + set_frequency(1481) + set_broadcasting(TRUE) diff --git a/code/game/objects/items/devices/radio/radio.dm b/code/game/objects/items/devices/radio/radio.dm index edbce19ab02b..1f406a07280b 100644 --- a/code/game/objects/items/devices/radio/radio.dm +++ b/code/game/objects/items/devices/radio/radio.dm @@ -22,7 +22,7 @@ GLOBAL_LIST_INIT(channel_tokens, list( desc = "A basic handheld radio that communicates with local telecommunication networks. Altclick to remove encryption key if panel is open." dog_fashion = /datum/dog_fashion/back - flags_1 = CONDUCT_1 | HEAR_1 + flags_1 = CONDUCT_1 slot_flags = ITEM_SLOT_BELT throw_speed = 3 throw_range = 7 @@ -30,39 +30,90 @@ GLOBAL_LIST_INIT(channel_tokens, list( materials = list(/datum/material/iron=75, /datum/material/glass=25) obj_flags = USES_TGUI - var/on = TRUE - var/frequency = FREQ_COMMON - var/canhear_range = 3 // The range around the radio in which mobs can hear what it receives. - var/emped = 0 // Tracks the number of EMPs currently stacked. - - var/broadcasting = FALSE // Whether the radio will transmit dialogue it hears nearby. - var/listening = TRUE // Whether the radio is currently receiving. - var/prison_radio = FALSE // If true, the transmit wire starts cut. - var/unscrewed = FALSE // Whether wires are accessible. Toggleable by screwdrivering. - var/freerange = FALSE // If true, the radio has access to the full spectrum. - var/subspace_transmission = FALSE // If true, the radio transmits and receives on subspace exclusively. - var/subspace_switchable = FALSE // If true, subspace_transmission can be toggled at will. - var/freqlock = FALSE // Frequency lock to stop the user from untuning specialist radios. - var/use_command = FALSE // If true, broadcasts will be large and BOLD. - var/command = FALSE // If true, use_command can be toggled at will. - - // Encryption key handling + ///if FALSE, broadcasting and listening dont matter and this radio shouldnt do anything + VAR_PRIVATE/on = TRUE + ///the "default" radio frequency this radio is set to, listens and transmits to this frequency by default. wont work if the channel is encrypted + VAR_PRIVATE/frequency = FREQ_COMMON + + /// Whether the radio will transmit dialogue it hears nearby into its radio channel. + VAR_PRIVATE/broadcasting = FALSE + /// Whether the radio is currently receiving radio messages from its radio frequencies. + VAR_PRIVATE/listening = TRUE + + //the below three vars are used to track listening and broadcasting should they be forced off for whatever reason but "supposed" to be active + //eg player sets the radio to listening, but an emp or whatever turns it off, its still supposed to be activated but was forced off, + //when it wears off it sets listening to should_be_listening + + ///used for tracking what broadcasting should be in the absence of things forcing it off, eg its set to broadcast but gets emp'd temporarily + var/should_be_broadcasting = FALSE + ///used for tracking what listening should be in the absence of things forcing it off, eg its set to listen but gets emp'd temporarily + var/should_be_listening = TRUE + + /// Both the range around the radio in which mobs can hear what it receives and the range the radio can hear + var/canhear_range = 3 + /// Tracks the number of EMPs currently stacked. + var/emped = 0 + + /// If true, the transmit wire starts cut. + var/prison_radio = FALSE + /// Whether wires are accessible. Toggleable by screwdrivering. + var/unscrewed = FALSE + /// If true, the radio has access to the full spectrum. + var/freerange = FALSE + /// If true, the radio transmits and receives on subspace exclusively. + var/subspace_transmission = FALSE + /// If true, subspace_transmission can be toggled at will. + var/subspace_switchable = FALSE + /// Frequency lock to stop the user from untuning specialist radios. + var/freqlock = FALSE + /// If true, broadcasts will be large and BOLD. + var/use_command = FALSE + /// If true, use_command can be toggled at will. + var/command = FALSE + + ///makes anyone who is talking through this anonymous. + var/anonymize = FALSE + + /// Encryption key handling var/obj/item/encryptionkey/keyslot var/obj/item/encryptionkey/keyslot2 - var/translate_binary = FALSE // If true, can hear the special binary channel. - var/independent = FALSE // If true, can say/hear on the special CentCom channel. - var/syndie = FALSE // If true, hears all well-known channels automatically, and can say/hear on the Syndicate channel. - var/list/channels = list() // Map from name (see communications.dm) to on/off. First entry is current department (:h). + /// If 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 var/list/radio_sounds = list('yogstation/sound/effects/radio1.ogg','yogstation/sound/effects/radio2.ogg','yogstation/sound/effects/radio3.ogg') var/const/FREQ_LISTENING = 1 //FREQ_BROADCASTING = 2 -/obj/item/radio/suicide_act(mob/living/user) - talk_into(user, pick_list_replacements(BRAIN_DAMAGE_FILE, "brain_damage"), null, SPAN_COMMAND) - use_command = TRUE // converts the radio in to use LOUD per poll. - return OXYLOSS // you die from oxygen loss by yelling the brain damage line at full volume +/obj/item/radio/Initialize(mapload) + wires = new /datum/wires/radio(src) + if(prison_radio) + wires.cut(WIRE_TX) // 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) + + +/obj/item/radio/Destroy() + remove_radio_all(src) //Just to be sure + QDEL_NULL(wires) + QDEL_NULL(keyslot) + QDEL_NULL(keyslot2) + return ..() /obj/item/radio/proc/set_frequency(new_frequency) SEND_SIGNAL(src, COMSIG_RADIO_NEW_FREQUENCY, args) @@ -102,6 +153,7 @@ GLOBAL_LIST_INIT(channel_tokens, list( // Used for cyborg override /obj/item/radio/proc/resetChannels() channels = list() + secure_radio_connections = list() translate_binary = FALSE syndie = FALSE independent = FALSE @@ -109,29 +161,9 @@ GLOBAL_LIST_INIT(channel_tokens, list( /obj/item/radio/proc/make_syndie() // Turns normal radios into Syndicate radios! qdel(keyslot) keyslot = new /obj/item/encryptionkey/syndicate - syndie = 1 + syndie = TRUE recalculateChannels() -/obj/item/radio/Destroy() - remove_radio_all(src) //Just to be sure - QDEL_NULL(wires) - QDEL_NULL(keyslot) - QDEL_NULL(keyslot2) - return ..() - -/obj/item/radio/Initialize(mapload) - wires = new /datum/wires/radio(src) - if(prison_radio) - wires.cut(WIRE_TX) // OH GOD WHY - secure_radio_connections = new - . = ..() - ADD_TRAIT(src, TRAIT_EMPPROOF_CONTENTS, "innate_empproof") - frequency = sanitize_frequency(frequency, freerange) - set_frequency(frequency) - - for(var/ch_name in channels) - secure_radio_connections[ch_name] = add_radio(src, GLOB.radiochannels[ch_name]) - /obj/item/radio/interact(mob/user) if(unscrewed && !isAI(user)) wires.interact(user) @@ -139,103 +171,106 @@ GLOBAL_LIST_INIT(channel_tokens, list( else ..() -/obj/item/radio/ui_state(mob/user) - return GLOB.inventory_state +//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 -/obj/item/radio/ui_interact(mob/user, datum/tgui/ui, datum/ui_state/state) - ui = SStgui.try_update_ui(user, src, ui) - if(!ui) - ui = new(user, src, "Radio", name) - if(state) - ui.set_state(state) - ui.open() +///simple getter for the on variable. necessary due to VAR_PROTECTED +/obj/item/radio/proc/is_on() + return on -/obj/item/radio/ui_data(mob/user) - var/list/data = list() +///simple getter for the frequency variable. necessary due to VAR_PROTECTED +/obj/item/radio/proc/get_frequency() + return frequency - data["broadcasting"] = broadcasting - data["listening"] = listening - data["frequency"] = frequency - data["minFrequency"] = freerange ? MIN_FREE_FREQ : MIN_FREQ - data["maxFrequency"] = freerange ? MAX_FREE_FREQ : MAX_FREQ - data["freqlock"] = freqlock - data["channels"] = list() - for(var/channel in channels) - data["channels"][channel] = channels[channel] & FREQ_LISTENING - data["command"] = command - data["useCommand"] = use_command - data["subspace"] = subspace_transmission - data["subspaceSwitchable"] = subspace_switchable - data["headset"] = FALSE +///simple getter for the broadcasting variable. necessary due to VAR_PROTECTED +/obj/item/radio/proc/get_broadcasting() + return broadcasting - return data +///simple getter for the listening variable. necessary due to VAR_PROTECTED +/obj/item/radio/proc/get_listening() + return listening -/obj/item/radio/ui_act(action, params, datum/tgui/ui) - if(..()) - return - switch(action) - if("frequency") - if(freqlock) - return - var/tune - var/adjust = text2num(params["adjust"]) - adjust -= frequency / 10 +//now for setters for the above protected vars - if(adjust) - tune = frequency + adjust * 10 - . = TRUE - else if(text2num(tune) != null) - tune = tune * 10 - . = TRUE - if(.) - set_frequency(sanitize_frequency(tune, freerange)) - if("listen") - listening = !listening - . = TRUE - if("broadcast") - broadcasting = !broadcasting - . = TRUE - if("channel") - var/channel = params["channel"] - if(!(channel in channels)) - return - if(channels[channel] & FREQ_LISTENING) - channels[channel] &= ~FREQ_LISTENING - else - channels[channel] |= FREQ_LISTENING - . = TRUE - if("command") - use_command = !use_command - . = TRUE - if("subspace") - if(subspace_switchable) - subspace_transmission = !subspace_transmission - if(!subspace_transmission) - channels = list() - else - recalculateChannels() - . = TRUE +/** + * 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) -/obj/item/radio/talk_into(atom/movable/M, message, channel, list/spans, datum/language/language, list/message_mods) + 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 / if implemented uncomment +// var/mob/living/carbon/mute = talking_movable +// if(istype(mute)) +// var/obj/item/clothing/gloves/radio/G = mute.get_item_by_slot(ITEM_SLOT_GLOVES) +// if(!istype(G)) +// return FALSE +// switch(mute.check_signables_state()) +// if(SIGN_ONE_HAND) // One hand full +// message = stars(message) +// if(SIGN_HANDS_FULL to SIGN_CUFFED) +// return FALSE if(!spans) - spans = list(M.speech_span) + spans = list(talking_movable.speech_span) if(!language) - language = M.get_selected_language() - INVOKE_ASYNC(src, PROC_REF(talk_into_impl), M, message, channel, spans.Copy(), language, message_mods) + language = talking_movable.get_selected_language() + INVOKE_ASYNC(src, PROC_REF(talk_into_impl), talking_movable, message, channel, LAZYLISTDUPLICATE(spans), language, LAZYLISTDUPLICATE(message_mods)) return ITALICS | REDUCE_RANGE -/obj/item/radio/proc/talk_into_impl(atom/movable/M, message, channel, list/spans, datum/language/language, list/message_mods) +/obj/item/radio/proc/talk_into_impl(atom/movable/talking_movable, message, channel, list/spans, datum/language/language, list/message_mods) if(!on) return // the device has to be on - if(!M || !message) + if(!talking_movable || !message) return if(wires.is_cut(WIRE_TX)) // Permacell and otherwise tampered-with radios return - if(!M.IsVocal()) + if(!talking_movable.IsVocal()) return if(radio_sounds.len) //Sephora - Radios make small static sounds now. var/sound/radio_sound = pick(radio_sounds) - playsound(M.loc, radio_sound, 50, 1) + playsound(talking_movable.loc, radio_sound, 50, 1) if(use_command) spans |= SPAN_COMMAND @@ -264,13 +299,13 @@ GLOBAL_LIST_INIT(channel_tokens, list( // Nearby active jammers prevent the message from transmitting var/turf/position = get_turf(src) - for(var/obj/item/jammer/jammer in GLOB.active_jammers) + for(var/obj/item/jammer/jammer as anything in GLOB.active_jammers) var/turf/jammer_turf = get_turf(jammer) if(position.z == jammer_turf.z && (get_dist(position, jammer_turf) <= jammer.range)) return // Determine the identity information which will be attached to the signal. - var/atom/movable/virtualspeaker/speaker = new(null, M, src) + var/atom/movable/virtualspeaker/speaker = new(null, talking_movable, src) // Construct the signal var/datum/signal/subspace/vocal/signal = new(src, freq, speaker, language, message, spans, message_mods) @@ -279,7 +314,7 @@ GLOBAL_LIST_INIT(channel_tokens, list( if (independent && (freq == FREQ_CENTCOM || freq == FREQ_CTF_RED || freq == FREQ_CTF_BLUE)) signal.data["compression"] = 0 signal.transmission_method = TRANSMISSION_SUPERSPACE - signal.levels = list(0) // reaches all Z-levels + signal.levels = list(0) signal.broadcast() return @@ -319,29 +354,105 @@ GLOBAL_LIST_INIT(channel_tokens, list( talk_into(speaker, raw_message, , spans, language=message_language) // Checks if this radio can receive on the given frequency. -/obj/item/radio/proc/can_receive(freq, level) +/obj/item/radio/proc/can_receive(input_frequency, list/levels) // deny checks - if (!on || !listening || wires.is_cut(WIRE_RX)) - return FALSE - if (freq == FREQ_SYNDICATE && !syndie) - return FALSE - if (freq == FREQ_CENTCOM) - return independent // hard-ignores the z-level check - if (!(0 in level)) - var/turf/position = get_turf_global(src) // yogs - get_turf_global instead of get_turf - if(!position || !(position.z in level)) + if (levels != RADIO_NO_Z_LEVEL_RESTRICTION) + var/turf/position = get_turf(src) + if(!position || !(position.z in levels)) return FALSE + if (input_frequency == FREQ_SYNDICATE && !syndie) + return FALSE + // allow checks: are we listening on that frequency? - if (freq == frequency) + if (input_frequency == frequency) return TRUE for(var/ch_name in channels) if(channels[ch_name] & FREQ_LISTENING) - //the GLOB.radiochannels list is located in communications.dm - if(GLOB.radiochannels[ch_name] == text2num(freq) || syndie) + if(GLOB.radiochannels[ch_name] == text2num(input_frequency) || syndie) return TRUE return FALSE +/obj/item/radio/ui_state(mob/user) + return GLOB.inventory_state + +/obj/item/radio/ui_interact(mob/user, datum/tgui/ui, datum/ui_state/state) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Radio", name) + if(state) + ui.set_state(state) + ui.open() + +/obj/item/radio/ui_data(mob/user) + var/list/data = list() + + data["broadcasting"] = broadcasting + data["listening"] = listening + data["frequency"] = frequency + data["minFrequency"] = freerange ? MIN_FREE_FREQ : MIN_FREQ + data["maxFrequency"] = freerange ? MAX_FREE_FREQ : MAX_FREQ + data["freqlock"] = freqlock + data["channels"] = list() + for(var/channel in channels) + data["channels"][channel] = channels[channel] & FREQ_LISTENING + data["command"] = command + data["useCommand"] = use_command + data["subspace"] = subspace_transmission + data["subspaceSwitchable"] = subspace_switchable + data["headset"] = FALSE + + return data + +/obj/item/radio/ui_act(action, params, datum/tgui/ui) + . = ..() + if(.) + return + switch(action) + if("frequency") + if(freqlock) + return + var/tune = params["tune"] + var/adjust = text2num(params["adjust"]) + if(adjust) + tune = frequency + adjust * 10 + . = TRUE + else if(text2num(tune) != null) + tune = tune * 10 + . = TRUE + if(.) + set_frequency(sanitize_frequency(tune, freerange)) + if("listen") + set_listening(!listening) + . = TRUE + if("broadcast") + set_broadcasting(!broadcasting) + . = TRUE + if("channel") + var/channel = params["channel"] + if(!(channel in channels)) + return + if(channels[channel] & FREQ_LISTENING) + channels[channel] &= ~FREQ_LISTENING + else + channels[channel] |= FREQ_LISTENING + . = TRUE + if("command") + use_command = !use_command + . = TRUE + if("subspace") + if(subspace_switchable) + subspace_transmission = !subspace_transmission + if(!subspace_transmission) + channels = list() + else + recalculateChannels() + . = TRUE + +/obj/item/radio/suicide_act(mob/living/user) + user.visible_message(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) . = ..() @@ -428,16 +539,15 @@ GLOBAL_LIST_INIT(channel_tokens, list( emped++ //There's been an EMP; better count it if (listening && ismob(loc)) // if the radio is turned on and on someone's person they notice to_chat(loc, span_warning("\The [src] overloads.")) - broadcasting = FALSE - listening = FALSE + set_on(FALSE) for (var/ch_name in channels) channels[ch_name] = 0 - on = FALSE + set_on(FALSE) addtimer(CALLBACK(src, PROC_REF(end_emp_effect)), 20 * severity, TIMER_UNIQUE | TIMER_OVERRIDE) /obj/item/radio/proc/end_emp_effect() emped = FALSE - on = TRUE + set_on(TRUE) return TRUE /////////////////////////////// @@ -457,10 +567,10 @@ GLOBAL_LIST_INIT(channel_tokens, list( var/mob/living/silicon/robot/R = loc if(istype(R)) for(var/ch_name in R.module.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) @@ -488,23 +598,14 @@ GLOBAL_LIST_INIT(channel_tokens, list( else to_chat(user, span_warning("This radio doesn't have any encryption keys!")) - else if(istype(W, /obj/item/encryptionkey/)) - if(keyslot) - to_chat(user, span_warning("The radio can't hold another key!")) - return - - if(!keyslot) - if(!user.transferItemToLoc(W, src)) - return - keyslot = W - - recalculateChannels() - /obj/item/radio/off // Station bounced radios, their only difference is spawning with the speakers off, this was made to help the lag. - listening = 0 // And it's nice to have a subtype too for future features. dog_fashion = /datum/dog_fashion/back +/obj/item/radio/off/Initialize(mapload) + . = ..() + set_listening(FALSE) + /obj/item/radio/off/makeshift // Makeshift SBR, limited use cases but could be useful. icon = 'icons/obj/improvised.dmi' icon_state = "radio_makeshift" diff --git a/code/game/objects/items/devices/taperecorder.dm b/code/game/objects/items/devices/taperecorder.dm index cafa41b6cf9a..12928ad7d192 100644 --- a/code/game/objects/items/devices/taperecorder.dm +++ b/code/game/objects/items/devices/taperecorder.dm @@ -7,7 +7,6 @@ lefthand_file = 'icons/mob/inhands/equipment/tools_lefthand.dmi' righthand_file = 'icons/mob/inhands/equipment/tools_righthand.dmi' w_class = WEIGHT_CLASS_SMALL - flags_1 = HEAR_1 slot_flags = ITEM_SLOT_BELT materials = list(/datum/material/iron=60, /datum/material/glass=30) force = 2 @@ -28,7 +27,7 @@ if(starting_tape_type) mytape = new starting_tape_type(src) update_appearance(UPDATE_ICON) - + become_hearing_sensitive() /obj/item/taperecorder/examine(mob/user) . = ..() diff --git a/code/game/objects/items/eightball.dm b/code/game/objects/items/eightball.dm index bdf87daf892a..10697ff0869a 100644 --- a/code/game/objects/items/eightball.dm +++ b/code/game/objects/items/eightball.dm @@ -97,7 +97,6 @@ /obj/item/toy/eightball/haunted shake_time = 30 SECONDS cooldown_time = 3 MINUTES - flags_1 = HEAR_1 var/last_message var/selected_message //these kind of store the same thing but one is easier to work with. @@ -137,7 +136,8 @@ for (var/answer in haunted_answers) votes[answer] = 0 GLOB.poi_list |= src - + become_hearing_sensitive() + /obj/item/toy/eightball/haunted/Destroy() GLOB.poi_list -= src . = ..() diff --git a/code/game/objects/items/implants/implant_biosig_ert.dm b/code/game/objects/items/implants/implant_biosig_ert.dm index 7a5c5459875b..3389b5cc43cf 100644 --- a/code/game/objects/items/implants/implant_biosig_ert.dm +++ b/code/game/objects/items/implants/implant_biosig_ert.dm @@ -9,7 +9,7 @@ . = ..() radio = new(src) radio.keyslot = new/obj/item/encryptionkey/headset_cent // Should broadcast exclusively on the centcom channel. - radio.listening = FALSE + radio.set_listening(FALSE) radio.recalculateChannels() /obj/item/implant/biosig_ert/activate(cause) diff --git a/code/game/objects/items/implants/implant_biosig_gorlex.dm b/code/game/objects/items/implants/implant_biosig_gorlex.dm index d91395e45670..b0a056a29490 100644 --- a/code/game/objects/items/implants/implant_biosig_gorlex.dm +++ b/code/game/objects/items/implants/implant_biosig_gorlex.dm @@ -9,7 +9,7 @@ . = ..() radio = new(src) radio.keyslot = new/obj/item/encryptionkey/syndicate // Should broadcast exclusively on the syndicate channel. - radio.listening = FALSE + radio.set_listening(FALSE) radio.recalculateChannels() /obj/item/implant/biosig_gorlex/activate(cause) diff --git a/code/game/objects/items/pinpointer.dm b/code/game/objects/items/pinpointer.dm index 4ca47a8f67ef..8c76d78139d9 100644 --- a/code/game/objects/items/pinpointer.dm +++ b/code/game/objects/items/pinpointer.dm @@ -68,6 +68,7 @@ return if(get_dist_euclidian(here,there) <= minimum_range) . += "pinon[alert ? "alert" : ""]direct" + else setDir(get_dir(here, there)) switch(get_dist(here, there)) diff --git a/code/game/say.dm b/code/game/say.dm index 45410e3f1656..bbe3586db6c1 100644 --- a/code/game/say.dm +++ b/code/game/say.dm @@ -37,11 +37,13 @@ GLOBAL_LIST_INIT(freqtospan, list( /atom/movable/proc/can_speak() return 1 -/atom/movable/proc/send_speech(message, range = 7, obj/source = src, bubble_type, list/spans, datum/language/message_language = null, list/message_mods = list()) +/atom/movable/proc/send_speech(message, range = 7, obj/source = src, bubble_type, list/spans, datum/language/message_language, list/message_mods = list()) var/rendered = compose_message(src, message_language, message, , spans, message_mods) - for(var/_AM in get_hearers_in_view(range, source)) - var/atom/movable/AM = _AM - AM.Hear(rendered, src, message_language, message, , spans, message_mods) + for(var/atom/movable/hearing_movable as anything in get_hearers_in_view(range, source)) + 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. @@ -152,7 +154,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 4da8f8530cf4..c90deee22950 100644 --- a/code/game/sound.dm +++ b/code/game/sound.dm @@ -30,46 +30,44 @@ GLOBAL_LIST_INIT(alt_sound_overrides, list( ) environment = SOUND_ENVIRONMENT_NONE //Default to none so sounds without overrides dont get reverb -/*! playsound - -playsound is a proc used to play a 3D sound in a specific range. This uses SOUND_RANGE + extra_range to determine that. - -source - Origin of sound -soundin - Either a file, or a string that can be used to get an SFX -vol - The volume of the sound, excluding falloff and pressure affection. -vary - bool that determines if the sound changes pitch every time it plays -extrarange - modifier for sound range. This gets added on top of SOUND_RANGE -falloff_exponent - Rate of falloff for the audio. Higher means quicker drop to low volume. Should generally be over 1 to indicate a quick dive to 0 rather than a slow dive. -frequency - playback speed of audio -channel - The channel the sound is played at -pressure_affected - Whether or not difference in pressure affects the sound (E.g. if you can hear in space) -ignore_walls - Whether or not the sound can pass through walls. -falloff_distance - Distance at which falloff begins. Sound is at peak volume (in regards to falloff) aslong as it is in this range. - -*/ +/** + * playsound is a proc used to play a 3D sound in a specific range. This uses SOUND_RANGE + extra_range to determine that. + * + * Arguments: + * * source - Origin of sound. + * * soundin - Either a file, or a string that can be used to get an SFX. + * * vol - The volume of the sound, excluding falloff and pressure affection. + * * vary - bool that determines if the sound changes pitch every time it plays. + * * extrarange - modifier for sound range. This gets added on top of SOUND_RANGE. + * * falloff_exponent - Rate of falloff for the audio. Higher means quicker drop to low volume. Should generally be over 1 to indicate a quick dive to 0 rather than a slow dive. + * * frequency - playback speed of audio. + * * channel - The channel the sound is played at. + * * pressure_affected - Whether or not difference in pressure affects the sound (E.g. if you can hear in space). + * * ignore_walls - Whether or not the sound can pass through walls. + * * falloff_distance - Distance at which falloff begins. Sound is at peak volume (in regards to falloff) aslong as it is in this range. + */ /proc/playsound(atom/source, soundin, vol as num, vary, extrarange as num, falloff_exponent = SOUND_FALLOFF_EXPONENT, frequency = null, channel = 0, pressure_affected = TRUE, ignore_walls = TRUE, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, use_reverb = TRUE) if(isarea(source)) CRASH("playsound(): source is an area") if(islist(soundin)) - CRASH("playsound(): soundin attempted to pass a list! Consider using pick() source: [source.name]") + CRASH("playsound(): soundin attempted to pass a list! Consider using pick()") var/turf/turf_source = get_turf(source) - if (!turf_source) + if (!turf_source || !soundin || !vol) return //allocate a channel if necessary now so its the same for everyone channel = channel || SSsounds.random_available_channel() - // Looping through the player list has the added bonus of working for mobs inside containers - var/sound/S = sound(get_sfx(soundin)) + var/sound/S = isdatum(soundin) ? soundin : sound(get_sfx(soundin)) var/maxdistance = SOUND_RANGE + extrarange var/source_z = turf_source.z var/list/listeners = SSmobs.clients_by_zlevel[source_z].Copy() - . = list() //output everything that successfully heard the sound + . = list()//output everything that successfully heard the sound var/turf/above_turf = GET_TURF_ABOVE(turf_source) var/turf/below_turf = GET_TURF_BELOW(turf_source) @@ -96,58 +94,57 @@ falloff_distance - Distance at which falloff begins. Sound is at peak volume (in listening_mob.playsound_local(turf_source, soundin, vol, vary, frequency, falloff_exponent, channel, pressure_affected, S, maxdistance, falloff_distance, 1, use_reverb) . += listening_mob -/*! playsound -playsound_local is a proc used to play a sound directly on a mob from a specific turf. -This is called by playsound to send sounds to players, in which case it also gets the max_distance of that sound. -turf_source - Origin of sound -soundin - Either a file, or a string that can be used to get an SFX -vol - The volume of the sound, excluding falloff -vary - bool that determines if the sound changes pitch every time it plays -frequency - playback speed of audio -falloff_exponent - Rate of falloff for the audio. Higher means quicker drop to low volume. Should generally be over 1 to indicate a quick dive to 0 rather than a slow dive. -channel - The channel the sound is played at -pressure_affected - Whether or not difference in pressure affects the sound (E.g. if you can hear in space) -max_distance - The peak distance of the sound, if this is a 3D sound -falloff_distance - Distance at which falloff begins, if this is a 3D sound -distance_multiplier - Can be used to multiply the distance at which the sound is heard -*/ - -/mob/proc/playsound_local(turf/turf_source, soundin, vol as num, vary, frequency, falloff_exponent = SOUND_FALLOFF_EXPONENT, channel = 0, pressure_affected = TRUE, sound/S, max_distance, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, distance_multiplier = 1, use_reverb = TRUE) +/** + * Plays a sound with a specific point of origin for src mob + * Affected by pressure, distance, terrain and environment (see arguments) + * + * Arguments: + * * turf_source - The turf our sound originates from, if this is not a turf, the sound is played with no spatial audio + * * soundin - Either a file, or a string that can be used to get an SFX. + * * vol - The volume of the sound, excluding falloff and pressure affection. + * * vary - bool that determines if the sound changes pitch every time it plays. + * * frequency - playback speed of audio. + * * falloff_exponent - Rate of falloff for the audio. Higher means quicker drop to low volume. Should generally be over 1 to indicate a quick dive to 0 rather than a slow dive. + * * channel - Optional: The channel the sound is played at. + * * pressure_affected - bool Whether or not difference in pressure affects the sound (E.g. if you can hear in space). + * * sound_to_use - Optional: Will default to soundin when absent + * * max_distance - number, determines the maximum distance of our sound + * * falloff_distance - Distance at which falloff begins. Sound is at peak volume (in regards to falloff) aslong as it is in this range. + * * distance_multiplier - Default 1, multiplies the maximum distance of our sound + * * use_reverb - bool default TRUE, determines if our sound has reverb + */ + +/mob/proc/playsound_local(turf/turf_source, soundin, vol as num, vary, frequency, falloff_exponent = SOUND_FALLOFF_EXPONENT, channel = 0, pressure_affected = TRUE, sound/sound_to_use, max_distance, falloff_distance = SOUND_DEFAULT_FALLOFF_DISTANCE, distance_multiplier = 1, use_reverb = TRUE) if(!client || !can_hear()) return - if(!S) - S = sound(get_sfx(soundin)) - - S.wait = 0 //No queue - S.channel = channel || SSsounds.random_available_channel() - S.volume = vol + if(!sound_to_use) + sound_to_use = sound(get_sfx(soundin)) - if (client?.prefs && !(client.prefs.toggles & SOUND_ALT) && (S.file in GLOB.alt_sound_overrides)) - S.file = GLOB.alt_sound_overrides[S.file] + sound_to_use.wait = 0 //No queue + sound_to_use.channel = channel || SSsounds.random_available_channel() + sound_to_use.volume = vol if(vary) if(frequency) - S.frequency = frequency + sound_to_use.frequency = frequency else - S.frequency = get_rand_frequency() + sound_to_use.frequency = get_rand_frequency() if(isturf(turf_source)) - var/turf/T = get_turf(src) + var/turf/turf_loc = get_turf(src) //sound volume falloff with distance - var/distance = get_dist(T, turf_source) - - distance *= distance_multiplier + var/distance = get_dist(turf_loc, turf_source) * distance_multiplier if(max_distance) //If theres no max_distance we're not a 3D sound, so no falloff. - S.volume -= (max(distance - falloff_distance, 0) ** (1 / falloff_exponent)) / ((max(max_distance, distance) - falloff_distance) ** (1 / falloff_exponent)) * S.volume + sound_to_use.volume -= (max(distance - falloff_distance, 0) ** (1 / falloff_exponent)) / ((max(max_distance, distance) - falloff_distance) ** (1 / falloff_exponent)) * sound_to_use.volume //https://www.desmos.com/calculator/sqdfl8ipgf if(pressure_affected) //Atmosphere affects sound var/pressure_factor = 1 - var/datum/gas_mixture/hearer_env = T.return_air() + var/datum/gas_mixture/hearer_env = turf_loc.return_air() var/datum/gas_mixture/source_env = turf_source.return_air() if(hearer_env && source_env) @@ -160,35 +157,35 @@ distance_multiplier - Can be used to multiply the distance at which the sound is if(distance <= 1) pressure_factor = max(pressure_factor, 0.15) //touching the source of the sound - S.volume *= pressure_factor + sound_to_use.volume *= pressure_factor //End Atmosphere affecting sound - if(S.volume <= 0) + if(sound_to_use.volume <= 0) return //No sound - var/dx = turf_source.x - T.x // Hearing from the right/left - S.x = dx * distance_multiplier - var/dz = turf_source.y - T.y // Hearing from infront/behind - S.z = dz * distance_multiplier - var/dy = (turf_source.z - T.z) * 5 * distance_multiplier // Hearing from above / below, multiplied by 5 because we assume height is further along coords. - S.y = dy + var/dx = turf_source.x - turf_loc.x // Hearing from the right/left + sound_to_use.x = dx * distance_multiplier + var/dz = turf_source.y - turf_loc.y // Hearing from infront/behind + sound_to_use.z = dz * distance_multiplier + var/dy = (turf_source.z - turf_loc.z) * 5 * distance_multiplier // Hearing from above / below, multiplied by 5 because we assume height is further along coords. + sound_to_use.y = dy - S.falloff = max_distance || 1 //use max_distance, else just use 1 as we are a direct sound so falloff isnt relevant. + sound_to_use.falloff = max_distance || 1 //use max_distance, else just use 1 as we are a direct sound so falloff isnt relevant. // Sounds can't have their own environment. A sound's environment will be: // 1. the mob's // 2. the area's (defaults to SOUND_ENVRIONMENT_NONE) if(sound_environment_override != SOUND_ENVIRONMENT_NONE) - S.environment = sound_environment_override + sound_to_use.environment = sound_environment_override else var/area/A = get_area(src) - S.environment = A.sound_environment + sound_to_use.environment = A.sound_environment - if(use_reverb && S.environment != SOUND_ENVIRONMENT_NONE) //We have reverb, reset our echo setting - S.echo[3] = 0 //Room setting, 0 means normal reverb - S.echo[4] = 0 //RoomHF setting, 0 means normal reverb. + if(use_reverb && sound_to_use.environment != SOUND_ENVIRONMENT_NONE) //We have reverb, reset our echo setting + sound_to_use.echo[3] = 0 //Room setting, 0 means normal reverb + sound_to_use.echo[4] = 0 //RoomHF setting, 0 means normal reverb. - SEND_SOUND(src, S) + SEND_SOUND(src, sound_to_use) /proc/sound_to_playing_players(soundin, volume = 100, vary = FALSE, frequency = 0, channel = 0, pressure_affected = FALSE, sound/S) if(!S) diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index 56d31bbf5903..dfd75e9b9c2b 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -7,6 +7,10 @@ GLOBAL_LIST_EMPTY(station_turfs) luminosity = 1 light_height = LIGHTING_HEIGHT_FLOOR + ///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 + /// Turf bitflags, see code/__DEFINES/flags.dm var/turf_flags = NONE diff --git a/code/modules/antagonists/abductor/equipment/abduction_gear.dm b/code/modules/antagonists/abductor/equipment/abduction_gear.dm index 09c67009cfef..980ad71429b0 100644 --- a/code/modules/antagonists/abductor/equipment/abduction_gear.dm +++ b/code/modules/antagonists/abductor/equipment/abduction_gear.dm @@ -278,12 +278,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/antagonists/clockcult/clock_items/soul_vessel.dm b/code/modules/antagonists/clockcult/clock_items/soul_vessel.dm index f0d8f5741854..1aba43ad9306 100644 --- a/code/modules/antagonists/clockcult/clock_items/soul_vessel.dm +++ b/code/modules/antagonists/clockcult/clock_items/soul_vessel.dm @@ -28,7 +28,7 @@ /obj/item/mmi/posibrain/soul_vessel/Initialize(mapload) . = ..() - radio.on = FALSE + radio.is_on(FALSE) laws = new /datum/ai_laws/ratvar() braintype = picked_name GLOB.all_clockwork_objects += src diff --git a/code/modules/assembly/voice.dm b/code/modules/assembly/voice.dm index 33a7f0dfd3ef..84d86a437e20 100644 --- a/code/modules/assembly/voice.dm +++ b/code/modules/assembly/voice.dm @@ -8,7 +8,6 @@ desc = "A small electronic device able to record a voice sample, and send a signal when that sample is repeated." icon_state = "voice" materials = list(/datum/material/iron=500, /datum/material/glass=50) - flags_1 = HEAR_1 attachable = TRUE verb_say = "beeps" verb_ask = "beeps" @@ -21,6 +20,11 @@ "recognizer", "voice sensor") + +/obj/item/assembly/voice/Initialize(mapload) + . = ..() + become_hearing_sensitive(ROUNDSTART_TRAIT) + /obj/item/assembly/voice/examine(mob/user) . = ..() . += span_notice("Use a multitool to swap between \"inclusive\", \"exclusive\", \"recognizer\", and \"voice sensor\" mode.") diff --git a/code/modules/atmospherics/machinery/components/fusion/hfr_core.dm b/code/modules/atmospherics/machinery/components/fusion/hfr_core.dm index 095b9f8d2aa9..21d722215fec 100644 --- a/code/modules/atmospherics/machinery/components/fusion/hfr_core.dm +++ b/code/modules/atmospherics/machinery/components/fusion/hfr_core.dm @@ -181,7 +181,7 @@ radio = new(src) radio.keyslot = new radio_key - radio.listening = FALSE + radio.set_listening(FALSE) radio.recalculateChannels() investigate_log("has been created.", INVESTIGATE_HYPERTORUS) diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 8a9c3875e8e5..62c3f032e739 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -526,8 +526,9 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( 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 + seen_messages = null Master.UpdateTickRate() world.sync_logout_with_db(connection_number) // yogs - logout logging diff --git a/code/modules/clothing/masks/hailer.dm b/code/modules/clothing/masks/hailer.dm index 2f111ccc0564..aff2aafe7432 100644 --- a/code/modules/clothing/masks/hailer.dm +++ b/code/modules/clothing/masks/hailer.dm @@ -11,7 +11,6 @@ item_state = "sechailer" clothing_flags = BLOCK_GAS_SMOKE_EFFECT | MASKINTERNALS flags_inv = HIDEFACIALHAIR|HIDEFACE - flags_1 = HEAR_1 w_class = WEIGHT_CLASS_SMALL visor_flags = BLOCK_GAS_SMOKE_EFFECT | MASKINTERNALS visor_flags_inv = HIDEFACIALHAIR|HIDEFACE @@ -71,6 +70,7 @@ "Hehe" = 'sound/voice/cpvoicelines/chuckle.ogg', ) + /obj/item/clothing/mask/gas/sechailer/swat/spacepol name = "spacepol mask" desc = "A close-fitting tactical mask created in cooperation with a certain megacorporation, comes with an especially aggressive Compli-o-nator 3000." diff --git a/code/modules/clothing/suits/miscellaneous.dm b/code/modules/clothing/suits/miscellaneous.dm index 168b6a33ac58..78a1faf6450c 100644 --- a/code/modules/clothing/suits/miscellaneous.dm +++ b/code/modules/clothing/suits/miscellaneous.dm @@ -605,7 +605,6 @@ icon_state = "unalive-vest" item_state = "reactiveoff" blood_overlay_type = "armor" - flags_1 = HEAR_1 resistance_flags = LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF //stop trying to get it off var/listening = FALSE var/activation_phrase = "" @@ -628,6 +627,7 @@ return say("Now recording.") listening = TRUE + become_hearing_sensitive() //to replace depreciated HEAR 1 /obj/item/clothing/suit/unalivevest/Hear(message, atom/movable/speaker, message_language, raw_message, radio_freq, list/spans, list/message_mods) . = ..() @@ -663,3 +663,4 @@ var/mob/living/L = loc L.gib(no_brain = TRUE, no_items = TRUE) qdel(src) + lose_hearing_sensitivity() diff --git a/code/modules/instruments/songs/play_legacy.dm b/code/modules/instruments/songs/play_legacy.dm index fbd6b12701a9..3d574a9bb356 100644 --- a/code/modules/instruments/songs/play_legacy.dm +++ b/code/modules/instruments/songs/play_legacy.dm @@ -87,5 +87,5 @@ L.apply_status_effect(STATUS_EFFECT_GOOD_MUSIC) if(!(M?.client?.prefs?.toggles & SOUND_INSTRUMENTS)) continue - M.playsound_local(source, null, volume * using_instrument.volume_multiplier, falloff_exponent = 5, S = music_played) + M.playsound_local(source, null, volume * using_instrument.volume_multiplier, falloff_exponent = 5, sound_to_use = music_played) // Could do environment and echo later but not for now diff --git a/code/modules/jobs/job_types/security_officer.dm b/code/modules/jobs/job_types/security_officer.dm index fed23668d65b..7250b30019a6 100644 --- a/code/modules/jobs/job_types/security_officer.dm +++ b/code/modules/jobs/job_types/security_officer.dm @@ -187,7 +187,7 @@ GLOBAL_LIST_INIT(available_depts_sec, list(SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICA /obj/item/radio/headset/headset_sec/alt/department/Initialize(mapload) . = ..() wires = new/datum/wires/radio(src) - secure_radio_connections = new + secure_radio_connections = list() recalculateChannels() /obj/item/radio/headset/headset_sec/alt/department/engi diff --git a/code/modules/mining/aux_base.dm b/code/modules/mining/aux_base.dm index 01cf7f048cb4..45fe916e1d21 100644 --- a/code/modules/mining/aux_base.dm +++ b/code/modules/mining/aux_base.dm @@ -47,7 +47,7 @@ /obj/machinery/computer/auxiliary_base/Initialize(mapload) . = ..() radio = new /obj/item/radio(src) - radio.frequency = radio_freq + radio.set_frequency(radio_freq) AddComponent(/datum/component/gps, "NT_AUX") /obj/machinery/computer/auxiliary_base/Destroy() diff --git a/code/modules/mining/laborcamp/laborstacker.dm b/code/modules/mining/laborcamp/laborstacker.dm index d8a2392268fb..167e2e2f3a6d 100644 --- a/code/modules/mining/laborcamp/laborstacker.dm +++ b/code/modules/mining/laborcamp/laborstacker.dm @@ -19,7 +19,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(!GLOB.labor_sheet_values) diff --git a/code/modules/mob/living/brain/MMI.dm b/code/modules/mob/living/brain/MMI.dm index 71a46b48d508..ee8df959a578 100644 --- a/code/modules/mob/living/brain/MMI.dm +++ b/code/modules/mob/living/brain/MMI.dm @@ -54,7 +54,7 @@ /obj/item/mmi/Initialize(mapload) . = ..() radio = new(src) //Spawns a radio inside the MMI. - radio.broadcasting = FALSE //researching radio mmis turned the robofabs into radios because this didnt start as 0. + radio.set_broadcasting(FALSE) //researching radio mmis turned the robofabs into radios because this didnt start as 0. laws.set_laws_config() /obj/item/mmi/attackby(obj/item/O, mob/user, params) @@ -144,8 +144,8 @@ reboot_timer = null /obj/item/mmi/AltClick(mob/user) - radio.on = !radio.on - to_chat(user, span_notice("You toggle [src]'s radio system [radio.on==1 ? "on" : "off"].")) + radio.set_on(!radio.is_on()) + to_chat(user, span_notice("You toggle [src]'s radio system [radio.is_on() == TRUE ? "on" : "off"].")) /obj/item/mmi/proc/eject_brain(mob/user) if(brainmob) @@ -211,12 +211,12 @@ if(brainmob.stat) to_chat(brainmob, span_warning("Can't do that while incapacitated or dead!")) - if(!radio.on) + if(!radio.is_on()) to_chat(brainmob, span_warning("Your radio is disabled!")) return - radio.listening = !radio.listening - to_chat(brainmob, span_notice("Radio is [radio.listening ? "now" : "no longer"] receiving broadcast.")) + radio.set_listening(!radio.get_listening()) + to_chat(brainmob, span_notice("Radio is [radio.get_listening() ? "now" : "no longer"] receiving broadcast.")) /obj/item/mmi/emp_act(severity) . = ..() @@ -258,7 +258,7 @@ /obj/item/mmi/examine(mob/user) . = ..() - . += span_notice("There is a switch to toggle the radio system [radio.on ? "off" : "on"].[brain ? " It is currently being covered by [brain]." : null]") + . += 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) diff --git a/code/modules/mob/living/carbon/human/species_types/dullahan.dm b/code/modules/mob/living/carbon/human/species_types/dullahan.dm index f9101b53130b..2b0bb0fcd5e6 100644 --- a/code/modules/mob/living/carbon/human/species_types/dullahan.dm +++ b/code/modules/mob/living/carbon/human/species_types/dullahan.dm @@ -24,17 +24,16 @@ /datum/species/dullahan/on_species_gain(mob/living/carbon/human/H, datum/species/old_species) . = ..() - H.flags_1 &= ~HEAR_1 + H.lose_hearing_sensitivity(TRAIT_GENERIC) var/obj/item/bodypart/head/head = H.get_bodypart(BODY_ZONE_HEAD) if(head) head.drop_limb() - head.flags_1 = HEAR_1 head.throwforce = 25 myhead = new /obj/item/dullahan_relay (head, H) H.put_in_hands(head) /datum/species/dullahan/on_species_loss(mob/living/carbon/human/H) - H.flags_1 |= ~HEAR_1 + H.become_hearing_sensitive() H.reset_perspective(H) if(myhead) var/obj/item/dullahan_relay/DR = myhead @@ -152,26 +151,26 @@ /obj/item/dullahan_relay var/mob/living/owner - flags_1 = HEAR_1 /obj/item/dullahan_relay/Initialize(mapload,new_owner) . = ..() owner = new_owner START_PROCESSING(SSobj, src) + become_hearing_sensitive(ROUNDSTART_TRAIT) /obj/item/dullahan_relay/process() if(!istype(loc, /obj/item/bodypart/head) || QDELETED(owner)) . = PROCESS_KILL qdel(src) -/obj/item/dullahan_relay/Hear(message, atom/movable/speaker, message_language, raw_message, radio_freq, list/spans, message_mode) - . = ..() - if(!QDELETED(owner)) - message = compose_message(speaker, message_language, raw_message, radio_freq, spans, message_mode) - to_chat(owner,message) - else - qdel(src) +/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)) +///Adds the owner to the list of hearers in hearers_in_view(), for visible/hearable on top of say messages +/obj/item/dullahan_relay/proc/include_owner(datum/source, list/hearers) + SIGNAL_HANDLER + if(!QDELETED(owner)) + hearers += owner /obj/item/dullahan_relay/Destroy() if(!QDELETED(owner)) @@ -181,4 +180,4 @@ D.myhead = null owner.gib() owner = null - ..() + return..() diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm index ce518c162584..9334b8b039ce 100644 --- a/code/modules/mob/living/say.dm +++ b/code/modules/mob/living/say.dm @@ -385,7 +385,7 @@ GLOBAL_LIST_INIT(special_radio_keys, list( /mob/living/proc/radio(message, list/message_mods = list(), list/spans, language) var/obj/item/implant/radio/imp = locate() in src - if(imp && imp.radio.on) + if(imp && imp.radio.set_on()) if(message_mods[MODE_HEADSET]) imp.radio.talk_into(src, message, null, spans, language, message_mods) return ITALICS | REDUCE_RANGE diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm index 5de37d1b4e61..3748ff6c00da 100644 --- a/code/modules/mob/living/silicon/robot/robot.dm +++ b/code/modules/mob/living/silicon/robot/robot.dm @@ -13,6 +13,8 @@ light_system = MOVABLE_LIGHT_DIRECTIONAL light_on = FALSE + radio = /obj/item/radio/borg + var/custom_name = "" var/braintype = "Cyborg" var/obj/item/mmi/mmi = null @@ -119,7 +121,6 @@ spark_system = new /datum/effect_system/spark_spread() spark_system.set_up(5, 0, src) spark_system.attach(src) - wires = new /datum/wires/robot(src) ADD_TRAIT(src, TRAIT_EMPPROOF_CONTENTS, "innate_empproof") @@ -142,7 +143,7 @@ update_law_history() //yogs - radio = new /obj/item/radio/borg(src) + if(!scrambledcodes && !builtInCamera) builtInCamera = new (src) builtInCamera.c_tag = real_name @@ -211,9 +212,9 @@ if(shell) GLOB.available_ai_shells -= src else - if(T && istype(radio) && istype(radio.keyslot)) - radio.keyslot.forceMove(T) - radio.keyslot = null + if(T && istype(radio) && istype(radio?.keyslot)) + radio?.keyslot?.forceMove(T) + radio?.keyslot = null qdel(wires) qdel(module) qdel(eye_lights) @@ -869,17 +870,21 @@ for(b=0, b!=2, b++) var/obj/item/assembly/flash/handheld/F = new /obj/item/assembly/flash/handheld(T) F.burn_out() - if (cell) //Sanity check. + if (cell) //Sanity check. cell.forceMove(T) cell = null + if (radio) + radio?.keyslot?.forceMove(T) + radio?.keyslot = null qdel(src) /mob/living/silicon/robot/modules - var/set_module = null + var/set_module = /obj/item/robot_module /mob/living/silicon/robot/modules/Initialize(mapload) . = ..() - module.transform_to(set_module) + INVOKE_ASYNC(module, TYPE_PROC_REF(/obj/item/robot_module, transform_to), set_module, TRUE) + /mob/living/silicon/robot/modules/standard set_module = /obj/item/robot_module/standard diff --git a/code/modules/mob/living/silicon/robot/robot_modules.dm b/code/modules/mob/living/silicon/robot/robot_modules.dm index ec1a786cb6bd..d314d3f43d79 100644 --- a/code/modules/mob/living/silicon/robot/robot_modules.dm +++ b/code/modules/mob/living/silicon/robot/robot_modules.dm @@ -228,7 +228,6 @@ R.module = RM R.update_module_innate() RM.rebuild_modules() - R.radio.recalculateChannels() var/datum/component/armor_plate/C = R.GetComponent(/datum/component/armor_plate) if(C) // Remove armor plating. C.dropplates() diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm index 2beb6588a216..7f2d2cd3c4b5 100644 --- a/code/modules/mob/living/silicon/silicon.dm +++ b/code/modules/mob/living/silicon/silicon.dm @@ -12,7 +12,7 @@ mob_biotypes = MOB_ROBOTIC deathsound = 'sound/voice/borg_deathsound.ogg' speech_span = SPAN_ROBOT - flags_1 = PREVENT_CONTENTS_EXPLOSION_1 | HEAR_1 | RAD_PROTECT_CONTENTS_1 | RAD_NO_CONTAMINATE_1 + flags_1 = PREVENT_CONTENTS_EXPLOSION_1 | RAD_PROTECT_CONTENTS_1 | RAD_NO_CONTAMINATE_1 var/datum/ai_laws/laws = null//Now... THEY ALL CAN ALL HAVE LAWS var/last_lawchange_announce = 0 @@ -58,6 +58,8 @@ armor = getArmor() else if(!istype(armor, /datum/armor)) stack_trace("Invalid type [armor.type] found in .armor during [type] Initialize()") + if(ispath(radio)) + radio = new radio(src) diag_hud_set_status() diag_hud_set_health() ADD_TRAIT(src, TRAIT_FORCED_STANDING, "cyborg") // not CYBORG_ITEM_TRAIT because not an item @@ -69,8 +71,8 @@ return //we use a different hud /mob/living/silicon/Destroy() - radio = null - aicamera = null + QDEL_NULL(radio) + QDEL_NULL(aicamera) QDEL_NULL(builtInCamera) GLOB.silicon_mobs -= src return ..() @@ -248,7 +250,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/living/simple_animal/hostile/megafauna/_megafauna.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm index 1dde8e462128..b07c60618757 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/_megafauna.dm @@ -28,7 +28,7 @@ mob_size = MOB_SIZE_HUGE layer = LARGE_MOB_LAYER //Looks weird with them slipping under mineral walls and cameras and shit otherwise mouse_opacity = MOUSE_OPACITY_OPAQUE // Easier to click on in melee, they're giant targets anyway - flags_1 = HEAR_1 | PREVENT_CONTENTS_EXPLOSION_1 + flags_1 = PREVENT_CONTENTS_EXPLOSION_1 /// Crusher loot dropped when the megafauna is killed with a crusher var/list/crusher_loot /// Achievement given to surrounding players when the megafauna is killed diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm index e1c81eef9b30..591f7e84e2e6 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm @@ -408,7 +408,6 @@ Difficulty: Very Hard use_power = NO_POWER_USE anchored = FALSE density = TRUE - flags_1 = HEAR_1 var/activation_method var/list/possible_methods = list(ACTIVATE_TOUCH, ACTIVATE_SPEECH, ACTIVATE_HEAT, ACTIVATE_BULLET, ACTIVATE_ENERGY, ACTIVATE_BOMB, ACTIVATE_MOB_BUMP, ACTIVATE_WEAPON, ACTIVATE_MAGIC) @@ -422,6 +421,7 @@ Difficulty: Very Hard . = ..() if(!activation_method) activation_method = pick(possible_methods) + become_hearing_sensitive(trait_source = ROUNDSTART_TRAIT) /obj/machinery/anomalous_crystal/examine(mob/user) . = ..() diff --git a/code/modules/mob/living/simple_animal/hostile/space_dragon.dm b/code/modules/mob/living/simple_animal/hostile/space_dragon.dm index 12a9589daa81..14436c501d8b 100644 --- a/code/modules/mob/living/simple_animal/hostile/space_dragon.dm +++ b/code/modules/mob/living/simple_animal/hostile/space_dragon.dm @@ -39,7 +39,7 @@ health_doll_icon = "spacedragon" obj_damage = 50 environment_smash = ENVIRONMENT_SMASH_NONE - flags_1 = HEAR_1 | PREVENT_CONTENTS_EXPLOSION_1 + flags_1 = PREVENT_CONTENTS_EXPLOSION_1 mob_size = MOB_SIZE_LARGE melee_damage_upper = 35 melee_damage_lower = 35 diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm index 3ceba75e89d3..dea774661883 100644 --- a/code/modules/mob/login.dm +++ b/code/modules/mob/login.dm @@ -67,7 +67,9 @@ if(!client) return FALSE - + + //We do this here to prevent hanging refs from ghostize or whatever, since if we were in another mob before this'll take care of it + enable_client_mobs_in_contents(client) SEND_SIGNAL(src, COMSIG_MOB_LOGIN) if (client && key != client.key) diff --git a/code/modules/mob/logout.dm b/code/modules/mob/logout.dm index 258c77c61664..eec55e3eb189 100644 --- a/code/modules/mob/logout.dm +++ b/code/modules/mob/logout.dm @@ -14,5 +14,6 @@ for(var/foo in client.player_details.post_logout_callbacks) 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 d93cfe9dec7a..9b25e5402a4f 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -86,6 +86,7 @@ . = ..() update_config_movespeed() update_movespeed(TRUE) + become_hearing_sensitive() /mob/New() // This needs to happen IMMEDIATELY. I'm sorry :( @@ -692,6 +693,12 @@ I.attack_self(src) update_inv_hands() +///clears the client mob in our client_mobs_in_contents list +/mob/proc/clear_client_in_contents() + if(client?.movingmob) + LAZYREMOVE(client.movingmob.client_mobs_in_contents, src) + client.movingmob = null + /** * Get the notes of this mob * diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index 6ee406be37fe..ab6a57ee182f 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -10,7 +10,6 @@ density = TRUE layer = MOB_LAYER animate_movement = 2 - flags_1 = HEAR_1 hud_possible = list(ANTAG_HUD) pressure_resistance = 8 mouse_drag_pointer = MOUSE_ACTIVE_POINTER diff --git a/code/modules/power/gravitygenerator.dm b/code/modules/power/gravitygenerator.dm index 29567ec00dc6..5248d86e9d3a 100644 --- a/code/modules/power/gravitygenerator.dm +++ b/code/modules/power/gravitygenerator.dm @@ -425,7 +425,7 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne M.update_gravity(M.mob_has_gravity()) if(M.client) shake_camera(M, 15, 1) - M.playsound_local(T, null, 100, 1, 0.5, S = alert_sound) + M.playsound_local(T, null, 100, 1, 0.5, sound_to_use = alert_sound) /obj/machinery/gravity_generator/main/proc/gravity_in_level() var/turf/T = get_turf(src) diff --git a/code/modules/power/supermatter/supermatter.dm b/code/modules/power/supermatter/supermatter.dm index 80d0c0e6efbc..b28172cd10ca 100644 --- a/code/modules/power/supermatter/supermatter.dm +++ b/code/modules/power/supermatter/supermatter.dm @@ -246,7 +246,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal) GLOB.poi_list |= src radio = new(src) radio.keyslot = new radio_key - radio.listening = 0 + radio.set_listening(FALSE) radio.recalculateChannels() distort = new(src) add_emitter(/obj/emitter/sparkle, "supermatter_sparkle") diff --git a/code/modules/research/nanites/nanite_programmer.dm b/code/modules/research/nanites/nanite_programmer.dm index b5f240397f25..b56b133f5f2f 100644 --- a/code/modules/research/nanites/nanite_programmer.dm +++ b/code/modules/research/nanites/nanite_programmer.dm @@ -10,6 +10,10 @@ anchored = TRUE density = TRUE +/obj/machinery/nanite_programmer/Initialize(mapload) + . = ..() + become_hearing_sensitive(trait_source = ROUNDSTART_TRAIT) + /obj/machinery/nanite_programmer/attackby(obj/item/I, mob/user) if(istype(I, /obj/item/disk/nanite_program)) var/obj/item/disk/nanite_program/N = I diff --git a/code/modules/ruins/objects_and_mobs/necropolis_gate.dm b/code/modules/ruins/objects_and_mobs/necropolis_gate.dm index f767042edc8c..9d4a1a7212ce 100644 --- a/code/modules/ruins/objects_and_mobs/necropolis_gate.dm +++ b/code/modules/ruins/objects_and_mobs/necropolis_gate.dm @@ -184,7 +184,7 @@ GLOBAL_DATUM(necropolis_gate, /obj/structure/necropolis_gate/legion_gate) for(var/mob/M in GLOB.player_list) if(M.z == z) to_chat(M, span_userdanger("Discordant whispers flood your mind in a thousand voices. Each one speaks your name, over and over. Something horrible has been released.")) - M.playsound_local(T, null, 100, FALSE, 0, FALSE, pressure_affected = FALSE, S = legion_sound) + M.playsound_local(T, null, 100, FALSE, 0, FALSE, pressure_affected = FALSE, sound_to_use = legion_sound) flash_color(M, flash_color = "#FF0000", flash_time = 50) var/mutable_appearance/release_overlay = mutable_appearance('icons/effects/effects.dmi', "legiondoor") notify_ghosts("Legion has been released in the [get_area(src)]!", source = src, alert_overlay = release_overlay, action = NOTIFY_JUMP) diff --git a/code/modules/security_levels/level_interface.dm b/code/modules/security_levels/level_interface.dm index 01d22e0ef5e2..ae989ba476d2 100644 --- a/code/modules/security_levels/level_interface.dm +++ b/code/modules/security_levels/level_interface.dm @@ -24,7 +24,7 @@ radio = new(src) radio.keyslot = new radio_key radio.subspace_transmission = TRUE - radio.listening = FALSE + radio.set_listening(FALSE) radio.independent = TRUE radio.recalculateChannels() RegisterSignal(SSsecurity_level, COMSIG_SECURITY_LEVEL_CHANGED, PROC_REF(update_security_level)) diff --git a/code/modules/spatial_grid/cell_tracker.dm b/code/modules/spatial_grid/cell_tracker.dm new file mode 100644 index 000000000000..fb0d2e0bcf1b --- /dev/null +++ b/code/modules/spatial_grid/cell_tracker.dm @@ -0,0 +1,99 @@ +/** + * Spatial gridmap, cell tracking + * + * This datum exists to make the large, repeated "everything in some range" pattern faster + * Rather then just refreshing against everything, we track all the cells in range of the passed in "window" + * This lets us do entered/left logic, and make ordinarially quite expensive logic much cheaper + * + * Note: This system should not be used for things who have strict requirements about what is NOT in their processed entries + * It should instead only be used for logic that only really cares about limiting how much gets "entered" in any one call + * Because we apply this limitation, we can do things to make our code much less prone to unneeded work + */ +/datum/cell_tracker + var/list/datum/spatial_grid_cell/member_cells = list() + // Inner window + // If a cell is inside this space, it will be entered into our membership list + /// The height (y radius) of our inner window + var/inner_window_x_radius + /// The width (x radius) of our inner window + var/inner_window_y_radius + + // Outer window + // If a cell is outside this space, it will be removed from our memebership list + // This effectively applies a grace window, to prevent moving back and forth across a border line causing issues + /// The height (y radius) of our outer window + var/outer_window_x_radius + /// The width (x radius) of our outer window + var/outer_window_y_radius + +/// Accepts a width and height to use for this tracker +/// Also accepts the ratio to use between inner and outer window. Optional, defaults to 2 +/datum/cell_tracker/New(width, height, inner_outer_ratio) + set_bounds(width, height, inner_outer_ratio) + return ..() + +/datum/cell_tracker/Destroy(force) + stack_trace("Attempted to delete a cell tracker. They don't hold any refs outside of cells, what are you doing") + if(!force) + return QDEL_HINT_LETMELIVE + member_cells.Cut() + return ..() + +/// Takes a width and height, and uses them to set the inner window, and interpolate the outer window +/datum/cell_tracker/proc/set_bounds(width = 0, height = 0, ratio = 2) + // We want to store these as radii, rather then width and height, since that's convineient for spatial grid code + var/x_radius = CEILING(width, 2) + var/y_radius = CEILING(height, 2) + inner_window_x_radius = x_radius + inner_window_y_radius = y_radius + + outer_window_x_radius = x_radius * ratio + outer_window_y_radius = y_radius * ratio + +/// Returns a list of newly and formerly joined spatial grid managed objects of type [type] in the form list(new, old) +/// Takes the center of our window as input +/datum/cell_tracker/proc/recalculate_type_members(turf/center, type) + var/list/new_and_old = recalculate_cells(center) + + var/list/new_members = list() + var/list/former_members = list() + /// Pull out all the new and old memebers we want + switch(type) + if(SPATIAL_GRID_CONTENTS_TYPE_CLIENTS) + for(var/datum/spatial_grid_cell/cell as anything in new_and_old[1]) + new_members += cell.client_contents + for(var/datum/spatial_grid_cell/cell as anything in new_and_old[2]) + former_members += cell.client_contents + if(SPATIAL_GRID_CONTENTS_TYPE_HEARING) + for(var/datum/spatial_grid_cell/cell as anything in new_and_old[1]) + new_members += cell.hearing_contents + for(var/datum/spatial_grid_cell/cell as anything in new_and_old[2]) + former_members += cell.hearing_contents + if(SPATIAL_GRID_CONTENTS_TYPE_ATMOS) + for(var/datum/spatial_grid_cell/cell as anything in new_and_old[1]) + new_members += cell.atmos_contents + for(var/datum/spatial_grid_cell/cell as anything in new_and_old[2]) + former_members += cell.atmos_contents + + return list(new_members, former_members) + +/// Recalculates our member list, returns a list in the form list(new members, old members) for reaction +/// Accepts the turf to use as our "center" +/datum/cell_tracker/proc/recalculate_cells(turf/center) + if(!center) + CRASH("/datum/cell_tracker had an invalid location on refresh, ya done fucked") + // This is a mild waste of cpu time. Consider optimizing by adding a new helper function to get just the space between two bounds + // Assuming it ever becomes a real problem + var/list/datum/spatial_grid_cell/inner_window = SSspatial_grid.get_cells_in_bounds(center, inner_window_x_radius, inner_window_y_radius) + var/list/datum/spatial_grid_cell/outer_window = SSspatial_grid.get_cells_in_bounds(center, outer_window_x_radius, outer_window_y_radius) + + var/list/datum/spatial_grid_cell/new_cells = inner_window - member_cells + // The outer window may contain cells we don't actually have, so we do it like this + var/list/datum/spatial_grid_cell/old_cells = member_cells - outer_window + + // This whole thing is a naive implementation, + // if it turns out to be expensive because of all the list operations I'll look closer at it + member_cells -= old_cells + member_cells += new_cells + + return list(new_cells, old_cells) diff --git a/code/modules/vending/_vending.dm b/code/modules/vending/_vending.dm index f8ea167a3e9c..016e642c91e2 100644 --- a/code/modules/vending/_vending.dm +++ b/code/modules/vending/_vending.dm @@ -247,7 +247,7 @@ IF YOU MODIFY THE PRODUCTS LIST OF A MACHINE, MAKE SURE TO UPDATE ITS RESUPPLY C onstation = circuit.onstation //if it was constructed outside mapload, sync the vendor up with the circuit's var so you can't bypass price requirements by moving / reconstructing it off station. if(isnull(alertradio)) alertradio = new(src) - alertradio.listening = 0 + alertradio.set_listening(FALSE) alertradio.set_frequency(FREQ_SECURITY) /obj/machinery/vending/Destroy() diff --git a/yogstation.dme b/yogstation.dme index 58551705d9d9..c5caca41c067 100644 --- a/yogstation.dme +++ b/yogstation.dme @@ -80,6 +80,7 @@ #include "code\__DEFINES\html_assistant.dm" #include "code\__DEFINES\hud.dm" #include "code\__DEFINES\icon_smoothing.dm" +#include "code\__DEFINES\important_recursive_contents.dm" #include "code\__DEFINES\instruments.dm" #include "code\__DEFINES\interaction_flags.dm" #include "code\__DEFINES\inventory.dm" @@ -144,6 +145,7 @@ #include "code\__DEFINES\space.dm" #include "code\__DEFINES\spaceman_dmm.dm" #include "code\__DEFINES\span.dm" +#include "code\__DEFINES\spatial_gridmap.dm" #include "code\__DEFINES\species_clothing_paths.dm" #include "code\__DEFINES\speech_channels.dm" #include "code\__DEFINES\stat.dm" @@ -204,6 +206,7 @@ #include "code\__DEFINES\dcs\signals\signals_object.dm" #include "code\__DEFINES\dcs\signals\signals_plane_master_group.dm" #include "code\__DEFINES\dcs\signals\signals_song.dm" +#include "code\__DEFINES\dcs\signals\signals_spatial_grid.dm" #include "code\__DEFINES\dcs\signals\signals_spell.dm" #include "code\__DEFINES\dcs\signals\signals_storage.dm" #include "code\__DEFINES\dcs\signals\signals_subsystem.dm" @@ -302,6 +305,7 @@ #include "code\__HELPERS\screen_objs.dm" #include "code\__HELPERS\see_through_maps.dm" #include "code\__HELPERS\shell.dm" +#include "code\__HELPERS\spatial_info.dm" #include "code\__HELPERS\stat_tracking.dm" #include "code\__HELPERS\string_assoc_lists.dm" #include "code\__HELPERS\text.dm" @@ -474,6 +478,7 @@ #include "code\controllers\subsystem\shuttle.dm" #include "code\controllers\subsystem\sounds.dm" #include "code\controllers\subsystem\spacedrift.dm" +#include "code\controllers\subsystem\spatial_gridmap.dm" #include "code\controllers\subsystem\statpanel.dm" #include "code\controllers\subsystem\stickyban.dm" #include "code\controllers\subsystem\sun.dm" @@ -3735,6 +3740,7 @@ #include "code\modules\shuttle\shuttle_creation\shuttle_creator_eye.dm" #include "code\modules\shuttle\shuttle_creation\shuttle_creator_overlay.dm" #include "code\modules\shuttle\shuttle_creation\shuttle_upgrades.dm" +#include "code\modules\spatial_grid\cell_tracker.dm" #include "code\modules\spells\spell.dm" #include "code\modules\spells\spell_types\devil.dm" #include "code\modules\spells\spell_types\devil_boons.dm" diff --git a/yogstation/code/game/objects/items/devices/powersink.dm b/yogstation/code/game/objects/items/devices/powersink.dm index a44bc3a0d2cc..63b7873578b5 100644 --- a/yogstation/code/game/objects/items/devices/powersink.dm +++ b/yogstation/code/game/objects/items/devices/powersink.dm @@ -52,7 +52,7 @@ GLOBAL_VAR_INIT(powersink_transmitted, 0) . = ..() alert_radio = new(src) alert_radio.make_syndie() - alert_radio.listening = FALSE + alert_radio.set_listening(FALSE) alert_radio.canhear_range = 0 /obj/item/powersink/infiltrator/on_drain(drained) diff --git a/yogstation/code/game/objects/items/devices/radio/radio.dm b/yogstation/code/game/objects/items/devices/radio/radio.dm index cd340b8fb719..ebde06782ddb 100644 --- a/yogstation/code/game/objects/items/devices/radio/radio.dm +++ b/yogstation/code/game/objects/items/devices/radio/radio.dm @@ -5,6 +5,7 @@ icon_state = "walkietalkiesec" item_state = "walkietalkiesec" freerange = TRUE - frequency = 1359 + var/radio_freq =1359 + set_frequency(radio_freq) freqlock = TRUE - keyslot = /obj/item/encryptionkey/headset_sec \ No newline at end of file + keyslot = /obj/item/encryptionkey/headset_sec diff --git a/yogstation/code/game/objects/items/implants/implant_infiltrator.dm b/yogstation/code/game/objects/items/implants/implant_infiltrator.dm index d4738a22850b..43228188e840 100644 --- a/yogstation/code/game/objects/items/implants/implant_infiltrator.dm +++ b/yogstation/code/game/objects/items/implants/implant_infiltrator.dm @@ -13,7 +13,7 @@ uplink.set_antagonist(ROLE_INFILTRATOR) alert_radio = new(src) alert_radio.make_syndie() - alert_radio.listening = FALSE + alert_radio.set_listening(FALSE) alert_radio.canhear_range = 0 alert_radio.set_frequency(FREQ_SYNDICATE) alert_radio.name = "infiltration cruiser autopilot" diff --git a/yogstation/code/modules/clothing/masks/hailer.dm b/yogstation/code/modules/clothing/masks/hailer.dm index 04a411f2fe14..fa93539e97ab 100644 --- a/yogstation/code/modules/clothing/masks/hailer.dm +++ b/yogstation/code/modules/clothing/masks/hailer.dm @@ -23,7 +23,7 @@ GLOB.sechailers += src radio = new(src) radio.keyslot = new radio_key - radio.listening = FALSE + radio.set_listening(FALSE) radio.recalculateChannels() /obj/item/clothing/mask/gas/sechailer/Destroy()