mirror of
https://github.com/yogstation13/Yogstation.git
synced 2025-02-26 09:04:50 +00:00
Spiritual successor and extension to #17798, an almost entire rebuild of the SQL ban system backend and interface. Bantypes are removed per #8584 and #6174. All bans are now 'role bans', server bans are when a ban's role is server. Admin bans are a column, meaning it's possible to ban admins from jobs. Bans now have only an expiry datetime, duration is calculated from this when queried. unbanned column is removed as it's superfluous, checking unban status is now done through checking unban_datetime. unban_round_id column added. Each ip and computerid columns rearranged so ip is always first, like in other tables. Bans now permit a null ckey, ip and computerid. Ban checking is split into two procs now is_banned_from() does a check if a ckey is banned from one or more roles and returns true or false. This effectively replaces jobban_isbanned() used in simple if() statements. If connected a client's ban cache is checked rather than querying the DB. This makes it possible for a client connected to two or more servers to ignore any bans made on one server until their ban cache is rebuilt on the others. Could be avoided with cross-server calls to update ban caches or just the removal of the ban cache but as is I've done neither since I think it's enough of an edge case to not be worth it. The second proc is is_banned_from_with_details(), this queries the DB for a role ban on a player's ckey, ip or CID and returns the details. This replaces direct queries in IsBanned.dm and the preferences menu. The legacy ban system is removed. The interfaces for banning, unbanning and editing bans have been remade to require less clicking and easier simultaneous operations. The banning and jobban panel are combined. They also store player connection details when opened so a client disconnecting no longer stops a ban being placed. New banning panel: Key, IP and CID can all be toggled to allow excluding them from a ban. Checking Use IP and CID from last connection lets you enter only a ckey and have the DB fill these fields in for you, if possible. Temporary bans have a drop-menu which lets you select between seconds, minutes, hours, days, weeks, months and years so you don't need to calculate how many minutes a long ban would be. The ban is still converted into minutes on the DB however. Checking any of the head roles will check both of the boxes for you. The red role box indicates there is already a ban on that role for this ckey. You can apply additional role bans to stack them. New unbanning panel: Unbanning panel is now separate from the banning panel but otherwise functionally the same. Ban editing panel: Actually just a modified banning panel, all the features from it work the same here. You can now edit almost all parameters of a ban instead of just the reason. You can't edit severity as it's not really part of the ban. The panels have been tested but I've not been able to get my local server to be accessible so ban functionality isn't properly confirmed. Plenty of testing will be required as I'd rather not break bans. cl admin: Ban interface rework. The banning and unbanning panels have received a new design which is easier to use and allows multiple role bans to be made at once. prefix: Ban search and unbanning moved to unbanning panel, which is now a separate panel to the old banning panel. /cl
542 lines
16 KiB
Plaintext
542 lines
16 KiB
Plaintext
//supposedly the fastest way to do this according to https://gist.github.com/Giacom/be635398926bb463b42a
|
|
#define RANGE_TURFS(RADIUS, CENTER) \
|
|
block( \
|
|
locate(max(CENTER.x-(RADIUS),1), max(CENTER.y-(RADIUS),1), CENTER.z), \
|
|
locate(min(CENTER.x+(RADIUS),world.maxx), min(CENTER.y+(RADIUS),world.maxy), CENTER.z) \
|
|
)
|
|
|
|
#define Z_TURFS(ZLEVEL) block(locate(1,1,ZLEVEL), locate(world.maxx, world.maxy, ZLEVEL))
|
|
#define CULT_POLL_WAIT 2400
|
|
|
|
/proc/get_area(atom/A)
|
|
if(isarea(A))
|
|
return A
|
|
var/turf/T = get_turf(A)
|
|
return T ? T.loc : null
|
|
|
|
/proc/get_area_name(atom/X, format_text = FALSE)
|
|
var/area/A = isarea(X) ? X : get_area(X)
|
|
if(!A)
|
|
return null
|
|
return format_text ? format_text(A.name) : A.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(.)
|
|
|
|
/proc/get_open_turf_in_dir(atom/center, dir)
|
|
var/turf/open/T = get_ranged_target_turf(center, dir, 1)
|
|
if(istype(T))
|
|
return T
|
|
|
|
/proc/get_adjacent_open_turfs(atom/center)
|
|
. = list(get_open_turf_in_dir(center, NORTH),
|
|
get_open_turf_in_dir(center, SOUTH),
|
|
get_open_turf_in_dir(center, EAST),
|
|
get_open_turf_in_dir(center, WEST))
|
|
listclearnulls(.)
|
|
|
|
/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)
|
|
|
|
// 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)
|
|
return sqrt(abs(Ax - Bx)**2 + abs(Ay - By)**2) //A squared + B squared = C squared
|
|
|
|
/proc/circlerange(center=usr,radius=3)
|
|
|
|
var/turf/centerturf = get_turf(center)
|
|
var/list/turfs = new/list()
|
|
var/rsq = radius * (radius+0.5)
|
|
|
|
for(var/atom/T in range(radius, centerturf))
|
|
var/dx = T.x - centerturf.x
|
|
var/dy = T.y - centerturf.y
|
|
if(dx*dx + dy*dy <= rsq)
|
|
turfs += T
|
|
|
|
//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
|
|
|
|
/proc/circlerangeturfs(center=usr,radius=3)
|
|
|
|
var/turf/centerturf = get_turf(center)
|
|
var/list/turfs = new/list()
|
|
var/rsq = radius * (radius+0.5)
|
|
|
|
for(var/turf/T in range(radius, centerturf))
|
|
var/dx = T.x - centerturf.x
|
|
var/dy = T.y - centerturf.y
|
|
if(dx*dx + dy*dy <= rsq)
|
|
turfs += T
|
|
return turfs
|
|
|
|
/proc/circleviewturfs(center=usr,radius=3) //Is there even a diffrence between this proc and circlerangeturfs()?
|
|
|
|
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()
|
|
while(processing_list.len)
|
|
var/atom/A = processing_list[1]
|
|
if(A.flags_1 & HEAR_1)
|
|
. += A
|
|
processing_list.Cut(1, 2)
|
|
processing_list += A.contents
|
|
|
|
// Better recursive loop, technically sort of not actually recursive cause that shit is retarded, 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
|
|
|
|
while(processing_list.len) // recursive_hear_check inlined here
|
|
var/atom/A = processing_list[1]
|
|
if(A.flags_1 & HEAR_1)
|
|
. += A
|
|
processing_list.Cut(1, 2)
|
|
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(T.opacity)
|
|
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<X2)
|
|
b+=m
|
|
while(X1!=X2 || Y1!=Y2)
|
|
if(round(m*X1+b-Y1))
|
|
Y1+=signY //Line exits tile vertically
|
|
else
|
|
X1+=signX //Line exits tile horizontally
|
|
T=locate(X1,Y1,Z)
|
|
if(T.opacity)
|
|
return 0
|
|
return 1
|
|
#undef SIGNV
|
|
|
|
|
|
/proc/isInSight(atom/A, atom/B)
|
|
var/turf/Aturf = get_turf(A)
|
|
var/turf/Bturf = get_turf(B)
|
|
|
|
if(!Aturf || !Bturf)
|
|
return 0
|
|
|
|
if(inLineOfSight(Aturf.x,Aturf.y, Bturf.x,Bturf.y,Aturf.z))
|
|
return 1
|
|
|
|
else
|
|
return 0
|
|
|
|
|
|
/proc/get_cardinal_step_away(atom/start, atom/finish) //returns the position of a step from start away from finish, in one of the cardinal directions
|
|
//returns only NORTH, SOUTH, EAST, or WEST
|
|
var/dx = finish.x - start.x
|
|
var/dy = finish.y - start.y
|
|
if(abs(dy) > abs (dx)) //slope is above 1:1 (move horizontally in a tie)
|
|
if(dy > 0)
|
|
return get_step(start, SOUTH)
|
|
else
|
|
return get_step(start, NORTH)
|
|
else
|
|
if(dx > 0)
|
|
return get_step(start, WEST)
|
|
else
|
|
return get_step(start, EAST)
|
|
|
|
|
|
/proc/try_move_adjacent(atom/movable/AM)
|
|
var/turf/T = get_turf(AM)
|
|
for(var/direction in GLOB.cardinals)
|
|
if(AM.Move(get_step(T, direction)))
|
|
break
|
|
|
|
/proc/get_mob_by_key(key)
|
|
var/ckey = ckey(key)
|
|
for(var/i in GLOB.player_list)
|
|
var/mob/M = i
|
|
if(M.ckey == ckey)
|
|
return M
|
|
return null
|
|
|
|
/proc/considered_alive(datum/mind/M, enforce_human = TRUE)
|
|
if(M && M.current)
|
|
if(enforce_human)
|
|
var/mob/living/carbon/human/H
|
|
if(ishuman(M.current))
|
|
H = M.current
|
|
return M.current.stat != DEAD && !issilicon(M.current) && !isbrain(M.current) && (!H || H.dna.species.id != "memezombies")
|
|
else if(isliving(M.current))
|
|
return M.current.stat != DEAD
|
|
return FALSE
|
|
|
|
/proc/considered_afk(datum/mind/M)
|
|
return !M || !M.current || !M.current.client || M.current.client.is_afk()
|
|
|
|
/proc/ScreenText(obj/O, maptext="", screen_loc="CENTER-7,CENTER-7", maptext_height=480, maptext_width=480)
|
|
if(!isobj(O))
|
|
O = new /obj/screen/text()
|
|
O.maptext = maptext
|
|
O.maptext_height = maptext_height
|
|
O.maptext_width = maptext_width
|
|
O.screen_loc = screen_loc
|
|
return O
|
|
|
|
/proc/remove_images_from_clients(image/I, list/show_to)
|
|
for(var/client/C in show_to)
|
|
C.images -= I
|
|
|
|
/proc/flick_overlay(image/I, list/show_to, duration)
|
|
for(var/client/C in show_to)
|
|
C.images += I
|
|
addtimer(CALLBACK(GLOBAL_PROC, /proc/remove_images_from_clients, I, show_to), duration, TIMER_CLIENT_TIME)
|
|
|
|
/proc/flick_overlay_view(image/I, atom/target, duration) //wrapper for the above, flicks to everyone who can see the target atom
|
|
var/list/viewing = list()
|
|
for(var/m in viewers(target))
|
|
var/mob/M = m
|
|
if(M.client)
|
|
viewing += M.client
|
|
flick_overlay(I, viewing, duration)
|
|
|
|
/proc/get_active_player_count(var/alive_check = 0, var/afk_check = 0, var/human_check = 0)
|
|
// Get active players who are playing in the round
|
|
var/active_players = 0
|
|
for(var/i = 1; i <= GLOB.player_list.len; i++)
|
|
var/mob/M = GLOB.player_list[i]
|
|
if(M && M.client)
|
|
if(alive_check && M.stat)
|
|
continue
|
|
else if(afk_check && M.client.is_afk())
|
|
continue
|
|
else if(human_check && !ishuman(M))
|
|
continue
|
|
else if(isnewplayer(M)) // exclude people in the lobby
|
|
continue
|
|
else if(isobserver(M)) // Ghosts are fine if they were playing once (didn't start as observers)
|
|
var/mob/dead/observer/O = M
|
|
if(O.started_as_observer) // Exclude people who started as observers
|
|
continue
|
|
active_players++
|
|
return active_players
|
|
|
|
/proc/showCandidatePollWindow(mob/M, poll_time, Question, list/candidates, ignore_category, time_passed, flashwindow = TRUE)
|
|
set waitfor = 0
|
|
|
|
SEND_SOUND(M, 'sound/misc/notice2.ogg') //Alerting them to their consideration
|
|
if(flashwindow)
|
|
window_flash(M.client)
|
|
switch(ignore_category ? askuser(M,Question,"Please answer in [DisplayTimeText(poll_time)]!","Yes","No","Never for this round", StealFocus=0, Timeout=poll_time) : askuser(M,Question,"Please answer in [DisplayTimeText(poll_time)]!","Yes","No", StealFocus=0, Timeout=poll_time))
|
|
if(1)
|
|
to_chat(M, "<span class='notice'>Choice registered: Yes.</span>")
|
|
if(time_passed + poll_time <= world.time)
|
|
to_chat(M, "<span class='danger'>Sorry, you answered too late to be considered!</span>")
|
|
SEND_SOUND(M, 'sound/machines/buzz-sigh.ogg')
|
|
candidates -= M
|
|
else
|
|
candidates += M
|
|
if(2)
|
|
to_chat(M, "<span class='danger'>Choice registered: No.</span>")
|
|
candidates -= M
|
|
if(3)
|
|
var/list/L = GLOB.poll_ignore[ignore_category]
|
|
if(!L)
|
|
GLOB.poll_ignore[ignore_category] = list()
|
|
GLOB.poll_ignore[ignore_category] += M.ckey
|
|
to_chat(M, "<span class='danger'>Choice registered: Never for this round.</span>")
|
|
candidates -= M
|
|
else
|
|
candidates -= M
|
|
|
|
/proc/pollGhostCandidates(Question, jobbanType, datum/game_mode/gametypeCheck, be_special_flag = 0, poll_time = 300, ignore_category = null, flashwindow = TRUE)
|
|
var/list/candidates = list()
|
|
|
|
for(var/mob/dead/observer/G in GLOB.player_list)
|
|
candidates += G
|
|
|
|
return pollCandidates(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category, flashwindow, candidates)
|
|
|
|
/proc/pollCandidates(Question, jobbanType, datum/game_mode/gametypeCheck, be_special_flag = 0, poll_time = 300, ignore_category = null, flashwindow = TRUE, list/group = null)
|
|
var/time_passed = world.time
|
|
if (!Question)
|
|
Question = "Would you like to be a special role?"
|
|
var/list/result = list()
|
|
for(var/m in group)
|
|
var/mob/M = m
|
|
if(!M.key || !M.client || (ignore_category && GLOB.poll_ignore[ignore_category] && M.ckey in GLOB.poll_ignore[ignore_category]))
|
|
continue
|
|
if(be_special_flag)
|
|
if(!(M.client.prefs) || !(be_special_flag in M.client.prefs.be_special))
|
|
continue
|
|
if(gametypeCheck)
|
|
if(!gametypeCheck.age_check(M.client))
|
|
continue
|
|
if(jobbanType)
|
|
if(is_banned_from(M.ckey, list(jobbanType, ROLE_SYNDICATE)) || QDELETED(M))
|
|
continue
|
|
|
|
showCandidatePollWindow(M, poll_time, Question, result, ignore_category, time_passed, flashwindow)
|
|
sleep(poll_time)
|
|
|
|
//Check all our candidates, to make sure they didn't log off or get deleted during the wait period.
|
|
for(var/mob/M in result)
|
|
if(!M.key || !M.client)
|
|
result -= M
|
|
|
|
listclearnulls(result)
|
|
|
|
return result
|
|
|
|
/proc/pollCandidatesForMob(Question, jobbanType, datum/game_mode/gametypeCheck, be_special_flag = 0, poll_time = 300, mob/M, ignore_category = null)
|
|
var/list/L = pollGhostCandidates(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category)
|
|
if(!M || QDELETED(M) || !M.loc)
|
|
return list()
|
|
return L
|
|
|
|
/proc/pollCandidatesForMobs(Question, jobbanType, datum/game_mode/gametypeCheck, be_special_flag = 0, poll_time = 300, list/mobs, ignore_category = null)
|
|
var/list/L = pollGhostCandidates(Question, jobbanType, gametypeCheck, be_special_flag, poll_time, ignore_category)
|
|
var/i=1
|
|
for(var/v in mobs)
|
|
var/atom/A = v
|
|
if(!A || QDELETED(A) || !A.loc)
|
|
mobs.Cut(i,i+1)
|
|
else
|
|
++i
|
|
return L
|
|
|
|
/proc/poll_helper(var/mob/living/M)
|
|
|
|
/proc/makeBody(mob/dead/observer/G_found) // Uses stripped down and bastardized code from respawn character
|
|
if(!G_found || !G_found.key)
|
|
return
|
|
|
|
//First we spawn a dude.
|
|
var/mob/living/carbon/human/new_character = new//The mob being spawned.
|
|
SSjob.SendToLateJoin(new_character)
|
|
|
|
G_found.client.prefs.copy_to(new_character)
|
|
new_character.dna.update_dna_identity()
|
|
new_character.key = G_found.key
|
|
|
|
return new_character
|
|
|
|
/proc/send_to_playing_players(thing) //sends a whatever to all playing players; use instead of to_chat(world, where needed)
|
|
for(var/M in GLOB.player_list)
|
|
if(M && !isnewplayer(M))
|
|
to_chat(M, thing)
|
|
|
|
/proc/window_flash(client/C, ignorepref = FALSE)
|
|
if(ismob(C))
|
|
var/mob/M = C
|
|
if(M.client)
|
|
C = M.client
|
|
if(!C || (!C.prefs.windowflashing && !ignorepref))
|
|
return
|
|
winset(C, "mainwindow", "flash=5")
|
|
|
|
//Recursively checks if an item is inside a given type, even through layers of storage. Returns the atom if it finds it.
|
|
/proc/recursive_loc_check(atom/movable/target, type)
|
|
var/atom/A = target
|
|
while(!istype(A.loc, type))
|
|
if(!A.loc)
|
|
return
|
|
A = A.loc
|
|
return A
|
|
|
|
/proc/AnnounceArrival(var/mob/living/carbon/human/character, var/rank)
|
|
if(!SSticker.IsRoundInProgress() || QDELETED(character))
|
|
return
|
|
var/area/A = get_area(character)
|
|
var/message = "<span class='game deadsay'><span class='name'>\
|
|
[character.real_name]</span> ([rank]) has arrived at the station at \
|
|
<span class='name'>[A.name]</span>.</span>"
|
|
deadchat_broadcast(message, follow_target = character, message_type=DEADCHAT_ARRIVALRATTLE)
|
|
if((!GLOB.announcement_systems.len) || (!character.mind))
|
|
return
|
|
if((character.mind.assigned_role == "Cyborg") || (character.mind.assigned_role == character.mind.special_role))
|
|
return
|
|
|
|
var/obj/machinery/announcement_system/announcer = pick(GLOB.announcement_systems)
|
|
announcer.announce("ARRIVAL", character.real_name, rank, list()) //make the list empty to make it announce it in common
|
|
|
|
/proc/GetRedPart(const/hexa)
|
|
return hex2num(copytext(hexa, 2, 4))
|
|
|
|
/proc/GetGreenPart(const/hexa)
|
|
return hex2num(copytext(hexa, 4, 6))
|
|
|
|
/proc/GetBluePart(const/hexa)
|
|
return hex2num(copytext(hexa, 6, 8))
|
|
|
|
/proc/lavaland_equipment_pressure_check(turf/T)
|
|
. = FALSE
|
|
if(!istype(T))
|
|
return
|
|
var/datum/gas_mixture/environment = T.return_air()
|
|
if(!istype(environment))
|
|
return
|
|
var/pressure = environment.return_pressure()
|
|
if(pressure <= LAVALAND_EQUIPMENT_EFFECT_PRESSURE)
|
|
. = TRUE
|