summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorThorbjørn Lindeijer <bjorn@lindeijer.nl>2024-10-01 17:59:04 +0200
committerThorbjørn Lindeijer <bjorn@lindeijer.nl>2024-10-08 21:01:45 +0200
commit59a7d5c58f8b3af21b3e19d4e78f5653bf011bfb (patch)
tree536f6b80e359922172e671fbbbeb2b7bc5d3f165 /src
parente115390f18734d9e3e0e2fc5e1ed05f44c9fdb60 (diff)
downloadmana-59a7d5c58f8b3af21b3e19d4e78f5653bf011bfb.tar.gz
mana-59a7d5c58f8b3af21b3e19d4e78f5653bf011bfb.tar.bz2
mana-59a7d5c58f8b3af21b3e19d4e78f5653bf011bfb.tar.xz
mana-59a7d5c58f8b3af21b3e19d4e78f5653bf011bfb.zip
Added convenient and efficient Timer class
The Timer is efficient because it does not depend on incrementing a counter to keep track of time, nor does it call SDL_GetTicks every time its state is checked (this happens once per frame instead). Along with global functions Time::absoluteTimeMs() and Time::deltaTimeMs(), this replaces previous globals tick_time, cur_time and get_elapsed_time(). For now, there is still a fixed 100 times per second logic call rate, but the new Time::deltaTimeMs() function should allow getting rid of this.
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt2
-rw-r--r--src/actorsprite.cpp7
-rw-r--r--src/being.cpp10
-rw-r--r--src/being.h4
-rw-r--r--src/client.cpp64
-rw-r--r--src/client.h20
-rw-r--r--src/game.cpp2
-rw-r--r--src/gui/gui.cpp4
-rw-r--r--src/gui/gui.h4
-rw-r--r--src/gui/ministatuswindow.cpp5
-rw-r--r--src/gui/palette.cpp146
-rw-r--r--src/gui/palette.h10
-rw-r--r--src/gui/socialwindow.cpp6
-rw-r--r--src/gui/socialwindow.h4
-rw-r--r--src/gui/viewport.cpp100
-rw-r--r--src/gui/viewport.h8
-rw-r--r--src/gui/widgets/browserbox.cpp8
-rw-r--r--src/gui/widgets/browserbox.h4
-rw-r--r--src/localplayer.cpp64
-rw-r--r--src/localplayer.h10
-rw-r--r--src/map.cpp15
-rw-r--r--src/net/tmwa/playerhandler.cpp9
-rw-r--r--src/resources/ambientlayer.cpp4
-rw-r--r--src/resources/userpalette.h2
-rw-r--r--src/utils/time.cpp63
-rw-r--r--src/utils/time.h106
26 files changed, 388 insertions, 293 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 9ced2c13..d5d512e8 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -381,6 +381,8 @@ set(SRCS
utils/specialfolder.h
utils/stringutils.cpp
utils/stringutils.h
+ utils/time.cpp
+ utils/time.h
utils/mutex.h
utils/mkdir.cpp
utils/mkdir.h
diff --git a/src/actorsprite.cpp b/src/actorsprite.cpp
index 62c5d1dc..20e724f5 100644
--- a/src/actorsprite.cpp
+++ b/src/actorsprite.cpp
@@ -20,7 +20,6 @@
#include "actorsprite.h"
-#include "client.h"
#include "configuration.h"
#include "event.h"
#include "imagesprite.h"
@@ -35,6 +34,8 @@
#include "resources/resourcemanager.h"
#include "resources/theme.h"
+#include "utils/time.h"
+
#include <cassert>
#define EFFECTS_FILE "effects.xml"
@@ -80,7 +81,7 @@ bool ActorSprite::draw(Graphics *graphics, int offsetX, int offsetY) const
if (mUsedTargetCursor)
{
mUsedTargetCursor->reset();
- mUsedTargetCursor->update(tick_time * MILLISECONDS_IN_A_TICK);
+ mUsedTargetCursor->update(Time::absoluteTimeMs());
mUsedTargetCursor->draw(graphics, px, py);
}
@@ -101,7 +102,7 @@ bool ActorSprite::drawSpriteAt(Graphics *graphics, int x, int y) const
void ActorSprite::logic()
{
// Update sprite animations
- update(tick_time * MILLISECONDS_IN_A_TICK);
+ update(Time::absoluteTimeMs());
// Restart status/particle effects, if needed
if (mMustResetParticles)
diff --git a/src/being.cpp b/src/being.cpp
index 95f7c0d5..88104d74 100644
--- a/src/being.cpp
+++ b/src/being.cpp
@@ -643,7 +643,7 @@ void Being::setAction(Action action, int attackId)
}
if (currentAction != SpriteAction::MOVE)
- mActionTime = tick_time;
+ mActionTimer.set();
}
void Being::lookAt(const Vector &destPos)
@@ -865,13 +865,9 @@ void Being::logic()
// Remove it after 1.5 secs if the dead animation isn't long enough,
// or simply play it until it's finished.
- if (!isAlive() && Net::getGameHandler()->removeDeadBeings() &&
- get_elapsed_time(mActionTime) > std::max(getDuration(), 1500))
- {
-
- if (getType() != PLAYER)
+ if (!isAlive() && Net::getGameHandler()->removeDeadBeings() && getType() != PLAYER)
+ if (mActionTimer.elapsed() > std::max(getDuration(), 1500))
actorSpriteManager->scheduleDelete(this);
- }
}
void Being::drawSpeech(int offsetX, int offsetY)
diff --git a/src/being.h b/src/being.h
index 3bd66b06..c8e8a87b 100644
--- a/src/being.h
+++ b/src/being.h
@@ -29,6 +29,8 @@
#include "position.h"
#include "vector.h"
+#include "utils/time.h"
+
#include <guichan/color.hpp>
#include <map>
@@ -484,7 +486,7 @@ class Being : public ActorSprite, public EventListener
const BeingInfo *mInfo;
- int mActionTime = 0; /**< Time spent in current action. TODO: Remove use of it */
+ Timer mActionTimer; /**< Time spent in current action. TODO: Remove use of it */
/** Time until the last speech sentence disappears */
int mSpeechTime = 0;
diff --git a/src/client.cpp b/src/client.cpp
index e98006a8..6137a88a 100644
--- a/src/client.cpp
+++ b/src/client.cpp
@@ -70,8 +70,11 @@
#include "utils/gettext.h"
#include "utils/mkdir.h"
+#if defined(_WIN32) || defined(__APPLE__)
#include "utils/specialfolder.h"
+#endif
#include "utils/stringutils.h"
+#include "utils/time.h"
#include <physfs.h>
#include <SDL_image.h>
@@ -84,12 +87,6 @@
#include <sys/stat.h>
#include <cassert>
-/**
- * Tells the max tick value,
- * setting it back to zero (and start again).
- */
-static const int MAX_TICK_VALUE = 10000;
-
// TODO: Get rid fo these globals
std::string errorMessage;
LoginData loginData;
@@ -109,23 +106,8 @@ HairDB hairDB; /**< Hair styles and colors info database */
Sound sound;
-volatile int tick_time; /**< Tick counter */
volatile int fps = 0; /**< Frames counted in the last second */
volatile int frame_count = 0; /**< Counts the frames during one second */
-volatile int cur_time;
-
-/**
- * Advances game logic counter.
- * Called every 10 milliseconds by SDL_AddTimer()
- * @see MILLISECONDS_IN_A_TICK value
- */
-Uint32 nextTick(Uint32 interval, void *param)
-{
- tick_time++;
- if (tick_time == MAX_TICK_VALUE)
- tick_time = 0;
- return interval;
-}
/**
* Updates fps.
@@ -139,27 +121,15 @@ Uint32 nextSecond(Uint32 interval, void *param)
return interval;
}
-int get_elapsed_time(int startTime)
-{
- if (startTime <= tick_time)
- return (tick_time - startTime) * MILLISECONDS_IN_A_TICK;
-
- return (tick_time + (MAX_TICK_VALUE - startTime)) * MILLISECONDS_IN_A_TICK;
-}
-
bool isDoubleClick(int selected)
{
- const Uint32 maximumDelay = 500;
- static Uint32 lastTime = 0;
+ static Timer timer;
static int lastSelected = -1;
- if (selected == lastSelected && lastTime + maximumDelay >= SDL_GetTicks())
- {
- lastTime = 0;
+ if (selected == lastSelected && !timer.passed())
return true;
- }
- lastTime = SDL_GetTicks();
+ timer.set(500);
lastSelected = selected;
return false;
}
@@ -419,9 +389,7 @@ Client::Client(const Options &options):
if (mState != STATE_ERROR)
mState = STATE_CHOOSE_SERVER;
- // Initialize logic and seconds counters
- tick_time = 0;
- mLogicCounterId = SDL_AddTimer(MILLISECONDS_IN_A_TICK, nextTick, nullptr);
+ // Initialize seconds counter
mSecondsCounterId = SDL_AddTimer(1000, nextSecond, nullptr);
listen(Event::ConfigChannel);
@@ -437,7 +405,6 @@ Client::Client(const Options &options):
Client::~Client()
{
- SDL_RemoveTimer(mLogicCounterId);
SDL_RemoveTimer(mSecondsCounterId);
// Unload XML databases
@@ -473,12 +440,18 @@ Client::~Client()
int Client::exec()
{
- int lastTickTime = tick_time;
+ Time::beginFrame(); // Prevent startup lag influencing the first frame
+
+ // Tick timer, used until logic has been updated to use Time::deltaTimeMs
+ Timer tickTimer;
+ tickTimer.set();
SDL_Event event;
while (mState != STATE_EXIT)
{
+ Time::beginFrame();
+
if (mGame)
{
// Let the game handle the events while it is active
@@ -512,19 +485,16 @@ int Client::exec()
if (Net::getGeneralHandler())
Net::getGeneralHandler()->flushNetwork();
- while (get_elapsed_time(lastTickTime) > 0)
+ while (tickTimer.passed())
{
gui->logic();
if (mGame)
mGame->logic();
- sound.logic();
-
- ++lastTickTime;
+ tickTimer.extend(MILLISECONDS_IN_A_TICK);
}
- // This is done because at some point tick_time will wrap.
- lastTickTime = tick_time;
+ sound.logic();
// Update the screen when application is active, delay otherwise.
if (isActive())
diff --git a/src/client.h b/src/client.h
index 49780a38..a2371a1c 100644
--- a/src/client.h
+++ b/src/client.h
@@ -40,35 +40,16 @@ class LoginData;
class Window;
class QuitDialog;
-/**
- * Set the milliseconds value of a tick time.
- */
-static const int MILLISECONDS_IN_A_TICK = 10;
-
//manaserv uses 9601
//static const short DEFAULT_PORT = 9601;
static const short DEFAULT_PORT = 6901;
extern volatile int fps;
-extern volatile int tick_time;
-extern volatile int cur_time;
extern std::string errorMessage;
extern LoginData loginData;
/**
- * @param startTime The value to check in client ticks.
- *
- * @return the elapsed time in milliseconds.
- * between startTime and the current client tick value.
- *
- * @warning This function can't handle delays > 100 seconds.
- * @see MILLISECONDS_IN_A_TICK
- * @see tick_time
- */
-int get_elapsed_time(int startTime);
-
-/**
* Returns whether this call and the last call were done for the same
* selected index and within a short time.
*/
@@ -256,7 +237,6 @@ private:
SDL_Surface *mIcon = nullptr;
- SDL_TimerID mLogicCounterId = 0;
SDL_TimerID mSecondsCounterId = 0;
int mFpsLimit = 0;
diff --git a/src/game.cpp b/src/game.cpp
index a9199817..13928036 100644
--- a/src/game.cpp
+++ b/src/game.cpp
@@ -343,8 +343,6 @@ void Game::logic()
if (mCurrentMap)
mCurrentMap->update();
- cur_time = time(nullptr);
-
// Handle network stuff
if (!Net::getGameHandler()->isConnected() && !mDisconnected)
{
diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp
index 26d0913e..2ff697c7 100644
--- a/src/gui/gui.cpp
+++ b/src/gui/gui.cpp
@@ -158,7 +158,7 @@ Gui::~Gui()
void Gui::logic()
{
// Hide mouse cursor after extended inactivity
- if (get_elapsed_time(mLastMouseActivityTime) > 15000)
+ if (mMouseActivityTimer.passed())
SDL_ShowCursor(SDL_DISABLE);
Palette::advanceGradients();
@@ -239,7 +239,7 @@ void Gui::updateCursor()
void Gui::handleMouseMoved(const gcn::MouseInput &mouseInput)
{
gcn::Gui::handleMouseMoved(mouseInput);
- mLastMouseActivityTime = tick_time;
+ mMouseActivityTimer.set(15000);
// Make sure the cursor is visible
SDL_ShowCursor(SDL_ENABLE);
diff --git a/src/gui/gui.h b/src/gui/gui.h
index 7c3c3afd..fd1dcf94 100644
--- a/src/gui/gui.h
+++ b/src/gui/gui.h
@@ -25,6 +25,8 @@
#include "eventlistener.h"
#include "guichanfwd.h"
+#include "utils/time.h"
+
#include <guichan/gui.hpp>
#include <SDL.h>
@@ -137,7 +139,7 @@ class Gui final : public gcn::Gui, public EventListener
float mCustomCursorScale = 1.0f;
std::vector<SDL_Cursor *> mSystemMouseCursors;
std::vector<SDL_Cursor *> mCustomMouseCursors;
- int mLastMouseActivityTime = 0;
+ Timer mMouseActivityTimer;
Cursor mCursorType = Cursor::POINTER;
};
diff --git a/src/gui/ministatuswindow.cpp b/src/gui/ministatuswindow.cpp
index d3e01bd2..e22172bc 100644
--- a/src/gui/ministatuswindow.cpp
+++ b/src/gui/ministatuswindow.cpp
@@ -43,8 +43,7 @@
#include "utils/gettext.h"
#include "utils/stringutils.h"
-
-extern volatile int tick_time;
+#include "utils/time.h"
MiniStatusWindow::MiniStatusWindow():
Popup("MiniStatus")
@@ -227,7 +226,7 @@ void MiniStatusWindow::logic()
for (auto &icon : mIcons)
if (icon)
- icon->update(tick_time * 10);
+ icon->update(Time::absoluteTimeMs());
}
void MiniStatusWindow::mouseMoved(gcn::MouseEvent &event)
diff --git a/src/gui/palette.cpp b/src/gui/palette.cpp
index aaa3165c..6c6e6f06 100644
--- a/src/gui/palette.cpp
+++ b/src/gui/palette.cpp
@@ -22,18 +22,11 @@
#include "palette.h"
-#include "configuration.h"
-#include "client.h"
-
-#include "gui/gui.h"
-
-#include "utils/gettext.h"
-#include "utils/stringutils.h"
-
#include <cmath>
-static const double PI = 3.14159265;
+static constexpr double PI = 3.14159265;
const gcn::Color Palette::BLACK = gcn::Color(0, 0, 0);
+Timer Palette::mRainbowTimer;
Palette::Palettes Palette::mInstances;
const gcn::Color Palette::RAINBOW_COLORS[7] = {
@@ -49,7 +42,6 @@ const gcn::Color Palette::RAINBOW_COLORS[7] = {
const int Palette::RAINBOW_COLOR_COUNT = 7;
Palette::Palette(int size) :
- mRainbowTime(tick_time),
mColors(size)
{
mInstances.insert(this);
@@ -76,97 +68,85 @@ const gcn::Color &Palette::getColor(char c, bool &valid)
void Palette::advanceGradients()
{
- auto it = mInstances.begin();
- auto it_end = mInstances.end();
+ const int advance = mRainbowTimer.elapsed() / 5;
+ if (advance <= 0)
+ return;
- for (; it != it_end; it++)
- {
- (*it)->advanceGradient();
- }
+ mRainbowTimer.extend(advance * 5);
+
+ for (auto palette : mInstances)
+ palette->advanceGradient(advance);
}
-void Palette::advanceGradient()
+void Palette::advanceGradient(int advance)
{
- if (get_elapsed_time(mRainbowTime) > 5)
+ for (auto elem : mGradVector)
{
- int pos, colIndex, colVal, delay, numOfColors;
- // For slower systems, advance can be greater than one (advance > 1
- // skips advance-1 steps). Should make gradient look the same
- // independent of the framerate.
- int advance = get_elapsed_time(mRainbowTime) / 5;
- double startColVal, destColVal;
-
- for (auto &elem : mGradVector)
- {
- delay = elem->delay;
+ int delay = elem->delay;
- if (elem->grad == PULSE)
- delay = delay / 20;
+ if (elem->grad == PULSE)
+ delay = delay / 20;
- numOfColors = (elem->grad == SPECTRUM ? 6 :
- elem->grad == PULSE ? 127 :
- RAINBOW_COLOR_COUNT);
+ const int numOfColors = (elem->grad == SPECTRUM ? 6 :
+ elem->grad == PULSE ? 127 :
+ RAINBOW_COLOR_COUNT);
- elem->gradientIndex =
- (elem->gradientIndex + advance) %
- (delay * numOfColors);
+ elem->gradientIndex = (elem->gradientIndex + advance) %
+ (delay * numOfColors);
- pos = elem->gradientIndex % delay;
- colIndex = elem->gradientIndex / delay;
+ const int pos = elem->gradientIndex % delay;
+ const int colIndex = elem->gradientIndex / delay;
- if (elem->grad == PULSE)
- {
- colVal = (int) (255.0 * sin(PI * colIndex / numOfColors));
+ if (elem->grad == PULSE)
+ {
+ const int colVal = (int) (255.0 * sin(PI * colIndex / numOfColors));
+ const gcn::Color &col = elem->testColor;
- const gcn::Color &col = elem->testColor;
+ elem->color.r = ((colVal * col.r) / 255) % (col.r + 1);
+ elem->color.g = ((colVal * col.g) / 255) % (col.g + 1);
+ elem->color.b = ((colVal * col.b) / 255) % (col.b + 1);
+ }
+ if (elem->grad == SPECTRUM)
+ {
+ int colVal;
- elem->color.r = ((colVal * col.r) / 255) % (col.r + 1);
- elem->color.g = ((colVal * col.g) / 255) % (col.g + 1);
- elem->color.b = ((colVal * col.b) / 255) % (col.b + 1);
+ if (colIndex % 2)
+ { // falling curve
+ colVal = (int)(255.0 * (cos(PI * pos / delay) + 1) / 2);
}
- if (elem->grad == SPECTRUM)
- {
- if (colIndex % 2)
- { // falling curve
- colVal = (int)(255.0 * (cos(PI * pos / delay) + 1) / 2);
- }
- else
- { // ascending curve
- colVal = (int)(255.0 * (cos(PI * (delay - pos) / delay) +
- 1) / 2);
- }
-
- elem->color.r =
- (colIndex == 0 || colIndex == 5) ? 255 :
- (colIndex == 1 || colIndex == 4) ? colVal : 0;
- elem->color.g =
- (colIndex == 1 || colIndex == 2) ? 255 :
- (colIndex == 0 || colIndex == 3) ? colVal : 0;
- elem->color.b =
- (colIndex == 3 || colIndex == 4) ? 255 :
- (colIndex == 2 || colIndex == 5) ? colVal : 0;
+ else
+ { // ascending curve
+ colVal = (int)(255.0 * (cos(PI * (delay - pos) / delay) +
+ 1) / 2);
}
- else if (elem->grad == RAINBOW)
- {
- const gcn::Color &startCol = RAINBOW_COLORS[colIndex];
- const gcn::Color &destCol =
- RAINBOW_COLORS[(colIndex + 1) % numOfColors];
- startColVal = (cos(PI * pos / delay) + 1) / 2;
- destColVal = 1 - startColVal;
+ elem->color.r =
+ (colIndex == 0 || colIndex == 5) ? 255 :
+ (colIndex == 1 || colIndex == 4) ? colVal : 0;
+ elem->color.g =
+ (colIndex == 1 || colIndex == 2) ? 255 :
+ (colIndex == 0 || colIndex == 3) ? colVal : 0;
+ elem->color.b =
+ (colIndex == 3 || colIndex == 4) ? 255 :
+ (colIndex == 2 || colIndex == 5) ? colVal : 0;
+ }
+ else if (elem->grad == RAINBOW)
+ {
+ const gcn::Color &startCol = RAINBOW_COLORS[colIndex];
+ const gcn::Color &destCol =
+ RAINBOW_COLORS[(colIndex + 1) % numOfColors];
- elem->color.r =(int)(startColVal * startCol.r +
- destColVal * destCol.r);
+ const double startColVal = (cos(PI * pos / delay) + 1) / 2;
+ const double destColVal = 1 - startColVal;
- elem->color.g =(int)(startColVal * startCol.g +
- destColVal * destCol.g);
+ elem->color.r =(int)(startColVal * startCol.r +
+ destColVal * destCol.r);
- elem->color.b =(int)(startColVal * startCol.b +
- destColVal * destCol.b);
- }
- }
+ elem->color.g =(int)(startColVal * startCol.g +
+ destColVal * destCol.g);
- if (advance)
- mRainbowTime = tick_time;
+ elem->color.b =(int)(startColVal * startCol.b +
+ destColVal * destCol.b);
+ }
}
}
diff --git a/src/gui/palette.h b/src/gui/palette.h
index 41378a43..b81d8865 100644
--- a/src/gui/palette.h
+++ b/src/gui/palette.h
@@ -23,6 +23,8 @@
#ifndef PALETTE_H
#define PALETTE_H
+#include "utils/time.h"
+
#include <guichan/color.hpp>
#include <cstdlib>
@@ -121,8 +123,8 @@ class Palette
static const gcn::Color RAINBOW_COLORS[];
static const int RAINBOW_COLOR_COUNT;
- /** Time tick, that gradient-type colors were updated the last time. */
- int mRainbowTime;
+ /** Timer used when updating gradient-type colors. */
+ static Timer mRainbowTimer;
using Palettes = std::set<Palette *>;
static Palettes mInstances;
@@ -131,7 +133,7 @@ class Palette
~Palette();
- void advanceGradient();
+ void advanceGradient(int advance);
struct ColorElem
{
@@ -147,7 +149,7 @@ class Palette
int delay;
int committedDelay;
- void set(int type, gcn::Color &color, GradientType grad, int delay)
+ void set(int type, const gcn::Color &color, GradientType grad, int delay)
{
ColorElem::type = type;
ColorElem::color = color;
diff --git a/src/gui/socialwindow.cpp b/src/gui/socialwindow.cpp
index 76235306..79a0fd53 100644
--- a/src/gui/socialwindow.cpp
+++ b/src/gui/socialwindow.cpp
@@ -262,7 +262,7 @@ private:
class PlayerListTab : public SocialTab
{
public:
- PlayerListTab()
+ PlayerListTab()
{
mPlayerList = new PlayerList;
@@ -679,10 +679,10 @@ void SocialWindow::setPlayersOnline(const std::vector<Avatar*> &players)
void SocialWindow::logic()
{
- if (mLastOnlineListUpdate == 0 || get_elapsed_time(mLastOnlineListUpdate) >= 18000)
+ if (mOnlineListUpdateTimer.passed())
{
Net::getChatHandler()->requestOnlineList();
- mLastOnlineListUpdate = tick_time;
+ mOnlineListUpdateTimer.set(18000);
}
Window::logic();
diff --git a/src/gui/socialwindow.h b/src/gui/socialwindow.h
index b6865fb2..350f919c 100644
--- a/src/gui/socialwindow.h
+++ b/src/gui/socialwindow.h
@@ -21,6 +21,8 @@
#ifndef SOCIALWINDOW_H
#define SOCIALWINDOW_H
+#include "utils/time.h"
+
#include "gui/widgets/window.h"
#include <guichan/actionevent.hpp>
@@ -87,7 +89,7 @@ protected:
void updateButtons();
int mGuildInvited = 0;
- int mLastOnlineListUpdate = 0;
+ Timer mOnlineListUpdateTimer;
ConfirmDialog *mGuildAcceptDialog = nullptr;
TextDialog *mGuildCreateDialog = nullptr;
diff --git a/src/gui/viewport.cpp b/src/gui/viewport.cpp
index cdc4076e..8dbdf7c7 100644
--- a/src/gui/viewport.cpp
+++ b/src/gui/viewport.cpp
@@ -79,8 +79,6 @@ void Viewport::setMap(Map *map)
void Viewport::draw(gcn::Graphics *gcnGraphics)
{
- static int lastTick = tick_time;
-
// Check whether map was successfully loaded since
// the rest of this function relies on it
if (!mMap || !local_player)
@@ -95,12 +93,6 @@ void Viewport::draw(gcn::Graphics *gcnGraphics)
auto *graphics = static_cast<Graphics*>(gcnGraphics);
- // Avoid freaking out when tick_time overflows
- if (tick_time < lastTick)
- {
- lastTick = tick_time;
- }
-
// Calculate viewpoint
int midTileX = (graphics->getWidth() + mScrollCenterOffsetX) / 2;
int midTileY = (graphics->getHeight() + mScrollCenterOffsetX) / 2;
@@ -109,49 +101,57 @@ void Viewport::draw(gcn::Graphics *gcnGraphics)
const int player_x = (int) playerPos.x - midTileX;
const int player_y = (int) playerPos.y - midTileY;
- if (mScrollLaziness < 1)
- mScrollLaziness = 1; // Avoids division by zero
+ const float ticks = Time::deltaTimeMs() / static_cast<float>(MILLISECONDS_IN_A_TICK);
+ float scrollFraction = 1.0f;
- while (lastTick < tick_time)
+ if (mScrollLaziness > 1)
{
- // Apply lazy scrolling
- if (player_x > mPixelViewX + mScrollRadius)
- {
- mPixelViewX += (player_x - mPixelViewX - mScrollRadius) /
- mScrollLaziness;
- }
- if (player_x < mPixelViewX - mScrollRadius)
- {
- mPixelViewX += (player_x - mPixelViewX + mScrollRadius) /
- mScrollLaziness;
- }
- if (player_y > mPixelViewY + mScrollRadius)
- {
- mPixelViewY += (player_y - mPixelViewY - mScrollRadius) /
- mScrollLaziness;
- }
- if (player_y < mPixelViewY - mScrollRadius)
- {
- mPixelViewY += (player_y - mPixelViewY + mScrollRadius) /
- mScrollLaziness;
- }
+ // mScrollLaziness defines the fraction of the desired camera movement
+ // that is applied every 10ms. To make this work independently of the
+ // frame duration, we calculate the actual scroll fraction based on the
+ // time delta.
+ scrollFraction = 1.0f - std::pow(1.0f - 1.0f / mScrollLaziness, ticks);
+ }
+
+ // Apply lazy scrolling
+ if (player_x > mPixelViewX + mScrollRadius)
+ {
+ mPixelViewX += (player_x - mPixelViewX - mScrollRadius) *
+ scrollFraction;
+ }
+ if (player_x < mPixelViewX - mScrollRadius)
+ {
+ mPixelViewX += (player_x - mPixelViewX + mScrollRadius) *
+ scrollFraction;
+ }
+ if (player_y > mPixelViewY + mScrollRadius)
+ {
+ mPixelViewY += (player_y - mPixelViewY - mScrollRadius) *
+ scrollFraction;
+ }
+ if (player_y < mPixelViewY - mScrollRadius)
+ {
+ mPixelViewY += (player_y - mPixelViewY + mScrollRadius) *
+ scrollFraction;
+ }
+
+ // manage shake effect
+ for (auto i = mShakeEffects.begin(); i != mShakeEffects.end(); i++)
+ {
+ // The decay defines the reduction in amplitude per 10ms. Here
+ // we calculate the reduction based on the ticks.
+ const float decay = std::pow(i->decay, ticks);
+
+ // apply the effect to viewport
+ mPixelViewX += i->x *= -decay;
+ mPixelViewY += i->y *= -decay;
- // manage shake effect
- for (auto i = mShakeEffects.begin();
- i != mShakeEffects.end();
- i++)
+ // check death conditions
+ if (std::abs(i->x) + std::abs(i->y) < 1.0f ||
+ (i->timer.isSet() && i->timer.passed()))
{
- // apply the effect to viewport
- mPixelViewX += i->x *= -i->decay;
- mPixelViewY += i->y *= -i->decay;
- // check death conditions
- if (abs(i->x) + abs(i->y) < 1.0f ||
- (i->duration > 0 && --i->duration == 0))
- {
- i = mShakeEffects.erase(i);
- }
+ i = mShakeEffects.erase(i);
}
- lastTick++;
}
// Auto center when player is off screen
@@ -264,7 +264,9 @@ void Viewport::shakeScreen(float x, float y, float decay, unsigned duration)
effect.x = x;
effect.y = y;
effect.decay = decay;
- effect.duration = duration;
+
+ if (duration > 0)
+ effect.timer.set(duration * MILLISECONDS_IN_A_TICK);
}
void Viewport::logic()
@@ -528,9 +530,9 @@ void Viewport::mouseDragged(gcn::MouseEvent &event)
if (mPlayerFollowMouse && !event.isShiftPressed())
{
- if (get_elapsed_time(mLocalWalkTime) >= walkingMouseDelay)
+ if (mLocalWalkTimer.passed())
{
- mLocalWalkTime = tick_time;
+ mLocalWalkTimer.set(walkingMouseDelay);
local_player->setDestination(event.getX() + (int) mPixelViewX,
event.getY() + (int) mPixelViewY);
local_player->pathSetByMouse();
diff --git a/src/gui/viewport.h b/src/gui/viewport.h
index 32d9bd5c..53edb50f 100644
--- a/src/gui/viewport.h
+++ b/src/gui/viewport.h
@@ -25,6 +25,8 @@
#include "eventlistener.h"
#include "position.h"
+#include "utils/time.h"
+
#include "gui/widgets/windowcontainer.h"
#include <guichan/mouselistener.hpp>
@@ -207,15 +209,15 @@ class Viewport : public WindowContainer, public gcn::MouseListener,
float x;
float y;
float decay;
- unsigned duration;
+ Timer timer;
};
std::list<ShakeEffect> mShakeEffects;
bool mPlayerFollowMouse = false;
- int mLocalWalkTime = -1; /**< Timestamp before the next walk can be sent. */
+ Timer mLocalWalkTimer; /**< Timer for sending walk messages. */
- PopupMenu *mPopupMenu; /**< Popup menu. */
+ PopupMenu *mPopupMenu; /**< Popup menu. */
Being *mHoverBeing = nullptr; /**< Being mouse is currently over. */
FloorItem *mHoverItem = nullptr; /**< FloorItem mouse is currently over. */
BeingPopup *mBeingPopup; /**< Being information popup. */
diff --git a/src/gui/widgets/browserbox.cpp b/src/gui/widgets/browserbox.cpp
index 40c9b66a..9eee9448 100644
--- a/src/gui/widgets/browserbox.cpp
+++ b/src/gui/widgets/browserbox.cpp
@@ -22,8 +22,6 @@
#include "gui/widgets/browserbox.h"
-#include "client.h"
-
#include "gui/gui.h"
#include "gui/truetypefont.h"
#include "gui/widgets/linkhandler.h"
@@ -32,6 +30,8 @@
#include "resources/iteminfo.h"
#include "resources/theme.h"
+#include "utils/stringutils.h"
+
#include <guichan/graphics.hpp>
#include <guichan/font.hpp>
#include <guichan/cliprectangle.hpp>
@@ -276,7 +276,7 @@ void BrowserBox::relayoutText()
layoutTextRow(row, context);
mLastLayoutWidth = getWidth();
- mLastLayoutTime = tick_time;
+ mLayoutTimer.set(100);
setHeight(context.y);
}
@@ -507,7 +507,7 @@ void BrowserBox::maybeRelayoutText()
{
// Reduce relayouting frequency when there is a lot of text
if (mTextRows.size() > 100)
- if (mLastLayoutTime && std::abs(mLastLayoutTime - tick_time) < 10)
+ if (!mLayoutTimer.passed())
return;
relayoutText();
diff --git a/src/gui/widgets/browserbox.h b/src/gui/widgets/browserbox.h
index d7ff0428..7278bb59 100644
--- a/src/gui/widgets/browserbox.h
+++ b/src/gui/widgets/browserbox.h
@@ -23,6 +23,8 @@
#ifndef BROWSERBOX_H
#define BROWSERBOX_H
+#include "utils/time.h"
+
#include <guichan/mouselistener.hpp>
#include <guichan/widget.hpp>
@@ -193,7 +195,7 @@ class BrowserBox : public gcn::Widget,
std::optional<BrowserLink> mHoveredLink;
unsigned int mMaxRows = 0;
int mLastLayoutWidth = 0;
- int mLastLayoutTime = -1;
+ Timer mLayoutTimer;
};
#endif
diff --git a/src/localplayer.cpp b/src/localplayer.cpp
index c6031e49..a7969fc3 100644
--- a/src/localplayer.cpp
+++ b/src/localplayer.cpp
@@ -21,7 +21,6 @@
#include "localplayer.h"
-#include "client.h"
#include "configuration.h"
#include "event.h"
#include "flooritem.h"
@@ -50,7 +49,10 @@
#include "utils/gettext.h"
#include "utils/stringutils.h"
-const int AWAY_LIMIT_TIMER = 60;
+constexpr unsigned AWAY_MESSAGE_TIMEOUT = 60 * 1000;
+
+// Actions are allowed at 5.5 per second
+constexpr unsigned ACTION_TIMEOUT = 182;
LocalPlayer *local_player = nullptr;
@@ -75,10 +77,6 @@ LocalPlayer::~LocalPlayer()
void LocalPlayer::logic()
{
- // Actions are allowed at 5.5 per second
- if (get_elapsed_time(mLastActionTime) >= 182)
- mLastActionTime = -1;
-
// Show XP messages
if (!mMessages.empty())
{
@@ -101,10 +99,6 @@ void LocalPlayer::logic()
PlayerInfo::logic();
- // Targeting allowed 4 times a second
- if (get_elapsed_time(mLastTargetTime) >= 250)
- mLastTargetTime = -1;
-
if (mTarget)
{
ActorSprite::Type targetType = mTarget->getType();
@@ -173,7 +167,7 @@ void LocalPlayer::setAction(Action action, int attackId)
{
if (action == DEAD)
{
- mLastTargetTime = -1;
+ mLastTargetTimer.reset();
setTarget(nullptr);
}
@@ -625,24 +619,18 @@ Being *LocalPlayer::getTarget() const
void LocalPlayer::setTarget(Being *target)
{
- if ((mLastTargetTime != -1 || target == this) && target)
+ if ((!mLastTargetTimer.passed() || target == this) && target)
return;
+ // Targeting allowed 4 times a second
if (target)
- mLastTargetTime = tick_time;
+ mLastTargetTimer.set(250);
if (target == mTarget)
return;
- if (target || mAction == ATTACK)
- {
- mTargetTime = tick_time;
- }
- else
- {
+ if (!target && mAction != ATTACK)
mKeepAttacking = false;
- mTargetTime = -1;
- }
Being *oldTarget = nullptr;
if (mTarget)
@@ -793,9 +781,9 @@ void LocalPlayer::stopWalking(bool sendToServer)
void LocalPlayer::toggleSit()
{
- if (mLastActionTime != -1)
+ if (!mLastActionTimer.passed())
return;
- mLastActionTime = tick_time;
+ mLastActionTimer.set(ACTION_TIMEOUT);
Being::Action newAction;
switch (mAction)
@@ -810,16 +798,16 @@ void LocalPlayer::toggleSit()
void LocalPlayer::emote(int emoteId)
{
- if (mLastActionTime != -1)
+ if (!mLastActionTimer.passed())
return;
- mLastActionTime = tick_time;
+ mLastActionTimer.set(ACTION_TIMEOUT);
Net::getPlayerHandler()->emote(emoteId);
}
void LocalPlayer::attack(Being *target, bool keep)
{
- if (mLastActionTime != -1)
+ if (!mLastActionTimer.passed())
return;
// Can only attack when standing still
@@ -830,23 +818,23 @@ void LocalPlayer::attack(Being *target, bool keep)
return;
// Can't attack more times than its attack speed
- static int lastAttackTime = 0;
- if (get_elapsed_time(lastAttackTime) < mAttackSpeed)
+ static Timer lastAttackTimer;
+ if (!lastAttackTimer.passed())
return;
- lastAttackTime = tick_time;
+ lastAttackTimer.set(mAttackSpeed);
mKeepAttacking = keep;
if (mTarget != target || !mTarget)
{
- mLastTargetTime = -1;
+ mLastTargetTimer.reset();
setTarget(target);
}
lookAt(mTarget->getPosition());
- mLastActionTime = tick_time;
+ mLastActionTimer.set(ACTION_TIMEOUT);
setAction(ATTACK);
@@ -872,7 +860,7 @@ void LocalPlayer::stopAttack()
setAction(STAND);
setTarget(nullptr);
}
- mLastTargetTime = -1;
+ mLastTargetTimer.reset();
cancelGoToTarget();
}
@@ -964,7 +952,7 @@ void LocalPlayer::setGotoTarget(Being *target)
if (!target)
return;
- mLastTargetTime = -1;
+ mLastTargetTimer.reset();
setTarget(target);
mGoingToTarget = true;
@@ -1039,7 +1027,8 @@ void LocalPlayer::event(Event::Channel channel, const Event &event)
void LocalPlayer::changeAwayMode()
{
mAwayMode = !mAwayMode;
- mAfkTime = 0;
+ mAfkTimer.reset();
+
if (mAwayMode)
{
mAwayDialog = new OkDialog(_("Away"),
@@ -1061,9 +1050,7 @@ void LocalPlayer::afkRespond(ChatTab *tab, const std::string &nick)
{
if (mAwayMode)
{
- if (mAfkTime == 0
- || cur_time < mAfkTime
- || cur_time - mAfkTime > AWAY_LIMIT_TIMER)
+ if (mAfkTimer.passed())
{
std::string msg = "*AFK*: "
+ config.getValue("afkMessage", "I am away from keyboard");
@@ -1078,7 +1065,8 @@ void LocalPlayer::afkRespond(ChatTab *tab, const std::string &nick)
{
tab->chatLog(getName(), msg);
}
- mAfkTime = cur_time;
+
+ mAfkTimer.set(AWAY_MESSAGE_TIMEOUT);
}
}
}
diff --git a/src/localplayer.h b/src/localplayer.h
index 4e3bbc88..9d844339 100644
--- a/src/localplayer.h
+++ b/src/localplayer.h
@@ -26,6 +26,8 @@
#include "resources/userpalette.h"
+#include "utils/time.h"
+
#include <guichan/actionlistener.hpp>
class ChatTab;
@@ -217,9 +219,7 @@ class LocalPlayer final : public Being
int mAttackRange = -1;
- int mTargetTime = -1; /**< How long the being has been targeted **/
- /** Time stamp of last targeting action, -1 if none. */
- int mLastTargetTime = -1;
+ Timer mLastTargetTimer; /**< Timer for last targeting action. */
int mGMLevel = 0;
@@ -229,7 +229,7 @@ class LocalPlayer final : public Being
bool mGoingToTarget = false;
bool mKeepAttacking = false; /**< Whether or not to continue to attack */
- int mLastActionTime = -1; /**< Time stamp of the last action, -1 if none. */
+ Timer mLastActionTimer; /**< Timeout for the last action. */
int mWalkingDir = 0; /**< The direction the player is walking in. */
bool mPathSetByMouse = false; /**< Tells if the path was set using mouse */
@@ -241,7 +241,7 @@ class LocalPlayer final : public Being
AwayListener *mAwayListener;
OkDialog *mAwayDialog = nullptr;
- int mAfkTime = 0;
+ Timer mAfkTimer;
bool mAwayMode = false;
};
diff --git a/src/map.cpp b/src/map.cpp
index e9d29b61..c4e4c3be 100644
--- a/src/map.cpp
+++ b/src/map.cpp
@@ -22,7 +22,6 @@
#include "map.h"
#include "actorspritemanager.h"
-#include "client.h"
#include "configuration.h"
#include "graphics.h"
#include "log.h"
@@ -34,6 +33,8 @@
#include "resources/image.h"
#include "resources/resourcemanager.h"
+#include "utils/time.h"
+
#include "net/net.h"
#include "utils/dtor.h"
@@ -247,8 +248,8 @@ void Map::initializeAmbientLayers()
{
auto &ambientLayer = list.emplace_back(img);
ambientLayer.mParallax = getFloatProperty(name + "parallax");
- ambientLayer.mSpeedX = getFloatProperty(name + "scrollX");
- ambientLayer.mSpeedY = getFloatProperty(name + "scrollY");
+ ambientLayer.mSpeedX = getFloatProperty(name + "scrollX") / MILLISECONDS_IN_A_TICK;
+ ambientLayer.mSpeedY = getFloatProperty(name + "scrollY") / MILLISECONDS_IN_A_TICK;
ambientLayer.mMask = getIntProperty(name + "mask", 1);
ambientLayer.mKeepRatio = getBoolProperty(name + "keepratio");
}
@@ -433,8 +434,6 @@ void Map::drawCollision(Graphics *graphics, int scrollX, int scrollY,
void Map::updateAmbientLayers(float scrollX, float scrollY)
{
- static int lastTick = tick_time; // static = only initialized at first call
-
if (mLastScrollX == 0.0f && mLastScrollY == 0.0f)
{
// First call - initialisation
@@ -445,25 +444,23 @@ void Map::updateAmbientLayers(float scrollX, float scrollY)
// Update Overlays
float dx = scrollX - mLastScrollX;
float dy = scrollY - mLastScrollY;
- int timePassed = get_elapsed_time(lastTick);
for (auto &background : mBackgrounds)
{
if ((background.mMask & mMask) == 0)
continue;
- background.update(timePassed, dx, dy);
+ background.update(Time::deltaTimeMs(), dx, dy);
}
for (auto &foreground : mForegrounds)
{
if ((foreground.mMask & mMask) == 0)
continue;
- foreground.update(timePassed, dx, dy);
+ foreground.update(Time::deltaTimeMs(), dx, dy);
}
mLastScrollX = scrollX;
mLastScrollY = scrollY;
- lastTick = tick_time;
}
void Map::drawAmbientLayers(Graphics *graphics, LayerType type,
diff --git a/src/net/tmwa/playerhandler.cpp b/src/net/tmwa/playerhandler.cpp
index 431137fd..c9baf27b 100644
--- a/src/net/tmwa/playerhandler.cpp
+++ b/src/net/tmwa/playerhandler.cpp
@@ -40,8 +40,9 @@
#include "net/tmwa/messageout.h"
#include "net/tmwa/protocol.h"
-#include "utils/stringutils.h"
#include "utils/gettext.h"
+#include "utils/stringutils.h"
+#include "utils/time.h"
extern OkDialog *weightNotice;
extern OkDialog *deathNotice;
@@ -563,17 +564,17 @@ void PlayerHandler::increaseSkill(int skillId)
void PlayerHandler::pickUp(FloorItem *floorItem)
{
- static Uint32 lastTime = 0;
+ static Timer lastPickupTimer;
// Avoid spamming the server with pick-up requests to prevent the player
// from being kicked.
- if (!floorItem || SDL_GetTicks() < lastTime + 100)
+ if (!floorItem || !lastPickupTimer.passed())
return;
MessageOut outMsg(CMSG_ITEM_PICKUP);
outMsg.writeInt32(floorItem->getId());
- lastTime = SDL_GetTicks();
+ lastPickupTimer.set(100);
}
void PlayerHandler::setDirection(char direction)
diff --git a/src/resources/ambientlayer.cpp b/src/resources/ambientlayer.cpp
index b292070b..a238afcc 100644
--- a/src/resources/ambientlayer.cpp
+++ b/src/resources/ambientlayer.cpp
@@ -34,8 +34,8 @@ AmbientLayer::~AmbientLayer() = default;
void AmbientLayer::update(int timePassed, float dx, float dy)
{
// Self scrolling of the overlay
- mPosX -= mSpeedX * timePassed / 10;
- mPosY -= mSpeedY * timePassed / 10;
+ mPosX -= mSpeedX * timePassed;
+ mPosY -= mSpeedY * timePassed;
// Parallax scrolling
mPosX += dx * mParallax;
diff --git a/src/resources/userpalette.h b/src/resources/userpalette.h
index 93056867..6eba44b2 100644
--- a/src/resources/userpalette.h
+++ b/src/resources/userpalette.h
@@ -90,7 +90,7 @@ class UserPalette : public Palette, public gcn::ListModel
* @param type the color type requested
* @param color the color that should be tested
*/
- void setTestColor(int type, gcn::Color color)
+ void setTestColor(int type, const gcn::Color &color)
{
mColors[type].testColor = color;
}
diff --git a/src/utils/time.cpp b/src/utils/time.cpp
new file mode 100644
index 00000000..c89914fa
--- /dev/null
+++ b/src/utils/time.cpp
@@ -0,0 +1,63 @@
+/*
+ * The Mana Client
+ * Copyright (C) 2004-2009 The Mana World Development Team
+ * Copyright (C) 2009-2012 The Mana Developers
+ *
+ * This file is part of The Mana Client.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "time.h"
+
+#include <algorithm>
+
+#include <SDL.h>
+
+namespace Time
+{
+
+static uint32_t s_absoluteTimeMs;
+static unsigned s_deltaTimeMs;
+
+uint32_t absoluteTimeMs()
+{
+ return s_absoluteTimeMs;
+}
+
+unsigned deltaTimeMs()
+{
+ return s_deltaTimeMs;
+}
+
+static int32_t getElapsedTime(uint32_t timeMs)
+{
+ return static_cast<int32_t>(s_absoluteTimeMs - timeMs);
+}
+
+void beginFrame()
+{
+ // todo: Use SDL_GetTicks64 once we require SDL 2.0.18 or newer.
+
+ const uint32_t previousTime = s_absoluteTimeMs;
+ s_absoluteTimeMs = SDL_GetTicks();
+ s_deltaTimeMs = std::clamp(getElapsedTime(previousTime), 0, 1000);
+}
+
+} // namespace Time
+
+int32_t Timer::elapsed() const
+{
+ return Time::getElapsedTime(mTimeout);
+}
diff --git a/src/utils/time.h b/src/utils/time.h
new file mode 100644
index 00000000..ac7cd351
--- /dev/null
+++ b/src/utils/time.h
@@ -0,0 +1,106 @@
+/*
+ * The Mana Client
+ * Copyright (C) 2004-2009 The Mana World Development Team
+ * Copyright (C) 2009-2012 The Mana Developers
+ *
+ * This file is part of The Mana Client.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+ #ifndef UTILS_TIME_H
+ #define UTILS_TIME_H
+
+#include <cstdint>
+
+/**
+ * The amount of milliseconds in a logic tick.
+ */
+static constexpr unsigned MILLISECONDS_IN_A_TICK = 10;
+
+namespace Time
+{
+
+/**
+ * The time in milliseconds since the program started (wraps around after ~49
+ * days).
+ */
+uint32_t absoluteTimeMs();
+
+/**
+ * The time in milliseconds since the last frame, maximized to 1000ms.
+ */
+unsigned deltaTimeMs();
+
+/**
+ * Called at the start of each frame, updates the above variables.
+ */
+void beginFrame();
+
+} // namespace Time
+
+/**
+ * Simple timer that can be used to check if a certain amount of time
+ * has passed.
+ */
+class Timer
+{
+public:
+ Timer() = default;
+
+ /**
+ * Sets the timer with an optional duration in milliseconds.
+ */
+ void set(uint32_t ms = 0)
+ { mTimeout = Time::absoluteTimeMs() + ms; }
+
+ /**
+ * Reset the timer.
+ */
+ void reset()
+ { mTimeout = 0; }
+
+ /**
+ * Returns whether the timer has been set.
+ */
+ bool isSet() const
+ { return mTimeout != 0; }
+
+ /**
+ * Returns whether the timer has passed.
+ * A timer that wasn't set will always return true.
+ */
+ bool passed() const
+ { return elapsed() >= 0; }
+
+ /**
+ * Extend the timer by the given number of milliseconds.
+ */
+ void extend(uint32_t ms)
+ { mTimeout += ms; }
+
+ /**
+ * Returns the number of milliseconds elapsed since the set time, or a
+ * negative value if the timer hasn't passed.
+ *
+ * Due to wrapping, this function is not suitable for measuring long
+ * periods of time (tens of days).
+ */
+ int32_t elapsed() const;
+
+private:
+ uint32_t mTimeout = 0;
+};
+
+#endif // UTILS_TIME_H