Documentation
¶
Index ¶
- Constants
- Variables
- type Broker
- type DailyLimitRule
- type GrantMode
- type GrantRequest
- type GrantResult
- type LimitsManager
- func (tm *LimitsManager) CheckBeforeLaunch() (string, error)
- func (tm *LimitsManager) EffectiveLimitsEnabled() bool
- func (tm *LimitsManager) GetStatus() *StatusInfo
- func (tm *LimitsManager) Grant(req *GrantRequest) (GrantResult, error)
- func (tm *LimitsManager) IsEnabled() bool
- func (tm *LimitsManager) OnMediaStarted()
- func (tm *LimitsManager) OnMediaStopped()
- func (tm *LimitsManager) ResetSession()
- func (tm *LimitsManager) RestoreExtensions(now time.Time)
- func (tm *LimitsManager) RestoreSessionFromHistory(now time.Time)
- func (tm *LimitsManager) SetBeforeExitHook(hook func())
- func (tm *LimitsManager) SetEnabled(enabled bool)
- func (tm *LimitsManager) SetLimitsProvider(limits LimitsProvider)
- func (tm *LimitsManager) Start(broker Broker, notificationsSend chan<- models.Notification)
- func (tm *LimitsManager) Stop()
- type LimitsProvider
- type Rule
- type RuleContext
- type SessionLimitRule
- type SessionState
- type StatusInfo
Constants ¶
const ( // MinGrantDuration is the smallest duration grant accepted. Anything // shorter is not worth the scan and would be swallowed by the 30 second // check interval. MinGrantDuration = 1 * time.Minute // MaxGrantDuration is the largest duration a single grant may add. MaxGrantDuration = 24 * time.Hour // MaxSessionExtension caps the duration accumulated across every grant // applied to one session. Grants that would exceed it are rejected // rather than clamped, so the caller learns the grant did not apply. MaxSessionExtension = 24 * time.Hour )
const ( // DefaultSessionResetTimeout is the default idle time before a session resets. // After this period of inactivity, the next game launch starts a fresh session. DefaultSessionResetTimeout = 20 * time.Minute // MinimumViableSession is the minimum time a session should run before being stoppable. // If remaining time < this value, the launch is blocked entirely rather than starting // a game that will be immediately killed. MinimumViableSession = 1 * time.Minute )
Variables ¶
var ( // ErrGrantModeInvalid is returned for an unrecognized grant mode. ErrGrantModeInvalid = errors.New("unknown playtime extension mode") // ErrGrantDurationRange is returned when a duration grant falls outside // MinGrantDuration..MaxGrantDuration. ErrGrantDurationRange = errors.New("playtime extension duration out of range") // ErrGrantCapExceeded is returned when a grant would push the session's // accumulated extension past MaxSessionExtension. ErrGrantCapExceeded = errors.New("playtime extension would exceed the session cap") // ErrGrantNoSession is returned when a duration grant is attempted with // no session to extend. ErrGrantNoSession = errors.New("no playtime session to extend") // ErrGrantLimitsDisabled is returned when limits are not being enforced, // so there is nothing to extend. ErrGrantLimitsDisabled = errors.New("playtime limits are not enabled") // ErrGrantClockUnreliable is returned when a day-scoped grant is // attempted while the system clock cannot be trusted to find midnight. ErrGrantClockUnreliable = errors.New("system clock is unreliable") // ErrGrantStateChanged is returned when the session changed underneath a // grant while it was being applied. The caller may retry. ErrGrantStateChanged = errors.New("playtime session changed during grant") // it is refused rather than held only in memory. ErrGrantUnavailable = errors.New("playtime extension storage unavailable") )
Grant rejection reasons. Callers map these onto their own error surfaces: a failed ZapScript command for the card path, a client error for the API.
var ErrLimitReached = errors.New("playtime limit reached")
ErrLimitReached is wrapped by CheckBeforeLaunch when a daily or session limit blocks a launch, as opposed to a failure looking up usage.
Functions ¶
This section is empty.
Types ¶
type Broker ¶
type Broker interface {
Subscribe(bufferSize int, methods ...string) (<-chan models.Notification, int)
Unsubscribe(id int)
}
Broker is the interface for subscribing to notifications.
type DailyLimitRule ¶
DailyLimitRule enforces a maximum total play time per day.
func (*DailyLimitRule) Evaluate ¶
func (r *DailyLimitRule) Evaluate(ctx RuleContext) (allowed bool, remaining time.Duration, reason string)
Evaluate checks if today's total usage has exceeded the daily limit.
type GrantMode ¶ added in v2.17.0
type GrantMode string
GrantMode selects what an extension grant does to the recipient's limits.
const ( // GrantModeDuration adds time to the current session's allowance. It // applies to one session and is cleared when that session resets. GrantModeDuration GrantMode = "duration" // GrantModeToday waives the session limit for the recipient profile // until the next local midnight. The daily limit still applies. GrantModeToday GrantMode = "today" )
type GrantRequest ¶ added in v2.17.0
type GrantRequest struct {
AuthorizerProfileID string
AuthorizerClientID string
Source string
IdempotencyKey string
Mode GrantMode
IdempotencyWindow time.Duration
Duration time.Duration
}
GrantRequest asks for an extension to the effective playtime session. The recipient is never chosen by the caller: it is the profile currently governing playtime, so a grant cannot be aimed at somebody else.
type GrantResult ¶ added in v2.17.0
type GrantResult struct {
// ExpiresAt is when a day waiver lapses. Zero for duration grants.
ExpiresAt time.Time
// RecipientProfileID is the profile the grant applies to. Empty is the
// shared profile, matching daily accounting elsewhere.
RecipientProfileID string
// AuthorizerProfileID is the profile that authorized the grant.
AuthorizerProfileID string
// Mode is the mode that was applied.
Mode GrantMode
// Duration is what this grant added. Zero for day waivers.
Duration time.Duration
// SessionExtension is the session's accumulated duration extension after
// this grant.
SessionExtension time.Duration
// Replayed is true when the request granted no new time, either because a
// matching idempotency key had already been applied or because the day
// was already waived.
Replayed bool
}
GrantResult describes an applied grant. It is also what a deduplicated repeat returns, so a retry sees the same answer as the original call.
type LimitsManager ¶
type LimitsManager struct {
// contains filtered or unexported fields
}
LimitsManager enforces time limits and warnings for gameplay sessions.
func NewLimitsManager ¶
func NewLimitsManager( db *database.Database, platform platforms.Platform, cfg *config.Instance, clock clockwork.Clock, player audio.Player, ) *LimitsManager
NewLimitsManager creates a new LimitsManager instance.
func (*LimitsManager) CheckBeforeLaunch ¶
func (tm *LimitsManager) CheckBeforeLaunch() (string, error)
CheckBeforeLaunch checks if launching new media would exceed daily or session limits. Returns a reason string (models.PlaytimeLimitReasonDaily or models.PlaytimeLimitReasonSession) and a non-nil error when the launch should be blocked: - Daily or session limit is already exceeded - Remaining time < MinimumViableSession (prevents launching a game that will be immediately killed) On success, reason is "" and error is nil.
Limit rejections wrap ErrLimitReached so callers can tell a limit apart from a usage-lookup failure.
func (*LimitsManager) EffectiveLimitsEnabled ¶ added in v2.17.0
func (tm *LimitsManager) EffectiveLimitsEnabled() bool
EffectiveLimitsEnabled reports whether limits are enforced for the current session, honoring the active profile's override and the launch-time pin.
func (*LimitsManager) GetStatus ¶
func (tm *LimitsManager) GetStatus() *StatusInfo
GetStatus returns the current playtime session and limit status. Always returns a StatusInfo struct with current state information.
func (*LimitsManager) Grant ¶ added in v2.17.0
func (tm *LimitsManager) Grant(req *GrantRequest) (GrantResult, error)
Grant applies an extension to the effective session. It resolves the recipient, validates the request against current state, persists the new snapshot, and only then updates memory: a grant that cannot be stored is refused rather than surviving only until the next restart.
On success the caller should treat the returned result as the record of what happened, including for a deduplicated repeat.
func (*LimitsManager) IsEnabled ¶
func (tm *LimitsManager) IsEnabled() bool
IsEnabled returns the runtime enabled toggle. It reflects global config only; use EffectiveLimitsEnabled to ask whether limits are actually being enforced for whoever is playing.
func (*LimitsManager) OnMediaStarted ¶
func (tm *LimitsManager) OnMediaStarted()
OnMediaStarted handles media.started events and begins time tracking.
func (*LimitsManager) OnMediaStopped ¶
func (tm *LimitsManager) OnMediaStopped()
OnMediaStopped handles media.stopped events and stops time tracking.
func (*LimitsManager) ResetSession ¶ added in v2.16.0
func (tm *LimitsManager) ResetSession()
ResetSession starts a fresh limit session, called when the active profile identity changes: a different person is playing, so accumulated session time belongs to the previous profile. Daily usage is unaffected — it is recalculated from the (profile-attributed) history on every check.
If a game is running, tracking restarts from now under the new profile's limits rather than stopping: the running game's already-played time was the previous profile's.
func (*LimitsManager) RestoreExtensions ¶ added in v2.17.0
func (tm *LimitsManager) RestoreExtensions(now time.Time)
RestoreExtensions reloads granted extensions after a restart. It must run after RestoreSessionFromHistory so it can see whether the session the duration grant belongs to actually came back.
Day waivers restore on their own: they are scoped to a profile and a calendar day, not to a session. A duration grant only restores when its session was restored and the recipient still matches, so a grant cannot be carried into somebody else's session by restarting the service.
func (*LimitsManager) RestoreSessionFromHistory ¶ added in v2.15.0
func (tm *LimitsManager) RestoreSessionFromHistory(now time.Time)
RestoreSessionFromHistory reconstructs session state from recent MediaHistory entries. Must be called after CloseHangingMediaHistory so any crashed sessions are closed first. If the most recent session ended within the cooldown window, cumulative session time and cooldown state are restored so session limits survive service restarts.
func (*LimitsManager) SetBeforeExitHook ¶ added in v2.17.0
func (tm *LimitsManager) SetBeforeExitHook(hook func())
SetBeforeExitHook registers the callback run immediately before a limit stops the running game. Must be called before Start.
func (*LimitsManager) SetEnabled ¶
func (tm *LimitsManager) SetEnabled(enabled bool)
SetEnabled records the runtime enabled state and, when disabling, resets the session completely (clears cooldown and cumulative time). Whether limits are actually enforced is decided by the LimitsProvider on every check (global config, possibly overridden by the active profile) — this flag exists for its session-reset side effect when the user toggles limits off, and is kept in sync with global config by the settings handler.
func (*LimitsManager) SetLimitsProvider ¶ added in v2.16.0
func (tm *LimitsManager) SetLimitsProvider(limits LimitsProvider)
SetLimitsProvider replaces the source of limit values, e.g. with the profile-aware resolver. Must be called before Start.
func (*LimitsManager) Start ¶
func (tm *LimitsManager) Start(broker Broker, notificationsSend chan<- models.Notification)
Start begins monitoring for time limit enforcement. It subscribes to the broker to listen for media.started and media.stopped events.
type LimitsProvider ¶ added in v2.16.0
type LimitsProvider interface {
// PlaytimeLimitsEnabled reports whether limits are enforced.
PlaytimeLimitsEnabled() bool
// DailyLimit returns the daily limit, or 0 for no limit.
DailyLimit() time.Duration
// SessionLimit returns the per-session limit, or 0 for no limit.
SessionLimit() time.Duration
// WarningIntervals returns the remaining-time warning thresholds.
WarningIntervals() []time.Duration
// ActiveProfileID returns the active profile's ID, or "" when no
// profile is active. Daily usage accounting is scoped to this
// profile's attributed history; "" sums all history (device-level).
ActiveProfileID() string
}
LimitsProvider is the source of playtime limit values for the LimitsManager. The default implementation reads global config directly; the profiles service provides an implementation that layers the active profile's overrides over global config (see pkg/service/profiles.LimitsResolver).
type Rule ¶
type Rule interface {
// Evaluate checks if the current context violates this rule's time limit.
// Returns:
// - allowed: true if play can continue, false if limit reached
// - remaining: time left before limit (0 if already exceeded)
// - reason: reason for violation (e.g., "daily", "session")
Evaluate(ctx RuleContext) (bool, time.Duration, string)
}
Rule evaluates time limit policies and determines if continued play is allowed.
type RuleContext ¶
type RuleContext struct {
// CurrentTime is the current time for evaluation
CurrentTime time.Time
// SessionDuration is how long the current session has been running
SessionDuration time.Duration
// DailyUsageToday is the total time used today (including current session)
DailyUsageToday time.Duration
// ClockReliable indicates whether the system clock is trustworthy.
// False when clock appears to be unset (e.g., year < 2024) or has jumped suspiciously.
// Daily limits are only enforced when ClockReliable is true.
ClockReliable bool
}
RuleContext provides time and usage information for rule evaluation.
type SessionLimitRule ¶
SessionLimitRule enforces a maximum time per gaming session.
func (*SessionLimitRule) Evaluate ¶
func (r *SessionLimitRule) Evaluate(ctx RuleContext) (allowed bool, remaining time.Duration, reason string)
Evaluate checks if the session has exceeded the session limit.
type SessionState ¶
type SessionState int
SessionState represents the current state of a playtime session.
const ( // StateReset indicates no active session, cumulative time is zero. StateReset SessionState = iota // StateActive indicates a game is currently running and time is being tracked. StateActive // StateCooldown indicates no game is running, but session may continue if another // game launches within the session reset timeout period. StateCooldown )
func (SessionState) String ¶
func (s SessionState) String() string
String returns the string representation of the session state.
type StatusInfo ¶
type StatusInfo struct {
SessionStarted time.Time
// SessionExtendedUntil is when an active day waiver lapses, or the zero
// time when the session limit is being enforced normally.
SessionExtendedUntil time.Time
DailyUsageToday *time.Duration
DailyRemaining *time.Duration
State string
SessionDuration time.Duration
SessionCumulativeTime time.Duration
SessionRemaining time.Duration
CooldownRemaining time.Duration
// SessionExtension is the duration granted to the current session on top
// of the configured session limit. Zero when nothing was granted.
SessionExtension time.Duration
SessionActive bool
}
StatusInfo contains current playtime session and limit status.