forked from 0ad/0ad
Optimised game-loading, by not reading terrain textures or calculating bounding boxes when not necessary; maps load about twenty seconds faster (at least in Debug mode). Also fixed possible minor bug (FLT_MIN vs -FLT_MAX) and other trivial things.
This was SVN commit r1917.
This commit is contained in:
parent
614e523c89
commit
3b1395d80b
@ -125,6 +125,17 @@ static CVector3D SkinPoint(const CVector3D& pos,const SVertexBlend& blend,
|
||||
// CalcBound: calculate the world space bounds of this model
|
||||
void CModel::CalcBounds()
|
||||
{
|
||||
// Need to calculate the object bounds first, if that hasn't already been done
|
||||
if (! m_Anim)
|
||||
CalcObjectBounds();
|
||||
else
|
||||
{
|
||||
if (m_Anim->m_ObjectBounds.IsEmpty())
|
||||
CalcAnimatedObjectBound(m_Anim->m_AnimDef, m_Anim->m_ObjectBounds);
|
||||
assert(! m_Anim->m_ObjectBounds.IsEmpty()); // (if this happens, it'll be recalculating the bounds every time)
|
||||
m_ObjectBounds = m_Anim->m_ObjectBounds;
|
||||
}
|
||||
|
||||
m_ObjectBounds.Transform(GetTransform(),m_Bounds);
|
||||
}
|
||||
|
||||
@ -148,13 +159,24 @@ void CModel::CalcAnimatedObjectBound(CSkeletonAnimDef* anim,CBound& result)
|
||||
{
|
||||
result.SetEmpty();
|
||||
|
||||
CSkeletonAnim dummyanim;
|
||||
dummyanim.m_AnimDef=anim;
|
||||
if (!SetAnimation(&dummyanim)) return;
|
||||
// Set the current animation on which to perform calculations (if it's necessary)
|
||||
if (anim != m_Anim->m_AnimDef)
|
||||
{
|
||||
CSkeletonAnim dummyanim;
|
||||
dummyanim.m_AnimDef=anim;
|
||||
if (!SetAnimation(&dummyanim)) return;
|
||||
}
|
||||
|
||||
int numverts=m_pModelDef->GetNumVertices();
|
||||
SModelVertex* verts=m_pModelDef->GetVertices();
|
||||
|
||||
|
||||
// Remove any transformations, so that we calculate the bounding box
|
||||
// at the origin. The box is later re-transformed onto the object, without
|
||||
// having to recalculate the size of the box.
|
||||
CMatrix3D transform, oldtransform = GetTransform();
|
||||
transform.SetIdentity();
|
||||
SetTransform(transform);
|
||||
|
||||
// iterate through every frame of the animation
|
||||
for (uint j=0;j<anim->GetNumFrames();j++) {
|
||||
// extend bounds by vertex positions at the frame
|
||||
@ -166,6 +188,8 @@ void CModel::CalcAnimatedObjectBound(CSkeletonAnimDef* anim,CBound& result)
|
||||
m_AnimTime+=anim->GetFrameTime();
|
||||
m_BoneMatricesValid=false;
|
||||
}
|
||||
|
||||
SetTransform(oldtransform);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -179,7 +203,8 @@ CSkeletonAnim* CModel::BuildAnimation(const char* filename,float speed)
|
||||
CSkeletonAnim* anim=new CSkeletonAnim;
|
||||
anim->m_AnimDef=def;
|
||||
anim->m_Speed=speed;
|
||||
CalcAnimatedObjectBound(def,anim->m_ObjectBounds);
|
||||
anim->m_ObjectBounds.SetEmpty();
|
||||
InvalidateBounds();
|
||||
|
||||
return anim;
|
||||
}
|
||||
@ -268,13 +293,15 @@ bool CModel::SetAnimation(CSkeletonAnim* anim, bool once)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (anim->m_AnimDef->GetNumKeys()!=m_pModelDef->GetNumBones()) {
|
||||
// mismatch between models skeleton and animations skeleton
|
||||
if (m_Anim->m_AnimDef->GetNumKeys()!=m_pModelDef->GetNumBones()) {
|
||||
// mismatch between model's skeleton and animation's skeleton
|
||||
return false;
|
||||
}
|
||||
|
||||
// update object bounds to the bounds when given animation applied
|
||||
m_ObjectBounds=m_Anim->m_ObjectBounds;
|
||||
// reset the cached bounds when the animation is changed
|
||||
m_ObjectBounds.SetEmpty();
|
||||
InvalidateBounds();
|
||||
|
||||
// start anim from beginning
|
||||
m_AnimTime=0;
|
||||
}
|
||||
@ -348,6 +375,7 @@ void CModel::SetTransform(const CMatrix3D& transform)
|
||||
// call base class to set transform on this object
|
||||
CRenderableObject::SetTransform(transform);
|
||||
m_BoneMatricesValid=false;
|
||||
InvalidateBounds();
|
||||
|
||||
// now set transforms on props
|
||||
const CMatrix3D* bonematrices=GetBoneMatrices();
|
||||
|
@ -85,8 +85,6 @@ public:
|
||||
void CalcObjectBounds();
|
||||
// calculate bounds encompassing all vertex positions for given animation
|
||||
void CalcAnimatedObjectBound(CSkeletonAnimDef* anim,CBound& result);
|
||||
// return object space bounds
|
||||
const CBound& GetObjectBounds() const { return m_ObjectBounds; }
|
||||
|
||||
// set transform of this object, and recurse down into props to update their world space transform
|
||||
void SetTransform(const CMatrix3D& transform);
|
||||
@ -134,7 +132,8 @@ private:
|
||||
// pointer to the model's raw 3d data
|
||||
CModelDefPtr m_pModelDef;
|
||||
// object space bounds of model - accounts for bounds of all possible animations
|
||||
// that can play on a model
|
||||
// that can play on a model. Not always up-to-date - currently CalcBounds()
|
||||
// updates it when necessary.
|
||||
CBound m_ObjectBounds;
|
||||
// animation currently playing on this model, if any
|
||||
CSkeletonAnim* m_Anim;
|
||||
|
@ -135,9 +135,6 @@ bool CObjectEntry::BuildModel()
|
||||
m_Model->SetFlags(m_Model->GetFlags()|MODELFLAG_CASTSHADOWS);
|
||||
}
|
||||
|
||||
// build world space bounds
|
||||
m_Model->CalcBounds();
|
||||
|
||||
// replace any units using old model to now use new model; also reprop models, if necessary
|
||||
// FIXME, RC - ugh, doesn't recurse correctly through props
|
||||
const std::vector<CUnit*>& units=g_UnitMan.GetUnits();
|
||||
|
@ -42,7 +42,7 @@ void CPatch::Initialize(CTerrain* parent,u32 x,u32 z)
|
||||
}
|
||||
}
|
||||
|
||||
CalcBounds();
|
||||
InvalidateBounds();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -42,7 +42,7 @@ class CRenderableObject
|
||||
{
|
||||
public:
|
||||
// constructor
|
||||
CRenderableObject() : m_RenderData(0) {
|
||||
CRenderableObject() : m_RenderData(0), m_BoundsValid(false) {
|
||||
m_Transform.SetIdentity();
|
||||
}
|
||||
// destructor
|
||||
@ -55,8 +55,8 @@ public:
|
||||
m_Transform.GetInverse(m_InvTransform);
|
||||
// normal recalculation likely required on transform change; flag it
|
||||
SetDirty(RENDERDATA_UPDATE_VERTICES);
|
||||
// rebuild world space bounds
|
||||
CalcBounds();
|
||||
// need to rebuild world space bounds
|
||||
InvalidateBounds();
|
||||
}
|
||||
// get object to world space transform
|
||||
const CMatrix3D& GetTransform() const { return m_Transform; }
|
||||
@ -71,10 +71,18 @@ public:
|
||||
|
||||
// calculate (and store in m_Bounds) the world space bounds of this object
|
||||
// - must be implemented by all concrete subclasses
|
||||
virtual void CalcBounds() = 0;
|
||||
virtual void CalcBounds() = 0;
|
||||
|
||||
// return world space bounds of this object
|
||||
const CBound& GetBounds() const { return m_Bounds; }
|
||||
const CBound& GetBounds() {
|
||||
if (! m_BoundsValid) {
|
||||
CalcBounds();
|
||||
m_BoundsValid = true;
|
||||
}
|
||||
return m_Bounds;
|
||||
}
|
||||
|
||||
void InvalidateBounds() { m_BoundsValid = false; }
|
||||
|
||||
// set the object renderdata
|
||||
// TODO,RC 10/04/04 - need to delete existing renderdata here, or can we
|
||||
@ -98,6 +106,10 @@ protected:
|
||||
CMatrix3D m_InvTransform;
|
||||
// object renderdata
|
||||
CRenderData* m_RenderData;
|
||||
|
||||
private:
|
||||
// remembers whether m_bounds needs to be recalculated
|
||||
bool m_BoundsValid;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -326,7 +326,7 @@ void CTerrain::SetHeightMap(u16* heightmap)
|
||||
for (u32 j=0;j<m_MapSizePatches;j++) {
|
||||
for (u32 i=0;i<m_MapSizePatches;i++) {
|
||||
CPatch* patch=GetPatch(i,j);
|
||||
patch->CalcBounds();
|
||||
patch->InvalidateBounds();
|
||||
patch->SetDirty(RENDERDATA_UPDATE_VERTICES);
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,9 @@ public:
|
||||
// accessor - return texture type
|
||||
int GetType() const { return m_Type; }
|
||||
|
||||
// returns whether this texture-entry has loaded any data yet
|
||||
bool IsLoaded() { return (m_Handle!=-1); }
|
||||
|
||||
private:
|
||||
// load texture from file
|
||||
void LoadTexture();
|
||||
|
@ -57,7 +57,11 @@ CTextureEntry* CTextureManager::FindTexture(Handle handle)
|
||||
for (uint k=0;k<m_TerrainTextures.size();k++) {
|
||||
STextureType& ttype=m_TerrainTextures[k];
|
||||
for (uint i=0;i<ttype.m_Textures.size();i++) {
|
||||
if (handle==ttype.m_Textures[i]->GetHandle()) {
|
||||
// Don't bother looking at textures that haven't been loaded yet - since
|
||||
// the caller has given us a Handle to the texture, it must be loaded.
|
||||
// (This matters because GetHandle would load the texture, even though
|
||||
// there's no need to.)
|
||||
if (ttype.m_Textures[i]->IsLoaded() && handle==ttype.m_Textures[i]->GetHandle()) {
|
||||
return ttype.m_Textures[i];
|
||||
}
|
||||
}
|
||||
|
@ -47,90 +47,98 @@ CBound& CBound::operator+=(const CVector3D& pt)
|
||||
// otherwise
|
||||
// note: incoming ray direction must be normalised
|
||||
bool CBound::RayIntersect(const CVector3D& origin,const CVector3D& dir,
|
||||
float& tmin,float& tmax) const
|
||||
float& tmin,float& tmax) const
|
||||
{
|
||||
float t1,t2;
|
||||
float tnear,tfar;
|
||||
float tnear,tfar;
|
||||
|
||||
if (dir[0]==0) {
|
||||
if (origin[0]<m_Data[0][0] || origin[0]>m_Data[1][0])
|
||||
return false;
|
||||
else {
|
||||
tnear=(float) FLT_MIN;
|
||||
tfar=(float) FLT_MAX;
|
||||
}
|
||||
} else {
|
||||
t1=(m_Data[0][0]-origin[0])/dir[0];
|
||||
t2=(m_Data[1][0]-origin[0])/dir[0];
|
||||
|
||||
if (dir[0]<0) {
|
||||
tnear = t2;
|
||||
tfar = t1;
|
||||
} else {
|
||||
tnear = t1;
|
||||
tfar = t2;
|
||||
}
|
||||
|
||||
if (tfar<0)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dir[1]==0 && (origin[1]<m_Data[0][1] || origin[1]>m_Data[1][1]))
|
||||
return false;
|
||||
else {
|
||||
t1=(m_Data[0][1]-origin[1])/dir[1];
|
||||
t2=(m_Data[1][1]-origin[1])/dir[1];
|
||||
|
||||
if (dir[1]<0) {
|
||||
if (t2>tnear)
|
||||
tnear = t2;
|
||||
if (t1<tfar)
|
||||
tfar = t1;
|
||||
} else {
|
||||
if (t1>tnear)
|
||||
tnear = t1;
|
||||
if (t2<tfar)
|
||||
tfar = t2;
|
||||
}
|
||||
|
||||
if (tnear>tfar || tfar<0)
|
||||
if (dir[0]==0) {
|
||||
if (origin[0]<m_Data[0][0] || origin[0]>m_Data[1][0])
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
tnear=(float) -FLT_MAX;
|
||||
tfar=(float) FLT_MAX;
|
||||
}
|
||||
} else {
|
||||
t1=(m_Data[0][0]-origin[0])/dir[0];
|
||||
t2=(m_Data[1][0]-origin[0])/dir[0];
|
||||
|
||||
if (dir[0]<0) {
|
||||
tnear = t2;
|
||||
tfar = t1;
|
||||
} else {
|
||||
tnear = t1;
|
||||
tfar = t2;
|
||||
}
|
||||
|
||||
if (tfar<0)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dir[1]==0 && (origin[1]<m_Data[0][1] || origin[1]>m_Data[1][1]))
|
||||
return false;
|
||||
else {
|
||||
t1=(m_Data[0][1]-origin[1])/dir[1];
|
||||
t2=(m_Data[1][1]-origin[1])/dir[1];
|
||||
|
||||
if (dir[1]<0) {
|
||||
if (t2>tnear)
|
||||
tnear = t2;
|
||||
if (t1<tfar)
|
||||
tfar = t1;
|
||||
} else {
|
||||
if (t1>tnear)
|
||||
tnear = t1;
|
||||
if (t2<tfar)
|
||||
tfar = t2;
|
||||
}
|
||||
|
||||
if (tnear>tfar || tfar<0)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dir[2]==0 && (origin[2]<m_Data[0][2] || origin[2]>m_Data[1][2]))
|
||||
return false;
|
||||
else {
|
||||
t1=(m_Data[0][2]-origin[2])/dir[2];
|
||||
t2=(m_Data[1][2]-origin[2])/dir[2];
|
||||
|
||||
if (dir[2]==0 && (origin[2]<m_Data[0][2] || origin[2]>m_Data[1][2]))
|
||||
return false;
|
||||
else {
|
||||
t1=(m_Data[0][2]-origin[2])/dir[2];
|
||||
t2=(m_Data[1][2]-origin[2])/dir[2];
|
||||
|
||||
if (dir[2]<0) {
|
||||
if (t2>tnear)
|
||||
tnear = t2;
|
||||
if (t1<tfar)
|
||||
tfar = t1;
|
||||
} else {
|
||||
if (t1>tnear)
|
||||
tnear = t1;
|
||||
if (t2<tfar)
|
||||
tfar = t2;
|
||||
}
|
||||
if (t2>tnear)
|
||||
tnear = t2;
|
||||
if (t1<tfar)
|
||||
tfar = t1;
|
||||
} else {
|
||||
if (t1>tnear)
|
||||
tnear = t1;
|
||||
if (t2<tfar)
|
||||
tfar = t2;
|
||||
}
|
||||
|
||||
if (tnear>tfar || tfar<0)
|
||||
return false;
|
||||
}
|
||||
if (tnear>tfar || tfar<0)
|
||||
return false;
|
||||
}
|
||||
|
||||
tmin=tnear;
|
||||
tmax=tfar;
|
||||
tmin=tnear;
|
||||
tmax=tfar;
|
||||
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// SetEmpty: initialise this bound as empty
|
||||
void CBound::SetEmpty()
|
||||
{
|
||||
m_Data[0]=CVector3D(FLT_MAX,FLT_MAX,FLT_MAX);
|
||||
m_Data[1]=CVector3D(FLT_MIN,FLT_MIN,FLT_MIN);
|
||||
m_Data[0]=CVector3D( FLT_MAX, FLT_MAX, FLT_MAX);
|
||||
m_Data[1]=CVector3D(-FLT_MAX,-FLT_MAX,-FLT_MAX);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// IsEmpty: tests whether this bound is empty
|
||||
bool CBound::IsEmpty()
|
||||
{
|
||||
return (m_Data[0].X == FLT_MAX && m_Data[0].Y == FLT_MAX && m_Data[0].Z == FLT_MAX
|
||||
&& m_Data[1].X == -FLT_MAX && m_Data[1].Y == -FLT_MAX && m_Data[1].Z == -FLT_MAX);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
@ -141,23 +149,23 @@ void CBound::Transform(const CMatrix3D& m,CBound& result) const
|
||||
{
|
||||
assert(this!=&result);
|
||||
|
||||
for (int i=0;i<3;++i) {
|
||||
// handle translation
|
||||
result[0][i]=result[1][i]=m(i,3);
|
||||
for (int i=0;i<3;++i) {
|
||||
// handle translation
|
||||
result[0][i]=result[1][i]=m(i,3);
|
||||
|
||||
// Now find the extreme points by considering the product of the
|
||||
// min and max with each component of matrix
|
||||
for(int j=0;j<3;j++) {
|
||||
float a=m(j,i)*m_Data[0][j];
|
||||
float b=m(j,i)*m_Data[1][j];
|
||||
// Now find the extreme points by considering the product of the
|
||||
// min and max with each component of matrix
|
||||
for(int j=0;j<3;j++) {
|
||||
float a=m(j,i)*m_Data[0][j];
|
||||
float b=m(j,i)*m_Data[1][j];
|
||||
|
||||
if (a<b) {
|
||||
result[0][i]+=a;
|
||||
result[1][i]+=b;
|
||||
} else {
|
||||
result[0][i]+=b;
|
||||
result[1][i]+=a;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (a<b) {
|
||||
result[0][i]+=a;
|
||||
result[1][i]+=b;
|
||||
} else {
|
||||
result[0][i]+=b;
|
||||
result[1][i]+=a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,22 +18,23 @@
|
||||
class CBound
|
||||
{
|
||||
public:
|
||||
CBound() {}
|
||||
CBound(const CVector3D& min,const CVector3D& max) {
|
||||
CBound() {}
|
||||
CBound(const CVector3D& min,const CVector3D& max) {
|
||||
m_Data[0]=min; m_Data[1]=max;
|
||||
}
|
||||
|
||||
|
||||
void Transform(const CMatrix3D& m,CBound& result) const;
|
||||
|
||||
CVector3D& operator[](int index) { return m_Data[index]; }
|
||||
const CVector3D& operator[](int index) const { return m_Data[index]; }
|
||||
const CVector3D& operator[](int index) const { return m_Data[index]; }
|
||||
|
||||
void SetEmpty();
|
||||
bool IsEmpty();
|
||||
|
||||
CBound& operator+=(const CBound& b);
|
||||
CBound& operator+=(const CVector3D& pt);
|
||||
|
||||
bool RayIntersect(const CVector3D& origin,const CVector3D& dir,float& tmin,float& tmax) const;
|
||||
bool RayIntersect(const CVector3D& origin,const CVector3D& dir,float& tmin,float& tmax) const;
|
||||
|
||||
// return the volume of this bounding box
|
||||
float GetVolume() const {
|
||||
@ -47,7 +48,7 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
CVector3D m_Data[2];
|
||||
CVector3D m_Data[2];
|
||||
};
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
@ -13,13 +13,6 @@
|
||||
|
||||
#include "Vector3D.h"
|
||||
|
||||
CVector3D::CVector3D (float x, float y, float z)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Z = z;
|
||||
}
|
||||
|
||||
int CVector3D::operator ! () const
|
||||
{
|
||||
if (X != 0.0f ||
|
||||
@ -34,13 +27,7 @@ int CVector3D::operator ! () const
|
||||
//vector addition
|
||||
CVector3D CVector3D::operator + (const CVector3D &vector) const
|
||||
{
|
||||
CVector3D Temp;
|
||||
|
||||
Temp.X = X + vector.X;
|
||||
Temp.Y = Y + vector.Y;
|
||||
Temp.Z = Z + vector.Z;
|
||||
|
||||
return Temp;
|
||||
return CVector3D(X+vector.X, Y+vector.Y, Z+vector.Z);
|
||||
}
|
||||
|
||||
//vector addition/assignment
|
||||
@ -56,25 +43,13 @@ CVector3D &CVector3D::operator += (const CVector3D &vector)
|
||||
//vector subtraction
|
||||
CVector3D CVector3D::operator - (const CVector3D &vector) const
|
||||
{
|
||||
CVector3D Temp;
|
||||
|
||||
Temp.X = X - vector.X;
|
||||
Temp.Y = Y - vector.Y;
|
||||
Temp.Z = Z - vector.Z;
|
||||
|
||||
return Temp;
|
||||
return CVector3D(X-vector.X, Y-vector.Y, Z-vector.Z);
|
||||
}
|
||||
|
||||
//vector negation
|
||||
CVector3D CVector3D::operator-() const
|
||||
{
|
||||
CVector3D Temp;
|
||||
|
||||
Temp.X = -X;
|
||||
Temp.Y = -Y;
|
||||
Temp.Z = -Z;
|
||||
|
||||
return Temp;
|
||||
return CVector3D(-X, -Y, -Z);
|
||||
}
|
||||
//vector subtrcation/assignment
|
||||
CVector3D &CVector3D::operator -= (const CVector3D &vector)
|
||||
@ -89,13 +64,7 @@ CVector3D &CVector3D::operator -= (const CVector3D &vector)
|
||||
//scalar multiplication
|
||||
CVector3D CVector3D::operator * (float value) const
|
||||
{
|
||||
CVector3D Temp;
|
||||
|
||||
Temp.X = X * value;
|
||||
Temp.Y = Y * value;
|
||||
Temp.Z = Z * value;
|
||||
|
||||
return Temp;
|
||||
return CVector3D(X*value, Y*value, Z*value);
|
||||
}
|
||||
|
||||
//scalar multiplication/assignment
|
||||
|
@ -22,8 +22,8 @@ class CVector3D
|
||||
float X, Y, Z;
|
||||
|
||||
public:
|
||||
CVector3D () { X = 0.0f; Y = 0.0f; Z = 0.0f; }
|
||||
CVector3D (float x, float y, float z);
|
||||
CVector3D () : X(0.0f), Y(0.0f), Z(0.0f) {}
|
||||
CVector3D (float x, float y, float z) : X(x), Y(y), Z(z) {}
|
||||
|
||||
int operator!() const;
|
||||
|
||||
|
@ -44,7 +44,7 @@ void CModelRData::Build()
|
||||
// build data
|
||||
BuildVertices();
|
||||
BuildIndices();
|
||||
// force a texture load on models texture
|
||||
// force a texture load on model's texture
|
||||
g_Renderer.LoadTexture(m_Model->GetTexture(),GL_CLAMP_TO_EDGE);
|
||||
// setup model render flags
|
||||
/*if (g_Renderer.IsTextureTransparent(m_Model->GetTexture())) {
|
||||
|
@ -47,7 +47,7 @@ bool sparsePathTree::slice()
|
||||
|
||||
if( len == 0.0f )
|
||||
{
|
||||
// Too wierd. (Heavy traffic, obstacles in positions leading to this degenerate state.
|
||||
// Too weird. (Heavy traffic, obstacles in positions leading to this degenerate state.
|
||||
type = SPF_IMPOSSIBLE;
|
||||
return( true );
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user