Workspace:GetServerTimeNow() documentation incorrect and DateTime.now() needs more information

Workspace:GetServerTimeNow() documentation is incorrect

Documentation says

Essentially, it is the client’s best guess of what os.clock would return on the server.

It is not at all an estimate based on what os.clock() would return on the server, os.clock() has no Unix epoch baseline. The single function it could be said to estimate the server value of would be tick(), but tick() is not monotonically increasing and Workspace:GetServerTimeNow() is.

This distinction is important, any code relying on that documentation will almost certainly be incorrect.

DateTime.now() documentation does not specify if it’s monotonically increasing

Documentation says nothing about whether DateTime.now() is monotonically increasing. tick() isn’t, os.time() is, Workspace:GetServerTimeNow() is, so it’s unclear.

6 Likes

We’ve filed a ticket to our internal database for this issue. We will come back as soon as we have updates!

Thanks for the report!

5 Likes

Workspace:GetServerTimeNow() can be called from the server to compare results

this seems like easy to find, but you normally try the idea of using os.time() etc. first and may never know

What is your use case? Hard to tell what kind of clock you want without knowing more.

Still trying to confirm with others, but from my reading of the code…

Docs are definitely incorrect, it probably meant to say os.time(), which does report system time. I believe DateTime.now() also reads system time as well. I don’t believe these would be monotonic on clients or servers given leap seconds, and other clock updates. On clients there’s no guarantee of any kind of accuracy or sanity.

I believe tick() is actually monotonic. It captures a timestamp offset on startup, but it uses a monotonic clock and should be monotonic from there. Leaves some possibility of it drifting over the runtime of the session (leap seconds, etc.), but in practice not by much.

GetServerTimeNow is like tick() (based off the same internal monotonic clock), but with constant synchronization and smoothing on top of . Like the docs say, it will slightly slow or speed up time to maintain synchronization instead of skipping backwards/forwards.

GetServerTimeNow does some additional NTP-like approximate latency compensation, attempting to sync it up with wall clock time on the server. Some monotonic smoothing on top of something like…

lastReceivedServerTime + (ping / 2)

However…

… for many game use cases this type of wall clock latency compensation is not actually something you want! We don’t do any kind of time extrapolation like this on the rest of a client’s view of the world! The only thing you know for sure is that by the time the server receives an event from a client, it is a user’s reaction to a view of the world that was full round tip latency in the past. Or locally from the perspective of the client its character is running full round trip latency in the future relative to the rest of the world! Outside of their own character simulation (or other owned simulation), the client’s view of the world only includes reactions to their actions RTT in the past.

In many cases it’s better to not do any latency compensation (i.e. extrapolation) and just accept the client’s view of the world being behind the server. If you pushed your client next to a monitor on the server it would be behind, but you’ll never be able to do that anyway! Extrapolation comes with it’s own problems that are often worse than the mixed timeline view inconsistency.

You usually usually never actually want a latency compensated wall clock for coordinating things in a networked simulation.

I’m a little disappointed we added this approximate latency compensation on GetServerTimeNow; use cases for it exist, but are extraordinarily niche.

Suggestion: client initiated actions either happen on the server at the moment they are received (with no compensation) or skipped ahead by that client’s full RTT (to line up with their predicted view by the time they receive it). If you’re doing anything coordinated with any other systems these are the only two options that make any sense.

Don’t know your actual use case, not sure if this applies. I just get worried when people start asking detailed questions about GetServerTimeNow.

Unless a lawyer is demanding that all clients see something happen in game when the clock strikes 12, and he will be verifying this with an atomic clock and a high speed camera and won’t be paying you otherwise (run away!), you probably don’t want GetServerTimeNow.

9 Likes

EDIT: I did the big stupid.
@Wunder_Wulfe is correct. You can use os.date(“%z”) to get the timezone.

I havn’t testing in live servers though so I’m not sure if it works.

4 Likes

Personally, I greatly prefer the extrapolated time system (provided it can maintain a time that is as accurate as possible to server time).

This means you can use the time in many complex systems such as communicating at what time a certain client/server event occurred, adjusting or stepping ahead when necessary; or when keeping unified time systems in games where the exact time of day is synced for all players (and can be overridden for local conditions, thus it needs to be able to determine the time of day independent of lighting properties).

Here is an example of “accurate” coordinated time, and also how to get the timezone of the server without HTTP service.

local ISOOffset: string = os.date("%z")


-- example: "-0500" (05:00 (5 hours) behind UTC time)
-- (-) (05) (00) -> -1, 5, 0
local sign: number = 
    if string.sub(ISOOffset, 1, 1) == '+' then 
        1 
    else 
        -1

local hours: number = tonumber(
    string.sub(ISOOffset, 2, 3)
)::number

local minutes: number = tonumber(
    string.sub(ISOOffset, 4, 5)
)::number

local UTCOffset: number = sign * (hours * 3600 + minutes * 60)

-- if you would like to format this as "UTC±hh:mm":
local sym: string = if sign > 0 then '+' else '-'
print(`UTC{sym}{hours}:{string.format("%02d", minutes)}`)

-- store for use later if you are doing this on the server
workspace:SetAttribute("UTCOffset", UTCOffset)


-- how to use the offset
local function GetUTCTime(): number
    -- subtract the offset from current server time
    -- i.e. if ISO offset was "-0500", would report a time 5 hours ahead
    return tick() - UTCOffset
end


print(GetUTCTime())

As for computing the actual name of the timezone, you can use os.date() with %Z in your format string; although I believe this depends on the locale of the machine executing the function call, so in order to report the server’s timezone, you would have to make sure that the server is the one calling os.date().

Example of that:

local TimeZone: string = os.date("%Z")

-- example: "Central Standard Time"
workspace:SetAttribute("TimeZoneFull", TimeZone)

-- to abbreviate, strip away lowercase and whitespace characters
-- example: "Ce̶n̶t̶r̶a̶l̶ ̶St̶a̶n̶d̶a̶r̶d̶ ̶Ti̶m̶e̶" -> "CST"
TimeZone = string.gsub(TimeZone, "[%l%s]+", "") 
workspace:SetAttribute("TimeZone", TimeZone)

This can also be done on the client if you want to be able to have synced time, or you can rely on workspace:GetServerTimeNow() and its respective offset.

1 Like

Lol that last little note is funny

I wish we had known about this ahead of time for the Swedish House Mafia concert because timing was actually very crucial for the lights to sync up with the music (as well as transitions). We used tick(), but I’m hearing that if a client’s computer time is inaccurate (like a dead CMOS battery on the mobo), then this tick() can actually be behind? Can anyone verify if this is true? During tests we only had problems with maybe 1% of users, but we were crunched for time and just accepted this since it would fix itself the next time they joined. (There were lots of problems with the codebase but I was brought onto the project too late to fix these)

For many of my projects that involve synchronization, it’s quite frequent that I need a clock time that is the same for every computer in every time zone at any given moment in time it’s called.

For the Swedish House Mafia concert, the music and light show were already pre-programmed. We just needed to know what time the client was seeing, compare that to the time the server said the song started playing for the rest of the players, and sync up their audio and visuals. The goal was that I can bring any 2 computers together, turn up the volume on all of them and the music + visuals were synced up on every device. Things like seeing other people move / jump didn’t need to be synced, but the show absolutely had to or else people would be missing transitions or transitioning too early to the next song since everything ran client-side.

1 Like