Add chat callback (#265)
This commit is contained in:
parent
0a852c6431
commit
144ba7efd3
@ -13,22 +13,33 @@ namespace Components
|
|||||||
std::mutex Chat::AccessMutex;
|
std::mutex Chat::AccessMutex;
|
||||||
std::unordered_set<std::uint64_t> Chat::MuteList;
|
std::unordered_set<std::uint64_t> Chat::MuteList;
|
||||||
|
|
||||||
const char* Chat::EvaluateSay(char* text, Game::gentity_t* player)
|
bool Chat::CanAddCallback = true;
|
||||||
|
std::vector<Scripting::Function> Chat::SayCallbacks;
|
||||||
|
|
||||||
|
const char* Chat::EvaluateSay(char* text, Game::gentity_t* player, int mode)
|
||||||
{
|
{
|
||||||
Chat::SendChat = true;
|
SendChat = true;
|
||||||
|
|
||||||
|
const auto _0 = gsl::finally([]
|
||||||
|
{
|
||||||
|
CanAddCallback = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent callbacks from adding a new callback (would make the vector iterator invalid)
|
||||||
|
CanAddCallback = false;
|
||||||
|
|
||||||
if (text[1] == '/')
|
if (text[1] == '/')
|
||||||
{
|
{
|
||||||
Chat::SendChat = false;
|
SendChat = false;
|
||||||
text[1] = text[0];
|
text[1] = text[0];
|
||||||
++text;
|
++text;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_lock<std::mutex> lock(Chat::AccessMutex);
|
std::unique_lock lock(AccessMutex);
|
||||||
if (Chat::MuteList.contains(Game::svs_clients[player->s.number].steamID))
|
if (MuteList.contains(Game::svs_clients[player->s.number].steamID))
|
||||||
{
|
{
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
Chat::SendChat = false;
|
SendChat = false;
|
||||||
Game::SV_GameSendServerCommand(player->s.number, Game::SV_CMD_CAN_IGNORE,
|
Game::SV_GameSendServerCommand(player->s.number, Game::SV_CMD_CAN_IGNORE,
|
||||||
Utils::String::VA("%c \"You are muted\"", 0x65));
|
Utils::String::VA("%c \"You are muted\"", 0x65));
|
||||||
}
|
}
|
||||||
@ -39,9 +50,17 @@ namespace Components
|
|||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Chat::sv_disableChat.get<bool>())
|
for (const auto& callback : SayCallbacks)
|
||||||
{
|
{
|
||||||
Chat::SendChat = false;
|
if (!ChatCallback(player, callback.getPos(), (text + 1), mode))
|
||||||
|
{
|
||||||
|
SendChat = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sv_disableChat.get<bool>())
|
||||||
|
{
|
||||||
|
SendChat = false;
|
||||||
Game::SV_GameSendServerCommand(player->s.number, Game::SV_CMD_CAN_IGNORE,
|
Game::SV_GameSendServerCommand(player->s.number, Game::SV_CMD_CAN_IGNORE,
|
||||||
Utils::String::VA("%c \"Chat is disabled\"", 0x65));
|
Utils::String::VA("%c \"Chat is disabled\"", 0x65));
|
||||||
}
|
}
|
||||||
@ -59,21 +78,22 @@ namespace Components
|
|||||||
{
|
{
|
||||||
__asm
|
__asm
|
||||||
{
|
{
|
||||||
mov eax, [esp + 100h + 10h]
|
mov eax, [esp + 0x100 + 0x10]
|
||||||
|
|
||||||
push eax
|
push eax
|
||||||
pushad
|
pushad
|
||||||
|
|
||||||
push [esp + 100h + 28h]
|
push [esp + 0x100 + 0x30] // mode
|
||||||
push eax
|
push [esp + 0x100 + 0x2C] // player
|
||||||
call Chat::EvaluateSay
|
push eax // text
|
||||||
add esp, 8h
|
call EvaluateSay
|
||||||
|
add esp, 0xC
|
||||||
|
|
||||||
mov [esp + 20h], eax
|
mov [esp + 0x20], eax
|
||||||
popad
|
popad
|
||||||
pop eax
|
pop eax
|
||||||
|
|
||||||
mov [esp + 100h + 10h], eax
|
mov [esp + 0x100 + 0x10], eax
|
||||||
|
|
||||||
jmp PlayerName::CleanStrStub
|
jmp PlayerName::CleanStrStub
|
||||||
}
|
}
|
||||||
@ -87,7 +107,7 @@ namespace Components
|
|||||||
push eax
|
push eax
|
||||||
|
|
||||||
xor eax, eax
|
xor eax, eax
|
||||||
mov al, Chat::SendChat
|
mov al, SendChat
|
||||||
|
|
||||||
test al, al
|
test al, al
|
||||||
jnz return
|
jnz return
|
||||||
@ -224,11 +244,11 @@ namespace Components
|
|||||||
|
|
||||||
void Chat::MuteClient(const Game::client_t* client)
|
void Chat::MuteClient(const Game::client_t* client)
|
||||||
{
|
{
|
||||||
std::unique_lock<std::mutex> lock(Chat::AccessMutex);
|
std::unique_lock lock(AccessMutex);
|
||||||
|
|
||||||
if (!Chat::MuteList.contains(client->steamID))
|
if (!MuteList.contains(client->steamID))
|
||||||
{
|
{
|
||||||
Chat::MuteList.insert(client->steamID);
|
MuteList.insert(client->steamID);
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
|
|
||||||
Logger::Print("{} was muted\n", client->name);
|
Logger::Print("{} was muted\n", client->name);
|
||||||
@ -245,7 +265,7 @@ namespace Components
|
|||||||
|
|
||||||
void Chat::UnmuteClient(const Game::client_t* client)
|
void Chat::UnmuteClient(const Game::client_t* client)
|
||||||
{
|
{
|
||||||
Chat::UnmuteInternal(client->steamID);
|
UnmuteInternal(client->steamID);
|
||||||
|
|
||||||
Logger::Print("{} was unmuted\n", client->name);
|
Logger::Print("{} was unmuted\n", client->name);
|
||||||
Game::SV_GameSendServerCommand(client->gentity->s.number, Game::SV_CMD_CAN_IGNORE,
|
Game::SV_GameSendServerCommand(client->gentity->s.number, Game::SV_CMD_CAN_IGNORE,
|
||||||
@ -254,12 +274,12 @@ namespace Components
|
|||||||
|
|
||||||
void Chat::UnmuteInternal(const std::uint64_t id, bool everyone)
|
void Chat::UnmuteInternal(const std::uint64_t id, bool everyone)
|
||||||
{
|
{
|
||||||
std::unique_lock<std::mutex> lock(Chat::AccessMutex);
|
std::unique_lock lock(AccessMutex);
|
||||||
|
|
||||||
if (everyone)
|
if (everyone)
|
||||||
Chat::MuteList.clear();
|
MuteList.clear();
|
||||||
else
|
else
|
||||||
Chat::MuteList.erase(id);
|
MuteList.erase(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Chat::AddChatCommands()
|
void Chat::AddChatCommands()
|
||||||
@ -282,7 +302,7 @@ namespace Components
|
|||||||
const auto* client = Game::SV_GetPlayerByNum();
|
const auto* client = Game::SV_GetPlayerByNum();
|
||||||
if (client != nullptr)
|
if (client != nullptr)
|
||||||
{
|
{
|
||||||
Chat::MuteClient(client);
|
MuteClient(client);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -305,27 +325,91 @@ namespace Components
|
|||||||
|
|
||||||
if (client != nullptr)
|
if (client != nullptr)
|
||||||
{
|
{
|
||||||
Chat::UnmuteClient(client);
|
UnmuteClient(client);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std::strcmp(params->get(1), "all") == 0)
|
if (std::strcmp(params->get(1), "all") == 0)
|
||||||
{
|
{
|
||||||
Logger::Print("All players were unmuted\n");
|
Logger::Print("All players were unmuted\n");
|
||||||
Chat::UnmuteInternal(0, true);
|
UnmuteInternal(0, true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
const auto steamId = std::strtoull(params->get(1), nullptr, 16);
|
const auto steamId = std::strtoull(params->get(1), nullptr, 16);
|
||||||
Chat::UnmuteInternal(steamId);
|
UnmuteInternal(steamId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int Chat::GetCallbackReturn()
|
||||||
|
{
|
||||||
|
if (Game::scrVmPub->inparamcount == 0)
|
||||||
|
{
|
||||||
|
// Nothing. Let's not mute the player
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Game::Scr_ClearOutParams();
|
||||||
|
Game::scrVmPub->outparamcount = Game::scrVmPub->inparamcount;
|
||||||
|
Game::scrVmPub->inparamcount = 0;
|
||||||
|
|
||||||
|
const auto* result = &Game::scrVmPub->top[1 - Game::scrVmPub->outparamcount];
|
||||||
|
|
||||||
|
if (result->type != Game::scrParamType_t::VAR_INTEGER)
|
||||||
|
{
|
||||||
|
// Garbage was returned
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result->u.intValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Chat::ChatCallback(Game::gentity_s* self, const char* codePos, const char* message, int mode)
|
||||||
|
{
|
||||||
|
const auto entityId = Game::Scr_GetEntityId(self->s.number, 0);
|
||||||
|
|
||||||
|
Game::Scr_AddInt(mode);
|
||||||
|
Game::Scr_AddString(message);
|
||||||
|
|
||||||
|
Game::VariableValue value;
|
||||||
|
value.type = Game::scrParamType_t::VAR_OBJECT;
|
||||||
|
value.u.uintValue = entityId;
|
||||||
|
|
||||||
|
Game::AddRefToValue(value.type, value.u);
|
||||||
|
const auto localId = Game::AllocThread(entityId);
|
||||||
|
|
||||||
|
const auto result = Game::VM_Execute_0(localId, codePos, 2);
|
||||||
|
Game::RemoveRefToObject(result);
|
||||||
|
|
||||||
|
return GetCallbackReturn();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Chat::AddScriptFunctions()
|
||||||
|
{
|
||||||
|
Script::AddFunction("OnPlayerSay", [] // gsc: OnPlayerSay(<function>)
|
||||||
|
{
|
||||||
|
if (Game::Scr_GetNumParam() != 1)
|
||||||
|
{
|
||||||
|
Game::Scr_Error("^1OnPlayerSay: Needs one function pointer!\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CanAddCallback)
|
||||||
|
{
|
||||||
|
Game::Scr_Error("^1OnPlayerSay: Cannot add a callback in this context");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* func = Script::GetCodePosForParam(0);
|
||||||
|
SayCallbacks.emplace_back(func);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Chat::Chat()
|
Chat::Chat()
|
||||||
{
|
{
|
||||||
cg_chatWidth = Dvar::Register<int>("cg_chatWidth", 52, 1, std::numeric_limits<int>::max(), Game::DVAR_ARCHIVE, "The normalized maximum width of a chat message");
|
cg_chatWidth = Dvar::Register<int>("cg_chatWidth", 52, 1, std::numeric_limits<int>::max(), Game::DVAR_ARCHIVE, "The normalized maximum width of a chat message");
|
||||||
Chat::sv_disableChat = Dvar::Register<bool>("sv_disableChat", false, Game::dvar_flag::DVAR_NONE, "Disable chat messages from clients");
|
sv_disableChat = Dvar::Register<bool>("sv_disableChat", false, Game::dvar_flag::DVAR_NONE, "Disable chat messages from clients");
|
||||||
Scheduler::Once(Chat::AddChatCommands, Scheduler::Pipeline::MAIN);
|
Scheduler::Once(Chat::AddChatCommands, Scheduler::Pipeline::MAIN);
|
||||||
|
|
||||||
// Intercept chat sending
|
// Intercept chat sending
|
||||||
@ -335,5 +419,13 @@ namespace Components
|
|||||||
|
|
||||||
// Change logic that does word splitting with new lines for chat messages to support fonticons
|
// Change logic that does word splitting with new lines for chat messages to support fonticons
|
||||||
Utils::Hook(0x592E10, CG_AddToTeamChat_Stub, HOOK_JUMP).install()->quick();
|
Utils::Hook(0x592E10, CG_AddToTeamChat_Stub, HOOK_JUMP).install()->quick();
|
||||||
|
|
||||||
|
AddScriptFunctions();
|
||||||
|
|
||||||
|
// Avoid duplicates
|
||||||
|
Events::OnVMShutdown([]
|
||||||
|
{
|
||||||
|
SayCallbacks.clear();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,10 @@ namespace Components
|
|||||||
static std::mutex AccessMutex;
|
static std::mutex AccessMutex;
|
||||||
static std::unordered_set<std::uint64_t> MuteList;
|
static std::unordered_set<std::uint64_t> MuteList;
|
||||||
|
|
||||||
static const char* EvaluateSay(char* text, Game::gentity_t* player);
|
static bool CanAddCallback; // ClientCommand & GSC thread are the same
|
||||||
|
static std::vector<Scripting::Function> SayCallbacks;
|
||||||
|
|
||||||
|
static const char* EvaluateSay(char* text, Game::gentity_t* player, int mode);
|
||||||
|
|
||||||
static void PreSayStub();
|
static void PreSayStub();
|
||||||
static void PostSayStub();
|
static void PostSayStub();
|
||||||
@ -34,5 +37,9 @@ namespace Components
|
|||||||
static void UnmuteClient(const Game::client_t* client);
|
static void UnmuteClient(const Game::client_t* client);
|
||||||
static void UnmuteInternal(const std::uint64_t id, bool everyone = false);
|
static void UnmuteInternal(const std::uint64_t id, bool everyone = false);
|
||||||
static void AddChatCommands();
|
static void AddChatCommands();
|
||||||
|
|
||||||
|
static int GetCallbackReturn();
|
||||||
|
static int ChatCallback(Game::gentity_s* self, const char* codePos, const char* message, int mode);
|
||||||
|
static void AddScriptFunctions();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@ namespace Components
|
|||||||
|
|
||||||
void MapRotation::AddMapRotationCommands()
|
void MapRotation::AddMapRotationCommands()
|
||||||
{
|
{
|
||||||
Command::Add("AddMap", [](Command::Params* params)
|
Command::Add("addMap", [](Command::Params* params)
|
||||||
{
|
{
|
||||||
if (params->size() < 2)
|
if (params->size() < 2)
|
||||||
{
|
{
|
||||||
@ -132,7 +132,7 @@ namespace Components
|
|||||||
DedicatedRotation.addEntry("map", params->get(1));
|
DedicatedRotation.addEntry("map", params->get(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
Command::Add("AddGametype", [](Command::Params* params)
|
Command::Add("addGametype", [](Command::Params* params)
|
||||||
{
|
{
|
||||||
if (params->size() < 2)
|
if (params->size() < 2)
|
||||||
{
|
{
|
||||||
|
@ -15,6 +15,8 @@ namespace Components
|
|||||||
|
|
||||||
static Game::client_t* GetClient(const Game::gentity_t* gentity);
|
static Game::client_t* GetClient(const Game::gentity_t* gentity);
|
||||||
|
|
||||||
|
static const char* GetCodePosForParam(int index);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static std::string ScriptName;
|
static std::string ScriptName;
|
||||||
static std::vector<int> ScriptHandles;
|
static std::vector<int> ScriptHandles;
|
||||||
@ -54,7 +56,6 @@ namespace Components
|
|||||||
|
|
||||||
static unsigned int SetExpFogStub();
|
static unsigned int SetExpFogStub();
|
||||||
|
|
||||||
static const char* GetCodePosForParam(int index);
|
|
||||||
static void GetReplacedPos(const char* pos);
|
static void GetReplacedPos(const char* pos);
|
||||||
static void SetReplacedPos(const char* what, const char* with);
|
static void SetReplacedPos(const char* what, const char* with);
|
||||||
static void VMExecuteInternalStub();
|
static void VMExecuteInternalStub();
|
||||||
|
@ -24,6 +24,9 @@ namespace Game
|
|||||||
|
|
||||||
AddRefToObject_t AddRefToObject = AddRefToObject_t(0x61C360);
|
AddRefToObject_t AddRefToObject = AddRefToObject_t(0x61C360);
|
||||||
AllocObject_t AllocObject = AllocObject_t(0x434320);
|
AllocObject_t AllocObject = AllocObject_t(0x434320);
|
||||||
|
AddRefToValue_t AddRefToValue = AddRefToValue_t(0x482740);
|
||||||
|
AllocThread_t AllocThread = AllocThread_t(0x4F78C0);
|
||||||
|
VM_Execute_0_t VM_Execute_0 = VM_Execute_0_t(0x6222A0);
|
||||||
|
|
||||||
AngleVectors_t AngleVectors = AngleVectors_t(0x4691A0);
|
AngleVectors_t AngleVectors = AngleVectors_t(0x4691A0);
|
||||||
|
|
||||||
@ -282,6 +285,7 @@ namespace Game
|
|||||||
Scr_GetInt_t Scr_GetInt = Scr_GetInt_t(0x4F31D0);
|
Scr_GetInt_t Scr_GetInt = Scr_GetInt_t(0x4F31D0);
|
||||||
Scr_GetObject_t Scr_GetObject = Scr_GetObject_t(0x462100);
|
Scr_GetObject_t Scr_GetObject = Scr_GetObject_t(0x462100);
|
||||||
Scr_GetNumParam_t Scr_GetNumParam = Scr_GetNumParam_t(0x4B0E90);
|
Scr_GetNumParam_t Scr_GetNumParam = Scr_GetNumParam_t(0x4B0E90);
|
||||||
|
Scr_GetEntityId_t Scr_GetEntityId = Scr_GetEntityId_t(0x4165E0);
|
||||||
|
|
||||||
Scr_ExecThread_t Scr_ExecThread = Scr_ExecThread_t(0x4AD0B0);
|
Scr_ExecThread_t Scr_ExecThread = Scr_ExecThread_t(0x4AD0B0);
|
||||||
Scr_FreeThread_t Scr_FreeThread = Scr_FreeThread_t(0x4BD320);
|
Scr_FreeThread_t Scr_FreeThread = Scr_FreeThread_t(0x4BD320);
|
||||||
|
@ -28,6 +28,15 @@ namespace Game
|
|||||||
typedef unsigned int(__cdecl * AllocObject_t)();
|
typedef unsigned int(__cdecl * AllocObject_t)();
|
||||||
extern AllocObject_t AllocObject;
|
extern AllocObject_t AllocObject;
|
||||||
|
|
||||||
|
typedef void(__cdecl * AddRefToValue_t)(int type, VariableUnion u);
|
||||||
|
extern AddRefToValue_t AddRefToValue;
|
||||||
|
|
||||||
|
typedef unsigned int(__cdecl * AllocThread_t)(unsigned int self);
|
||||||
|
extern AllocThread_t AllocThread;
|
||||||
|
|
||||||
|
typedef unsigned int(__cdecl * VM_Execute_0_t)(unsigned int localId, const char* pos, unsigned int paramcount);
|
||||||
|
extern VM_Execute_0_t VM_Execute_0;
|
||||||
|
|
||||||
typedef void(__cdecl * AngleVectors_t)(float *angles, float *forward, float *right, float *up);
|
typedef void(__cdecl * AngleVectors_t)(float *angles, float *forward, float *right, float *up);
|
||||||
extern AngleVectors_t AngleVectors;
|
extern AngleVectors_t AngleVectors;
|
||||||
|
|
||||||
@ -735,12 +744,18 @@ namespace Game
|
|||||||
typedef unsigned int(__cdecl * Scr_GetNumParam_t)();
|
typedef unsigned int(__cdecl * Scr_GetNumParam_t)();
|
||||||
extern Scr_GetNumParam_t Scr_GetNumParam;
|
extern Scr_GetNumParam_t Scr_GetNumParam;
|
||||||
|
|
||||||
|
typedef unsigned int(__cdecl * Scr_GetEntityId_t)(int entnum, unsigned int classnum);
|
||||||
|
extern Scr_GetEntityId_t Scr_GetEntityId;
|
||||||
|
|
||||||
typedef int(__cdecl * Scr_GetFunctionHandle_t)(const char* filename, const char* name);
|
typedef int(__cdecl * Scr_GetFunctionHandle_t)(const char* filename, const char* name);
|
||||||
extern Scr_GetFunctionHandle_t Scr_GetFunctionHandle;
|
extern Scr_GetFunctionHandle_t Scr_GetFunctionHandle;
|
||||||
|
|
||||||
typedef int(__cdecl * Scr_ExecThread_t)(int, int);
|
typedef int(__cdecl * Scr_ExecThread_t)(int, int);
|
||||||
extern Scr_ExecThread_t Scr_ExecThread;
|
extern Scr_ExecThread_t Scr_ExecThread;
|
||||||
|
|
||||||
|
typedef int(__cdecl * Scr_ExecEntThread_t)(gentity_s* ent, int handle, unsigned int paramcount);
|
||||||
|
extern Scr_ExecEntThread_t Scr_ExecEntThread;
|
||||||
|
|
||||||
typedef int(__cdecl * Scr_FreeThread_t)(int);
|
typedef int(__cdecl * Scr_FreeThread_t)(int);
|
||||||
extern Scr_FreeThread_t Scr_FreeThread;
|
extern Scr_FreeThread_t Scr_FreeThread;
|
||||||
|
|
||||||
|
14
src/Game/Scripting/Function.cpp
Normal file
14
src/Game/Scripting/Function.cpp
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#include <STDInclude.hpp>
|
||||||
|
|
||||||
|
namespace Scripting
|
||||||
|
{
|
||||||
|
Function::Function(const char* pos)
|
||||||
|
: pos_(pos)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* Function::getPos() const
|
||||||
|
{
|
||||||
|
return this->pos_;
|
||||||
|
}
|
||||||
|
}
|
15
src/Game/Scripting/Function.hpp
Normal file
15
src/Game/Scripting/Function.hpp
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Scripting
|
||||||
|
{
|
||||||
|
class Function
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Function(const char* pos);
|
||||||
|
|
||||||
|
[[nodiscard]] const char* getPos() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const char* pos_;
|
||||||
|
};
|
||||||
|
}
|
@ -138,6 +138,7 @@ using namespace std::literals;
|
|||||||
|
|
||||||
#include "Game/Structs.hpp"
|
#include "Game/Structs.hpp"
|
||||||
#include "Game/Functions.hpp"
|
#include "Game/Functions.hpp"
|
||||||
|
#include <Game/Scripting/Function.hpp>
|
||||||
|
|
||||||
#include "Utils/Stream.hpp" // Breaks order on purpose
|
#include "Utils/Stream.hpp" // Breaks order on purpose
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user