package common import ( "encoding/json" "fmt" "os" "path/filepath" "time" ) // Only game-facing calendar time changes. Network timeouts, diagnostics, leases // and battle duration measurement continue to use the host's real clock. type GameClockState struct { OffsetMS int64 `json:"offset_ms"` } func gameClockPath() (string, error) { cfg, err := LoadRuntimeConfig() if err != nil { return "", err } return cfg.GameClock.StateFile, nil } func ReadGameClock() (GameClockState, error) { path, err := gameClockPath() if err != nil { return GameClockState{}, err } if path == "" { return GameClockState{}, nil } raw, err := os.ReadFile(path) if os.IsNotExist(err) { return GameClockState{}, nil } if err != nil { return GameClockState{}, err } var state GameClockState if err := json.Unmarshal(raw, &state); err != nil { return state, fmt.Errorf("game clock state: %w", err) } return state, nil } func GameTime() (time.Time, error) { s, err := ReadGameClock() if err != nil { return time.Time{}, err } base := time.Now().UnixMilli() if s.OffsetMS < -base || s.OffsetMS > 253402300799999-base { return time.Time{}, fmt.Errorf("game clock outside supported calendar range") } ms := base + s.OffsetMS return time.UnixMilli(ms), nil } func SetGameTime(target *time.Time) (time.Time, error) { path, err := gameClockPath() if err != nil { return time.Time{}, err } if path == "" { return time.Time{}, fmt.Errorf("configure game_clock.state_file or GAC_GAME_CLOCK_STATE_FILE first") } now := time.Now() state := GameClockState{} if target != nil { if target.UnixMilli() < 0 || target.Year() > 9999 { return time.Time{}, fmt.Errorf("game time must be between Unix epoch and year 9999") } state.OffsetMS = target.UnixMilli() - now.UnixMilli() } if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { return time.Time{}, err } raw, err := json.MarshalIndent(state, "", " ") if err != nil { return time.Time{}, err } f, err := os.CreateTemp(filepath.Dir(path), ".game-clock-*.tmp") if err != nil { return time.Time{}, err } name := f.Name() defer os.Remove(name) if _, err = f.Write(raw); err != nil { f.Close() return time.Time{}, err } if err = f.Sync(); err != nil { f.Close() return time.Time{}, err } if err = f.Close(); err != nil { return time.Time{}, err } if err = os.Rename(name, path); err != nil { return time.Time{}, err } return time.UnixMilli(now.UnixMilli() + state.OffsetMS), nil }