playtime

package
v2.17.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 31, 2026 License: GPL-3.0 Imports: 18 Imported by: 0

Documentation

Index

Constants

View Source
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
)
View Source
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

View Source
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")
	// ErrGrantUnavailable is returned when the grant cannot be persisted, so
	// 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.

View Source
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

type DailyLimitRule struct {
	Limit time.Duration
}

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.

func (*LimitsManager) Stop

func (tm *LimitsManager) Stop()

Stop shuts down the LimitsManager.

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

type SessionLimitRule struct {
	Limit time.Duration
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL