array( 'YYYY-MM-DD' => seconds )`. Backs the * Steam-style "last two weeks" figure; days are bucketed in the * site's timezone and pruned past a rolling window (see * `openstation_games_playtime_history_days`). The lifetime totals * in {@see OPENSTATION_GAMES_PLAYTIME_META} are authoritative and * never pruned. * * The VALUE keeps its pre-rebrand spelling on purpose: it is a * persisted or externally-visible identifier, so renaming it would * orphan data already written by live installs (or break a live * URL). The mismatch between this constant's name and its value is * deliberate — it is NOT a half-finished rename. */ define( 'OPENSTATION_GAMES_PLAYTIME_DAYS_META', 'desktop_mode_game_playtime_days' ); /** * Today's daily-bucket key (`YYYY-MM-DD`, site timezone). * * @return string */ function openstation_games_playtime_today_key() { return current_datetime()->format( 'Y-m-d' ); } /** * Read a user's accumulated play time. * * @param int $user_id Player. * @param string $game Optional game id. Empty returns the full map. * @return int|array Seconds for one game, or the whole * `game id => seconds` map. */ function openstation_games_get_playtime( $user_id, $game = '' ) { $map = get_user_meta( (int) $user_id, OPENSTATION_GAMES_PLAYTIME_META, true ); if ( ! is_array( $map ) ) { $map = array(); } $clean = array(); foreach ( $map as $key => $seconds ) { $key = sanitize_key( (string) $key ); if ( '' === $key ) { continue; } $clean[ $key ] = max( 0, (int) $seconds ); } if ( '' !== (string) $game ) { $game = sanitize_key( (string) $game ); return isset( $clean[ $game ] ) ? $clean[ $game ] : 0; } return $clean; } /** * Read a user's daily play-time buckets. * * @param int $user_id Player. * @param string $game Optional game id. Empty returns the full map. * @return array Day buckets (`'YYYY-MM-DD' => seconds`) for one game, * or the whole `game id => buckets` map. */ function openstation_games_get_playtime_daily( $user_id, $game = '' ) { $map = get_user_meta( (int) $user_id, OPENSTATION_GAMES_PLAYTIME_DAYS_META, true ); if ( ! is_array( $map ) ) { $map = array(); } $clean = array(); foreach ( $map as $key => $days ) { $key = sanitize_key( (string) $key ); if ( '' === $key || ! is_array( $days ) ) { continue; } $clean_days = array(); foreach ( $days as $day => $seconds ) { if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', (string) $day ) ) { continue; } $clean_days[ (string) $day ] = max( 0, (int) $seconds ); } $clean[ $key ] = $clean_days; } if ( '' !== (string) $game ) { $game = sanitize_key( (string) $game ); return isset( $clean[ $game ] ) ? $clean[ $game ] : array(); } return $clean; } /** * Add seconds to a user's play-time total for a game. * * @param string $game Registered game id. * @param int $user_id Player. * @param int $seconds Seconds to add. Clamped to * `[1, openstation_games_playtime_max_increment]`. * @return int|WP_Error The new total for the game on success. */ function openstation_games_add_playtime( $game, $user_id, $seconds ) { $game = sanitize_key( (string) $game ); $user_id = (int) $user_id; $seconds = (int) $seconds; if ( ! openstation_games_is_registered( $game ) ) { return new WP_Error( 'openstation_unknown_game', __( 'Unknown game.', 'desktop-mode' ), array( 'status' => 404 ) ); } if ( $user_id <= 0 ) { return new WP_Error( 'openstation_invalid_user', __( 'A valid user is required to record play time.', 'desktop-mode' ), array( 'status' => 400 ) ); } if ( $seconds < 1 ) { return new WP_Error( 'openstation_invalid_playtime', __( 'Play time must be a positive number of seconds.', 'desktop-mode' ), array( 'status' => 400 ) ); } /** * Filter the largest play-time increment accepted in one request. * The framework flushes roughly once a minute, so anything far * past that is either a background-throttled tab catching up or a * hostile client; the clamp bounds the damage either way. * * @param int $max_seconds Default 900 (15 minutes). * @param string $game Game id. * @param int $user_id Player. */ $max = max( 1, (int) apply_filters( 'openstation_games_playtime_max_increment', 900, $game, $user_id ) ); $seconds = min( $seconds, $max ); /** * Short-circuit / veto filter for play-time recording. Return a * `WP_Error` to reject the increment (surfaced to the client), or * `null` to proceed. * * @param null|WP_Error $pre Null to proceed. * @param string $game Game id. * @param int $user_id Player. * @param int $seconds Clamped increment. */ $pre = apply_filters( 'openstation_game_playtime_pre_record', null, $game, $user_id, $seconds ); if ( is_wp_error( $pre ) ) { return $pre; } $map = openstation_games_get_playtime( $user_id ); $map[ $game ] = ( isset( $map[ $game ] ) ? $map[ $game ] : 0 ) + $seconds; update_user_meta( $user_id, OPENSTATION_GAMES_PLAYTIME_META, $map ); // Daily bucket (site timezone) for the recent-activity figure, // pruned to a rolling window so the meta row stays bounded. The // lifetime total above is the source of truth and never shrinks. $today = openstation_games_playtime_today_key(); /** * Filter how many days of daily play-time buckets are retained. * The Games hub needs 14 for its "last two weeks" figure. * * @param int $days Default 30. */ $window = max( 1, (int) apply_filters( 'openstation_games_playtime_history_days', 30 ) ); $cutoff = current_datetime()->modify( '-' . ( $window - 1 ) . ' days' )->format( 'Y-m-d' ); $daily = openstation_games_get_playtime_daily( $user_id ); $days = isset( $daily[ $game ] ) ? $daily[ $game ] : array(); $days[ $today ] = ( isset( $days[ $today ] ) ? $days[ $today ] : 0 ) + $seconds; foreach ( array_keys( $days ) as $day ) { // `Y-m-d` sorts lexicographically, so string compare suffices. if ( $day < $cutoff ) { unset( $days[ $day ] ); } } $daily[ $game ] = $days; update_user_meta( $user_id, OPENSTATION_GAMES_PLAYTIME_DAYS_META, $daily ); /** * Fires after a play-time increment is recorded. * * @param string $game Game id. * @param int $user_id Player. * @param int $seconds The recorded increment. * @param int $total The user's new total for the game. */ do_action( 'openstation_game_playtime_recorded', $game, $user_id, $seconds, $map[ $game ] ); return $map[ $game ]; }