From 63c94fa9610ad83c5a96780e53a6d2c9189ff7b1 Mon Sep 17 00:00:00 2001 From: Ferywir Date: Fri, 12 Jun 2026 14:04:36 +0200 Subject: [PATCH] feat(preferences): apply time preference (timezone + date format) [#198] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the "Time Preference" section of the player preferences (issue #198), which until now was stored in DB but never applied in-game. procMtime() — the central date/time formatter used across reports, messages, notices and troop movements — now honours the per-user settings: - timezone: the stored value is mapped to a DateTimeZone, either a named DST-aware region (Europe, UK, Turkey, Kolkata, Bangkok, New York, Chicago, New Zealand) or a fixed UTC offset (general zones); timestamps are converted before formatting. - tformat: drives the date layout and 12h/24h clock (0=EU dd.mm.yy 24h, 1=US mm/dd/yy 12h, 2=UK dd/mm/yy 12h, 3=ISO yy/mm/dd 24h). tformat 0 keeps the previous EU output unchanged. The today/yesterday sentinels are preserved (callers compare them) and are computed in the player's timezone. The time-only path (mode 9) stays 24h H:i:s because it feeds the live JS clock counters (unx.js), which parse "H:i:s" by splitting on ":". A few alliance/report/farm-list rows that combined procMtime's day part with a raw date('H:i') now take the time from the same procMtime result, to avoid mixing timezones. A second "local time" clock is shown next to the server-time one: a small vanilla-JS snippet emitted once from menu.tpl finds the visible #tp1 clock and appends a row ticking in the player's timezone (Date.now() + UTC offset), independent of the browser timezone and of the unx.js counters. It aligns the value under the server time, is skipped when the player's timezone matches the server's, and lets the clock box grow so the extra row fits the frame. Adds the LOCAL_TIME string (EN/FR/RO). When no player session is available (admin / pre-login) procMtime falls back to the server timezone and EU layout, i.e. the previous behaviour. Removes the last "not coded yet" tag from the preferences form. Co-Authored-By: Claude Opus 4.8 --- GameEngine/Generator.php | 156 ++++++++++++++++++++----- GameEngine/Lang/en.php | 1 + GameEngine/Lang/fr.php | 1 + GameEngine/Lang/ro.php | 1 + Templates/Alliance/attack-filtered.tpl | 2 +- Templates/Alliance/attacks.tpl | 2 +- Templates/Map/vilview.tpl | 2 +- Templates/Profile/preference.tpl | 3 - Templates/goldClub/farmlist.tpl | 2 +- Templates/menu.tpl | 62 ++++++++++ 10 files changed, 199 insertions(+), 33 deletions(-) diff --git a/GameEngine/Generator.php b/GameEngine/Generator.php index 25341ccb..2f54fe7d 100755 --- a/GameEngine/Generator.php +++ b/GameEngine/Generator.php @@ -136,45 +136,149 @@ class MyGenerator } /** - * Format timestamp into readable date/time + * Resolve a player's "timezone" preference (issue #198) to a DateTimeZone. + * + * The preference form exposes two kinds of values: + * - 8 named, DST-aware regions (Europe, UK, Turkey, Kolkata, Bangkok, + * New York, Chicago, New Zealand) stored as their legacy magic ids; + * - fixed UTC offsets (general zones): 0..11 => UTC+1..UTC+12, + * 12..22 => UTC-11..UTC-1, 23 => UTC. + * Falls back to the server's default timezone when the value is unknown. + * + * @param int|null $tz Stored timezone preference value (null = server time). + * @return DateTimeZone + */ + private function resolveUserTimeZone($tz) + { + if ($tz === null) { + return new DateTimeZone(date_default_timezone_get()); + } + + $tz = (int) $tz; + + $named = [ + 495 => 'Europe/Berlin', + 99 => 'Europe/London', + 492 => 'Europe/Istanbul', + 328 => 'Asia/Kolkata', + 345 => 'Asia/Bangkok', + 257 => 'America/New_York', + 189 => 'America/Chicago', + 474 => 'Pacific/Auckland', + ]; + + try { + if (isset($named[$tz])) { + return new DateTimeZone($named[$tz]); + } + + if ($tz === 23) { + $offset = 0; + } elseif ($tz >= 0 && $tz <= 11) { + $offset = $tz + 1; + } elseif ($tz >= 12 && $tz <= 22) { + $offset = $tz - 23; + } else { + $offset = 0; + } + + $sign = $offset >= 0 ? '+' : '-'; + return new DateTimeZone(sprintf('%s%02d:00', $sign, abs($offset))); + } catch (\Exception $e) { + return new DateTimeZone(date_default_timezone_get()); + } + } + + /** + * Format an absolute timestamp into a readable date/time, honouring the + * current player's time preference (issue #198): timezone conversion + + * date layout / 12h-24h clock derived from the "tformat" setting. + * + * tformat 0 => EU dd.mm.yy 24h (legacy default, unchanged) + * tformat 1 => US mm/dd/yy 12h + * tformat 2 => UK dd/mm/yy 12h + * tformat 3 => ISO yy/mm/dd 24h + * + * When no player session is available (e.g. admin / pre-login) it falls + * back to the server timezone and the EU layout, i.e. the previous output. + * The "today"/"yesterday" sentinels are kept verbatim (callers compare them). + * + * @param int $time Unix timestamp. + * @param int $pref Pass 9 to get the time-of-day string only. + * @return array|string [$day, $time] normally, or the time string when $pref == 9. */ public function procMtime($time, $pref = 3) { - $time = (int) $time; - $time += 0; // placeholder for timezone adjustments + global $session; - $today = date('d', time()) - 1; + $time = (int) $time; - if (date('Ymd', time()) == date('Ymd', $time)) { - $day = "today"; - } elseif ($today == date('d', $time)) { - $day = "yesterday"; - } else { - switch ($pref) { - case 1: - $day = date("m/j/y", $time); - break; - case 2: - $day = date("j/m/y", $time); - break; - case 3: - $day = date("j.m.y", $time); - break; - default: - $day = date("y/m/j", $time); - break; - } + $tzPref = (isset($session) && isset($session->userinfo['timezone'])) + ? $session->userinfo['timezone'] : null; + $tformat = (isset($session) && isset($session->userinfo['tformat'])) + ? (int) $session->userinfo['tformat'] : 0; + + $zone = $this->resolveUserTimeZone($tzPref); + + $dt = new DateTime('@' . $time); + $dt->setTimezone($zone); + $now = new DateTime('now', $zone); + + // date layout + clock (12h/24h) per the tformat preference + switch ($tformat) { + case 1: $dateFmt = "m/d/y"; $timeFmt = "h:i:s A"; break; + case 2: $dateFmt = "d/m/y"; $timeFmt = "h:i:s A"; break; + case 3: $dateFmt = "y/m/d"; $timeFmt = "H:i:s"; break; + default: $dateFmt = "j.m.y"; $timeFmt = "H:i:s"; break; // legacy EU output, unchanged } - $new = date("H:i:s", $time); - + // Time-only mode (9) feeds the live JS clock counters (unx.js ob()/rb()), + // which parse "H:i:s" by splitting on ":" — keep 24h here regardless of + // the 12h/24h tformat, while still honouring the timezone conversion. if ($pref == 9) { - return $new; + return $dt->format("H:i:s"); + } + + $new = $dt->format($timeFmt); + + $yesterday = (clone $now)->modify('-1 day'); + + if ($dt->format('Ymd') == $now->format('Ymd')) { + $day = "today"; + } elseif ($dt->format('Ymd') == $yesterday->format('Ymd')) { + $day = "yesterday"; + } else { + $day = $dt->format($dateFmt); } return [$day, $new]; } + /** + * Resolve the current player's timezone preference (issue #198), shared by + * the "local time" header clock helpers below. + */ + private function currentPlayerZone() + { + global $session; + + $tzPref = (isset($session) && isset($session->userinfo['timezone'])) + ? $session->userinfo['timezone'] : null; + + return $this->resolveUserTimeZone($tzPref); + } + + /** + * Current UTC offset (in seconds, DST-aware) of the player's timezone. + * Feeds the live "local time" clock in the page header (issue #198). + * + * @return int + */ + public function userTimeZoneOffset() + { + return (new DateTime('now', $this->currentPlayerZone()))->getOffset(); + } + /** * Convert map coordinates to base ID */ diff --git a/GameEngine/Lang/en.php b/GameEngine/Lang/en.php index 7d4dc61b..c7386ab3 100755 --- a/GameEngine/Lang/en.php +++ b/GameEngine/Lang/en.php @@ -152,6 +152,7 @@ tz_def('HI', 'HI'); tz_def('P_IN', 'in'); tz_def('MS', 'ms'); tz_def('SERVER_TIME', 'Server time:'); +tz_def('LOCAL_TIME', 'Local time:'); tz_def('REMAINING_GOLD', 'Remaining gold'); // HEADER && MENU && Messages && Reports diff --git a/GameEngine/Lang/fr.php b/GameEngine/Lang/fr.php index 31ce8fe8..b8560dd1 100644 --- a/GameEngine/Lang/fr.php +++ b/GameEngine/Lang/fr.php @@ -149,6 +149,7 @@ define('HI', 'Bonjour'); define('P_IN', 'dans'); define('MS', 'ms'); define('SERVER_TIME', 'Heure du serveur :'); +define('LOCAL_TIME', 'Heure locale :'); define('REMAINING_GOLD', 'Or restant'); // HEADER && MENU && Messages && Reports diff --git a/GameEngine/Lang/ro.php b/GameEngine/Lang/ro.php index 04e68b32..eedd5c0a 100644 --- a/GameEngine/Lang/ro.php +++ b/GameEngine/Lang/ro.php @@ -149,6 +149,7 @@ define('HI', 'Salut'); define('P_IN', 'în'); define('MS', 'ms'); define('SERVER_TIME', 'Ora serverului:'); +define('LOCAL_TIME', 'Ora locală:'); define('REMAINING_GOLD', 'Aur rămas'); // HEADER && MENU && Messages && Reports diff --git a/Templates/Alliance/attack-filtered.tpl b/Templates/Alliance/attack-filtered.tpl index b9a80c4b..c7ddf11d 100644 --- a/Templates/Alliance/attack-filtered.tpl +++ b/Templates/Alliance/attack-filtered.tpl @@ -140,7 +140,7 @@ if (!$sql || mysqli_num_rows($sql) == 0) { $outputList .= "" . $allyLink . ""; // date column - $outputList .= "" . $date[0] . " " . date('H:i', $time) . ""; + $outputList .= "" . $date[0] . " " . substr($date[1], 0, 5) . ""; $outputList .= ""; } diff --git a/Templates/Alliance/attacks.tpl b/Templates/Alliance/attacks.tpl index 1f7a4652..eeadd735 100644 --- a/Templates/Alliance/attacks.tpl +++ b/Templates/Alliance/attacks.tpl @@ -180,7 +180,7 @@ if ($f === 31 || $f === 32) { $outputList .= ""; $outputList .= "" . $allyName . ""; - $outputList .= "" . $date[0] . " " . date('H:i', $time) . ""; + $outputList .= "" . $date[0] . " " . substr($date[1], 0, 5) . ""; $outputList .= ""; } diff --git a/Templates/Map/vilview.tpl b/Templates/Map/vilview.tpl index aaec8028..a1a9ec49 100644 --- a/Templates/Map/vilview.tpl +++ b/Templates/Map/vilview.tpl @@ -68,7 +68,7 @@ function renderReports($database,$generator,$session,$d,$limit,$typeMap=null){ $icon = ''; } $date = $generator->procMtime($row['time']); - echo ''.$icon.' '.$date[0].' '.date('H:i',$row['time']).''; + echo ''.$icon.' '.$date[0].' '.substr($date[1],0,5).''; } } ?> diff --git a/Templates/Profile/preference.tpl b/Templates/Profile/preference.tpl index 8fdf4c3e..2e83fa3d 100644 --- a/Templates/Profile/preference.tpl +++ b/Templates/Profile/preference.tpl @@ -435,9 +435,6 @@ if(isset($_POST['lang'])) - - - diff --git a/Templates/goldClub/farmlist.tpl b/Templates/goldClub/farmlist.tpl index 7a9ce560..49eeabfc 100644 --- a/Templates/goldClub/farmlist.tpl +++ b/Templates/goldClub/farmlist.tpl @@ -231,7 +231,7 @@ if (mysqli_num_rows($getnotice) > 0) { $date = $generator->procMtime($row2['time']); echo '' - .$date[0]." ".date('H:i', $row2['time']).''; + .$date[0]." ".substr($date[1],0,5).''; } } ?> diff --git a/Templates/menu.tpl b/Templates/menu.tpl index 71f6146e..8be9ab80 100644 --- a/Templates/menu.tpl +++ b/Templates/menu.tpl @@ -242,6 +242,68 @@ $idUser = isset($_SESSION['id_user']) ? (int)$_SESSION['id_user'] : 0; +userTimeZoneOffset(); +$serverOffset = (int) date('Z'); +if ($localOffset !== $serverOffset): +?> + + +