Merge remote-tracking branch 'upstream/dev' into develop
This commit is contained in:
@@ -159,7 +159,11 @@ namespace Barotrauma.Networking
|
||||
return command;
|
||||
}
|
||||
|
||||
public static float GetGarbleAmount(Entity listener, Entity sender, float range, float obstructionmult = 2.0f)
|
||||
/// <summary>
|
||||
/// How much messages sent by <paramref name="sender"/> should get garbled. Takes the distance between the entities and optionally the obstructions between them into account (see <paramref name="obstructionMultiplier"/>).
|
||||
/// </summary>
|
||||
/// <param name="obstructionMultiplier">Values greater than or equal to 1 cause the message to get garbled more heavily when there's some obstruction between the characters. Values smaller than 1 mean the garbling only depends on distance.</param>
|
||||
public static float GetGarbleAmount(Entity listener, Entity sender, float range, float obstructionMultiplier = 2.0f)
|
||||
{
|
||||
if (listener == null || sender == null)
|
||||
{
|
||||
@@ -172,12 +176,12 @@ namespace Barotrauma.Networking
|
||||
|
||||
Hull listenerHull = listener == null ? null : Hull.FindHull(listener.WorldPosition);
|
||||
Hull sourceHull = sender == null ? null : Hull.FindHull(sender.WorldPosition);
|
||||
if (sourceHull != listenerHull)
|
||||
if (sourceHull != listenerHull && obstructionMultiplier >= 1.0f)
|
||||
{
|
||||
if ((sourceHull == null || !sourceHull.GetConnectedHulls(includingThis: false, searchDepth: 2, ignoreClosedGaps: true).Contains(listenerHull)) &&
|
||||
Submarine.CheckVisibility(listener.SimPosition, sender.SimPosition) != null)
|
||||
{
|
||||
dist = (dist + 100f) * obstructionmult;
|
||||
dist = (dist + 100f) * obstructionMultiplier;
|
||||
}
|
||||
}
|
||||
if (dist > range) { return 1.0f; }
|
||||
@@ -192,9 +196,9 @@ namespace Barotrauma.Networking
|
||||
return ApplyDistanceEffect(listener, Sender, Text, SpeakRange);
|
||||
}
|
||||
|
||||
public static string ApplyDistanceEffect(Entity listener, Entity sender, string text, float range, float obstructionmult = 2.0f)
|
||||
public static string ApplyDistanceEffect(Entity listener, Entity sender, string text, float range, float obstructionMultiplier = 2.0f)
|
||||
{
|
||||
return ApplyDistanceEffect(text, GetGarbleAmount(listener, sender, range, obstructionmult));
|
||||
return ApplyDistanceEffect(text, GetGarbleAmount(listener, sender, range, obstructionMultiplier));
|
||||
}
|
||||
|
||||
public static string ApplyDistanceEffect(string text, float garbleAmount)
|
||||
@@ -247,7 +251,7 @@ namespace Barotrauma.Networking
|
||||
var senderRadio = senderItem.GetComponent<WifiComponent>();
|
||||
if (!receiverRadio.CanReceive(senderRadio)) { continue; }
|
||||
|
||||
string msg = ApplyDistanceEffect(receiverItem, senderItem, message, senderRadio.Range);
|
||||
string msg = ApplyDistanceEffect(receiverItem, senderItem, message, senderRadio.Range, obstructionMultiplier: 0);
|
||||
if (sender.SpeechImpediment > 0.0f)
|
||||
{
|
||||
//speech impediment doesn't reduce the range when using a radio, but adds extra garbling
|
||||
|
||||
@@ -23,13 +23,22 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
Success = 0x00,
|
||||
Heartbeat = 0x01,
|
||||
RequestShutdown = 0xCC,
|
||||
Crash = 0xFF
|
||||
}
|
||||
|
||||
private static ManualResetEvent writeManualResetEvent;
|
||||
|
||||
private static volatile bool shutDown;
|
||||
public static bool HasShutDown => shutDown;
|
||||
private enum StatusEnum
|
||||
{
|
||||
NeverStarted,
|
||||
Active,
|
||||
RequestedShutDown,
|
||||
ShutDown
|
||||
}
|
||||
|
||||
private static volatile StatusEnum status = StatusEnum.NeverStarted;
|
||||
public static bool HasShutDown => status is StatusEnum.ShutDown;
|
||||
|
||||
private const int ReadBufferSize = MsgConstants.MTU * 2;
|
||||
private static byte[] readTempBytes;
|
||||
@@ -38,7 +47,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
private static ConcurrentQueue<byte[]> msgsToWrite;
|
||||
private static ConcurrentQueue<string> errorsToWrite;
|
||||
|
||||
|
||||
private static ConcurrentQueue<byte[]> msgsToRead;
|
||||
|
||||
private static Thread readThread;
|
||||
@@ -48,6 +57,8 @@ namespace Barotrauma.Networking
|
||||
|
||||
private static void PrivateStart()
|
||||
{
|
||||
status = StatusEnum.Active;
|
||||
|
||||
readIncOffset = 0;
|
||||
readIncTotal = 0;
|
||||
|
||||
@@ -58,8 +69,6 @@ namespace Barotrauma.Networking
|
||||
|
||||
msgsToRead = new ConcurrentQueue<byte[]>();
|
||||
|
||||
shutDown = false;
|
||||
|
||||
readCancellationToken = new CancellationTokenSource();
|
||||
|
||||
writeManualResetEvent = new ManualResetEvent(false);
|
||||
@@ -80,7 +89,13 @@ namespace Barotrauma.Networking
|
||||
|
||||
private static void PrivateShutDown()
|
||||
{
|
||||
shutDown = true;
|
||||
if (Thread.CurrentThread != GameMain.MainThread)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot call {nameof(ChildServerRelay)}.{nameof(PrivateShutDown)} from a thread other than the main one");
|
||||
}
|
||||
if (status is StatusEnum.NeverStarted) { return; }
|
||||
status = StatusEnum.ShutDown;
|
||||
writeManualResetEvent?.Set();
|
||||
readCancellationToken?.Cancel();
|
||||
readThread?.Join(); readThread = null;
|
||||
@@ -93,18 +108,18 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
|
||||
|
||||
private static int ReadIncomingMsgs()
|
||||
private static Option<int> ReadIncomingMsgs()
|
||||
{
|
||||
Task<int> readTask = readStream?.ReadAsync(readTempBytes, 0, readTempBytes.Length, readCancellationToken.Token);
|
||||
if (readTask is null) { return -1; }
|
||||
if (readTask is null) { return Option<int>.None(); }
|
||||
|
||||
int timeOutMilliseconds = 100;
|
||||
for (int i = 0; i < 150; i++)
|
||||
{
|
||||
if (shutDown)
|
||||
if (status is StatusEnum.ShutDown)
|
||||
{
|
||||
readCancellationToken?.Cancel();
|
||||
return -1;
|
||||
return Option<int>.None();
|
||||
}
|
||||
try
|
||||
{
|
||||
@@ -115,36 +130,36 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
catch (AggregateException aggregateException)
|
||||
{
|
||||
if (aggregateException.InnerException is OperationCanceledException) { return -1; }
|
||||
if (aggregateException.InnerException is OperationCanceledException) { return Option<int>.None(); }
|
||||
throw;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return -1;
|
||||
return Option<int>.None();
|
||||
}
|
||||
}
|
||||
|
||||
if (readTask.Status != TaskStatus.RanToCompletion)
|
||||
if (readTask.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
bool swallowException = shutDown
|
||||
&& ((readTask.Exception?.InnerException is ObjectDisposedException)
|
||||
|| (readTask.Exception?.InnerException is System.IO.IOException));
|
||||
if (swallowException)
|
||||
{
|
||||
readCancellationToken?.Cancel();
|
||||
return -1;
|
||||
}
|
||||
throw new Exception(
|
||||
$"ChildServerRelay readTask did not run to completion: status was {readTask.Status}.",
|
||||
readTask.Exception);
|
||||
return Option<int>.Some(readTask.Result);
|
||||
}
|
||||
|
||||
return readTask.Result;
|
||||
bool swallowException =
|
||||
status is not StatusEnum.Active
|
||||
&& readTask.Exception?.InnerException is ObjectDisposedException or System.IO.IOException;
|
||||
if (swallowException)
|
||||
{
|
||||
readCancellationToken?.Cancel();
|
||||
return Option<int>.None();
|
||||
}
|
||||
throw new Exception(
|
||||
$"ChildServerRelay readTask did not run to completion: status was {readTask.Status}.",
|
||||
readTask.Exception);
|
||||
}
|
||||
|
||||
private static void CheckPipeConnected(string name, PipeType pipe)
|
||||
{
|
||||
if (!(pipe is { IsConnected: true }))
|
||||
if (status is StatusEnum.Active && pipe is not { IsConnected: true })
|
||||
{
|
||||
throw new Exception($"{name} was disconnected unexpectedly");
|
||||
}
|
||||
@@ -155,7 +170,7 @@ namespace Barotrauma.Networking
|
||||
private static void UpdateRead()
|
||||
{
|
||||
Span<byte> msgLengthSpan = stackalloc byte[4 + 1];
|
||||
while (!shutDown)
|
||||
while (!HasShutDown)
|
||||
{
|
||||
CheckPipeConnected(nameof(readStream), readStream);
|
||||
|
||||
@@ -165,10 +180,9 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
if (readIncOffset >= readIncTotal)
|
||||
{
|
||||
readIncTotal = ReadIncomingMsgs();
|
||||
if (!ReadIncomingMsgs().TryUnwrap(out readIncTotal)) { return false; }
|
||||
readIncOffset = 0;
|
||||
if (readIncTotal == 0) { Thread.Yield(); continue; }
|
||||
if (readIncTotal < 0) { return false; }
|
||||
}
|
||||
readTo[i] = readTempBytes[readIncOffset];
|
||||
readIncOffset++;
|
||||
@@ -176,7 +190,7 @@ namespace Barotrauma.Networking
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!readBytes(msgLengthSpan)) { shutDown = true; break; }
|
||||
if (!readBytes(msgLengthSpan)) { status = StatusEnum.ShutDown; break; }
|
||||
|
||||
int msgLength = msgLengthSpan[0]
|
||||
| (msgLengthSpan[1] << 8)
|
||||
@@ -184,24 +198,24 @@ namespace Barotrauma.Networking
|
||||
| (msgLengthSpan[3] << 24);
|
||||
WriteStatus writeStatus = (WriteStatus)msgLengthSpan[4];
|
||||
|
||||
if (msgLength > 0)
|
||||
{
|
||||
byte[] msg = new byte[msgLength];
|
||||
if (!readBytes(msg.AsSpan())) { shutDown = true; break; }
|
||||
byte[] msg = msgLength > 0 ? new byte[msgLength] : Array.Empty<byte>();
|
||||
if (msg.Length > 0 && !readBytes(msg.AsSpan())) { status = StatusEnum.ShutDown; break; }
|
||||
|
||||
switch (writeStatus)
|
||||
{
|
||||
case WriteStatus.Success:
|
||||
msgsToRead.Enqueue(msg);
|
||||
break;
|
||||
case WriteStatus.Heartbeat:
|
||||
//do nothing
|
||||
break;
|
||||
case WriteStatus.Crash:
|
||||
HandleCrashString(Encoding.UTF8.GetString(msg));
|
||||
shutDown = true;
|
||||
break;
|
||||
}
|
||||
switch (writeStatus)
|
||||
{
|
||||
case WriteStatus.Success:
|
||||
msgsToRead.Enqueue(msg);
|
||||
break;
|
||||
case WriteStatus.Heartbeat:
|
||||
//do nothing
|
||||
break;
|
||||
case WriteStatus.RequestShutdown:
|
||||
status = StatusEnum.ShutDown;
|
||||
break;
|
||||
case WriteStatus.Crash:
|
||||
HandleCrashString(Encoding.UTF8.GetString(msg));
|
||||
status = StatusEnum.ShutDown;
|
||||
break;
|
||||
}
|
||||
|
||||
Thread.Yield();
|
||||
@@ -210,13 +224,11 @@ namespace Barotrauma.Networking
|
||||
|
||||
private static void UpdateWrite()
|
||||
{
|
||||
while (!shutDown)
|
||||
while (!HasShutDown)
|
||||
{
|
||||
CheckPipeConnected(nameof(writeStream), writeStream);
|
||||
|
||||
byte[] msg;
|
||||
|
||||
void writeMsg(WriteStatus writeStatus)
|
||||
void writeMsg(WriteStatus writeStatus, byte[] msg)
|
||||
{
|
||||
// It's SUPER IMPORTANT that this stack allocation
|
||||
// remains in this local function and is never inlined,
|
||||
@@ -224,21 +236,19 @@ namespace Barotrauma.Networking
|
||||
// when the function returns; placing it in the loop
|
||||
// this method is based around would lead to a stack
|
||||
// overflow real quick!
|
||||
Span<byte> bytesToWrite = stackalloc byte[4 + 1 + msg.Length];
|
||||
Span<byte> headerBytes = stackalloc byte[4 + 1];
|
||||
|
||||
bytesToWrite[0] = (byte)(msg.Length & 0xFF);
|
||||
bytesToWrite[1] = (byte)((msg.Length >> 8) & 0xFF);
|
||||
bytesToWrite[2] = (byte)((msg.Length >> 16) & 0xFF);
|
||||
bytesToWrite[3] = (byte)((msg.Length >> 24) & 0xFF);
|
||||
headerBytes[0] = (byte)(msg.Length & 0xFF);
|
||||
headerBytes[1] = (byte)((msg.Length >> 8) & 0xFF);
|
||||
headerBytes[2] = (byte)((msg.Length >> 16) & 0xFF);
|
||||
headerBytes[3] = (byte)((msg.Length >> 24) & 0xFF);
|
||||
|
||||
bytesToWrite[4] = (byte)writeStatus;
|
||||
Span<byte> msgSlice = bytesToWrite.Slice(4 + 1, msg.Length);
|
||||
|
||||
msg.AsSpan().CopyTo(msgSlice);
|
||||
headerBytes[4] = (byte)writeStatus;
|
||||
|
||||
try
|
||||
{
|
||||
writeStream?.Write(bytesToWrite);
|
||||
writeStream?.Write(headerBytes);
|
||||
writeStream?.Write(msg);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
@@ -246,7 +256,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
case ObjectDisposedException _:
|
||||
case System.IO.IOException _:
|
||||
if (!shutDown) { throw; }
|
||||
if (!HasShutDown) { throw; }
|
||||
break;
|
||||
default:
|
||||
throw;
|
||||
@@ -254,29 +264,34 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
}
|
||||
|
||||
if (status is StatusEnum.RequestedShutDown)
|
||||
{
|
||||
writeMsg(WriteStatus.RequestShutdown, Array.Empty<byte>());
|
||||
status = StatusEnum.ShutDown;
|
||||
}
|
||||
|
||||
while (errorsToWrite.TryDequeue(out var error))
|
||||
{
|
||||
msg = Encoding.UTF8.GetBytes(error);
|
||||
writeMsg(WriteStatus.Crash);
|
||||
shutDown = true;
|
||||
writeMsg(WriteStatus.Crash, Encoding.UTF8.GetBytes(error));
|
||||
status = StatusEnum.ShutDown;
|
||||
}
|
||||
|
||||
while (msgsToWrite.TryDequeue(out msg))
|
||||
while (msgsToWrite.TryDequeue(out var msg))
|
||||
{
|
||||
writeMsg(WriteStatus.Success);
|
||||
writeMsg(WriteStatus.Success, msg);
|
||||
|
||||
if (shutDown) { break; }
|
||||
if (HasShutDown) { break; }
|
||||
}
|
||||
|
||||
if (!shutDown)
|
||||
if (!HasShutDown)
|
||||
{
|
||||
writeManualResetEvent.Reset();
|
||||
if (!writeManualResetEvent.WaitOne(1000))
|
||||
{
|
||||
if (shutDown) { return; }
|
||||
if (HasShutDown) { return; }
|
||||
|
||||
//heartbeat to keep the other end alive
|
||||
msg = Array.Empty<byte>(); writeMsg(WriteStatus.Heartbeat);
|
||||
writeMsg(WriteStatus.Heartbeat, Array.Empty<byte>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,7 +299,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
public static void Write(byte[] msg)
|
||||
{
|
||||
if (shutDown) { return; }
|
||||
if (HasShutDown) { return; }
|
||||
|
||||
if (msg.Length > 0x1fff_ffff)
|
||||
{
|
||||
@@ -298,7 +313,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
public static bool Read(out byte[] msg)
|
||||
{
|
||||
if (shutDown) { msg = null; return false; }
|
||||
if (HasShutDown) { msg = null; return false; }
|
||||
|
||||
return msgsToRead.TryDequeue(out msg);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ namespace Barotrauma.Networking
|
||||
SellSubItems = 0x4000,
|
||||
ManageMap = 0x8000,
|
||||
ManageHires = 0x10000,
|
||||
All = 0x1FFFF
|
||||
ManageBotTalents = 0x20000,
|
||||
All = 0x3FFFF
|
||||
}
|
||||
|
||||
class PermissionPreset
|
||||
|
||||
@@ -40,15 +40,15 @@ namespace Barotrauma.Networking
|
||||
|
||||
LUA_NET_MESSAGE
|
||||
}
|
||||
enum ClientNetObject
|
||||
|
||||
enum ClientNetSegment
|
||||
{
|
||||
END_OF_MESSAGE, //self-explanatory
|
||||
SYNC_IDS, //ids of the last changes the client knows about
|
||||
CHAT_MESSAGE, //also self-explanatory
|
||||
VOTE, //you get the idea
|
||||
CHARACTER_INPUT,
|
||||
ENTITY_STATE,
|
||||
SPECTATING_POS
|
||||
SyncIds, //ids of the last changes the client knows about
|
||||
ChatMessage, //also self-explanatory
|
||||
Vote, //you get the idea
|
||||
CharacterInput,
|
||||
EntityState,
|
||||
SpectatingPos
|
||||
}
|
||||
|
||||
enum ClientNetError
|
||||
@@ -92,16 +92,15 @@ namespace Barotrauma.Networking
|
||||
|
||||
LUA_NET_MESSAGE
|
||||
}
|
||||
enum ServerNetObject
|
||||
enum ServerNetSegment
|
||||
{
|
||||
END_OF_MESSAGE,
|
||||
SYNC_IDS,
|
||||
CHAT_MESSAGE,
|
||||
VOTE,
|
||||
CLIENT_LIST,
|
||||
ENTITY_POSITION,
|
||||
ENTITY_EVENT,
|
||||
ENTITY_EVENT_INITIAL
|
||||
SyncIds,
|
||||
ChatMessage,
|
||||
Vote,
|
||||
ClientList,
|
||||
EntityPosition,
|
||||
EntityEvent,
|
||||
EntityEventInitial
|
||||
}
|
||||
|
||||
enum TraitorMessageType
|
||||
|
||||
+3
-8
@@ -17,14 +17,9 @@ namespace Barotrauma.Networking
|
||||
|
||||
public LidgrenAddress(IPAddress netAddress)
|
||||
{
|
||||
if (IPAddress.IsLoopback(netAddress))
|
||||
{
|
||||
NetAddress = IPAddress.Loopback;
|
||||
}
|
||||
else
|
||||
{
|
||||
NetAddress = netAddress;
|
||||
}
|
||||
if (IPAddress.IsLoopback(netAddress)) { netAddress = IPAddress.Loopback; }
|
||||
if (netAddress.IsIPv4MappedToIPv6) { netAddress = netAddress.MapToIPv4(); }
|
||||
NetAddress = netAddress;
|
||||
}
|
||||
|
||||
public new static Option<LidgrenAddress> Parse(string endpointStr)
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
public LidgrenEndpoint(IPEndPoint netEndpoint) : base(new LidgrenAddress(netEndpoint.Address))
|
||||
{
|
||||
NetEndpoint = netEndpoint;
|
||||
NetEndpoint = new IPEndPoint((Address as LidgrenAddress)!.NetAddress, netEndpoint.Port);
|
||||
}
|
||||
|
||||
public new static Option<LidgrenEndpoint> Parse(string endpointStr)
|
||||
|
||||
@@ -272,6 +272,9 @@ namespace Barotrauma.Networking
|
||||
[NetworkSerialize]
|
||||
public bool IsMandatory;
|
||||
|
||||
[NetworkSerialize]
|
||||
public bool IsVanilla;
|
||||
|
||||
private Md5Hash? cachedHash;
|
||||
private DateTime? cachedDateTime;
|
||||
|
||||
@@ -305,6 +308,7 @@ namespace Barotrauma.Networking
|
||||
? ugcId.StringRepresentation
|
||||
: "";
|
||||
IsMandatory = !contentPackage.Files.All(f => f is SubmarineFile);
|
||||
IsVanilla = contentPackage == ContentPackageManager.VanillaCorePackage;
|
||||
InstallTimeDiffInSeconds =
|
||||
contentPackage.InstallTime.TryUnwrap(out var installTime)
|
||||
? (uint)(installTime - referenceTime).TotalSeconds
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
public State CurrentState { get; private set; }
|
||||
|
||||
public bool UseRespawnPrompt
|
||||
public static bool UseRespawnPrompt
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -199,6 +199,7 @@ namespace Barotrauma.Networking
|
||||
public void ForceRespawn()
|
||||
{
|
||||
ResetShuttle();
|
||||
RespawnCountdownStarted = true;
|
||||
RespawnTime = DateTime.Now;
|
||||
CurrentState = State.Waiting;
|
||||
}
|
||||
@@ -274,7 +275,7 @@ namespace Barotrauma.Networking
|
||||
var powerContainer = item.GetComponent<PowerContainer>();
|
||||
if (powerContainer != null)
|
||||
{
|
||||
powerContainer.Charge = powerContainer.Capacity;
|
||||
powerContainer.Charge = powerContainer.GetCapacity();
|
||||
}
|
||||
|
||||
var door = item.GetComponent<Door>();
|
||||
|
||||
@@ -386,12 +386,17 @@ namespace Barotrauma.Networking
|
||||
|
||||
private bool autoRestart;
|
||||
|
||||
public bool IsPublic;
|
||||
|
||||
private int maxPlayers;
|
||||
|
||||
public List<SavedClientPermission> ClientPermissions { get; private set; } = new List<SavedClientPermission>();
|
||||
|
||||
[Serialize(true, IsPropertySaveable.Yes)]
|
||||
public bool IsPublic
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private int tickRate = 20;
|
||||
[Serialize(20, IsPropertySaveable.Yes)]
|
||||
public int TickRate
|
||||
@@ -522,13 +527,20 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(Barotrauma.LosMode.Opaque, IsPropertySaveable.Yes)]
|
||||
[Serialize(LosMode.Opaque, IsPropertySaveable.Yes)]
|
||||
public LosMode LosMode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(EnemyHealthBarMode.ShowAll, IsPropertySaveable.Yes)]
|
||||
public EnemyHealthBarMode ShowEnemyHealthBars
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(800, IsPropertySaveable.Yes)]
|
||||
public int LinesPerLogFile
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user