mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-15 12:43:13 +00:00
@@ -12,7 +12,7 @@
|
|||||||
var/obj/screen/component_button/button_expand
|
var/obj/screen/component_button/button_expand
|
||||||
var/obj/screen/component_button/button_shrink
|
var/obj/screen/component_button/button_shrink
|
||||||
|
|
||||||
var/mutable_appearance/standard_background
|
var/list/background_mas = list()
|
||||||
var/const/max_dimensions = 10
|
var/const/max_dimensions = 10
|
||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/Initialize()
|
/obj/screen/movable/pic_in_pic/Initialize()
|
||||||
@@ -36,11 +36,17 @@
|
|||||||
set_view_size(width-1, height-1)
|
set_view_size(width-1, height-1)
|
||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/proc/make_backgrounds()
|
/obj/screen/movable/pic_in_pic/proc/make_backgrounds()
|
||||||
standard_background = new /mutable_appearance()
|
var/mutable_appearance/base = new /mutable_appearance()
|
||||||
standard_background.icon = 'icons/misc/pic_in_pic.dmi'
|
base.icon = 'icons/misc/pic_in_pic.dmi'
|
||||||
standard_background.icon_state = "background"
|
base.layer = DISPOSAL_LAYER
|
||||||
standard_background.layer = DISPOSAL_LAYER
|
base.plane = PLATING_PLANE
|
||||||
standard_background.plane = PLATING_PLANE
|
base.appearance_flags = PIXEL_SCALE
|
||||||
|
|
||||||
|
for(var/direction in cardinal)
|
||||||
|
var/mutable_appearance/dir = new /mutable_appearance(base)
|
||||||
|
dir.dir = direction
|
||||||
|
dir.icon_state = "background_[direction]"
|
||||||
|
background_mas += dir
|
||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/proc/add_buttons()
|
/obj/screen/movable/pic_in_pic/proc/add_buttons()
|
||||||
var/static/mutable_appearance/move_tab
|
var/static/mutable_appearance/move_tab
|
||||||
@@ -97,11 +103,34 @@
|
|||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/proc/add_background()
|
/obj/screen/movable/pic_in_pic/proc/add_background()
|
||||||
if((width > 0) && (height > 0))
|
if((width > 0) && (height > 0))
|
||||||
|
for(var/mutable_appearance/dir in background_mas)
|
||||||
var/matrix/M = matrix()
|
var/matrix/M = matrix()
|
||||||
M.Scale(width + 0.5, height + 0.5)
|
var/x_scale = 1
|
||||||
M.Translate((width-1)/2 * world.icon_size, (height-1)/2 * world.icon_size)
|
var/y_scale = 1
|
||||||
standard_background.transform = M
|
|
||||||
overlays += standard_background
|
var/x_off = 0
|
||||||
|
var/y_off = 0
|
||||||
|
|
||||||
|
if(dir.dir & (NORTH|SOUTH))
|
||||||
|
x_scale = width
|
||||||
|
x_off = (width-1)/2 * world.icon_size
|
||||||
|
if(dir.dir & NORTH)
|
||||||
|
y_off = ((height-1) * world.icon_size) + 3
|
||||||
|
else
|
||||||
|
y_off = -3
|
||||||
|
|
||||||
|
if(dir.dir & (EAST|WEST))
|
||||||
|
y_scale = height
|
||||||
|
y_off = (height-1)/2 * world.icon_size
|
||||||
|
if(dir.dir & EAST)
|
||||||
|
x_off = ((width-1) * world.icon_size) + 3
|
||||||
|
else
|
||||||
|
x_off = -3
|
||||||
|
|
||||||
|
M.Scale(x_scale, y_scale)
|
||||||
|
M.Translate(x_off, y_off)
|
||||||
|
dir.transform = M
|
||||||
|
overlays += dir
|
||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/proc/set_view_size(width, height, do_refresh = TRUE)
|
/obj/screen/movable/pic_in_pic/proc/set_view_size(width, height, do_refresh = TRUE)
|
||||||
width = CLAMP(width, 0, max_dimensions)
|
width = CLAMP(width, 0, max_dimensions)
|
||||||
|
|||||||
@@ -610,7 +610,6 @@
|
|||||||
/obj/screen/component_button
|
/obj/screen/component_button
|
||||||
var/obj/screen/parent
|
var/obj/screen/parent
|
||||||
|
|
||||||
|
|
||||||
/obj/screen/component_button/Initialize(mapload, obj/screen/new_parent)
|
/obj/screen/component_button/Initialize(mapload, obj/screen/new_parent)
|
||||||
. = ..()
|
. = ..()
|
||||||
parent = new_parent
|
parent = new_parent
|
||||||
|
|||||||
@@ -37,8 +37,8 @@
|
|||||||
|
|
||||||
// During dynamic mapload (reader.dm) this assigns the var overrides from the .dmm file
|
// During dynamic mapload (reader.dm) this assigns the var overrides from the .dmm file
|
||||||
// Native BYOND maploading sets those vars before invoking New(), by doing this FIRST we come as close to that behavior as we can.
|
// Native BYOND maploading sets those vars before invoking New(), by doing this FIRST we come as close to that behavior as we can.
|
||||||
if(use_preloader && (src.type == _preloader.target_path))//in case the instanciated atom is creating other atoms in New()
|
if(GLOB.use_preloader && (src.type == GLOB._preloader.target_path))//in case the instanciated atom is creating other atoms in New()
|
||||||
_preloader.load(src)
|
GLOB._preloader.load(src)
|
||||||
|
|
||||||
// Pass our arguments to InitAtom so they can be passed to initialize(), but replace 1st with if-we're-during-mapload.
|
// Pass our arguments to InitAtom so they can be passed to initialize(), but replace 1st with if-we're-during-mapload.
|
||||||
var/do_initialize = SSatoms.initialized
|
var/do_initialize = SSatoms.initialized
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
return (available_in_playhours(C) == 0)
|
return (available_in_playhours(C) == 0)
|
||||||
|
|
||||||
/datum/job/proc/available_in_playhours(client/C)
|
/datum/job/proc/available_in_playhours(client/C)
|
||||||
if(C && config.use_playtime_restriction_for_jobs && isnum(C.play_hours[pto_type]) && dept_time_required > 0)
|
if(C && config.use_playtime_restriction_for_jobs)
|
||||||
|
if(isnum(C.play_hours[pto_type])) // Has played that department before
|
||||||
return max(0, dept_time_required - C.play_hours[pto_type])
|
return max(0, dept_time_required - C.play_hours[pto_type])
|
||||||
|
else // List doesn't have that entry, maybe never played, maybe invalid PTO type (you should fix that...)
|
||||||
|
return dept_time_required // Could be 0, too, which is fine! They can play that
|
||||||
return 0
|
return 0
|
||||||
@@ -2,15 +2,9 @@
|
|||||||
//SS13 Optimized Map loader
|
//SS13 Optimized Map loader
|
||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
/*
|
|
||||||
//global datum that will preload variables on atoms instanciation
|
//global datum that will preload variables on atoms instanciation
|
||||||
GLOBAL_VAR_INIT(use_preloader, FALSE)
|
GLOBAL_VAR_INIT(use_preloader, FALSE)
|
||||||
GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
|
GLOBAL_DATUM_INIT(_preloader, /dmm_suite/preloader, new)
|
||||||
*/
|
|
||||||
|
|
||||||
//global datum that will preload variables on atoms instanciation
|
|
||||||
var/global/dmm_suite/preloader/_preloader = new()
|
|
||||||
var/global/use_preloader = FALSE
|
|
||||||
|
|
||||||
/dmm_suite
|
/dmm_suite
|
||||||
// /"([a-zA-Z]+)" = \(((?:.|\n)*?)\)\n(?!\t)|\((\d+),(\d+),(\d+)\) = \{"([a-zA-Z\n]*)"\}/g
|
// /"([a-zA-Z]+)" = \(((?:.|\n)*?)\)\n(?!\t)|\((\d+),(\d+),(\d+)\) = \{"([a-zA-Z\n]*)"\}/g
|
||||||
@@ -36,6 +30,12 @@ var/global/use_preloader = FALSE
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
/dmm_suite/load_map(dmm_file as file, x_offset as num, y_offset as num, z_offset as num, cropMap as num, measureOnly as num, no_changeturf as num, orientation as num)
|
/dmm_suite/load_map(dmm_file as file, x_offset as num, y_offset as num, z_offset as num, cropMap as num, measureOnly as num, no_changeturf as num, orientation as num)
|
||||||
|
|
||||||
|
dmmRegex = new/regex({""(\[a-zA-Z]+)" = \\(((?:.|\n)*?)\\)\n(?!\t)|\\((\\d+),(\\d+),(\\d+)\\) = \\{"(\[a-zA-Z\n]*)"\\}"}, "g")
|
||||||
|
trimQuotesRegex = new/regex({"^\[\\s\n]+"?|"?\[\\s\n]+$|^"|"$"}, "g")
|
||||||
|
trimRegex = new/regex("^\[\\s\n]+|\[\\s\n]+$", "g")
|
||||||
|
modelCache = list()
|
||||||
|
|
||||||
//How I wish for RAII
|
//How I wish for RAII
|
||||||
if(!measureOnly)
|
if(!measureOnly)
|
||||||
Master.StartLoadingMap()
|
Master.StartLoadingMap()
|
||||||
@@ -350,7 +350,7 @@ var/global/use_preloader = FALSE
|
|||||||
index = members.len
|
index = members.len
|
||||||
if(members[index] != /area/template_noop)
|
if(members[index] != /area/template_noop)
|
||||||
var/atom/instance
|
var/atom/instance
|
||||||
_preloader.setup(members_attributes[index])//preloader for assigning set variables on atom creation
|
GLOB._preloader.setup(members_attributes[index])//preloader for assigning set variables on atom creation
|
||||||
var/atype = members[index]
|
var/atype = members[index]
|
||||||
for(var/area/A in world)
|
for(var/area/A in world)
|
||||||
if(A.type == atype)
|
if(A.type == atype)
|
||||||
@@ -361,8 +361,8 @@ var/global/use_preloader = FALSE
|
|||||||
if(crds)
|
if(crds)
|
||||||
instance.contents.Add(crds)
|
instance.contents.Add(crds)
|
||||||
|
|
||||||
if(use_preloader && instance)
|
if(GLOB.use_preloader && instance)
|
||||||
_preloader.load(instance)
|
GLOB._preloader.load(instance)
|
||||||
|
|
||||||
//then instance the /turf and, if multiple tiles are presents, simulates the DMM underlays piling effect
|
//then instance the /turf and, if multiple tiles are presents, simulates the DMM underlays piling effect
|
||||||
|
|
||||||
@@ -398,7 +398,7 @@ var/global/use_preloader = FALSE
|
|||||||
|
|
||||||
//Instance an atom at (x,y,z) and gives it the variables in attributes
|
//Instance an atom at (x,y,z) and gives it the variables in attributes
|
||||||
/dmm_suite/proc/instance_atom(path,list/attributes, turf/crds, no_changeturf, orientation=0)
|
/dmm_suite/proc/instance_atom(path,list/attributes, turf/crds, no_changeturf, orientation=0)
|
||||||
_preloader.setup(attributes, path)
|
GLOB._preloader.setup(attributes, path)
|
||||||
|
|
||||||
if(crds)
|
if(crds)
|
||||||
if(!no_changeturf && ispath(path, /turf))
|
if(!no_changeturf && ispath(path, /turf))
|
||||||
@@ -406,8 +406,8 @@ var/global/use_preloader = FALSE
|
|||||||
else
|
else
|
||||||
. = create_atom(path, crds)//first preloader pass
|
. = create_atom(path, crds)//first preloader pass
|
||||||
|
|
||||||
if(use_preloader && .)//second preloader pass, for those atoms that don't ..() in New()
|
if(GLOB.use_preloader && .)//second preloader pass, for those atoms that don't ..() in New()
|
||||||
_preloader.load(.)
|
GLOB._preloader.load(.)
|
||||||
|
|
||||||
//custom CHECK_TICK here because we don't want things created while we're sleeping to not initialize
|
//custom CHECK_TICK here because we don't want things created while we're sleeping to not initialize
|
||||||
if(TICK_CHECK)
|
if(TICK_CHECK)
|
||||||
@@ -530,7 +530,7 @@ var/global/use_preloader = FALSE
|
|||||||
|
|
||||||
/dmm_suite/preloader/proc/setup(list/the_attributes, path)
|
/dmm_suite/preloader/proc/setup(list/the_attributes, path)
|
||||||
if(the_attributes.len)
|
if(the_attributes.len)
|
||||||
use_preloader = TRUE
|
GLOB.use_preloader = TRUE
|
||||||
attributes = the_attributes
|
attributes = the_attributes
|
||||||
target_path = path
|
target_path = path
|
||||||
|
|
||||||
@@ -540,7 +540,7 @@ var/global/use_preloader = FALSE
|
|||||||
if(islist(value))
|
if(islist(value))
|
||||||
value = deepCopyList(value)
|
value = deepCopyList(value)
|
||||||
what.vars[attribute] = value
|
what.vars[attribute] = value
|
||||||
use_preloader = FALSE
|
GLOB.use_preloader = FALSE
|
||||||
|
|
||||||
/area/template_noop
|
/area/template_noop
|
||||||
name = "Area Passthrough"
|
name = "Area Passthrough"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/ai
|
/obj/screen/movable/pic_in_pic/ai
|
||||||
var/mob/living/silicon/ai/ai
|
var/mob/living/silicon/ai/ai
|
||||||
var/mutable_appearance/highlighted_background
|
var/list/highlighted_mas = list()
|
||||||
var/highlighted = FALSE
|
var/highlighted = FALSE
|
||||||
var/mob/observer/eye/aiEye/pic_in_pic/aiEye
|
var/mob/observer/eye/aiEye/pic_in_pic/aiEye
|
||||||
|
|
||||||
@@ -12,9 +12,12 @@
|
|||||||
aiEye.screen = src
|
aiEye.screen = src
|
||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/ai/Destroy()
|
/obj/screen/movable/pic_in_pic/ai/Destroy()
|
||||||
set_ai(null)
|
. = ..()
|
||||||
|
if(!QDELETED(aiEye))
|
||||||
QDEL_NULL(aiEye)
|
QDEL_NULL(aiEye)
|
||||||
return ..()
|
else
|
||||||
|
aiEye = null
|
||||||
|
set_ai(null)
|
||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/ai/Click()
|
/obj/screen/movable/pic_in_pic/ai/Click()
|
||||||
..()
|
..()
|
||||||
@@ -23,22 +26,57 @@
|
|||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/ai/make_backgrounds()
|
/obj/screen/movable/pic_in_pic/ai/make_backgrounds()
|
||||||
..()
|
..()
|
||||||
highlighted_background = new /mutable_appearance()
|
var/mutable_appearance/base = new /mutable_appearance()
|
||||||
highlighted_background.icon = 'icons/misc/pic_in_pic.dmi'
|
base.icon = 'icons/misc/pic_in_pic.dmi'
|
||||||
highlighted_background.icon_state = "background_highlight"
|
base.layer = DISPOSAL_LAYER
|
||||||
highlighted_background.layer = DISPOSAL_LAYER
|
base.plane = PLATING_PLANE
|
||||||
highlighted_background.plane = PLATING_PLANE
|
base.appearance_flags = PIXEL_SCALE
|
||||||
|
|
||||||
|
for(var/direction in cardinal)
|
||||||
|
var/mutable_appearance/dir = new /mutable_appearance(base)
|
||||||
|
dir.dir = direction
|
||||||
|
dir.icon_state = "background_highlight_[direction]"
|
||||||
|
highlighted_mas += dir
|
||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/ai/add_background()
|
/obj/screen/movable/pic_in_pic/ai/add_background()
|
||||||
if((width > 0) && (height > 0))
|
if((width > 0) && (height > 0))
|
||||||
|
if(!highlighted)
|
||||||
|
return ..()
|
||||||
|
|
||||||
|
for(var/mutable_appearance/dir in highlighted_mas)
|
||||||
var/matrix/M = matrix()
|
var/matrix/M = matrix()
|
||||||
M.Scale(width + 0.5, height + 0.5)
|
var/x_scale = 1
|
||||||
M.Translate((width-1)/2 * world.icon_size, (height-1)/2 * world.icon_size)
|
var/y_scale = 1
|
||||||
highlighted_background.transform = M
|
|
||||||
standard_background.transform = M
|
var/x_off = 0
|
||||||
overlays += highlighted ? highlighted_background : standard_background
|
var/y_off = 0
|
||||||
|
|
||||||
|
if(dir.dir & (NORTH|SOUTH))
|
||||||
|
x_scale = width
|
||||||
|
x_off = (width-1)/2 * world.icon_size
|
||||||
|
if(dir.dir & NORTH)
|
||||||
|
y_off = ((height-1) * world.icon_size) + 3
|
||||||
|
else
|
||||||
|
y_off = -3
|
||||||
|
|
||||||
|
if(dir.dir & (EAST|WEST))
|
||||||
|
y_scale = height
|
||||||
|
y_off = (height-1)/2 * world.icon_size
|
||||||
|
if(dir.dir & EAST)
|
||||||
|
x_off = ((width-1) * world.icon_size) + 3
|
||||||
|
else
|
||||||
|
x_off = -3
|
||||||
|
|
||||||
|
M.Scale(x_scale, y_scale)
|
||||||
|
M.Translate(x_off, y_off)
|
||||||
|
dir.transform = M
|
||||||
|
overlays += dir
|
||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/ai/set_view_size(width, height, do_refresh = TRUE)
|
/obj/screen/movable/pic_in_pic/ai/set_view_size(width, height, do_refresh = TRUE)
|
||||||
|
if(!aiEye)
|
||||||
|
qdel(src)
|
||||||
|
return
|
||||||
|
|
||||||
aiEye.static_visibility_range = (round(max(width, height) / 2) + 1)
|
aiEye.static_visibility_range = (round(max(width, height) / 2) + 1)
|
||||||
if(ai)
|
if(ai)
|
||||||
ai.camera_visibility(aiEye)
|
ai.camera_visibility(aiEye)
|
||||||
@@ -46,27 +84,50 @@
|
|||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/ai/set_view_center(atom/target, do_refresh = TRUE)
|
/obj/screen/movable/pic_in_pic/ai/set_view_center(atom/target, do_refresh = TRUE)
|
||||||
..()
|
..()
|
||||||
|
if(!aiEye)
|
||||||
|
qdel(src)
|
||||||
|
return
|
||||||
|
|
||||||
aiEye.setLoc(get_turf(target))
|
aiEye.setLoc(get_turf(target))
|
||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/ai/refresh_view()
|
/obj/screen/movable/pic_in_pic/ai/refresh_view()
|
||||||
..()
|
..()
|
||||||
|
if(!aiEye)
|
||||||
|
qdel(src)
|
||||||
|
return
|
||||||
|
|
||||||
aiEye.setLoc(get_turf(center))
|
aiEye.setLoc(get_turf(center))
|
||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/ai/proc/highlight()
|
/obj/screen/movable/pic_in_pic/ai/proc/highlight()
|
||||||
if(highlighted)
|
if(highlighted)
|
||||||
return
|
return
|
||||||
|
if(!aiEye)
|
||||||
|
qdel(src)
|
||||||
|
return
|
||||||
highlighted = TRUE
|
highlighted = TRUE
|
||||||
overlays -= standard_background
|
overlays.Cut()
|
||||||
overlays += highlighted_background
|
add_background()
|
||||||
|
add_buttons()
|
||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/ai/proc/unhighlight()
|
/obj/screen/movable/pic_in_pic/ai/proc/unhighlight()
|
||||||
if(!highlighted)
|
if(!highlighted)
|
||||||
return
|
return
|
||||||
|
if(!aiEye)
|
||||||
|
qdel(src)
|
||||||
|
return
|
||||||
highlighted = FALSE
|
highlighted = FALSE
|
||||||
overlays -= highlighted_background
|
overlays.Cut()
|
||||||
overlays += standard_background
|
add_background()
|
||||||
|
add_buttons()
|
||||||
|
|
||||||
/obj/screen/movable/pic_in_pic/ai/proc/set_ai(mob/living/silicon/ai/new_ai)
|
/obj/screen/movable/pic_in_pic/ai/proc/set_ai(mob/living/silicon/ai/new_ai)
|
||||||
|
if(!aiEye && !QDELETED(src))
|
||||||
|
if(new_ai)
|
||||||
|
to_chat(new_ai, "<span class='danger'><h2>You've run into a unfixable bug with AI eye code. \
|
||||||
|
In order to create a new multicam, you will have to select a different camera first before trying to add one, or ask an admin to fix you. \
|
||||||
|
Whatever you did that made the last camera window disappear-- don't do that again.</h2></span>")
|
||||||
|
qdel(src)
|
||||||
|
return
|
||||||
if(ai)
|
if(ai)
|
||||||
ai.multicam_screens -= src
|
ai.multicam_screens -= src
|
||||||
ai.all_eyes -= aiEye
|
ai.all_eyes -= aiEye
|
||||||
@@ -88,6 +149,8 @@
|
|||||||
icon = 'icons/misc/pic_in_pic.dmi'
|
icon = 'icons/misc/pic_in_pic.dmi'
|
||||||
icon_state = "room_background"
|
icon_state = "room_background"
|
||||||
flags = NOJAUNT
|
flags = NOJAUNT
|
||||||
|
plane = SPACE_PLANE
|
||||||
|
layer = AREA_LAYER + 0.1
|
||||||
|
|
||||||
/turf/unsimulated/ai_visible/Initialize()
|
/turf/unsimulated/ai_visible/Initialize()
|
||||||
. = ..()
|
. = ..()
|
||||||
@@ -182,6 +245,10 @@ GLOBAL_DATUM(ai_camera_room_landmark, /obj/effect/landmark/ai_multicam_room)
|
|||||||
disable_camera_telegraphing()
|
disable_camera_telegraphing()
|
||||||
if(screen && screen.ai)
|
if(screen && screen.ai)
|
||||||
screen.ai.all_eyes -= src
|
screen.ai.all_eyes -= src
|
||||||
|
if(!QDELETED(screen))
|
||||||
|
QDEL_NULL(screen)
|
||||||
|
else
|
||||||
|
screen = null
|
||||||
return ..()
|
return ..()
|
||||||
|
|
||||||
//AI procs
|
//AI procs
|
||||||
|
|||||||
@@ -63,11 +63,16 @@ proc/overmap_spacetravel(var/turf/space/T, var/atom/movable/A)
|
|||||||
if (!M)
|
if (!M)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
// Don't let AI eyes yeet themselves off the map
|
||||||
|
if(istype(A, /mob/observer/eye))
|
||||||
|
return
|
||||||
|
|
||||||
if(A.lost_in_space())
|
if(A.lost_in_space())
|
||||||
if(!QDELETED(A))
|
if(!QDELETED(A))
|
||||||
qdel(A)
|
qdel(A)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
var/nx = 1
|
var/nx = 1
|
||||||
var/ny = 1
|
var/ny = 1
|
||||||
var/nz = 1
|
var/nz = 1
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.5 KiB |
Reference in New Issue
Block a user