- Fixed amb1802: Crash when client was disconnected as a result of false being returned in OnClientConnect and a function that operated on this client was used. A client's connection state was not reset when this happened.
- Removed IForwardFilter due to overall horribleness (should be safe since no one seems to use it). Perhaps it might be back one day? - Added ET_LowEvent forward exec type which is exactly the same as ET_Event, except that it returns the lowest value rather than the highest --HG-- extra : convert_revision : svn%3A39bc706e-5318-0410-9160-8a85361fbb7c/trunk%402386
This commit is contained in:
parent
53acb70260
commit
c9a81c0651
@ -121,7 +121,7 @@ void PlayerManager::OnSourceModAllInitialized()
|
||||
ParamType p1[] = {Param_Cell, Param_String, Param_Cell};
|
||||
ParamType p2[] = {Param_Cell};
|
||||
|
||||
m_clconnect = g_Forwards.CreateForward("OnClientConnect", ET_Event, 3, p1);
|
||||
m_clconnect = g_Forwards.CreateForward("OnClientConnect", ET_LowEvent, 3, p1);
|
||||
m_clputinserver = g_Forwards.CreateForward("OnClientPutInServer", ET_Ignore, 1, p2);
|
||||
m_cldisconnect = g_Forwards.CreateForward("OnClientDisconnect", ET_Ignore, 1, p2);
|
||||
m_cldisconnect_post = g_Forwards.CreateForward("OnClientDisconnect_Post", ET_Ignore, 1, p2);
|
||||
@ -288,7 +288,7 @@ void PlayerManager::RunAuthChecks()
|
||||
unsigned int removed = 0;
|
||||
for (unsigned int i=1; i<=m_AuthQueue[0]; i++)
|
||||
{
|
||||
pPlayer = GetPlayerByIndex(m_AuthQueue[i]);
|
||||
pPlayer = &m_Players[m_AuthQueue[i]];
|
||||
authstr = engine->GetPlayerNetworkIDString(pPlayer->m_pEdict);
|
||||
if (authstr && authstr[0] != '\0'
|
||||
&& (strcmp(authstr, "STEAM_ID_PENDING") != 0))
|
||||
@ -362,6 +362,7 @@ void PlayerManager::RunAuthChecks()
|
||||
bool PlayerManager::OnClientConnect(edict_t *pEntity, const char *pszName, const char *pszAddress, char *reject, int maxrejectlen)
|
||||
{
|
||||
int client = engine->IndexOfEdict(pEntity);
|
||||
CPlayer *pPlayer = &m_Players[client];
|
||||
|
||||
List<IClientListener *>::iterator iter;
|
||||
IClientListener *pListener = NULL;
|
||||
@ -376,26 +377,29 @@ bool PlayerManager::OnClientConnect(edict_t *pEntity, const char *pszName, const
|
||||
|
||||
cell_t res = 1;
|
||||
|
||||
m_Players[client].Initialize(pszName, pszAddress, pEntity);
|
||||
pPlayer->Initialize(pszName, pszAddress, pEntity);
|
||||
m_clconnect->PushCell(client);
|
||||
m_clconnect->PushStringEx(reject, maxrejectlen, SM_PARAM_STRING_UTF8, SM_PARAM_COPYBACK);
|
||||
m_clconnect->PushCell(maxrejectlen);
|
||||
m_clconnect->Execute(&res, NULL);
|
||||
m_clconnect->Execute(&res);
|
||||
|
||||
if (res)
|
||||
{
|
||||
if (!m_Players[client].IsAuthorized())
|
||||
if (!pPlayer->IsAuthorized())
|
||||
{
|
||||
m_AuthQueue[++m_AuthQueue[0]] = client;
|
||||
}
|
||||
|
||||
m_UserIdLookUp[engine->GetPlayerUserId(pEntity)] = client;
|
||||
}
|
||||
else
|
||||
{
|
||||
RETURN_META_VALUE(MRES_SUPERCEDE, false);
|
||||
if (!pPlayer->IsFakeClient())
|
||||
{
|
||||
RETURN_META_VALUE(MRES_SUPERCEDE, false);
|
||||
}
|
||||
}
|
||||
|
||||
m_UserIdLookUp[engine->GetPlayerUserId(pEntity)] = client;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -403,7 +407,7 @@ bool PlayerManager::OnClientConnect_Post(edict_t *pEntity, const char *pszName,
|
||||
{
|
||||
int client = engine->IndexOfEdict(pEntity);
|
||||
bool orig_value = META_RESULT_ORIG_RET(bool);
|
||||
CPlayer *pPlayer = GetPlayerByIndex(client);
|
||||
CPlayer *pPlayer = &m_Players[client];
|
||||
|
||||
if (orig_value)
|
||||
{
|
||||
@ -418,13 +422,17 @@ bool PlayerManager::OnClientConnect_Post(edict_t *pEntity, const char *pszName,
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!pPlayer->IsFakeClient()
|
||||
&& m_bIsListenServer
|
||||
&& strncmp(pszAddress, "127.0.0.1", 9) == 0)
|
||||
if (!pPlayer->IsFakeClient()
|
||||
&& m_bIsListenServer
|
||||
&& strncmp(pszAddress, "127.0.0.1", 9) == 0)
|
||||
{
|
||||
m_ListenClient = client;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ListenClient = client;
|
||||
InvalidatePlayer(pPlayer);
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -434,8 +442,8 @@ void PlayerManager::OnClientPutInServer(edict_t *pEntity, const char *playername
|
||||
{
|
||||
cell_t res;
|
||||
int client = engine->IndexOfEdict(pEntity);
|
||||
CPlayer *pPlayer = &m_Players[client];
|
||||
|
||||
CPlayer *pPlayer = GetPlayerByIndex(client);
|
||||
/* If they're not connected, they're a bot */
|
||||
if (!pPlayer->IsConnected())
|
||||
{
|
||||
@ -494,7 +502,7 @@ void PlayerManager::OnClientPutInServer(edict_t *pEntity, const char *playername
|
||||
}
|
||||
}
|
||||
|
||||
m_Players[client].Connect();
|
||||
pPlayer->Connect();
|
||||
m_PlayerCount++;
|
||||
|
||||
List<IClientListener *>::iterator iter;
|
||||
@ -508,9 +516,9 @@ void PlayerManager::OnClientPutInServer(edict_t *pEntity, const char *playername
|
||||
m_clputinserver->PushCell(client);
|
||||
m_clputinserver->Execute(&res, NULL);
|
||||
|
||||
if (m_Players[client].IsAuthorized())
|
||||
if (pPlayer->IsAuthorized())
|
||||
{
|
||||
m_Players[client].DoPostConnectAuthorization();
|
||||
pPlayer->DoPostConnectAuthorization();
|
||||
}
|
||||
}
|
||||
|
||||
@ -531,8 +539,9 @@ void PlayerManager::OnClientDisconnect(edict_t *pEntity)
|
||||
{
|
||||
cell_t res;
|
||||
int client = engine->IndexOfEdict(pEntity);
|
||||
CPlayer *pPlayer = &m_Players[client];
|
||||
|
||||
if (m_Players[client].IsConnected())
|
||||
if (pPlayer->IsConnected())
|
||||
{
|
||||
m_cldisconnect->PushCell(client);
|
||||
m_cldisconnect->Execute(&res, NULL);
|
||||
@ -543,7 +552,7 @@ void PlayerManager::OnClientDisconnect(edict_t *pEntity)
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_Players[client].WasCountedAsInGame())
|
||||
if (pPlayer->WasCountedAsInGame())
|
||||
{
|
||||
m_PlayerCount--;
|
||||
}
|
||||
@ -556,29 +565,7 @@ void PlayerManager::OnClientDisconnect(edict_t *pEntity)
|
||||
pListener->OnClientDisconnecting(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove client from auth queue if necessary
|
||||
*/
|
||||
if (!m_Players[client].IsAuthorized())
|
||||
{
|
||||
for (unsigned int i=1; i<=m_AuthQueue[0]; i++)
|
||||
{
|
||||
if (m_AuthQueue[i] == (unsigned)client)
|
||||
{
|
||||
/* Move everything ahead of us back by one */
|
||||
for (unsigned int j=i+1; j<=m_AuthQueue[0]; j++)
|
||||
{
|
||||
m_AuthQueue[j-1] = m_AuthQueue[j];
|
||||
}
|
||||
/* Remove us and break */
|
||||
m_AuthQueue[0]--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_Players[client].Disconnect();
|
||||
m_UserIdLookUp[engine->GetPlayerUserId(pEntity)] = 0;
|
||||
InvalidatePlayer(pPlayer);
|
||||
|
||||
if (m_ListenClient == client)
|
||||
{
|
||||
@ -613,9 +600,9 @@ void PlayerManager::OnClientCommand(edict_t *pEntity)
|
||||
#endif
|
||||
int client = engine->IndexOfEdict(pEntity);
|
||||
cell_t res = Pl_Continue;
|
||||
CPlayer *pPlayer = &m_Players[client];
|
||||
|
||||
CPlayer *pPlayer = GetPlayerByIndex(client);
|
||||
if (!pPlayer || !pPlayer->IsConnected())
|
||||
if (!pPlayer->IsConnected())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -667,8 +654,9 @@ void PlayerManager::OnClientSettingsChanged(edict_t *pEntity)
|
||||
{
|
||||
cell_t res;
|
||||
int client = engine->IndexOfEdict(pEntity);
|
||||
CPlayer *pPlayer = &m_Players[client];
|
||||
|
||||
if (!m_Players[client].IsConnected())
|
||||
if (!pPlayer->IsConnected())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -676,42 +664,42 @@ void PlayerManager::OnClientSettingsChanged(edict_t *pEntity)
|
||||
m_clinfochanged->PushCell(engine->IndexOfEdict(pEntity));
|
||||
m_clinfochanged->Execute(&res, NULL);
|
||||
|
||||
IPlayerInfo *info = m_Players[client].GetPlayerInfo();
|
||||
IPlayerInfo *info = pPlayer->GetPlayerInfo();
|
||||
const char *new_name = info ? info->GetName() : engine->GetClientConVarValue(client, "name");
|
||||
const char *old_name = m_Players[client].m_Name.c_str();
|
||||
const char *old_name = pPlayer->m_Name.c_str();
|
||||
|
||||
if (strcmp(old_name, new_name) != 0)
|
||||
{
|
||||
AdminId id = g_Admins.FindAdminByIdentity("name", new_name);
|
||||
if (id != INVALID_ADMIN_ID && m_Players[client].GetAdminId() != id)
|
||||
if (id != INVALID_ADMIN_ID && pPlayer->GetAdminId() != id)
|
||||
{
|
||||
if (!CheckSetAdminName(client, &m_Players[client], id))
|
||||
if (!CheckSetAdminName(client, pPlayer, id))
|
||||
{
|
||||
m_Players[client].Kick("Your name is reserved by SourceMod; set your password to use it.");
|
||||
pPlayer->Kick("Your name is reserved by SourceMod; set your password to use it.");
|
||||
RETURN_META(MRES_IGNORED);
|
||||
}
|
||||
} else if ((id = g_Admins.FindAdminByIdentity("name", old_name)) != INVALID_ADMIN_ID) {
|
||||
if (id == m_Players[client].GetAdminId())
|
||||
if (id == pPlayer->GetAdminId())
|
||||
{
|
||||
/* This player is changing their name; force them to drop admin privileges! */
|
||||
m_Players[client].SetAdminId(INVALID_ADMIN_ID, false);
|
||||
pPlayer->SetAdminId(INVALID_ADMIN_ID, false);
|
||||
}
|
||||
}
|
||||
m_Players[client].SetName(new_name);
|
||||
pPlayer->SetName(new_name);
|
||||
}
|
||||
|
||||
if (m_PassInfoVar.size() > 0)
|
||||
{
|
||||
/* Try for a password change */
|
||||
const char *old_pass = m_Players[client].m_LastPassword.c_str();
|
||||
const char *old_pass = pPlayer->m_LastPassword.c_str();
|
||||
const char *new_pass = engine->GetClientConVarValue(client, m_PassInfoVar.c_str());
|
||||
if (strcmp(old_pass, new_pass) != 0)
|
||||
{
|
||||
m_Players[client].m_LastPassword.assign(new_pass);
|
||||
if (m_Players[client].IsInGame() && m_Players[client].IsAuthorized())
|
||||
pPlayer->m_LastPassword.assign(new_pass);
|
||||
if (pPlayer->IsInGame() && pPlayer->IsAuthorized())
|
||||
{
|
||||
/* If there is already an admin id assigned, this will just bail out. */
|
||||
m_Players[client].DoBasicAdminChecks();
|
||||
pPlayer->DoBasicAdminChecks();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -862,6 +850,33 @@ void PlayerManager::UnregisterCommandTargetProcessor(ICommandTargetProcessor *pH
|
||||
target_processors.remove(pHandler);
|
||||
}
|
||||
|
||||
void PlayerManager::InvalidatePlayer(CPlayer *pPlayer)
|
||||
{
|
||||
/**
|
||||
* Remove client from auth queue if necessary
|
||||
*/
|
||||
if (!pPlayer->IsAuthorized())
|
||||
{
|
||||
for (unsigned int i=1; i<=m_AuthQueue[0]; i++)
|
||||
{
|
||||
if (m_AuthQueue[i] == (unsigned)pPlayer->m_iIndex)
|
||||
{
|
||||
/* Move everything ahead of us back by one */
|
||||
for (unsigned int j=i+1; j<=m_AuthQueue[0]; j++)
|
||||
{
|
||||
m_AuthQueue[j-1] = m_AuthQueue[j];
|
||||
}
|
||||
/* Remove us and break */
|
||||
m_AuthQueue[0]--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_UserIdLookUp[engine->GetPlayerUserId(pPlayer->m_pEdict)] = 0;
|
||||
pPlayer->Disconnect();
|
||||
}
|
||||
|
||||
int PlayerManager::InternalFilterCommandTarget(CPlayer *pAdmin, CPlayer *pTarget, int flags)
|
||||
{
|
||||
if ((flags & COMMAND_FILTER_CONNECTED) == COMMAND_FILTER_CONNECTED
|
||||
|
@ -173,6 +173,7 @@ public:
|
||||
unsigned int SetReplyTo(unsigned int reply);
|
||||
private:
|
||||
void OnServerActivate(edict_t *pEdictList, int edictCount, int clientMax);
|
||||
void InvalidatePlayer(CPlayer *pPlayer);
|
||||
private:
|
||||
List<IClientListener *> m_hooks;
|
||||
IForward *m_clconnect;
|
||||
|
@ -186,11 +186,6 @@ void CForwardManager::ReleaseForward(IForward *forward)
|
||||
|
||||
void CForwardManager::ForwardFree(CForward *fwd)
|
||||
{
|
||||
if (fwd == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_FreeForwards.push(fwd);
|
||||
m_managed.remove(fwd);
|
||||
}
|
||||
@ -288,15 +283,11 @@ int CForward::Execute(cell_t *result, IForwardFilter *filter)
|
||||
return err;
|
||||
}
|
||||
|
||||
if (filter)
|
||||
{
|
||||
filter->OnExecuteBegin();
|
||||
}
|
||||
|
||||
FuncIter iter = m_functions.begin();
|
||||
IPluginFunction *func;
|
||||
cell_t cur_result = 0;
|
||||
cell_t high_result = 0;
|
||||
cell_t low_result = 0;
|
||||
int err;
|
||||
unsigned int failed=0, success=0;
|
||||
unsigned int num_params = m_curparam;
|
||||
@ -316,12 +307,16 @@ int CForward::Execute(cell_t *result, IForwardFilter *filter)
|
||||
{
|
||||
int err = SP_ERROR_PARAM;
|
||||
param = &temp_info[i];
|
||||
|
||||
if (i >= m_numparams || m_types[i] == Param_Any)
|
||||
{
|
||||
type = param->pushedas;
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
type = m_types[i];
|
||||
}
|
||||
|
||||
if ((i >= m_numparams) || (type & SP_PARAMFLAG_BYREF))
|
||||
{
|
||||
/* If we're byref or we're vararg, we always push everything by ref.
|
||||
@ -330,26 +325,30 @@ int CForward::Execute(cell_t *result, IForwardFilter *filter)
|
||||
if (type == Param_String)
|
||||
{
|
||||
err = func->PushStringEx((char *)param->byref.orig_addr, param->byref.cells, param->byref.sz_flags, param->byref.flags);
|
||||
} else if (type == Param_Float || type == Param_Cell) {
|
||||
}
|
||||
else if (type == Param_Float || type == Param_Cell)
|
||||
{
|
||||
err = func->PushCellByRef(¶m->val);
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
err = func->PushArray(param->byref.orig_addr, param->byref.cells, param->byref.flags);
|
||||
assert(type == Param_Array || type == Param_FloatByRef || type == Param_CellByRef);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
/* If we're not byref or not vararg, our job is a bit easier. */
|
||||
assert(type == Param_Cell || type == Param_Float);
|
||||
err = func->PushCell(param->val);
|
||||
}
|
||||
|
||||
if (err != SP_ERROR_NONE)
|
||||
{
|
||||
if (!filter || !filter->OnErrorReport(this, func, err))
|
||||
{
|
||||
g_DbgReporter.GenerateError(func->GetParentContext(),
|
||||
func->GetFunctionID(),
|
||||
err,
|
||||
"Failed to push parameter while executing forward");
|
||||
}
|
||||
g_DbgReporter.GenerateError(func->GetParentContext(),
|
||||
func->GetFunctionID(),
|
||||
err,
|
||||
"Failed to push parameter while executing forward");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -357,12 +356,10 @@ int CForward::Execute(cell_t *result, IForwardFilter *filter)
|
||||
/* Call the function and deal with the return value. */
|
||||
if ((err=func->Execute(&cur_result)) != SP_ERROR_NONE)
|
||||
{
|
||||
if (filter)
|
||||
{
|
||||
filter->OnErrorReport(this, func, err);
|
||||
}
|
||||
failed++;
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
success++;
|
||||
switch (m_ExecType)
|
||||
{
|
||||
@ -386,14 +383,11 @@ int CForward::Execute(cell_t *result, IForwardFilter *filter)
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ET_Custom:
|
||||
case ET_LowEvent:
|
||||
{
|
||||
if (filter)
|
||||
if (cur_result < low_result)
|
||||
{
|
||||
if (filter->OnFunctionReturn(this, func, &cur_result) == Pl_Stop)
|
||||
{
|
||||
goto done;
|
||||
}
|
||||
low_result = cur_result;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -406,25 +400,39 @@ int CForward::Execute(cell_t *result, IForwardFilter *filter)
|
||||
}
|
||||
|
||||
done:
|
||||
|
||||
if (success)
|
||||
{
|
||||
if (m_ExecType == ET_Event || m_ExecType == ET_Hook)
|
||||
switch (m_ExecType)
|
||||
{
|
||||
cur_result = high_result;
|
||||
} else if (m_ExecType == ET_Ignore) {
|
||||
cur_result = 0;
|
||||
case ET_Ignore:
|
||||
{
|
||||
cur_result = 0;
|
||||
break;
|
||||
}
|
||||
case ET_Event:
|
||||
case ET_Hook:
|
||||
{
|
||||
cur_result = high_result;
|
||||
break;
|
||||
}
|
||||
case ET_LowEvent:
|
||||
{
|
||||
cur_result = low_result;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (result)
|
||||
{
|
||||
*result = cur_result;
|
||||
}
|
||||
}
|
||||
|
||||
if (filter)
|
||||
{
|
||||
filter->OnExecuteEnd(&cur_result, success, failed);
|
||||
}
|
||||
|
||||
return SP_ERROR_NONE;
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ using namespace SourceHook;
|
||||
|
||||
typedef List<IPluginFunction *>::iterator FuncIter;
|
||||
|
||||
//:TODO: a global name max define for sourcepawn, should mirror compiler's sNAMEMAX
|
||||
/* :TODO: a global name max define for sourcepawn, should mirror compiler's sNAMEMAX */
|
||||
#define FORWARDS_NAME_MAX 64
|
||||
|
||||
struct ByrefInfo
|
||||
|
@ -79,66 +79,11 @@ namespace SourceMod
|
||||
ET_Single = 1, /**< Only return the last exec, ignore all others */
|
||||
ET_Event = 2, /**< Acts as an event with the ResultTypes above, no mid-Stops allowed, returns highest */
|
||||
ET_Hook = 3, /**< Acts as a hook with the ResultTypes above, mid-Stops allowed, returns highest */
|
||||
ET_Custom = 4, /**< Ignored or handled by an IForwardFilter */
|
||||
ET_LowEvent = 4, /**< Same as ET_Event except that it returns the lowest value */
|
||||
};
|
||||
|
||||
class IForward;
|
||||
|
||||
/**
|
||||
* @brief Allows interception of how the Forward System executes functions.
|
||||
*/
|
||||
class IForwardFilter
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Called when an error occurs executing a plugin.
|
||||
*
|
||||
* @param fwd IForward pointer.
|
||||
* @param func IPluginFunction pointer to the failed function.
|
||||
* @param err Error code.
|
||||
* @return True to handle, false to pass to global error reporter.
|
||||
*/
|
||||
virtual bool OnErrorReport(IForward *fwd,
|
||||
IPluginFunction *func,
|
||||
int err)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Called after each function return during execution.
|
||||
* NOTE: Only used for ET_Custom.
|
||||
*
|
||||
* @param fwd IForward pointer.
|
||||
* @param func IPluginFunction pointer to the executed function.
|
||||
* @param retval Pointer to current return value (can be modified).
|
||||
* @return ResultType denoting the next action to take.
|
||||
*/
|
||||
virtual ResultType OnFunctionReturn(IForward *fwd,
|
||||
IPluginFunction *func,
|
||||
cell_t *retval)
|
||||
{
|
||||
return Pl_Continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Called when execution begins.
|
||||
*/
|
||||
virtual void OnExecuteBegin()
|
||||
{
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Called when execution ends.
|
||||
*
|
||||
* @param final_ret Final return value (modifiable).
|
||||
* @param success Number of successful execs.
|
||||
* @param failed Number of failed execs.
|
||||
*/
|
||||
virtual void OnExecuteEnd(cell_t *final_ret, unsigned int success, unsigned int failed)
|
||||
{
|
||||
}
|
||||
};
|
||||
class IForwardFilter;
|
||||
|
||||
/**
|
||||
* @brief Unmanaged Forward, abstracts calling multiple functions as "forwards," or collections of functions.
|
||||
@ -180,7 +125,7 @@ namespace SourceMod
|
||||
* @brief Executes the forward.
|
||||
*
|
||||
* @param result Optional pointer to store result in.
|
||||
* @param filter Optional pointer to an IForwardFilter.
|
||||
* @param filter Do not use.
|
||||
* @return Error code, if any.
|
||||
*/
|
||||
virtual int Execute(cell_t *result, IForwardFilter *filter=NULL) =0;
|
||||
|
Loading…
Reference in New Issue
Block a user