Add dust particles on under-construction buildings, varying based on number of active builders.
This was SVN commit r9175.
This commit is contained in:
parent
2fd4c62cbb
commit
d03559f2c9
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<actor version="1">
|
||||
<group>
|
||||
<variant name="Base">
|
||||
<particles file="construction_dust.xml"/>
|
||||
</variant>
|
||||
</group>
|
||||
<material>basic_trans.xml</material>
|
||||
</actor>
|
@ -16,6 +16,7 @@
|
||||
<variant frequency="1">
|
||||
<props>
|
||||
<prop actor="props/structures/decals/dirt_small.xml" attachpoint="root"/>
|
||||
<prop actor="particle/construction_dust.xml" attachpoint="root"/>
|
||||
</props>
|
||||
</variant>
|
||||
</group>
|
||||
|
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<particles>
|
||||
|
||||
<texture>art/textures/particles/dust.png</texture>
|
||||
<blend mode="over"/>
|
||||
|
||||
<expr name="emissionrate" from="numbuilders" mul="5.0" max="50.0"/>
|
||||
|
||||
<uniform name="lifetime" min="4.0" max="6.0"/>
|
||||
|
||||
<uniform name="position.x" min="-8.0" max="8.0"/>
|
||||
<uniform name="position.z" min="-8.0" max="8.0"/>
|
||||
<constant name="position.y" value="0.0"/>
|
||||
|
||||
<uniform name="angle" min="-3.14" max="3.14"/>
|
||||
|
||||
<uniform name="velocity.x" min="-1.5" max="1.5"/>
|
||||
<uniform name="velocity.y" min="4.0" max="4.5"/>
|
||||
<uniform name="velocity.z" min="-1.5" max="1.5"/>
|
||||
<uniform name="velocity.angle" min="-2.0" max="2.0"/>
|
||||
|
||||
<uniform name="size" min="2.5" max="3.5"/>
|
||||
|
||||
<uniform name="color.r" min="0.5" max="1.0"/>
|
||||
<copy name="color.g" from="color.r"/>
|
||||
<copy name="color.b" from="color.r"/>
|
||||
|
||||
<force y="-3"/>
|
||||
|
||||
</particles>
|
BIN
binaries/data/mods/public/art/textures/particles/dust.png
(Stored with Git LFS)
Normal file
BIN
binaries/data/mods/public/art/textures/particles/dust.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -13,6 +13,22 @@ Foundation.prototype.Init = function()
|
||||
this.committed = false;
|
||||
|
||||
this.buildProgress = 0.0; // 0 <= progress <= 1
|
||||
|
||||
// Set up a timer so we can count the number of builders in a 1-second period.
|
||||
// (We assume each builder only builds once per second, which is what UnitAI
|
||||
// implements.)
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
this.timer = cmpTimer.SetInterval(this.entity, IID_Foundation, "UpdateTimeout", 1000, 1000, {});
|
||||
this.recentBuilders = []; // builder entities since the last timeout
|
||||
this.numRecentBuilders = 0; // number of builder entities as of the last timeout
|
||||
};
|
||||
|
||||
Foundation.prototype.UpdateTimeout = function()
|
||||
{
|
||||
this.numRecentBuilders = this.recentBuilders.length;
|
||||
this.recentBuilders = [];
|
||||
|
||||
Engine.QueryInterface(this.entity, IID_Visual).SetVariable("numbuilders", this.numRecentBuilders);
|
||||
};
|
||||
|
||||
Foundation.prototype.InitialiseConstruction = function(owner, template)
|
||||
@ -61,6 +77,10 @@ Foundation.prototype.OnDestroy = function()
|
||||
if (scaled)
|
||||
cmpPlayer.AddResource(r, scaled);
|
||||
}
|
||||
|
||||
// Reset the timer
|
||||
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
|
||||
cmpTimer.CancelTimer(this.timer);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -119,6 +139,9 @@ Foundation.prototype.Build = function(builderEnt, work)
|
||||
var cmpCost = Engine.QueryInterface(this.entity, IID_Cost);
|
||||
var amount = work / cmpCost.GetBuildTime();
|
||||
|
||||
// Record this builder so we can count the total number
|
||||
this.recentBuilders.push(builderEnt);
|
||||
|
||||
// TODO: implement some kind of diminishing returns for multiple builders.
|
||||
// e.g. record the set of entities that build this, then every ~2 seconds
|
||||
// count them (and reset the list), and apply some function to the count to get
|
||||
|
@ -127,6 +127,12 @@ public:
|
||||
m_Props[i].m_Model->SetTerrainDirty(i0, j0, i1, j1);
|
||||
}
|
||||
|
||||
virtual void SetEntityVariable(const std::string& name, float value)
|
||||
{
|
||||
for (size_t i = 0; i < m_Props.size(); ++i)
|
||||
m_Props[i].m_Model->SetEntityVariable(name, value);
|
||||
}
|
||||
|
||||
// calculate object space bounds of this model, based solely on vertex positions
|
||||
void CalcObjectBounds();
|
||||
// calculate bounds encompassing all vertex positions for given animation
|
||||
|
@ -69,6 +69,12 @@ public:
|
||||
*/
|
||||
virtual void SetTerrainDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1) = 0;
|
||||
|
||||
/**
|
||||
* Called when the entity tries to set some variable to affect the display of this model
|
||||
* and/or its child objects.
|
||||
*/
|
||||
virtual void SetEntityVariable(const std::string& UNUSED(name), float UNUSED(value)) { }
|
||||
|
||||
/**
|
||||
* Ensure that both the transformation and the bone
|
||||
* matrices are correct for this model and all its props.
|
||||
|
@ -172,6 +172,11 @@ void CParticleEmitter::AddParticle(const SParticle& particle)
|
||||
m_NextParticleIdx = (m_NextParticleIdx + 1) % m_Type->m_MaxParticles;
|
||||
}
|
||||
|
||||
void CParticleEmitter::SetEntityVariable(const std::string& name, float value)
|
||||
{
|
||||
m_EntityVariables[name] = value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
CModelParticleEmitter::CModelParticleEmitter(const CParticleEmitterTypePtr& type) :
|
||||
@ -185,6 +190,11 @@ CModelParticleEmitter::~CModelParticleEmitter()
|
||||
m_Emitter->Unattach(m_Emitter);
|
||||
}
|
||||
|
||||
void CModelParticleEmitter::SetEntityVariable(const std::string& name, float value)
|
||||
{
|
||||
m_Emitter->SetEntityVariable(name, value);
|
||||
}
|
||||
|
||||
CModelAbstract* CModelParticleEmitter::Clone() const
|
||||
{
|
||||
return new CModelParticleEmitter(m_Type);
|
||||
|
@ -107,9 +107,7 @@ public:
|
||||
*/
|
||||
void Unattach(const CParticleEmitterPtr& self);
|
||||
|
||||
private:
|
||||
friend class CParticleEmitterType;
|
||||
friend struct EmitterHasNoParticles;
|
||||
void SetEntityVariable(const std::string& name, float value);
|
||||
|
||||
CParticleEmitterTypePtr m_Type;
|
||||
|
||||
@ -118,12 +116,15 @@ private:
|
||||
|
||||
CVector3D m_Pos;
|
||||
|
||||
std::map<std::string, float> m_EntityVariables;
|
||||
|
||||
std::vector<SParticle> m_Particles;
|
||||
size_t m_NextParticleIdx;
|
||||
|
||||
float m_LastUpdateTime;
|
||||
float m_EmissionTimer;
|
||||
|
||||
private:
|
||||
VertexArray m_VertexArray;
|
||||
VertexArray::Attribute m_AttributePos;
|
||||
VertexArray::Attribute m_AttributeAxis;
|
||||
@ -157,6 +158,8 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
virtual void SetEntityVariable(const std::string& name, float value);
|
||||
|
||||
virtual void CalcBounds();
|
||||
virtual void ValidatePosition();
|
||||
virtual void InvalidatePosition();
|
||||
|
@ -32,6 +32,7 @@
|
||||
|
||||
#include <boost/random/uniform_real.hpp>
|
||||
|
||||
|
||||
/**
|
||||
* Interface for particle state variables, which get evaluated for each newly
|
||||
* constructed particle.
|
||||
@ -43,9 +44,9 @@ public:
|
||||
virtual ~IParticleVar() {}
|
||||
|
||||
/// Computes and returns a new value.
|
||||
float Evaluate(CParticleEmitterType& type)
|
||||
float Evaluate(CParticleEmitter& emitter)
|
||||
{
|
||||
m_LastValue = Compute(type);
|
||||
m_LastValue = Compute(*emitter.m_Type, emitter);
|
||||
return m_LastValue;
|
||||
}
|
||||
|
||||
@ -64,7 +65,7 @@ public:
|
||||
virtual float Max(CParticleEmitterType& type) = 0;
|
||||
|
||||
protected:
|
||||
virtual float Compute(CParticleEmitterType& type) = 0;
|
||||
virtual float Compute(CParticleEmitterType& type, CParticleEmitter& emitter) = 0;
|
||||
|
||||
private:
|
||||
float m_LastValue;
|
||||
@ -81,7 +82,7 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
virtual float Compute(CParticleEmitterType& UNUSED(type))
|
||||
virtual float Compute(CParticleEmitterType& UNUSED(type), CParticleEmitter& UNUSED(emitter))
|
||||
{
|
||||
return m_Value;
|
||||
}
|
||||
@ -106,7 +107,7 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
virtual float Compute(CParticleEmitterType& type)
|
||||
virtual float Compute(CParticleEmitterType& type, CParticleEmitter& UNUSED(emitter))
|
||||
{
|
||||
return boost::uniform_real<>(m_Min, m_Max)(type.m_Manager.m_RNG);
|
||||
}
|
||||
@ -133,7 +134,7 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
virtual float Compute(CParticleEmitterType& type)
|
||||
virtual float Compute(CParticleEmitterType& type, CParticleEmitter& UNUSED(emitter))
|
||||
{
|
||||
return type.m_Variables[m_From]->LastValue();
|
||||
}
|
||||
@ -147,6 +148,75 @@ private:
|
||||
int m_From;
|
||||
};
|
||||
|
||||
/**
|
||||
* A terrible ad-hoc attempt at handling some particular variable calculation,
|
||||
* which really needs to be cleaned up and generalised.
|
||||
*/
|
||||
class CParticleVarExpr : public IParticleVar
|
||||
{
|
||||
public:
|
||||
CParticleVarExpr(const CStr& from, float mul, float max) :
|
||||
m_From(from), m_Mul(mul), m_Max(max)
|
||||
{
|
||||
}
|
||||
|
||||
virtual float Compute(CParticleEmitterType& UNUSED(type), CParticleEmitter& emitter)
|
||||
{
|
||||
return std::min(m_Max, emitter.m_EntityVariables[m_From] * m_Mul);
|
||||
}
|
||||
|
||||
virtual float Max(CParticleEmitterType& UNUSED(type))
|
||||
{
|
||||
return m_Max;
|
||||
}
|
||||
|
||||
private:
|
||||
CStr m_From;
|
||||
float m_Mul;
|
||||
float m_Max;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Interface for particle effectors, which get evaluated every frame to
|
||||
* update particles.
|
||||
*/
|
||||
class IParticleEffector
|
||||
{
|
||||
public:
|
||||
IParticleEffector() { }
|
||||
virtual ~IParticleEffector() {}
|
||||
|
||||
/// Updates all particles.
|
||||
virtual void Evaluate(std::vector<SParticle>& particles, float dt) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Particle effector that applies a constant acceleration.
|
||||
*/
|
||||
class CParticleEffectorForce : public IParticleEffector
|
||||
{
|
||||
public:
|
||||
CParticleEffectorForce(float x, float y, float z) :
|
||||
m_Accel(x, y, z)
|
||||
{
|
||||
}
|
||||
|
||||
virtual void Evaluate(std::vector<SParticle>& particles, float dt)
|
||||
{
|
||||
CVector3D dv = m_Accel * dt;
|
||||
|
||||
for (size_t i = 0; i < particles.size(); ++i)
|
||||
particles[i].velocity += dv;
|
||||
}
|
||||
|
||||
private:
|
||||
CVector3D m_Accel;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
CParticleEmitterType::CParticleEmitterType(const VfsPath& path, CParticleManager& manager) :
|
||||
m_Manager(manager)
|
||||
@ -162,6 +232,9 @@ int CParticleEmitterType::GetVariableID(const std::string& name)
|
||||
{
|
||||
if (name == "emissionrate") return VAR_EMISSIONRATE;
|
||||
if (name == "lifetime") return VAR_LIFETIME;
|
||||
if (name == "position.x") return VAR_POSITION_X;
|
||||
if (name == "position.y") return VAR_POSITION_Y;
|
||||
if (name == "position.z") return VAR_POSITION_Z;
|
||||
if (name == "angle") return VAR_ANGLE;
|
||||
if (name == "velocity.x") return VAR_VELOCITY_X;
|
||||
if (name == "velocity.y") return VAR_VELOCITY_Y;
|
||||
@ -182,6 +255,9 @@ bool CParticleEmitterType::LoadXML(const VfsPath& path)
|
||||
m_Variables.resize(VAR__MAX);
|
||||
m_Variables[VAR_EMISSIONRATE] = IParticleVarPtr(new CParticleVarConstant(10.f));
|
||||
m_Variables[VAR_LIFETIME] = IParticleVarPtr(new CParticleVarConstant(3.f));
|
||||
m_Variables[VAR_POSITION_X] = IParticleVarPtr(new CParticleVarConstant(0.f));
|
||||
m_Variables[VAR_POSITION_Y] = IParticleVarPtr(new CParticleVarConstant(0.f));
|
||||
m_Variables[VAR_POSITION_Z] = IParticleVarPtr(new CParticleVarConstant(0.f));
|
||||
m_Variables[VAR_ANGLE] = IParticleVarPtr(new CParticleVarConstant(0.f));
|
||||
m_Variables[VAR_VELOCITY_X] = IParticleVarPtr(new CParticleVarConstant(0.f));
|
||||
m_Variables[VAR_VELOCITY_Y] = IParticleVarPtr(new CParticleVarConstant(1.f));
|
||||
@ -210,12 +286,18 @@ bool CParticleEmitterType::LoadXML(const VfsPath& path)
|
||||
EL(constant);
|
||||
EL(uniform);
|
||||
EL(copy);
|
||||
EL(expr);
|
||||
EL(force);
|
||||
AT(mode);
|
||||
AT(name);
|
||||
AT(value);
|
||||
AT(min);
|
||||
AT(max);
|
||||
AT(mul);
|
||||
AT(from);
|
||||
AT(x);
|
||||
AT(y);
|
||||
AT(z);
|
||||
#undef AT
|
||||
#undef EL
|
||||
|
||||
@ -285,6 +367,22 @@ bool CParticleEmitterType::LoadXML(const VfsPath& path)
|
||||
if (id != -1 && from != -1)
|
||||
m_Variables[id] = IParticleVarPtr(new CParticleVarCopy(from));
|
||||
}
|
||||
else if (Child.GetNodeName() == el_expr)
|
||||
{
|
||||
int id = GetVariableID(Child.GetAttributes().GetNamedItem(at_name));
|
||||
CStr from = Child.GetAttributes().GetNamedItem(at_from);
|
||||
float mul = Child.GetAttributes().GetNamedItem(at_mul).ToFloat();
|
||||
float max = Child.GetAttributes().GetNamedItem(at_max).ToFloat();
|
||||
if (id != -1)
|
||||
m_Variables[id] = IParticleVarPtr(new CParticleVarExpr(from, mul, max));
|
||||
}
|
||||
else if (Child.GetNodeName() == el_force)
|
||||
{
|
||||
float x = Child.GetAttributes().GetNamedItem(at_x).ToFloat();
|
||||
float y = Child.GetAttributes().GetNamedItem(at_y).ToFloat();
|
||||
float z = Child.GetAttributes().GetNamedItem(at_z).ToFloat();
|
||||
m_Effectors.push_back(IParticleEffectorPtr(new CParticleEffectorForce(x, y, z)));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -299,12 +397,13 @@ void CParticleEmitterType::UpdateEmitter(CParticleEmitter& emitter, float dt)
|
||||
|
||||
if (emitter.m_Active)
|
||||
{
|
||||
float emissionRate = m_Variables[VAR_EMISSIONRATE]->Evaluate(*this);
|
||||
float emissionRate = m_Variables[VAR_EMISSIONRATE]->Evaluate(emitter);
|
||||
|
||||
// Find how many new particles to spawn, and accumulate any rounding errors into EmissionTimer
|
||||
emitter.m_EmissionTimer += dt;
|
||||
int newParticles = floor(emitter.m_EmissionTimer * emissionRate);
|
||||
emitter.m_EmissionTimer -= newParticles / emissionRate;
|
||||
if (newParticles) // avoid divide-by-zero if emissionRate == 0
|
||||
emitter.m_EmissionTimer -= newParticles / emissionRate;
|
||||
|
||||
// If dt was very large, there's no point spawning new particles that
|
||||
// we'll immediately overwrite, so clamp it
|
||||
@ -314,20 +413,29 @@ void CParticleEmitterType::UpdateEmitter(CParticleEmitter& emitter, float dt)
|
||||
{
|
||||
// Compute new particle state based on variables
|
||||
SParticle particle;
|
||||
particle.pos = emitter.m_Pos;
|
||||
particle.velocity.X = m_Variables[VAR_VELOCITY_X]->Evaluate(*this);
|
||||
particle.velocity.Y = m_Variables[VAR_VELOCITY_Y]->Evaluate(*this);
|
||||
particle.velocity.Z = m_Variables[VAR_VELOCITY_Z]->Evaluate(*this);
|
||||
particle.angle = m_Variables[VAR_ANGLE]->Evaluate(*this);
|
||||
particle.angleSpeed = m_Variables[VAR_VELOCITY_ANGLE]->Evaluate(*this);
|
||||
particle.size = m_Variables[VAR_SIZE]->Evaluate(*this);
|
||||
|
||||
particle.pos.X = m_Variables[VAR_POSITION_X]->Evaluate(emitter);
|
||||
particle.pos.Y = m_Variables[VAR_POSITION_Y]->Evaluate(emitter);
|
||||
particle.pos.Z = m_Variables[VAR_POSITION_Z]->Evaluate(emitter);
|
||||
particle.pos += emitter.m_Pos;
|
||||
|
||||
particle.velocity.X = m_Variables[VAR_VELOCITY_X]->Evaluate(emitter);
|
||||
particle.velocity.Y = m_Variables[VAR_VELOCITY_Y]->Evaluate(emitter);
|
||||
particle.velocity.Z = m_Variables[VAR_VELOCITY_Z]->Evaluate(emitter);
|
||||
|
||||
particle.angle = m_Variables[VAR_ANGLE]->Evaluate(emitter);
|
||||
particle.angleSpeed = m_Variables[VAR_VELOCITY_ANGLE]->Evaluate(emitter);
|
||||
|
||||
particle.size = m_Variables[VAR_SIZE]->Evaluate(emitter);
|
||||
|
||||
RGBColor color;
|
||||
color.X = m_Variables[VAR_COLOR_R]->Evaluate(*this);
|
||||
color.Y = m_Variables[VAR_COLOR_G]->Evaluate(*this);
|
||||
color.Z = m_Variables[VAR_COLOR_B]->Evaluate(*this);
|
||||
color.X = m_Variables[VAR_COLOR_R]->Evaluate(emitter);
|
||||
color.Y = m_Variables[VAR_COLOR_G]->Evaluate(emitter);
|
||||
color.Z = m_Variables[VAR_COLOR_B]->Evaluate(emitter);
|
||||
particle.color = ConvertRGBColorTo4ub(color);
|
||||
|
||||
particle.age = 0.f;
|
||||
particle.maxAge = m_Variables[VAR_LIFETIME]->Evaluate(*this);
|
||||
particle.maxAge = m_Variables[VAR_LIFETIME]->Evaluate(emitter);
|
||||
|
||||
emitter.AddParticle(particle);
|
||||
}
|
||||
@ -349,5 +457,8 @@ void CParticleEmitterType::UpdateEmitter(CParticleEmitter& emitter, float dt)
|
||||
p.color.A = clamp((int)(a*255.f), 0, 255);
|
||||
}
|
||||
|
||||
// TODO: maybe we want to add forces like gravity or wind
|
||||
for (size_t i = 0; i < m_Effectors.size(); ++i)
|
||||
{
|
||||
m_Effectors[i]->Evaluate(emitter.m_Particles, dt);
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@
|
||||
class CParticleEmitter;
|
||||
class CParticleManager;
|
||||
class IParticleVar;
|
||||
class IParticleEffector;
|
||||
|
||||
/**
|
||||
* Particle emitter type - stores the common state data for all emitters of that
|
||||
@ -53,6 +54,9 @@ private:
|
||||
{
|
||||
VAR_EMISSIONRATE,
|
||||
VAR_LIFETIME,
|
||||
VAR_POSITION_X,
|
||||
VAR_POSITION_Y,
|
||||
VAR_POSITION_Z,
|
||||
VAR_ANGLE,
|
||||
VAR_VELOCITY_X,
|
||||
VAR_VELOCITY_Y,
|
||||
@ -82,6 +86,9 @@ private:
|
||||
typedef shared_ptr<IParticleVar> IParticleVarPtr;
|
||||
std::vector<IParticleVarPtr> m_Variables;
|
||||
|
||||
typedef shared_ptr<IParticleEffector> IParticleEffectorPtr;
|
||||
std::vector<IParticleEffectorPtr> m_Effectors;
|
||||
|
||||
CParticleManager& m_Manager;
|
||||
};
|
||||
|
||||
|
@ -327,6 +327,14 @@ public:
|
||||
UNUSED2(a); // TODO: why is this even an argument?
|
||||
}
|
||||
|
||||
virtual void SetVariable(std::string name, float value)
|
||||
{
|
||||
if (!m_Unit)
|
||||
return;
|
||||
|
||||
m_Unit->GetModel().SetEntityVariable(name, value);
|
||||
}
|
||||
|
||||
virtual void Hotload(const VfsPath& name)
|
||||
{
|
||||
if (!m_Unit)
|
||||
|
@ -27,4 +27,5 @@ DEFINE_INTERFACE_METHOD_1("SelectMovementAnimation", void, ICmpVisual, SelectMov
|
||||
DEFINE_INTERFACE_METHOD_1("SetAnimationSyncRepeat", void, ICmpVisual, SetAnimationSyncRepeat, float)
|
||||
DEFINE_INTERFACE_METHOD_1("SetAnimationSyncOffset", void, ICmpVisual, SetAnimationSyncOffset, float)
|
||||
DEFINE_INTERFACE_METHOD_4("SetShadingColour", void, ICmpVisual, SetShadingColour, fixed, fixed, fixed, fixed)
|
||||
DEFINE_INTERFACE_METHOD_2("SetVariable", void, ICmpVisual, SetVariable, std::string, float)
|
||||
END_INTERFACE_WRAPPER(Visual)
|
||||
|
@ -102,6 +102,12 @@ public:
|
||||
*/
|
||||
virtual void SetShadingColour(fixed r, fixed g, fixed b, fixed a) = 0;
|
||||
|
||||
/**
|
||||
* Set an arbitrarily-named variable that the model may use to alter its appearance
|
||||
* (e.g. in particle emitter parameter computations).
|
||||
*/
|
||||
virtual void SetVariable(std::string name, float value) = 0;
|
||||
|
||||
/**
|
||||
* Called when an actor file has been modified and reloaded dynamically.
|
||||
* If this component uses the named actor file, it should regenerate its actor
|
||||
|
Loading…
Reference in New Issue
Block a user