How to Use fabrica-util's xtime Package for Advanced Timezone Management and Period Calculations in Game Servers
Initialize the package with xtime.Init(cfg) to atomically set a global timezone and locale, then use period helpers like NextDailyTime and InTimezone to schedule game events and convert player timestamps without lock contention.
The xtime package in go-pantheon/fabrica-util provides a concurrent-safe abstraction layer atop Go's standard time library, purpose-built for game servers that must coordinate daily quest resets, weekly leaderboards, and player-local timestamp formatting across multiple timezones. Unlike raw time operations, xtime stores the server location in an atomic.Value for lock-free reads and provides deterministic period calculations that guarantee "next occurrence" times always fall after the current timestamp.
Initializing the Global Timezone and Locale
All xtime operations depend on a one-time initialization that loads the server-wide timezone and default language. The Init function in xtime/time.go (lines 22–54) performs three atomic setup steps:
- Loads the IANA location via
time.LoadLocationand stores it in anatomic.Value(lines 33–38) - Parses and validates the language code against a whitelist in
xtime/locale.go(lines 15–24) - Initializes locale data from embedded JSON or falls back to English
If the package is used without initialization, GetLocation() safely returns time.UTC, ensuring nil-pointer protection.
import "github.com/go-pantheon/fabrica-util/xtime"
func initServerTime() error {
cfg := xtime.Config{
Language: xtime.LanguageEn, // Default for system logs
Timezone: "UTC", // Server-wide timezone
}
return xtime.Init(cfg)
}
Managing Timezones for Server and Player Time
The package distinguishes between the server's configured location (used for cron-like scheduling) and arbitrary timezone conversion (used for player display).
Retrieving the Server Location
GetLocation() returns the *time.Location stored during Init. Because it reads from an atomic.Value, it is safe to call from thousands of concurrent goroutines handling player connections without mutex overhead.
now := time.Now().In(xtime.GetLocation())
Converting to Player Local Time
InTimezone(t, tz) converts any time.Time to a specified IANA timezone string. This is essential when displaying event times to players in regions like JST or EST while storing everything as UTC in your database.
func showPlayerLocalTime(unixSeconds int64, playerTZ string) (time.Time, error) {
utcTime := time.Unix(unixSeconds, 0)
return xtime.InTimezone(utcTime, playerTZ)
}
Period Calculations for Game Events
The xtime/time.go file exports boundary and scheduling helpers that handle the complex logic of "when is the next 04:00 daily reset" or "when is the first Monday of next month."
Calculating Period Boundaries
Use these functions to snap timestamps to the start of a period:
StartOfDay(t)– Midnight of the current dayStartOfWeek(t)– Monday 00:00 of the current weekStartOfMonth(t)– First day of month at 00:00
Scheduling Next Occurrences with Offsets
The NextDailyTime, NextWeeklyTime, and NextMonthlyTime functions accept a time.Duration offset to schedule events that don't start at midnight. Internally, they rewind the input by the delay, snap to the period boundary, advance one full period, then re-add the delay—guaranteeing the result is always strictly after the input time.
Daily Quest Example (04:00 server time):
var dailyQuestDelay = 4 * time.Hour
func nextDailyQuest(now time.Time) time.Time {
// Align to server location, then compute next 04:00 occurrence
now = now.In(xtime.GetLocation())
return xtime.NextDailyTime(now, dailyQuestDelay)
}
Weekly Raid Example (Monday 20:00):
var raidDelay = 20 * time.Hour // 20:00 is 20 hours after midnight
func nextWeeklyRaid(now time.Time) time.Time {
now = now.In(xtime.GetLocation())
return xtime.NextWeeklyTime(now, raidDelay)
}
Monthly Leaderboard Reset (1st day 00:00):
func nextMonthlyReset(now time.Time) time.Time {
now = now.In(xtime.GetLocation())
return xtime.NextMonthlyTime(now, 0) // No offset needed
}
Locale-Aware Formatting
The xtime/format.go and xtime/locale.go files provide human-readable output that respects the language set during Init or overridden per-call.
Displaying Relative Times
FormatRelative (lines 40–47 of format.go) renders strings like "5 minutes ago" or "2 hours from now" based on the current locale.
func formatEventCountdown(eventTime time.Time) string {
return xtime.FormatRelative(eventTime)
}
Localized Date Templates
FormatLocalized uses template tokens like {%w} for weekday, {%M} for month name, and {%d} for day. Switch languages at runtime with SetLocale from locale.go.
func localizedAnnouncement(t time.Time, lang xtime.Language) string {
// Temporarily switch locale for this player
_ = xtime.SetLocale(lang)
// Template: "Today is Monday, December 25, 2023"
return xtime.FormatLocalized(t, "Today is {%w}, {%M} {%d}, {%y}")
}
Complete Game Server Integration Example
This full example demonstrates initializing the package, calculating the next daily quest, converting to a player's Tokyo timezone, and rendering a Chinese-language timestamp:
package main
import (
"fmt"
"time"
"github.com/go-pantheon/fabrica-util/xtime"
)
func main() {
// 1. Initialize server-wide configuration
if err := xtime.Init(xtime.Config{
Language: xtime.LanguageEn,
Timezone: "UTC",
}); err != nil {
panic(err)
}
now := time.Now()
// 2. Calculate next daily quest at 04:00 UTC
nextQuest := xtime.NextDailyTime(now, 4*time.Hour)
fmt.Println("Next daily quest:", nextQuest.In(xtime.GetLocation()))
// 3. Convert to player local time (Tokyo)
playerTime, _ := xtime.InTimezone(now, "Asia/Tokyo")
fmt.Println("Player local time:", playerTime)
// 4. Show relative time for an event that started 2 hours ago
pastEvent := now.Add(-2 * time.Hour)
fmt.Println("Event status:", xtime.FormatRelative(pastEvent))
// 5. Render localized log in Chinese
_ = xtime.SetLocale(xtime.LanguageZhCN)
fmt.Println("中文日志:", xtime.FormatLocalized(now, "现在是{%y}年{%M}{%d}日,{%w}"))
}
Summary
- Initialize once with
xtime.Init(cfg)to atomically set the global timezone and language; the package safely falls back to UTC if uninitialized. - Use
GetLocation()for lock-free access to the server timezone, andInTimezone(t, tz)to convert to arbitrary player timezones. - Schedule recurring events with
NextDailyTime,NextWeeklyTime, andNextMonthlyTime, which accept duration offsets to handle non-midnight resets (e.g., 04:00 daily quests). - Format for players using
FormatRelativefor durations orFormatLocalizedwith template tokens ({%w},{%M}), switching languages viaSetLocale. - Reference implementations are found in
xtime/time.go(period logic),xtime/locale.go(language handling), andxtime/format.go(template rendering).
Frequently Asked Questions
What happens if I call xtime.Init multiple times?
xtime.Init is designed to be called once at server startup. While subsequent calls will attempt to reload the location and locale, the package is optimized for a single initialization pattern. For dynamic language switching per player, use SetLocale rather than reinitializing the entire package.
How does the package handle daylight saving time transitions?
Because xtime stores the *time.Location object loaded via time.LoadLocation, it automatically respects all IANA timezone rules including DST shifts. When you pass a time.Time through InTimezone or calculate next occurrences using NextDailyTime, the underlying time package handles the offset transitions according to the zone database.
Can I calculate periods for a timezone different from the server default?
Yes. While NextDailyTime and similar functions use the server location from GetLocation(), you can convert any time to a different zone first using InTimezone, then pass that converted time into the period functions. The calculations will respect the location embedded in the time.Time object.
Where is the locale data stored?
Locale JSON files are embedded in the binary and loaded into memory during Init or SetLocale. The validLanguageCodes map in xtime/locale.go validates languages against available translations, and the active locale templates are held in package-level variables for fast template lookups during FormatLocalized calls.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →