Fix: removed calls to inexisting "escoria" singleton after plugin disbld (#578)
* Fix: removed calls to inexisting "escoria" singleton after plugin disabled * review: added a factory-like function to get the escoria singleton Co-authored-by: Duncan Brown <duncan@prometheussoftware.ca> * Keep a reference of escoria singleton in plugin.gd
This commit is contained in:
@@ -3,16 +3,40 @@ tool
|
|||||||
extends EditorPlugin
|
extends EditorPlugin
|
||||||
|
|
||||||
|
|
||||||
# Setup Escoria
|
# The path to node 'escoria'.
|
||||||
func _enter_tree():
|
const ESCORIA: String = "/root/escoria"
|
||||||
|
|
||||||
|
|
||||||
|
# Reference to "escoria" singleton instance.
|
||||||
|
var escoria_instance: Node = null
|
||||||
|
|
||||||
|
|
||||||
|
# Returns the 'escoria' singleton/autoload.
|
||||||
|
#
|
||||||
|
# *Returns*
|
||||||
|
# The escoria singleton.
|
||||||
|
func _get_escoria():
|
||||||
|
if not is_instance_valid(escoria_instance):
|
||||||
|
escoria_instance = get_node(ESCORIA)
|
||||||
|
if not is_instance_valid(escoria_instance):
|
||||||
|
printerr(
|
||||||
|
"Escoria could not load the 'escoria' singleton/autoload.\n",
|
||||||
|
"Please try to disable and re-enable 'Escoria' plugin."
|
||||||
|
)
|
||||||
|
|
||||||
|
return escoria_instance
|
||||||
|
|
||||||
|
|
||||||
|
# Virtual function called when plugin is enabled.
|
||||||
|
func _enable_plugin():
|
||||||
add_autoload_singleton(
|
add_autoload_singleton(
|
||||||
"escoria",
|
"escoria",
|
||||||
"res://addons/escoria-core/game/escoria.tscn"
|
"res://addons/escoria-core/game/escoria.tscn"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add input actions in InputMap
|
# Add input actions in InputMap
|
||||||
InputMap.add_action(escoria.inputs_manager.SWITCH_ACTION_VERB)
|
InputMap.add_action(_get_escoria().inputs_manager.SWITCH_ACTION_VERB)
|
||||||
InputMap.add_action(escoria.inputs_manager.ESC_SHOW_DEBUG_PROMPT)
|
InputMap.add_action(_get_escoria().inputs_manager.ESC_SHOW_DEBUG_PROMPT)
|
||||||
|
|
||||||
# Define standard settings
|
# Define standard settings
|
||||||
ProjectSettings.set_setting(
|
ProjectSettings.set_setting(
|
||||||
@@ -26,6 +50,18 @@ func _enter_tree():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Virtual function called when plugin is disabled.
|
||||||
|
func _disable_plugin():
|
||||||
|
InputMap.erase_action(_get_escoria().inputs_manager.SWITCH_ACTION_VERB)
|
||||||
|
InputMap.erase_action(_get_escoria().inputs_manager.ESC_SHOW_DEBUG_PROMPT)
|
||||||
|
remove_autoload_singleton("escoria")
|
||||||
|
|
||||||
|
|
||||||
|
# Setup Escoria
|
||||||
|
func _enter_tree():
|
||||||
|
_enable_plugin()
|
||||||
|
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
# Prepare settings
|
# Prepare settings
|
||||||
set_escoria_main_settings()
|
set_escoria_main_settings()
|
||||||
@@ -38,67 +74,67 @@ func _ready():
|
|||||||
|
|
||||||
# Prepare the settings in the Escoria UI category
|
# Prepare the settings in the Escoria UI category
|
||||||
func set_escoria_ui_settings():
|
func set_escoria_ui_settings():
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.DEFAULT_DIALOG_TYPE,
|
_get_escoria().project_settings_manager.DEFAULT_DIALOG_TYPE,
|
||||||
"",
|
"",
|
||||||
{
|
{
|
||||||
"type": TYPE_STRING
|
"type": TYPE_STRING
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.GAME_SCENE,
|
_get_escoria().project_settings_manager.GAME_SCENE,
|
||||||
"",
|
"",
|
||||||
{
|
{
|
||||||
"name": escoria.project_settings_manager.GAME_SCENE,
|
"name": _get_escoria().project_settings_manager.GAME_SCENE,
|
||||||
"type": TYPE_STRING,
|
"type": TYPE_STRING,
|
||||||
"hint": PROPERTY_HINT_FILE,
|
"hint": PROPERTY_HINT_FILE,
|
||||||
"hint_string": "*.tscn, *.scn"
|
"hint_string": "*.tscn, *.scn"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.ITEMS_AUTOREGISTER_PATH,
|
_get_escoria().project_settings_manager.ITEMS_AUTOREGISTER_PATH,
|
||||||
"res://game/items/escitems/",
|
"res://game/items/escitems/",
|
||||||
{
|
{
|
||||||
"name": escoria.project_settings_manager.ITEMS_AUTOREGISTER_PATH,
|
"name": _get_escoria().project_settings_manager.ITEMS_AUTOREGISTER_PATH,
|
||||||
"type": TYPE_STRING,
|
"type": TYPE_STRING,
|
||||||
"hint": PROPERTY_HINT_DIR
|
"hint": PROPERTY_HINT_DIR
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.DEFAULT_TRANSITION,
|
_get_escoria().project_settings_manager.DEFAULT_TRANSITION,
|
||||||
"curtain",
|
"curtain",
|
||||||
{
|
{
|
||||||
"name": escoria.project_settings_manager.DEFAULT_TRANSITION,
|
"name": _get_escoria().project_settings_manager.DEFAULT_TRANSITION,
|
||||||
"type": TYPE_STRING
|
"type": TYPE_STRING
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.TRANSITION_PATHS,
|
_get_escoria().project_settings_manager.TRANSITION_PATHS,
|
||||||
[
|
[
|
||||||
"res://addons/escoria-core/game/scenes/transitions/shaders/"
|
"res://addons/escoria-core/game/scenes/transitions/shaders/"
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
"name": escoria.project_settings_manager.TRANSITION_PATHS,
|
"name": _get_escoria().project_settings_manager.TRANSITION_PATHS,
|
||||||
"type": TYPE_STRING_ARRAY,
|
"type": TYPE_STRING_ARRAY,
|
||||||
"hint": PROPERTY_HINT_DIR
|
"hint": PROPERTY_HINT_DIR
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.INVENTORY_ITEM_SIZE,
|
_get_escoria().project_settings_manager.INVENTORY_ITEM_SIZE,
|
||||||
Vector2(72, 72),
|
Vector2(72, 72),
|
||||||
{
|
{
|
||||||
"name": escoria.project_settings_manager.INVENTORY_ITEM_SIZE,
|
"name": _get_escoria().project_settings_manager.INVENTORY_ITEM_SIZE,
|
||||||
"type": TYPE_VECTOR2
|
"type": TYPE_VECTOR2
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.DIALOG_MANAGERS,
|
_get_escoria().project_settings_manager.DIALOG_MANAGERS,
|
||||||
[],
|
[],
|
||||||
{
|
{
|
||||||
"type": TYPE_STRING_ARRAY
|
"type": TYPE_STRING_ARRAY
|
||||||
@@ -108,16 +144,16 @@ func set_escoria_ui_settings():
|
|||||||
|
|
||||||
# Prepare the settings in the Escoria main category
|
# Prepare the settings in the Escoria main category
|
||||||
func set_escoria_main_settings():
|
func set_escoria_main_settings():
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.GAME_VERSION,
|
_get_escoria().project_settings_manager.GAME_VERSION,
|
||||||
"",
|
"",
|
||||||
{
|
{
|
||||||
"type": TYPE_STRING
|
"type": TYPE_STRING
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.GAME_START_SCRIPT,
|
_get_escoria().project_settings_manager.GAME_START_SCRIPT,
|
||||||
"",
|
"",
|
||||||
{
|
{
|
||||||
"type": TYPE_STRING,
|
"type": TYPE_STRING,
|
||||||
@@ -126,16 +162,16 @@ func set_escoria_main_settings():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.FORCE_QUIT,
|
_get_escoria().project_settings_manager.FORCE_QUIT,
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
"type": TYPE_BOOL
|
"type": TYPE_BOOL
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.COMMAND_DIRECTORIES,
|
_get_escoria().project_settings_manager.COMMAND_DIRECTORIES,
|
||||||
[
|
[
|
||||||
"res://addons/escoria-core/game/core-scripts/esc/commands"
|
"res://addons/escoria-core/game/core-scripts/esc/commands"
|
||||||
],
|
],
|
||||||
@@ -144,8 +180,8 @@ func set_escoria_main_settings():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.TEXT_LANG,
|
_get_escoria().project_settings_manager.TEXT_LANG,
|
||||||
TranslationServer.get_locale(),
|
TranslationServer.get_locale(),
|
||||||
{
|
{
|
||||||
"type": TYPE_STRING,
|
"type": TYPE_STRING,
|
||||||
@@ -153,8 +189,8 @@ func set_escoria_main_settings():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.VOICE_LANG,
|
_get_escoria().project_settings_manager.VOICE_LANG,
|
||||||
TranslationServer.get_locale(),
|
TranslationServer.get_locale(),
|
||||||
{
|
{
|
||||||
"type": TYPE_STRING,
|
"type": TYPE_STRING,
|
||||||
@@ -162,8 +198,8 @@ func set_escoria_main_settings():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.SAVEGAMES_PATH,
|
_get_escoria().project_settings_manager.SAVEGAMES_PATH,
|
||||||
"user://saves/",
|
"user://saves/",
|
||||||
{
|
{
|
||||||
"type": TYPE_STRING,
|
"type": TYPE_STRING,
|
||||||
@@ -171,8 +207,8 @@ func set_escoria_main_settings():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.SETTINGS_PATH,
|
_get_escoria().project_settings_manager.SETTINGS_PATH,
|
||||||
"user://",
|
"user://",
|
||||||
{
|
{
|
||||||
"type": TYPE_STRING,
|
"type": TYPE_STRING,
|
||||||
@@ -180,8 +216,8 @@ func set_escoria_main_settings():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.GAME_MIGRATION_PATH,
|
_get_escoria().project_settings_manager.GAME_MIGRATION_PATH,
|
||||||
"",
|
"",
|
||||||
{
|
{
|
||||||
"type": TYPE_STRING,
|
"type": TYPE_STRING,
|
||||||
@@ -192,32 +228,32 @@ func set_escoria_main_settings():
|
|||||||
|
|
||||||
# Prepare the settings in the Escoria debug category
|
# Prepare the settings in the Escoria debug category
|
||||||
func set_escoria_debug_settings():
|
func set_escoria_debug_settings():
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.TERMINATE_ON_WARNINGS,
|
_get_escoria().project_settings_manager.TERMINATE_ON_WARNINGS,
|
||||||
false,
|
false,
|
||||||
{
|
{
|
||||||
"type": TYPE_BOOL
|
"type": TYPE_BOOL
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.TERMINATE_ON_ERRORS,
|
_get_escoria().project_settings_manager.TERMINATE_ON_ERRORS,
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
"type": TYPE_BOOL
|
"type": TYPE_BOOL
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.DEVELOPMENT_LANG,
|
_get_escoria().project_settings_manager.DEVELOPMENT_LANG,
|
||||||
"en",
|
"en",
|
||||||
{
|
{
|
||||||
"type": TYPE_STRING
|
"type": TYPE_STRING
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.LOG_LEVEL,
|
_get_escoria().project_settings_manager.LOG_LEVEL,
|
||||||
"ERROR",
|
"ERROR",
|
||||||
{
|
{
|
||||||
"type": TYPE_STRING,
|
"type": TYPE_STRING,
|
||||||
@@ -226,8 +262,8 @@ func set_escoria_debug_settings():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.LOG_FILE_PATH,
|
_get_escoria().project_settings_manager.LOG_FILE_PATH,
|
||||||
"user://",
|
"user://",
|
||||||
{
|
{
|
||||||
"type": TYPE_STRING,
|
"type": TYPE_STRING,
|
||||||
@@ -235,8 +271,8 @@ func set_escoria_debug_settings():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.LOG_FILE_PATH,
|
_get_escoria().project_settings_manager.LOG_FILE_PATH,
|
||||||
"We're sorry, but the game crashed. Please send us the " +
|
"We're sorry, but the game crashed. Please send us the " +
|
||||||
"following files:\n\n%s",
|
"following files:\n\n%s",
|
||||||
{
|
{
|
||||||
@@ -245,16 +281,16 @@ func set_escoria_debug_settings():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.ENABLE_ROOM_SELECTOR,
|
_get_escoria().project_settings_manager.ENABLE_ROOM_SELECTOR,
|
||||||
false,
|
false,
|
||||||
{
|
{
|
||||||
"type": TYPE_BOOL
|
"type": TYPE_BOOL
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.ROOM_SELECTOR_ROOM_DIR,
|
_get_escoria().project_settings_manager.ROOM_SELECTOR_ROOM_DIR,
|
||||||
"",
|
"",
|
||||||
{
|
{
|
||||||
"type": TYPE_STRING,
|
"type": TYPE_STRING,
|
||||||
@@ -265,8 +301,8 @@ func set_escoria_debug_settings():
|
|||||||
|
|
||||||
# Prepare the settings in the Escoria sound settings
|
# Prepare the settings in the Escoria sound settings
|
||||||
func set_escoria_sound_settings():
|
func set_escoria_sound_settings():
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.MASTER_VOLUME,
|
_get_escoria().project_settings_manager.MASTER_VOLUME,
|
||||||
1,
|
1,
|
||||||
{
|
{
|
||||||
"type": TYPE_REAL,
|
"type": TYPE_REAL,
|
||||||
@@ -275,8 +311,8 @@ func set_escoria_sound_settings():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.MUSIC_VOLUME,
|
_get_escoria().project_settings_manager.MUSIC_VOLUME,
|
||||||
1,
|
1,
|
||||||
{
|
{
|
||||||
"type": TYPE_REAL,
|
"type": TYPE_REAL,
|
||||||
@@ -285,8 +321,8 @@ func set_escoria_sound_settings():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.SFX_VOLUME,
|
_get_escoria().project_settings_manager.SFX_VOLUME,
|
||||||
1,
|
1,
|
||||||
{
|
{
|
||||||
"type": TYPE_REAL,
|
"type": TYPE_REAL,
|
||||||
@@ -295,8 +331,8 @@ func set_escoria_sound_settings():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.SPEECH_VOLUME,
|
_get_escoria().project_settings_manager.SPEECH_VOLUME,
|
||||||
1,
|
1,
|
||||||
{
|
{
|
||||||
"type": TYPE_REAL,
|
"type": TYPE_REAL,
|
||||||
@@ -305,16 +341,16 @@ func set_escoria_sound_settings():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.SPEECH_ENABLED,
|
_get_escoria().project_settings_manager.SPEECH_ENABLED,
|
||||||
1,
|
1,
|
||||||
{
|
{
|
||||||
"type": TYPE_BOOL
|
"type": TYPE_BOOL
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.SPEECH_FOLDER,
|
_get_escoria().project_settings_manager.SPEECH_FOLDER,
|
||||||
"res://speech",
|
"res://speech",
|
||||||
{
|
{
|
||||||
"type": TYPE_STRING,
|
"type": TYPE_STRING,
|
||||||
@@ -322,8 +358,8 @@ func set_escoria_sound_settings():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.SPEECH_EXTENSION,
|
_get_escoria().project_settings_manager.SPEECH_EXTENSION,
|
||||||
"ogg",
|
"ogg",
|
||||||
{
|
{
|
||||||
"type": TYPE_STRING
|
"type": TYPE_STRING
|
||||||
@@ -338,16 +374,16 @@ func set_escoria_platform_settings():
|
|||||||
# scenes.
|
# scenes.
|
||||||
# If set to true, all generic scenes (UI, inventory, etc) will be loaded
|
# If set to true, all generic scenes (UI, inventory, etc) will be loaded
|
||||||
# as any other scene.
|
# as any other scene.
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.SKIP_CACHE,
|
_get_escoria().project_settings_manager.SKIP_CACHE,
|
||||||
false,
|
false,
|
||||||
{
|
{
|
||||||
"type": TYPE_BOOL
|
"type": TYPE_BOOL
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
escoria.project_settings_manager.register_setting(
|
_get_escoria().project_settings_manager.register_setting(
|
||||||
escoria.project_settings_manager.SKIP_CACHE_MOBILE,
|
_get_escoria().project_settings_manager.SKIP_CACHE_MOBILE,
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
"type": TYPE_BOOL
|
"type": TYPE_BOOL
|
||||||
@@ -357,8 +393,5 @@ func set_escoria_platform_settings():
|
|||||||
|
|
||||||
# Uninstall plugin
|
# Uninstall plugin
|
||||||
func _exit_tree():
|
func _exit_tree():
|
||||||
remove_autoload_singleton("escoria")
|
_disable_plugin()
|
||||||
|
|
||||||
InputMap.erase_action(escoria.inputs_manager.SWITCH_ACTION_VERB)
|
|
||||||
InputMap.erase_action(escoria.inputs_manager.ESC_SHOW_DEBUG_PROMPT)
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user