feat(preferences): apply time preference (timezone + date format) [#198]

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 <noreply@anthropic.com>
This commit is contained in:
Ferywir
2026-06-12 14:04:36 +02:00
committed by Catalin Novgorodschi
parent af1b8c7ce7
commit 63c94fa961
10 changed files with 199 additions and 33 deletions
+130 -26
View File
@@ -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
*/
+1
View File
@@ -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
+1
View File
@@ -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
+1
View File
@@ -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
+1 -1
View File
@@ -140,7 +140,7 @@ if (!$sql || mysqli_num_rows($sql) == 0) {
$outputList .= "<td class=\"al\">" . $allyLink . "</td>";
// date column
$outputList .= "<td class=\"dat\">" . $date[0] . " " . date('H:i', $time) . "</td>";
$outputList .= "<td class=\"dat\">" . $date[0] . " " . substr($date[1], 0, 5) . "</td>";
$outputList .= "</tr>";
}
+1 -1
View File
@@ -180,7 +180,7 @@ if ($f === 31 || $f === 32) {
$outputList .= "</a></div></td>";
$outputList .= "<td class=\"al\">" . $allyName . "</td>";
$outputList .= "<td class=\"dat\">" . $date[0] . " " . date('H:i', $time) . "</td>";
$outputList .= "<td class=\"dat\">" . $date[0] . " " . substr($date[1], 0, 5) . "</td>";
$outputList .= "</tr>";
}
+1 -1
View File
@@ -68,7 +68,7 @@ function renderReports($database,$generator,$session,$d,$limit,$typeMap=null){
$icon = '<img src="img/x.gif" class="iReport iReport'.$row['ntype'].'" title="'.$row['topic'].'">';
}
$date = $generator->procMtime($row['time']);
echo '<tr><td>'.$icon.' <a href="berichte.php?id='.$row['id'].'">'.$date[0].' '.date('H:i',$row['time']).'</a></td></tr>';
echo '<tr><td>'.$icon.' <a href="berichte.php?id='.$row['id'].'">'.$date[0].' '.substr($date[1],0,5).'</a></td></tr>';
}
}
?>
-3
View File
@@ -435,9 +435,6 @@ if(isset($_POST['lang']))
<tr>
<th colspan="2">
<?php echo TZ_TIME_PREFERENCE; ?>
<span style="color:#999; font-weight:400; font-size:0.9em; font-style:italic; opacity:0.7;">
<?php echo TZ_NOT_CODED_YET; ?>
</span>
</th>
</tr>
<tr><td colspan="2"><?php echo TZ_HERE_YOU_CAN_CHANGE_TRAVIAN_S_DISP; ?></td></tr>
+1 -1
View File
@@ -231,7 +231,7 @@ if (mysqli_num_rows($getnotice) > 0) {
$date = $generator->procMtime($row2['time']);
echo '<a href="berichte.php?id='.$row2['id'].'">'
.$date[0]." ".date('H:i', $row2['time']).'</a>';
.$date[0]." ".substr($date[1],0,5).'</a>';
}
}
?>
+62
View File
@@ -242,6 +242,68 @@ $idUser = isset($_SESSION['id_user']) ? (int)$_SESSION['id_user'] : 0;
</div>
<?php
/**
* Live "local time" clock (issue #198): show a second clock next to the
* server-time one, ticking in the player's chosen timezone. The server-time
* block lives in each page's own footer (rendered after this menu), so we wait
* for the DOM and target the last #tp1 (the visible one). Vanilla JS, driven by
* Date.now() + the player's UTC offset, so it is independent of the browser
* timezone and does not touch the unx.js tp+i counters (arrival timers).
*
* Skipped entirely when the player's timezone matches the server's, so no
* redundant line is shown.
*/
$localOffset = (int) $generator->userTimeZoneOffset();
$serverOffset = (int) date('Z');
if ($localOffset !== $serverOffset):
?>
<script>
document.addEventListener('DOMContentLoaded', function () {
var anchors = document.querySelectorAll('#tp1');
if (!anchors.length) return;
var tp = anchors[anchors.length - 1];
var off = <?php echo $localOffset; ?> * 1000;
var label = <?php echo json_encode(LOCAL_TIME); ?>;
var br = document.createElement('br');
var lbl = document.createElement('span');
lbl.appendChild(document.createTextNode(label + ' '));
var val = document.createElement('span');
val.className = 'b';
var parent = tp.parentNode, next = tp.nextSibling;
parent.insertBefore(br, next);
parent.insertBefore(lbl, next);
parent.insertBefore(val, next);
// align the local-time value vertically under the server-time value
var delta = tp.offsetLeft - val.offsetLeft;
if (delta > 0) {
lbl.style.display = 'inline-block';
lbl.style.width = (lbl.offsetWidth + delta) + 'px';
}
// make room for the extra line and lift the block so it stays in the frame
var box = tp;
while (box && box.id !== 'ltime') box = box.parentNode;
if (box) {
box.style.height = 'auto';
var top = parseInt(window.getComputedStyle(box).top, 10);
if (!isNaN(top)) box.style.top = (top - 8) + 'px';
}
function p(n) { return n < 10 ? '0' + n : n; }
function tick() {
var d = new Date(Date.now() + off);
val.innerHTML = p(d.getUTCHours()) + ':' + p(d.getUTCMinutes()) + ':' + p(d.getUTCSeconds());
}
tick();
setInterval(tick, 1000);
});
</script>
<?php endif; ?>
<?php
/**
* Announcement screen