Fixes for removal mode
This commit is contained in:
BIN
Content/Prefabs/Bush1.uasset
LFS
BIN
Content/Prefabs/Bush1.uasset
LFS
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Content/Prefabs/Bush2.uasset
LFS
BIN
Content/Prefabs/Bush2.uasset
LFS
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Content/Prefabs/Bush3.uasset
LFS
BIN
Content/Prefabs/Bush3.uasset
LFS
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Content/Prefabs/Bush4.uasset
LFS
BIN
Content/Prefabs/Bush4.uasset
LFS
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Content/Prefabs/Tree2.uasset
LFS
BIN
Content/Prefabs/Tree2.uasset
LFS
Binary file not shown.
BIN
Content/Prefabs/Tree3.uasset
LFS
BIN
Content/Prefabs/Tree3.uasset
LFS
Binary file not shown.
BIN
Content/Prefabs/Tree4.uasset
LFS
BIN
Content/Prefabs/Tree4.uasset
LFS
Binary file not shown.
Binary file not shown.
@@ -4,7 +4,6 @@
|
||||
#include "MoveActorsComponent.h"
|
||||
#include "../System/Subsystem/ObjectPersistenceSubsystem.h"
|
||||
#include "../System/MainBlueprintFunctionLibrary.h"
|
||||
#include "Kismet/KismetMathLibrary.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "../GameObjects/RemovableStaticMeshActor.h"
|
||||
@@ -14,6 +13,17 @@
|
||||
#include "Engine/StaticMeshActor.h"
|
||||
#include "LandscapeProxy.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
/** Snaps a single coordinate onto the grid lattice. The grid lines run through the world origin,
|
||||
* exactly like UKismetMathLibrary::Vector_SnappedToGrid, but on one axis at a time and in double
|
||||
* precision so that the result does not depend on the size of the world. */
|
||||
double SnapCoordinateToGrid(double Coordinate, double GridSnap)
|
||||
{
|
||||
return GridSnap <= 0.0 ? Coordinate : FMath::RoundToDouble(Coordinate / GridSnap) * GridSnap;
|
||||
}
|
||||
}
|
||||
|
||||
// Sets default values for this component's properties
|
||||
UMoveActorsComponent::UMoveActorsComponent()
|
||||
{
|
||||
@@ -66,17 +76,27 @@ void UMoveActorsComponent::TickComponent(float DeltaTime, ELevelTick TickType, F
|
||||
|
||||
// Prefer the per-actor tuning values, falling back to the component defaults.
|
||||
const float Distance = MovingActor->DistanceFromActor > 0.f ? MovingActor->DistanceFromActor : PlacementDistance;
|
||||
const float GridSnap = MovingActor->GridScale > 0.f ? MovingActor->GridScale : GridSnapSize;
|
||||
|
||||
FVector PosVector = PlayerCharacter->GetActorLocation() + (ForwardVec * Distance) + FVector(0.f, 0.f, PlacementHeightOffset);
|
||||
|
||||
if (bGridMovementActive)
|
||||
{
|
||||
PosVector = UKismetMathLibrary::Vector_SnappedToGrid(PosVector, GridSnap);
|
||||
}
|
||||
// The grid pitch is the base cell size of this component grown by the whole number multiplier of the
|
||||
// object that is being moved, so an object that reserves more room still lands on the same lattice.
|
||||
const double GridSnap = GetGridPitchForActor(MovingActor);
|
||||
|
||||
const FVector TargetScale = (MovingActor->ScaleOverride != FVector::ZeroVector) ? MovingActor->ScaleOverride : MovingActor->GetActorScale();
|
||||
MovingActor->SetActorTransform(FTransform(MovingActor->GetActorRotation(), PosVector, TargetScale));
|
||||
|
||||
// The object is only snapped in the grid plane. Its height is decided by the surface probe below, so
|
||||
// snapping the hover position on Z as well would make the object jump in grid sized steps while
|
||||
// aiming, long before it ever touches a surface.
|
||||
FVector PosVector = PlayerCharacter->GetActorLocation() + (ForwardVec * Distance) + FVector(0.f, 0.f, PlacementHeightOffset);
|
||||
|
||||
FRotator TargetRotation = MovingActor->GetActorRotation();
|
||||
if (bGridMovementActive && GridRotationSnapDegrees > 0.f)
|
||||
{
|
||||
// A rotated object does not cover whole cells, which is what pulls it out of sync with the
|
||||
// objects that are placed around it, so its yaw is quantized while the grid is active.
|
||||
TargetRotation.Yaw = SnapCoordinateToGrid(TargetRotation.Yaw, GridRotationSnapDegrees);
|
||||
}
|
||||
|
||||
MovingActor->SetActorLocation(SnapFootprintToGrid(PosVector, TargetScale, TargetRotation, GridSnap));
|
||||
|
||||
// Look for a surface to rest the object on. The probe starts above the object so it can
|
||||
// also land on top of already placed actors (allowing objects to be stacked on each other)
|
||||
@@ -262,6 +282,126 @@ FBoxSphereBounds UMoveActorsComponent::GetMovingActorMeshBounds() const
|
||||
return FBoxSphereBounds(MovingActor->GetActorLocation(), FVector::ZeroVector, 0.0);
|
||||
}
|
||||
|
||||
FVector UMoveActorsComponent::SnapFootprintToGrid(const FVector& InLocation, const FVector& TargetScale, const FRotator& TargetRotation, double GridSnap)
|
||||
{
|
||||
if (!bGridMovementActive || GridSnap <= 0.0 || !IsValid(MovingActor))
|
||||
{
|
||||
return InLocation;
|
||||
}
|
||||
|
||||
// The rotation and the scale are applied before the footprint is measured: both of them change the
|
||||
// bounds that the footprint is derived from.
|
||||
MovingActor->SetActorTransform(FTransform(TargetRotation, InLocation, TargetScale));
|
||||
|
||||
if (GridFitMode == EPlaceableGridFitMode::Pivot)
|
||||
{
|
||||
return FVector(
|
||||
SnapCoordinateToGrid(InLocation.X, GridSnap),
|
||||
SnapCoordinateToGrid(InLocation.Y, GridSnap),
|
||||
InLocation.Z);
|
||||
}
|
||||
|
||||
FBoxSphereBounds MeshBounds = GetMovingActorMeshBounds();
|
||||
const FVector Size = MeshBounds.BoxExtent * 2.0;
|
||||
|
||||
// An object without a mesh has no footprint to align, so it keeps the plain pivot snap.
|
||||
if (Size.IsNearlyZero())
|
||||
{
|
||||
return FVector(
|
||||
SnapCoordinateToGrid(InLocation.X, GridSnap),
|
||||
SnapCoordinateToGrid(InLocation.Y, GridSnap),
|
||||
InLocation.Z);
|
||||
}
|
||||
|
||||
// A mesh that is not an exact multiple of the grid still owns whole cells: 175cm on a 50cm grid is
|
||||
// four cells. Whole cells keep every object on the same lattice, so neighbours are always a whole
|
||||
// number of cells apart and can never drift. Rounding up (instead of to the nearest cell) is what
|
||||
// guarantees that an object fits inside the cells it claims and can never overlap the object next to
|
||||
// it - the price is a symmetric margin inside the cell instead of a different gap for every object.
|
||||
// This is what makes it possible to use meshes that were not modelled to the grid.
|
||||
const double CellsX = GetGridCellCount(Size.X, GridSnap);
|
||||
const double CellsY = GetGridCellCount(Size.Y, GridSnap);
|
||||
|
||||
if (GridFitMode == EPlaceableGridFitMode::FootprintFitted)
|
||||
{
|
||||
// The smaller of the two axis factors is used so that the mesh never spills over a cell and its
|
||||
// proportions are kept. A mesh that would need a bigger change than GridFitMaxScaleDelta keeps
|
||||
// its margin instead of being distorted.
|
||||
const double MaxDelta = GridFitMaxScaleDelta;
|
||||
const double FitScale = FMath::Min(
|
||||
(CellsX * GridSnap) / FMath::Max(Size.X, 1.0),
|
||||
(CellsY * GridSnap) / FMath::Max(Size.Y, 1.0));
|
||||
|
||||
const double ClampedFitScale = FMath::Clamp(FitScale, 1.0 - MaxDelta, 1.0 + MaxDelta);
|
||||
if (!FMath::IsNearlyEqual(ClampedFitScale, 1.0, 0.001))
|
||||
{
|
||||
MovingActor->SetActorScale3D(TargetScale * ClampedFitScale);
|
||||
MeshBounds = GetMovingActorMeshBounds();
|
||||
}
|
||||
}
|
||||
|
||||
// The bounding box is centered in the cells it occupies instead of the pivot being put on a grid
|
||||
// line, so meshes whose pivot sits in a corner line up with the rest of the world. Z is untouched.
|
||||
const double BlockMinX = SnapCoordinateToGrid(MeshBounds.Origin.X - MeshBounds.BoxExtent.X, GridSnap);
|
||||
const double BlockMinY = SnapCoordinateToGrid(MeshBounds.Origin.Y - MeshBounds.BoxExtent.Y, GridSnap);
|
||||
|
||||
const FVector DesiredCenter(
|
||||
BlockMinX + (CellsX * GridSnap) * 0.5,
|
||||
BlockMinY + (CellsY * GridSnap) * 0.5,
|
||||
MeshBounds.Origin.Z);
|
||||
|
||||
return InLocation + (DesiredCenter - MeshBounds.Origin);
|
||||
}
|
||||
|
||||
FVector2D UMoveActorsComponent::GetMovingActorGridFootprint() const
|
||||
{
|
||||
const double GridSnap = GetGridPitchForActor(MovingActor);
|
||||
if (GridSnap <= 0.0)
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
|
||||
const FVector Size = GetMovingActorMeshBounds().BoxExtent * 2.0;
|
||||
if (Size.IsNearlyZero())
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
|
||||
return FVector2D(
|
||||
GetGridCellCount(Size.X, GridSnap),
|
||||
GetGridCellCount(Size.Y, GridSnap));
|
||||
}
|
||||
|
||||
double UMoveActorsComponent::GetGridPitchForActor(const ARemovableStaticMeshActor* Actor) const
|
||||
{
|
||||
return GetGridPitch(Actor, GridSnapSize);
|
||||
}
|
||||
|
||||
double UMoveActorsComponent::GetGridPitch(const ARemovableStaticMeshActor* Actor, double BaseGridSnapSize)
|
||||
{
|
||||
// The multiplier is clamped here instead of being trusted from the property: it is writable from
|
||||
// Blueprint, where the editor-only ClampMin does not apply, and a zero multiplier would turn the cell
|
||||
// size into zero, which reads as "no grid" in every bit of maths below.
|
||||
const double Multiplier = Actor ? static_cast<double>(Actor->GetGridSizeMultiplier()) : 1.0;
|
||||
|
||||
return BaseGridSnapSize * Multiplier;
|
||||
}
|
||||
|
||||
double UMoveActorsComponent::GetGridCellCount(double Size, double GridSnap)
|
||||
{
|
||||
if (GridSnap <= 0.0)
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// The slack keeps a mesh that is exactly as wide as a whole number of cells (or a hair wider, since
|
||||
// mesh bounds are never exact) from claiming an extra cell, which would leave a gap the size of a
|
||||
// whole cell next to it.
|
||||
constexpr double CellCountTolerance = 0.001;
|
||||
|
||||
return FMath::Max(1.0, FMath::CeilToDouble(Size / GridSnap - CellCountTolerance));
|
||||
}
|
||||
|
||||
void UMoveActorsComponent::ToggleGridMovement()
|
||||
{
|
||||
bGridMovementActive = !bGridMovementActive;
|
||||
|
||||
@@ -22,6 +22,22 @@ enum class ERemovalAction : uint8
|
||||
MAX
|
||||
};
|
||||
|
||||
/** How the object being moved is aligned with the placement grid. Meshes are rarely exact multiples of
|
||||
* the grid pitch, which is what makes placed objects drift out of sync with the objects around them. */
|
||||
UENUM(BlueprintType)
|
||||
enum class EPlaceableGridFitMode : uint8
|
||||
{
|
||||
/** Snap the actor pivot to the grid. Only correct when every pivot sits at the center of its mesh. */
|
||||
Pivot,
|
||||
/** Quantize the mesh' footprint to whole grid cells (rounded up, so the mesh always fits inside the
|
||||
* cells it claims) and center it in them. A 175cm mesh on a 50cm grid claims four cells (200cm) and
|
||||
* keeps a 12.5cm margin on either side, so neighbours can never drift apart. */
|
||||
FootprintCentered,
|
||||
/** As FootprintCentered, but the mesh is also grown (within GridFitMaxScaleDelta) so that it fills
|
||||
* the cells it occupies exactly, which removes the margin. */
|
||||
FootprintFitted
|
||||
};
|
||||
|
||||
class ARemovableStaticMeshActor;
|
||||
class AEleriPlayerController;
|
||||
class AMyCharacter;
|
||||
@@ -53,8 +69,33 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement")
|
||||
float PlacementHeightOffset = 100.f;
|
||||
|
||||
/** Default size of one grid cell, in Unreal units: 50 units is half a metre. The placed actors fall
|
||||
* back on this value when they report their footprint while this component is not around yet (in the
|
||||
* editor), so both use the same base size. */
|
||||
static constexpr float DefaultGridSnapSize = 50.f;
|
||||
|
||||
/** Size of one grid cell, in Unreal units. Every object is placed on the lattice this defines, grown
|
||||
* by the whole number multiplier that the placed actor carries (see ARemovableStaticMeshActor::
|
||||
* GridScale). Keeping the base size in one place is what keeps objects of every size on a single
|
||||
* lattice instead of letting each of them drift on a lattice of its own. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement", meta = (ClampMin = "1.0", UIMin = "1.0"))
|
||||
float GridSnapSize = DefaultGridSnapSize;
|
||||
|
||||
/** How the object being moved is aligned with the grid, see EPlaceableGridFitMode. The pivot based
|
||||
* mode is only correct when every mesh' pivot sits at the center of the mesh. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement")
|
||||
float GridSnapSize = 250.f;
|
||||
EPlaceableGridFitMode GridFitMode = EPlaceableGridFitMode::FootprintCentered;
|
||||
|
||||
/** Yaw steps the object is quantized to while the grid is active. An object that is rotated freely
|
||||
* does not cover whole cells, which pushes it out of sync with the objects placed around it.
|
||||
* Use 0 to keep free rotation. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement", meta = (ClampMin = "0.0"))
|
||||
float GridRotationSnapDegrees = 90.f;
|
||||
|
||||
/** Largest relative size change FootprintFitted is allowed to apply so that a mesh fills its cells
|
||||
* exactly. 0.15 = up to 15%. A mesh that would need more than this keeps its cell margin. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement", meta = (ClampMin = "0.0"))
|
||||
float GridFitMaxScaleDelta = 0.15f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement")
|
||||
float GroundTraceLength = 1000.f;
|
||||
@@ -106,6 +147,12 @@ protected:
|
||||
* the detection box and the interaction widget, which do not match the mesh that is placed. */
|
||||
FBoxSphereBounds GetMovingActorMeshBounds() const;
|
||||
|
||||
/** Aligns the footprint of the object being moved with the placement grid and applies the rotation
|
||||
* and the scale it is going to be placed with. Returns the location the actor has to be moved to.
|
||||
* The pivot is not used for the alignment, because it is not guaranteed to sit at the center (or
|
||||
* at the edge) of the mesh. */
|
||||
FVector SnapFootprintToGrid(const FVector& InLocation, const FVector& TargetScale, const FRotator& TargetRotation, double GridSnap);
|
||||
|
||||
|
||||
public:
|
||||
// Called every frame
|
||||
@@ -115,6 +162,28 @@ public:
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure)
|
||||
FORCEINLINE bool IsGridMovementActive() const { return bGridMovementActive; }
|
||||
|
||||
/** Footprint of the object that is currently being moved, in whole grid cells (X = cells along X,
|
||||
* Y = cells along Y). Handy for showing the size of an object in a build UI. Zero when nothing is
|
||||
* being moved. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Placement")
|
||||
FVector2D GetMovingActorGridFootprint() const;
|
||||
|
||||
/** Amount of whole grid cells a size spans. The footprint is rounded up, so that a mesh always fits
|
||||
* inside the cells it claims, with a small tolerance against the float noise that mesh bounds carry.
|
||||
* The placement and the footprint report both use this, so the two can never disagree. */
|
||||
static double GetGridCellCount(double Size, double GridSnap);
|
||||
|
||||
/** Size of one grid cell for an actor: the base cell size of this component scaled by the whole
|
||||
* number GridScale multiplier of the actor, so 1 keeps the base grid and 3 gives 150 units cells on
|
||||
* a 50 units grid. The multiplier is applied here instead of being trusted from the property, so a
|
||||
* stray zero (the property is writable from Blueprint as well) can not shrink the grid to nothing. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Placement")
|
||||
double GetGridPitchForActor(const ARemovableStaticMeshActor* Actor) const;
|
||||
|
||||
/** As GetGridPitchForActor, but against an explicit base cell size. The actors use this to report
|
||||
* their footprint while the component that owns the base size is not available (in the editor). */
|
||||
static double GetGridPitch(const ARemovableStaticMeshActor* Actor, double BaseGridSnapSize);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure)
|
||||
bool IsRemovingModePossible() const { return bRemovalModePossible; }
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure)
|
||||
|
||||
@@ -154,10 +154,44 @@ void ARemovableStaticMeshActor::Place(bool ReturnToOldLocation, bool bForce)
|
||||
void ARemovableStaticMeshActor::Rotate(bool bPositive)
|
||||
{
|
||||
FRotator CurrentRotation = this->GetActorRotation();
|
||||
CurrentRotation.Yaw += bPositive ? 45.f : -45.f;
|
||||
CurrentRotation.Yaw += bPositive ? 90.f : -90.f;
|
||||
this->SetActorRotation(CurrentRotation);
|
||||
}
|
||||
|
||||
void ARemovableStaticMeshActor::ReportGridFootprint()
|
||||
{
|
||||
// The base cell size is a property of the Move Actors component, which only exists at runtime. While
|
||||
// editing a placed actor the component is missing, so the class default is used, which is the value
|
||||
// the placement uses unless the level overrides it.
|
||||
const double BaseGridSnapSize = MoveableComponent ? MoveableComponent->GridSnapSize : UMoveActorsComponent::DefaultGridSnapSize;
|
||||
const double Pitch = UMoveActorsComponent::GetGridPitch(this, BaseGridSnapSize);
|
||||
|
||||
if (Pitch <= 0.0)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("%s: no grid pitch to report against. Set GridSnapSize on the Move Actors component."), *GetName());
|
||||
return;
|
||||
}
|
||||
|
||||
const UStaticMeshComponent* MeshComponent = GetStaticMeshComponent();
|
||||
if (!MeshComponent || !MeshComponent->GetStaticMesh())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("%s: no static mesh assigned."), *GetName());
|
||||
return;
|
||||
}
|
||||
|
||||
const FBoxSphereBounds MeshBounds = MeshComponent->CalcBounds(MeshComponent->GetComponentTransform());
|
||||
const FVector Size = MeshBounds.BoxExtent * 2.0;
|
||||
// The same cell rule that the placement uses, so the reported margin is the margin that the object
|
||||
// will actually keep on the grid.
|
||||
const double CellsX = UMoveActorsComponent::GetGridCellCount(Size.X, Pitch);
|
||||
const double CellsY = UMoveActorsComponent::GetGridCellCount(Size.Y, Pitch);
|
||||
const double FillScale = FMath::Max((CellsX * Pitch) / FMath::Max(Size.X, 1.0), (CellsY * Pitch) / FMath::Max(Size.Y, 1.0));
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("%s: mesh %.1f x %.1f on a %.1f grid (%d x %.1f base) -> %.0f x %.0f cells (margin %.1f x %.1f, fill scale %.2f)"),
|
||||
*GetName(), Size.X, Size.Y, Pitch, GetGridSizeMultiplier(), BaseGridSnapSize, CellsX, CellsY,
|
||||
CellsX * Pitch - Size.X, CellsY * Pitch - Size.Y, FillScale);
|
||||
}
|
||||
|
||||
void ARemovableStaticMeshActor::ReturnToInventory()
|
||||
{
|
||||
if (InventoryRef)
|
||||
|
||||
@@ -68,8 +68,19 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float DistanceFromActor;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float GridScale;
|
||||
/** Whole number multiplier on the placement grid: the object is placed on cells that are
|
||||
* GridSnapSize * GridScale units, so 1 (the default) keeps the grid the Move Actors component
|
||||
* defines and 3 reserves 150 units blocks on a 50 units grid. Keep it a whole number: a fractional
|
||||
* multiplier puts the object on a lattice of its own that only lines up with everything around it
|
||||
* every now and then, which is exactly what makes placed objects drift. The value is clamped to at
|
||||
* least 1 where it is used, a zero sized cell would mean "no grid at all". */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "1", UIMin = "1"))
|
||||
int32 GridScale = 1;
|
||||
|
||||
/** GridScale with its lower bound applied. The property is writable from Blueprint, where ClampMin
|
||||
* does not apply, so the value is sanitised at the point of use instead of being trusted. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure)
|
||||
FORCEINLINE int32 GetGridSizeMultiplier() const { return FMath::Max(1, GridScale); }
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
UMaterialInterface* CanPlaceMaterial;
|
||||
@@ -92,6 +103,12 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Removable Object")
|
||||
void UpdateOverlapBoxExtent();
|
||||
|
||||
/** Logs how the mesh of this actor relates to the placement grid: its size, the amount of cells it
|
||||
* occupies and the scale that would make it fill those cells. Meshes do not have to be modelled to
|
||||
* the grid, this only makes the margin they keep visible so that near misses stand out. */
|
||||
UFUNCTION(CallInEditor, BlueprintCallable, Category = "Removable Object")
|
||||
void ReportGridFootprint();
|
||||
|
||||
//Used for movable actors to override their box extent for checking collision
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Removable Object")
|
||||
FVector BoxExtentOverride = FVector::ZeroVector;
|
||||
|
||||
Reference in New Issue
Block a user