Merge branch 'master' of https://github.com/Citadel-Station-13/Citadel-Station-13 into dissection
This commit is contained in:
@@ -88,17 +88,50 @@
|
||||
return
|
||||
close_machine(target)
|
||||
|
||||
/obj/machinery/sleeper/attackby(obj/item/I, mob/user, params)
|
||||
if(!state_open && !occupant)
|
||||
if(default_deconstruction_screwdriver(user, "[initial(icon_state)]-o", initial(icon_state), I))
|
||||
return
|
||||
/obj/machinery/sleeper/screwdriver_act(mob/living/user, obj/item/I)
|
||||
. = TRUE
|
||||
if(..())
|
||||
return
|
||||
if(occupant)
|
||||
to_chat(user, "<span class='warning'>[src] is currently occupied!</span>")
|
||||
return
|
||||
if(state_open)
|
||||
to_chat(user, "<span class='warning'>[src] must be closed to [panel_open ? "close" : "open"] its maintenance hatch!</span>")
|
||||
return
|
||||
if(default_deconstruction_screwdriver(user, "[initial(icon_state)]-o", initial(icon_state), I))
|
||||
return
|
||||
return FALSE
|
||||
|
||||
/obj/machinery/sleeper/wrench_act(mob/living/user, obj/item/I)
|
||||
. = ..()
|
||||
if(default_change_direction_wrench(user, I))
|
||||
return
|
||||
return TRUE
|
||||
|
||||
/obj/machinery/sleeper/crowbar_act(mob/living/user, obj/item/I)
|
||||
. = ..()
|
||||
if(default_pry_open(I))
|
||||
return
|
||||
return TRUE
|
||||
if(default_deconstruction_crowbar(I))
|
||||
return TRUE
|
||||
|
||||
/obj/machinery/sleeper/default_pry_open(obj/item/I) //wew
|
||||
. = !(state_open || panel_open || (flags_1 & NODECONSTRUCT_1)) && I.tool_behaviour == TOOL_CROWBAR
|
||||
if(.)
|
||||
I.play_tool_sound(src, 50)
|
||||
visible_message("<span class='notice'>[usr] pries open [src].</span>", "<span class='notice'>You pry open [src].</span>")
|
||||
open_machine()
|
||||
|
||||
/obj/machinery/sleeper/AltClick(mob/user)
|
||||
if(!user.canUseTopic(src, !issilicon(user)))
|
||||
return
|
||||
return ..()
|
||||
if(state_open)
|
||||
close_machine()
|
||||
else
|
||||
open_machine()
|
||||
|
||||
/obj/machinery/sleeper/examine(mob/user)
|
||||
..()
|
||||
to_chat(user, "<span class='notice'>Alt-click [src] to [state_open ? "close" : "open"] it.</span>")
|
||||
|
||||
/obj/machinery/sleeper/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, \
|
||||
datum/tgui/master_ui = null, datum/ui_state/state = GLOB.notcontained_state)
|
||||
@@ -146,7 +179,7 @@
|
||||
data["occupant"]["toxLoss"] = mob_occupant.getToxLoss()
|
||||
data["occupant"]["fireLoss"] = mob_occupant.getFireLoss()
|
||||
data["occupant"]["cloneLoss"] = mob_occupant.getCloneLoss()
|
||||
data["occupant"]["brainLoss"] = mob_occupant.getBrainLoss()
|
||||
data["occupant"]["brainLoss"] = mob_occupant.getOrganLoss(ORGAN_SLOT_BRAIN)
|
||||
data["occupant"]["reagents"] = list()
|
||||
if(mob_occupant.reagents && mob_occupant.reagents.reagent_list.len)
|
||||
for(var/datum/reagent/R in mob_occupant.reagents.reagent_list)
|
||||
@@ -190,7 +223,7 @@
|
||||
if(inject_chem(chem, usr))
|
||||
. = TRUE
|
||||
if(scrambled_chems && prob(5))
|
||||
to_chat(usr, "<span class='warning'>Chem System Re-route detected, results may not be as expected!</span>")
|
||||
to_chat(usr, "<span class='warning'>Chemical system re-route detected, results may not be as expected!</span>")
|
||||
|
||||
/obj/machinery/sleeper/emag_act(mob/user)
|
||||
. = ..()
|
||||
|
||||
@@ -186,6 +186,7 @@ Class Procs:
|
||||
if(isliving(A))
|
||||
var/mob/living/L = A
|
||||
L.update_canmove()
|
||||
SEND_SIGNAL(src, COMSIG_MACHINE_EJECT_OCCUPANT, occupant)
|
||||
occupant = null
|
||||
|
||||
/obj/machinery/proc/can_be_occupant(atom/movable/am)
|
||||
@@ -498,6 +499,7 @@ Class Procs:
|
||||
/obj/machinery/Exited(atom/movable/AM, atom/newloc)
|
||||
. = ..()
|
||||
if (AM == occupant)
|
||||
SEND_SIGNAL(src, COMSIG_MACHINE_EJECT_OCCUPANT, occupant)
|
||||
occupant = null
|
||||
|
||||
/obj/machinery/proc/adjust_item_drop_location(atom/movable/AM) // Adjust item drop location to a 3x3 grid inside the tile, returns slot id from 0 to 8
|
||||
@@ -566,6 +568,7 @@ Class Procs:
|
||||
updateUsrDialog()
|
||||
|
||||
/obj/machinery/AltClick(mob/user)
|
||||
. = ..()
|
||||
if(!user.canUseTopic(src, !issilicon(user)) || !is_operational())
|
||||
return
|
||||
if(inserted_modify_id)
|
||||
|
||||
@@ -242,13 +242,14 @@
|
||||
var/obj/item/I = pick_n_take(unattached_flesh)
|
||||
if(isorgan(I))
|
||||
var/obj/item/organ/O = I
|
||||
O.organ_flags &= ~ORGAN_FROZEN
|
||||
O.Insert(mob_occupant)
|
||||
else if(isbodypart(I))
|
||||
var/obj/item/bodypart/BP = I
|
||||
BP.attach_limb(mob_occupant)
|
||||
|
||||
//Premature clones may have brain damage.
|
||||
mob_occupant.adjustBrainLoss(-((speed_coeff / 2) * dmg_mult))
|
||||
mob_occupant.adjustOrganLoss(ORGAN_SLOT_BRAIN, -((speed_coeff / 2) * dmg_mult))
|
||||
|
||||
use_power(7500) //This might need tweaking.
|
||||
|
||||
@@ -262,6 +263,7 @@
|
||||
for(var/i in unattached_flesh)
|
||||
if(isorgan(i))
|
||||
var/obj/item/organ/O = i
|
||||
O.organ_flags &= ~ORGAN_FROZEN
|
||||
O.Insert(mob_occupant)
|
||||
else if(isbodypart(i))
|
||||
var/obj/item/bodypart/BP = i
|
||||
@@ -347,6 +349,9 @@
|
||||
if(mess) //Clean that mess and dump those gibs!
|
||||
for(var/obj/fl in unattached_flesh)
|
||||
fl.forceMove(T)
|
||||
if(istype(fl, /obj/item/organ))
|
||||
var/obj/item/organ/O = fl
|
||||
O.organ_flags &= ~ORGAN_FROZEN
|
||||
unattached_flesh.Cut()
|
||||
mess = FALSE
|
||||
new /obj/effect/gibspawner/generic(get_turf(src))
|
||||
@@ -444,7 +449,7 @@
|
||||
unattached_flesh.Cut()
|
||||
|
||||
H.setCloneLoss(CLONE_INITIAL_DAMAGE) //Yeah, clones start with very low health, not with random, because why would they start with random health
|
||||
H.setBrainLoss(CLONE_INITIAL_DAMAGE)
|
||||
//H.setOrganLoss(ORGAN_SLOT_BRAIN, CLONE_INITIAL_DAMAGE)
|
||||
// In addition to being cellularly damaged and having barely any
|
||||
|
||||
// brain function, they also have no limbs or internal organs.
|
||||
@@ -460,8 +465,9 @@
|
||||
|
||||
for(var/o in H.internal_organs)
|
||||
var/obj/item/organ/organ = o
|
||||
if(!istype(organ) || organ.vital)
|
||||
if(!istype(organ) || (organ.organ_flags & ORGAN_VITAL))
|
||||
continue
|
||||
organ.organ_flags |= ORGAN_FROZEN
|
||||
organ.Remove(H, special=TRUE)
|
||||
organ.forceMove(src)
|
||||
unattached_flesh += organ
|
||||
|
||||
@@ -374,7 +374,6 @@ GLOBAL_VAR_INIT(time_last_changed_position, 0)
|
||||
else
|
||||
if((ACCESS_HOP in scan.access) && ((target_dept==1) || !target_dept))
|
||||
region_access |= 1
|
||||
region_access |= 6
|
||||
get_subordinates("Head of Personnel")
|
||||
if((ACCESS_HOS in scan.access) && ((target_dept==2) || !target_dept))
|
||||
region_access |= 2
|
||||
@@ -388,6 +387,9 @@ GLOBAL_VAR_INIT(time_last_changed_position, 0)
|
||||
if((ACCESS_CE in scan.access) && ((target_dept==5) || !target_dept))
|
||||
region_access |= 5
|
||||
get_subordinates("Chief Engineer")
|
||||
if((ACCESS_QM in scan.access) && ((target_dept==6) || !target_dept))
|
||||
region_access |= 6
|
||||
get_subordinates("Quartermaster")
|
||||
if(region_access)
|
||||
authenticated = 1
|
||||
else if ((!( authenticated ) && issilicon(usr)) && (!modify))
|
||||
@@ -607,7 +609,7 @@ GLOBAL_VAR_INIT(time_last_changed_position, 0)
|
||||
typed_circuit.target_dept = target_dept
|
||||
else
|
||||
target_dept = typed_circuit.target_dept
|
||||
var/list/dept_list = list("general","security","medical","science","engineering")
|
||||
var/list/dept_list = list("civilian","security","medical","science","engineering","cargo")
|
||||
name = "[dept_list[target_dept]] department console"
|
||||
|
||||
/obj/machinery/computer/card/minor/hos
|
||||
@@ -631,3 +633,9 @@ GLOBAL_VAR_INIT(time_last_changed_position, 0)
|
||||
icon_screen = "idce"
|
||||
|
||||
light_color = LIGHT_COLOR_YELLOW
|
||||
|
||||
/obj/machinery/computer/card/minor/qm
|
||||
target_dept = 6
|
||||
icon_screen = "idqm"
|
||||
|
||||
light_color = LIGHT_COLOR_ORANGE
|
||||
@@ -159,11 +159,11 @@
|
||||
|
||||
if(scanner && HasEfficientPod() && scanner.scan_level >= AUTOCLONING_MINIMAL_LEVEL)
|
||||
if(!autoprocess)
|
||||
dat += "<a href='byond://?src=[REF(src)];task=autoprocess'>Autoprocess</a>"
|
||||
dat += "<a href='byond://?src=[REF(src)];task=autoprocess'>Autoclone</a>"
|
||||
else
|
||||
dat += "<a href='byond://?src=[REF(src)];task=stopautoprocess'>Stop autoprocess</a>"
|
||||
dat += "<a href='byond://?src=[REF(src)];task=stopautoprocess'>Stop autoclone</a>"
|
||||
else
|
||||
dat += "<span class='linkOff'>Autoprocess</span>"
|
||||
dat += "<span class='linkOff'>Autoclone</span>"
|
||||
dat += "<h3>Cloning Pod Status</h3>"
|
||||
dat += "<div class='statusDisplay'>[temp] </div>"
|
||||
|
||||
@@ -228,7 +228,7 @@
|
||||
dat += "<h4>[src.active_record.fields["name"]]</h4>"
|
||||
dat += "Scan ID [src.active_record.fields["id"]] <a href='byond://?src=[REF(src)];clone=[active_record.fields["id"]]'>Clone</a><br>"
|
||||
|
||||
var/obj/item/implant/health/H = locate(src.active_record.fields["imp"])
|
||||
var/obj/item/implant/health/H = locate(active_record.fields["imp"])
|
||||
|
||||
if ((H) && (istype(H)))
|
||||
dat += "<b>Health Implant Data:</b><br />[H.sensehealth()]<br><br />"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/obj/machinery/computer/prisoner
|
||||
@@ -35,11 +35,11 @@
|
||||
dat += "<HR>Chemical Implants<BR>"
|
||||
var/turf/Tr = null
|
||||
for(var/obj/item/implant/chem/C in GLOB.tracked_chem_implants)
|
||||
Tr = get_turf(C)
|
||||
if((Tr) && (Tr.z != src.z))
|
||||
continue//Out of range
|
||||
if(!C.imp_in)
|
||||
continue
|
||||
Tr = get_turf(C.imp_in)
|
||||
if((Tr) && (Tr.z != src.z))
|
||||
continue//Out of range
|
||||
dat += "ID: [C.imp_in.name] | Remaining Units: [C.reagents.total_volume] <BR>"
|
||||
dat += "| Inject: "
|
||||
dat += "<A href='?src=[REF(src)];inject1=[REF(C)]'>(<font class='bad'>(1)</font>)</A>"
|
||||
@@ -48,9 +48,9 @@
|
||||
dat += "********************************<BR>"
|
||||
dat += "<HR>Tracking Implants<BR>"
|
||||
for(var/obj/item/implant/tracking/T in GLOB.tracked_implants)
|
||||
if(!isliving(T.imp_in))
|
||||
if(!T.imp_in || !isliving(T.imp_in))
|
||||
continue
|
||||
Tr = get_turf(T)
|
||||
Tr = get_turf(T.imp_in)
|
||||
if((Tr) && (Tr.z != src.z))
|
||||
continue//Out of range
|
||||
|
||||
|
||||
@@ -124,14 +124,14 @@
|
||||
L[avoid_assoc_duplicate_keys(A.name, areaindex)] = R
|
||||
|
||||
for(var/obj/item/implant/tracking/I in GLOB.tracked_implants)
|
||||
if(!I.imp_in || !isliving(I.loc))
|
||||
if(!I.imp_in || !isliving(I.imp_in))
|
||||
continue
|
||||
else
|
||||
var/mob/living/M = I.loc
|
||||
var/mob/living/M = I.imp_in
|
||||
if(M.stat == DEAD)
|
||||
if(M.timeofdeath + 6000 < world.time)
|
||||
continue
|
||||
if(is_eligible(I))
|
||||
if(is_eligible(M))
|
||||
L[avoid_assoc_duplicate_keys(M.real_name, areaindex)] = I
|
||||
|
||||
var/desc = input("Please select a location to lock in.", "Locking Computer") as null|anything in L
|
||||
|
||||
@@ -0,0 +1,497 @@
|
||||
/*
|
||||
* Cryogenic refrigeration unit. Basically a despawner.
|
||||
* Stealing a lot of concepts/code from sleepers due to massive laziness.
|
||||
* The despawn tick will only fire if it's been more than time_till_despawned ticks
|
||||
* since time_entered, which is world.time when the occupant moves in.
|
||||
* ~ Zuhayr
|
||||
*/
|
||||
|
||||
|
||||
//Main cryopod console.
|
||||
|
||||
/obj/machinery/computer/cryopod
|
||||
name = "cryogenic oversight console"
|
||||
desc = "An interface between crew and the cryogenic storage oversight systems."
|
||||
icon = 'icons/obj/Cryogenic2.dmi'
|
||||
icon_state = "cellconsole_1"
|
||||
circuit = /obj/item/circuitboard/cryopodcontrol
|
||||
density = FALSE
|
||||
interaction_flags_machine = INTERACT_MACHINE_OFFLINE
|
||||
req_one_access = list(ACCESS_HEADS, ACCESS_ARMORY) //Heads of staff or the warden can go here to claim recover items from their department that people went were cryodormed with.
|
||||
var/mode = null
|
||||
|
||||
var/menu = 1 //Which menu screen to display
|
||||
|
||||
//Used for logging people entering cryosleep and important items they are carrying.
|
||||
var/list/frozen_crew = list()
|
||||
var/list/frozen_items = list()
|
||||
|
||||
// Used for containing rare items traitors need to steal, so it's not
|
||||
// game-over if they get iced
|
||||
var/list/objective_items = list()
|
||||
// A cache of theft datums so you don't have to re-create them for
|
||||
// each item check
|
||||
var/list/theft_cache = list()
|
||||
|
||||
var/allow_items = TRUE
|
||||
|
||||
/obj/machinery/computer/cryopod/attack_ai()
|
||||
attack_hand()
|
||||
|
||||
/obj/machinery/computer/cryopod/ui_interact(mob/user = usr)
|
||||
if(!is_operational())
|
||||
return
|
||||
|
||||
user.set_machine(src)
|
||||
add_fingerprint(user)
|
||||
|
||||
var/dat = ""
|
||||
|
||||
dat += "<h2>Welcome, [user.real_name].</h2><hr/>"
|
||||
dat += "<br><br>"
|
||||
|
||||
switch(src.menu)
|
||||
if(1)
|
||||
dat += "<a href='byond://?src=[REF(src)];menu=2'>View crew storage log</a><br><br>"
|
||||
if(allow_items)
|
||||
dat += "<a href='byond://?src=[REF(src)];menu=3'>View objects storage log</a><br><br>"
|
||||
dat += "<a href='byond://?src=[REF(src)];item=1'>Recover object</a><br><br>"
|
||||
dat += "<a href='byond://?src=[REF(src)];allitems=1'>Recover all objects</a><br>"
|
||||
if(2)
|
||||
dat += "<a href='byond://?src=[REF(src)];menu=1'><< Back</a><br><br>"
|
||||
dat += "<h3>Recently stored Crew</h3><br/><hr/><br/>"
|
||||
if(!frozen_crew.len)
|
||||
dat += "There has been no storage usage at this terminal.<br/>"
|
||||
else
|
||||
for(var/person in frozen_crew)
|
||||
dat += "[person]<br/>"
|
||||
dat += "<hr/>"
|
||||
if(3)
|
||||
dat += "<a href='byond://?src=[REF(src)];menu=1'><< Back</a><br><br>"
|
||||
dat += "<h3>Recently stored objects</h3><br/><hr/><br/>"
|
||||
if(!frozen_items.len)
|
||||
dat += "There has been no storage usage at this terminal.<br/>"
|
||||
else
|
||||
for(var/obj/item/I in frozen_items)
|
||||
dat += "[I.name]<br/>"
|
||||
dat += "<hr/>"
|
||||
|
||||
var/datum/browser/popup = new(user, "cryopod_console", "Cryogenic System Control")
|
||||
popup.set_content(dat)
|
||||
popup.set_title_image(user.browse_rsc_icon(src.icon, src.icon_state))
|
||||
popup.open()
|
||||
|
||||
/obj/machinery/computer/cryopod/Topic(href, href_list)
|
||||
if(..())
|
||||
return TRUE
|
||||
|
||||
var/mob/user = usr
|
||||
|
||||
add_fingerprint(user)
|
||||
|
||||
if(href_list["item"])
|
||||
if(!allowed(user))
|
||||
to_chat(user, "<span class='warning'>Access Denied.</span>")
|
||||
playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, 0)
|
||||
updateUsrDialog()
|
||||
return
|
||||
if(!allow_items) return
|
||||
|
||||
if(frozen_items.len == 0)
|
||||
to_chat(user, "<span class='notice'>There is nothing to recover from storage.</span>")
|
||||
playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, 0)
|
||||
updateUsrDialog()
|
||||
return
|
||||
|
||||
var/obj/item/I = input(user, "Please choose which object to retrieve.","Object recovery",null) as null|anything in frozen_items
|
||||
playsound(src, "terminal_type", 25, 0)
|
||||
if(!I)
|
||||
return
|
||||
|
||||
if(!(I in frozen_items))
|
||||
to_chat(user, "<span class='notice'>\The [I] is no longer in storage.</span>")
|
||||
playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, 0)
|
||||
updateUsrDialog()
|
||||
return
|
||||
|
||||
visible_message("<span class='notice'>The console beeps happily as it disgorges \the [I].</span>")
|
||||
playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, 0)
|
||||
|
||||
I.forceMove(drop_location())
|
||||
if(user && Adjacent(user) && !issiliconoradminghost(user))
|
||||
user.put_in_hands(I)
|
||||
frozen_items -= I
|
||||
updateUsrDialog()
|
||||
|
||||
else if(href_list["allitems"])
|
||||
playsound(src, "terminal_type", 25, 0)
|
||||
if(!allowed(user))
|
||||
to_chat(user, "<span class='warning'>Access Denied.</span>")
|
||||
playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, 0)
|
||||
updateUsrDialog()
|
||||
return
|
||||
if(!allow_items) return
|
||||
|
||||
if(frozen_items.len == 0)
|
||||
to_chat(user, "<span class='notice'>There is nothing to recover from storage.</span>")
|
||||
playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, 0)
|
||||
return
|
||||
|
||||
visible_message("<span class='notice'>The console beeps happily as it disgorges the desired objects.</span>")
|
||||
playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, 0)
|
||||
|
||||
for(var/obj/item/I in frozen_items)
|
||||
I.forceMove(drop_location())
|
||||
frozen_items -= I
|
||||
updateUsrDialog()
|
||||
|
||||
else if (href_list["menu"])
|
||||
src.menu = text2num(href_list["menu"])
|
||||
playsound(src, "terminal_type", 25, 0)
|
||||
updateUsrDialog()
|
||||
|
||||
ui_interact(usr)
|
||||
updateUsrDialog()
|
||||
return
|
||||
|
||||
/obj/item/circuitboard/cryopodcontrol
|
||||
name = "Circuit board (Cryogenic Oversight Console)"
|
||||
build_path = "/obj/machinery/computer/cryopod"
|
||||
|
||||
/obj/machinery/computer/cryopod/contents_explosion()
|
||||
return
|
||||
|
||||
//Cryopods themselves.
|
||||
/obj/machinery/cryopod
|
||||
name = "cryogenic freezer"
|
||||
desc = "Suited for Cyborgs and Humanoids, the pod is a safe place for personnel affected by the Space Sleep Disorder to get some rest."
|
||||
icon = 'icons/obj/cryogenic2.dmi'
|
||||
icon_state = "cryopod-open"
|
||||
density = TRUE
|
||||
anchored = TRUE
|
||||
state_open = TRUE
|
||||
|
||||
var/on_store_message = "has entered long-term storage."
|
||||
var/on_store_name = "Cryogenic Oversight"
|
||||
|
||||
// 15 minutes-ish safe period before being despawned.
|
||||
var/time_till_despawn = 15 * 600 // This is reduced by 90% if a player manually enters cryo
|
||||
var/despawn_world_time = null // Used to keep track of the safe period.
|
||||
|
||||
var/obj/machinery/computer/cryopod/control_computer
|
||||
var/last_no_computer_message = 0
|
||||
|
||||
// These items are preserved when the process() despawn proc occurs.
|
||||
var/list/preserve_items = list(
|
||||
/obj/item/hand_tele,
|
||||
/obj/item/card/id/captains_spare,
|
||||
/obj/item/aicard,
|
||||
/obj/item/mmi,
|
||||
/obj/item/paicard,
|
||||
/obj/item/gun,
|
||||
/obj/item/pinpointer,
|
||||
/obj/item/clothing/shoes/magboots,
|
||||
/obj/item/areaeditor/blueprints,
|
||||
/obj/item/clothing/head/helmet/space,
|
||||
/obj/item/clothing/suit/space,
|
||||
/obj/item/clothing/suit/armor,
|
||||
/obj/item/defibrillator/compact,
|
||||
/obj/item/reagent_containers/hypospray/CMO,
|
||||
/obj/item/clothing/accessory/medal/gold/captain,
|
||||
/obj/item/clothing/gloves/krav_maga,
|
||||
/obj/item/nullrod,
|
||||
/obj/item/tank/jetpack,
|
||||
/obj/item/documents,
|
||||
/obj/item/nuke_core_container
|
||||
)
|
||||
// These items will NOT be preserved
|
||||
var/list/do_not_preserve_items = list (
|
||||
/obj/item/mmi/posibrain,
|
||||
/obj/item/gun/energy/laser/mounted,
|
||||
/obj/item/gun/energy/e_gun/advtaser/mounted,
|
||||
/obj/item/gun/ballistic/revolver/grenadelauncher/cyborg,
|
||||
/obj/item/gun/energy/disabler/cyborg,
|
||||
/obj/item/gun/energy/e_gun/advtaser/cyborg,
|
||||
/obj/item/gun/energy/printer,
|
||||
/obj/item/gun/energy/kinetic_accelerator/cyborg,
|
||||
/obj/item/gun/energy/laser/cyborg
|
||||
)
|
||||
|
||||
/obj/machinery/cryopod/Initialize(mapload)
|
||||
. = ..()
|
||||
update_icon()
|
||||
find_control_computer(mapload)
|
||||
|
||||
/obj/machinery/cryopod/proc/find_control_computer(urgent = FALSE)
|
||||
for(var/obj/machinery/computer/cryopod/C in get_area(src))
|
||||
control_computer = C
|
||||
if(C)
|
||||
return C
|
||||
break
|
||||
|
||||
// Don't send messages unless we *need* the computer, and less than five minutes have passed since last time we messaged
|
||||
if(!control_computer && urgent && last_no_computer_message + 5*60*10 < world.time)
|
||||
log_admin("Cryopod in [get_area(src)] could not find control computer!")
|
||||
message_admins("Cryopod in [get_area(src)] could not find control computer!")
|
||||
last_no_computer_message = world.time
|
||||
|
||||
return control_computer != null
|
||||
|
||||
/obj/machinery/cryopod/close_machine(mob/user)
|
||||
if(!control_computer)
|
||||
find_control_computer(TRUE)
|
||||
if((isnull(user) || istype(user)) && state_open && !panel_open)
|
||||
..(user)
|
||||
var/mob/living/mob_occupant = occupant
|
||||
if(mob_occupant && mob_occupant.stat != DEAD)
|
||||
to_chat(occupant, "<span class='boldnotice'>You feel cool air surround you. You go numb as your senses turn inward.</span>")
|
||||
if(mob_occupant.client)//if they're logged in
|
||||
despawn_world_time = world.time + (time_till_despawn * 0.1)
|
||||
else
|
||||
despawn_world_time = world.time + time_till_despawn
|
||||
icon_state = "cryopod"
|
||||
|
||||
/obj/machinery/cryopod/open_machine()
|
||||
..()
|
||||
icon_state = "cryopod-open"
|
||||
density = TRUE
|
||||
name = initial(name)
|
||||
|
||||
/obj/machinery/cryopod/container_resist(mob/living/user)
|
||||
visible_message("<span class='notice'>[occupant] emerges from [src]!</span>",
|
||||
"<span class='notice'>You climb out of [src]!</span>")
|
||||
open_machine()
|
||||
|
||||
/obj/machinery/cryopod/relaymove(mob/user)
|
||||
container_resist(user)
|
||||
|
||||
/obj/machinery/cryopod/process()
|
||||
if(!occupant)
|
||||
return
|
||||
|
||||
var/mob/living/mob_occupant = occupant
|
||||
if(mob_occupant)
|
||||
// Eject dead people
|
||||
if(mob_occupant.stat == DEAD)
|
||||
open_machine()
|
||||
|
||||
if(!(world.time > despawn_world_time + 100))//+ 10 seconds
|
||||
return
|
||||
|
||||
if(!mob_occupant.client && mob_occupant.stat < 2) //Occupant is living and has no client.
|
||||
if(!control_computer)
|
||||
find_control_computer(urgent = TRUE)//better hope you found it this time
|
||||
|
||||
despawn_occupant()
|
||||
|
||||
#define CRYO_DESTROY 0
|
||||
#define CRYO_PRESERVE 1
|
||||
#define CRYO_OBJECTIVE 2
|
||||
|
||||
/obj/machinery/cryopod/proc/should_preserve_item(obj/item/I)
|
||||
for(var/datum/objective_item/steal/T in control_computer.theft_cache)
|
||||
if(istype(I, T.targetitem) && T.check_special_completion(I))
|
||||
return CRYO_OBJECTIVE
|
||||
for(var/T in preserve_items)
|
||||
if(istype(I, T) && !(I.type in do_not_preserve_items))
|
||||
return CRYO_PRESERVE
|
||||
return CRYO_DESTROY
|
||||
|
||||
// This function can not be undone; do not call this unless you are sure
|
||||
/obj/machinery/cryopod/proc/despawn_occupant()
|
||||
if(!control_computer)
|
||||
find_control_computer()
|
||||
|
||||
var/mob/living/mob_occupant = occupant
|
||||
|
||||
//Handle Borg stuff first
|
||||
if(iscyborg(mob_occupant))
|
||||
var/mob/living/silicon/robot/R = mob_occupant
|
||||
if(!istype(R)) return ..()
|
||||
|
||||
R.contents -= R.mmi
|
||||
qdel(R.mmi)
|
||||
for(var/obj/item/I in R.module) // the tools the borg has; metal, glass, guns etc
|
||||
for(var/obj/item/O in I) // the things inside the tools, if anything; mainly for janiborg trash bags
|
||||
if(should_preserve_item(O) != CRYO_DESTROY) // Preserve important things inside the item
|
||||
continue
|
||||
O.forceMove(src)
|
||||
R.module.remove_module(I, TRUE) //delete the module itself so it doesn't transfer over.
|
||||
|
||||
//Drop all items into the pod.
|
||||
for(var/obj/item/I in mob_occupant)
|
||||
mob_occupant.doUnEquip(I)
|
||||
I.forceMove(src)
|
||||
|
||||
if(I.contents.len) //Make sure we catch anything not handled by qdel() on the items.
|
||||
if(should_preserve_item(I) != CRYO_DESTROY) // Don't remove the contents of things that need preservation
|
||||
continue
|
||||
for(var/obj/item/O in I.contents)
|
||||
if(istype(O, /obj/item/tank)) //Stop eating pockets, you fuck!
|
||||
continue
|
||||
O.forceMove(src)
|
||||
|
||||
//Delete all items not on the preservation list.
|
||||
var/list/items = contents
|
||||
items -= mob_occupant // Don't delete the occupant
|
||||
|
||||
for(var/obj/item/I in items)
|
||||
if(istype(I, /obj/item/pda))
|
||||
var/obj/item/pda/P = I
|
||||
QDEL_NULL(P.id)
|
||||
qdel(P)
|
||||
continue
|
||||
|
||||
var/preserve = should_preserve_item(I)
|
||||
if(preserve == CRYO_DESTROY)
|
||||
qdel(I)
|
||||
else if(control_computer && control_computer.allow_items)
|
||||
control_computer.frozen_items += I
|
||||
if(preserve == CRYO_OBJECTIVE)
|
||||
control_computer.objective_items += I
|
||||
I.loc = null
|
||||
else
|
||||
I.forceMove(loc)
|
||||
|
||||
//Update any existing objectives involving this mob.
|
||||
for(var/datum/objective/O in GLOB.objectives)
|
||||
// We don't want revs to get objectives that aren't for heads of staff. Letting
|
||||
// them win or lose based on cryo is silly so we remove the objective.
|
||||
if(istype(O,/datum/objective/mutiny) && O.target == mob_occupant.mind)
|
||||
qdel(O)
|
||||
else if(O.target && istype(O.target, /datum/mind))
|
||||
if(O.target == mob_occupant.mind)
|
||||
if(O.owner && O.owner.current)
|
||||
to_chat(O.owner.current, "<BR><span class='userdanger'>You get the feeling your target is no longer within reach. Time for Plan [pick("A","B","C","D","X","Y","Z")]. Objectives updated!</span>")
|
||||
O.target = null
|
||||
spawn(10) //This should ideally fire after the occupant is deleted.
|
||||
if(!O)
|
||||
return
|
||||
O.find_target()
|
||||
O.update_explanation_text()
|
||||
if(!(O.target))
|
||||
O.owner.objectives -= O
|
||||
qdel(O)
|
||||
|
||||
if(mob_occupant.mind && mob_occupant.mind.assigned_role)
|
||||
//Handle job slot/tater cleanup.
|
||||
var/job = mob_occupant.mind.assigned_role
|
||||
SSjob.FreeRole(job)
|
||||
if(mob_occupant.mind.objectives.len)
|
||||
mob_occupant.mind.objectives.Cut()
|
||||
mob_occupant.mind.special_role = null
|
||||
|
||||
// Delete them from datacore.
|
||||
|
||||
var/announce_rank = null
|
||||
for(var/datum/data/record/R in GLOB.data_core.medical)
|
||||
if((R.fields["name"] == mob_occupant.real_name))
|
||||
qdel(R)
|
||||
for(var/datum/data/record/T in GLOB.data_core.security)
|
||||
if((T.fields["name"] == mob_occupant.real_name))
|
||||
qdel(T)
|
||||
for(var/datum/data/record/G in GLOB.data_core.general)
|
||||
if((G.fields["name"] == mob_occupant.real_name))
|
||||
announce_rank = G.fields["rank"]
|
||||
qdel(G)
|
||||
|
||||
for(var/obj/machinery/computer/cloning/cloner in world)
|
||||
for(var/datum/data/record/R in cloner.records)
|
||||
if(R.fields["name"] == mob_occupant.real_name)
|
||||
cloner.records.Remove(R)
|
||||
|
||||
//Make an announcement and log the person entering storage.
|
||||
if(control_computer)
|
||||
control_computer.frozen_crew += "[mob_occupant.real_name]"
|
||||
|
||||
if(GLOB.announcement_systems.len)
|
||||
var/obj/machinery/announcement_system/announcer = pick(GLOB.announcement_systems)
|
||||
announcer.announce("CRYOSTORAGE", mob_occupant.real_name, announce_rank, list())
|
||||
visible_message("<span class='notice'>\The [src] hums and hisses as it moves [mob_occupant.real_name] into storage.</span>")
|
||||
|
||||
// Ghost and delete the mob.
|
||||
if(!mob_occupant.get_ghost(1))
|
||||
mob_occupant.ghostize(0) // Players who cryo out may not re-enter the round
|
||||
|
||||
QDEL_NULL(occupant)
|
||||
open_machine()
|
||||
name = initial(name)
|
||||
|
||||
#undef CRYO_DESTROY
|
||||
#undef CRYO_PRESERVE
|
||||
#undef CRYO_OBJECTIVE
|
||||
|
||||
/obj/machinery/cryopod/MouseDrop_T(mob/living/target, mob/user)
|
||||
if(!istype(target) || user.incapacitated() || !target.Adjacent(user) || !Adjacent(user) || !ismob(target) || (!ishuman(user) && !iscyborg(user)) || !istype(user.loc, /turf) || target.buckled)
|
||||
return
|
||||
|
||||
if(occupant)
|
||||
to_chat(user, "<span class='boldnotice'>The cryo pod is already occupied!</span>")
|
||||
return
|
||||
|
||||
if(target.stat == DEAD)
|
||||
to_chat(user, "<span class='notice'>Dead people can not be put into cryo.</span>")
|
||||
return
|
||||
|
||||
if(target.client && user != target)
|
||||
if(iscyborg(target))
|
||||
to_chat(user, "<span class='danger'>You can't put [target] into [src]. They're online.</span>")
|
||||
else
|
||||
to_chat(user, "<span class='danger'>You can't put [target] into [src]. They're conscious.</span>")
|
||||
return
|
||||
else if(target.client)
|
||||
if(alert(target,"Would you like to enter cryosleep?",,"Yes","No") == "No")
|
||||
return
|
||||
|
||||
var/generic_plsnoleave_message = " Please adminhelp before leaving the round, even if there are no administrators online!"
|
||||
|
||||
if(target == user && world.time - target.client.cryo_warned > 5 MINUTES)//if we haven't warned them in the last 5 minutes
|
||||
var/caught = FALSE
|
||||
if(target.mind.assigned_role in GLOB.command_positions)
|
||||
alert("<span class='userdanger'>You're a Head of Staff![generic_plsnoleave_message] Be sure to put your locker items back into your locker!</span>")
|
||||
caught = TRUE
|
||||
if(iscultist(target) || is_servant_of_ratvar(target))
|
||||
to_chat(target, "<span class='userdanger'>You're a Cultist![generic_plsnoleave_message]</span>")
|
||||
caught = TRUE
|
||||
if(is_devil(target))
|
||||
alert("<span class='userdanger'>You're a Devil![generic_plsnoleave_message]</span>")
|
||||
caught = TRUE
|
||||
if(istype(SSticker.mode, /datum/antagonist/gang))
|
||||
if(target.mind.has_antag_datum(/datum/antagonist/gang))
|
||||
alert("<span class='userdanger'>You're a Gangster![generic_plsnoleave_message]</span>")
|
||||
caught = TRUE
|
||||
if(istype(SSticker.mode, /datum/antagonist/rev))
|
||||
if(target.mind.has_antag_datum(/datum/antagonist/rev/head))
|
||||
alert("<span class='userdanger'>You're a Head Revolutionary![generic_plsnoleave_message]</span>")
|
||||
caught = TRUE
|
||||
else if(target.mind.has_antag_datum(/datum/antagonist/rev))
|
||||
alert("<span class='userdanger'>You're a Revolutionary![generic_plsnoleave_message]</span>")
|
||||
caught = TRUE
|
||||
|
||||
if(caught)
|
||||
target.client.cryo_warned = world.time
|
||||
return
|
||||
|
||||
if(!target || user.incapacitated() || !target.Adjacent(user) || !Adjacent(user) || (!ishuman(user) && !iscyborg(user)) || !istype(user.loc, /turf) || target.buckled)
|
||||
return
|
||||
//rerun the checks in case of shenanigans
|
||||
|
||||
if(target == user)
|
||||
visible_message("[user] starts climbing into the cryo pod.")
|
||||
else
|
||||
visible_message("[user] starts putting [target] into the cryo pod.")
|
||||
|
||||
if(occupant)
|
||||
to_chat(user, "<span class='boldnotice'>\The [src] is in use.</span>")
|
||||
return
|
||||
close_machine(target)
|
||||
|
||||
to_chat(target, "<span class='boldnotice'>If you ghost, log out or close your client now, your character will shortly be permanently removed from the round.</span>")
|
||||
name = "[name] ([occupant.name])"
|
||||
log_admin("<span class='notice'>[key_name(target)] entered a stasis pod.</span>")
|
||||
message_admins("[key_name_admin(target)] entered a stasis pod. (<A HREF='?_src_=holder;[HrefToken()];adminplayerobservecoodjump=1;X=[x];Y=[y];Z=[z]'>JMP</a>)")
|
||||
add_fingerprint(target)
|
||||
|
||||
//Attacks/effects.
|
||||
/obj/machinery/cryopod/blob_act()
|
||||
return //Sorta gamey, but we don't really want these to be destroyed.
|
||||
@@ -89,11 +89,11 @@
|
||||
return C
|
||||
return null
|
||||
|
||||
/obj/machinery/dna_scannernew/close_machine(mob/living/carbon/user)
|
||||
/obj/machinery/dna_scannernew/close_machine(atom/movable/target)
|
||||
if(!state_open)
|
||||
return FALSE
|
||||
|
||||
..(user)
|
||||
..(target)
|
||||
|
||||
// search for ghosts, if the corpse is empty and the scanner is connected to a cloner
|
||||
var/mob/living/mob_occupant = get_mob_or_brainmob(occupant)
|
||||
@@ -111,7 +111,7 @@
|
||||
return TRUE
|
||||
|
||||
/obj/machinery/dna_scannernew/open_machine()
|
||||
if(state_open)
|
||||
if(state_open || panel_open)
|
||||
return FALSE
|
||||
|
||||
..()
|
||||
@@ -126,23 +126,48 @@
|
||||
return
|
||||
open_machine()
|
||||
|
||||
/obj/machinery/dna_scannernew/attackby(obj/item/I, mob/user, params)
|
||||
|
||||
if(!occupant && default_deconstruction_screwdriver(user, icon_state, icon_state, I))//sent icon_state is irrelevant...
|
||||
update_icon()//..since we're updating the icon here, since the scanner can be unpowered when opened/closed
|
||||
/obj/machinery/dna_scannernew/screwdriver_act(mob/living/user, obj/item/I)
|
||||
. = TRUE
|
||||
if(..())
|
||||
return
|
||||
if(occupant)
|
||||
to_chat(user, "<span class='warning'>[src] is currently occupied!</span>")
|
||||
return
|
||||
if(state_open)
|
||||
to_chat(user, "<span class='warning'>[src] must be closed to [panel_open ? "close" : "open"] its maintenance hatch!</span>")
|
||||
return
|
||||
if(default_deconstruction_screwdriver(user, icon_state, icon_state, I)) //sent icon_state is irrelevant...
|
||||
update_icon() //..since we're updating the icon here, since the scanner can be unpowered when opened/closed
|
||||
return
|
||||
return FALSE
|
||||
|
||||
/obj/machinery/dna_scannernew/wrench_act(mob/living/user, obj/item/I)
|
||||
. = ..()
|
||||
if(default_change_direction_wrench(user, I))
|
||||
return TRUE
|
||||
|
||||
/obj/machinery/dna_scannernew/crowbar_act(mob/living/user, obj/item/I)
|
||||
. = ..()
|
||||
if(default_pry_open(I))
|
||||
return
|
||||
|
||||
return TRUE
|
||||
if(default_deconstruction_crowbar(I))
|
||||
return
|
||||
return TRUE
|
||||
|
||||
return ..()
|
||||
/obj/machinery/dna_scannernew/default_pry_open(obj/item/I) //wew
|
||||
. = !(state_open || panel_open || (flags_1 & NODECONSTRUCT_1)) && I.tool_behaviour == TOOL_CROWBAR
|
||||
if(.)
|
||||
I.play_tool_sound(src, 50)
|
||||
visible_message("<span class='notice'>[usr] pries open [src].</span>", "<span class='notice'>You pry open [src].</span>")
|
||||
open_machine()
|
||||
|
||||
/obj/machinery/dna_scannernew/interact(mob/user)
|
||||
toggle_open(user)
|
||||
|
||||
/obj/machinery/dna_scannernew/AltClick(mob/user)
|
||||
if(!user.canUseTopic(src, !issilicon(user)))
|
||||
return
|
||||
interact(user)
|
||||
|
||||
/obj/machinery/dna_scannernew/MouseDrop_T(mob/target, mob/user)
|
||||
if(user.stat || user.lying || !Adjacent(user) || !user.Adjacent(target) || !iscarbon(target) || !user.IsAdvancedToolUser())
|
||||
return
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
var/list/candidates = pollCandidatesForMob("Do you want to play as [clonename]'s defective clone?", null, null, null, 100, H)
|
||||
if(LAZYLEN(candidates))
|
||||
var/mob/dead/observer/C = pick(candidates)
|
||||
H.key = C.key
|
||||
C.transfer_ckey(H)
|
||||
|
||||
if(grab_ghost_when == CLONER_FRESH_CLONE)
|
||||
H.grab_ghost()
|
||||
|
||||
@@ -136,16 +136,17 @@ The console is located at computer/gulag_teleporter.dm
|
||||
linked_reclaimer.stored_items[occupant] = list()
|
||||
var/mob/living/mob_occupant = occupant
|
||||
for(var/obj/item/W in mob_occupant)
|
||||
if(!is_type_in_typecache(W, telegulag_required_items) && mob_occupant.temporarilyRemoveItemFromInventory(W))
|
||||
if(istype(W, /obj/item/restraints/handcuffs))
|
||||
W.forceMove(get_turf(src))
|
||||
continue
|
||||
if(linked_reclaimer)
|
||||
linked_reclaimer.stored_items[mob_occupant] += W
|
||||
linked_reclaimer.contents += W
|
||||
W.forceMove(linked_reclaimer)
|
||||
else
|
||||
W.forceMove(src)
|
||||
if(!is_type_in_typecache(W, telegulag_required_items))
|
||||
if(mob_occupant.temporarilyRemoveItemFromInventory(W))
|
||||
if(istype(W, /obj/item/restraints/handcuffs))
|
||||
W.forceMove(get_turf(src))
|
||||
continue
|
||||
if(linked_reclaimer)
|
||||
linked_reclaimer.stored_items[mob_occupant] += W
|
||||
linked_reclaimer.contents += W
|
||||
W.forceMove(linked_reclaimer)
|
||||
else
|
||||
W.forceMove(src)
|
||||
|
||||
/obj/machinery/gulag_teleporter/proc/handle_prisoner(obj/item/id, datum/data/record/R)
|
||||
if(!ishuman(occupant))
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
to_chat(user, "<span class='warning'>[src] is currently occupied!</span>")
|
||||
return
|
||||
if(state_open)
|
||||
to_chat(user, "<span class='warning'>[src] must be closed to [panel_open ? "close" : "open"] it's maintenance hatch!</span>")
|
||||
to_chat(user, "<span class='warning'>[src] must be closed to [panel_open ? "close" : "open"] its maintenance hatch!</span>")
|
||||
return
|
||||
if(default_deconstruction_screwdriver(user, "[initial(icon_state)]-o", initial(icon_state), I))
|
||||
return
|
||||
|
||||
@@ -210,13 +210,13 @@
|
||||
add_fingerprint(user)
|
||||
|
||||
/obj/machinery/suit_storage_unit/proc/cook()
|
||||
var/mob/living/mob_occupant = occupant
|
||||
if(uv_cycles)
|
||||
uv_cycles--
|
||||
uv = TRUE
|
||||
locked = TRUE
|
||||
update_icon()
|
||||
if(occupant)
|
||||
var/mob/living/mob_occupant = occupant
|
||||
if(uv_super)
|
||||
mob_occupant.adjustFireLoss(rand(20, 36))
|
||||
else
|
||||
@@ -245,10 +245,27 @@
|
||||
visible_message("<span class='notice'>[src]'s door slides open. The glowing yellow lights dim to a gentle green.</span>")
|
||||
else
|
||||
visible_message("<span class='warning'>[src]'s door slides open, barraging you with the nauseating smell of charred flesh.</span>")
|
||||
mob_occupant.radiation = 0
|
||||
playsound(src, 'sound/machines/airlockclose.ogg', 25, 1)
|
||||
for(var/obj/item/I in src) //Scorches away blood and forensic evidence, although the SSU itself is unaffected
|
||||
SEND_SIGNAL(I, COMSIG_COMPONENT_CLEAN_ACT, CLEAN_STRONG)
|
||||
var/datum/component/radioactive/contamination = I.GetComponent(/datum/component/radioactive)
|
||||
var/list/things_to_clear = list() //Done this way since using GetAllContents on the SSU itself would include circuitry and such.
|
||||
if(suit)
|
||||
things_to_clear += suit
|
||||
things_to_clear += suit.GetAllContents()
|
||||
if(helmet)
|
||||
things_to_clear += helmet
|
||||
things_to_clear += helmet.GetAllContents()
|
||||
if(mask)
|
||||
things_to_clear += mask
|
||||
things_to_clear += mask.GetAllContents()
|
||||
if(storage)
|
||||
things_to_clear += storage
|
||||
things_to_clear += storage.GetAllContents()
|
||||
if(occupant)
|
||||
things_to_clear += occupant
|
||||
things_to_clear += occupant.GetAllContents()
|
||||
for(var/atom/movable/AM in things_to_clear) //Scorches away blood and forensic evidence, although the SSU itself is unaffected
|
||||
SEND_SIGNAL(AM, COMSIG_COMPONENT_CLEAN_ACT, CLEAN_STRONG)
|
||||
var/datum/component/radioactive/contamination = AM.GetComponent(/datum/component/radioactive)
|
||||
if(contamination)
|
||||
qdel(contamination)
|
||||
open_machine(FALSE)
|
||||
|
||||
@@ -421,6 +421,7 @@
|
||||
"name" = "[customsender]",
|
||||
"job" = "[customjob]",
|
||||
"message" = custommessage,
|
||||
"emoji_message" = emoji_parse(custommessage),
|
||||
"targets" = list("[customrecepient.owner] ([customrecepient.ownjob])")
|
||||
))
|
||||
// this will log the signal and transmit it to the target
|
||||
|
||||
@@ -106,10 +106,11 @@
|
||||
return "Everyone"
|
||||
return data["targets"][1]
|
||||
|
||||
/datum/signal/subspace/pda/proc/format_message()
|
||||
/datum/signal/subspace/pda/proc/format_message(emojify = FALSE)
|
||||
var/message = emojify ? data["emoji_message"] : data["message"]
|
||||
if (logged && data["photo"])
|
||||
return "\"[data["message"]]\" (<a href='byond://?src=[REF(logged)];photo=1'>Photo</a>)"
|
||||
return "\"[data["message"]]\""
|
||||
return "\"[message]\" (<a href='byond://?src=[REF(logged)];photo=1'>Photo</a>)"
|
||||
return "\"[message]\""
|
||||
|
||||
/datum/signal/subspace/pda/broadcast()
|
||||
if (!logged) // Can only go through if a message server logs it
|
||||
|
||||
Reference in New Issue
Block a user