TickModule - Easily get synced UTC or Local Time

UPDATE: Added a syncing system between the client and the server, this means UTC time is practically identical between the client and the server. It can thus be used for precise syncing of actions between the client and server, without ping getting in the way
This update also adds a new GetPing method, calculated as client → server → client


So, how do you get UTC time in roblox? Use the DateTime object, but ummm… There are no DateTime.UTC or other constructor for that, the only useful methods to get UTC time are ToUniversalTime and FormatUniversalTime, the first one returns are table containing the year, month, day, second, … all separated, the second returns a string…
(Turns out DateTime.now() (and other things) returns the time in UTC, and I got that wrong, look at the reply below. That doesn’t make this module unless in any way though, you can use it to easily get UTC or LocalTime, and now, as an added bonus, you can avoid the stupidity of os.date() taking UTC time and returning a date in LocalTime)

All I want a simple function that returns the amount of time since the epox, with precision, and that is the same for everyone. Is that too much to ask???

Anyway, made a module for that

Source code

With the addition of the time syncing system, the module now uses a Unreliable Remote Event, and some require scripts (the module must be required on the server for the syncing to work, and on the client it can be nice to have it start syncing before other scripts use it, though requiring it from ReplicatedFirst would be best for that)

local RunService = game:GetService("RunService")

local SyncRemote = script.Sync

-- // Default Offsets // --

local UTC = DateTime.now():ToUniversalTime()
UTC = DateTime.fromUniversalTime(UTC.Year,UTC.Month,UTC.Day,UTC.Hour,UTC.Minute,UTC.Second,UTC.Millisecond).UnixTimestampMillis/1000
local UTCOffset = UTC - os.clock()

local localTime = DateTime.now():ToLocalTime()
localTime = DateTime.fromUniversalTime(localTime.Year,localTime.Month,localTime.Day,localTime.Hour,localTime.Minute,localTime.Second,localTime.Millisecond).UnixTimestampMillis/1000 -- Using fromUniversalTime because that function doesn't mess with the time
local localOffset = localTime - os.clock()

local UTC_Local_Offset = UTCOffset - localOffset

local Ping = 0 -- This will stay 0 on the server

-- // Module // -- 

local TickModule = {}

function TickModule:GetPing()
	if RunService:IsServer() then error("Cannot get the ping from the server") end
	return Ping
end

function TickModule:Tick()
	return os.clock() + UTCOffset
end

function TickModule:UTC()
	return os.clock() + UTCOffset
end

function TickModule:LocalTime()
	return os.clock() + localOffset,0
end

function TickModule:ToUTC(t : number)
	return t + UTC_Local_Offset
end

function TickModule:ToLocalTime(t : number)
	return t - UTC_Local_Offset
end

-- // This function is basically os.date() but it doesn't convert UTC time to LocalTime
-- // You give it UTC time, it will format a UTC date, give it a LocalTime, and you'll get a Local date
function TickModule:FormatDate(FormatString : string, Time : number?)
	Time = math.max(Time + UTC_Local_Offset or self:UTC(),0)
	return os.date(FormatString, Time)
end

-- // Sync // --

if RunService:IsServer() then 
	SyncRemote.OnServerEvent:Connect(function(Player) 
		SyncRemote:FireClient(Player,TickModule:UTC())
	end)
end

if RunService:IsClient() then
	
	local UTCOffsets = {}
	local Pings = {}
	
	local TIMEOUT = 30
	
	local function PingServer()
		local StartTime = os.clock()
		local DiffTime
		local ServerUTC
		
		local Coroutine = coroutine.running()
		
		local Connection = SyncRemote.OnClientEvent:Once(function(_UTC) 
			ServerUTC = _UTC
			DiffTime = os.clock() - StartTime
			coroutine.resume(Coroutine)
		end)
		
		task.delay(TIMEOUT,function()
			if ServerUTC then return end
			Connection:Disconnect()
			coroutine.resume(Coroutine)
		end)
		
		SyncRemote:FireServer()
		
		coroutine.yield()
		
		if DiffTime and DiffTime > TIMEOUT then return end -- Possible that it is an unrelated remote
		
		return ServerUTC, DiffTime
	end

	task.defer(function()
		local WaitTime = 0
		
		while true do
			task.wait(WaitTime)
			WaitTime = math.min(WaitTime + .5,10)
			
			-- // Get time from server, plus the ping, and calculate stuff with it
			
			local ServerUTC, DiffTime = PingServer()
			if not ServerUTC then continue end
			
			ServerUTC += DiffTime/2 -- Taking ping into account
			
			table.insert(UTCOffsets,1,ServerUTC - os.clock())
			UTCOffsets[11] = nil
			
			table.insert(Pings,1,DiffTime)
			Pings[11] = nil
			
			-- // Calculate the écart type
			
			local c_UTCOffsets = table.clone(UTCOffsets)
			local c_Pings = table.clone(Pings)
			
			table.sort(c_UTCOffsets)
			table.sort(c_Pings)
			
			UTCOffset = c_UTCOffsets[math.max(#c_UTCOffsets//2,1)]
			Ping = Pings[math.max(#Pings//2,1)]
		end
	end)
end

return TickModule

TickModule.rbxm (3.3 KB)
https://create.roblox.com/store/asset/18879903572

So you have some functions to get the time, Tick and UTC are the exact same (but depending on the use case one or the other might be more clear, I guess. Just delete one if you don’t want it lol)

There are also the ToUTC and ToLocalTime methods, which basically return a LocalTime/UTC seconds since epox number into UTC/LocalTime seconds since epox

It uses os.clock() under the hood as a baseline time. Well you can read the script, it’s not that complicated

This should provides a time that is identical between clients and servers (if the time on the clients/server’s computer isn’t out of sync…), so it can be used to safely store times in datastores. Hoping DateTime.now():ToUniversalTime() isn’t affected by daylights saving or some other stupid thing… If anything is flawed in my code, please tell me

I hope this simple and short “module” is useful :wink:

8 Likes

IMPORTANT UPDATE

Turns out my module was wrong (it should use .fromUniversalTime() instead of .fromLocalTime, which makes more sense)
What threw me off is that os.date() can return two different dates, from the same number because it converts the time into local time, which in my opinion is very stupid. So you need to give it the time in UTC to get the local time, and if you want the UTC date, then you have to give it like UTC - LocalTime offset probably.

This is why I think a module like this is important lmao, but now I am a bit mad because it might have created a little mess I have to clean up in a game I just released that relies on every server having the same time (mainly the time between studio and running servers)

I hope this isn’t affecting anyone else, as my “module” didn’t gain any traction I hope that is the case. If you did use my code for your project, well hopefully you saw this…

1 Like

Very cool :3
I hope more people see this!

Thanks

Now I realized that the UTC time and Local time is the same in my module. I will have to figure out what the hell is going on and get the right time. I’ll update this reply when I am done with that :wink:
Turns out DateTime.now() returns time in UTC. Oh well

Thankfully, my previous mistake did not mess up my game, it ended up being fine even with that error

Ok, I fixed it, I’m pretty confident that it now works properly, I tested it (if I tested it properly)

1 Like

Things are fixed now, I also added a new method for formatting the time (aka os.date()), but UTC returns a UTC date and LocalTime returns a LocalTime date (how it should be smh)

Glad you like it :happy1:

whats this exactly do? is it just to get the current unix timestamp?
os.time() “Returns how many seconds have passed since the Unix epoch under current UTC time.”

Well yeah, but os.time() rounds to the nearest second. The script also provides methods to get the unix timestamp in local time instead of UTC. Basically, it makes working with time a lot easier since you know exactly what you are working with

There is also the method I just added that is basically os.date but not stupid. I found out today that os.date converts the time you give it to local time, so you need to give it UTC time (which will return a date in local time). The new method returns a date in UTC if you give it a time in UTC and returns a date in local time if you give it local time

Yet another reply

Ok, so, os.date() might not be as stupid as I thought, because if you put “!” in front of the format string, it wont convert the time
I still think it makes no sense to convert the time at times like this. If you want to use Local or UTC time, then get Local or UTC “unix timestamps”

DateTime exists for that…

local CurrentTime = DateTime.now()

local UnixSeconds = CurrentTime.UnixTimestamp
local UnixMilliseconds = CurrentTime.UnixTimestampMillis

I don’t really see the point of this module tbh

1 Like

For the local time, you either need to use tick() or this atrocity

local localTime = DateTime.now():ToLocalTime()
localTime = DateTime.fromUniversalTime(localTime.Year,localTime.Month,localTime.Day,localTime.Hour,localTime.Minute,localTime.Second,localTime.Millisecond).UnixTimestampMillis/1000 -- Using fromUniversalTime because that function doesn't mess with the time

The “module” also contains functions for converting time/

It uses os.clock() under the hood so you get even more precision (smaller than 1ms). This precision is not time precise, but it basically allows scripts that would typically rely on tick() to have more numbers
(I have a couple of scripts that basically move a part to some position given a “tick” value, if it would round to the nearest millisecond, that would not be great)

That’s fine. You might not be running into the same stuff as I have. I made this module for myself, and thought it wouldn’t hurt to post it

A universal tick makes things so much simpler thank you

1 Like

UPDATE: Added a syncing system between the client and the server, this means UTC time is practically identical between the client and the server. It can thus be used for precise syncing of actions between the client and server, without ping getting in the way
This update also adds a new GetPing method, calculated as client → server → client