mirror of
https://github.com/ncblakely/GiantsTools
synced 2024-11-21 13:45:37 +01:00
Update SDK for 1.5.1.103.
This commit is contained in:
parent
71b675f5cb
commit
6a455cb09c
@ -1,179 +0,0 @@
|
|||||||
#include "AI/Public/Behaviors/DodgeBehavior.h"
|
|
||||||
#include "AI/Public/Components/Senses.h"
|
|
||||||
#include "AI/Public/Components/MoveEnactor.h"
|
|
||||||
#include "AI/Public/Components/PhysicsView.h"
|
|
||||||
#include "GameObject/Public/Core.h"
|
|
||||||
#include "Navigation/Public/Core.h"
|
|
||||||
#include "projectile.h"
|
|
||||||
|
|
||||||
namespace AI
|
|
||||||
{
|
|
||||||
using namespace beehive;
|
|
||||||
using namespace ECS;
|
|
||||||
using namespace GameObj;
|
|
||||||
using namespace Nav;
|
|
||||||
|
|
||||||
// TODO: Parameterize
|
|
||||||
const float DodgeStartTime = 5.0f;
|
|
||||||
const float DodgeSafeRadius = 10.0f;
|
|
||||||
|
|
||||||
static bool DodgeTargetValid(const std::optional<DodgeTarget>& target)
|
|
||||||
{
|
|
||||||
return target && !target->Projectile.expired();
|
|
||||||
}
|
|
||||||
|
|
||||||
static float GetTimeToImpact(const P3D& avoidLocation, const PhysicsView& projectilePhysics)
|
|
||||||
{
|
|
||||||
float timeToImpact = avoidLocation.Distance3D(projectilePhysics.Location) / projectilePhysics.Velocity.Length();
|
|
||||||
return timeToImpact;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void AbortDodge(DodgeBehaviorState& state, BehaviorTreeContext& context)
|
|
||||||
{
|
|
||||||
state.DodgeTarget = std::nullopt;
|
|
||||||
context.ResetResumeIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::optional<DodgeTarget> ChooseDodgeTarget(Entity* entity, Senses& senses, const PhysicsView& objectPhysics)
|
|
||||||
{
|
|
||||||
Object* aiObject = entity->GetComponent<ObjectRef>().GetObj();
|
|
||||||
|
|
||||||
std::sort(senses.KnownEnemyProjectiles.begin(), senses.KnownEnemyProjectiles.end(), [&objectPhysics](const auto& e1, const auto& e2)
|
|
||||||
{
|
|
||||||
const PhysicsView& entity1Physics = e1->GetComponent<PhysicsView>();
|
|
||||||
const PhysicsView& entity2Physics = e2->GetComponent<PhysicsView>();
|
|
||||||
|
|
||||||
return entity1Physics.Location.DistanceSquared3D(objectPhysics.Location) < entity2Physics.Location.DistanceSquared3D(objectPhysics.Location);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const auto& projectile : senses.KnownEnemyProjectiles)
|
|
||||||
{
|
|
||||||
Object* projectileObject = projectile->GetComponent<ObjectRef>().GetObj();
|
|
||||||
const PhysicsView& projectilePhysics = projectile->GetComponent<PhysicsView>();
|
|
||||||
ObjSpecProjectile* spec = ObjSpecProjectile::Cast(projectileObject);
|
|
||||||
|
|
||||||
if (!spec->ground_collision)
|
|
||||||
{
|
|
||||||
TRACE_BEHAVIOR("Projectile is not expected to collide with ground");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PROJ_Def* def = PROJ_DefGet(spec->projectile_index);
|
|
||||||
|
|
||||||
float speed = projectileObject->velocity.Length();
|
|
||||||
float timeToImpact = GetTimeToImpact(spec->ground_collision_position, projectilePhysics);
|
|
||||||
if (timeToImpact > DodgeStartTime)
|
|
||||||
{
|
|
||||||
TRACE_BEHAVIOR("Projectile time to impact of {0} is above threshold", timeToImpact);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
P3D aiProjectedLocation = aiObject->location + (aiObject->velocity * timeToImpact);
|
|
||||||
if (aiProjectedLocation.DistanceSquared3D(spec->ground_collision_position) < Squared(def->fardist))
|
|
||||||
{
|
|
||||||
return DodgeTarget{ projectile, def->fardist, spec->ground_collision_position };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DodgeBehavior::ShouldStartDodge(Entity* entity, Object* object, BehaviorTreeContext& context)
|
|
||||||
{
|
|
||||||
if (!entity->HasComponent<Senses, PhysicsView>())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!World->navMesh)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
auto& state = entity->GetComponent<DodgeBehaviorState>();
|
|
||||||
if (!DodgeTargetValid(state.DodgeTarget))
|
|
||||||
{
|
|
||||||
// Make sure invalid targets have been cleared out
|
|
||||||
state.DodgeTarget = std::nullopt;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Already have dodge in progress
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto [senses, physicsView] = entity->GetComponent<Senses, PhysicsView>();
|
|
||||||
|
|
||||||
state.DodgeTarget = ChooseDodgeTarget(entity, senses, physicsView);
|
|
||||||
if (!state.DodgeTarget)
|
|
||||||
{
|
|
||||||
// No nearby threats
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
float angleToCenter = dirfcalcp3d(&state.DodgeTarget->AvoidLocation, &physicsView.Location);
|
|
||||||
|
|
||||||
float sa, ca;
|
|
||||||
calc_sincosd(dirfadjust(angleToCenter), &sa, &ca);
|
|
||||||
P3D wantLoc = state.DodgeTarget->AvoidLocation;
|
|
||||||
wantLoc.x += (state.DodgeTarget->ImpactRadius + DodgeSafeRadius) * ca;
|
|
||||||
wantLoc.y += (state.DodgeTarget->ImpactRadius + DodgeSafeRadius) * sa;
|
|
||||||
|
|
||||||
MoveGoalParams params;
|
|
||||||
params.Speed = MoveGoalSpeed::Fast;
|
|
||||||
std::shared_ptr<Path> path = PathUtil::GetPath(entity, World->navMesh.get(), physicsView.Location, wantLoc, ¶ms);
|
|
||||||
if (!path)
|
|
||||||
{
|
|
||||||
TRACE_BEHAVIOR("Couldn't path away from projectile");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
TRACE_BEHAVIOR("Dodging to location {0} {1} {2}, est. impact time {3}",
|
|
||||||
wantLoc.x,
|
|
||||||
wantLoc.y,
|
|
||||||
wantLoc.z,
|
|
||||||
GetTimeToImpact(state.DodgeTarget->AvoidLocation, state.DodgeTarget->Projectile.lock()->GetComponent<PhysicsView>()));
|
|
||||||
|
|
||||||
PathUtil::SetPath(entity, path);
|
|
||||||
|
|
||||||
context.SetChainingEnabled(false);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Status DodgeBehavior::MoveToDodgeLocation(Entity* entity, Object* object, BehaviorTreeContext& context)
|
|
||||||
{
|
|
||||||
auto& state = entity->GetComponent<DodgeBehaviorState>();
|
|
||||||
const auto& moveEnactor = entity->GetComponent<MoveEnactor>();
|
|
||||||
|
|
||||||
if (!DodgeTargetValid(state.DodgeTarget)
|
|
||||||
|| !moveEnactor.Path)
|
|
||||||
{
|
|
||||||
TRACE_BEHAVIOR("Dodge target no longer valid; canceling");
|
|
||||||
AbortDodge(state, context);
|
|
||||||
return Status::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& aiPhysics = entity->GetComponent<PhysicsView>();
|
|
||||||
|
|
||||||
float distanceFromImpactSq = state.DodgeTarget->AvoidLocation.DistanceSquared3D(aiPhysics.Location);
|
|
||||||
if (distanceFromImpactSq > Squared(state.DodgeTarget->ImpactRadius + DodgeSafeRadius))
|
|
||||||
{
|
|
||||||
// Got far enough away somehow, we can reset
|
|
||||||
TRACE_BEHAVIOR("AI is {0} units away from impact location with still active dodge behavior; canceling", sqrt(distanceFromImpactSq));
|
|
||||||
AbortDodge(state, context);
|
|
||||||
return Status::SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Status::RUNNING;
|
|
||||||
}
|
|
||||||
|
|
||||||
InnerBehaviorTree DodgeBehavior::Build(ECS::Entity* entity)
|
|
||||||
{
|
|
||||||
assert(entity);
|
|
||||||
|
|
||||||
DodgeBehaviorState& state = entity->AddComponent<DodgeBehaviorState>();
|
|
||||||
|
|
||||||
return BehaviorTreeBuilder()
|
|
||||||
.sequence()
|
|
||||||
.leaf(CreateLeaf(entity, ShouldStartDodge))
|
|
||||||
.leaf(CreateLeaf(entity, MoveToDodgeLocation))
|
|
||||||
.end()
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
7
Sdk/Include/Core/Public/EventListenerResult.h
Normal file
7
Sdk/Include/Core/Public/EventListenerResult.h
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
enum class EventListenerResult
|
||||||
|
{
|
||||||
|
Continue,
|
||||||
|
Cancel
|
||||||
|
};
|
@ -3,11 +3,13 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <guiddef.h>
|
#include <guiddef.h>
|
||||||
|
|
||||||
|
#include "EventListenerResult.h"
|
||||||
|
|
||||||
template<typename TEventType, typename TEvent>
|
template<typename TEventType, typename TEvent>
|
||||||
struct IEventSource
|
struct IEventSource
|
||||||
{
|
{
|
||||||
virtual ~IEventSource() = default;
|
virtual ~IEventSource() = default;
|
||||||
|
|
||||||
virtual GUID Listen(TEventType event, std::function<void(const TEvent&)> function) noexcept = 0;
|
virtual GUID Listen(TEventType event, std::function<EventListenerResult(const TEvent&)> function) noexcept = 0;
|
||||||
virtual void Unlisten(TEventType event, GUID uuid) noexcept = 0;
|
virtual void Unlisten(TEventType event, GUID uuid) noexcept = 0;
|
||||||
};
|
};
|
@ -7,9 +7,6 @@
|
|||||||
enum class NetPlayerState;
|
enum class NetPlayerState;
|
||||||
enum class GameTeam;
|
enum class GameTeam;
|
||||||
|
|
||||||
// {770DEBD3-165D-4340-829D-5262F473FBE3}
|
|
||||||
inline const GUID IID_ITextLookupService = { 0x770debd3, 0x165d, 0x4340, 0x82, 0x9d, 0x52, 0x62, 0xf4, 0x73, 0xfb, 0xe3 };
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service providing localization of text placeholders and friendly-name mappings of common enums.
|
/// Service providing localization of text placeholders and friendly-name mappings of common enums.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -46,6 +46,7 @@ struct ChatMessageEvent : GameServerEvent
|
|||||||
|
|
||||||
tstring_view message{};
|
tstring_view message{};
|
||||||
PlayerIndex senderIndex{};
|
PlayerIndex senderIndex{};
|
||||||
|
ChatFlag flags{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct WorldLoadedEvent : GameServerEvent
|
struct WorldLoadedEvent : GameServerEvent
|
||||||
|
@ -8,9 +8,6 @@
|
|||||||
#include "GameServerEvents.h"
|
#include "GameServerEvents.h"
|
||||||
#include "NetCommon.h"
|
#include "NetCommon.h"
|
||||||
|
|
||||||
// {B2D67EE7-8063-488F-B3B9-E7DA675CB752}
|
|
||||||
inline const GUID IID_IGameServer = { 0xb2d67ee7, 0x8063, 0x488f, 0xb3, 0xb9, 0xe7, 0xda, 0x67, 0x5c, 0xb7, 0x52 };
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines an API for communicating with the game server.
|
/// Defines an API for communicating with the game server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -31,15 +28,24 @@ DEFINE_SERVICE_MULTI("{B2D67EE7-8063-488F-B3B9-E7DA675CB752}", IGameServer, IEve
|
|||||||
/// Bans the player at the specified index.
|
/// Bans the player at the specified index.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="index">The player index.</param>
|
/// <param name="index">The player index.</param>
|
||||||
/// <returns></returns>
|
|
||||||
virtual void BanPlayer(int index) = 0;
|
virtual void BanPlayer(int index) = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the IP address from the ban list.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">The IP address.</param>
|
||||||
|
virtual void UnbanPlayer(const IPAddress & ipAddress) = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the IP addresses that are currently banned.
|
||||||
|
/// </summary>
|
||||||
|
virtual const std::vector<IPAddress> GetBans() const = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Kicks the player at the specified index.
|
/// Kicks the player at the specified index.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="index">The player index.</param>
|
/// <param name="index">The player index.</param>
|
||||||
/// <param name="reason">The reason for kicking the player.</param>
|
/// <param name="reason">The reason for kicking the player.</param>
|
||||||
/// <returns></returns>
|
|
||||||
virtual void KickPlayer(int index, KickReason reason) = 0;
|
virtual void KickPlayer(int index, KickReason reason) = 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -72,6 +78,4 @@ DEFINE_SERVICE_MULTI("{B2D67EE7-8063-488F-B3B9-E7DA675CB752}", IGameServer, IEve
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="gameDetails">The game details.</param>
|
/// <param name="gameDetails">The game details.</param>
|
||||||
virtual void ChangeGameDetails(const NetGameDetails& gameDetails) = 0;
|
virtual void ChangeGameDetails(const NetGameDetails& gameDetails) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DECLSPEC_UUID("{B2D67EE7-8063-488F-B3B9-E7DA675CB752}") IGameServer;
|
|
@ -7,9 +7,6 @@
|
|||||||
|
|
||||||
typedef std::future<std::vector<ServerInfoResponse>> ServerInfoFuture;
|
typedef std::future<std::vector<ServerInfoResponse>> ServerInfoFuture;
|
||||||
|
|
||||||
// {EE129A81-0A86-49C4-8D23-A771A7350952}
|
|
||||||
inline const GUID IID_IGiantsApiClient = { 0xee129a81, 0xa86, 0x49c4, 0x8d, 0x23, 0xa7, 0x71, 0xa7, 0x35, 0x9, 0x52 };
|
|
||||||
|
|
||||||
DEFINE_SERVICE("{EE129A81-0A86-49C4-8D23-A771A7350952}", IGiantsApiClient)
|
DEFINE_SERVICE("{EE129A81-0A86-49C4-8D23-A771A7350952}", IGiantsApiClient)
|
||||||
{
|
{
|
||||||
virtual ~IGiantsApiClient() = default;
|
virtual ~IGiantsApiClient() = default;
|
||||||
@ -17,6 +14,4 @@ DEFINE_SERVICE("{EE129A81-0A86-49C4-8D23-A771A7350952}", IGiantsApiClient)
|
|||||||
virtual void DeleteServerInformationAsync(tstring_view gameName, int hostPort) = 0;
|
virtual void DeleteServerInformationAsync(tstring_view gameName, int hostPort) = 0;
|
||||||
virtual ServerInfoFuture GetServerInformationAsync() = 0;
|
virtual ServerInfoFuture GetServerInformationAsync() = 0;
|
||||||
virtual void PostServerInformationAsync(const nlohmann::json& requestBody) = 0;
|
virtual void PostServerInformationAsync(const nlohmann::json& requestBody) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DECLSPEC_UUID("{EE129A81-0A86-49C4-8D23-A771A7350952}") IGiantsApiClient;
|
|
82
Sdk/Include/Network/Public/IPAddress.h
Normal file
82
Sdk/Include/Network/Public/IPAddress.h
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
class IPAddress
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
int operator[](int index) const
|
||||||
|
{
|
||||||
|
if (index < countof(m_octets) && index > 0)
|
||||||
|
{
|
||||||
|
return m_octets[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::invalid_argument("Invalid octet index.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator==(const IPAddress& right) const
|
||||||
|
{
|
||||||
|
return (m_octets[0] == right.m_octets[0] &&
|
||||||
|
m_octets[1] == right.m_octets[1] &&
|
||||||
|
m_octets[2] == right.m_octets[2] &&
|
||||||
|
m_octets[3] == right.m_octets[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator!= (const IPAddress& right) const
|
||||||
|
{
|
||||||
|
return !operator==(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator<(const IPAddress& right) const
|
||||||
|
{
|
||||||
|
return ToInt() < right.ToInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator>(const IPAddress& right) const
|
||||||
|
{
|
||||||
|
return right.operator<(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static IPAddress FromString(const tstring_view str)
|
||||||
|
{
|
||||||
|
IPAddress ipAddress;
|
||||||
|
|
||||||
|
char octetBuff[4]{};
|
||||||
|
std::string tmp(str);
|
||||||
|
std::istringstream iss(tmp);
|
||||||
|
|
||||||
|
int octetsRead = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
iss.getline(octetBuff, sizeof(octetBuff), '.');
|
||||||
|
|
||||||
|
if (iss.gcount() <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
int octet = atoi(octetBuff);
|
||||||
|
if (octet > 255 || octet < 0)
|
||||||
|
{
|
||||||
|
throw std::invalid_argument(fmt::format("Octet '{0}' is invalid.", octet));
|
||||||
|
}
|
||||||
|
|
||||||
|
ipAddress.m_octets[octetsRead] = octet;
|
||||||
|
octetsRead++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ToString() const
|
||||||
|
{
|
||||||
|
return fmt::sprintf("%d.%d.%d.%d", m_octets[0], m_octets[1], m_octets[2], m_octets[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint ToInt() const
|
||||||
|
{
|
||||||
|
return ((m_octets[0] << 24) + (m_octets[1] << 16) + (m_octets[2] << 8) + m_octets[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int m_octets[4]{};
|
||||||
|
};
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "IPAddress.h"
|
||||||
|
|
||||||
typedef int PlayerIndex;
|
typedef int PlayerIndex;
|
||||||
typedef int PlayerTeamIndex;
|
typedef int PlayerTeamIndex;
|
||||||
|
|
||||||
@ -81,13 +83,14 @@ enum class GameTeam
|
|||||||
struct PlayerInfo
|
struct PlayerInfo
|
||||||
{
|
{
|
||||||
PlayerIndex index = 0;
|
PlayerIndex index = 0;
|
||||||
std::string name;
|
tstring name;
|
||||||
float score = 0;
|
float score = 0;
|
||||||
float deaths = 0;
|
float deaths = 0;
|
||||||
int ping = 0;
|
int ping = 0;
|
||||||
PlayerTeamIndex team = 0;
|
PlayerTeamIndex team = 0;
|
||||||
bool host = false;
|
bool host = false;
|
||||||
NetPlayerState state = NetPlayerState::None;
|
NetPlayerState state = NetPlayerState::None;
|
||||||
|
IPAddress ipAddress;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct NetGameSettings
|
struct NetGameSettings
|
||||||
@ -105,6 +108,7 @@ struct NetGameSettings
|
|||||||
bool vimpsDisabled = false;
|
bool vimpsDisabled = false;
|
||||||
bool weaponAvailabilityModified = false;
|
bool weaponAvailabilityModified = false;
|
||||||
int worldId = 0;
|
int worldId = 0;
|
||||||
|
tstring sessionName;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct NetGameDetails
|
struct NetGameDetails
|
||||||
@ -112,7 +116,7 @@ struct NetGameDetails
|
|||||||
// User-adjustable settings for the current game.
|
// User-adjustable settings for the current game.
|
||||||
NetGameSettings settings;
|
NetGameSettings settings;
|
||||||
|
|
||||||
std::string worldName;
|
tstring worldName;
|
||||||
std::string teamTypeName;
|
tstring teamTypeName;
|
||||||
std::string gameTypeName;
|
tstring gameTypeName;
|
||||||
};
|
};
|
@ -28,6 +28,7 @@ ServerDialog::~ServerDialog()
|
|||||||
pGameServer->Unlisten(GameServerEventType::PlayerConnected, m_playerConnectedEventHandle);
|
pGameServer->Unlisten(GameServerEventType::PlayerConnected, m_playerConnectedEventHandle);
|
||||||
pGameServer->Unlisten(GameServerEventType::PlayerDisconnected, m_playerDisconnectedEventHandle);
|
pGameServer->Unlisten(GameServerEventType::PlayerDisconnected, m_playerDisconnectedEventHandle);
|
||||||
pGameServer->Unlisten(GameServerEventType::ChatMessage, m_playerChatMessageHandle);
|
pGameServer->Unlisten(GameServerEventType::ChatMessage, m_playerChatMessageHandle);
|
||||||
|
pGameServer->Unlisten(GameServerEventType::WorldLoaded, m_worldLoadedHandle);
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user