1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-01-30 12:32:36 +00:00
OpenMW/apps/openmw/mwrender/animation.hpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

540 lines
20 KiB
C++
Raw Normal View History

#ifndef GAME_RENDER_ANIMATION_H
#define GAME_RENDER_ANIMATION_H
#include "../mwworld/ptr.hpp"
#include <components/misc/strings/algorithm.hpp>
2015-06-14 23:13:26 +02:00
#include <components/sceneutil/controller.hpp>
#include <components/sceneutil/nodecallback.hpp>
#include <components/sceneutil/textkeymap.hpp>
#include <components/sceneutil/util.hpp>
#include <unordered_map>
#include <vector>
2014-02-23 20:11:05 +01:00
namespace ESM
{
struct Light;
struct MagicEffect;
2014-02-23 20:11:05 +01:00
}
namespace Resource
{
class ResourceSystem;
}
namespace SceneUtil
{
class KeyframeHolder;
2015-06-14 23:13:26 +02:00
class KeyframeController;
class LightSource;
class LightListCallback;
2015-12-03 20:06:00 +01:00
class Skeleton;
}
namespace MWRender
{
2015-04-23 22:46:07 +02:00
class ResetAccumRootCallback;
2015-04-19 17:55:56 +02:00
class RotateController;
2015-05-31 01:07:43 +02:00
class TransparencyUpdater;
2022-04-08 22:04:32 +02:00
class EffectAnimationTime : public SceneUtil::ControllerSource
2015-05-31 01:07:43 +02:00
{
private:
float mTime;
2022-09-22 21:26:05 +03:00
public:
float getValue(osg::NodeVisitor* nv) override;
void addTime(float duration);
2015-04-19 17:55:56 +02:00
void resetTime(float time);
float getTime() const;
EffectAnimationTime()
2015-04-19 17:55:56 +02:00
: mTime(0)
{
}
};
2015-05-22 00:55:43 +02:00
/// @brief Detaches the node from its parent when the object goes out of scope.
class PartHolder
{
public:
PartHolder(osg::ref_ptr<osg::Node> node);
~PartHolder();
2015-05-22 00:55:43 +02:00
const osg::ref_ptr<osg::Node>& getNode() const { return mNode; }
private:
osg::ref_ptr<osg::Node> mNode;
void operator=(const PartHolder&);
PartHolder(const PartHolder&);
2022-09-22 21:26:05 +03:00
};
using PartHolderPtr = std::unique_ptr<PartHolder>;
2013-05-10 16:33:13 -07:00
2018-07-15 12:44:25 +04:00
struct EffectParams
2022-09-22 21:26:05 +03:00
{
std::string mModelName; // Just here so we don't add the same effect twice
2018-07-15 12:44:25 +04:00
std::shared_ptr<EffectAnimationTime> mAnimTime;
float mMaxControllerLength;
int mEffectId;
2018-07-15 12:44:25 +04:00
bool mLoop;
std::string mBoneName;
};
class Animation : public osg::Referenced
{
public:
enum BoneGroup
{
BoneGroup_LowerBody = 0,
BoneGroup_Torso,
BoneGroup_LeftArm,
BoneGroup_RightArm
};
enum BlendMask
2022-09-22 21:26:05 +03:00
{
BlendMask_LowerBody = 1 << 0,
2015-06-14 23:13:26 +02:00
BlendMask_Torso = 1 << 1,
BlendMask_LeftArm = 1 << 2,
BlendMask_RightArm = 1 << 3,
BlendMask_UpperBody = BlendMask_Torso | BlendMask_LeftArm | BlendMask_RightArm,
BlendMask_All = BlendMask_LowerBody | BlendMask_UpperBody
2022-09-22 21:26:05 +03:00
};
/* This is the number of *discrete* blend masks. */
static constexpr size_t sNumBlendMasks = 4;
/// Holds an animation priority value for each BoneGroup.
struct AnimPriority
2022-09-22 21:26:05 +03:00
{
/// Convenience constructor, initialises all priorities to the same value.
AnimPriority(int priority)
2022-09-22 21:26:05 +03:00
{
for (unsigned int i = 0; i < sNumBlendMasks; ++i)
mPriority[i] = priority;
2022-09-22 21:26:05 +03:00
}
bool operator==(const AnimPriority& other) const
2022-09-22 21:26:05 +03:00
{
for (unsigned int i = 0; i < sNumBlendMasks; ++i)
if (other.mPriority[i] != mPriority[i])
return false;
return true;
2022-09-22 21:26:05 +03:00
}
int& operator[](BoneGroup n) { return mPriority[n]; }
2022-09-22 21:26:05 +03:00
const int& operator[](BoneGroup n) const { return mPriority[n]; }
2022-09-22 21:26:05 +03:00
bool contains(int priority) const
2022-09-22 21:26:05 +03:00
{
for (unsigned int i = 0; i < sNumBlendMasks; ++i)
if (priority == mPriority[i])
return true;
return false;
2022-09-22 21:26:05 +03:00
}
int mPriority[sNumBlendMasks];
2022-09-22 21:26:05 +03:00
};
2015-05-22 00:55:43 +02:00
class TextKeyListener
2022-09-22 21:26:05 +03:00
{
public:
virtual void handleTextKey(
2022-08-23 18:25:25 +02:00
std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map)
2022-09-22 21:26:05 +03:00
= 0;
virtual ~TextKeyListener() = default;
2022-09-22 21:26:05 +03:00
};
void setTextKeyListener(TextKeyListener* listener);
2015-06-14 23:13:26 +02:00
virtual bool updateCarriedLeftVisible(const int weaptype) const { return false; };
2016-08-22 22:58:24 +02:00
typedef std::unordered_map<std::string, osg::ref_ptr<osg::MatrixTransform>, Misc::StringUtils::CiHash,
Misc::StringUtils::CiEqual>
2022-09-22 21:26:05 +03:00
NodeMap;
protected:
2016-08-22 22:58:24 +02:00
class AnimationTime : public SceneUtil::ControllerSource
{
2022-08-23 18:25:25 +02:00
private:
std::shared_ptr<float> mTimePtr;
public:
void setTimePtr(std::shared_ptr<float> time) { mTimePtr = time; }
std::shared_ptr<float> getTimePtr() const { return mTimePtr; }
float getValue(osg::NodeVisitor* nv) override;
2022-09-22 21:26:05 +03:00
};
2015-12-03 20:06:00 +01:00
class NullAnimationTime : public SceneUtil::ControllerSource
2022-09-22 21:26:05 +03:00
{
public:
2015-12-03 20:06:00 +01:00
float getValue(osg::NodeVisitor* nv) override { return 0.f; }
2022-09-22 21:26:05 +03:00
};
struct AnimSource;
struct AnimState
2022-09-22 21:26:05 +03:00
{
std::shared_ptr<AnimSource> mSource;
float mStartTime;
float mLoopStartTime;
float mLoopStopTime;
float mStopTime;
2015-04-23 22:46:07 +02:00
typedef std::shared_ptr<float> TimePtr;
TimePtr mTime;
float mSpeedMult;
bool mPlaying;
bool mLoopingEnabled;
size_t mLoopCount;
AnimPriority mPriority;
int mBlendMask;
bool mAutoDisable;
AnimState()
: mStartTime(0.0f)
, mLoopStartTime(0.0f)
, mLoopStopTime(0.0f)
, mStopTime(0.0f)
, mTime(new float)
, mSpeedMult(1.0f)
, mPlaying(false)
, mLoopingEnabled(true)
, mLoopCount(0)
, mPriority(0)
, mBlendMask(0)
, mAutoDisable(true)
2022-09-22 21:26:05 +03:00
{
}
~AnimState() = default;
float getTime() const { return *mTime; }
void setTime(float time) { *mTime = time; }
bool shouldLoop() const { return getTime() >= mLoopStopTime && mLoopingEnabled && mLoopCount > 0; }
2022-09-22 21:26:05 +03:00
};
typedef std::map<std::string, AnimState, std::less<>> AnimStateMap;
AnimStateMap mStates;
typedef std::vector<std::shared_ptr<AnimSource>> AnimSourceList;
AnimSourceList mAnimSources;
2022-09-22 21:26:05 +03:00
osg::ref_ptr<osg::Group> mInsert;
2022-09-22 21:26:05 +03:00
osg::ref_ptr<osg::Group> mObjectRoot;
2015-04-23 23:30:06 +02:00
SceneUtil::Skeleton* mSkeleton;
2015-05-22 00:55:43 +02:00
// The node expected to accumulate movement during movement animations.
osg::ref_ptr<osg::Node> mAccumRoot;
// The controller animating that node.
osg::ref_ptr<SceneUtil::KeyframeController> mAccumCtrl;
2022-09-22 21:26:05 +03:00
2015-04-23 22:46:07 +02:00
// Used to reset the position of the accumulation root every frame - the movement should be applied to the
// physics system
osg::ref_ptr<ResetAccumRootCallback> mResetAccumRootCallback;
2022-09-22 21:26:05 +03:00
// Keep track of controllers that we added to our scene graph.
// We may need to rebuild these controllers when the active animation groups / sources change.
std::vector<std::pair<osg::ref_ptr<osg::Node>, osg::ref_ptr<osg::Callback>>> mActiveControllers;
2022-09-22 21:26:05 +03:00
std::shared_ptr<AnimationTime> mAnimationTimePtr[sNumBlendMasks];
2022-09-22 21:26:05 +03:00
mutable NodeMap mNodeMap;
mutable bool mNodeMapCreated;
2022-09-22 21:26:05 +03:00
MWWorld::Ptr mPtr;
2022-09-22 21:26:05 +03:00
2015-04-15 22:11:38 +02:00
Resource::ResourceSystem* mResourceSystem;
2022-09-22 21:26:05 +03:00
osg::Vec3f mAccumulate;
2022-09-22 21:26:05 +03:00
2015-05-22 00:55:43 +02:00
TextKeyListener* mTextKeyListener;
2022-09-22 21:26:05 +03:00
osg::ref_ptr<RotateController> mHeadController;
2020-06-22 02:03:38 +02:00
osg::ref_ptr<RotateController> mSpineController;
osg::ref_ptr<RotateController> mRootController;
float mHeadYawRadians;
float mHeadPitchRadians;
2020-06-22 02:03:38 +02:00
float mUpperBodyYawRadians;
float mLegsYawRadians;
float mBodyPitchRadians;
2022-09-22 21:26:05 +03:00
2022-08-23 18:25:25 +02:00
osg::ref_ptr<RotateController> addRotateController(std::string_view bone);
2022-09-22 21:26:05 +03:00
bool mHasMagicEffects;
2022-09-22 21:26:05 +03:00
osg::ref_ptr<SceneUtil::LightSource> mGlowLight;
osg::ref_ptr<SceneUtil::GlowUpdater> mGlowUpdater;
osg::ref_ptr<TransparencyUpdater> mTransparencyUpdater;
2021-04-05 10:43:17 -07:00
osg::ref_ptr<SceneUtil::LightSource> mExtraLightSource;
2022-09-22 21:26:05 +03:00
float mAlpha;
2022-09-22 21:26:05 +03:00
mutable std::map<std::string, float, std::less<>> mAnimVelocities;
2022-09-22 21:26:05 +03:00
osg::ref_ptr<SceneUtil::LightListCallback> mLightListCallback;
2020-06-22 02:03:38 +02:00
2022-08-23 18:25:25 +02:00
const NodeMap& getNodeMap() const;
2020-06-22 02:03:38 +02:00
/* Sets the appropriate animations on the bone groups based on priority.
2022-09-22 21:26:05 +03:00
*/
void resetActiveGroups();
size_t detectBlendMask(const osg::Node* node) const;
/* Updates the position of the accum root node for the given time, and
* returns the wanted movement vector from the previous time. */
void updatePosition(float oldtime, float newtime, osg::Vec3f& position);
/* Resets the animation to the time of the specified start marker, without
* moving anything, and set the end time to the specified stop marker. If
* the marker is not found, or if the markers are the same, it returns
2022-09-22 21:26:05 +03:00
* false.
*/
bool reset(AnimState& state, const SceneUtil::TextKeyMap& keys, std::string_view groupname,
std::string_view start, std::string_view stop, float startpoint, bool loopfallback);
2017-02-04 17:41:41 +01:00
void handleTextKey(AnimState& state, std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key,
const SceneUtil::TextKeyMap& map);
2015-04-23 23:30:06 +02:00
/** Sets the root model of the object.
2022-09-22 21:26:05 +03:00
*
* Note that you must make sure all animation sources are cleared before resetting the object
* root. All nodes previously retrieved with getNode will also become invalidated.
* @param forceskeleton Wrap the object root in a Skeleton, even if it contains no skinned parts. Use this if
2015-04-23 23:30:06 +02:00
* you intend to add skinned parts manually.
* @param baseonly If true, then any meshes or particle systems in the model are ignored
* (useful for NPCs, where only the skeleton is needed for the root, and the actual NPC parts are then
* assembled from separate files).
2022-09-22 21:26:05 +03:00
*/
void setObjectRoot(const std::string& model, bool forceskeleton, bool baseonly, bool isCreature);
void loadAllAnimationsInFolder(const std::string& model, const std::string& baseModel);
2016-05-19 22:30:14 +02:00
/** Adds the keyframe controllers in the specified model as a new animation source.
* @note Later added animation sources have the highest priority when it comes to finding a particular
2016-02-03 14:40:59 +01:00
* animation.
2016-05-19 22:30:14 +02:00
* @param model The file to add the keyframes for. Note that the .nif file extension will be replaced with .kf.
* @param baseModel The filename of the mObjectRoot, only used for error messages.
2022-09-22 21:26:05 +03:00
*/
2022-08-21 18:53:38 +02:00
void addAnimSource(std::string_view model, const std::string& baseModel);
2016-05-19 22:30:14 +02:00
void addSingleAnimSource(const std::string& model, const std::string& baseModel);
/** Adds an additional light to the given node using the specified ESM record. */
2015-04-23 22:46:07 +02:00
void addExtraLight(osg::ref_ptr<osg::Group> parent, const ESM::Light* light);
void clearAnimSources();
2022-09-22 21:26:05 +03:00
/**
2022-08-23 18:25:25 +02:00
* Provided to allow derived classes adding their own controllers. Note, the controllers must be added to
* mActiveControllers so they get cleaned up properly on the next controller rebuild. A controller rebuild may
* be necessary to ensure correct ordering.
2022-09-22 21:26:05 +03:00
*/
virtual void addControllers();
2022-09-22 21:26:05 +03:00
public:
2016-12-14 16:39:33 +01:00
Animation(
const MWWorld::Ptr& ptr, osg::ref_ptr<osg::Group> parentNode, Resource::ResourceSystem* resourceSystem);
2022-09-22 21:26:05 +03:00
2016-12-14 16:39:33 +01:00
/// Must be thread safe
virtual ~Animation();
2022-09-22 21:26:05 +03:00
MWWorld::ConstPtr getPtr() const { return mPtr; }
2022-09-22 21:26:05 +03:00
MWWorld::Ptr getPtr() { return mPtr; }
2022-09-22 21:26:05 +03:00
2016-12-14 16:39:33 +01:00
/// Set active flag on the object skeleton, if one exists.
/// @see SceneUtil::Skeleton::setActive
/// 0 = Inactive, 1 = Active in place, 2 = Active
void setActive(int active);
2022-09-22 21:26:05 +03:00
osg::Group* getOrCreateObjectRoot();
2022-09-22 21:26:05 +03:00
2016-12-14 16:39:33 +01:00
osg::Group* getObjectRoot();
2022-09-22 21:26:05 +03:00
/**
2016-12-14 16:39:33 +01:00
* @brief Add an effect mesh attached to a bone or the insert scene node
* @param model
2015-04-23 23:30:06 +02:00
* @param effectId An ID for this effect by which you can identify it later. If this is not wanted, set to -1.
* @param loop Loop the effect. If false, it is removed automatically after it finishes playing. If true,
* you need to remove it manually using removeEffect when the effect should end.
* @param bonename Bone to attach to, or empty string to use the scene node instead
* @param texture override the texture specified in the model's materials - if empty, do not override
* @note Will not add an effect twice.
*/
void addEffect(const std::string& model, int effectId, bool loop = false, std::string_view bonename = {},
std::string_view texture = {});
2015-04-19 01:57:52 +02:00
void removeEffect(int effectId);
void removeEffects();
void getLoopingEffects(std::vector<int>& out) const;
2022-09-22 21:26:05 +03:00
// Add a spell casting glow to an object. From measuring video taken from the original engine,
// the glow seems to be about 1.5 seconds except for telekinesis, which is 1 second.
2016-08-23 23:13:37 +09:00
void addSpellCastGlow(const ESM::MagicEffect* effect, float glowDuration = 1.5);
2022-09-22 21:26:05 +03:00
2015-05-14 17:34:55 +02:00
virtual void updatePtr(const MWWorld::Ptr& ptr);
2022-09-22 21:26:05 +03:00
bool hasAnimation(std::string_view anim) const;
2022-09-22 21:26:05 +03:00
// Specifies the axis' to accumulate on. Non-accumulated axis will just
// move visually, but not affect the actual movement. Each x/y/z value
// should be on the scale of 0 to 1.
void setAccumulation(const osg::Vec3f& accum);
2022-09-22 21:26:05 +03:00
/** Plays an animation.
* \param groupname Name of the animation group to play.
* \param priority Priority of the animation. The animation will play on
* bone groups that don't have another animation set of a
* higher priority.
* \param blendMask Bone groups to play the animation on.
* \param autodisable Automatically disable the animation when it stops
* playing.
* \param speedmult Speed multiplier for the animation.
* \param start Key marker from which to start.
* \param stop Key marker to stop at.
* \param startpoint How far in between the two markers to start. 0 starts
* at the start marker, 1 starts at the stop marker.
* \param loops How many times to loop the animation. This will use the
* "loop start" and "loop stop" markers if they exist,
* otherwise it may fall back to "start" and "stop", but only if
* the \a loopFallback parameter is true.
* \param loopFallback Allow looping an animation that has no loop keys, i.e. fall back to use
* the "start" and "stop" keys for looping?
2022-09-22 21:26:05 +03:00
*/
void play(std::string_view groupname, const AnimPriority& priority, int blendMask, bool autodisable,
2022-08-23 18:25:25 +02:00
float speedmult, std::string_view start, std::string_view stop, float startpoint, size_t loops,
bool loopfallback = false);
2022-09-22 21:26:05 +03:00
/** Adjust the speed multiplier of an already playing animation.
2022-09-22 21:26:05 +03:00
*/
void adjustSpeedMult(const std::string& groupname, float speedmult);
2022-09-22 21:26:05 +03:00
2013-05-12 05:08:01 -07:00
/** Returns true if the named animation group is playing. */
2022-08-23 18:25:25 +02:00
bool isPlaying(std::string_view groupname) const;
2022-09-22 21:26:05 +03:00
/// Returns true if no important animations are currently playing on the upper body.
2015-05-12 17:40:42 +02:00
bool upperBodyReady() const;
2022-09-22 21:26:05 +03:00
/** Gets info about the given animation group.
* \param groupname Animation group to check.
* \param complete Stores completion amount (0 = at start key, 0.5 = half way between start and stop keys), etc.
* \param speedmult Stores the animation speed multiplier
* \return True if the animation is active, false otherwise.
2022-09-22 21:26:05 +03:00
*/
2022-08-23 18:25:25 +02:00
bool getInfo(std::string_view groupname, float* complete = nullptr, float* speedmult = nullptr) const;
2022-09-22 21:26:05 +03:00
/// Get the absolute position in the animation track of the first text key with the given group.
float getStartTime(const std::string& groupname) const;
2022-09-22 21:26:05 +03:00
2014-06-11 00:20:46 +04:00
/// Get the absolute position in the animation track of the text key
2022-08-23 18:25:25 +02:00
float getTextKeyTime(std::string_view textKey) const;
2022-09-22 21:26:05 +03:00
/// Get the current absolute position in the animation track for the animation that is currently playing from
/// the given group.
float getCurrentTime(const std::string& groupname) const;
2022-09-22 21:26:05 +03:00
size_t getCurrentLoopCount(const std::string& groupname) const;
2022-09-22 21:26:05 +03:00
/** Disables the specified animation group;
* \param groupname Animation group to disable.
2022-09-22 21:26:05 +03:00
*/
void disable(std::string_view groupname);
2022-09-22 21:26:05 +03:00
/** Retrieves the velocity (in units per second) that the animation will move. */
float getVelocity(std::string_view groupname) const;
2022-09-22 21:26:05 +03:00
virtual osg::Vec3f runAnimation(float duration);
2022-09-22 21:26:05 +03:00
2022-08-23 18:25:25 +02:00
void setLoopingEnabled(std::string_view groupname, bool enabled);
2022-09-22 21:26:05 +03:00
/// This is typically called as part of runAnimation, but may be called manually if needed.
void updateEffects();
2022-09-22 21:26:05 +03:00
2018-10-09 10:21:12 +04:00
/// Return a node with the specified name, or nullptr if not existing.
2015-05-20 02:18:20 +02:00
/// @note The matching is case-insensitive.
2022-08-23 18:25:25 +02:00
const osg::Node* getNode(std::string_view name) const;
2022-09-22 21:26:05 +03:00
virtual bool useShieldAnimations() const { return false; }
virtual bool getWeaponsShown() const { return false; }
2015-04-25 01:20:07 +02:00
virtual void showWeapons(bool showWeapon) {}
virtual bool getCarriedLeftShown() const { return false; }
2015-04-25 01:20:07 +02:00
virtual void showCarriedLeft(bool show) {}
virtual void setWeaponGroup(const std::string& group, bool relativeDuration) {}
2015-04-25 01:20:07 +02:00
virtual void setVampire(bool vampire) {}
/// A value < 1 makes the animation translucent, 1.f = fully opaque
void setAlpha(float alpha);
2015-04-25 01:20:07 +02:00
virtual void setPitchFactor(float factor) {}
virtual void attachArrow() {}
virtual void detachArrow() {}
virtual void releaseArrow(float attackStrength) {}
2015-04-25 01:20:07 +02:00
virtual void enableHeadAnimation(bool enable) {}
// TODO: move outside of this class
/// Makes this object glow, by placing a Light in its center.
/// @param effect Controls the radius and intensity of the light.
virtual void setLightEffect(float effect);
2022-09-22 21:26:05 +03:00
virtual void setHeadPitch(float pitchRadians);
virtual void setHeadYaw(float yawRadians);
virtual float getHeadPitch() const;
virtual float getHeadYaw() const;
2022-09-22 21:26:05 +03:00
2020-06-22 02:03:38 +02:00
virtual void setUpperBodyYawRadians(float v) { mUpperBodyYawRadians = v; }
virtual void setLegsYawRadians(float v) { mLegsYawRadians = v; }
virtual float getUpperBodyYawRadians() const { return mUpperBodyYawRadians; }
virtual float getLegsYawRadians() const { return mLegsYawRadians; }
virtual void setBodyPitchRadians(float v) { mBodyPitchRadians = v; }
virtual float getBodyPitchRadians() const { return mBodyPitchRadians; }
2022-09-22 21:26:05 +03:00
virtual void setAccurateAiming(bool enabled) {}
virtual bool canBeHarvested() const { return false; }
2022-09-22 21:26:05 +03:00
virtual void removeFromScene();
private:
Animation(const Animation&);
void operator=(Animation&);
2022-09-22 21:26:05 +03:00
};
class ObjectAnimation : public Animation
2018-07-15 12:44:25 +04:00
{
2022-09-22 21:26:05 +03:00
public:
2015-05-28 15:44:58 +02:00
ObjectAnimation(const MWWorld::Ptr& ptr, const std::string& model, Resource::ResourceSystem* resourceSystem,
bool animated, bool allowLight);
2018-07-15 12:44:25 +04:00
bool canBeHarvested() const override;
};
class UpdateVfxCallback : public SceneUtil::NodeCallback<UpdateVfxCallback>
2022-09-22 21:26:05 +03:00
{
public:
2018-07-15 12:44:25 +04:00
UpdateVfxCallback(EffectParams& params)
: mFinished(false)
, mParams(params)
2018-07-15 12:44:25 +04:00
, mStartingTime(0)
2022-09-22 21:26:05 +03:00
{
}
2018-07-15 12:44:25 +04:00
2015-05-28 15:44:58 +02:00
bool mFinished;
2018-07-15 12:44:25 +04:00
EffectParams mParams;
2022-09-22 21:26:05 +03:00
void operator()(osg::Node* node, osg::NodeVisitor* nv);
2022-09-22 21:26:05 +03:00
2018-07-15 12:44:25 +04:00
private:
double mStartingTime;
};
}
#endif