Skip to content

Instantly share code, notes, and snippets.

@TrueBrain
Last active April 9, 2023 14:00
Show Gist options
  • Save TrueBrain/3f2677d28fbb626f8d8bce94cff021ed to your computer and use it in GitHub Desktop.
Save TrueBrain/3f2677d28fbb626f8d8bce94cff021ed to your computer and use it in GitHub Desktop.
OpenTTD and date/time

In OpenTTD, we have a few different places that uses the concept of date and time.

  • Windows has several counters. The Window timing is one of the first that was changed to work based on real-time, instead of game-time, but with a twist. The solution for this is created locally in the Window code, by tracking the delta_ms between calls to UpdateWindows. But instead of having the rest of the code change to show things in "real-time" values, the rest of the code still uses "ticks". Just the Window code makes sure that a tick is always MILLISECONDS_PER_TICK, no matter if you fast-forward or not. There are six counter-like things in total:
    • scroller-click: rate-limits how fast you can scroll; not influenced by fast-forward.
    • OnHundredthTick callback: meant to be called every 3 seconds for various of jobs in a Window (like refreshing the list); not influenced by fast-forward.
    • OnTimeout callback: called when a timeout expires, which can be set per window; not influenced by fast-forward.
    • OnRealtimeTick callback: called with the millisecond delta from last call.
    • OnGameTick callback: called every game tick.
    • highlight_timer: defines how long something is highlighted, in real-time.
  • Network has several counters too, a bit all of the place.
    • Chat has remove_time, which is a real-time value (std::chrono) for when to remove / not show a chat message.
    • ServerNetworkAdminSocketHandler uses real-time (std::chrono) to detect some timeouts (like authorization-took-too-long)
    • ClientNetworkGameSocketHandler uses real-time (std::chrono) to detect timeouts during connecting (recent change, as it used to be game-time based too.
    • ClientNetworkCoordinatorSocketHandler uses real-time (std::chrono) to detect timeouts during connecting.
    • Refresh of the server-listing is triggered by OnRealtimeTick, so real-time, but with a twist!
    • NetworkServer_Tick checks if a client is lagging, based on _frame_counter, which is basically game-time but can't be paused. The lag detection is everything from "client downloading map" to actually playing the game. Some parts however do use real-time. So it is a mixed bag of different types of lag-detection.
  • Bankrupt "do you want to take over" question (HandleBankruptcyTakeover) is game-time based, set for 90 days.
  • Linkgraph is based on game-time.
  • AI logic when to create a new company is based on game-time.
  • Draw logic has its own way of dealing with time (as it runs on 60Hz, for example), and is uncoupled from game-time.
  • Music on Windows (via DMusic) does its own thing too.
  • Autosave is based on game-time.
  • We have the intend to split up game-time in calendar-time and economy-time.
    • These include things like Monthly callbacks, etc.

Anything not mentioned above explicitly, is in game-time of course.

We kinda need to untangle this web of date/time, to make it less of a mess.

It is important to remember we once were fully game-time based, and over time we started to add the concept of real-time in specific places. This also means we have places where we still use game-time (deduced via real-time sometimes) to do things, that don't actually make sense. Autosave is a clear example. Another is for example the scroll-speed. This is based on real-time, but delays it with MILLISECONDS_PER_TICK. Although fine, it in fact makes little sense to use that enum for it; it no longer has anything to do with "per-tick".

There are several types of date/time:

  • Game-time: for most of the game. Splits into three categories:
    • Calendar (including things like leap-year, etc): for most of the game.
    • Economy: future work, the changes 2TallTyler is working on.
    • Network: can't be paused, helps the network to exist.
  • Real-time: for things like Window, network, AI-start-date, etc. This means we have to replace those OnHunderthTick callbacks in Window with some proper millisecond based callbacks.
  • Play-time: this needs a few more words, and is meant for autosave / linkgraph. Both should trigger every N seconds if the game is not paused. But if the game is paused, they should not trigger every N seconds. Unless build-when-paused is enabled, and someone actually made a change to the map. So, play-time: time you have been playing (where building something when paused is considered playing).
  • Draw-time: based on Hz of screens.

Basically I see 3 (or 4) classes which have to be initialized on a global, which is then used by that part of the system to deduce time with. For real-time this basically means wrapping std::chrono, but for the others it allows for specific logic.

This also allows for example to have Economy to have multiple implementations, of which only one is initialized. This makes room to switch between the current Calendar based economy, or the new suggested Real-time based economy.

After some fiddling, I am thinking in this direction:

void showcase_usage()
{
    CreateIntervalTimer<TimerGameCalendar>(TimerGameCalendar::DAY, [](uint count) {
        Debug(misc, 0, "new day!");
        // Do something
    });
    CreateIntervalTimer<TimerGameCalendar>(TimerGameCalendar::MONTH, [](uint count) {
        Debug(misc, 0, "new month!");
        // Do something
    });
    CreateIntervalTimer<TimerReal>(std::chrono::seconds(1), [](uint count) {
        // Do something
    });
}

Next to Interval there is also OneShot that can be Reset. Both have their usages.

So basically, we have four timers:

  • TimerGameCalendar
  • TimerGameNetwork
  • TimerReal
  • TimerPlay

Most of the code is like:

  • Call me every new day
  • Call me every tick
  • Call me every 300ms

So you can create an Interval (or OneShot) timer, based on the type of information you want to be triggered, and your callback will be called.

This basically replaces the whole "OnNewDay" chain we currently have, and things can just register themselves locally.

For example in the Window code, there are now GUITimers which check if a certain amount of time is passed. These are in a function that is called by the main Window handler by using a GUITimer to see if a single "window" tick has passed. This would be replaced by a single CreateIntervalTimer<TimerReal>(std::chrono::milliseconds(100), ..) instead.

This also means that Windows that have multiple GUITimers don't have to share the OnTimeout callback; each timer will get their own.

The one thing I am not sure about in this implementation, is the use of templates. They have a benefit: it makes all the timers equal. Otherwise you will get a CreateIntervalTimerGameCalender, CreateIntervalTimerReal, etc. This is a lot of code duplication for essentially the same code. Their main difference is some minor behaviour and the type of the paramters. (GameCalendar uses DAY/MONTH/YEAR, where Real uses std::chrono).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment