diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e6919f8e71..2af0f95b2e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -24,6 +24,7 @@ add_subdirectory(sound) add_subdirectory(spriteloader) add_subdirectory(table) add_subdirectory(tests) +add_subdirectory(timer) add_subdirectory(video) add_subdirectory(widgets) diff --git a/src/date.cpp b/src/date.cpp index 198dd89585..9b51a4b01a 100644 --- a/src/date.cpp +++ b/src/date.cpp @@ -20,6 +20,8 @@ #include "saveload/saveload.h" #include "newgrf_profiling.h" #include "widgets/statusbar_widget.h" +#include "timer/timer.h" +#include "timer/timer_game_calendar.h" #include "safeguards.h" @@ -189,7 +191,7 @@ static const Month _autosave_months[] = { /** * Runs various procedures that have to be done yearly */ -static void OnNewYear() +static IntervalTimer _on_new_year({TimerGameCalendar::YEAR, TimerGameCalendar::Priority::NONE}, [](auto) { CompaniesYearlyLoop(); VehiclesYearlyLoop(); @@ -222,12 +224,12 @@ static void OnNewYear() } if (_settings_client.gui.auto_euro) CheckSwitchToEuro(); -} +}); /** * Runs various procedures that have to be done monthly */ -static void OnNewMonth() +static IntervalTimer _on_new_month({TimerGameCalendar::MONTH, TimerGameCalendar::Priority::NONE}, [](auto) { if (_settings_client.gui.autosave != 0 && (_cur_month % _autosave_months[_settings_client.gui.autosave]) == 0) { _do_autosave = true; @@ -242,12 +244,12 @@ static void OnNewMonth() SubsidyMonthlyLoop(); StationMonthlyLoop(); if (_network_server) NetworkServerMonthlyLoop(); -} +}); /** * Runs various procedures that have to be done daily */ -static void OnNewDay() +static IntervalTimer _on_new_day({TimerGameCalendar::DAY, TimerGameCalendar::Priority::NONE}, [](auto) { if (!_newgrf_profilers.empty() && _newgrf_profile_end_date <= _date) { NewGRFProfiler::FinishAll(); @@ -263,45 +265,4 @@ static void OnNewDay() /* Refresh after possible snowline change */ SetWindowClassesDirty(WC_TOWN_VIEW); -} - -/** - * Increases the tick counter, increases date and possibly calls - * procedures that have to be called daily, monthly or yearly. - */ -void IncreaseDate() -{ - /* increase day, and check if a new day is there? */ - _tick_counter++; - - if (_game_mode == GM_MENU) return; - - _date_fract++; - if (_date_fract < DAY_TICKS) return; - _date_fract = 0; - - /* increase day counter */ - _date++; - - YearMonthDay ymd; - ConvertDateToYMD(_date, &ymd); - - /* check if we entered a new month? */ - bool new_month = ymd.month != _cur_month; - - /* check if we entered a new year? */ - bool new_year = ymd.year != _cur_year; - - /* update internal variables before calling the daily/monthly/yearly loops */ - _cur_month = ymd.month; - _cur_year = ymd.year; - - /* yes, call various daily loops */ - OnNewDay(); - - /* yes, call various monthly loops */ - if (new_month) OnNewMonth(); - - /* yes, call various yearly loops */ - if (new_year) OnNewYear(); -} +}); diff --git a/src/openttd.cpp b/src/openttd.cpp index 4a7806384a..f719a4dcd5 100644 --- a/src/openttd.cpp +++ b/src/openttd.cpp @@ -68,6 +68,8 @@ #include "industry.h" #include "network/network_gui.h" #include "misc_cmd.h" +#include "timer/timer.h" +#include "timer/timer_game_calendar.h" #include "linkgraph/linkgraphschedule.h" @@ -82,7 +84,6 @@ #endif void CallLandscapeTick(); -void IncreaseDate(); void DoPaletteAnimations(); void MusicLoop(); void ResetMusic(); @@ -1408,7 +1409,7 @@ void StateGameLoop() BasePersistentStorageArray::SwitchMode(PSM_ENTER_GAMELOOP); AnimateAnimatedTiles(); - IncreaseDate(); + TimerManager::Elapsed(1); RunTileLoop(); CallVehicleTicks(); CallLandscapeTick(); diff --git a/src/stdafx.h b/src/stdafx.h index f62a83e83c..d7da1ad8fc 100644 --- a/src/stdafx.h +++ b/src/stdafx.h @@ -155,6 +155,13 @@ # define NOACCESS(args) #endif +/* [[nodiscard]] on constructors doesn't work in GCC older than 10.1. */ +#if __GNUC__ < 10 || (__GNUC__ == 10 && __GNUC_MINOR__ < 1) +# define NODISCARD +#else +# define NODISCARD [[nodiscard]] +#endif + #if defined(__WATCOMC__) # define NORETURN # define CDECL diff --git a/src/timer/CMakeLists.txt b/src/timer/CMakeLists.txt new file mode 100644 index 0000000000..de9e01636f --- /dev/null +++ b/src/timer/CMakeLists.txt @@ -0,0 +1,8 @@ +add_files( + timer_game_calendar.cpp + timer_game_calendar.h + timer_window.cpp + timer_window.h + timer_manager.h + timer.h +) diff --git a/src/timer/timer.h b/src/timer/timer.h new file mode 100644 index 0000000000..3c45df55a9 --- /dev/null +++ b/src/timer/timer.h @@ -0,0 +1,187 @@ +/* + * This file is part of OpenTTD. + * OpenTTD 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, version 2. + * OpenTTD 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 OpenTTD. If not, see . + */ + +/** @file timer.h Definition of Interval and OneShot timers */ + +#ifndef TIMER_H +#define TIMER_H + +#include "timer_manager.h" + +#include + +/** + * The base where every other type of timer is derived from. + * + * Never use this class directly yourself. + */ +template +class BaseTimer { +public: + using TPeriod = typename TTimerType::TPeriod; + using TElapsed = typename TTimerType::TElapsed; + using TStorage = typename TTimerType::TStorage; + + /** + * Create a new timer. + * + * @param period The period of the timer. + */ + NODISCARD BaseTimer(const TPeriod period) : + period(period) + { + TimerManager::RegisterTimer(*this); + } + + /** + * Delete the timer. + */ + virtual ~BaseTimer() + { + TimerManager::UnregisterTimer(*this); + } + + /* Although these variables are public, they are only public to make saveload easier; not for common use. */ + + TPeriod period; ///< The period of the timer. + TStorage storage = {}; ///< The storage of the timer. + +protected: + /** + * Called by the timer manager to notify the timer that the given amount of time has elapsed. + * + * @param delta Depending on the time type, this is either in milliseconds or in ticks. + */ + virtual void Elapsed(TElapsed delta) = 0; + + /* To ensure only TimerManager can access Elapsed. */ + friend class TimerManager; +}; + +/** + * An interval timer will fire every interval, and will continue to fire until it is deleted. + * + * The callback receives how many times the timer has fired since the last time it fired. + * It will always try to fire every interval, but in times of severe stress it might be late. + * + * Each Timer-type needs to implement the Elapsed() method, and call the callback if needed. + * + * Setting the period to zero disables the interval. It can be reenabled at any time by + * calling SetInterval() with a non-zero period. + */ +template +class IntervalTimer : public BaseTimer { +public: + using TPeriod = typename TTimerType::TPeriod; + using TElapsed = typename TTimerType::TElapsed; + + /** + * Create a new interval timer. + * + * @param interval The interval between each callback. + * @param callback The callback to call when the interval has passed. + */ + NODISCARD IntervalTimer(const TPeriod interval, std::function callback) : + BaseTimer(interval), + callback(callback) + { + } + + /** + * Set a new interval for the timer. + * + * @param interval The interval between each callback. + * @param reset Whether to reset the timer to zero. + */ + void SetInterval(const TPeriod interval, bool reset = true) + { + this->period = interval; + if (reset) this->storage = {}; + } + +private: + std::function callback; + + void Elapsed(TElapsed count) override; +}; + +/** + * A timeout timer will fire once after the interval. You can reset it to fire again. + * The timer will never fire before the interval has passed, but in times of severe stress it might be late. + */ +template +class TimeoutTimer : public BaseTimer { +public: + using TPeriod = typename TTimerType::TPeriod; + using TElapsed = typename TTimerType::TElapsed; + + /** + * Create a new timeout timer. + * + * By default the timeout starts aborted; you will have to call Reset() before it starts. + * + * @param timeout The timeout after which the timer will fire. + * @param callback The callback to call when the timeout has passed. + * @param start Whether to start the timer immediately. If false, you can call Reset() to start it. + */ + NODISCARD TimeoutTimer(const TPeriod timeout, std::function callback, bool start = false) : + BaseTimer(timeout), + fired(!start), + callback(callback) + { + } + + /** + * Reset the timer, so it will fire again after the timeout. + */ + void Reset() + { + this->fired = false; + this->storage = {}; + } + + /** + * Reset the timer, so it will fire again after the timeout. + * + * @param timeout Set a new timeout for the next trigger. + */ + void Reset(const TPeriod timeout) + { + this->period = timeout; + this->fired = false; + this->storage = {}; + } + + /** + * Abort the timer so it doesn't fire if it hasn't yet. + */ + void Abort() + { + this->fired = true; + } + + /** + * Check whether the timeout occurred. + * + * @return True iff the timeout occurred. + */ + bool HasFired() const + { + return this->fired; + } + + /* Although these variables are public, they are only public to make saveload easier; not for common use. */ + + bool fired; ///< Whether the timeout has occurred. + +private: + std::function callback; + + void Elapsed(TElapsed count) override; +}; + +#endif /* TIMER_H */ diff --git a/src/timer/timer_game_calendar.cpp b/src/timer/timer_game_calendar.cpp new file mode 100644 index 0000000000..6ab290eb13 --- /dev/null +++ b/src/timer/timer_game_calendar.cpp @@ -0,0 +1,89 @@ +/* + * This file is part of OpenTTD. + * OpenTTD 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, version 2. + * OpenTTD 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 OpenTTD. If not, see . + */ + +/** + * @file timer_game_calendar.cpp + * This file implements the timer logic for the game-calendar-timer. + */ + +#include "stdafx.h" +#include "date_func.h" +#include "openttd.h" +#include "timer.h" +#include "timer_game_calendar.h" +#include "vehicle_base.h" +#include "linkgraph/linkgraph.h" + +#include "safeguards.h" + +template<> +void IntervalTimer::Elapsed(TimerGameCalendar::TElapsed trigger) +{ + if (trigger == this->period.trigger) { + this->callback(1); + } +} + +template<> +void TimeoutTimer::Elapsed(TimerGameCalendar::TElapsed trigger) +{ + if (this->fired) return; + + if (trigger == this->period.trigger) { + this->callback(); + this->fired = true; + } +} + +template<> +void TimerManager::Elapsed(TimerGameCalendar::TElapsed delta) +{ + assert(delta == 1); + + _tick_counter++; + + if (_game_mode == GM_MENU) return; + + _date_fract++; + if (_date_fract < DAY_TICKS) return; + _date_fract = 0; + + /* increase day counter */ + _date++; + + YearMonthDay ymd; + ConvertDateToYMD(_date, &ymd); + + /* check if we entered a new month? */ + bool new_month = ymd.month != _cur_month; + + /* check if we entered a new year? */ + bool new_year = ymd.year != _cur_year; + + /* update internal variables before calling the daily/monthly/yearly loops */ + _cur_month = ymd.month; + _cur_year = ymd.year; + + /* Make a temporary copy of the timers, as a timer's callback might add/remove other timers. */ + auto timers = TimerManager::GetTimers(); + + for (auto timer : timers) { + timer->Elapsed(TimerGameCalendar::DAY); + } + + if (new_month) { + for (auto timer : timers) { + timer->Elapsed(TimerGameCalendar::MONTH); + } + } + + if (new_year) { + for (auto timer : timers) { + timer->Elapsed(TimerGameCalendar::YEAR); + } + } +} diff --git a/src/timer/timer_game_calendar.h b/src/timer/timer_game_calendar.h new file mode 100644 index 0000000000..f7dd3dd499 --- /dev/null +++ b/src/timer/timer_game_calendar.h @@ -0,0 +1,76 @@ +/* + * This file is part of OpenTTD. + * OpenTTD 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, version 2. + * OpenTTD 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 OpenTTD. If not, see . + */ + +/** @file timer_game_calendar.h Definition of the game-calendar-timer */ + +#ifndef TIMER_GAME_CALENDAR_H +#define TIMER_GAME_CALENDAR_H + +/** + * Timer that is increased every 27ms, and counts towards ticks / days / months / years. + * + * The amount of days in a month depends on the month and year (leap-years). + * There are always 74 ticks in a day (and with 27ms, this makes 1 day 1.998 seconds). + * + * IntervalTimer and TimeoutTimer based on this Timer are a bit unusual, as their count is always one. + * You create those timers based on a transition: a new day, a new month or a new year. + * + * Additionally, you need to set a priority. To ensure deterministic behaviour, events are executed + * in priority. It is important that if you assign NONE, you do not use Random() in your callback. + * Other than that, make sure you only set one callback per priority. + * + * For example: + * IntervalTimer({TimerGameCalendar::DAY, TimerGameCalendar::Priority::NONE}, [](uint count){}); + * + */ +class TimerGameCalendar { +public: + enum Trigger { + DAY, + MONTH, + YEAR, + }; + enum Priority { + NONE, ///< These timers can be executed in any order; there is no Random() in them, so order is not relevant. + + /* All other may have a Random() call in them, so order is important. + * For safety, you can only setup a single timer on a single priority. */ + AUTOSAVE, + COMPANY, + DISASTER, + ENGINE, + INDUSTRY, + STATION, + SUBSIDY, + TOWN, + VEHICLE, + }; + + struct TPeriod { + Trigger trigger; + Priority priority; + + TPeriod(Trigger trigger, Priority priority) : trigger(trigger), priority(priority) {} + + bool operator < (const TPeriod &other) const + { + if (this->trigger != other.trigger) return this->trigger < other.trigger; + return this->priority < other.priority; + } + + bool operator == (const TPeriod &other) const + { + return this->trigger == other.trigger && this->priority == other.priority; + } + }; + + using TElapsed = uint; + struct TStorage { + }; +}; + +#endif /* TIMER_GAME_CALENDAR_H */ diff --git a/src/timer/timer_manager.h b/src/timer/timer_manager.h new file mode 100644 index 0000000000..2efe583ad9 --- /dev/null +++ b/src/timer/timer_manager.h @@ -0,0 +1,86 @@ +/* + * This file is part of OpenTTD. + * OpenTTD 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, version 2. + * OpenTTD 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 OpenTTD. If not, see . + */ + +/** @file timer_manager.h Definition of the TimerManager */ +/** @note don't include this file; include "timer.h". */ + +#ifndef TIMER_MANAGER_H +#define TIMER_MANAGER_H + +#include "stdafx.h" + +#include + +template +class BaseTimer; + +/** + * The TimerManager manages a single Timer-type. + * + * It allows for automatic registration and unregistration of timers like Interval and OneShot. + * + * Each Timer-type needs to implement the Elapsed() method, and distribute that to the timers if needed. + */ +template +class TimerManager { +public: + using TElapsed = typename TTimerType::TElapsed; + + /* Avoid copying this object; it is a singleton object. */ + TimerManager(TimerManager const &) = delete; + TimerManager &operator=(TimerManager const &) = delete; + + /** + * Register a timer. + * + * @param timer The timer to register. + */ + static void RegisterTimer(BaseTimer &timer) { + GetTimers().insert(&timer); + } + + /** + * Unregister a timer. + * + * @param timer The timer to unregister. + */ + static void UnregisterTimer(BaseTimer &timer) { + GetTimers().erase(&timer); + } + + /** + * Called when time for this timer elapsed. + * + * The implementation per type is different, but they all share a similar goal: + * Call the Elapsed() method of all active timers. + * + * @param value The amount of time that has elapsed. + */ + static void Elapsed(TElapsed value); + +private: + /** + * Sorter for timers. + * + * It will sort based on the period, smaller first. If the period is the + * same, it will sort based on the pointer value. + */ + struct base_timer_sorter { + bool operator() (BaseTimer *a, BaseTimer *b) const { + if (a->period == b->period) return a < b; + return a->period < b->period; + } + }; + + /** Singleton list, to store all the active timers. */ + static std::set *, base_timer_sorter> &GetTimers() { + static std::set *, base_timer_sorter> timers; + return timers; + } +}; + +#endif /* TIMER_MANAGER_H */ diff --git a/src/timer/timer_window.cpp b/src/timer/timer_window.cpp new file mode 100644 index 0000000000..dff0b45b8a --- /dev/null +++ b/src/timer/timer_window.cpp @@ -0,0 +1,60 @@ +/* + * This file is part of OpenTTD. + * OpenTTD 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, version 2. + * OpenTTD 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 OpenTTD. If not, see . + */ + +/** + * @file timer_window.cpp + * This file implements the timer logic for the Window system. + */ + +#include "stdafx.h" +#include "timer.h" +#include "timer_window.h" + +#include "safeguards.h" + +template<> +void IntervalTimer::Elapsed(TimerWindow::TElapsed delta) +{ + if (this->period == std::chrono::milliseconds::zero()) return; + + this->storage.elapsed += delta; + + uint count = 0; + while (this->storage.elapsed >= this->period) { + this->storage.elapsed -= this->period; + count++; + } + + if (count > 0) { + this->callback(count); + } +} + +template<> +void TimeoutTimer::Elapsed(TimerWindow::TElapsed delta) +{ + if (this->fired) return; + if (this->period == std::chrono::milliseconds::zero()) return; + + this->storage.elapsed += delta; + + if (this->storage.elapsed >= this->period) { + this->callback(); + this->fired = true; + } +} + +template<> +void TimerManager::Elapsed(TimerWindow::TElapsed delta) +{ + /* Make a temporary copy of the timers, as a timer's callback might add/remove other timers. */ + auto timers = TimerManager::GetTimers(); + + for (auto timer : timers) { + timer->Elapsed(delta); + } +} diff --git a/src/timer/timer_window.h b/src/timer/timer_window.h new file mode 100644 index 0000000000..b32048b4c4 --- /dev/null +++ b/src/timer/timer_window.h @@ -0,0 +1,35 @@ +/* + * This file is part of OpenTTD. + * OpenTTD 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, version 2. + * OpenTTD 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 OpenTTD. If not, see . + */ + +/** @file timer_window.h Definition of the Window system */ + +#ifndef TIMER_WINDOW_H +#define TIMER_WINDOW_H + +#include + +/** + * Timer that represents the real time, usable for the Window system. + * + * This can be used to create intervals based on milliseconds, seconds, etc. + * Mostly used for animation, scrolling, etc. + * + * Please be mindful that the order in which timers are called is not guaranteed. + * + * @note The lowest possible interval is 1ms. + * @note These timers can only be used in the Window system. + */ +class TimerWindow { +public: + using TPeriod = std::chrono::milliseconds; + using TElapsed = std::chrono::milliseconds; + struct TStorage { + std::chrono::milliseconds elapsed; + }; +}; + +#endif /* TIMER_WINDOW_H */ diff --git a/src/window.cpp b/src/window.cpp index 00ed25c7dc..e7db416316 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -39,6 +39,8 @@ #include "network/network_func.h" #include "guitimer_func.h" #include "news_func.h" +#include "timer/timer.h" +#include "timer/timer_window.h" #include "safeguards.h" @@ -3076,6 +3078,34 @@ void CallWindowRealtimeTickEvent(uint delta_ms) } } +/** Update various of window-related information on a regular interval. */ +static IntervalTimer window_interval(std::chrono::milliseconds(30), [](auto) { + extern int _caret_timer; + _caret_timer += 3; + CursorTick(); + + HandleKeyScrolling(); + HandleAutoscroll(); + DecreaseWindowCounters(); +}); + +/** Blink the window highlight colour constantly. */ +static IntervalTimer highlight_interval(std::chrono::milliseconds(450), [](auto) { + _window_highlight_colour = !_window_highlight_colour; +}); + +/** Blink all windows marked with a white border. */ +static IntervalTimer white_border_interval(std::chrono::milliseconds(30), [](auto) { + if (_network_dedicated) return; + + for (Window *w : Window::Iterate()) { + if ((w->flags & WF_WHITE_BORDER) && --w->white_border_timer == 0) { + CLRBITS(w->flags, WF_WHITE_BORDER); + w->SetDirty(); + } + } +}); + /** * Update the continuously changing contents of the windows, such as the viewports */ @@ -3093,56 +3123,21 @@ void UpdateWindows() ProcessPendingPerformanceMeasurements(); + TimerManager::Elapsed(std::chrono::milliseconds(delta_ms)); CallWindowRealtimeTickEvent(delta_ms); - static GUITimer network_message_timer = GUITimer(1); - if (network_message_timer.Elapsed(delta_ms)) { - network_message_timer.SetInterval(1000); - NetworkChatMessageLoop(); - } - /* Process invalidations before anything else. */ for (Window *w : Window::Iterate()) { w->ProcessScheduledInvalidations(); w->ProcessHighlightedInvalidations(); } - static GUITimer window_timer = GUITimer(1); - if (window_timer.Elapsed(delta_ms)) { - if (_network_dedicated) window_timer.SetInterval(MILLISECONDS_PER_TICK); - - extern int _caret_timer; - _caret_timer += 3; - CursorTick(); - - HandleKeyScrolling(); - HandleAutoscroll(); - DecreaseWindowCounters(); - } - - static GUITimer highlight_timer = GUITimer(1); - if (highlight_timer.Elapsed(delta_ms)) { - highlight_timer.SetInterval(450); - _window_highlight_colour = !_window_highlight_colour; - } - if (!_pause_mode || _game_mode == GM_EDITOR || _settings_game.construction.command_pause_level > CMDPL_NO_CONSTRUCTION) MoveAllTextEffects(delta_ms); /* Skip the actual drawing on dedicated servers without screen. * But still empty the invalidation queues above. */ if (_network_dedicated) return; - if (window_timer.HasElapsed()) { - window_timer.SetInterval(MILLISECONDS_PER_TICK); - - for (Window *w : Window::Iterate()) { - if ((w->flags & WF_WHITE_BORDER) && --w->white_border_timer == 0) { - CLRBITS(w->flags, WF_WHITE_BORDER); - w->SetDirty(); - } - } - } - DrawDirtyBlocks(); for (Window *w : Window::Iterate()) {