mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-10 20:47:04 +01:00
The enforcer now imports the Steam/launcher/counted-process scan from leisure_watch.procscan (v0.1.0, shared with the leisure watcher) and drops its own copies (_playtime_procs, _counted_procs, _total_block_launchers and enforcer's two scanners). Every counted-process read passes config.CONFIG_FILE explicitly, as before. Each tick also bills a shadow leisure budget beside the enforced one, from the same incoming stamp and delta clamp, before accumulate: paused (held gatelock grab or i3lock) -> shadow_paused_seconds; else a qualifying game, a "leisure" now.json, or an untrusted/missing now.json (fail closed) bills shadow_seconds once, split over shadow_per_source. shadow_budget_seconds is earned_time.resolve(leisure=True) from the same answers, held as a same-day high-water mark. Not billed under a total block. Shadow verdict changes go to budget.jsonl as shadow_change (the live game view skips them); /api/budget and MCP get_gaming_time carry a "shadow" block. Enforced fields, cutoff, warnings and existing log records are unchanged. Also fixes two tests that assumed earned_time < 0.14 internals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dsh4Znwh2KnC7q6vbGpX1N
79 lines
2.7 KiB
Python
79 lines
2.7 KiB
Python
"""Gaming-day arithmetic: which day *now* is, and what it already granted.
|
|
|
|
A gaming "day" runs 06:00-05:59 local, so late-night sessions count against
|
|
the day they started in.
|
|
|
|
There is no carry-over: unspent time is dropped at 06:00, every day. The
|
|
Friday-Monday carry ledger was removed on 2026-10-18, after a Sunday ran
|
|
past the 6.0h the i3 bar showed because 2h47m had been carried into it.
|
|
``load_state`` takes a legacy ledger's share back out of the stored
|
|
high-water mark.
|
|
|
|
Everything here is pure: it works on primitives handed to it, so it imports
|
|
nothing from the playtime modules and cannot close an import cycle.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import timedelta
|
|
from typing import TYPE_CHECKING, Final
|
|
|
|
if TYPE_CHECKING:
|
|
from datetime import datetime
|
|
|
|
from steam_backlog_enforcer._playtime_state import PlaytimeState
|
|
|
|
# The gaming day starts here, local time. 05:59 belongs to the previous day.
|
|
_DAY_BOUNDARY_HOURS: Final = 6
|
|
|
|
|
|
def gaming_day_key(now: datetime) -> str:
|
|
"""Return the ``YYYY-MM-DD`` gaming day containing *now*.
|
|
|
|
The boundary is 06:00 local, so 02:00 on the 5th belongs to the 4th.
|
|
|
|
Args:
|
|
now: Timezone-aware local timestamp.
|
|
|
|
Returns:
|
|
The gaming day key.
|
|
"""
|
|
return (now - timedelta(hours=_DAY_BOUNDARY_HOURS)).date().isoformat()
|
|
|
|
|
|
def held_today(stored: PlaytimeState | None, now: datetime) -> float:
|
|
"""Return the budget already granted earlier in *now*'s gaming day.
|
|
|
|
Earners reset at calendar midnight but the gaming day runs to 05:59, and a
|
|
cold restart starts with empty answer caches, so a live re-resolution can
|
|
come in lower than what was granted hours ago. ``rules_for`` holds the day
|
|
at this high-water mark so neither can cut a session off unwarned.
|
|
|
|
Args:
|
|
stored: The persisted production state, or ``None`` if unreadable.
|
|
now: Local timestamp for this tick.
|
|
|
|
Returns:
|
|
The recorded high-water budget; 0 when the state is another day's.
|
|
"""
|
|
if stored is None or stored.day_key != gaming_day_key(now):
|
|
return 0.0
|
|
return stored.budget_seconds
|
|
|
|
|
|
def held_shadow_today(stored: PlaytimeState | None, now: datetime) -> float:
|
|
"""Return the shadow leisure budget already granted in *now*'s gaming day.
|
|
|
|
The same high-water rule as :func:`held_today`, for ``shadow_budget_seconds``.
|
|
|
|
Args:
|
|
stored: The persisted production state, or ``None`` if unreadable.
|
|
now: Local timestamp for this tick.
|
|
|
|
Returns:
|
|
The recorded shadow high-water budget; 0 when the state is another day's.
|
|
"""
|
|
if stored is None or stored.day_key != gaming_day_key(now):
|
|
return 0.0
|
|
return stored.shadow_budget_seconds
|