fix: improve endgame clock and gameplay stability

This commit is contained in:
CAtlas0784 committed 2026-11-04 15:59:48 +07:00
1 parent ce6aabfc55
commit 658298ee86
46 files changed
+1915 -99

No files matched your search

+52 -16
View File
@@ -11,7 +11,9 @@ import (
// 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"`
OffsetMS int64 `json:"offset_ms"`
WindowBeginMS int64 `json:"window_begin_ms,omitempty"`
WindowEndMS int64 `json:"window_end_ms,omitempty"`
}
func gameClockPath() (string, error) {
@@ -41,6 +43,9 @@ func ReadGameClock() (GameClockState, error) {
if err := json.Unmarshal(raw, &state); err != nil {
return state, fmt.Errorf("game clock state: %w", err)
}
if (state.WindowBeginMS != 0 || state.WindowEndMS != 0) && (state.WindowBeginMS < 0 || state.WindowEndMS <= state.WindowBeginMS || state.WindowEndMS > 253402300799999) {
return state, fmt.Errorf("invalid game clock window")
}
return state, nil
}
@@ -54,17 +59,18 @@ func GameTime() (time.Time, error) {
return time.Time{}, fmt.Errorf("game clock outside supported calendar range")
}
ms := base + s.OffsetMS
if s.WindowEndMS != 0 {
span := s.WindowEndMS - s.WindowBeginMS
delta := (ms - s.WindowBeginMS) % span
if delta < 0 {
delta += span
}
ms = s.WindowBeginMS + delta
}
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 {
@@ -73,32 +79,62 @@ func SetGameTime(target *time.Time) (time.Time, error) {
}
state.OffsetMS = target.UnixMilli() - now.UnixMilli()
}
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
if err := saveGameClockState(state); err != nil {
return time.Time{}, err
}
return time.UnixMilli(now.UnixMilli() + state.OffsetMS), nil
}
// SetGameTimeWindow keeps the shared game calendar inside an observed Client
// interval. It wraps at its end; the host clock and operational timers remain real.
func SetGameTimeWindow(begin, end time.Time) (time.Time, error) {
start, finish := begin.UnixMilli(), end.UnixMilli()
if start < 0 || finish <= start || finish > 253402300799999 {
return time.Time{}, fmt.Errorf("invalid game clock window")
}
now := time.Now()
midpoint := start + (finish-start)/2
state := GameClockState{OffsetMS: midpoint - now.UnixMilli(), WindowBeginMS: start, WindowEndMS: finish}
if err := saveGameClockState(state); err != nil {
return time.Time{}, err
}
return time.UnixMilli(midpoint), nil
}
func saveGameClockState(state GameClockState) error {
path, err := gameClockPath()
if err != nil {
return err
}
if path == "" {
return fmt.Errorf("configure game_clock.state_file or GAC_GAME_CLOCK_STATE_FILE first")
}
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
raw, err := json.MarshalIndent(state, "", " ")
if err != nil {
return time.Time{}, err
return err
}
f, err := os.CreateTemp(filepath.Dir(path), ".game-clock-*.tmp")
if err != nil {
return time.Time{}, err
return err
}
name := f.Name()
defer os.Remove(name)
if _, err = f.Write(raw); err != nil {
f.Close()
return time.Time{}, err
return err
}
if err = f.Sync(); err != nil {
f.Close()
return time.Time{}, err
return err
}
if err = f.Close(); err != nil {
return time.Time{}, err
return err
}
if err = os.Rename(name, path); err != nil {
return time.Time{}, err
return err
}
return time.UnixMilli(now.UnixMilli() + state.OffsetMS), nil
return nil
}