forked from mirror/openmw-tes3mp
Implement AiWander fast-forward (Feature #1125)
This commit is contained in:
parent
d26d5f6c26
commit
a8ae0dec52
11 changed files with 141 additions and 108 deletions
|
@ -1641,4 +1641,18 @@ namespace MWMechanics
|
||||||
|
|
||||||
return it->second->getCharacterController()->isReadyToBlock();
|
return it->second->getCharacterController()->isReadyToBlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Actors::fastForwardAi()
|
||||||
|
{
|
||||||
|
if (!MWBase::Environment::get().getMechanicsManager()->isAIActive())
|
||||||
|
return;
|
||||||
|
for (PtrActorMap::iterator it = mActors.begin(); it != mActors.end(); ++it)
|
||||||
|
{
|
||||||
|
MWWorld::Ptr ptr = it->first;
|
||||||
|
if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr())
|
||||||
|
continue;
|
||||||
|
MWMechanics::AiSequence& seq = ptr.getClass().getCreatureStats(ptr).getAiSequence();
|
||||||
|
seq.fastForward(ptr, it->second->getAiState());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,6 +101,9 @@ namespace MWMechanics
|
||||||
int getHoursToRest(const MWWorld::Ptr& ptr) const;
|
int getHoursToRest(const MWWorld::Ptr& ptr) const;
|
||||||
///< Calculate how many hours the given actor needs to rest in order to be fully healed
|
///< Calculate how many hours the given actor needs to rest in order to be fully healed
|
||||||
|
|
||||||
|
void fastForwardAi();
|
||||||
|
///< Simulate the passing of time
|
||||||
|
|
||||||
int countDeaths (const std::string& id) const;
|
int countDeaths (const std::string& id) const;
|
||||||
///< Return the number of deaths for actors with the given ID.
|
///< Return the number of deaths for actors with the given ID.
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,9 @@ namespace MWMechanics
|
||||||
|
|
||||||
virtual void writeState (ESM::AiSequence::AiSequence& sequence) const {}
|
virtual void writeState (ESM::AiSequence::AiSequence& sequence) const {}
|
||||||
|
|
||||||
|
/// Simulates the passing of time
|
||||||
|
virtual void fastForward(const MWWorld::Ptr& actor, AiState& state) {}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/// Causes the actor to attempt to walk to the specified location
|
/// Causes the actor to attempt to walk to the specified location
|
||||||
/** \return If the actor has arrived at his destination **/
|
/** \return If the actor has arrived at his destination **/
|
||||||
|
|
|
@ -390,4 +390,13 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AiSequence::fastForward(const MWWorld::Ptr& actor, AiState& state)
|
||||||
|
{
|
||||||
|
if (!mPackages.empty())
|
||||||
|
{
|
||||||
|
MWMechanics::AiPackage* package = mPackages.front();
|
||||||
|
package->fastForward(actor, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace MWMechanics
|
} // namespace MWMechanics
|
||||||
|
|
|
@ -97,6 +97,9 @@ namespace MWMechanics
|
||||||
/// Execute current package, switching if needed.
|
/// Execute current package, switching if needed.
|
||||||
void execute (const MWWorld::Ptr& actor, MWMechanics::AiState& state, float duration);
|
void execute (const MWWorld::Ptr& actor, MWMechanics::AiState& state, float duration);
|
||||||
|
|
||||||
|
/// Simulate the passing of time using the currently active AI package
|
||||||
|
void fastForward(const MWWorld::Ptr &actor, AiState &state);
|
||||||
|
|
||||||
/// Remove all packages.
|
/// Remove all packages.
|
||||||
void clear();
|
void clear();
|
||||||
|
|
||||||
|
|
|
@ -76,24 +76,6 @@ namespace MWMechanics
|
||||||
mStorage = p;
|
mStorage = p;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// \brief gives away ownership of object. Throws exception if storage does not contain Derived or is empty.
|
|
||||||
template< class Derived >
|
|
||||||
Derived* moveOut()
|
|
||||||
{
|
|
||||||
assert_derived<Derived>();
|
|
||||||
|
|
||||||
|
|
||||||
if(!mStorage)
|
|
||||||
throw std::runtime_error("Cant move out: empty storage.");
|
|
||||||
|
|
||||||
Derived* result = dynamic_cast<Derived*>(mStorage);
|
|
||||||
|
|
||||||
if(!mStorage)
|
|
||||||
throw std::runtime_error("Cant move out: wrong type requested.");
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool empty() const
|
bool empty() const
|
||||||
{
|
{
|
||||||
return mStorage == NULL;
|
return mStorage == NULL;
|
||||||
|
@ -120,7 +102,7 @@ namespace MWMechanics
|
||||||
/// \brief base class for the temporary storage of AiPackages.
|
/// \brief base class for the temporary storage of AiPackages.
|
||||||
/**
|
/**
|
||||||
* Each AI package with temporary values needs a AiPackageStorage class
|
* Each AI package with temporary values needs a AiPackageStorage class
|
||||||
* which is derived from AiTemporaryBase. The CharacterController holds a container
|
* which is derived from AiTemporaryBase. The Actor holds a container
|
||||||
* AiState where one of these storages can be stored at a time.
|
* AiState where one of these storages can be stored at a time.
|
||||||
* The execute(...) member function takes this container as an argument.
|
* The execute(...) member function takes this container as an argument.
|
||||||
* */
|
* */
|
||||||
|
|
|
@ -113,6 +113,11 @@ namespace MWMechanics
|
||||||
return TypeIdTravel;
|
return TypeIdTravel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AiTravel::fastForward(const MWWorld::Ptr& actor, AiState& state)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
void AiTravel::writeState(ESM::AiSequence::AiSequence &sequence) const
|
void AiTravel::writeState(ESM::AiSequence::AiSequence &sequence) const
|
||||||
{
|
{
|
||||||
std::auto_ptr<ESM::AiSequence::AiTravel> travel(new ESM::AiSequence::AiTravel());
|
std::auto_ptr<ESM::AiSequence::AiTravel> travel(new ESM::AiSequence::AiTravel());
|
||||||
|
|
|
@ -23,6 +23,9 @@ namespace MWMechanics
|
||||||
AiTravel(float x, float y, float z);
|
AiTravel(float x, float y, float z);
|
||||||
AiTravel(const ESM::AiSequence::AiTravel* travel);
|
AiTravel(const ESM::AiSequence::AiTravel* travel);
|
||||||
|
|
||||||
|
/// Simulates the passing of time
|
||||||
|
virtual void fastForward(const MWWorld::Ptr& actor, AiState& state);
|
||||||
|
|
||||||
void writeState(ESM::AiSequence::AiSequence &sequence) const;
|
void writeState(ESM::AiSequence::AiSequence &sequence) const;
|
||||||
|
|
||||||
virtual AiTravel *clone() const;
|
virtual AiTravel *clone() const;
|
||||||
|
|
|
@ -40,16 +40,9 @@ namespace MWMechanics
|
||||||
|
|
||||||
AiWander::GreetingState mSaidGreeting;
|
AiWander::GreetingState mSaidGreeting;
|
||||||
int mGreetingTimer;
|
int mGreetingTimer;
|
||||||
|
|
||||||
// Cached current cell location
|
|
||||||
int mCellX;
|
|
||||||
int mCellY;
|
|
||||||
// Cell location multiplied by ESM::Land::REAL_SIZE
|
|
||||||
float mXCell;
|
|
||||||
float mYCell;
|
|
||||||
|
|
||||||
const MWWorld::CellStore* mCell; // for detecting cell change
|
const MWWorld::CellStore* mCell; // for detecting cell change
|
||||||
|
|
||||||
// AiWander states
|
// AiWander states
|
||||||
bool mChooseAction;
|
bool mChooseAction;
|
||||||
bool mIdleNow;
|
bool mIdleNow;
|
||||||
|
@ -66,10 +59,6 @@ namespace MWMechanics
|
||||||
mReaction(0),
|
mReaction(0),
|
||||||
mSaidGreeting(AiWander::Greet_None),
|
mSaidGreeting(AiWander::Greet_None),
|
||||||
mGreetingTimer(0),
|
mGreetingTimer(0),
|
||||||
mCellX(std::numeric_limits<int>::max()),
|
|
||||||
mCellY(std::numeric_limits<int>::max()),
|
|
||||||
mXCell(0),
|
|
||||||
mYCell(0),
|
|
||||||
mCell(NULL),
|
mCell(NULL),
|
||||||
mChooseAction(true),
|
mChooseAction(true),
|
||||||
mIdleNow(false),
|
mIdleNow(false),
|
||||||
|
@ -183,7 +172,6 @@ namespace MWMechanics
|
||||||
currentCell = actor.getCell();
|
currentCell = actor.getCell();
|
||||||
mStoredAvailableNodes = false; // prob. not needed since mDistance = 0
|
mStoredAvailableNodes = false; // prob. not needed since mDistance = 0
|
||||||
}
|
}
|
||||||
const ESM::Cell *cell = currentCell->getCell();
|
|
||||||
|
|
||||||
cStats.setDrawState(DrawState_Nothing);
|
cStats.setDrawState(DrawState_Nothing);
|
||||||
cStats.setMovementFlag(CreatureStats::Flag_Run, false);
|
cStats.setMovementFlag(CreatureStats::Flag_Run, false);
|
||||||
|
@ -371,81 +359,10 @@ namespace MWMechanics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
int& cachedCellX = storage.mCellX;
|
|
||||||
int& cachedCellY = storage.mCellY;
|
|
||||||
float& cachedCellXposition = storage.mXCell;
|
|
||||||
float& cachedCellYposition = storage.mYCell;
|
|
||||||
// Initialization to discover & store allowed node points for this actor.
|
// Initialization to discover & store allowed node points for this actor.
|
||||||
if(!mStoredAvailableNodes)
|
if(!mStoredAvailableNodes)
|
||||||
{
|
{
|
||||||
// infrequently used, therefore no benefit in caching it as a member
|
getAllowedNodes(actor, currentCell->getCell());
|
||||||
const ESM::Pathgrid *
|
|
||||||
pathgrid = world->getStore().get<ESM::Pathgrid>().search(*cell);
|
|
||||||
|
|
||||||
// cache the current cell location
|
|
||||||
cachedCellX = cell->mData.mX;
|
|
||||||
cachedCellY = cell->mData.mY;
|
|
||||||
|
|
||||||
// If there is no path this actor doesn't go anywhere. See:
|
|
||||||
// https://forum.openmw.org/viewtopic.php?t=1556
|
|
||||||
// http://www.fliggerty.com/phpBB3/viewtopic.php?f=30&t=5833
|
|
||||||
if(!pathgrid || pathgrid->mPoints.empty())
|
|
||||||
mDistance = 0;
|
|
||||||
|
|
||||||
// A distance value passed into the constructor indicates how far the
|
|
||||||
// actor can wander from the spawn position. AiWander assumes that
|
|
||||||
// pathgrid points are available, and uses them to randomly select wander
|
|
||||||
// destinations within the allowed set of pathgrid points (nodes).
|
|
||||||
if(mDistance)
|
|
||||||
{
|
|
||||||
cachedCellXposition = 0;
|
|
||||||
cachedCellYposition = 0;
|
|
||||||
if(cell->isExterior())
|
|
||||||
{
|
|
||||||
cachedCellXposition = cachedCellX * ESM::Land::REAL_SIZE;
|
|
||||||
cachedCellYposition = cachedCellY * ESM::Land::REAL_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: There might be a bug here. The allowed node points are
|
|
||||||
// based on the actor's current position rather than the actor's
|
|
||||||
// spawn point. As a result the allowed nodes for wander can change
|
|
||||||
// between saves, for example.
|
|
||||||
//
|
|
||||||
// convert npcPos to local (i.e. cell) co-ordinates
|
|
||||||
Ogre::Vector3 npcPos(pos.pos);
|
|
||||||
npcPos[0] = npcPos[0] - cachedCellXposition;
|
|
||||||
npcPos[1] = npcPos[1] - cachedCellYposition;
|
|
||||||
|
|
||||||
// mAllowedNodes for this actor with pathgrid point indexes based on mDistance
|
|
||||||
// NOTE: mPoints and mAllowedNodes are in local co-ordinates
|
|
||||||
for(unsigned int counter = 0; counter < pathgrid->mPoints.size(); counter++)
|
|
||||||
{
|
|
||||||
Ogre::Vector3 nodePos(pathgrid->mPoints[counter].mX, pathgrid->mPoints[counter].mY,
|
|
||||||
pathgrid->mPoints[counter].mZ);
|
|
||||||
if(npcPos.squaredDistance(nodePos) <= mDistance * mDistance)
|
|
||||||
mAllowedNodes.push_back(pathgrid->mPoints[counter]);
|
|
||||||
}
|
|
||||||
if(!mAllowedNodes.empty())
|
|
||||||
{
|
|
||||||
Ogre::Vector3 firstNodePos(mAllowedNodes[0].mX, mAllowedNodes[0].mY, mAllowedNodes[0].mZ);
|
|
||||||
float closestNode = npcPos.squaredDistance(firstNodePos);
|
|
||||||
unsigned int index = 0;
|
|
||||||
for(unsigned int counterThree = 1; counterThree < mAllowedNodes.size(); counterThree++)
|
|
||||||
{
|
|
||||||
Ogre::Vector3 nodePos(mAllowedNodes[counterThree].mX, mAllowedNodes[counterThree].mY,
|
|
||||||
mAllowedNodes[counterThree].mZ);
|
|
||||||
float tempDist = npcPos.squaredDistance(nodePos);
|
|
||||||
if(tempDist < closestNode)
|
|
||||||
index = counterThree;
|
|
||||||
}
|
|
||||||
mCurrentNode = mAllowedNodes[index];
|
|
||||||
mAllowedNodes.erase(mAllowedNodes.begin() + index);
|
|
||||||
|
|
||||||
mStoredAvailableNodes = true; // set only if successful in finding allowed nodes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actor becomes stationary - see above URL's for previous research
|
// Actor becomes stationary - see above URL's for previous research
|
||||||
|
@ -581,8 +498,8 @@ namespace MWMechanics
|
||||||
|
|
||||||
// convert dest to use world co-ordinates
|
// convert dest to use world co-ordinates
|
||||||
ESM::Pathgrid::Point dest;
|
ESM::Pathgrid::Point dest;
|
||||||
dest.mX = destNodePos[0] + cachedCellXposition;
|
dest.mX = destNodePos[0] + currentCell->getCell()->mData.mX * ESM::Land::REAL_SIZE;
|
||||||
dest.mY = destNodePos[1] + cachedCellYposition;
|
dest.mY = destNodePos[1] + currentCell->getCell()->mData.mY * ESM::Land::REAL_SIZE;
|
||||||
dest.mZ = destNodePos[2];
|
dest.mZ = destNodePos[2];
|
||||||
|
|
||||||
// actor position is already in world co-ordinates
|
// actor position is already in world co-ordinates
|
||||||
|
@ -732,6 +649,96 @@ namespace MWMechanics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AiWander::fastForward(const MWWorld::Ptr& actor, AiState &state)
|
||||||
|
{
|
||||||
|
if (mDistance == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!mStoredAvailableNodes)
|
||||||
|
getAllowedNodes(actor, actor.getCell()->getCell());
|
||||||
|
|
||||||
|
if (mAllowedNodes.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
state.moveIn(new AiWanderStorage());
|
||||||
|
|
||||||
|
int index = std::rand() / (static_cast<double> (RAND_MAX) + 1) * mAllowedNodes.size();
|
||||||
|
ESM::Pathgrid::Point dest = mAllowedNodes[index];
|
||||||
|
|
||||||
|
// apply a slight offset to prevent overcrowding
|
||||||
|
dest.mX += Ogre::Math::RangeRandom(-64, 64);
|
||||||
|
dest.mY += Ogre::Math::RangeRandom(-64, 64);
|
||||||
|
|
||||||
|
MWBase::Environment::get().getWorld()->moveObject(actor, dest.mX, dest.mY, dest.mZ);
|
||||||
|
actor.getClass().adjustPosition(actor, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiWander::getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell)
|
||||||
|
{
|
||||||
|
// infrequently used, therefore no benefit in caching it as a member
|
||||||
|
const ESM::Pathgrid *
|
||||||
|
pathgrid = MWBase::Environment::get().getWorld()->getStore().get<ESM::Pathgrid>().search(*cell);
|
||||||
|
|
||||||
|
// If there is no path this actor doesn't go anywhere. See:
|
||||||
|
// https://forum.openmw.org/viewtopic.php?t=1556
|
||||||
|
// http://www.fliggerty.com/phpBB3/viewtopic.php?f=30&t=5833
|
||||||
|
if(!pathgrid || pathgrid->mPoints.empty())
|
||||||
|
mDistance = 0;
|
||||||
|
|
||||||
|
// A distance value passed into the constructor indicates how far the
|
||||||
|
// actor can wander from the spawn position. AiWander assumes that
|
||||||
|
// pathgrid points are available, and uses them to randomly select wander
|
||||||
|
// destinations within the allowed set of pathgrid points (nodes).
|
||||||
|
if(mDistance)
|
||||||
|
{
|
||||||
|
float cellXOffset = 0;
|
||||||
|
float cellYOffset = 0;
|
||||||
|
if(cell->isExterior())
|
||||||
|
{
|
||||||
|
cellXOffset = cell->mData.mX * ESM::Land::REAL_SIZE;
|
||||||
|
cellYOffset = cell->mData.mY * ESM::Land::REAL_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: There might be a bug here. The allowed node points are
|
||||||
|
// based on the actor's current position rather than the actor's
|
||||||
|
// spawn point. As a result the allowed nodes for wander can change
|
||||||
|
// between saves, for example.
|
||||||
|
//
|
||||||
|
// convert npcPos to local (i.e. cell) co-ordinates
|
||||||
|
Ogre::Vector3 npcPos(actor.getRefData().getPosition().pos);
|
||||||
|
npcPos[0] = npcPos[0] - cellXOffset;
|
||||||
|
npcPos[1] = npcPos[1] - cellYOffset;
|
||||||
|
|
||||||
|
// mAllowedNodes for this actor with pathgrid point indexes based on mDistance
|
||||||
|
// NOTE: mPoints and mAllowedNodes are in local co-ordinates
|
||||||
|
for(unsigned int counter = 0; counter < pathgrid->mPoints.size(); counter++)
|
||||||
|
{
|
||||||
|
Ogre::Vector3 nodePos(pathgrid->mPoints[counter].mX, pathgrid->mPoints[counter].mY,
|
||||||
|
pathgrid->mPoints[counter].mZ);
|
||||||
|
if(npcPos.squaredDistance(nodePos) <= mDistance * mDistance)
|
||||||
|
mAllowedNodes.push_back(pathgrid->mPoints[counter]);
|
||||||
|
}
|
||||||
|
if(!mAllowedNodes.empty())
|
||||||
|
{
|
||||||
|
Ogre::Vector3 firstNodePos(mAllowedNodes[0].mX, mAllowedNodes[0].mY, mAllowedNodes[0].mZ);
|
||||||
|
float closestNode = npcPos.squaredDistance(firstNodePos);
|
||||||
|
unsigned int index = 0;
|
||||||
|
for(unsigned int counterThree = 1; counterThree < mAllowedNodes.size(); counterThree++)
|
||||||
|
{
|
||||||
|
Ogre::Vector3 nodePos(mAllowedNodes[counterThree].mX, mAllowedNodes[counterThree].mY,
|
||||||
|
mAllowedNodes[counterThree].mZ);
|
||||||
|
float tempDist = npcPos.squaredDistance(nodePos);
|
||||||
|
if(tempDist < closestNode)
|
||||||
|
index = counterThree;
|
||||||
|
}
|
||||||
|
mCurrentNode = mAllowedNodes[index];
|
||||||
|
mAllowedNodes.erase(mAllowedNodes.begin() + index);
|
||||||
|
|
||||||
|
mStoredAvailableNodes = true; // set only if successful in finding allowed nodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AiWander::writeState(ESM::AiSequence::AiSequence &sequence) const
|
void AiWander::writeState(ESM::AiSequence::AiSequence &sequence) const
|
||||||
{
|
{
|
||||||
std::auto_ptr<ESM::AiSequence::AiWander> wander(new ESM::AiSequence::AiWander());
|
std::auto_ptr<ESM::AiSequence::AiWander> wander(new ESM::AiSequence::AiWander());
|
||||||
|
|
|
@ -57,6 +57,7 @@ namespace MWMechanics
|
||||||
|
|
||||||
virtual void writeState(ESM::AiSequence::AiSequence &sequence) const;
|
virtual void writeState(ESM::AiSequence::AiSequence &sequence) const;
|
||||||
|
|
||||||
|
virtual void fastForward(const MWWorld::Ptr& actor, AiState& state);
|
||||||
|
|
||||||
enum GreetingState {
|
enum GreetingState {
|
||||||
Greet_None,
|
Greet_None,
|
||||||
|
@ -77,7 +78,6 @@ namespace MWMechanics
|
||||||
int mTimeOfDay;
|
int mTimeOfDay;
|
||||||
std::vector<unsigned char> mIdle;
|
std::vector<unsigned char> mIdle;
|
||||||
bool mRepeat;
|
bool mRepeat;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
bool mHasReturnPosition; // NOTE: Could be removed if mReturnPosition was initialized to actor position,
|
bool mHasReturnPosition; // NOTE: Could be removed if mReturnPosition was initialized to actor position,
|
||||||
|
@ -98,6 +98,9 @@ namespace MWMechanics
|
||||||
|
|
||||||
// allowed pathgrid nodes based on mDistance from the spawn point
|
// allowed pathgrid nodes based on mDistance from the spawn point
|
||||||
std::vector<ESM::Pathgrid::Point> mAllowedNodes;
|
std::vector<ESM::Pathgrid::Point> mAllowedNodes;
|
||||||
|
|
||||||
|
void getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell);
|
||||||
|
|
||||||
ESM::Pathgrid::Point mCurrentNode;
|
ESM::Pathgrid::Point mCurrentNode;
|
||||||
bool mTrimCurrentNode;
|
bool mTrimCurrentNode;
|
||||||
void trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes,
|
void trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes,
|
||||||
|
|
|
@ -472,6 +472,7 @@ namespace MWMechanics
|
||||||
void MechanicsManager::rest(bool sleep)
|
void MechanicsManager::rest(bool sleep)
|
||||||
{
|
{
|
||||||
mActors.restoreDynamicStats (sleep);
|
mActors.restoreDynamicStats (sleep);
|
||||||
|
mActors.fastForwardAi();
|
||||||
}
|
}
|
||||||
|
|
||||||
int MechanicsManager::getHoursToRest() const
|
int MechanicsManager::getHoursToRest() const
|
||||||
|
|
Loading…
Reference in a new issue