mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-10 01:34:01 +00:00
[MIRROR] Hands management element [MDB IGNORE] (#24257)
* Hands management element * Update basic.dm * Update dextrous.dm * Fix screenshot test --------- Co-authored-by: Jacquerel <hnevard@gmail.com> Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com>
This commit is contained in:
@@ -172,6 +172,10 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
|
||||
#define TRAIT_DEFIB_BLACKLISTED "defib_blacklisted"
|
||||
#define TRAIT_BADDNA "baddna"
|
||||
#define TRAIT_CLUMSY "clumsy"
|
||||
/// Trait that means you are capable of holding items in some form
|
||||
#define TRAIT_CAN_HOLD_ITEMS "can_hold_items"
|
||||
/// Trait which lets you clamber over a barrier
|
||||
#define TRAIT_FENCE_CLIMBER "can_climb_fences"
|
||||
/// means that you can't use weapons with normal trigger guards.
|
||||
#define TRAIT_CHUNKYFINGERS "chunkyfingers"
|
||||
#define TRAIT_CHUNKYFINGERS_IGNORE_BATON "chunkyfingers_ignore_baton"
|
||||
|
||||
@@ -30,6 +30,8 @@ GLOBAL_LIST_INIT(traits_by_type, list(
|
||||
"TRAIT_CHUNKYFINGERS" = TRAIT_CHUNKYFINGERS,
|
||||
"TRAIT_CHUNKYFINGERS_IGNORE_BATON" = TRAIT_CHUNKYFINGERS_IGNORE_BATON,
|
||||
"TRAIT_FIST_MINING" = TRAIT_FIST_MINING,
|
||||
"TRAIT_CAN_HOLD_ITEMS" = TRAIT_CAN_HOLD_ITEMS,
|
||||
"TRAIT_FENCE_CLIMBER" = TRAIT_FENCE_CLIMBER,
|
||||
"TRAIT_DUMB" = TRAIT_DUMB,
|
||||
"TRAIT_ADVANCEDTOOLUSER" = TRAIT_ADVANCEDTOOLUSER,
|
||||
"TRAIT_DISCOORDINATED_TOOL_USER" = TRAIT_DISCOORDINATED_TOOL_USER,
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
using.icon = ui_style
|
||||
static_inventory += using
|
||||
|
||||
mymob.canon_client.clear_screen()
|
||||
mymob.canon_client?.clear_screen()
|
||||
|
||||
for(var/atom/movable/screen/inventory/inv in (static_inventory + toggleable_inventory))
|
||||
if(inv.slot_id)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
/atom/movable/screen/screentip/proc/update_view(datum/source)
|
||||
SIGNAL_HANDLER
|
||||
if(!hud || !hud.mymob.canon_client.view_size) //Might not have been initialized by now
|
||||
if(!hud || !hud.mymob.canon_client?.view_size) //Might not have been initialized by now
|
||||
return
|
||||
maptext_width = view_to_pixels(hud.mymob.canon_client.view_size.getView())[1]
|
||||
|
||||
|
||||
@@ -287,34 +287,13 @@
|
||||
/atom/proc/attack_pai_secondary(mob/user, list/modifiers)
|
||||
return SECONDARY_ATTACK_CALL_NORMAL
|
||||
|
||||
/*
|
||||
Simple animals
|
||||
*/
|
||||
|
||||
/mob/living/simple_animal/resolve_unarmed_attack(atom/attack_target, list/modifiers)
|
||||
if(dextrous && (isitem(attack_target) || !combat_mode))
|
||||
attack_target.attack_hand(src, modifiers)
|
||||
update_held_items()
|
||||
else
|
||||
return ..()
|
||||
|
||||
/mob/living/simple_animal/resolve_right_click_attack(atom/target, list/modifiers)
|
||||
if(dextrous && (isitem(target) || !combat_mode))
|
||||
. = target.attack_hand_secondary(src, modifiers)
|
||||
update_held_items()
|
||||
else
|
||||
return ..()
|
||||
|
||||
/*
|
||||
Hostile animals
|
||||
*/
|
||||
|
||||
/mob/living/simple_animal/hostile/resolve_unarmed_attack(atom/attack_target, list/modifiers)
|
||||
GiveTarget(attack_target)
|
||||
if(dextrous && (isitem(attack_target) || !combat_mode))
|
||||
return ..()
|
||||
else
|
||||
INVOKE_ASYNC(src, PROC_REF(AttackingTarget), attack_target)
|
||||
|
||||
#undef LIVING_UNARMED_ATTACK_BLOCKED
|
||||
|
||||
|
||||
@@ -114,15 +114,13 @@
|
||||
///Handles climbing onto the atom when you click-drag
|
||||
/datum/element/climbable/proc/mousedrop_receive(atom/climbed_thing, atom/movable/dropped_atom, mob/user, params)
|
||||
SIGNAL_HANDLER
|
||||
if(user == dropped_atom && isliving(dropped_atom))
|
||||
var/mob/living/living_target = dropped_atom
|
||||
if(isanimal(living_target))
|
||||
var/mob/living/simple_animal/animal = dropped_atom
|
||||
if (!animal.dextrous)
|
||||
if(user != dropped_atom || !isliving(dropped_atom))
|
||||
return
|
||||
if(!HAS_TRAIT(dropped_atom, TRAIT_FENCE_CLIMBER) && !HAS_TRAIT(dropped_atom, TRAIT_CAN_HOLD_ITEMS)) // If you can hold items you can probably climb a fence
|
||||
return
|
||||
var/mob/living/living_target = dropped_atom
|
||||
if(living_target.mobility_flags & MOBILITY_MOVE)
|
||||
INVOKE_ASYNC(src, PROC_REF(climb_structure), climbed_thing, living_target, params)
|
||||
return
|
||||
|
||||
///Tries to climb onto the target if the forced movement of the mob allows it
|
||||
/datum/element/climbable/proc/try_speedrun(datum/source, mob/bumpee)
|
||||
|
||||
69
code/datums/elements/dextrous.dm
Normal file
69
code/datums/elements/dextrous.dm
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Sets up the attachee to have hands and manages things like dropping items on death and displaying them on examine
|
||||
* Actual hand performance is managed by code on /living/ and not encapsulated here, we just enable it
|
||||
*/
|
||||
/datum/element/dextrous
|
||||
|
||||
/datum/element/dextrous/Attach(datum/target, hands_count = 2, hud_type = /datum/hud/dextrous)
|
||||
. = ..()
|
||||
if (!isliving(target) || iscarbon(target))
|
||||
return ELEMENT_INCOMPATIBLE // Incompatible with the carbon typepath because that already has its own hand handling and doesn't need hand holding
|
||||
|
||||
var/mob/living/mob_parent = target
|
||||
set_available_hands(mob_parent, hands_count)
|
||||
mob_parent.set_hud_used(new hud_type(target))
|
||||
mob_parent.hud_used.show_hud(mob_parent.hud_used.hud_version)
|
||||
ADD_TRAIT(target, TRAIT_CAN_HOLD_ITEMS, REF(src))
|
||||
RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_death))
|
||||
RegisterSignal(target, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_hand_clicked))
|
||||
RegisterSignal(target, COMSIG_ATOM_EXAMINE, PROC_REF(on_examined))
|
||||
|
||||
/datum/element/dextrous/Detach(datum/source)
|
||||
. = ..()
|
||||
var/mob/living/mob_parent = source
|
||||
set_available_hands(mob_parent, initial(mob_parent.default_num_hands))
|
||||
var/initial_hud = initial(mob_parent.hud_type)
|
||||
mob_parent.set_hud_used(new initial_hud(source))
|
||||
mob_parent.hud_used.show_hud(mob_parent.hud_used.hud_version)
|
||||
REMOVE_TRAIT(source, TRAIT_CAN_HOLD_ITEMS, REF(src))
|
||||
UnregisterSignal(source, list(
|
||||
COMSIG_ATOM_EXAMINE,
|
||||
COMSIG_LIVING_DEATH,
|
||||
COMSIG_LIVING_UNARMED_ATTACK,
|
||||
))
|
||||
|
||||
/// Set up how many hands we should have
|
||||
/datum/element/dextrous/proc/set_available_hands(mob/living/hand_owner, hands_count)
|
||||
hand_owner.drop_all_held_items()
|
||||
var/held_items = list()
|
||||
for (var/i in 1 to hands_count)
|
||||
held_items += null
|
||||
hand_owner.held_items = held_items
|
||||
hand_owner.set_num_hands(hands_count)
|
||||
hand_owner.set_usable_hands(hands_count)
|
||||
|
||||
/// Drop our shit when we die
|
||||
/datum/element/dextrous/proc/on_death(mob/living/died, gibbed)
|
||||
SIGNAL_HANDLER
|
||||
died.drop_all_held_items()
|
||||
|
||||
/// Try picking up items
|
||||
/datum/element/dextrous/proc/on_hand_clicked(mob/living/hand_haver, atom/target, proximity, modifiers)
|
||||
SIGNAL_HANDLER
|
||||
if (!isitem(target) && hand_haver.combat_mode)
|
||||
return
|
||||
if (LAZYACCESS(modifiers, RIGHT_CLICK))
|
||||
INVOKE_ASYNC(target, TYPE_PROC_REF(/atom, attack_hand_secondary), hand_haver, modifiers)
|
||||
else
|
||||
INVOKE_ASYNC(target, TYPE_PROC_REF(/atom, attack_hand), hand_haver, modifiers)
|
||||
INVOKE_ASYNC(hand_haver, TYPE_PROC_REF(/mob, update_held_items))
|
||||
return COMPONENT_CANCEL_ATTACK_CHAIN
|
||||
|
||||
/// Tell people what we are holding
|
||||
/datum/element/dextrous/proc/on_examined(mob/living/examined, mob/user, list/examine_list)
|
||||
SIGNAL_HANDLER
|
||||
for(var/obj/item/held_item in examined.held_items)
|
||||
if(held_item.item_flags & (ABSTRACT|EXAMINE_SKIP|HAND_ITEM))
|
||||
continue
|
||||
examine_list += span_info("[examined.p_They()] [examined.p_have()] [held_item.get_examine_string(user)] in [examined.p_their()] \
|
||||
[examined.get_held_index_name(examined.get_held_index_of_item(held_item))].")
|
||||
@@ -591,13 +591,7 @@
|
||||
if(!isliving(user))
|
||||
return FALSE //no ghosts allowed, sorry
|
||||
|
||||
var/is_dextrous = FALSE
|
||||
if(isanimal(user))
|
||||
var/mob/living/simple_animal/user_as_animal = user
|
||||
if (user_as_animal.dextrous)
|
||||
is_dextrous = TRUE
|
||||
|
||||
if(!issilicon(user) && !is_dextrous && !user.can_hold_items())
|
||||
if(!issilicon(user) && !user.can_hold_items())
|
||||
return FALSE //spiders gtfo
|
||||
|
||||
if(issilicon(user)) // If we are a silicon, make sure the machine allows silicons to interact with it
|
||||
|
||||
@@ -285,6 +285,21 @@
|
||||
return last_icon_state
|
||||
return null
|
||||
|
||||
/mob/living/basic/put_in_hands(obj/item/I, del_on_fail = FALSE, merge_stacks = TRUE, ignore_animation = TRUE)
|
||||
. = ..()
|
||||
if (.)
|
||||
update_held_items()
|
||||
|
||||
/mob/living/basic/update_held_items()
|
||||
if(isnull(client) || isnull(hud_used) || hud_used.hud_version == HUD_STYLE_NOHUD)
|
||||
return
|
||||
var/turf/our_turf = get_turf(src)
|
||||
for(var/obj/item/held in held_items)
|
||||
var/index = get_held_index_of_item(held)
|
||||
SET_PLANE(held, ABOVE_HUD_PLANE, our_turf)
|
||||
held.screen_loc = ui_hand_position(index)
|
||||
client.screen |= held
|
||||
|
||||
/mob/living/basic/get_body_temp_heat_damage_limit()
|
||||
return maximum_survivable_temperature
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
/mob/living/basic/bear/Initialize(mapload)
|
||||
. = ..()
|
||||
ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
|
||||
add_traits(list(TRAIT_SPACEWALK, TRAIT_FENCE_CLIMBER), INNATE_TRAIT)
|
||||
AddElement(/datum/element/ai_retaliate)
|
||||
AddComponent(/datum/component/tree_climber, climbing_distance = 15)
|
||||
AddElement(/datum/element/swabable, CELL_LINE_TABLE_BEAR, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
/mob/living/basic/spider/Initialize(mapload)
|
||||
. = ..()
|
||||
ADD_TRAIT(src, TRAIT_WEB_SURFER, INNATE_TRAIT)
|
||||
add_traits(list(TRAIT_WEB_SURFER, TRAIT_FENCE_CLIMBER), INNATE_TRAIT)
|
||||
AddElement(/datum/element/footstep, FOOTSTEP_MOB_CLAW)
|
||||
AddElement(/datum/element/nerfed_pulling, GLOB.typecache_general_bad_things_to_easily_move)
|
||||
AddElement(/datum/element/prevent_attacking_of_types, GLOB.typecache_general_bad_hostile_attack_targets, "this tastes awful!")
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
COMSIG_CARBON_DISARM_COLLIDE = PROC_REF(disarm_collision),
|
||||
)
|
||||
AddElement(/datum/element/connect_loc, loc_connections)
|
||||
ADD_TRAIT(src, TRAIT_CAN_HOLD_ITEMS, INNATE_TRAIT) // Carbons are assumed to be innately capable of having arms, we check their arms count instead
|
||||
|
||||
/mob/living/carbon/Destroy()
|
||||
//This must be done first, so the mob ghosts correctly before DNA etc is nulled
|
||||
@@ -27,45 +28,6 @@
|
||||
QDEL_NULL(dna)
|
||||
GLOB.carbon_list -= src
|
||||
|
||||
/mob/living/carbon/perform_hand_swap(held_index)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return
|
||||
|
||||
if(!held_index)
|
||||
held_index = (active_hand_index % held_items.len)+1
|
||||
|
||||
if(!isnum(held_index))
|
||||
CRASH("You passed [held_index] into swap_hand instead of a number. WTF man")
|
||||
|
||||
var/oindex = active_hand_index
|
||||
active_hand_index = held_index
|
||||
if(hud_used)
|
||||
var/atom/movable/screen/inventory/hand/H
|
||||
H = hud_used.hand_slots["[oindex]"]
|
||||
if(H)
|
||||
H.update_appearance()
|
||||
H = hud_used.hand_slots["[held_index]"]
|
||||
if(H)
|
||||
H.update_appearance()
|
||||
|
||||
|
||||
/mob/living/carbon/activate_hand(selhand) //l/r OR 1-held_items.len
|
||||
if(!selhand)
|
||||
selhand = (active_hand_index % held_items.len)+1
|
||||
|
||||
if(istext(selhand))
|
||||
selhand = lowertext(selhand)
|
||||
if(selhand == "right" || selhand == "r")
|
||||
selhand = 2
|
||||
if(selhand == "left" || selhand == "l")
|
||||
selhand = 1
|
||||
|
||||
if(selhand != active_hand_index)
|
||||
swap_hand(selhand)
|
||||
else
|
||||
mode() // Activate held item
|
||||
|
||||
/mob/living/carbon/attackby(obj/item/item, mob/living/user, params)
|
||||
if(!all_wounds || !(!user.combat_mode || user == src))
|
||||
return ..()
|
||||
|
||||
@@ -1308,7 +1308,7 @@
|
||||
return
|
||||
|
||||
/mob/living/can_hold_items(obj/item/I)
|
||||
return usable_hands && ..()
|
||||
return ..() && HAS_TRAIT(src, TRAIT_CAN_HOLD_ITEMS) && usable_hands
|
||||
|
||||
/mob/living/can_perform_action(atom/movable/target, action_bitflags)
|
||||
if(!istype(target))
|
||||
|
||||
@@ -401,6 +401,7 @@
|
||||
|
||||
/mob/living/silicon/robot/perform_hand_swap()
|
||||
cycle_modules()
|
||||
return TRUE
|
||||
|
||||
/mob/living/silicon/robot/can_hold_items(obj/item/I)
|
||||
return (I && (I in model.modules)) //Only if it's part of our model.
|
||||
|
||||
@@ -22,17 +22,9 @@
|
||||
dropItemToGround(internal_storage)
|
||||
|
||||
/mob/living/simple_animal/hostile/guardian/dextrous/examine(mob/user)
|
||||
if(dextrous)
|
||||
. = list("<span class='info'>This is [icon2html(src)] \a <b>[src]</b>!\n[desc]", EXAMINE_SECTION_BREAK) //SKYRAT EDIT CHANGE
|
||||
for(var/obj/item/held_item in held_items)
|
||||
if(held_item.item_flags & (ABSTRACT|EXAMINE_SKIP|HAND_ITEM))
|
||||
continue
|
||||
. += "It has [held_item.get_examine_string(user)] in its [get_held_index_name(get_held_index_of_item(held_item))]."
|
||||
. = ..()
|
||||
if(internal_storage && !(internal_storage.item_flags & ABSTRACT))
|
||||
. += "It is holding [internal_storage.get_examine_string(user)] in its internal storage."
|
||||
. += "</span>"
|
||||
else
|
||||
return ..()
|
||||
. += span_info("It is holding [internal_storage.get_examine_string(user)] in its internal storage.")
|
||||
|
||||
/mob/living/simple_animal/hostile/guardian/dextrous/recall_effects()
|
||||
drop_all_held_items()
|
||||
|
||||
@@ -177,6 +177,7 @@
|
||||
stack_trace("Simple animal being instantiated in nullspace")
|
||||
update_simplemob_varspeed()
|
||||
if(dextrous)
|
||||
AddElement(/datum/element/dextrous, hud_type = hud_type)
|
||||
AddComponent(/datum/component/personal_crafting)
|
||||
add_traits(list(TRAIT_ADVANCEDTOOLUSER, TRAIT_CAN_STRIP), ROUNDSTART_TRAIT)
|
||||
ADD_TRAIT(src, TRAIT_NOFIRE_SPREAD, ROUNDSTART_TRAIT)
|
||||
@@ -447,9 +448,6 @@
|
||||
|
||||
/mob/living/simple_animal/death(gibbed)
|
||||
drop_loot()
|
||||
if(dextrous)
|
||||
drop_all_held_items()
|
||||
|
||||
if(del_on_death)
|
||||
..()
|
||||
//Prevent infinite loops if the mob Destroy() is overridden in such
|
||||
@@ -560,44 +558,6 @@
|
||||
/mob/living/simple_animal/get_idcard(hand_first)
|
||||
return (..() || access_card)
|
||||
|
||||
/mob/living/simple_animal/can_hold_items(obj/item/I)
|
||||
return dextrous && ..()
|
||||
|
||||
/mob/living/simple_animal/activate_hand(selhand)
|
||||
if(!dextrous)
|
||||
return ..()
|
||||
if(!selhand)
|
||||
selhand = (active_hand_index % held_items.len)+1
|
||||
if(istext(selhand))
|
||||
selhand = lowertext(selhand)
|
||||
if(selhand == "right" || selhand == "r")
|
||||
selhand = 2
|
||||
if(selhand == "left" || selhand == "l")
|
||||
selhand = 1
|
||||
if(selhand != active_hand_index)
|
||||
swap_hand(selhand)
|
||||
else
|
||||
mode()
|
||||
|
||||
/mob/living/simple_animal/perform_hand_swap(hand_index)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return
|
||||
if(!dextrous)
|
||||
return
|
||||
if(!hand_index)
|
||||
hand_index = (active_hand_index % held_items.len)+1
|
||||
var/oindex = active_hand_index
|
||||
active_hand_index = hand_index
|
||||
if(hud_used)
|
||||
var/atom/movable/screen/inventory/hand/H
|
||||
H = hud_used.hand_slots["[hand_index]"]
|
||||
if(H)
|
||||
H.update_appearance()
|
||||
H = hud_used.hand_slots["[oindex]"]
|
||||
if(H)
|
||||
H.update_appearance()
|
||||
|
||||
/mob/living/simple_animal/put_in_hands(obj/item/I, del_on_fail = FALSE, merge_stacks = TRUE, ignore_animation = TRUE)
|
||||
. = ..()
|
||||
update_held_items()
|
||||
|
||||
@@ -984,11 +984,46 @@
|
||||
/// Performs the actual ritual of swapping hands, such as setting the held index variables
|
||||
/mob/proc/perform_hand_swap(held_index)
|
||||
PROTECTED_PROC(TRUE)
|
||||
if (!HAS_TRAIT(src, TRAIT_CAN_HOLD_ITEMS))
|
||||
return FALSE
|
||||
|
||||
if(!held_index)
|
||||
held_index = (active_hand_index % held_items.len) + 1
|
||||
|
||||
if(!isnum(held_index))
|
||||
CRASH("You passed [held_index] into swap_hand instead of a number. WTF man")
|
||||
|
||||
var/previous_index = active_hand_index
|
||||
active_hand_index = held_index
|
||||
if(hud_used)
|
||||
var/atom/movable/screen/inventory/hand/held_location
|
||||
held_location = hud_used.hand_slots["[previous_index]"]
|
||||
if(!isnull(held_location))
|
||||
held_location.update_appearance()
|
||||
held_location = hud_used.hand_slots["[held_index]"]
|
||||
if(!isnull(held_location))
|
||||
held_location.update_appearance()
|
||||
return TRUE
|
||||
|
||||
/mob/proc/activate_hand(selhand)
|
||||
/mob/proc/activate_hand(selected_hand)
|
||||
if (!HAS_TRAIT(src, TRAIT_CAN_HOLD_ITEMS))
|
||||
return
|
||||
|
||||
if(!selected_hand)
|
||||
selected_hand = (active_hand_index % held_items.len)+1
|
||||
|
||||
if(istext(selected_hand))
|
||||
selected_hand = lowertext(selected_hand)
|
||||
if(selected_hand == "right" || selected_hand == "r")
|
||||
selected_hand = 2
|
||||
if(selected_hand == "left" || selected_hand == "l")
|
||||
selected_hand = 1
|
||||
|
||||
if(selected_hand != active_hand_index)
|
||||
swap_hand(selected_hand)
|
||||
else
|
||||
mode()
|
||||
|
||||
/mob/proc/assess_threat(judgement_criteria, lasercolor = "", datum/callback/weaponcheck=null) //For sec bot threat assessment
|
||||
return 0
|
||||
|
||||
|
||||
@@ -59,11 +59,10 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/keycard_auth, 26)
|
||||
/obj/machinery/keycard_auth/ui_status(mob/user)
|
||||
if(isdrone(user))
|
||||
return UI_CLOSE
|
||||
if(!isanimal(user))
|
||||
if(!isanimal_or_basicmob(user))
|
||||
return ..()
|
||||
var/mob/living/simple_animal/A = user
|
||||
if(!A.dextrous)
|
||||
to_chat(user, span_warning("You are too primitive to use this device!"))
|
||||
if(!HAS_TRAIT(user, TRAIT_CAN_HOLD_ITEMS))
|
||||
balloon_alert(user, "no hands!")
|
||||
return UI_CLOSE
|
||||
return ..()
|
||||
|
||||
|
||||
@@ -1378,6 +1378,7 @@
|
||||
#include "code\datums\elements\death_gases.dm"
|
||||
#include "code\datums\elements\delete_on_drop.dm"
|
||||
#include "code\datums\elements\deliver_first.dm"
|
||||
#include "code\datums\elements\dextrous.dm"
|
||||
#include "code\datums\elements\diggable.dm"
|
||||
#include "code\datums\elements\digitalcamo.dm"
|
||||
#include "code\datums\elements\drag_pickup.dm"
|
||||
|
||||
Reference in New Issue
Block a user