fix(Core/Movement): prevent false MovementInform on gossip pause (#25428)
This commit is contained in:
@@ -3091,6 +3091,21 @@ CreatureMovementData const& Creature::GetMovementTemplate() const
|
|||||||
return GetCreatureTemplate()->Movement;
|
return GetCreatureTemplate()->Movement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Creature::PauseMovementForInteraction()
|
||||||
|
{
|
||||||
|
if (uint32 pause = GetMovementTemplate().GetInteractionPauseTimer())
|
||||||
|
{
|
||||||
|
uint8 pauseSlot = MOTION_SLOT_IDLE;
|
||||||
|
|
||||||
|
if (GetMotionMaster()->GetMotionSlotType(MOTION_SLOT_ACTIVE) == POINT_MOTION_TYPE)
|
||||||
|
pauseSlot = MOTION_SLOT_ACTIVE;
|
||||||
|
else if (GetMotionMaster()->GetMotionSlotType(MOTION_SLOT_CONTROLLED) == POINT_MOTION_TYPE)
|
||||||
|
pauseSlot = MOTION_SLOT_CONTROLLED;
|
||||||
|
|
||||||
|
PauseMovement(pause, pauseSlot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Creature::AllLootRemovedFromCorpse()
|
void Creature::AllLootRemovedFromCorpse()
|
||||||
{
|
{
|
||||||
if (loot.loot_type != LOOT_SKINNING && !IsPet() && GetCreatureTemplate()->SkinLootId && hasLootRecipient())
|
if (loot.loot_type != LOOT_SKINNING && !IsPet() && GetCreatureTemplate()->SkinLootId && hasLootRecipient())
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ public:
|
|||||||
[[nodiscard]] bool IsTrigger() const { return HasFlagsExtra(CREATURE_FLAG_EXTRA_TRIGGER); }
|
[[nodiscard]] bool IsTrigger() const { return HasFlagsExtra(CREATURE_FLAG_EXTRA_TRIGGER); }
|
||||||
[[nodiscard]] bool IsGuard() const { return HasFlagsExtra(CREATURE_FLAG_EXTRA_GUARD); }
|
[[nodiscard]] bool IsGuard() const { return HasFlagsExtra(CREATURE_FLAG_EXTRA_GUARD); }
|
||||||
CreatureMovementData const& GetMovementTemplate() const;
|
CreatureMovementData const& GetMovementTemplate() const;
|
||||||
|
void PauseMovementForInteraction();
|
||||||
[[nodiscard]] bool CanWalk() const { return GetMovementTemplate().IsGroundAllowed(); }
|
[[nodiscard]] bool CanWalk() const { return GetMovementTemplate().IsGroundAllowed(); }
|
||||||
[[nodiscard]] bool CanSwim() const override;
|
[[nodiscard]] bool CanSwim() const override;
|
||||||
[[nodiscard]] bool CanEnterWater() const override;
|
[[nodiscard]] bool CanEnterWater() const override;
|
||||||
|
|||||||
@@ -47,9 +47,7 @@ void WorldSession::HandleBattlemasterHelloOpcode(WorldPacket& recvData)
|
|||||||
if (!unit->IsBattleMaster()) // it's not battlemaster
|
if (!unit->IsBattleMaster()) // it's not battlemaster
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Stop the npc if moving
|
unit->PauseMovementForInteraction();
|
||||||
if (uint32 pause = unit->GetMovementTemplate().GetInteractionPauseTimer())
|
|
||||||
unit->PauseMovement(pause);
|
|
||||||
unit->SetHomePosition(unit->GetPosition());
|
unit->SetHomePosition(unit->GetPosition());
|
||||||
|
|
||||||
BattlegroundTypeId bgTypeId = sBattlegroundMgr->GetBattleMasterBG(unit->GetEntry());
|
BattlegroundTypeId bgTypeId = sBattlegroundMgr->GetBattleMasterBG(unit->GetEntry());
|
||||||
|
|||||||
@@ -871,9 +871,7 @@ void WorldSession::SendListInventory(ObjectGuid vendorGuid, uint32 vendorEntry)
|
|||||||
GetPlayer()->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);
|
GetPlayer()->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the npc if moving
|
vendor->PauseMovementForInteraction();
|
||||||
if (uint32 pause = vendor->GetMovementTemplate().GetInteractionPauseTimer())
|
|
||||||
vendor->PauseMovement(pause);
|
|
||||||
|
|
||||||
// Update home position for patrolling NPCs only (prevents drift for stationary NPCs)
|
// Update home position for patrolling NPCs only (prevents drift for stationary NPCs)
|
||||||
if (vendor->GetDefaultMovementType() == WAYPOINT_MOTION_TYPE ||
|
if (vendor->GetDefaultMovementType() == WAYPOINT_MOTION_TYPE ||
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ void WorldSession::SendTrainerList(Creature* npc)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
npc->PauseMovementForInteraction();
|
||||||
|
|
||||||
trainer->SendSpells(npc, _player, GetSessionDbLocaleIndex());
|
trainer->SendSpells(npc, _player, GetSessionDbLocaleIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,9 +167,7 @@ void WorldSession::HandleGossipHelloOpcode(WorldPacket& recvData)
|
|||||||
//if (GetPlayer()->HasUnitState(UNIT_STATE_DIED))
|
//if (GetPlayer()->HasUnitState(UNIT_STATE_DIED))
|
||||||
// GetPlayer()->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);
|
// GetPlayer()->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);
|
||||||
|
|
||||||
// Stop the npc if moving
|
unit->PauseMovementForInteraction();
|
||||||
if (uint32 pause = unit->GetMovementTemplate().GetInteractionPauseTimer())
|
|
||||||
unit->PauseMovement(pause);
|
|
||||||
|
|
||||||
// Update home position for patrolling NPCs only (prevents drift for stationary NPCs)
|
// Update home position for patrolling NPCs only (prevents drift for stationary NPCs)
|
||||||
if (unit->GetDefaultMovementType() == WAYPOINT_MOTION_TYPE ||
|
if (unit->GetDefaultMovementType() == WAYPOINT_MOTION_TYPE ||
|
||||||
|
|||||||
@@ -93,9 +93,7 @@ void WorldSession::HandleQuestgiverHelloOpcode(WorldPacket& recvData)
|
|||||||
if (GetPlayer()->HasUnitState(UNIT_STATE_DIED))
|
if (GetPlayer()->HasUnitState(UNIT_STATE_DIED))
|
||||||
GetPlayer()->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);
|
GetPlayer()->RemoveAurasByType(SPELL_AURA_FEIGN_DEATH);
|
||||||
|
|
||||||
// Stop the npc if moving
|
creature->PauseMovementForInteraction();
|
||||||
if (uint32 pause = creature->GetMovementTemplate().GetInteractionPauseTimer())
|
|
||||||
creature->PauseMovement(pause);
|
|
||||||
|
|
||||||
// Update home position for patrolling NPCs only (prevents drift for stationary NPCs)
|
// Update home position for patrolling NPCs only (prevents drift for stationary NPCs)
|
||||||
if (creature->GetDefaultMovementType() == WAYPOINT_MOTION_TYPE ||
|
if (creature->GetDefaultMovementType() == WAYPOINT_MOTION_TYPE ||
|
||||||
|
|||||||
@@ -23,11 +23,16 @@
|
|||||||
#include "ObjectAccessor.h"
|
#include "ObjectAccessor.h"
|
||||||
#include "Player.h"
|
#include "Player.h"
|
||||||
#include "World.h"
|
#include "World.h"
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
//----- Point Movement Generator
|
//----- Point Movement Generator
|
||||||
template<class T>
|
template<class T>
|
||||||
void PointMovementGenerator<T>::DoInitialize(T* unit)
|
void PointMovementGenerator<T>::DoInitialize(T* unit)
|
||||||
{
|
{
|
||||||
|
_stalled = false;
|
||||||
|
_hasBeenStalled = false;
|
||||||
|
_pauseTime.reset();
|
||||||
|
|
||||||
if (unit->HasUnitState(UNIT_STATE_NOT_MOVE) || unit->IsMovementPreventedByCasting())
|
if (unit->HasUnitState(UNIT_STATE_NOT_MOVE) || unit->IsMovementPreventedByCasting())
|
||||||
{
|
{
|
||||||
// the next line is to ensure that a new spline is created in DoUpdate() once the unit is no longer rooted/stunned
|
// the next line is to ensure that a new spline is created in DoUpdate() once the unit is no longer rooted/stunned
|
||||||
@@ -101,7 +106,7 @@ void PointMovementGenerator<T>::DoInitialize(T* unit)
|
|||||||
}
|
}
|
||||||
|
|
||||||
template<class T>
|
template<class T>
|
||||||
bool PointMovementGenerator<T>::DoUpdate(T* unit, uint32 /*diff*/)
|
bool PointMovementGenerator<T>::DoUpdate(T* unit, uint32 diff)
|
||||||
{
|
{
|
||||||
if (!unit)
|
if (!unit)
|
||||||
return false;
|
return false;
|
||||||
@@ -115,38 +120,102 @@ bool PointMovementGenerator<T>::DoUpdate(T* unit, uint32 /*diff*/)
|
|||||||
if (unit->HasUnitState(UNIT_STATE_NOT_MOVE))
|
if (unit->HasUnitState(UNIT_STATE_NOT_MOVE))
|
||||||
{
|
{
|
||||||
if (!unit->HasUnitState(UNIT_STATE_CHARGING))
|
if (!unit->HasUnitState(UNIT_STATE_CHARGING))
|
||||||
{
|
|
||||||
unit->StopMoving();
|
unit->StopMoving();
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
unit->AddUnitState(UNIT_STATE_ROAMING_MOVE);
|
unit->AddUnitState(UNIT_STATE_ROAMING_MOVE);
|
||||||
|
|
||||||
if (id != EVENT_CHARGE_PREPATH && i_recalculateSpeed && !unit->movespline->Finalized())
|
if (_pauseTime.has_value())
|
||||||
|
{
|
||||||
|
if (diff >= static_cast<uint32>(_pauseTime.value()))
|
||||||
|
_pauseTime.reset();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pauseTime = static_cast<int32>(_pauseTime.value() - diff);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasBeenStalled = false;
|
||||||
|
_stalled = false;
|
||||||
|
i_recalculateSpeed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relaunch path when speed changed or when resuming from a stall.
|
||||||
|
// Keep an indefinitely paused movement stalled until Resume() clears _stalled.
|
||||||
|
if (id != EVENT_CHARGE_PREPATH && !_stalled && (i_recalculateSpeed || _hasBeenStalled))
|
||||||
{
|
{
|
||||||
i_recalculateSpeed = false;
|
i_recalculateSpeed = false;
|
||||||
Movement::MoveSplineInit init(unit);
|
Movement::MoveSplineInit init(unit);
|
||||||
|
auto rebasePrecomputedPath = [this, unit](std::optional<uint32> offset = std::nullopt)
|
||||||
|
{
|
||||||
|
Movement::PointsArray rebasedPath;
|
||||||
|
G3D::Vector3 currentPos(unit->GetPositionX(), unit->GetPositionY(), unit->GetPositionZ());
|
||||||
|
|
||||||
|
if (m_precomputedPath.empty())
|
||||||
|
return rebasedPath;
|
||||||
|
|
||||||
|
if (offset.has_value())
|
||||||
|
{
|
||||||
|
if (offset.value() >= m_precomputedPath.size())
|
||||||
|
{
|
||||||
|
rebasedPath.push_back(currentPos);
|
||||||
|
rebasedPath.push_back(m_precomputedPath.back());
|
||||||
|
return rebasedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
rebasedPath.insert(rebasedPath.end(), m_precomputedPath.begin() + offset.value(), m_precomputedPath.end());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
uint32 closestPointIndex = 0;
|
||||||
|
float closestPointDist = std::numeric_limits<float>::max();
|
||||||
|
for (uint32 pointIndex = 1; pointIndex < m_precomputedPath.size(); ++pointIndex)
|
||||||
|
{
|
||||||
|
float const sqDist = (currentPos - m_precomputedPath[pointIndex]).squaredLength();
|
||||||
|
if (sqDist < closestPointDist)
|
||||||
|
{
|
||||||
|
closestPointDist = sqDist;
|
||||||
|
closestPointIndex = pointIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rebasedPath.insert(rebasedPath.end(), m_precomputedPath.begin() + closestPointIndex, m_precomputedPath.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
// MovebyPath requires the first point to be the mover's current position.
|
||||||
|
rebasedPath.insert(rebasedPath.begin(), currentPos);
|
||||||
|
return rebasedPath;
|
||||||
|
};
|
||||||
|
|
||||||
// xinef: speed changed during path execution, calculate remaining path and launch it once more
|
|
||||||
if (m_precomputedPath.size())
|
if (m_precomputedPath.size())
|
||||||
{
|
{
|
||||||
uint32 offset = std::min(uint32(unit->movespline->_currentSplineIdx()), uint32(m_precomputedPath.size()));
|
if (!unit->movespline->Finalized())
|
||||||
Movement::PointsArray::iterator offsetItr = m_precomputedPath.begin();
|
{
|
||||||
std::advance(offsetItr, offset);
|
uint32 offset = std::min(uint32(unit->movespline->_currentSplineIdx()), uint32(m_precomputedPath.size()));
|
||||||
m_precomputedPath.erase(m_precomputedPath.begin(), offsetItr);
|
m_precomputedPath = rebasePrecomputedPath(offset);
|
||||||
|
|
||||||
// restore 0 element (current position)
|
if (m_precomputedPath.size() > 2)
|
||||||
m_precomputedPath.insert(m_precomputedPath.begin(), G3D::Vector3(unit->GetPositionX(), unit->GetPositionY(), unit->GetPositionZ()));
|
init.MovebyPath(m_precomputedPath);
|
||||||
|
else if (m_precomputedPath.size() == 2)
|
||||||
|
init.MoveTo(m_precomputedPath[1].x, m_precomputedPath[1].y, m_precomputedPath[1].z, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unit was stopped (finalized) due to Pause/StopMoving; trim path from current position.
|
||||||
|
m_precomputedPath = rebasePrecomputedPath();
|
||||||
|
|
||||||
if (m_precomputedPath.size() > 2)
|
if (m_precomputedPath.size() > 2)
|
||||||
init.MovebyPath(m_precomputedPath);
|
init.MovebyPath(m_precomputedPath);
|
||||||
else if (m_precomputedPath.size() == 2)
|
else if (m_precomputedPath.size() == 2)
|
||||||
init.MoveTo(m_precomputedPath[1].x, m_precomputedPath[1].y, m_precomputedPath[1].z, true);
|
init.MoveTo(m_precomputedPath[1].x, m_precomputedPath[1].y, m_precomputedPath[1].z, true);
|
||||||
|
else
|
||||||
|
init.MoveTo(i_x, i_y, i_z, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
init.MoveTo(i_x, i_y, i_z, true);
|
init.MoveTo(i_x, i_y, i_z, true);
|
||||||
|
|
||||||
if (speed > 0.0f) // Default value for point motion type is 0.0, if 0.0 spline will use GetSpeed on unit
|
if (speed > 0.0f) // Default value for point motion type is 0.0, if 0.0 spline will use GetSpeed on unit
|
||||||
init.SetVelocity(speed);
|
init.SetVelocity(speed);
|
||||||
|
|
||||||
@@ -159,14 +228,21 @@ bool PointMovementGenerator<T>::DoUpdate(T* unit, uint32 /*diff*/)
|
|||||||
init.SetAnimation(*_animTier);
|
init.SetAnimation(*_animTier);
|
||||||
|
|
||||||
if (i_orientation > 0.0f)
|
if (i_orientation > 0.0f)
|
||||||
{
|
|
||||||
init.SetFacing(i_orientation);
|
init.SetFacing(i_orientation);
|
||||||
}
|
|
||||||
|
|
||||||
init.Launch();
|
init.Launch();
|
||||||
}
|
}
|
||||||
|
|
||||||
return !unit->movespline->Finalized();
|
// If finalized but we were stalled (paused) keep generator active so Finalize() won't be called
|
||||||
|
if (unit->movespline->Finalized())
|
||||||
|
{
|
||||||
|
if (_hasBeenStalled)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<class T>
|
template<class T>
|
||||||
@@ -186,10 +262,35 @@ void PointMovementGenerator<T>::DoFinalize(T* unit)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unit->movespline->Finalized())
|
// Only inform AI if this is a real arrival, not a finalize caused by a Pause/Stop
|
||||||
|
if (unit->movespline->Finalized() && !_hasBeenStalled)
|
||||||
MovementInform(unit);
|
MovementInform(unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<class T>
|
||||||
|
void PointMovementGenerator<T>::Pause(uint32 timer)
|
||||||
|
{
|
||||||
|
_stalled = timer ? false : true;
|
||||||
|
_hasBeenStalled = true;
|
||||||
|
if (timer)
|
||||||
|
_pauseTime = static_cast<int32>(timer);
|
||||||
|
else
|
||||||
|
_pauseTime.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class T>
|
||||||
|
void PointMovementGenerator<T>::Resume(uint32 overrideTimer)
|
||||||
|
{
|
||||||
|
_hasBeenStalled = false;
|
||||||
|
_stalled = false;
|
||||||
|
if (overrideTimer)
|
||||||
|
_pauseTime = static_cast<int32>(overrideTimer);
|
||||||
|
else
|
||||||
|
_pauseTime.reset();
|
||||||
|
|
||||||
|
i_recalculateSpeed = true;
|
||||||
|
}
|
||||||
|
|
||||||
template<class T>
|
template<class T>
|
||||||
void PointMovementGenerator<T>::DoReset(T* unit)
|
void PointMovementGenerator<T>::DoReset(T* unit)
|
||||||
{
|
{
|
||||||
@@ -236,6 +337,11 @@ template void PointMovementGenerator<Creature>::DoReset(Creature*);
|
|||||||
template bool PointMovementGenerator<Player>::DoUpdate(Player*, uint32);
|
template bool PointMovementGenerator<Player>::DoUpdate(Player*, uint32);
|
||||||
template bool PointMovementGenerator<Creature>::DoUpdate(Creature*, uint32);
|
template bool PointMovementGenerator<Creature>::DoUpdate(Creature*, uint32);
|
||||||
|
|
||||||
|
template void PointMovementGenerator<Player>::Pause(uint32);
|
||||||
|
template void PointMovementGenerator<Creature>::Pause(uint32);
|
||||||
|
template void PointMovementGenerator<Player>::Resume(uint32);
|
||||||
|
template void PointMovementGenerator<Creature>::Resume(uint32);
|
||||||
|
|
||||||
void AssistanceMovementGenerator::Finalize(Unit* unit)
|
void AssistanceMovementGenerator::Finalize(Unit* unit)
|
||||||
{
|
{
|
||||||
unit->ToCreature()->SetNoCallAssistance(false);
|
unit->ToCreature()->SetNoCallAssistance(false);
|
||||||
|
|||||||
@@ -39,13 +39,17 @@ public:
|
|||||||
void DoInitialize(T*);
|
void DoInitialize(T*);
|
||||||
void DoFinalize(T*);
|
void DoFinalize(T*);
|
||||||
void DoReset(T*);
|
void DoReset(T*);
|
||||||
|
|
||||||
bool DoUpdate(T*, uint32);
|
bool DoUpdate(T*, uint32);
|
||||||
|
|
||||||
void MovementInform(T*);
|
void MovementInform(T*);
|
||||||
|
|
||||||
void unitSpeedChanged() { i_recalculateSpeed = true; }
|
void Pause(uint32 timer = 0) override;
|
||||||
|
void Resume(uint32 overrideTimer = 0) override;
|
||||||
|
|
||||||
MovementGeneratorType GetMovementGeneratorType() { return POINT_MOTION_TYPE; }
|
void unitSpeedChanged() override { i_recalculateSpeed = true; }
|
||||||
|
|
||||||
|
MovementGeneratorType GetMovementGeneratorType() override { return POINT_MOTION_TYPE; }
|
||||||
|
|
||||||
bool GetDestination(float& x, float& y, float& z) const { x = i_x; y = i_y; z = i_z; return true; }
|
bool GetDestination(float& x, float& y, float& z) const { x = i_x; y = i_y; z = i_z; return true; }
|
||||||
private:
|
private:
|
||||||
@@ -53,7 +57,7 @@ private:
|
|||||||
float i_x, i_y, i_z;
|
float i_x, i_y, i_z;
|
||||||
float speed;
|
float speed;
|
||||||
float i_orientation;
|
float i_orientation;
|
||||||
bool i_recalculateSpeed;
|
bool i_recalculateSpeed{};
|
||||||
Movement::PointsArray m_precomputedPath;
|
Movement::PointsArray m_precomputedPath;
|
||||||
bool _generatePath;
|
bool _generatePath;
|
||||||
bool _forceDestination;
|
bool _forceDestination;
|
||||||
@@ -62,6 +66,9 @@ private:
|
|||||||
ForcedMovement _forcedMovement;
|
ForcedMovement _forcedMovement;
|
||||||
ObjectGuid _facingTargetGuid;
|
ObjectGuid _facingTargetGuid;
|
||||||
std::optional<AnimTier> _animTier;
|
std::optional<AnimTier> _animTier;
|
||||||
|
bool _stalled{};
|
||||||
|
bool _hasBeenStalled{};
|
||||||
|
std::optional<int32> _pauseTime;
|
||||||
};
|
};
|
||||||
|
|
||||||
class AssistanceMovementGenerator : public PointMovementGenerator<Creature>
|
class AssistanceMovementGenerator : public PointMovementGenerator<Creature>
|
||||||
@@ -70,8 +77,8 @@ public:
|
|||||||
AssistanceMovementGenerator(float _x, float _y, float _z) :
|
AssistanceMovementGenerator(float _x, float _y, float _z) :
|
||||||
PointMovementGenerator<Creature>(0, _x, _y, _z, FORCED_MOVEMENT_NONE) {}
|
PointMovementGenerator<Creature>(0, _x, _y, _z, FORCED_MOVEMENT_NONE) {}
|
||||||
|
|
||||||
MovementGeneratorType GetMovementGeneratorType() { return ASSISTANCE_MOTION_TYPE; }
|
MovementGeneratorType GetMovementGeneratorType() override { return ASSISTANCE_MOTION_TYPE; }
|
||||||
void Finalize(Unit*);
|
void Finalize(Unit*) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Does almost nothing - just doesn't allows previous movegen interrupt current effect.
|
// Does almost nothing - just doesn't allows previous movegen interrupt current effect.
|
||||||
|
|||||||
Reference in New Issue
Block a user