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:

  1. Loads the IANA location via time.LoadLocation and stores it in an atomic.Value (lines 33–38)
  2. Parses and validates the language code against a whitelist in xtime/locale.go (lines 15–24)
  3. 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 day
  • StartOfWeek(t) – Monday 00:00 of the current week
  • StartOfMonth(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, and InTimezone(t, tz) to convert to arbitrary player timezones.
  • Schedule recurring events with NextDailyTime, NextWeeklyTime, and NextMonthlyTime, which accept duration offsets to handle non-midnight resets (e.g., 04:00 daily quests).
  • Format for players using FormatRelative for durations or FormatLocalized with template tokens ({%w}, {%M}), switching languages via SetLocale.
  • Reference implementations are found in xtime/time.go (period logic), xtime/locale.go (language handling), and xtime/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →