Files
GaCy-GO/internal/common/game_clock.go
T

105 lines
2.4 KiB
Go

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
}