[Save/Sync] In-Progress rewrite of ConfigService and ModConfigService
This commit is contained in:
-31
@@ -1,31 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Xml.Linq;
|
|
||||||
using Barotrauma.LuaCs.Data;
|
|
||||||
using FluentResults;
|
|
||||||
|
|
||||||
namespace Barotrauma.LuaCs.Services.Processing;
|
|
||||||
|
|
||||||
public partial class ModConfigService
|
|
||||||
{
|
|
||||||
private partial async Task<Result<IModConfigInfo>> GetModConfigInfoAsync(ContentPackage package, XElement root)
|
|
||||||
{
|
|
||||||
var asm = root.GetChildElements("Assembly").ToImmutableArray();
|
|
||||||
var cfg = root.GetChildElements("Config").ToImmutableArray();
|
|
||||||
var lua = root.GetChildElements("Lua").ToImmutableArray();
|
|
||||||
|
|
||||||
return FluentResults.Result.Ok<IModConfigInfo>(new ModConfigInfo()
|
|
||||||
{
|
|
||||||
Package = package,
|
|
||||||
PackageName = package.Name,
|
|
||||||
Assemblies = asm.Any() ? GetAssemblies(package, asm) : ImmutableArray<IAssemblyResourceInfo>.Empty,
|
|
||||||
Configs = cfg.Any() ? GetConfigs(package, cfg) : ImmutableArray<IConfigResourceInfo>.Empty,
|
|
||||||
ConfigProfiles = cfg.Any() ? GetConfigProfiles(package, cfg) : ImmutableArray<IConfigProfileResourceInfo>.Empty,
|
|
||||||
LuaScripts = lua.Any() ? GetLuaScripts(package, lua) : ImmutableArray<ILuaScriptResourceInfo>.Empty
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -54,10 +54,7 @@ namespace Barotrauma
|
|||||||
// TODO: INetworkingService
|
// TODO: INetworkingService
|
||||||
// TODO: [Resource Converter/Parser Services]
|
// TODO: [Resource Converter/Parser Services]
|
||||||
|
|
||||||
// Loaders and Processors (yes the naming is reversed, oops).
|
_servicesProvider.RegisterServiceType<IModConfigService, ModConfigService>(ServiceLifetime.Transient);
|
||||||
_servicesProvider.RegisterServiceType<IParserService<ContentPackage, IModConfigInfo>, ModConfigService>(ServiceLifetime.Transient);
|
|
||||||
_servicesProvider.RegisterServiceType<IParserServiceAsync<ContentPackage, IModConfigInfo>, ModConfigService>(ServiceLifetime.Transient);
|
|
||||||
_servicesProvider.RegisterServiceType<IConfigIOService, ConfigIOService>(ServiceLifetime.Transient);
|
|
||||||
|
|
||||||
// service config data
|
// service config data
|
||||||
_servicesProvider.RegisterServiceType<IStorageServiceConfig, StorageServiceConfig>(ServiceLifetime.Singleton);
|
_servicesProvider.RegisterServiceType<IStorageServiceConfig, StorageServiceConfig>(ServiceLifetime.Singleton);
|
||||||
|
|||||||
-278
@@ -1,278 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Xml.Linq;
|
|
||||||
using Barotrauma.LuaCs.Data;
|
|
||||||
using FarseerPhysics.Common;
|
|
||||||
using FluentResults;
|
|
||||||
using OneOf;
|
|
||||||
|
|
||||||
namespace Barotrauma.LuaCs.Services.Processing;
|
|
||||||
|
|
||||||
public class ConfigIOService : IConfigIOService
|
|
||||||
{
|
|
||||||
private readonly IStorageService _storageService;
|
|
||||||
private readonly IConfigServiceConfig _configServiceConfig;
|
|
||||||
|
|
||||||
public ConfigIOService(IStorageService storageService, IConfigServiceConfig configServiceConfig)
|
|
||||||
{
|
|
||||||
this._storageService = storageService;
|
|
||||||
storageService.UseCaching = true;
|
|
||||||
_configServiceConfig = configServiceConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
// stateless service
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// stateless service
|
|
||||||
public bool IsDisposed => false;
|
|
||||||
public FluentResults.Result Reset()
|
|
||||||
{
|
|
||||||
_storageService.PurgeCache();
|
|
||||||
return FluentResults.Result.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Result<IReadOnlyList<IConfigInfo>>> TryParseResourceAsync(IConfigResourceInfo src)
|
|
||||||
{
|
|
||||||
if (src?.OwnerPackage is null || src.FilePaths.IsDefaultOrEmpty)
|
|
||||||
return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: Config resource and/or components were null.");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var infos = await _storageService.LoadPackageXmlFilesAsync(src.OwnerPackage, [..src.FilePaths.Select(fp => fp.FullPath)]);
|
|
||||||
if (infos.IsDefaultOrEmpty)
|
|
||||||
return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: No resources found.");
|
|
||||||
|
|
||||||
var errList = new List<IError>();
|
|
||||||
|
|
||||||
var resList = infos.Select(info =>
|
|
||||||
{
|
|
||||||
if (info.Item2.Errors.Any())
|
|
||||||
errList.AddRange(info.Item2.Errors);
|
|
||||||
if (info.Item2.IsFailed || info.Item2.Value is not { } configXDoc)
|
|
||||||
{
|
|
||||||
errList.Add(new Error($"Unable to parse file: {info.Item1}"));
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (info.Item1, configXDoc);
|
|
||||||
})
|
|
||||||
.Where(doc => !doc.Item1.IsNullOrWhiteSpace() && doc.configXDoc != null)
|
|
||||||
.SelectMany(doc => doc.configXDoc.Root.GetChildElements("Configuration"))
|
|
||||||
.SelectMany(cfgContainer => cfgContainer.GetChildElements("Configs"))
|
|
||||||
.SelectMany(cfgContainer => cfgContainer.GetChildElements("Config"))
|
|
||||||
.Select(async cfgElement =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
OneOf.OneOf<string, XElement> defaultValue = cfgElement.GetChildElement("Value");
|
|
||||||
if (defaultValue.AsT1 is null)
|
|
||||||
defaultValue = cfgElement.GetAttributeString("Value", string.Empty);
|
|
||||||
|
|
||||||
var internalName = cfgElement.GetAttributeString("Name", string.Empty);
|
|
||||||
if (internalName.IsNullOrWhiteSpace())
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new ConfigInfo()
|
|
||||||
{
|
|
||||||
DataType = Type.GetType(cfgElement.GetAttributeString("Type", "string")),
|
|
||||||
OwnerPackage = src.OwnerPackage,
|
|
||||||
DefaultValue = defaultValue,
|
|
||||||
Value = await LoadConfigDataFromLocal(src.OwnerPackage, internalName) is { IsSuccess: true } res
|
|
||||||
? res.Value : defaultValue,
|
|
||||||
EditableStates = cfgElement.GetAttributeBool("ReadOnly", false)
|
|
||||||
? RunState.Unloaded // read-only
|
|
||||||
: RunState.Running, // editable at runtime
|
|
||||||
InternalName = internalName,
|
|
||||||
NetSync = Enum.Parse<NetSync>(
|
|
||||||
cfgElement.GetAttributeString("NetSync", nameof(NetSync.None))),
|
|
||||||
#if CLIENT
|
|
||||||
DisplayName = cfgElement.GetAttributeString("DisplayName", null),
|
|
||||||
Description = cfgElement.GetAttributeString("Description", null),
|
|
||||||
DisplayCategory = cfgElement.GetAttributeString("Category", null),
|
|
||||||
ShowInMenus = cfgElement.GetAttributeBool("ShowInMenus", true),
|
|
||||||
Tooltip = cfgElement.GetAttributeString("Tooltip", null),
|
|
||||||
ImageIconPath = cfgElement.GetAttributeString("Image", null)
|
|
||||||
#endif
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
errList.Add(new Error($"Failed to parse config var for package {src.OwnerPackage}"));
|
|
||||||
errList.Add(new ExceptionalError(e));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.Where(task => task is not null)
|
|
||||||
.ToImmutableArray();
|
|
||||||
|
|
||||||
var result = (await Task.WhenAll(resList)).ToImmutableArray();
|
|
||||||
|
|
||||||
var ret = FluentResults.Result.Ok((IReadOnlyList<IConfigInfo>)result);
|
|
||||||
if (errList.Any())
|
|
||||||
ret.Errors.AddRange(errList);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
catch(Exception e)
|
|
||||||
{
|
|
||||||
return FluentResults.Result.Fail($"Failed to parse config resource for package {src.OwnerPackage}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ImmutableArray<Result<IReadOnlyList<IConfigInfo>>>> TryParseResourcesAsync(IEnumerable<IConfigResourceInfo> sources)
|
|
||||||
{
|
|
||||||
var results = new ConcurrentQueue<Result<IReadOnlyList<IConfigInfo>>>();
|
|
||||||
|
|
||||||
var src = sources.ToImmutableArray();
|
|
||||||
if (!src.Any())
|
|
||||||
return ImmutableArray<Result<IReadOnlyList<IConfigInfo>>>.Empty;
|
|
||||||
|
|
||||||
await src.ParallelForEachAsync(async cfg =>
|
|
||||||
{
|
|
||||||
var res = await TryParseResourceAsync(cfg);
|
|
||||||
results.Enqueue(res);
|
|
||||||
}, 2); // we only need 2 parallels to buffer against disk loading.
|
|
||||||
|
|
||||||
return results.ToImmutableArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Result<IReadOnlyList<IConfigProfileInfo>>> TryParseResourceAsync(IConfigProfileResourceInfo src)
|
|
||||||
{
|
|
||||||
if (src?.OwnerPackage is null || src.FilePaths.IsDefaultOrEmpty)
|
|
||||||
return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: Profile resource and/or components were null.");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var infos = await _storageService.LoadPackageXmlFilesAsync(src.OwnerPackage, src.FilePaths);
|
|
||||||
if (infos.IsDefaultOrEmpty)
|
|
||||||
return FluentResults.Result.Fail($"{nameof(TryParseResourceAsync)}: No resources found.");
|
|
||||||
|
|
||||||
var errList = new List<IError>();
|
|
||||||
|
|
||||||
var resList = infos.Select(info =>
|
|
||||||
{
|
|
||||||
if (info.Item2.Errors.Any())
|
|
||||||
errList.AddRange(info.Item2.Errors);
|
|
||||||
if (info.Item2.IsFailed || info.Item2.Value is not { } configXDoc)
|
|
||||||
{
|
|
||||||
errList.Add(new Error($"Unable to parse file: {info.Item1}"));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return configXDoc;
|
|
||||||
})
|
|
||||||
.Where(doc => doc is not null)
|
|
||||||
.SelectMany(doc => doc.Root.GetChildElements("Configuration"))
|
|
||||||
.SelectMany(cfgContainer => cfgContainer.GetChildElements("Profiles"))
|
|
||||||
.SelectMany(cfgContainer => cfgContainer.GetChildElements("Profile"))
|
|
||||||
.Select(cfgElement =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return new ConfigProfileInfo()
|
|
||||||
{
|
|
||||||
OwnerPackage = src.OwnerPackage,
|
|
||||||
InternalName = cfgElement.GetAttributeString("Name", null),
|
|
||||||
ProfileValues = cfgElement.GetChildElements("ConfigValue")
|
|
||||||
.Select<XElement, (string ConfigName, OneOf.OneOf<string, XElement> Value)>(element =>
|
|
||||||
{
|
|
||||||
if (element.GetAttributeString("Name", null) is not { } name)
|
|
||||||
return default;
|
|
||||||
if (element.GetAttributeString("Value", null) is { } value)
|
|
||||||
return (name, value);
|
|
||||||
if (element.GetChildElement("Value") is { } xValue)
|
|
||||||
return (name, xValue);
|
|
||||||
return default;
|
|
||||||
})
|
|
||||||
.Where(val => val.ConfigName is not null && val.Value.Match<bool>(
|
|
||||||
s => !s.IsNullOrWhiteSpace(),
|
|
||||||
element => element is not null))
|
|
||||||
.ToList()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
errList.Add(new Error($"Failed to parse profile var for package {src.OwnerPackage}"));
|
|
||||||
errList.Add(new ExceptionalError(e));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.Where(cfgInfo => cfgInfo != null && !cfgInfo.InternalName.IsNullOrWhiteSpace())
|
|
||||||
.ToImmutableArray();
|
|
||||||
|
|
||||||
var ret = FluentResults.Result.Ok((IReadOnlyList<IConfigProfileInfo>)resList);
|
|
||||||
if (errList.Any())
|
|
||||||
ret.Errors.AddRange(errList);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
catch(Exception e)
|
|
||||||
{
|
|
||||||
return FluentResults.Result.Fail($"Failed to parse profile resource for package {src.OwnerPackage}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ImmutableArray<Result<IReadOnlyList<IConfigProfileInfo>>>> TryParseResourcesAsync(IEnumerable<IConfigProfileResourceInfo> sources)
|
|
||||||
{
|
|
||||||
var results = new ConcurrentQueue<Result<IReadOnlyList<IConfigProfileInfo>>>();
|
|
||||||
|
|
||||||
var src = sources.ToImmutableArray();
|
|
||||||
if (!src.Any())
|
|
||||||
return ImmutableArray<Result<IReadOnlyList<IConfigProfileInfo>>>.Empty;
|
|
||||||
|
|
||||||
await src.ParallelForEachAsync(async cfg =>
|
|
||||||
{
|
|
||||||
var res = await TryParseResourceAsync(cfg);
|
|
||||||
results.Enqueue(res);
|
|
||||||
}, 2); // we only need 2 parallels to buffer against disk loading.
|
|
||||||
|
|
||||||
return results.ToImmutableArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly Regex RemoveInvalidChars = new Regex($"[{Regex.Escape(new string(System.IO.Path.GetInvalidFileNameChars()))}]",
|
|
||||||
RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.CultureInvariant);
|
|
||||||
|
|
||||||
private string SanitizedFileName(string fileName, string replacement = "_")
|
|
||||||
{
|
|
||||||
return RemoveInvalidChars.Replace(fileName, replacement);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<FluentResults.Result> SaveConfigDataLocal(ContentPackage package, string configName, XElement serializedValue)
|
|
||||||
{
|
|
||||||
if (package is null || package.Name.IsNullOrWhiteSpace() || configName.IsNullOrWhiteSpace() || serializedValue is null)
|
|
||||||
return FluentResults.Result.Fail($"{nameof(SaveConfigDataLocal)}: Argument(s) were null");
|
|
||||||
|
|
||||||
var res = await LoadPackageConfigDocInternal(package);
|
|
||||||
|
|
||||||
throw new NotImplementedException(); //TODO: Complete once the locally saved data structure is finalized.
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Result<OneOf<string, XElement>>> LoadConfigDataFromLocal(ContentPackage package, string configName)
|
|
||||||
{
|
|
||||||
if (package is null || package.Name.IsNullOrWhiteSpace() || configName.IsNullOrWhiteSpace())
|
|
||||||
return FluentResults.Result.Fail($"{nameof(LoadConfigDataFromLocal)}: Argument(s) were null");
|
|
||||||
|
|
||||||
var filePath = _configServiceConfig.LocalConfigPathPartial.Replace(
|
|
||||||
_configServiceConfig.FileNamePattern,
|
|
||||||
$"{SanitizedFileName(package.Name)}.xml");
|
|
||||||
|
|
||||||
var res = await _storageService.LoadLocalXmlAsync(package, filePath);
|
|
||||||
|
|
||||||
throw new NotImplementedException(); //TODO: Complete once the locally saved data structure is finalized.
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<FluentResults.Result<XDocument>> LoadPackageConfigDocInternal(ContentPackage package)
|
|
||||||
{
|
|
||||||
var filePath = _configServiceConfig.LocalConfigPathPartial.Replace(
|
|
||||||
_configServiceConfig.FileNamePattern,
|
|
||||||
$"{SanitizedFileName(package.Name)}.xml");
|
|
||||||
|
|
||||||
throw new NotImplementedException(); //TODO: Complete once the locally saved data structure is finalized.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Xml.Linq;
|
|
||||||
using Barotrauma.LuaCs.Data;
|
|
||||||
using Barotrauma.LuaCs.Services.Processing;
|
|
||||||
|
|
||||||
namespace Barotrauma.LuaCs.Services.Processing;
|
|
||||||
|
|
||||||
public interface IConfigIOService : IReusableService,
|
|
||||||
IParserServiceAsync<IConfigResourceInfo, IReadOnlyList<IConfigInfo>>,
|
|
||||||
IParserServiceAsync<IConfigProfileResourceInfo, IReadOnlyList<IConfigProfileInfo>>
|
|
||||||
{
|
|
||||||
Task<FluentResults.Result> SaveConfigDataLocal(ContentPackage package, string configName, XElement serializedValue);
|
|
||||||
Task<FluentResults.Result<OneOf.OneOf<string, XElement>>> LoadConfigDataFromLocal(ContentPackage package, string configName);
|
|
||||||
}
|
|
||||||
+15
-11
@@ -9,6 +9,8 @@ using System.Threading.Tasks;
|
|||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
using Barotrauma.LuaCs.Data;
|
using Barotrauma.LuaCs.Data;
|
||||||
using FluentResults;
|
using FluentResults;
|
||||||
|
using Microsoft.Toolkit.Diagnostics;
|
||||||
|
using MoonSharp.VsCodeDebugger.SDK;
|
||||||
|
|
||||||
namespace Barotrauma.LuaCs.Services.Processing;
|
namespace Barotrauma.LuaCs.Services.Processing;
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ public sealed class ModConfigService : IModConfigService
|
|||||||
_configProfileParserService = configProfileParserService;
|
_configProfileParserService = configProfileParserService;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Disposal
|
#region Dispose
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
@@ -44,15 +46,14 @@ public sealed class ModConfigService : IModConfigService
|
|||||||
public bool IsDisposed
|
public bool IsDisposed
|
||||||
{
|
{
|
||||||
get => ModUtils.Threading.GetBool(ref _isDisposed);
|
get => ModUtils.Threading.GetBool(ref _isDisposed);
|
||||||
protected set => ModUtils.Threading.SetBool(ref _isDisposed, value);
|
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public async Task<Result<IModConfigInfo>> CreateConfigAsync(ContentPackage src)
|
public async Task<Result<IModConfigInfo>> CreateConfigAsync(ContentPackage src)
|
||||||
{
|
{
|
||||||
if (src is null)
|
Guard.IsNotNull(src, nameof(src));
|
||||||
ArgumentNullException.ThrowIfNull($"{nameof(CreateConfigAsync)}: Source is null.");
|
|
||||||
|
|
||||||
if (await TryGetModConfigXmlAsync(src) is { IsSuccess: true, Value: { } config })
|
if (await TryGetModConfigXmlAsync(src) is { IsSuccess: true, Value: { } config })
|
||||||
{
|
{
|
||||||
@@ -64,20 +65,23 @@ public sealed class ModConfigService : IModConfigService
|
|||||||
|
|
||||||
public async Task<ImmutableArray<(ContentPackage Source, Result<IModConfigInfo> Config)>> CreateConfigsAsync(ImmutableArray<ContentPackage> src)
|
public async Task<ImmutableArray<(ContentPackage Source, Result<IModConfigInfo> Config)>> CreateConfigsAsync(ImmutableArray<ContentPackage> src)
|
||||||
{
|
{
|
||||||
var builder = ImmutableArray.CreateBuilder<(ContentPackage Source, Result<IModConfigInfo> Config)>();
|
var builder = new ConcurrentQueue<(ContentPackage Source, Result<IModConfigInfo> Config)>();
|
||||||
|
|
||||||
foreach (var package in src)
|
await src.ParallelForEachAsync(async package =>
|
||||||
{
|
{
|
||||||
builder.Add((package, await CreateConfigAsync(package)));
|
var res = await CreateConfigAsync(package);
|
||||||
}
|
builder.Enqueue((package, res));
|
||||||
|
});
|
||||||
return builder.ToImmutable();
|
|
||||||
|
return builder.OrderBy(pkg => src.IndexOf(pkg.Source)).ToImmutableArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
//--- Helpers
|
//--- Helpers
|
||||||
private async Task<Result<XElement>> TryGetModConfigXmlAsync(ContentPackage src)
|
private async Task<Result<XElement>> TryGetModConfigXmlAsync(ContentPackage src)
|
||||||
{
|
{
|
||||||
|
return await _storageService.LoadPackageXmlAsync(src, "ModConfig.xml") is { IsSuccess: true, Value: { Root: {} config} }
|
||||||
|
? FluentResults.Result.Ok(config)
|
||||||
|
: FluentResults.Result.Fail<XElement>("ModConfig.xml not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result<IModConfigInfo>> CreateFromConfigXmlAsync(XElement src)
|
private async Task<Result<IModConfigInfo>> CreateFromConfigXmlAsync(XElement src)
|
||||||
|
|||||||
Reference in New Issue
Block a user