mirror of
https://bitbucket.org/theswgsource/src-1.2.git
synced 2026-08-04 05:15:51 -04:00
Added serverGame library
This commit is contained in:
@@ -0,0 +1,594 @@
|
||||
// ======================================================================
|
||||
//
|
||||
// TriggerVolume.cpp
|
||||
//
|
||||
// Copyright 2000-04 Sony Online Entertainment
|
||||
//
|
||||
// ======================================================================
|
||||
|
||||
#include "serverGame/FirstServerGame.h"
|
||||
#include "serverGame/ConfigServerGame.h"
|
||||
#include "serverGame/ServerObject.h"
|
||||
#include "serverGame/ServerWorld.h"
|
||||
#include "serverGame/TriggerVolume.h"
|
||||
#include "serverScript/GameScriptObject.h"
|
||||
#include "serverScript/ScriptDictionary.h"
|
||||
#include "serverScript/ScriptParameters.h"
|
||||
#include "serverScript/ScriptMessage.h"
|
||||
#include "sharedDebug/Profiler.h"
|
||||
#include "sharedObject/NetworkIdManager.h"
|
||||
#include <algorithm>
|
||||
#include <hash_map>
|
||||
|
||||
// ======================================================================
|
||||
|
||||
namespace TriggerVolumeNamespace
|
||||
{
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
class TriggerScriptParam
|
||||
{
|
||||
public:
|
||||
void add(NetworkId const & source, NetworkId const & target, const char * name, bool isEnter);
|
||||
void remove(NetworkId const & id);
|
||||
void clear();
|
||||
|
||||
std::deque<bool> const & getIsEnter() const;
|
||||
std::vector<const char *> const & getNames() const;
|
||||
std::vector<NetworkId> const & getSources() const;
|
||||
std::vector<NetworkId> const & getTargets() const;
|
||||
|
||||
private:
|
||||
std::deque<bool> m_isEnter;
|
||||
std::vector<const char *> m_names;
|
||||
std::vector<NetworkId> m_sources;
|
||||
std::vector<NetworkId> m_targets;
|
||||
|
||||
// optimization to track how many time a NetworkId appears
|
||||
// in sources and targets, so that we can avoid unnecessarily
|
||||
// searching the entire sources and targets vectors
|
||||
std::hash_map<NetworkId, int> m_sourcesCount;
|
||||
std::hash_map<NetworkId, int> m_targetsCount;
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerScriptParam::add(NetworkId const & source, NetworkId const & target, const char * name, bool isEnter)
|
||||
{
|
||||
m_isEnter.push_back(isEnter);
|
||||
m_names.push_back(name);
|
||||
m_sources.push_back(source);
|
||||
m_targets.push_back(target);
|
||||
|
||||
std::hash_map<NetworkId, int>::iterator i = m_sourcesCount.find(source);
|
||||
if (i == m_sourcesCount.end())
|
||||
m_sourcesCount[source] = 1;
|
||||
else
|
||||
++(i->second);
|
||||
|
||||
i = m_targetsCount.find(target);
|
||||
if (i == m_targetsCount.end())
|
||||
m_targetsCount[target] = 1;
|
||||
else
|
||||
++(i->second);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerScriptParam::remove(NetworkId const & id)
|
||||
{
|
||||
// remove all entries from sources
|
||||
// and targets for the specified id
|
||||
std::hash_map<NetworkId, int>::iterator const iterCountSource = m_sourcesCount.find(id);
|
||||
if (iterCountSource != m_sourcesCount.end())
|
||||
{
|
||||
int countSource = iterCountSource->second;
|
||||
for (std::vector<NetworkId>::iterator s = m_sources.begin(); ((s != m_sources.end()) && (countSource > 0));)
|
||||
{
|
||||
if (*s == id)
|
||||
{
|
||||
size_t const index = std::distance(m_sources.begin(), s);
|
||||
|
||||
// remove corresponding item from m_isEnter
|
||||
std::deque<bool>::iterator enterIter = m_isEnter.begin();
|
||||
std::advance(enterIter, index);
|
||||
IGNORE_RETURN(m_isEnter.erase(enterIter));
|
||||
|
||||
// remove corresponding item from m_names
|
||||
std::vector<const char *>::iterator namesIter = m_names.begin();
|
||||
std::advance(namesIter, index);
|
||||
IGNORE_RETURN(m_names.erase(namesIter));
|
||||
|
||||
// remove corresponding item from m_targets
|
||||
std::vector<NetworkId>::iterator targetsIter = m_targets.begin();
|
||||
std::advance(targetsIter, index);
|
||||
|
||||
std::hash_map<NetworkId, int>::iterator const iterCountTarget = m_targetsCount.find(*targetsIter);
|
||||
if (iterCountTarget != m_targetsCount.end())
|
||||
{
|
||||
if (iterCountTarget->second <= 1)
|
||||
m_targetsCount.erase(iterCountTarget);
|
||||
else
|
||||
--(iterCountTarget->second);
|
||||
}
|
||||
|
||||
IGNORE_RETURN(m_targets.erase(targetsIter));
|
||||
|
||||
// remove item from m_sources
|
||||
s = m_sources.erase(s);
|
||||
--countSource;
|
||||
}
|
||||
else
|
||||
{
|
||||
++s;
|
||||
}
|
||||
}
|
||||
|
||||
m_sourcesCount.erase(iterCountSource);
|
||||
}
|
||||
|
||||
std::hash_map<NetworkId, int>::iterator const iterCountTarget = m_targetsCount.find(id);
|
||||
if (iterCountTarget != m_targetsCount.end())
|
||||
{
|
||||
int countTarget = iterCountTarget->second;
|
||||
for (std::vector<NetworkId>::iterator s = m_targets.begin(); ((s != m_targets.end()) && (countTarget > 0));)
|
||||
{
|
||||
if (*s == id)
|
||||
{
|
||||
size_t const index = std::distance(m_targets.begin(), s);
|
||||
|
||||
// remove corresponding item from m_isEnter
|
||||
std::deque<bool>::iterator enterIter = m_isEnter.begin();
|
||||
std::advance(enterIter, index);
|
||||
IGNORE_RETURN(m_isEnter.erase(enterIter));
|
||||
|
||||
// remove corresponding item from m_names
|
||||
std::vector<const char *>::iterator namesIter = m_names.begin();
|
||||
std::advance(namesIter, index);
|
||||
IGNORE_RETURN(m_names.erase(namesIter));
|
||||
|
||||
// remove corresponding item from m_sources
|
||||
std::vector<NetworkId>::iterator sourcesIter = m_sources.begin();
|
||||
std::advance(sourcesIter, index);
|
||||
|
||||
std::hash_map<NetworkId, int>::iterator const iterCountSource = m_sourcesCount.find(*sourcesIter);
|
||||
if (iterCountSource != m_sourcesCount.end())
|
||||
{
|
||||
if (iterCountSource->second <= 1)
|
||||
m_sourcesCount.erase(iterCountSource);
|
||||
else
|
||||
--(iterCountSource->second);
|
||||
}
|
||||
|
||||
IGNORE_RETURN(m_sources.erase(sourcesIter));
|
||||
|
||||
// remove item from m_targets
|
||||
s = m_targets.erase(s);
|
||||
--countTarget;
|
||||
}
|
||||
else
|
||||
{
|
||||
++s;
|
||||
}
|
||||
}
|
||||
|
||||
m_targetsCount.erase(iterCountTarget);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerScriptParam::clear()
|
||||
{
|
||||
m_isEnter.clear();
|
||||
m_names.clear();
|
||||
m_sources.clear();
|
||||
m_targets.clear();
|
||||
m_sourcesCount.clear();
|
||||
m_targetsCount.clear();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
std::deque<bool> const & TriggerScriptParam::getIsEnter() const
|
||||
{
|
||||
return m_isEnter;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
std::vector<const char *> const & TriggerScriptParam::getNames() const
|
||||
{
|
||||
return m_names;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
std::vector<NetworkId> const & TriggerScriptParam::getSources() const
|
||||
{
|
||||
return m_sources;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
std::vector<NetworkId> const & TriggerScriptParam::getTargets() const
|
||||
{
|
||||
return m_targets;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
// don't update s_triggers1 and s_triggers2 directly; use s_triggers instead
|
||||
TriggerScriptParam s_triggers1;
|
||||
TriggerScriptParam s_triggers2;
|
||||
TriggerScriptParam * s_triggers = &s_triggers1;
|
||||
}
|
||||
|
||||
using namespace TriggerVolumeNamespace;
|
||||
|
||||
// ======================================================================
|
||||
|
||||
TriggerVolume::TriggerVolume(ServerObject &newOwner, float radius, std::string const &newName, bool isPromiscuous) :
|
||||
m_radius(radius),
|
||||
m_contents(),
|
||||
m_eventSources(),
|
||||
m_name(newName),
|
||||
m_owner(newOwner),
|
||||
m_spatialSubdivisionHandle(0),
|
||||
m_promiscuous(isPromiscuous)
|
||||
{
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
TriggerVolume::~TriggerVolume()
|
||||
{
|
||||
// remove all entries from s_triggers.sources
|
||||
// and s_triggers.targets corresponding to m_owner
|
||||
s_triggers->remove(m_owner.getNetworkId());
|
||||
|
||||
// tell all objects in our trigger volume that we are going away
|
||||
for (ContentsSet::const_iterator i = m_contents.begin(); i != m_contents.end(); ++i)
|
||||
(*i)->onRemovedFromTriggerVolume(*this);
|
||||
|
||||
m_spatialSubdivisionHandle = 0;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
bool TriggerVolume::intersectsExtent(ServerObject const &object) const
|
||||
{
|
||||
Vector const &position_w = m_owner.getPosition_w();
|
||||
return Sphere(position_w, m_radius).intersectsSphere(object.getSphereExtent());
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerVolume::addEventSource(NetworkId const &source)
|
||||
{
|
||||
IGNORE_RETURN(m_eventSources.insert(source));
|
||||
ServerObject * const object = ServerWorld::findObjectByNetworkId(source);
|
||||
if (object && intersectsExtent(*object))
|
||||
s_triggers->add(object->getNetworkId(), m_owner.getNetworkId(), m_name.c_str(), true);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerVolume::addObject(ServerObject &object)
|
||||
{
|
||||
if (intersectsExtent(object))
|
||||
onEnter(object);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerVolume::onEnter(ServerObject &object)
|
||||
{
|
||||
std::pair<ContentsSet::iterator, bool> const result = m_contents.insert(&object);
|
||||
if (result.second)
|
||||
{
|
||||
virtualOnEnter(object);
|
||||
object.onAddedToTriggerVolume(*this);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerVolume::onExit(ServerObject &object)
|
||||
{
|
||||
if (m_contents.erase(&object) > 0)
|
||||
{
|
||||
virtualOnExit(object);
|
||||
object.onRemovedFromTriggerVolume(*this);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerVolume::virtualOnEnter(ServerObject &object)
|
||||
{
|
||||
if ( m_owner.isAuthoritative()
|
||||
&& &object != &m_owner
|
||||
&& !isNetworkTriggerVolume()
|
||||
&& ( !ConfigServerGame::getEnforcePlayerTriggerVolumesOnly()
|
||||
|| object.isPlayerControlled()
|
||||
|| m_owner.isPlayerControlled()
|
||||
|| object.isInterested(m_owner)
|
||||
|| m_owner.isInterested(object))
|
||||
&& ( m_promiscuous
|
||||
|| m_eventSources.find(object.getNetworkId()) != m_eventSources.end()))
|
||||
{
|
||||
s_triggers->add(object.getNetworkId(), m_owner.getNetworkId(), m_name.c_str(), true);
|
||||
}
|
||||
m_owner.onTriggerEnter(object, *this);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerVolume::virtualOnExit(ServerObject &object)
|
||||
{
|
||||
if ( m_owner.isAuthoritative()
|
||||
&& &object != &m_owner
|
||||
&& !isNetworkTriggerVolume()
|
||||
&& ( !ConfigServerGame::getEnforcePlayerTriggerVolumesOnly()
|
||||
|| object.isPlayerControlled()
|
||||
|| m_owner.isPlayerControlled()
|
||||
|| object.isInterested(m_owner)
|
||||
|| m_owner.isInterested(object))
|
||||
&& ( m_promiscuous
|
||||
|| m_eventSources.find(object.getNetworkId()) != m_eventSources.end()))
|
||||
{
|
||||
s_triggers->add(object.getNetworkId(), m_owner.getNetworkId(), m_name.c_str(), false);
|
||||
}
|
||||
m_owner.onTriggerExit(object, *this);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerVolume::moveObject(ServerObject &object, Vector const &start, Vector const &end)
|
||||
{
|
||||
static const int notInTrigger = 0x0000;
|
||||
static const int startedInTrigger = 0x0001;
|
||||
static const int endedInTrigger = 0x0002;
|
||||
|
||||
// update the sphere position
|
||||
Vector const &position_w = m_owner.getPosition_w();
|
||||
Sphere const &objectExtentSphere = object.getSphereExtent();
|
||||
|
||||
Vector intersectionPoint;
|
||||
if (end.magnitudeBetweenSquared(start) > Vector::NORMALIZE_THRESHOLD)
|
||||
intersectionPoint = position_w.findClosestPointOnLineSegment(start, end);
|
||||
else
|
||||
intersectionPoint = end;
|
||||
|
||||
if (position_w.magnitudeBetween(intersectionPoint) - m_radius < objectExtentSphere.getRadius())
|
||||
{
|
||||
// there was an intersection along the path. Determine start and end sphere intersection status
|
||||
int flags = notInTrigger;
|
||||
|
||||
Sphere const triggerSphere(position_w, m_radius);
|
||||
|
||||
if (triggerSphere.intersectsSphere(Sphere(end, objectExtentSphere.getRadius())))
|
||||
flags |= endedInTrigger;
|
||||
|
||||
if (triggerSphere.intersectsSphere(Sphere(start, objectExtentSphere.getRadius())))
|
||||
flags |= startedInTrigger;
|
||||
|
||||
switch (flags)
|
||||
{
|
||||
case (startedInTrigger | endedInTrigger):
|
||||
onEnter(object); // started and ended in the trigger volume
|
||||
break;
|
||||
case startedInTrigger:
|
||||
onExit(object); // it started in the trigger but ended outside of it
|
||||
break;
|
||||
case endedInTrigger:
|
||||
onEnter(object); // started outside the trigger, but ended inside of it
|
||||
break;
|
||||
case notInTrigger:
|
||||
// The object crossed the trigger volume but did not start or end inside
|
||||
// it, so if we didn't move more than the triggerVolumeWarpDistance,
|
||||
// consider us as having entered and left the trigger volume.
|
||||
if (start.magnitudeBetweenSquared(end) < sqr(ConfigServerGame::getTriggerVolumeWarpDistance()))
|
||||
{
|
||||
onEnter(object);
|
||||
onExit(object);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
onExit(object);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
/**
|
||||
@brief move the trigger volume relative to an object to determine if
|
||||
there was an interaction.
|
||||
*/
|
||||
void TriggerVolume::moveTriggerVolume(ServerObject &object, Vector const &start, Vector const &end)
|
||||
{
|
||||
PROFILER_AUTO_BLOCK_DEFINE("TriggerVolume::moveTriggerVolume");
|
||||
static const int notInTrigger = 0x0000;
|
||||
static const int startedInTrigger = 0x0001;
|
||||
static const int endedInTrigger = 0x0002;
|
||||
|
||||
Sphere const &objectExtentSphere = object.getSphereExtent();
|
||||
|
||||
Vector intersectionPoint;
|
||||
if (end.magnitudeBetweenSquared(start) > Vector::NORMALIZE_THRESHOLD)
|
||||
intersectionPoint = objectExtentSphere.getCenter().findClosestPointOnLineSegment(start, end);
|
||||
else
|
||||
intersectionPoint = end;
|
||||
|
||||
if (objectExtentSphere.getCenter().magnitudeBetween(intersectionPoint) - objectExtentSphere.getRadius() < m_radius)
|
||||
{
|
||||
// there was an intersection along the path. Determine start and end sphere intersection
|
||||
// status
|
||||
int flags = notInTrigger;
|
||||
|
||||
if (objectExtentSphere.intersectsSphere(Sphere(end, m_radius)))
|
||||
flags |= endedInTrigger;
|
||||
|
||||
if (objectExtentSphere.intersectsSphere(Sphere(start, m_radius)))
|
||||
flags |= startedInTrigger;
|
||||
|
||||
switch (flags)
|
||||
{
|
||||
case (startedInTrigger | endedInTrigger):
|
||||
onEnter(object); // started and ended in the trigger volume
|
||||
break;
|
||||
case startedInTrigger:
|
||||
onExit(object); // it started in the trigger but ended outside of it
|
||||
break;
|
||||
case endedInTrigger:
|
||||
onEnter(object); // started outside the trigger, but ended inside of it
|
||||
break;
|
||||
case notInTrigger:
|
||||
onExit(object); // handle warping out
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerVolume::removeEventSource(NetworkId const &source)
|
||||
{
|
||||
IGNORE_RETURN(m_eventSources.erase(source));
|
||||
ServerObject * const object = ServerWorld::findObjectByNetworkId(source);
|
||||
if (object && intersectsExtent(*object))
|
||||
onExit(*object);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerVolume::removeObject(ServerObject &object)
|
||||
{
|
||||
onExit(object); // doesn't hurt if it isn't already in the volume
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerVolume::runScriptTriggers()
|
||||
{
|
||||
if (!s_triggers->getIsEnter().empty())
|
||||
{
|
||||
ScriptParams s;
|
||||
s.addParam(const_cast<std::deque<bool> &>(s_triggers->getIsEnter()), "is_enter_trigger");
|
||||
s.addParam(const_cast<std::vector<const char *> &>(s_triggers->getNames()), "trigger_volume_names");
|
||||
s.addParam(const_cast<std::vector<NetworkId> &>(s_triggers->getSources()), "source_objects");
|
||||
s.addParam(const_cast<std::vector<NetworkId> &>(s_triggers->getTargets()), "target_objects");
|
||||
|
||||
// set s_triggers to use the other TriggerScriptParam so that
|
||||
// any TriggerVolume update that occurs because of the call
|
||||
// into script below will get processed the next time
|
||||
// TriggerVolume::runScriptTriggers() gets called;
|
||||
// hang on to the current TriggerScriptParam so that we can
|
||||
// clear it out after the call into script below
|
||||
TriggerScriptParam * current = s_triggers;
|
||||
s_triggers = ((s_triggers == &s_triggers1) ? &s_triggers2 : &s_triggers1);
|
||||
|
||||
// invoke the script for trigger enters
|
||||
GameScriptObject::runOneScript("base_class", "dispatchTriggerVolumeEvents", "b[s[O[O[", s);
|
||||
|
||||
// triggers have been processed
|
||||
// MUST be done after calling GameScriptObject::runOneScript() since
|
||||
// ScriptParams just save off the pointers to the vectors; the vectors
|
||||
// don't actually get packed until GameScriptObject::runOneScript()
|
||||
// is called
|
||||
current->clear();
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerVolume::setPromiscuous(bool isPromiscuous)
|
||||
{
|
||||
m_promiscuous = isPromiscuous;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerVolume::setSpatialSubdivisionHandle(SpatialSubdivisionHandle *newHandle)
|
||||
{
|
||||
m_spatialSubdivisionHandle = newHandle;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
bool TriggerVolume::isNetworkTriggerVolume() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
bool TriggerVolume::isPortalTriggerVolume() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerVolume::objectMoved(ServerObject &object)
|
||||
{
|
||||
if (intersectsExtent(object))
|
||||
onEnter(object);
|
||||
else
|
||||
onExit(object);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerVolume::objectDestroyed(ServerObject &object)
|
||||
{
|
||||
// NOTE: do not reference the ServerObject because
|
||||
// it is being destroyed and is not guaranteed to be
|
||||
// in a valid state
|
||||
IGNORE_RETURN(m_contents.erase(&object));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void TriggerVolume::contentsChanged(std::vector<ServerObject *> const &newSortedContents)
|
||||
{
|
||||
static std::vector<ServerObject *> lostObjects;
|
||||
static std::vector<ServerObject *> gainedObjects;
|
||||
|
||||
IGNORE_RETURN(std::set_difference(
|
||||
m_contents.begin(), m_contents.end(),
|
||||
newSortedContents.begin(), newSortedContents.end(),
|
||||
std::back_insert_iterator<std::vector<ServerObject *> >(lostObjects)));
|
||||
IGNORE_RETURN(std::set_difference(
|
||||
newSortedContents.begin(), newSortedContents.end(),
|
||||
m_contents.begin(), m_contents.end(),
|
||||
std::back_insert_iterator<std::vector<ServerObject *> >(gainedObjects)));
|
||||
|
||||
m_contents.clear();
|
||||
m_contents.insert(newSortedContents.begin(), newSortedContents.end());
|
||||
|
||||
for (std::vector<ServerObject *>::const_iterator i = lostObjects.begin(); i != lostObjects.end(); ++i)
|
||||
{
|
||||
virtualOnExit(**i);
|
||||
(*i)->onRemovedFromTriggerVolume(*this);
|
||||
}
|
||||
for (std::vector<ServerObject *>::const_iterator j = gainedObjects.begin(); j != gainedObjects.end(); ++j)
|
||||
{
|
||||
virtualOnEnter(**j);
|
||||
(*j)->onAddedToTriggerVolume(*this);
|
||||
}
|
||||
|
||||
lostObjects.clear();
|
||||
gainedObjects.clear();
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
|
||||
Reference in New Issue
Block a user