Implemented the start-up logic in the MonoGame port.
This commit is contained in:
parent
97e71a8e3c
commit
2b67f56e9c
@ -115,6 +115,11 @@ public class BaseGame : Game
|
||||
}
|
||||
Monitor.Exit(_imGuiRenderables);
|
||||
|
||||
Console.WriteLine("X = {0}, Y = {1}", _imGuiRenderer.LastKnownMouseX,_imGuiRenderer.LastKnownMouseY);
|
||||
if (DebugMouse)
|
||||
{
|
||||
Console.WriteLine("X = {0}, Y = {1}", _imGuiRenderer.LastKnownMouseX, _imGuiRenderer.LastKnownMouseY);
|
||||
}
|
||||
}
|
||||
|
||||
public bool DebugMouse { get; set; }
|
||||
}
|
||||
63
GUIs/skyscraper8.UI.ImGui.MonoGame/Forms/MessageWindow.cs
Normal file
63
GUIs/skyscraper8.UI.ImGui.MonoGame/Forms/MessageWindow.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using ImGuiNET;
|
||||
using skyscraper8.UI.MonoGame.Bridge;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Numerics;
|
||||
|
||||
namespace skyscraper8.UI.MonoGame.Forms
|
||||
{
|
||||
internal class MessageWindow : ImGuiRenderable
|
||||
{
|
||||
public string Message { get; }
|
||||
private string WindowUuid;
|
||||
|
||||
public MessageWindow(string message)
|
||||
{
|
||||
Message = message;
|
||||
WindowUuid = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
private bool sizeSet;
|
||||
public void Update()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private bool Closed;
|
||||
public void Render()
|
||||
{
|
||||
bool closeMe = true;
|
||||
if (!sizeSet)
|
||||
{
|
||||
ImGui.SetNextWindowSize(new Vector2(300, 160));
|
||||
sizeSet = true;
|
||||
}
|
||||
|
||||
ImGui.Begin(String.Format("Information ##{0}", WindowUuid), ref closeMe);
|
||||
ImGui.TextWrapped(Message);
|
||||
if (ImGui.Button("OK"))
|
||||
closeMe = false;
|
||||
ImGui.End();
|
||||
if (!closeMe)
|
||||
{
|
||||
Closed = true;
|
||||
if (OnClose != null)
|
||||
OnClose();
|
||||
}
|
||||
}
|
||||
|
||||
public bool WasClosed()
|
||||
{
|
||||
return Closed;
|
||||
}
|
||||
|
||||
public Action OnClose { get; set; }
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,308 @@
|
||||
namespace skyscraper8.UI.MonoGame
|
||||
using skyscraper5.Skyscraper;
|
||||
using skyscraper5.Skyscraper.Equipment;
|
||||
using skyscraper5.Skyscraper.Gps;
|
||||
using skyscraper5.Skyscraper.IO.TunerInterface;
|
||||
using skyscraper5.Skyscraper.Scraper.Storage.Filesystem;
|
||||
using skyscraper5.Skyscraper.Scraper.Storage.InMemory;
|
||||
using skyscraper5.src.Skyscraper;
|
||||
using skyscraper8.Skyscraper.Plugins;
|
||||
using skyscraper8.Skyscraper.Scraper.Storage;
|
||||
using System.IO;
|
||||
using System.Net.NetworkInformation;
|
||||
|
||||
namespace skyscraper8.UI.MonoGame
|
||||
{
|
||||
class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
SkyscraperGame game = new SkyscraperGame();
|
||||
PluginLogger localLogger = PluginLogManager.GetLogger(typeof(Program));
|
||||
SkyscraperHandleCollection handles = new SkyscraperHandleCollection();
|
||||
Queue<string> errors = new Queue<string>();
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info,"Starting up...");
|
||||
|
||||
//LOAD STORAGE ----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info, "Starting Storage Connection Manager...");
|
||||
StorageConnectionManager connectionManager = StorageConnectionManager.GetInstance();
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info, "Enumerating Data Storage Factories...");
|
||||
handles.AllDataStorages = connectionManager.GetDataStorages().ToList().AsReadOnly();
|
||||
|
||||
localLogger.Log(PluginLogLevel.Debug, "Enumerating Object Storage Factories...");
|
||||
handles.AllObjectStorages = connectionManager.GetObjectStorages().ToList().AsReadOnly();
|
||||
|
||||
localLogger.Log(PluginLogLevel.Debug, "Checking for configuration file...");
|
||||
if (!connectionManager.IniExists())
|
||||
{
|
||||
localLogger.Log(PluginLogLevel.Warn, "Configuration file not found!");
|
||||
errors.Enqueue(String.Format("The configuration file does not exist.\r\nPlease create one in the UI!"));
|
||||
handles.Ini = new Ini();
|
||||
}
|
||||
|
||||
DataStorageFactory dataStorageFactory;
|
||||
try
|
||||
{
|
||||
localLogger.Log(PluginLogLevel.Debug, "Getting default data storage factory...");
|
||||
dataStorageFactory = connectionManager.GetDefaultDataStorageFactory();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
localLogger.Log(PluginLogLevel.Error, "Failed to start the data storage factory: {0}", e.Message);
|
||||
errors.Enqueue(String.Format("Could not load the data storage factory.\nThe following went wrong:\n\n{0}\n\n", e.Message));
|
||||
dataStorageFactory = new InMemoryScraperStorageFactory();
|
||||
}
|
||||
|
||||
ObjectStorageFactory objectStorageFactory;
|
||||
try
|
||||
{
|
||||
localLogger.Log(PluginLogLevel.Debug, "Getting default object storage factory...");
|
||||
objectStorageFactory = connectionManager.GetDefaultObjectStorageFactory();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
localLogger.Log(PluginLogLevel.Error, "Failed to start the object storage factory: {0}", e.Message);
|
||||
errors.Enqueue(String.Format("Could not load the object storage factory.\nThe following went wrong:\n\n{0}\n\n", e.Message));
|
||||
objectStorageFactory = new FilesystemScraperStorageFactory()
|
||||
{
|
||||
Directory = "dummy_object_storage"
|
||||
};
|
||||
}
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info, "Creating Data Storage...");
|
||||
handles.DataStorage = dataStorageFactory.CreateDataStorage();
|
||||
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info, "Checking whether the data storage and the object storage are the same...");
|
||||
bool equivalentStorages = objectStorageFactory.IsEquivalent(dataStorageFactory);
|
||||
|
||||
if (equivalentStorages)
|
||||
{
|
||||
localLogger.Log(PluginLogLevel.Info, "Casting Data Storage to Object Storage.");
|
||||
handles.ObjectStorage = (ObjectStorage)handles.DataStorage;
|
||||
}
|
||||
else
|
||||
{
|
||||
localLogger.Log(PluginLogLevel.Info,"It isn't -> Creating object storage...");
|
||||
try
|
||||
{
|
||||
handles.ObjectStorage = objectStorageFactory.CreateObjectStorage();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
string objectStorageName = connectionManager.GetName(objectStorageFactory);
|
||||
localLogger.Log(PluginLogLevel.Error, "Failed to start the object storage factory: {0}", e.Message);
|
||||
errors.Enqueue(String.Format("Could not load {1}.\nThe following went wrong:\n\n{0}\n\n", e.Message, objectStorageName));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info,"Reporting UI Version to the storages...");
|
||||
handles.DataStorage?.UiSetVersion(2);
|
||||
handles.ObjectStorage?.UiSetVersion(2);
|
||||
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info,"Testing whether the data storage is responding...");
|
||||
try
|
||||
{
|
||||
handles.DataStorage.Ping();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
string brokenStorageName = connectionManager.GetName(dataStorageFactory);
|
||||
InMemoryScraperStorageFactory fssf = new InMemoryScraperStorageFactory();
|
||||
handles.DataStorage = fssf.CreateDataStorage();
|
||||
errors.Enqueue(String.Format(
|
||||
"{0} failed to respond.\nThe following went wrong:\n\n{1}\n\nI've switched to a volatile in-memory storage. This will work, but is quite possibly not what you want. It will not and cannot save any data. Please consider configuring {0} correctly.",
|
||||
brokenStorageName, e.Message));
|
||||
}
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info,"Please wait while I test whether the object storage is responding...");
|
||||
try
|
||||
{
|
||||
handles.ObjectStorage.Ping();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
string brokenStorageName = connectionManager.GetName(objectStorageFactory);
|
||||
FilesystemScraperStorageFactory fssf = new FilesystemScraperStorageFactory();
|
||||
fssf.Directory = "dummy_object_storage";
|
||||
handles.ObjectStorage = fssf.CreateObjectStorage();
|
||||
errors.Enqueue(String.Format(
|
||||
"{0} failed to respond.\nThe following went wrong:\n\n{1}\n\nI've switched to a file system based storage. This will work, but is quite possibly not what you want. Please consider configuring {0} correctly.",
|
||||
brokenStorageName, e.Message));
|
||||
}
|
||||
|
||||
//LOAD GPS ----------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
localLogger.Log(PluginLogLevel.Info, "Please wait while I load the GPS receiver library...");
|
||||
int gpsReceiverId = GpsManager.GetConfiguredGpsId();
|
||||
IGpsReceiverFactory gpsReceiverFactory = GpsManager.GetGpsReceiverFactoryById(gpsReceiverId);
|
||||
GpsManager.AutoconfigureGpsReceiverFactory(gpsReceiverFactory);
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info,"Instantiating the GPS receiver...");
|
||||
handles.Gps = gpsReceiverFactory.CreateGpsReceiver();
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info,"Starting the GPS receiver...");
|
||||
handles.Gps.Start();
|
||||
|
||||
//LOAD BASE DATA --------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
localLogger.Log(PluginLogLevel.Info,"Querying the storage for known satellite positions...");
|
||||
handles.SatellitePositions = handles.DataStorage.UiSatellitesListAll();
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info,"Checking for the default LNB types...");
|
||||
EquipmentUtilities.InsertDefaultLnbTypes(handles.DataStorage);
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info,"Querying the storage for known LNB types...");
|
||||
handles.LnbTypes = handles.DataStorage.UiLnbTypesListAll();
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info,"Checking for the default dish types...");
|
||||
EquipmentUtilities.InsertDefaultDishTypes(handles.DataStorage);
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info,"Querying the storage for known Dish types...");
|
||||
handles.DishTypes = handles.DataStorage.UiDishTypesListAll();
|
||||
|
||||
//LOAD TUNER --------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
localLogger.Log(PluginLogLevel.Info,"Chcking the Tuner Factory classes...");
|
||||
TunerFactoryConnectionManager tunerFactoryConnectionManager = TunerFactoryConnectionManager.GetInstance();
|
||||
handles.AllTunerFactories = tunerFactoryConnectionManager.GetKnownFactories();
|
||||
int tunerFactory = handles.Ini.ReadValue("startup", "tunerFactory", 0);
|
||||
KeyValuePair<TunerFactoryIdAttribute, ITunerFactory> bootingTuner = handles.AllTunerFactories.First(x => x.Key.Id == tunerFactory);
|
||||
bool isNoTuner = bootingTuner.Key.DisplayName.Equals("No tuner");
|
||||
if (isNoTuner)
|
||||
{
|
||||
errors.Enqueue("Please not that Skyscraper is currently configured to not use a Tuner Factory Class. This will work, but functionality will be severely limited. Please configure a Tuner Factory Class in order to get the best experience.");
|
||||
}
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info,"Applying the tuner factory class configuration..:");
|
||||
TunerFactoryConnectionManager.ConfigureFactoryFromIni(bootingTuner, handles.Ini);
|
||||
|
||||
List<TunerMetadata> foundTuners = new List<TunerMetadata>();
|
||||
localLogger.Log(PluginLogLevel.Info,"Please wait while the Tuner Factory class code is being executed...");
|
||||
try
|
||||
{
|
||||
handles.StreamReader = bootingTuner.Value.CreateStreamReader();
|
||||
try
|
||||
{
|
||||
localLogger.Log(PluginLogLevel.Info,"Trying to see whether the tuner factory works...");
|
||||
handles.StreamReader.CheckForDVB();
|
||||
}
|
||||
catch (BadImageFormatException e)
|
||||
{
|
||||
errors.Enqueue("The configured tuner factory is not suitable for the current processor architecture. Tuning won't work, therefore functionality will be severely limited. Please configure a Tuner Factory Class suitable for this processor architecture in order to get the best experience. If you need to run crazycat69's StreamReader.dll on 64-bit machines, try RemoteStreamReader.");
|
||||
handles.StreamReader = new NullTunerFactory().CreateStreamReader();
|
||||
}
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info, "Checking for your tuners...");
|
||||
bool checkForDvbExEx = handles.StreamReader.CheckForDVBExEx((index, name, type) =>
|
||||
{
|
||||
TunerMetadata tuner = new TunerMetadata(index, name, type);
|
||||
localLogger.Log(PluginLogLevel.Info, String.Format("Found tuner {0}", name));
|
||||
foundTuners.Add(tuner);
|
||||
});
|
||||
if (!checkForDvbExEx)
|
||||
{
|
||||
if (!isNoTuner)
|
||||
localLogger.Log(PluginLogLevel.Error, String.Format("{0} has failed. Tuning won't be possible!", nameof(handles.StreamReader.CheckForDVBExEx)));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
localLogger.Log(PluginLogLevel.Error,"Oh dear, it failed.");
|
||||
errors.Enqueue(
|
||||
"Please not that the Tuner Factory class code failed to execute. " +
|
||||
"This won't stop the program from working, but functionality will be severely limited. " +
|
||||
"Please make sure the Tuner Factory Class is properly configured in order to get the best experience.\n\n" +
|
||||
"The following went wrong:\n" + e.Message);
|
||||
handles.StreamReader = new NullTunerFactory().CreateStreamReader();
|
||||
}
|
||||
|
||||
|
||||
|
||||
foreach (TunerMetadata foundTuner in foundTuners)
|
||||
{
|
||||
handles.StreamReader.StopDVB();
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info,String.Format("Starting tuner {0}", foundTuner.Name));
|
||||
bool startDvbEx = handles.StreamReader.StartDvbEx(foundTuner.Index);
|
||||
if (!startDvbEx)
|
||||
{
|
||||
localLogger.Log(PluginLogLevel.Error,String.Format("Failed to start {0}", foundTuner.Name));
|
||||
Thread.Sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info, String.Format("Checking capabilities of {0}", foundTuner.Name));
|
||||
foundTuner.Caps = handles.StreamReader.GetCaps();
|
||||
|
||||
byte[] macBuffer = new byte[6];
|
||||
localLogger.Log(PluginLogLevel.Info, String.Format("Reading MAC Address of {0}", foundTuner.Name));
|
||||
bool mac = handles.StreamReader.GetMAC(macBuffer);
|
||||
if (!mac)
|
||||
{
|
||||
localLogger.Log(PluginLogLevel.Error,String.Format("Failed to read MAC Address of {0}", foundTuner.Name));
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
foundTuner.MacAddress = new PhysicalAddress(macBuffer);
|
||||
}
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info, String.Format("Stopping {0}", foundTuner.Name));
|
||||
bool stopDvb = handles.StreamReader.StopDVB();
|
||||
if (!stopDvb)
|
||||
{
|
||||
localLogger.Log(PluginLogLevel.Error, String.Format("Failed to stop {0}", foundTuner.Name));
|
||||
Thread.Sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info, String.Format("Querying storage for configuration of {0}...", foundTuner.Name));
|
||||
if (handles.DataStorage.UiTunerTestFor(foundTuner))
|
||||
{
|
||||
handles.DataStorage.UiTunerGetConfiguration(foundTuner);
|
||||
}
|
||||
|
||||
if (handles.Tuners == null)
|
||||
handles.Tuners = new List<TunerMetadata>();
|
||||
handles.Tuners.Add(foundTuner);
|
||||
}
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info,"Checking the engine Version...");
|
||||
string engineProductName = handles.StreamReader.GetEngineName();
|
||||
Version engineVersion = handles.StreamReader.GetEngineVersion();
|
||||
|
||||
if (!engineProductName.Equals("NullStreamReader"))
|
||||
{
|
||||
bool finalCaps = handles.StreamReader.CheckForDVB();
|
||||
if (!finalCaps)
|
||||
{
|
||||
errors.Enqueue("Somehow CheckForDVB failed after a second call.");
|
||||
}
|
||||
}
|
||||
|
||||
localLogger.Log(PluginLogLevel.Info,"Qualifying the engine...");
|
||||
QualificationToolResultEnum qualification = QualificationTool.QualifyTunerFactory(engineProductName, engineVersion);
|
||||
switch (qualification)
|
||||
{
|
||||
case QualificationToolResultEnum.Good:
|
||||
break;
|
||||
case QualificationToolResultEnum.Bad:
|
||||
errors.Enqueue(String.Format("You are using {0}, Version {1}\nThis version is known to cause issues with skyscraper. You can continue using it, but if it causes issues, you're on your own.", engineProductName, engineVersion));
|
||||
break;
|
||||
case QualificationToolResultEnum.Unknown:
|
||||
errors.Enqueue(String.Format("You are using {0}, Version {1}\nThis version has not been tested with skyscraper, and might cause some issues. Consider submitting a copy of this version to the author.", engineProductName, engineVersion));
|
||||
break;
|
||||
case QualificationToolResultEnum.PossibleIssues:
|
||||
errors.Enqueue(String.Format("You are using {0}, Version {1}\nThis version will work, but might have minor issues in some edge cases.", engineProductName, engineVersion));
|
||||
break;
|
||||
default:
|
||||
errors.Enqueue(String.Format("You are using {0}, Version {1}\nThe Qualification said \"{2}\", but this status is not implemented.\n Possibly your skyscraper Version and your testdrid Version mismatch?", engineProductName, engineVersion, qualification.ToString()));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
SkyscraperGame game = new SkyscraperGame(handles,errors);
|
||||
game.Run();
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,23 @@ namespace skyscraper8.UI.MonoGame
|
||||
{
|
||||
internal class SkyscraperGame : BaseGame
|
||||
{
|
||||
public SkyscraperGame(SkyscraperHandleCollection handles, Queue<string> errors)
|
||||
{
|
||||
this.Handles = handles;
|
||||
|
||||
while (errors.Count > 0)
|
||||
{
|
||||
string dequeue = errors.Dequeue();
|
||||
EnqueueUpdateJob(() =>
|
||||
{
|
||||
_imGuiRenderables.Add(new MessageWindow(dequeue));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public SkyscraperHandleCollection Handles { get; set; }
|
||||
|
||||
|
||||
private ScreenhackManager screenhackManager;
|
||||
protected override void LoadContent()
|
||||
{
|
||||
@ -46,11 +63,12 @@ namespace skyscraper8.UI.MonoGame
|
||||
base.Update(gameTime);
|
||||
}
|
||||
|
||||
private const int MENU_TEST_ABOUT = 1;
|
||||
private bool MenuTest(int opcode)
|
||||
{
|
||||
switch (opcode)
|
||||
{
|
||||
case 1:
|
||||
case MENU_TEST_ABOUT:
|
||||
if (aboutWindow != null)
|
||||
{
|
||||
if (!aboutWindow.WasClosed())
|
||||
@ -65,6 +83,9 @@ namespace skyscraper8.UI.MonoGame
|
||||
}
|
||||
|
||||
private AboutWindow aboutWindow;
|
||||
|
||||
|
||||
|
||||
protected override void ImGuiLayout()
|
||||
{
|
||||
|
||||
@ -89,7 +110,7 @@ namespace skyscraper8.UI.MonoGame
|
||||
|
||||
if (ImGui.BeginMenu("Help"))
|
||||
{
|
||||
if (ImGui.MenuItem("About",MenuTest(1)))
|
||||
if (ImGui.MenuItem("About",MenuTest(MENU_TEST_ABOUT)))
|
||||
{
|
||||
EnqueueUpdateJob(() =>
|
||||
{
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using skyscraper5.Skyscraper;
|
||||
using skyscraper5.Skyscraper.Equipment;
|
||||
using skyscraper5.Skyscraper.Gps;
|
||||
using skyscraper5.Skyscraper.IO;
|
||||
using skyscraper5.Skyscraper.IO.TunerInterface;
|
||||
using skyscraper8.Skyscraper.Scraper.Storage;
|
||||
|
||||
namespace skyscraper8.UI.MonoGame
|
||||
{
|
||||
internal class SkyscraperHandleCollection
|
||||
{
|
||||
public ReadOnlyCollection<KeyValuePair<int, DataStorageFactory>> AllDataStorages { get; set; }
|
||||
public ReadOnlyCollection<KeyValuePair<int, ObjectStorageFactory>> AllObjectStorages { get; set; }
|
||||
public Ini Ini { get; set; }
|
||||
public DataStorage DataStorage { get; set; }
|
||||
public ObjectStorage ObjectStorage { get; set; }
|
||||
public IGpsReceiver Gps { get; set; }
|
||||
public List<SatellitePosition> SatellitePositions { get; set; }
|
||||
public List<LnbType> LnbTypes { get; set; }
|
||||
public List<DishType> DishTypes { get; set; }
|
||||
public ReadOnlyCollection<KeyValuePair<TunerFactoryIdAttribute, ITunerFactory>> AllTunerFactories { get; set; }
|
||||
public IStreamReader StreamReader { get; set; }
|
||||
public List<TunerMetadata> Tuners { get; set; }
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user