mirror of
https://github.com/RPCS3/rpcs3.git
synced 2024-11-22 02:32:36 +01:00
cellMsgDialog: only abort dialogs that were actually spawned by cellMsgDialog
This commit is contained in:
parent
d88b7f6fde
commit
c89e30b3d9
@ -72,7 +72,7 @@ struct cross_controller
|
||||
msg_dialog_callback.set(g_fxo->get<ppu_function_manager>().func_addr(FIND_FUNC(finish_callback)));
|
||||
|
||||
// TODO: Show icons from comboplay_plugin.rco in dialog. Maybe use a new dialog or add an optional icon to this one.
|
||||
error_code res = open_msg_dialog(false, CELL_MSGDIALOG_TYPE_DISABLE_CANCEL_OFF, vm::make_str(msg), msg_dialog_callback, userdata);
|
||||
error_code res = open_msg_dialog(false, CELL_MSGDIALOG_TYPE_DISABLE_CANCEL_OFF, vm::make_str(msg), msg_dialog_source::_cellCrossController, msg_dialog_callback, userdata);
|
||||
|
||||
sysutil_register_cb([this, res](ppu_thread& ppu) -> s32
|
||||
{
|
||||
|
@ -579,7 +579,7 @@ error_code cellHddGameCheck(ppu_thread& ppu, u32 version, vm::cptr<char> dirName
|
||||
lv2_obj::sleep(ppu);
|
||||
|
||||
// Get user confirmation by opening a blocking dialog
|
||||
error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_ERROR | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_OK | CELL_MSGDIALOG_TYPE_DISABLE_CANCEL_ON, vm::make_str(error_msg));
|
||||
error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_ERROR | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_OK | CELL_MSGDIALOG_TYPE_DISABLE_CANCEL_ON, vm::make_str(error_msg), msg_dialog_source::_cellGame);
|
||||
|
||||
// Reschedule after a blocking dialog returns
|
||||
if (ppu.check_state())
|
||||
@ -664,7 +664,7 @@ error_code cellHddGameSetSystemVer(vm::cptr<char> systemVersion)
|
||||
error_code cellHddGameExitBroken()
|
||||
{
|
||||
cellGame.warning("cellHddGameExitBroken()");
|
||||
return open_exit_dialog(get_localized_string(localized_string_id::CELL_HDD_GAME_EXIT_BROKEN), true);
|
||||
return open_exit_dialog(get_localized_string(localized_string_id::CELL_HDD_GAME_EXIT_BROKEN), true, msg_dialog_source::_cellGame);
|
||||
}
|
||||
|
||||
error_code cellGameDataGetSizeKB(ppu_thread& ppu, vm::ptr<u32> size)
|
||||
@ -723,7 +723,7 @@ error_code cellGameDataSetSystemVer(vm::cptr<char> systemVersion)
|
||||
error_code cellGameDataExitBroken()
|
||||
{
|
||||
cellGame.warning("cellGameDataExitBroken()");
|
||||
return open_exit_dialog(get_localized_string(localized_string_id::CELL_GAME_DATA_EXIT_BROKEN), true);
|
||||
return open_exit_dialog(get_localized_string(localized_string_id::CELL_GAME_DATA_EXIT_BROKEN), true, msg_dialog_source::_cellGame);
|
||||
}
|
||||
|
||||
error_code cellGameBootCheck(vm::ptr<u32> type, vm::ptr<u32> attributes, vm::ptr<CellGameContentSize> size, vm::ptr<char[CELL_GAME_DIRNAME_SIZE]> dirName)
|
||||
@ -1198,7 +1198,7 @@ error_code cellGameDataCheckCreate2(ppu_thread& ppu, u32 version, vm::cptr<char>
|
||||
lv2_obj::sleep(ppu);
|
||||
|
||||
// Get user confirmation by opening a blocking dialog
|
||||
error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_ERROR | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_OK | CELL_MSGDIALOG_TYPE_DISABLE_CANCEL_ON, vm::make_str(error_msg));
|
||||
error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_ERROR | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_OK | CELL_MSGDIALOG_TYPE_DISABLE_CANCEL_ON, vm::make_str(error_msg), msg_dialog_source::_cellGame);
|
||||
|
||||
// Reschedule after a blocking dialog returns
|
||||
if (ppu.check_state())
|
||||
@ -1714,7 +1714,7 @@ error_code cellGameContentErrorDialog(s32 type, s32 errNeedSizeKB, vm::cptr<char
|
||||
error_msg += get_localized_string(localized_string_id::CELL_GAME_ERROR_DIR_NAME, fmt::format("%s", dirName).c_str());
|
||||
}
|
||||
|
||||
return open_exit_dialog(error_msg, type > CELL_GAME_ERRDIALOG_NOSPACE);
|
||||
return open_exit_dialog(error_msg, type > CELL_GAME_ERRDIALOG_NOSPACE, msg_dialog_source::_cellGame);
|
||||
}
|
||||
|
||||
error_code cellGameThemeInstall(vm::cptr<char> usrdirPath, vm::cptr<char> fileName, u32 option)
|
||||
|
@ -33,6 +33,27 @@ void fmt_class_string<CellMsgDialogError>::format(std::string& out, u64 arg)
|
||||
});
|
||||
}
|
||||
|
||||
template<>
|
||||
void fmt_class_string<msg_dialog_source>::format(std::string& out, u64 arg)
|
||||
{
|
||||
format_enum(out, arg, [](auto src)
|
||||
{
|
||||
switch (src)
|
||||
{
|
||||
case msg_dialog_source::_cellMsgDialog: return "cellMsgDialog";
|
||||
case msg_dialog_source::_cellSaveData: return "cellSaveData";
|
||||
case msg_dialog_source::_cellGame: return "cellGame";
|
||||
case msg_dialog_source::_cellCrossController: return "cellCrossController";
|
||||
case msg_dialog_source::_sceNp: return "sceNp";
|
||||
case msg_dialog_source::_sceNpTrophy: return "sceNpTrophy";
|
||||
case msg_dialog_source::sys_progress: return "sys_progress";
|
||||
case msg_dialog_source::shader_loading: return "shader_loading";
|
||||
}
|
||||
|
||||
return unknown;
|
||||
});
|
||||
}
|
||||
|
||||
MsgDialogBase::~MsgDialogBase()
|
||||
{
|
||||
}
|
||||
@ -141,9 +162,9 @@ using msg_dlg_thread = named_thread<msg_dlg_thread_info>;
|
||||
error_code cellMsgDialogOpen2(u32 type, vm::cptr<char> msgString, vm::ptr<CellMsgDialogCallback> callback, vm::ptr<void> userData, vm::ptr<void> extParam);
|
||||
|
||||
// wrapper to call for other hle dialogs
|
||||
error_code open_msg_dialog(bool is_blocking, u32 type, vm::cptr<char> msgString, vm::ptr<CellMsgDialogCallback> callback, vm::ptr<void> userData, vm::ptr<void> extParam, s32* return_code)
|
||||
error_code open_msg_dialog(bool is_blocking, u32 type, vm::cptr<char> msgString, msg_dialog_source source, vm::ptr<CellMsgDialogCallback> callback, vm::ptr<void> userData, vm::ptr<void> extParam, s32* return_code)
|
||||
{
|
||||
cellSysutil.notice("open_msg_dialog(is_blocking=%d, type=0x%x, msgString=%s, callback=*0x%x, userData=*0x%x, extParam=*0x%x, return_code=*0x%x)", is_blocking, type, msgString, callback, userData, extParam, return_code);
|
||||
cellSysutil.notice("open_msg_dialog(is_blocking=%d, type=0x%x, msgString=%s, source=%s, callback=*0x%x, userData=*0x%x, extParam=*0x%x, return_code=*0x%x)", is_blocking, type, msgString, source, callback, userData, extParam, return_code);
|
||||
|
||||
const MsgDialogType _type{ type };
|
||||
|
||||
@ -166,7 +187,7 @@ error_code open_msg_dialog(bool is_blocking, u32 type, vm::cptr<char> msgString,
|
||||
|
||||
const auto notify = std::make_shared<atomic_t<u32>>(0);
|
||||
|
||||
const auto res = manager->create<rsx::overlays::message_dialog>()->show(is_blocking, msgString.get_ptr(), _type, [callback, userData, &return_code, is_blocking, notify](s32 status)
|
||||
const auto res = manager->create<rsx::overlays::message_dialog>()->show(is_blocking, msgString.get_ptr(), _type, source, [callback, userData, &return_code, is_blocking, notify](s32 status)
|
||||
{
|
||||
if (is_blocking && return_code)
|
||||
{
|
||||
@ -213,6 +234,7 @@ error_code open_msg_dialog(bool is_blocking, u32 type, vm::cptr<char> msgString,
|
||||
}
|
||||
|
||||
dlg->type = _type;
|
||||
dlg->source = source;
|
||||
|
||||
dlg->on_close = [callback, userData, is_blocking, &return_code, wptr = std::weak_ptr<MsgDialogBase>(dlg)](s32 status)
|
||||
{
|
||||
@ -318,9 +340,9 @@ void exit_game(s32/* buttonType*/, vm::ptr<void>/* userData*/)
|
||||
sysutil_send_system_cmd(CELL_SYSUTIL_REQUEST_EXITGAME, 0);
|
||||
}
|
||||
|
||||
error_code open_exit_dialog(const std::string& message, bool is_exit_requested)
|
||||
error_code open_exit_dialog(const std::string& message, bool is_exit_requested, msg_dialog_source source)
|
||||
{
|
||||
cellSysutil.notice("open_exit_dialog(message=%s, is_exit_requested=%d)", message, is_exit_requested);
|
||||
cellSysutil.notice("open_exit_dialog(message=%s, is_exit_requested=%d, source=%s)", message, is_exit_requested, source);
|
||||
|
||||
vm::bptr<CellMsgDialogCallback> callback = vm::null;
|
||||
|
||||
@ -334,6 +356,7 @@ error_code open_exit_dialog(const std::string& message, bool is_exit_requested)
|
||||
true,
|
||||
CELL_MSGDIALOG_TYPE_SE_TYPE_ERROR | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_OK | CELL_MSGDIALOG_TYPE_DISABLE_CANCEL_ON,
|
||||
vm::make_str(message),
|
||||
source,
|
||||
callback
|
||||
);
|
||||
|
||||
@ -401,7 +424,7 @@ error_code cellMsgDialogOpen2(u32 type, vm::cptr<char> msgString, vm::ptr<CellMs
|
||||
cellSysutil.error("Opening error message dialog with message: %s", msgString);
|
||||
}
|
||||
|
||||
return open_msg_dialog(false, type, msgString, callback, userData, extParam);
|
||||
return open_msg_dialog(false, type, msgString, msg_dialog_source::_cellMsgDialog, callback, userData, extParam);
|
||||
}
|
||||
|
||||
error_code cellMsgDialogOpen(u32 type, vm::cptr<char> msgString, vm::ptr<CellMsgDialogCallback> callback, vm::ptr<void> userData, vm::ptr<void> extParam)
|
||||
@ -496,7 +519,8 @@ error_code cellMsgDialogClose(f32 delay)
|
||||
|
||||
if (auto manager = g_fxo->try_get<rsx::overlays::display_manager>())
|
||||
{
|
||||
if (auto dlg = manager->get<rsx::overlays::message_dialog>())
|
||||
if (auto dlg = manager->get<rsx::overlays::message_dialog>();
|
||||
dlg && dlg->source() == msg_dialog_source::_cellMsgDialog)
|
||||
{
|
||||
auto& thr = g_fxo->get<msg_dlg_thread>();
|
||||
thr.wait_until = wait_until;
|
||||
@ -509,7 +533,7 @@ error_code cellMsgDialogClose(f32 delay)
|
||||
|
||||
const auto dlg = g_fxo->get<msg_info>().get();
|
||||
|
||||
if (!dlg)
|
||||
if (!dlg || dlg->source != msg_dialog_source::_cellMsgDialog)
|
||||
{
|
||||
return CELL_MSGDIALOG_ERROR_DIALOG_NOT_OPENED;
|
||||
}
|
||||
@ -526,7 +550,8 @@ error_code cellMsgDialogAbort()
|
||||
|
||||
if (auto manager = g_fxo->try_get<rsx::overlays::display_manager>())
|
||||
{
|
||||
if (auto dlg = manager->get<rsx::overlays::message_dialog>())
|
||||
if (auto dlg = manager->get<rsx::overlays::message_dialog>();
|
||||
dlg && dlg->source() == msg_dialog_source::_cellMsgDialog)
|
||||
{
|
||||
g_fxo->get<msg_dlg_thread>().wait_until = 0;
|
||||
dlg->close(false, true); // this doesn't call on_close
|
||||
@ -539,7 +564,7 @@ error_code cellMsgDialogAbort()
|
||||
|
||||
const auto dlg = g_fxo->get<msg_info>().get();
|
||||
|
||||
if (!dlg)
|
||||
if (!dlg || dlg->source != msg_dialog_source::_cellMsgDialog)
|
||||
{
|
||||
return CELL_OK; // Not CELL_MSGDIALOG_ERROR_DIALOG_NOT_OPENED, tested on HW.
|
||||
}
|
||||
|
@ -89,9 +89,21 @@ enum class MsgDialogState
|
||||
Close,
|
||||
};
|
||||
|
||||
enum class msg_dialog_source
|
||||
{
|
||||
_cellMsgDialog,
|
||||
_cellSaveData,
|
||||
_cellGame,
|
||||
_cellCrossController,
|
||||
_sceNp,
|
||||
_sceNpTrophy,
|
||||
sys_progress,
|
||||
shader_loading,
|
||||
};
|
||||
|
||||
void close_msg_dialog();
|
||||
error_code open_msg_dialog(bool is_blocking, u32 type, vm::cptr<char> msgString, vm::ptr<CellMsgDialogCallback> callback = vm::null, vm::ptr<void> userData = vm::null, vm::ptr<void> extParam = vm::null, s32* return_code = nullptr);
|
||||
error_code open_exit_dialog(const std::string& message, bool is_exit_requested);
|
||||
error_code open_msg_dialog(bool is_blocking, u32 type, vm::cptr<char> msgString, msg_dialog_source source, vm::ptr<CellMsgDialogCallback> callback = vm::null, vm::ptr<void> userData = vm::null, vm::ptr<void> extParam = vm::null, s32* return_code = nullptr);
|
||||
error_code open_exit_dialog(const std::string& message, bool is_exit_requested, msg_dialog_source source);
|
||||
|
||||
class MsgDialogBase
|
||||
{
|
||||
@ -103,6 +115,7 @@ public:
|
||||
atomic_t<MsgDialogState> state{ MsgDialogState::Close };
|
||||
|
||||
MsgDialogType type{};
|
||||
msg_dialog_source source = msg_dialog_source::_cellMsgDialog;
|
||||
|
||||
std::function<void(s32 status)> on_close = nullptr;
|
||||
|
||||
|
@ -333,7 +333,7 @@ static error_code select_and_delete(ppu_thread& ppu)
|
||||
|
||||
// Get user confirmation by opening a blocking dialog
|
||||
s32 return_code = CELL_MSGDIALOG_BUTTON_NONE;
|
||||
error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_NORMAL | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_YESNO, vm::make_str(msg), vm::null, vm::null, vm::null, &return_code);
|
||||
error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_NORMAL | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_YESNO, vm::make_str(msg), msg_dialog_source::_cellSaveData, vm::null, vm::null, vm::null, &return_code);
|
||||
|
||||
// Reschedule after a blocking dialog returns
|
||||
if (ppu.check_state())
|
||||
@ -370,7 +370,7 @@ static error_code select_and_delete(ppu_thread& ppu)
|
||||
lv2_obj::sleep(ppu);
|
||||
|
||||
// Display success message by opening a blocking dialog (return value should be irrelevant here)
|
||||
res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_NORMAL | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_OK, vm::make_str(msg));
|
||||
res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_NORMAL | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_OK, vm::make_str(msg), msg_dialog_source::_cellSaveData);
|
||||
|
||||
// Reschedule after blocking dialog returns
|
||||
if (ppu.check_state())
|
||||
@ -423,7 +423,7 @@ static error_code display_callback_result_error_message(ppu_thread& ppu, const C
|
||||
lv2_obj::sleep(ppu);
|
||||
|
||||
// Get user confirmation by opening a blocking dialog (return value should be irrelevant here)
|
||||
[[maybe_unused]] error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_NORMAL | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_OK, use_invalid_message ? result.invalidMsg : vm::make_str(msg));
|
||||
[[maybe_unused]] error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_NORMAL | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_OK, use_invalid_message ? result.invalidMsg : vm::make_str(msg), msg_dialog_source::_cellSaveData);
|
||||
|
||||
// Reschedule after a blocking dialog returns
|
||||
if (ppu.check_state())
|
||||
@ -1219,7 +1219,7 @@ static NEVER_INLINE error_code savedata_op(ppu_thread& ppu, u32 operation, u32 v
|
||||
|
||||
// Get user confirmation by opening a blocking dialog
|
||||
s32 return_code = CELL_MSGDIALOG_BUTTON_NONE;
|
||||
error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_NORMAL | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_YESNO, vm::make_str(message), vm::null, vm::null, vm::null, &return_code);
|
||||
error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_NORMAL | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_YESNO, vm::make_str(message), msg_dialog_source::_cellSaveData, vm::null, vm::null, vm::null, &return_code);
|
||||
|
||||
// Reschedule after a blocking dialog returns
|
||||
if (ppu.check_state())
|
||||
@ -1350,7 +1350,7 @@ static NEVER_INLINE error_code savedata_op(ppu_thread& ppu, u32 operation, u32 v
|
||||
|
||||
// Get user confirmation by opening a blocking dialog
|
||||
s32 return_code = CELL_MSGDIALOG_BUTTON_NONE;
|
||||
error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_NORMAL | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_YESNO, vm::make_str(message), vm::null, vm::null, vm::null, &return_code);
|
||||
error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_SE_TYPE_NORMAL | CELL_MSGDIALOG_TYPE_BUTTON_TYPE_YESNO, vm::make_str(message), msg_dialog_source::_cellSaveData, vm::null, vm::null, vm::null, &return_code);
|
||||
|
||||
// Reschedule after a blocking dialog returns
|
||||
if (ppu.check_state())
|
||||
|
@ -572,7 +572,7 @@ error_code sceNpTrophyRegisterContext(ppu_thread& ppu, u32 context, u32 handle,
|
||||
{
|
||||
if (!!(options & SCE_NP_TROPHY_OPTIONS_REGISTER_CONTEXT_SHOW_ERROR_EXIT))
|
||||
{
|
||||
static_cast<void>(open_exit_dialog("Error during trophy registration! The game will now be terminated.", true));
|
||||
static_cast<void>(open_exit_dialog("Error during trophy registration! The game will now be terminated.", true, msg_dialog_source::_sceNpTrophy));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -247,7 +247,7 @@ namespace rsx
|
||||
// Hide list
|
||||
visible = false;
|
||||
|
||||
error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_BUTTON_TYPE_YESNO, vm::make_str(confirmation_msg), vm::null, vm::null, vm::null, &confirmation_code);
|
||||
error_code res = open_msg_dialog(true, CELL_MSGDIALOG_TYPE_BUTTON_TYPE_YESNO, vm::make_str(confirmation_msg), msg_dialog_source::_sceNp, vm::null, vm::null, vm::null, &confirmation_code);
|
||||
if (res != CELL_OK)
|
||||
{
|
||||
rsx_log.fatal("sendmessage dialog failed to open confirmation dialog (error=%d)", +res);
|
||||
|
@ -21,7 +21,7 @@ namespace rsx
|
||||
|
||||
dlg = g_fxo->get<rsx::overlays::display_manager>().create<rsx::overlays::message_dialog>(true);
|
||||
dlg->progress_bar_set_taskbar_index(-1);
|
||||
dlg->show(false, msg, type, [](s32 status)
|
||||
dlg->show(false, msg, type, msg_dialog_source::shader_loading, [](s32 status)
|
||||
{
|
||||
if (status != CELL_OK)
|
||||
{
|
||||
|
@ -201,9 +201,10 @@ namespace rsx
|
||||
fade_animation.update(timestamp_us);
|
||||
}
|
||||
|
||||
error_code message_dialog::show(bool is_blocking, const std::string& text, const MsgDialogType& type, std::function<void(s32 status)> on_close)
|
||||
error_code message_dialog::show(bool is_blocking, const std::string& text, const MsgDialogType& type, msg_dialog_source source, std::function<void(s32 status)> on_close)
|
||||
{
|
||||
visible = false;
|
||||
m_source = source;
|
||||
|
||||
num_progress_bars = type.progress_bar_count;
|
||||
if (num_progress_bars)
|
||||
|
@ -10,6 +10,8 @@ namespace rsx
|
||||
{
|
||||
class message_dialog : public user_interface
|
||||
{
|
||||
msg_dialog_source m_source = msg_dialog_source::_cellMsgDialog;
|
||||
|
||||
label text_display;
|
||||
image_button btn_ok;
|
||||
image_button btn_cancel;
|
||||
@ -44,7 +46,7 @@ namespace rsx
|
||||
void on_button_pressed(pad_button button_press, bool is_auto_repeat) override;
|
||||
void close(bool use_callback, bool stop_pad_interception) override;
|
||||
|
||||
error_code show(bool is_blocking, const std::string& text, const MsgDialogType& type, std::function<void(s32 status)> on_close);
|
||||
error_code show(bool is_blocking, const std::string& text, const MsgDialogType& type, msg_dialog_source source, std::function<void(s32 status)> on_close);
|
||||
|
||||
void set_text(std::string text);
|
||||
void update_custom_background();
|
||||
@ -56,6 +58,8 @@ namespace rsx
|
||||
error_code progress_bar_set_value(u32 index, f32 value);
|
||||
error_code progress_bar_reset(u32 index);
|
||||
error_code progress_bar_set_limit(u32 index, u32 limit);
|
||||
|
||||
msg_dialog_source source() const { return m_source; }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ void progress_dialog_server::operator()()
|
||||
type.progress_bar_count = 1;
|
||||
|
||||
native_dlg = manager->create<rsx::overlays::progress_dialog>(true);
|
||||
native_dlg->show(false, text0, type, nullptr);
|
||||
native_dlg->show(false, text0, type, msg_dialog_source::sys_progress, nullptr);
|
||||
native_dlg->progress_bar_set_message(0, "Please wait");
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user