# Updates to actor prop-switching system
Forced variant names to lowercase. Allowed empty prop model names to remove inherited props. Removed a little code duplication. Entity: Changed confusing (and probably incorrect) loop/STL logic, to use slightly more confusing syntax instead. This was SVN commit r3761.
This commit is contained in:
parent
8b1b11c0c6
commit
c41248b585
@ -5,6 +5,7 @@
|
||||
#include "ObjectManager.h"
|
||||
#include "XML/Xeromyces.h"
|
||||
#include "CLogger.h"
|
||||
#include "lib/timer.h"
|
||||
|
||||
#define LOG_CATEGORY "graphics"
|
||||
|
||||
@ -16,7 +17,7 @@ CObjectBase::CObjectBase()
|
||||
|
||||
bool CObjectBase::Load(const char* filename)
|
||||
{
|
||||
m_Variants.clear();
|
||||
m_VariantGroups.clear();
|
||||
|
||||
CStr filePath ("art/actors/");
|
||||
filePath += filename;
|
||||
@ -69,27 +70,25 @@ bool CObjectBase::Load(const char* filename)
|
||||
// of elements, to avoid wasteful copying/reallocation later.
|
||||
{
|
||||
// Count the variants in each group
|
||||
std::vector<int> variantSizes;
|
||||
std::vector<int> variantGroupSizes;
|
||||
XERO_ITER_EL(root, child)
|
||||
{
|
||||
if (child.getNodeName() == el_group)
|
||||
{
|
||||
variantSizes.push_back(0);
|
||||
XERO_ITER_EL(child, variant)
|
||||
++variantSizes.back();
|
||||
variantGroupSizes.push_back(child.getChildNodes().Count);
|
||||
}
|
||||
}
|
||||
|
||||
m_Variants.resize(variantSizes.size());
|
||||
m_VariantGroups.resize(variantGroupSizes.size());
|
||||
// Set each vector to match the number of variants
|
||||
for (size_t i = 0; i < variantSizes.size(); ++i)
|
||||
m_Variants[i].resize(variantSizes[i]);
|
||||
for (size_t i = 0; i < variantGroupSizes.size(); ++i)
|
||||
m_VariantGroups[i].resize(variantGroupSizes[i]);
|
||||
}
|
||||
|
||||
|
||||
// (This XML-reading code is rather worryingly verbose...)
|
||||
|
||||
std::vector<std::vector<Variant> >::iterator currentGroup = m_Variants.begin();
|
||||
std::vector<std::vector<Variant> >::iterator currentGroup = m_VariantGroups.begin();
|
||||
|
||||
XERO_ITER_EL(root, child)
|
||||
{
|
||||
@ -104,7 +103,7 @@ bool CObjectBase::Load(const char* filename)
|
||||
XERO_ITER_ATTR(variant, attr)
|
||||
{
|
||||
if (attr.Name == at_name)
|
||||
currentVariant->m_VariantName = attr.Value;
|
||||
currentVariant->m_VariantName = CStr(attr.Value).LowerCase();
|
||||
|
||||
else if (attr.Name == at_frequency)
|
||||
currentVariant->m_Frequency = CStr(attr.Value).ToInt();
|
||||
@ -134,9 +133,13 @@ bool CObjectBase::Load(const char* filename)
|
||||
XERO_ITER_ATTR(anim_element, ae)
|
||||
{
|
||||
if (ae.Name == at_name)
|
||||
{
|
||||
anim.m_AnimName = ae.Value;
|
||||
}
|
||||
else if (ae.Name == at_file)
|
||||
{
|
||||
anim.m_FileName = "art/animation/" + CStr(ae.Value);
|
||||
}
|
||||
else if (ae.Name == at_speed)
|
||||
{
|
||||
anim.m_Speed = CStr(ae.Value).ToInt() / 100.f;
|
||||
@ -209,8 +212,14 @@ bool CObjectBase::Load(const char* filename)
|
||||
return true;
|
||||
}
|
||||
|
||||
TIMER_ADD_CLIENT(tc_CalculateVariationKey)
|
||||
|
||||
std::vector<u8> CObjectBase::CalculateVariationKey(const std::vector<std::set<CStrW> >& selections)
|
||||
{
|
||||
TIMER_ACCRUE(tc_CalculateVariationKey);
|
||||
// (TODO: see CObjectManager::FindObjectVariation for an opportunity to
|
||||
// call this function a bit less frequently)
|
||||
|
||||
// Calculate a complete list of choices, one per group, based on the
|
||||
// supposedly-complete selections (i.e. not making random choices at this
|
||||
// stage).
|
||||
@ -223,8 +232,8 @@ std::vector<u8> CObjectBase::CalculateVariationKey(const std::vector<std::set<CS
|
||||
|
||||
std::map<CStr, CStr> chosenProps;
|
||||
|
||||
for (std::vector<std::vector<CObjectBase::Variant> >::iterator grp = m_Variants.begin();
|
||||
grp != m_Variants.end();
|
||||
for (std::vector<std::vector<CObjectBase::Variant> >::iterator grp = m_VariantGroups.begin();
|
||||
grp != m_VariantGroups.end();
|
||||
++grp)
|
||||
{
|
||||
// Ignore groups with nothing inside. (A warning will have been
|
||||
@ -274,7 +283,10 @@ std::vector<u8> CObjectBase::CalculateVariationKey(const std::vector<std::set<CS
|
||||
CObjectBase::Variant& var ((*grp)[match]);
|
||||
for (std::vector<CObjectBase::Prop>::iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it)
|
||||
{
|
||||
chosenProps[it->m_PropPointName] = it->m_ModelName;
|
||||
if (it->m_ModelName.Length())
|
||||
chosenProps[it->m_PropPointName] = it->m_ModelName;
|
||||
else
|
||||
chosenProps.erase(it->m_PropPointName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -292,6 +304,70 @@ std::vector<u8> CObjectBase::CalculateVariationKey(const std::vector<std::set<CS
|
||||
return choices;
|
||||
}
|
||||
|
||||
const CObjectBase::Variation CObjectBase::BuildVariation(const std::vector<u8>& variationKey)
|
||||
{
|
||||
Variation variation;
|
||||
|
||||
// variationKey should correspond with m_Variants, giving the id of the
|
||||
// chosen variant from each group. (Except variationKey has some bits stuck
|
||||
// on the end for props, but we don't care about those in here.)
|
||||
|
||||
std::vector<std::vector<CObjectBase::Variant> >::iterator grp = m_VariantGroups.begin();
|
||||
std::vector<u8>::const_iterator match = variationKey.begin();
|
||||
for ( ;
|
||||
grp != m_VariantGroups.end() && match != variationKey.end();
|
||||
++grp, ++match)
|
||||
{
|
||||
// Ignore groups with nothing inside. (A warning will have been
|
||||
// emitted by the loading code.)
|
||||
if (grp->size() == 0)
|
||||
continue;
|
||||
|
||||
size_t id = *match;
|
||||
if (id >= grp->size())
|
||||
{
|
||||
// This should be impossible
|
||||
debug_warn("BuildVariation: invalid variant id");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the matched variant
|
||||
CObjectBase::Variant& var ((*grp)[id]);
|
||||
|
||||
// Apply its data:
|
||||
|
||||
if (var.m_TextureFilename.Length())
|
||||
variation.texture = var.m_TextureFilename;
|
||||
|
||||
if (var.m_ModelFilename.Length())
|
||||
variation.model = var.m_ModelFilename;
|
||||
|
||||
if (var.m_Color.Length())
|
||||
variation.color = var.m_Color;
|
||||
|
||||
for (std::vector<CObjectBase::Prop>::iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it)
|
||||
{
|
||||
if (it->m_ModelName.Length())
|
||||
variation.props[it->m_PropPointName] = *it;
|
||||
else
|
||||
variation.props.erase(it->m_PropPointName);
|
||||
}
|
||||
|
||||
// If one variant defines one animation called e.g. "attack", and this
|
||||
// variant defines two different animations with the same name, the one
|
||||
// original should be erased, and replaced by the two new ones.
|
||||
//
|
||||
// So, erase all existing animations which are overridden by this variant:
|
||||
for (std::vector<CObjectBase::Anim>::iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it)
|
||||
variation.anims.erase(it->m_AnimName);
|
||||
// and then insert the new ones:
|
||||
for (std::vector<CObjectBase::Anim>::iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it)
|
||||
variation.anims.insert(make_pair(it->m_AnimName, *it));
|
||||
}
|
||||
|
||||
return variation;
|
||||
}
|
||||
|
||||
std::set<CStrW> CObjectBase::CalculateRandomVariation(const std::set<CStrW>& initialSelections)
|
||||
{
|
||||
std::set<CStrW> selections = initialSelections;
|
||||
@ -308,8 +384,8 @@ std::set<CStrW> CObjectBase::CalculateRandomVariation(const std::set<CStrW>& ini
|
||||
// When choosing randomly, make use of each variant's frequency. If all
|
||||
// variants have frequency 0, treat them as if they were 1.
|
||||
|
||||
for (std::vector<std::vector<CObjectBase::Variant> >::iterator grp = m_Variants.begin();
|
||||
grp != m_Variants.end();
|
||||
for (std::vector<std::vector<CObjectBase::Variant> >::iterator grp = m_VariantGroups.begin();
|
||||
grp != m_VariantGroups.end();
|
||||
++grp)
|
||||
{
|
||||
// Ignore groups with nothing inside. (A warning will have been
|
||||
@ -383,7 +459,10 @@ std::set<CStrW> CObjectBase::CalculateRandomVariation(const std::set<CStrW>& ini
|
||||
CObjectBase::Variant& var ((*grp)[match]);
|
||||
for (std::vector<CObjectBase::Prop>::iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it)
|
||||
{
|
||||
chosenProps[it->m_PropPointName] = it->m_ModelName;
|
||||
if (it->m_ModelName.Length())
|
||||
chosenProps[it->m_PropPointName] = it->m_ModelName;
|
||||
else
|
||||
chosenProps.erase(it->m_PropPointName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -394,6 +473,7 @@ std::set<CStrW> CObjectBase::CalculateRandomVariation(const std::set<CStrW>& ini
|
||||
if (prop)
|
||||
{
|
||||
std::set<CStrW> propSelections = prop->CalculateRandomVariation(selections);
|
||||
// selections = union(propSelections, selections)
|
||||
std::set<CStrW> newSelections;
|
||||
std::set_union(propSelections.begin(), propSelections.end(),
|
||||
selections.begin(), selections.end(),
|
||||
|
@ -38,7 +38,7 @@ public:
|
||||
struct Variant
|
||||
{
|
||||
Variant() : m_Frequency(0) {}
|
||||
CStr m_VariantName;
|
||||
CStr m_VariantName; // lowercase name
|
||||
int m_Frequency;
|
||||
CStr m_ModelFilename;
|
||||
CStr m_TextureFilename;
|
||||
@ -48,12 +48,21 @@ public:
|
||||
std::vector<Prop> m_Props;
|
||||
};
|
||||
|
||||
struct Variation
|
||||
{
|
||||
CStr texture;
|
||||
CStr model;
|
||||
CStr color;
|
||||
std::map<CStr, CObjectBase::Prop> props;
|
||||
std::multimap<CStr, CObjectBase::Anim> anims;
|
||||
};
|
||||
|
||||
CObjectBase();
|
||||
|
||||
std::vector< std::vector<Variant> > m_Variants;
|
||||
|
||||
std::vector<u8> CalculateVariationKey(const std::vector<std::set<CStrW> >& selections);
|
||||
|
||||
const Variation BuildVariation(const std::vector<u8>& variationKey);
|
||||
|
||||
std::set<CStrW> CalculateRandomVariation(const std::set<CStrW>& initialSelections);
|
||||
|
||||
bool Load(const char* filename);
|
||||
@ -73,6 +82,9 @@ public:
|
||||
|
||||
// the material file
|
||||
CStr m_Material;
|
||||
|
||||
private:
|
||||
std::vector< std::vector<Variant> > m_VariantGroups;
|
||||
};
|
||||
|
||||
|
||||
|
@ -37,106 +37,29 @@ CObjectEntry::~CObjectEntry()
|
||||
}
|
||||
|
||||
|
||||
bool CObjectEntry::BuildVariation(const std::vector<std::set<CStrW> >& selections)
|
||||
bool CObjectEntry::BuildVariation(const std::vector<std::set<CStrW> >& selections, const std::vector<u8>& variationKey)
|
||||
{
|
||||
CStr chosenTexture;
|
||||
CStr chosenModel;
|
||||
CStr chosenColor;
|
||||
std::map<CStr, CObjectBase::Prop> chosenProps;
|
||||
std::multimap<CStr, CObjectBase::Anim> chosenAnims;
|
||||
|
||||
for (std::vector<std::vector<CObjectBase::Variant> >::iterator grp = m_Base->m_Variants.begin();
|
||||
grp != m_Base->m_Variants.end();
|
||||
++grp)
|
||||
{
|
||||
// Ignore groups with nothing inside. (A warning will have been
|
||||
// emitted by the loading code.)
|
||||
if (grp->size() == 0)
|
||||
continue;
|
||||
|
||||
int match = -1; // -1 => none found yet
|
||||
|
||||
// If there's only a single variant, choose that one
|
||||
if (grp->size() == 1)
|
||||
{
|
||||
match = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Determine the first variant that matches the provided strings,
|
||||
// starting with the highest priority selections set:
|
||||
|
||||
for (std::vector<std::set<CStrW> >::const_iterator selset = selections.begin(); selset < selections.end(); ++selset)
|
||||
{
|
||||
debug_assert(grp->size() < 256); // else they won't fit in 'choices'
|
||||
|
||||
for (size_t i = 0; i < grp->size(); ++i)
|
||||
{
|
||||
if (selset->count((*grp)[i].m_VariantName))
|
||||
{
|
||||
match = (u8)i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop after finding the first match
|
||||
if (match != -1)
|
||||
break;
|
||||
}
|
||||
|
||||
// If no match, just choose the first
|
||||
if (match == -1)
|
||||
match = 0;
|
||||
}
|
||||
|
||||
// Get the matched variant
|
||||
CObjectBase::Variant& var ((*grp)[match]);
|
||||
|
||||
// Apply its data:
|
||||
|
||||
if (var.m_TextureFilename.Length())
|
||||
chosenTexture = var.m_TextureFilename;
|
||||
|
||||
if (var.m_ModelFilename.Length())
|
||||
chosenModel = var.m_ModelFilename;
|
||||
|
||||
if (var.m_Color.Length())
|
||||
chosenColor = var.m_Color;
|
||||
|
||||
for (std::vector<CObjectBase::Prop>::iterator it = var.m_Props.begin(); it != var.m_Props.end(); ++it)
|
||||
chosenProps[it->m_PropPointName] = *it;
|
||||
|
||||
// If one variant defines one animation called e.g. "attack", and this
|
||||
// variant defines two different animations with the same name, the one
|
||||
// original should be erased, and replaced by the two new ones.
|
||||
//
|
||||
// So, erase all existing animations which are overridden by this variant:
|
||||
for (std::vector<CObjectBase::Anim>::iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it)
|
||||
chosenAnims.erase(chosenAnims.lower_bound(it->m_AnimName), chosenAnims.upper_bound(it->m_AnimName));
|
||||
// and then insert the new ones:
|
||||
for (std::vector<CObjectBase::Anim>::iterator it = var.m_Anims.begin(); it != var.m_Anims.end(); ++it)
|
||||
chosenAnims.insert(make_pair(it->m_AnimName, *it));
|
||||
}
|
||||
CObjectBase::Variation variation = m_Base->BuildVariation(variationKey);
|
||||
|
||||
// Copy the chosen data onto this model:
|
||||
|
||||
m_TextureName = chosenTexture;
|
||||
m_ModelName = chosenModel;
|
||||
m_TextureName = variation.texture;
|
||||
m_ModelName = variation.model;
|
||||
|
||||
if (chosenColor.Length())
|
||||
if (variation.color.Length())
|
||||
{
|
||||
std::stringstream str;
|
||||
str << chosenColor;
|
||||
str << variation.color;
|
||||
int r, g, b;
|
||||
if (! (str >> r >> g >> b)) // Any trailing data is ignored
|
||||
LOG(ERROR, LOG_CATEGORY, "Invalid RGB colour '%s'", chosenColor.c_str());
|
||||
LOG(ERROR, LOG_CATEGORY, "Invalid RGB colour '%s'", variation.color.c_str());
|
||||
else
|
||||
m_Color = CColor(r/255.0f, g/255.0f, b/255.0f, 1.0f);
|
||||
}
|
||||
|
||||
std::vector<CObjectBase::Prop> props;
|
||||
|
||||
for (std::map<CStr, CObjectBase::Prop>::iterator it = chosenProps.begin(); it != chosenProps.end(); ++it)
|
||||
for (std::map<CStr, CObjectBase::Prop>::iterator it = variation.props.begin(); it != variation.props.end(); ++it)
|
||||
props.push_back(it->second);
|
||||
|
||||
// Build the model:
|
||||
@ -169,7 +92,7 @@ bool CObjectEntry::BuildVariation(const std::vector<std::set<CStrW> >& selection
|
||||
m_Model->CalcObjectBounds();
|
||||
|
||||
// load the animations
|
||||
for (std::multimap<CStr, CObjectBase::Anim>::iterator it = chosenAnims.begin(); it != chosenAnims.end(); ++it)
|
||||
for (std::multimap<CStr, CObjectBase::Anim>::iterator it = variation.anims.begin(); it != variation.anims.end(); ++it)
|
||||
{
|
||||
CStr name = it->first.LowerCase();
|
||||
|
||||
|
@ -18,7 +18,7 @@ public:
|
||||
CObjectEntry(int type, CObjectBase* base);
|
||||
~CObjectEntry();
|
||||
|
||||
bool BuildVariation(const std::vector<std::set<CStrW> >& selections);
|
||||
bool BuildVariation(const std::vector<std::set<CStrW> >& selections, const std::vector<u8>& variationKey);
|
||||
|
||||
// Base actor. Contains all the things that don't change between
|
||||
// different variations of the actor.
|
||||
|
@ -109,7 +109,11 @@ CObjectEntry* CObjectManager::FindObjectVariation(CObjectBase* base, const std::
|
||||
|
||||
CObjectEntry* obj = new CObjectEntry(0, base); // TODO: type ?
|
||||
|
||||
if (! obj->BuildVariation(selections))
|
||||
// TODO (for some efficiency): use the pre-calculated choices for this object,
|
||||
// which has already worked out what to do for props, instead of passing the
|
||||
// selections into BuildVariation and having it recalculate the props' choices.
|
||||
|
||||
if (! obj->BuildVariation(selections, choices))
|
||||
{
|
||||
DeleteObject(obj);
|
||||
return NULL;
|
||||
|
@ -92,13 +92,15 @@ void CUnit::SetPlayerID(int id)
|
||||
|
||||
void CUnit::SetEntitySelection(const CStrW& selection)
|
||||
{
|
||||
CStrW selection_lc = selection.LowerCase();
|
||||
|
||||
// If we've already selected this, don't do anything
|
||||
if (m_EntitySelections.find(selection) != m_EntitySelections.end())
|
||||
if (m_EntitySelections.find(selection_lc) != m_EntitySelections.end())
|
||||
return;
|
||||
|
||||
// Just allow one selection at a time
|
||||
m_EntitySelections.clear();
|
||||
m_EntitySelections.insert(selection);
|
||||
m_EntitySelections.insert(selection_lc);
|
||||
|
||||
ReloadObject();
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ bool CBaseFormation::loadXML(CStr filename)
|
||||
|
||||
if( order <= 0 )
|
||||
{
|
||||
LOG( ERROR, LOG_CATEGORY, "CBaseFormation::LoadXML: Invalid (negative number or 0) order defined in formation file %s. The game will try to continue anyway.", filename.c_str() );
|
||||
LOG( ERROR, LOG_CATEGORY, "CBaseFormation::LoadXML: Invalid (negative number or 0) order defined in formation file %s. The game will try to continue anyway.", filename.c_str() );
|
||||
continue;
|
||||
}
|
||||
--order; //We need this to be in line with arrays, so start at 0
|
||||
@ -173,7 +173,7 @@ bool CBaseFormation::loadXML(CStr filename)
|
||||
{
|
||||
if ( m_slots.find(i) == m_slots.end() )
|
||||
{
|
||||
LOG( ERROR, LOG_CATEGORY, "CBaseFormation::LoadXML: Missing orders in %s. Load failed.", filename.c_str() );
|
||||
LOG( ERROR, LOG_CATEGORY, "CBaseFormation::LoadXML: Missing orders in %s. Load failed.", filename.c_str() );
|
||||
return false;
|
||||
}
|
||||
else
|
||||
|
@ -740,36 +740,43 @@ void CEntity::DispatchNotification( CEntityOrder order, int type )
|
||||
CEventNotification evt( order, type );
|
||||
DispatchEvent( &evt );
|
||||
}
|
||||
|
||||
struct isListenerSender
|
||||
{
|
||||
CEntity* sender;
|
||||
isListenerSender(CEntity* sender) : sender(sender) {}
|
||||
bool operator()(CEntityListener& listener)
|
||||
{
|
||||
return listener.m_sender == sender;
|
||||
}
|
||||
};
|
||||
|
||||
int CEntity::DestroyNotifier( CEntity* target )
|
||||
{
|
||||
if (target->m_listeners.empty() || !m_destroyNotifiers)
|
||||
return 0;
|
||||
//Stop listening
|
||||
for ( size_t i=0; i < target->m_listeners.size(); i++ )
|
||||
{
|
||||
if ( target->m_listeners[i].m_sender == this )
|
||||
target->m_listeners.erase(target->m_listeners.begin() + i);
|
||||
}
|
||||
int removed=0;
|
||||
// (Don't just loop and use 'erase', because modifying the deque while
|
||||
// looping over it is a bit dangerous)
|
||||
std::deque<CEntityListener>::iterator newEnd = std::remove_if(
|
||||
target->m_listeners.begin(), target->m_listeners.end(),
|
||||
isListenerSender(this));
|
||||
target->m_listeners.erase(newEnd, target->m_listeners.end());
|
||||
|
||||
//Get rid of our copy
|
||||
for ( size_t i=0; i < target->m_notifiers.size(); i++ )
|
||||
{
|
||||
if ( m_notifiers[i] == target )
|
||||
{
|
||||
m_notifiers.erase(m_notifiers.begin() + i);
|
||||
++removed;
|
||||
}
|
||||
}
|
||||
std::vector<CEntity*>::iterator newEnd2 = std::remove_if(
|
||||
m_notifiers.begin(), m_notifiers.end(),
|
||||
bind2nd(std::equal_to<CEntity*>(), target));
|
||||
int removed = std::distance(newEnd2, m_notifiers.end());
|
||||
m_notifiers.erase(newEnd2, m_notifiers.end());
|
||||
return removed;
|
||||
}
|
||||
void CEntity::DestroyAllNotifiers()
|
||||
{
|
||||
debug_assert(m_destroyNotifiers);
|
||||
//Make them stop listening to us
|
||||
if ( m_notifiers.empty() )
|
||||
return;
|
||||
for ( size_t i=0; i<m_notifiers.size(); i++ )
|
||||
i -= DestroyNotifier( m_notifiers[i] );
|
||||
while ( ! m_notifiers.empty() )
|
||||
DestroyNotifier( m_notifiers[0] );
|
||||
}
|
||||
CEntityFormation* CEntity::GetFormation()
|
||||
{
|
||||
@ -1768,6 +1775,7 @@ bool CEntity::RequestNotification( JSContext* cx, uintN argc, jsval* argv )
|
||||
CEntity *target = ToNative<CEntity>( argv[0] );
|
||||
(int&)notify.m_type = ToPrimitive<int>( argv[1] );
|
||||
bool tmpDestroyNotifiers = ToPrimitive<bool>( argv[2] );
|
||||
// TODO: ??? This local variable overrides the member variable of the same name...
|
||||
bool m_destroyNotifiers = !ToPrimitive<bool>( argv[3] );
|
||||
|
||||
if (target == this)
|
||||
|
Loading…
Reference in New Issue
Block a user