Init
This commit is contained in:
48
Scripts/Camera/CameraService.cs
Normal file
48
Scripts/Camera/CameraService.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Godot;
|
||||
|
||||
namespace BITKit;
|
||||
/// <summary>
|
||||
/// 摄像头服务,该服务需要加载到Camera3D节点中
|
||||
/// </summary>
|
||||
public partial class CameraService:Camera3D
|
||||
{
|
||||
/// <summary>
|
||||
/// 场景中所有的摄像头
|
||||
/// </summary>
|
||||
private static readonly List<IVirtualCamera> _cameras=new();
|
||||
/// <summary>
|
||||
/// 当前已激活的摄像头
|
||||
/// </summary>
|
||||
public static IVirtualCamera ActiveCamera { get; private set; }
|
||||
/// <summary>
|
||||
/// 注册摄像头
|
||||
/// </summary>
|
||||
/// <param name="camera">摄像头</param>
|
||||
/// <returns></returns>
|
||||
public static bool Register(IVirtualCamera camera) => _cameras.TryAdd(camera);
|
||||
/// <summary>
|
||||
/// 注销摄像头
|
||||
/// </summary>
|
||||
/// <param name="camera">摄像头</param>
|
||||
/// <returns></returns>
|
||||
public static bool UnRegister(IVirtualCamera camera) => _cameras.TryRemove(camera);
|
||||
/// <summary>
|
||||
/// 处理摄像头的位置
|
||||
/// </summary>
|
||||
/// <param name="delta"></param>
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
//获取所有已启用的相机并加载一个虚拟相机的数据
|
||||
foreach (var x in _cameras.Where(x => x.IsEnabled))
|
||||
{
|
||||
//应用相机坐标
|
||||
Position = x.Position;
|
||||
//应用相机角度
|
||||
Rotation = x.Rotation.GetEuler();
|
||||
//已加载相机位置,退出循环
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
58
Scripts/Camera/VirtualCamera.cs
Normal file
58
Scripts/Camera/VirtualCamera.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Godot;
|
||||
|
||||
namespace BITKit;
|
||||
|
||||
/// <summary>
|
||||
/// 虚拟相机接口定义
|
||||
/// </summary>
|
||||
public interface IVirtualCamera:IActivable
|
||||
{
|
||||
/// <summary>
|
||||
/// 相机的FOV
|
||||
/// </summary>
|
||||
int FOV { get; }
|
||||
/// <summary>
|
||||
/// 相机是否已启用
|
||||
/// </summary>
|
||||
bool IsEnabled { get; }
|
||||
/// <summary>
|
||||
/// 相机坐标
|
||||
/// </summary>
|
||||
Vector3 Position { get; }
|
||||
/// <summary>
|
||||
/// 相机旋转
|
||||
/// </summary>
|
||||
Quaternion Rotation { get; }
|
||||
}
|
||||
/// <summary>
|
||||
/// 基于Node3D的包括基础功能的虚拟相机
|
||||
/// </summary>
|
||||
public partial class VirtualCamera : Node3D, IVirtualCamera
|
||||
{
|
||||
[Export] private int fov;
|
||||
[Export] private bool isEnabled;
|
||||
public int FOV => fov;
|
||||
|
||||
public bool IsEnabled => isEnabled;
|
||||
|
||||
Vector3 IVirtualCamera.Position => GlobalPosition;
|
||||
Quaternion IVirtualCamera.Rotation => Quaternion.FromEuler(GlobalRotation);
|
||||
|
||||
public void SetActive(bool active)
|
||||
{
|
||||
isEnabled = active;
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
CameraService.Register(this);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
CameraService.UnRegister(this);
|
||||
}
|
||||
}
|
||||
}
|
56
Scripts/Core/BITAppForGodot.cs
Normal file
56
Scripts/Core/BITAppForGodot.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using Godot;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
using HttpClient = System.Net.Http.HttpClient;
|
||||
|
||||
namespace BITKit;
|
||||
/// <summary>
|
||||
/// 为Godot提供的BITApp加载服务
|
||||
/// </summary>
|
||||
public partial class BITAppForGodot : Node
|
||||
{
|
||||
/// <summary>
|
||||
/// 依赖服务集合
|
||||
/// </summary>
|
||||
public static ServiceCollection ServiceCollection { get; private set; } = new();
|
||||
/// <summary>
|
||||
/// 依赖服务提供接口
|
||||
/// </summary>
|
||||
public static ServiceProvider ServiceProvider { get; private set; }
|
||||
/// <summary>
|
||||
/// 在构造函数中注册Logger
|
||||
/// </summary>
|
||||
public BITAppForGodot()
|
||||
{
|
||||
BIT4Log.OnLog += GD.Print;
|
||||
BIT4Log.OnWarning += GD.PushWarning;
|
||||
BIT4Log.OnNextLine += () => GD.Print();
|
||||
BIT4Log.OnException += x=>GD.PrintErr(x.ToString());
|
||||
|
||||
//启动BITApp
|
||||
BITApp.Start();
|
||||
BIT4Log.Log<BITAppForGodot>("已创建BITApp");
|
||||
}
|
||||
|
||||
public override async void _Ready()
|
||||
{
|
||||
BIT4Log.Log<BITAppForGodot>("正在创建BITWebApp");
|
||||
|
||||
//添加测试用HttpClient
|
||||
ServiceCollection.AddSingleton<HttpClient>();
|
||||
|
||||
//构造依赖服务提供接口
|
||||
ServiceProvider = ServiceCollection.BuildServiceProvider();
|
||||
|
||||
}
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
#pragma warning disable CS4014
|
||||
//停止BITApp
|
||||
BITApp.Stop();
|
||||
#pragma warning restore CS4014
|
||||
BIT4Log.Log<BITAppForGodot>("已安全退出App");
|
||||
}
|
||||
}
|
56
Scripts/Core/IntervalTimer.cs
Normal file
56
Scripts/Core/IntervalTimer.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Godot;
|
||||
|
||||
namespace BITKit;
|
||||
|
||||
/// <summary>
|
||||
/// 固定间隔工具,用于控制执行速率
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class IntervalTimer
|
||||
{
|
||||
public IntervalTimer()
|
||||
{
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// 在构造函数中声明间隔时间
|
||||
/// </summary>
|
||||
/// <param name="interval"></param>
|
||||
public IntervalTimer(ulong interval)
|
||||
{
|
||||
this.interval = interval;
|
||||
}
|
||||
/// <summary>
|
||||
/// 间隔时间
|
||||
/// </summary>
|
||||
private readonly ulong interval;
|
||||
/// <summary>
|
||||
/// 可以执行的事件
|
||||
/// </summary>
|
||||
private ulong allowTime;
|
||||
/// <summary>
|
||||
/// 是否可以执行(执行重置间隔)
|
||||
/// </summary>
|
||||
public bool Allow
|
||||
{
|
||||
get
|
||||
{
|
||||
if (allowTime >= Time.GetTicksMsec()) return false;
|
||||
Release();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 是否可以执行(不会执行重置间隔)
|
||||
/// </summary>
|
||||
public bool AllowWithoutRelease => allowTime >= Time.GetTicksMsec();
|
||||
/// <summary>
|
||||
/// 重置执行间隔
|
||||
/// </summary>
|
||||
/// <param name="immediately">是否可以立即执行</param>
|
||||
public void Release(bool immediately=false)
|
||||
{
|
||||
var currentTime = Time.GetTicksMsec();
|
||||
allowTime = immediately ? currentTime : currentTime + interval*1000;
|
||||
}
|
||||
}
|
77
Scripts/ECS/Entity.cs
Normal file
77
Scripts/ECS/Entity.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BITKit.Core.Entites;
|
||||
namespace BITKit;
|
||||
/// <summary>
|
||||
/// 用于Godot的ECS.Entity实现
|
||||
/// </summary>
|
||||
public partial class Entity : Node,IEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 类型组件的缓存
|
||||
/// </summary>
|
||||
private readonly Dictionary<Type,IEntityComponent> TypeComponents=new ();
|
||||
/// <summary>
|
||||
/// IEntityService的缓存
|
||||
/// </summary>
|
||||
private IEntitiesService _entitiesService;
|
||||
/// <summary>
|
||||
/// 所有EntityComponent
|
||||
/// </summary>
|
||||
private IEntityComponent[] _components;
|
||||
IEntityComponent[] IEntity.Components => _components;
|
||||
/// <summary>
|
||||
/// IEntity.Id实现
|
||||
/// </summary>
|
||||
public ulong Id { get; private set; }
|
||||
/// <summary>
|
||||
/// 加载所有EntityComponent的内部实现
|
||||
/// </summary>
|
||||
public override void _Ready()
|
||||
{
|
||||
List<IEntityComponent> entityComponents = new();
|
||||
Id = GetInstanceId();
|
||||
_entitiesService = DI.Get<IEntitiesService>();
|
||||
foreach (var x in MathNode.GetAllNode(this))
|
||||
{
|
||||
GetInstanceId();
|
||||
if (x is not IEntityComponent component) continue;
|
||||
component.Entity = this;
|
||||
TypeComponents.TryAdd(x.GetType(),component);
|
||||
BIT4Log.Log<Entity>($"已加载组件:{x.Name}");
|
||||
component.OnAwake();
|
||||
entityComponents.Add(component);
|
||||
}
|
||||
foreach (var component in TypeComponents.Values)
|
||||
{
|
||||
component.OnStart();
|
||||
}
|
||||
_entitiesService.Register(this);
|
||||
this._components = entityComponents.ToArray();
|
||||
}
|
||||
public bool TryGetComponent<T>(out T component) where T : IEntityComponent
|
||||
{
|
||||
if (TypeComponents.TryGetValue(typeof(T), out var iComponent) && iComponent is T _component)
|
||||
{
|
||||
component = _component;
|
||||
return true;
|
||||
}
|
||||
component = default;
|
||||
return false;
|
||||
}
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (disposing)
|
||||
{
|
||||
_entitiesService.UnRegister(this);
|
||||
}
|
||||
}
|
||||
|
||||
public bool RegisterComponent<T>(T component) where T : IEntityComponent
|
||||
{
|
||||
return TypeComponents.TryAdd(typeof(T), component);
|
||||
}
|
||||
}
|
13
Scripts/ECS/EntityComponent.cs
Normal file
13
Scripts/ECS/EntityComponent.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using BITKit.Core.Entites;
|
||||
using Godot;
|
||||
|
||||
namespace BITKit;
|
||||
/// <summary>
|
||||
/// 基于Godot.Node3D的IEntityComponent实现
|
||||
/// </summary>
|
||||
public partial class EntityComponent : Node3D,IEntityComponent
|
||||
{
|
||||
public IEntity Entity { get; set; }
|
||||
public virtual void OnStart(){}
|
||||
public virtual void OnAwake(){}
|
||||
}
|
42
Scripts/ECS/GodotEntitiesService.cs
Normal file
42
Scripts/ECS/GodotEntitiesService.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using BITKit.Core.Entites;
|
||||
// ReSharper disable All
|
||||
|
||||
namespace BITKit;
|
||||
/// <summary>
|
||||
/// 基于Godot.Node的IEntitiesService实现
|
||||
/// </summary>
|
||||
public partial class GodotEntitiesService : Node,IEntitiesService
|
||||
{
|
||||
public GodotEntitiesService()
|
||||
{
|
||||
DI.Register<IEntitiesService>(this);
|
||||
}
|
||||
private readonly Dictionary<ulong,IEntity> _entities=new ();
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
public IEntity[] Entities => _entities.Values.ToArray();
|
||||
public bool Register(IEntity entity)
|
||||
{
|
||||
return _entities.TryAdd(entity.Id, entity);
|
||||
}
|
||||
public bool UnRegister(IEntity entity)
|
||||
{
|
||||
return _entities.TryRemove(entity.Id);
|
||||
}
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_cancellationTokenSource = new();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if(disposing)_cancellationTokenSource.Cancel();
|
||||
}
|
||||
|
||||
public CancellationToken CancellationToken => _cancellationTokenSource.Token;
|
||||
}
|
30
Scripts/Extensions/Node.cs
Normal file
30
Scripts/Extensions/Node.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Godot;
|
||||
|
||||
namespace BITKit;
|
||||
/// <summary>
|
||||
/// 为Godot.Node提供数学工具
|
||||
/// </summary>
|
||||
public static partial class MathNode
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取Node下所有的子Node节点
|
||||
/// </summary>
|
||||
/// <param name="self">Root Node</param>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<Node> GetAllNode(Node self)
|
||||
{
|
||||
List<Node> nodes = new() { self };
|
||||
For(self);
|
||||
void For(Node node)
|
||||
{
|
||||
foreach (var x in node.GetChildren())
|
||||
{
|
||||
For(x);
|
||||
nodes.Add(x);
|
||||
}
|
||||
}
|
||||
return nodes.Distinct();
|
||||
}
|
||||
}
|
12
Scripts/Factory/IdComponent.cs
Normal file
12
Scripts/Factory/IdComponent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Godot;
|
||||
|
||||
namespace BITKit;
|
||||
|
||||
/// <summary>
|
||||
/// ECS的Id组件,用于提供Id
|
||||
/// </summary>
|
||||
public partial class IdComponent:EntityComponent
|
||||
{
|
||||
[Export]
|
||||
public string Id;
|
||||
}
|
16
Scripts/Factory/ROSClientService.cs
Normal file
16
Scripts/Factory/ROSClientService.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Godot;
|
||||
using RosBridgeClient;
|
||||
using RosSharp.RosBridgeClient.Protocols;
|
||||
|
||||
namespace BITKit;
|
||||
/// <summary>
|
||||
/// 使用Godot.Node作为容器的RosClient
|
||||
/// </summary>
|
||||
public partial class ROSClientService : Node
|
||||
{
|
||||
public override void _Ready()
|
||||
{
|
||||
WebSocketNetProtocol protocol = new("ws://");
|
||||
RosSharp.RosBridgeClient.RosSocket client = new(protocol);
|
||||
}
|
||||
}
|
30
Scripts/Factory/RotationComponent.cs
Normal file
30
Scripts/Factory/RotationComponent.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Godot;
|
||||
|
||||
namespace BITKit;
|
||||
/// <summary>
|
||||
/// ECS中iFactory.Rotation的角度组件
|
||||
/// </summary>
|
||||
public partial class RotationComponent : EntityComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取角度的路径
|
||||
/// </summary>
|
||||
[Export] public string Path { get; private set; }
|
||||
/// <summary>
|
||||
/// 角度的绝对权重,例如90*0,0,1 = 0,0,90
|
||||
/// </summary>
|
||||
[Export] public Vector3 Weight { get; private set; }
|
||||
/// <summary>
|
||||
/// 角度的相对偏移
|
||||
/// </summary>
|
||||
[Export] public Vector3 Offset { get; private set; }
|
||||
/// <summary>
|
||||
/// 默认角度的缓存
|
||||
/// </summary>
|
||||
public Vector3 OriginalEuler { get; private set; }
|
||||
public override void _Ready()
|
||||
{
|
||||
//保存默认角度
|
||||
OriginalEuler = Rotation;
|
||||
}
|
||||
}
|
168
Scripts/Factory/SCADAService.cs
Normal file
168
Scripts/Factory/SCADAService.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
using Godot;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using BITKit.Core.Entites;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BITKit;
|
||||
/// <summary>
|
||||
/// 单例SCADA Service,从http接口获取json后解析为指定数据
|
||||
/// </summary>
|
||||
public partial class SCADAService : Node
|
||||
{
|
||||
/// <summary>
|
||||
/// 在构造函数中注入依赖
|
||||
/// </summary>
|
||||
public SCADAService()
|
||||
{
|
||||
BITAppForGodot.ServiceCollection.AddSingleton(this);
|
||||
}
|
||||
/// <summary>
|
||||
/// 获取json的Url
|
||||
/// </summary>
|
||||
[Export]
|
||||
private string url;
|
||||
/// <summary>
|
||||
/// 是否固定控制的Entity,如果固定,只会刷新一次,如果不固定,每帧都会刷新
|
||||
/// </summary>
|
||||
[Export]
|
||||
private bool fixedEntities;
|
||||
/// <summary>
|
||||
/// 已加载的Entity
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, IEntity> _entities = new();
|
||||
/// <summary>
|
||||
/// 最大并行请求数量
|
||||
/// </summary>
|
||||
private readonly LimitTimes requestRate =new (1);
|
||||
/// <summary>
|
||||
/// 请求数据的间隔
|
||||
/// </summary>
|
||||
private readonly IntervalTimer _intervalTimer = new(1);
|
||||
/// <summary>
|
||||
/// 取消令牌,用于取消Http Get
|
||||
/// </summary>
|
||||
private CancellationToken _cancellationToken;
|
||||
/// <summary>
|
||||
/// http客户端
|
||||
/// </summary>
|
||||
private System.Net.Http.HttpClient httpClient;
|
||||
/// <summary>
|
||||
/// 获取Entity并加载依赖
|
||||
/// </summary>
|
||||
public override async void _Ready()
|
||||
{
|
||||
_cancellationToken = new CancellationToken();
|
||||
if (!fixedEntities) return;
|
||||
await UniTask.Yield();
|
||||
LoadAllEntities();
|
||||
BIT4Log.Log<SCADAService>($"已加载{_entities.Count}个设备");
|
||||
httpClient = BITAppForGodot.ServiceProvider.GetService<System.Net.Http.HttpClient>();
|
||||
}
|
||||
/// <summary>
|
||||
/// 内部方法,从EntityService加载所有Entity
|
||||
/// </summary>
|
||||
private void LoadAllEntities()
|
||||
{
|
||||
foreach (var entity in DI.Get<IEntitiesService>().Entities)
|
||||
{
|
||||
if (entity.TryGetComponent<IdComponent>(out var deviceComponent))
|
||||
{
|
||||
_entities.Add(deviceComponent.Id,deviceComponent.Entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 物理帧用于控制并发和间隔的同时请求数据
|
||||
/// </summary>
|
||||
/// <param name="delta"></param>
|
||||
public override void _PhysicsProcess(double delta)
|
||||
{
|
||||
//依赖加载
|
||||
httpClient ??= BITAppForGodot.ServiceProvider.GetService<System.Net.Http.HttpClient>();
|
||||
//请求间隔控制+请求并发控制+检查依赖是否为Null
|
||||
if (_intervalTimer.Allow && requestRate && httpClient is not null)
|
||||
{
|
||||
//发送请求
|
||||
Request();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 从http请求json
|
||||
/// </summary>
|
||||
private async void Request()
|
||||
{
|
||||
//获取json
|
||||
var json =await httpClient.GetStringAsync(url, _cancellationToken);
|
||||
try
|
||||
{
|
||||
//取消执行,如果已取消令牌
|
||||
_cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
//返回并发数量
|
||||
requestRate.Release();
|
||||
return;
|
||||
}
|
||||
//返回并发数量
|
||||
requestRate.Release();
|
||||
//处理json
|
||||
ProcessJson(json);
|
||||
}
|
||||
/// <summary>
|
||||
/// 解析json
|
||||
/// </summary>
|
||||
/// <param name="json">从SCADA获取的Json</param>
|
||||
private void ProcessJson(string json)
|
||||
{
|
||||
//首先从result中获取数组
|
||||
var jArray = JsonConvert.DeserializeObject<JObject>(json)["result"]!.ToObject<JArray>();
|
||||
//然后遍历所有数组的内容
|
||||
foreach (var element in jArray)
|
||||
{
|
||||
//获取数组元素的Id
|
||||
var id = element["id"]!.ToObject<string>();
|
||||
//通过Id查找已加载的Entity
|
||||
if (!_entities.TryGetValue(id, out var entity)) continue;
|
||||
//加载数组中的"value"为json
|
||||
var _json = element["value"]!.ToObject<string>();
|
||||
//获取被加载为string的json
|
||||
var obj = Json.ParseString(_json);
|
||||
//反序列化string为原始json
|
||||
var value = JObject.Parse(obj.ToString());
|
||||
//提交json和entity
|
||||
ProcessEntity(value,entity);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 提交jObject数据和Entity进行解析和处理
|
||||
/// </summary>
|
||||
/// <param name="jObject">json [result] [0] [value] 中的原始json</param>
|
||||
/// <param name="entity">引用实体,如PLC-ZL</param>
|
||||
private static void ProcessEntity(JObject jObject, IEntity entity)
|
||||
{
|
||||
//从Entity中加载所有Rotation Component
|
||||
var rotationComponents = entity
|
||||
.Components
|
||||
.Where(x => x is RotationComponent)
|
||||
.Select((x => (RotationComponent)x));
|
||||
//遍历所有Rotation Component
|
||||
foreach (var rotationComponent in rotationComponents)
|
||||
{
|
||||
//加载rotation需要的path,如 var angle = value["J1"]
|
||||
var path = rotationComponent.Path;
|
||||
//加载以获取到的角度
|
||||
var currentAngle = jObject[path]!.ToObject<float>();
|
||||
//最终角度 = 当前角度*角度权重 + 角度偏移 + 原始角度
|
||||
var euler = currentAngle * rotationComponent.Weight + rotationComponent.Offset + rotationComponent.OriginalEuler;
|
||||
//为Node3D.Rotation提交最后的角度计算结果
|
||||
rotationComponent.Rotation = euler;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user