refactor!: Identifiable interface extracted from IEntity
This commit is contained in:
@@ -3,18 +3,4 @@ namespace Engine.Core;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a basic entity in the engine.
|
/// Represents a basic entity in the engine.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IEntity : IInitializable, IHasStateEnable
|
public interface IEntity : IInitializable, IIdentifiable, IHasStateEnable;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Event triggered when the <see cref="Id"/> of the <see cref="IEntity"/> changes.
|
|
||||||
/// The string action parameter is the previous <see cref="Id"/> of the <see cref="IEntity"/>.
|
|
||||||
/// </summary>
|
|
||||||
Event<IEntity, IdChangedArguments> OnIdChanged { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The ID of the <see cref="IEntity"/>.
|
|
||||||
/// </summary>
|
|
||||||
string Id { get; set; }
|
|
||||||
|
|
||||||
readonly record struct IdChangedArguments(string PreviousId);
|
|
||||||
}
|
|
||||||
|
|||||||
20
Engine.Core/Abstract/IHasId.cs
Normal file
20
Engine.Core/Abstract/IHasId.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
namespace Engine.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents any instance in the engine with an id.
|
||||||
|
/// </summary>
|
||||||
|
public interface IIdentifiable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Event triggered when the <see cref="Id"/> of the <see cref="IIdentifiable"/> changes.
|
||||||
|
/// The string action parameter is the previous <see cref="Id"/> of the <see cref="IIdentifiable"/>.
|
||||||
|
/// </summary>
|
||||||
|
Event<IIdentifiable, IdChangedArguments> OnIdChanged { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the <see cref="IIdentifiable"/>.
|
||||||
|
/// </summary>
|
||||||
|
string Id { get; set; }
|
||||||
|
|
||||||
|
readonly record struct IdChangedArguments(string PreviousId);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ namespace Engine.Core;
|
|||||||
|
|
||||||
public abstract class BaseEntity : IEntity
|
public abstract class BaseEntity : IEntity
|
||||||
{
|
{
|
||||||
public Event<IEntity, IEntity.IdChangedArguments> OnIdChanged { get; } = new();
|
public Event<IIdentifiable, IIdentifiable.IdChangedArguments> OnIdChanged { get; } = new();
|
||||||
public Event<IInitializable> OnInitialized { get; } = new();
|
public Event<IInitializable> OnInitialized { get; } = new();
|
||||||
public Event<IInitializable> OnFinalized { get; } = new();
|
public Event<IInitializable> OnFinalized { get; } = new();
|
||||||
public Event<IHasStateEnable> OnStateEnableAssigned { get; } = new();
|
public Event<IHasStateEnable> OnStateEnableAssigned { get; } = new();
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Engine.Core.Serialization;
|
|
||||||
|
|
||||||
public class EntityRegistry
|
|
||||||
{
|
|
||||||
public Event<EntityRegistry, EntityRegisteredArguments> OnEntityRegistered = null!;
|
|
||||||
|
|
||||||
private readonly Dictionary<string, Action<IEntity>?> assignCallbacks = [];
|
|
||||||
private readonly Dictionary<string, IEntity> registeredEntities = [];
|
|
||||||
public IReadOnlyDictionary<string, IEntity> RegisteredEntities => registeredEntities;
|
|
||||||
|
|
||||||
public void Add(IEntity entity)
|
|
||||||
{
|
|
||||||
if (registeredEntities.TryAdd(entity.Id, entity))
|
|
||||||
OnEntityRegistered?.Invoke(this, new(entity));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void QueueAssign(string id, Action<IEntity> setMethod)
|
|
||||||
{
|
|
||||||
assignCallbacks.TryAdd(id, null);
|
|
||||||
assignCallbacks[id] = assignCallbacks[id] + setMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AssignAll()
|
|
||||||
{
|
|
||||||
foreach ((string id, Action<IEntity>? action) in assignCallbacks)
|
|
||||||
action?.Invoke(registeredEntities[id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reset()
|
|
||||||
{
|
|
||||||
assignCallbacks.Clear();
|
|
||||||
registeredEntities.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly record struct EntityRegisteredArguments(IEntity Entity);
|
|
||||||
}
|
|
||||||
39
Engine.Core/Serialization/IdentifiableRegistry.cs
Normal file
39
Engine.Core/Serialization/IdentifiableRegistry.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Engine.Core.Serialization;
|
||||||
|
|
||||||
|
public class IdentifiableRegistry
|
||||||
|
{
|
||||||
|
public Event<IdentifiableRegistry, EntityRegisteredArguments> OnEntityRegistered = null!;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, Action<IIdentifiable>?> assignCallbacks = [];
|
||||||
|
private readonly Dictionary<string, IIdentifiable> registeredEntities = [];
|
||||||
|
public IReadOnlyDictionary<string, IIdentifiable> RegisteredEntities => registeredEntities;
|
||||||
|
|
||||||
|
public void Add(IIdentifiable identifiable)
|
||||||
|
{
|
||||||
|
if (registeredEntities.TryAdd(identifiable.Id, identifiable))
|
||||||
|
OnEntityRegistered?.Invoke(this, new(identifiable));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void QueueAssign(string id, Action<IIdentifiable> setMethod)
|
||||||
|
{
|
||||||
|
assignCallbacks.TryAdd(id, null);
|
||||||
|
assignCallbacks[id] = assignCallbacks[id] + setMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AssignAll()
|
||||||
|
{
|
||||||
|
foreach ((string id, Action<IIdentifiable>? action) in assignCallbacks)
|
||||||
|
action?.Invoke(registeredEntities[id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
assignCallbacks.Clear();
|
||||||
|
registeredEntities.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct EntityRegisteredArguments(IIdentifiable Entity);
|
||||||
|
}
|
||||||
@@ -43,8 +43,8 @@ public class SerializedClass
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
object? value = privatePropertyInfo.GetValue(@class);
|
object? value = privatePropertyInfo.GetValue(@class);
|
||||||
if (value is IEntity entity)
|
if (value is IIdentifiable identifiable)
|
||||||
Private.Add(privatePropertyInfo.Name, entity.Id);
|
Private.Add(privatePropertyInfo.Name, identifiable.Id);
|
||||||
else
|
else
|
||||||
Private.Add(privatePropertyInfo.Name, value);
|
Private.Add(privatePropertyInfo.Name, value);
|
||||||
}
|
}
|
||||||
@@ -61,8 +61,8 @@ public class SerializedClass
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
object? value = publicPropertyInfo.GetValue(@class);
|
object? value = publicPropertyInfo.GetValue(@class);
|
||||||
if (value is IEntity entity)
|
if (value is IIdentifiable identifiable)
|
||||||
Public.Add(publicPropertyInfo.Name, entity.Id);
|
Public.Add(publicPropertyInfo.Name, identifiable.Id);
|
||||||
else
|
else
|
||||||
Public.Add(publicPropertyInfo.Name, value);
|
Public.Add(publicPropertyInfo.Name, value);
|
||||||
}
|
}
|
||||||
@@ -76,8 +76,8 @@ public class SerializedClass
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
object? value = privateFieldInfo.GetValue(@class);
|
object? value = privateFieldInfo.GetValue(@class);
|
||||||
if (value is IEntity entity)
|
if (value is IIdentifiable identifiable)
|
||||||
Private.Add(privateFieldInfo.Name, entity.Id);
|
Private.Add(privateFieldInfo.Name, identifiable.Id);
|
||||||
else
|
else
|
||||||
Private.Add(privateFieldInfo.Name, value);
|
Private.Add(privateFieldInfo.Name, value);
|
||||||
}
|
}
|
||||||
@@ -91,8 +91,8 @@ public class SerializedClass
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
object? value = publicFieldInfo.GetValue(@class);
|
object? value = publicFieldInfo.GetValue(@class);
|
||||||
if (value is IEntity entity)
|
if (value is IIdentifiable identifiable)
|
||||||
Public.Add(publicFieldInfo.Name, entity.Id);
|
Public.Add(publicFieldInfo.Name, identifiable.Id);
|
||||||
else
|
else
|
||||||
Public.Add(publicFieldInfo.Name, value);
|
Public.Add(publicFieldInfo.Name, value);
|
||||||
}
|
}
|
||||||
@@ -112,36 +112,36 @@ public class SerializedClass
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public object CreateInstance(EntityRegistry? entityRegistry)
|
public object CreateInstance(IdentifiableRegistry? identifiableRegistry)
|
||||||
{
|
{
|
||||||
if (entityRegistry is null)
|
if (identifiableRegistry is null)
|
||||||
return CreateInstance();
|
return CreateInstance();
|
||||||
|
|
||||||
Type type = TypeFactory.GetType(Type);
|
Type type = TypeFactory.GetType(Type);
|
||||||
object instance = TypeFactory.Get(type);
|
object instance = TypeFactory.Get(type);
|
||||||
|
|
||||||
foreach ((string key, object? value) in Private)
|
foreach ((string key, object? value) in Private)
|
||||||
AssignVariable(key, type, instance, value, PRIVATE_BINDING_FLAGS, entityRegistry);
|
AssignVariable(key, type, instance, value, PRIVATE_BINDING_FLAGS, identifiableRegistry);
|
||||||
|
|
||||||
foreach ((string key, object? value) in Public)
|
foreach ((string key, object? value) in Public)
|
||||||
AssignVariable(key, type, instance, value, PUBLIC_BINDING_FLAGS, entityRegistry);
|
AssignVariable(key, type, instance, value, PUBLIC_BINDING_FLAGS, identifiableRegistry);
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AssignVariable(string key, Type type, object instance, object? value, BindingFlags bindingFlags, EntityRegistry entityRegistry)
|
private static void AssignVariable(string key, Type type, object instance, object? value, BindingFlags bindingFlags, IdentifiableRegistry identifiableRegistry)
|
||||||
{
|
{
|
||||||
if (type.GetField(key, bindingFlags) is FieldInfo fieldInfo)
|
if (type.GetField(key, bindingFlags) is FieldInfo fieldInfo)
|
||||||
{
|
{
|
||||||
if (typeof(IEntity).IsAssignableFrom(fieldInfo.FieldType))
|
if (typeof(IIdentifiable).IsAssignableFrom(fieldInfo.FieldType))
|
||||||
entityRegistry.QueueAssign(value?.ToString() ?? "", (entity) => fieldInfo.SetValue(instance, entity));
|
identifiableRegistry.QueueAssign(value?.ToString() ?? "", (entity) => fieldInfo.SetValue(instance, entity));
|
||||||
else
|
else
|
||||||
fieldInfo.SetValue(instance, value);
|
fieldInfo.SetValue(instance, value);
|
||||||
}
|
}
|
||||||
else if (type.GetProperty(key, bindingFlags) is PropertyInfo propertyInfo)
|
else if (type.GetProperty(key, bindingFlags) is PropertyInfo propertyInfo)
|
||||||
{
|
{
|
||||||
if (typeof(IEntity).IsAssignableFrom(propertyInfo.PropertyType))
|
if (typeof(IIdentifiable).IsAssignableFrom(propertyInfo.PropertyType))
|
||||||
entityRegistry.QueueAssign(value?.ToString() ?? "", (entity) => propertyInfo.SetValue(instance, entity));
|
identifiableRegistry.QueueAssign(value?.ToString() ?? "", (entity) => propertyInfo.SetValue(instance, entity));
|
||||||
else
|
else
|
||||||
propertyInfo.SetValue(instance, value);
|
propertyInfo.SetValue(instance, value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ namespace Engine.Serializers.Yaml;
|
|||||||
public interface IEngineTypeYamlConverter : IYamlTypeConverter
|
public interface IEngineTypeYamlConverter : IYamlTypeConverter
|
||||||
{
|
{
|
||||||
YamlSerializer Serializer { get; set; }
|
YamlSerializer Serializer { get; set; }
|
||||||
EntityRegistry EntityRegistry { get; set; }
|
IdentifiableRegistry IdentifiableRegistry { get; set; }
|
||||||
IProgressionTracker ProgressionTracker { get; set; }
|
IProgressionTracker ProgressionTracker { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public class BehaviourControllerConverter : EngineTypeYamlSerializerBase<IBehavi
|
|||||||
throw new();
|
throw new();
|
||||||
SerializedClass instanceSerializedClass = (SerializedClass)rootDeserializer(typeof(SerializedClass))!;
|
SerializedClass instanceSerializedClass = (SerializedClass)rootDeserializer(typeof(SerializedClass))!;
|
||||||
ProgressionTracker.Set(isTrackingController ? .5f : ProgressionTracker.Progression, $"Creating {instanceSerializedClass.Type}");
|
ProgressionTracker.Set(isTrackingController ? .5f : ProgressionTracker.Progression, $"Creating {instanceSerializedClass.Type}");
|
||||||
behaviourController = (IBehaviourController)instanceSerializedClass.CreateInstance(EntityRegistry);
|
behaviourController = (IBehaviourController)instanceSerializedClass.CreateInstance(IdentifiableRegistry);
|
||||||
|
|
||||||
string value = parser.Consume<Scalar>().Value;
|
string value = parser.Consume<Scalar>().Value;
|
||||||
if (value.CompareTo(nameof(IBehaviourController.StateEnable)) != 0)
|
if (value.CompareTo(nameof(IBehaviourController.StateEnable)) != 0)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public class BehaviourConverter : EngineTypeYamlSerializerBase<IBehaviour>
|
|||||||
throw new();
|
throw new();
|
||||||
SerializedClass instanceSerializedClass = (SerializedClass)rootDeserializer(typeof(SerializedClass))!;
|
SerializedClass instanceSerializedClass = (SerializedClass)rootDeserializer(typeof(SerializedClass))!;
|
||||||
ProgressionTracker.Set(isTrackingController ? .5f : ProgressionTracker.Progression, $"Creating {instanceSerializedClass.Type}");
|
ProgressionTracker.Set(isTrackingController ? .5f : ProgressionTracker.Progression, $"Creating {instanceSerializedClass.Type}");
|
||||||
behaviour = (IBehaviour)instanceSerializedClass.CreateInstance(EntityRegistry);
|
behaviour = (IBehaviour)instanceSerializedClass.CreateInstance(IdentifiableRegistry);
|
||||||
|
|
||||||
if (parser.Consume<Scalar>().Value.CompareTo(nameof(IBehaviour.StateEnable)) != 0)
|
if (parser.Consume<Scalar>().Value.CompareTo(nameof(IBehaviour.StateEnable)) != 0)
|
||||||
throw new();
|
throw new();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public abstract class EngineTypeYamlSerializerBase<T> : IEngineTypeYamlConverter
|
|||||||
{
|
{
|
||||||
protected const string SERIALIZED_SCALAR_NAME = "Properties";
|
protected const string SERIALIZED_SCALAR_NAME = "Properties";
|
||||||
|
|
||||||
public EntityRegistry EntityRegistry { get; set; } = null!;
|
public IdentifiableRegistry IdentifiableRegistry { get; set; } = null!;
|
||||||
public YamlSerializer Serializer { get; set; } = null!;
|
public YamlSerializer Serializer { get; set; } = null!;
|
||||||
public IProgressionTracker ProgressionTracker { get; set; } = null!;
|
public IProgressionTracker ProgressionTracker { get; set; } = null!;
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ public abstract class EngineTypeYamlSerializerBase<T> : IEngineTypeYamlConverter
|
|||||||
T? result = Read(parser, type, rootDeserializer);
|
T? result = Read(parser, type, rootDeserializer);
|
||||||
|
|
||||||
if (result is IEntity entity)
|
if (result is IEntity entity)
|
||||||
EntityRegistry.Add(entity);
|
IdentifiableRegistry.Add(entity);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class StateEnableConverter : EngineTypeYamlSerializerBase<IStateEnable>
|
|||||||
if (parser.Consume<Scalar>().Value.CompareTo(SERIALIZED_SCALAR_NAME) != 0)
|
if (parser.Consume<Scalar>().Value.CompareTo(SERIALIZED_SCALAR_NAME) != 0)
|
||||||
throw new();
|
throw new();
|
||||||
SerializedClass instanceSerializedClass = (SerializedClass)rootDeserializer(typeof(SerializedClass))!;
|
SerializedClass instanceSerializedClass = (SerializedClass)rootDeserializer(typeof(SerializedClass))!;
|
||||||
stateEnable = (IStateEnable)instanceSerializedClass.CreateInstance(EntityRegistry);
|
stateEnable = (IStateEnable)instanceSerializedClass.CreateInstance(IdentifiableRegistry);
|
||||||
|
|
||||||
parser.Consume<MappingEnd>();
|
parser.Consume<MappingEnd>();
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class UniverseObjectSerializer : EngineTypeYamlSerializerBase<IUniverseOb
|
|||||||
throw new();
|
throw new();
|
||||||
SerializedClass instanceSerializedClass = (SerializedClass)rootDeserializer(typeof(SerializedClass))!;
|
SerializedClass instanceSerializedClass = (SerializedClass)rootDeserializer(typeof(SerializedClass))!;
|
||||||
ProgressionTracker.Set(isTrackingController ? .3f : ProgressionTracker.Progression, $"Creating {instanceSerializedClass.Type}");
|
ProgressionTracker.Set(isTrackingController ? .3f : ProgressionTracker.Progression, $"Creating {instanceSerializedClass.Type}");
|
||||||
universeObject = (IUniverseObject)instanceSerializedClass.CreateInstance(EntityRegistry);
|
universeObject = (IUniverseObject)instanceSerializedClass.CreateInstance(IdentifiableRegistry);
|
||||||
|
|
||||||
if (parser.Consume<Scalar>().Value.CompareTo(nameof(IUniverseObject.StateEnable)) != 0)
|
if (parser.Consume<Scalar>().Value.CompareTo(nameof(IUniverseObject.StateEnable)) != 0)
|
||||||
throw new();
|
throw new();
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ public class YamlSerializer : Core.Serialization.ISerializer
|
|||||||
private readonly YamlDotNet.Serialization.ISerializer serializer = null!;
|
private readonly YamlDotNet.Serialization.ISerializer serializer = null!;
|
||||||
private readonly YamlDotNet.Serialization.IDeserializer deserializer = null!;
|
private readonly YamlDotNet.Serialization.IDeserializer deserializer = null!;
|
||||||
|
|
||||||
private readonly EntityRegistry entityRegistry = null!;
|
private readonly IdentifiableRegistry identifiableRegistry = null!;
|
||||||
private readonly IProgressionTracker progressionTracker = null!;
|
private readonly IProgressionTracker progressionTracker = null!;
|
||||||
|
|
||||||
private readonly System.Threading.Lock Lock = new();
|
private readonly System.Threading.Lock Lock = new();
|
||||||
|
|
||||||
public YamlSerializer()
|
public YamlSerializer()
|
||||||
{
|
{
|
||||||
entityRegistry = new();
|
identifiableRegistry = new();
|
||||||
progressionTracker = new ProgressionTracker();
|
progressionTracker = new ProgressionTracker();
|
||||||
|
|
||||||
SerializerBuilder serializerBuilder = new SerializerBuilder()
|
SerializerBuilder serializerBuilder = new SerializerBuilder()
|
||||||
@@ -37,7 +37,7 @@ public class YamlSerializer : Core.Serialization.ISerializer
|
|||||||
foreach (IEngineTypeYamlConverter typeConverter in GetEngineYamlTypeConverters())
|
foreach (IEngineTypeYamlConverter typeConverter in GetEngineYamlTypeConverters())
|
||||||
{
|
{
|
||||||
typeConverter.Serializer = this;
|
typeConverter.Serializer = this;
|
||||||
typeConverter.EntityRegistry = entityRegistry;
|
typeConverter.IdentifiableRegistry = identifiableRegistry;
|
||||||
typeConverter.ProgressionTracker = progressionTracker;
|
typeConverter.ProgressionTracker = progressionTracker;
|
||||||
|
|
||||||
deserializerBuilder = deserializerBuilder.WithTypeConverter(typeConverter);
|
deserializerBuilder = deserializerBuilder.WithTypeConverter(typeConverter);
|
||||||
@@ -66,9 +66,9 @@ public class YamlSerializer : Core.Serialization.ISerializer
|
|||||||
{
|
{
|
||||||
lock (Lock)
|
lock (Lock)
|
||||||
{
|
{
|
||||||
entityRegistry.Reset();
|
identifiableRegistry.Reset();
|
||||||
object result = deserializer.Deserialize(configuration)!;
|
object result = deserializer.Deserialize(configuration)!;
|
||||||
entityRegistry.AssignAll();
|
identifiableRegistry.AssignAll();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,9 +77,9 @@ public class YamlSerializer : Core.Serialization.ISerializer
|
|||||||
{
|
{
|
||||||
lock (Lock)
|
lock (Lock)
|
||||||
{
|
{
|
||||||
entityRegistry.Reset();
|
identifiableRegistry.Reset();
|
||||||
object result = deserializer.Deserialize(configuration, type)!;
|
object result = deserializer.Deserialize(configuration, type)!;
|
||||||
entityRegistry.AssignAll();
|
identifiableRegistry.AssignAll();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,9 +88,9 @@ public class YamlSerializer : Core.Serialization.ISerializer
|
|||||||
{
|
{
|
||||||
lock (Lock)
|
lock (Lock)
|
||||||
{
|
{
|
||||||
entityRegistry.Reset();
|
identifiableRegistry.Reset();
|
||||||
T result = deserializer.Deserialize<T>(configuration);
|
T result = deserializer.Deserialize<T>(configuration);
|
||||||
entityRegistry.AssignAll();
|
identifiableRegistry.AssignAll();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user