diff --git a/Admin/Templates/debug_log.tpl b/Admin/Templates/debug_log.tpl
new file mode 100644
index 00000000..f1b0e921
--- /dev/null
+++ b/Admin/Templates/debug_log.tpl
@@ -0,0 +1,146 @@
+
+
+
+ - TravianZ
+
+
+
+
+
+
+
+getDebugMode();
+$isOn = !empty($cfg['active']);
+
+// Resolve project root (max 5 levels up) to find the log file.
+$autoprefix = '';
+for ($i = 0; $i < 5; $i++) {
+ $autoprefix = str_repeat('../', $i);
+ if (file_exists($autoprefix . 'autoloader.php')) {
+ break;
+ }
+}
+$logFile = $autoprefix . 'var/log/debug-players.log';
+
+// Read the last lines for the on-screen viewer.
+$maxLines = 400;
+$lines = [];
+$logSize = 0;
+if (is_file($logFile)) {
+ $logSize = filesize($logFile);
+ $all = file($logFile, FILE_IGNORE_NEW_LINES);
+ if ($all !== false) {
+ $lines = array_slice($all, -$maxLines);
+ }
+}
+
+// Active-since label + auto-off info.
+$since = !empty($cfg['started_at']) ? date('d.m.Y H:i', $cfg['started_at']) : '-';
+$autoOff = (int)($cfg['auto_off_hours'] ?? 0);
+?>
+
+
+
+
🐞 Debug Error Log
+
+
+
+
+
+
+
Status
+
+ Capture is .
+ Auto-off: 0 ? $autoOff.' h' : 'never'; ?>
+ Log size: KB
+
+
+
Transparent to players: errors are only written to the log file, never shown in-game and gameplay is unaffected.
+
+
+
+
+
+
+
diff --git a/Admin/admin.php b/Admin/admin.php
index 33ed821c..19f1a1f0 100644
--- a/Admin/admin.php
+++ b/Admin/admin.php
@@ -147,6 +147,10 @@ if (!empty($_GET['p'])) {
$subpage = 'Server Settings';
break;
+ case 'debug_log':
+ $subpage = 'Debug Error Log';
+ break;
+
case 'editServerSet':
$subpage = 'Server Configuration';
break;
@@ -622,6 +626,7 @@ body.app #lmid3 a{color:#15803d !important;font-weight:600 !important;}
Admin
- Admin Log
+ - Debug Error Log
- Server Settings
- Server Maintenance
- Server Resetting
diff --git a/GameEngine/Admin/Mods/debugLog.php b/GameEngine/Admin/Mods/debugLog.php
new file mode 100644
index 00000000..d6b35e59
--- /dev/null
+++ b/GameEngine/Admin/Mods/debugLog.php
@@ -0,0 +1,75 @@
+ persist capture settings (levels, size cap, auto-off) ##
+## do=toggle -> turn the debug capture on/off ##
+## do=clear -> empty the log file(s) ##
+## do=download -> stream the log file as a download ##
+#################################################################################
+
+if(!isset($_SESSION)) session_start();
+if(($_SESSION['access'] ?? 0) < 9) die("Access denied: You are not Admin!");
+
+include_once("../../Database.php");
+
+// Resolve project root (max 5 levels up), like the rest of the codebase.
+$autoprefix = '';
+for ($i = 0; $i < 5; $i++) {
+ $autoprefix = str_repeat('../', $i);
+ if (file_exists($autoprefix . 'autoloader.php')) {
+ break;
+ }
+}
+$logFile = $autoprefix . 'var/log/debug-players.log';
+
+$uid = (int)($_SESSION['id_user'] ?? 0);
+$do = $_REQUEST['do'] ?? '';
+
+switch ($do) {
+
+ case 'save':
+ $database->setDebugSettings(
+ isset($_POST['lvl_warning']),
+ isset($_POST['lvl_notice']),
+ isset($_POST['lvl_deprecated']),
+ isset($_POST['lvl_fatal']),
+ $_POST['max_size_mb'] ?? 5,
+ $_POST['auto_off_hours'] ?? 6
+ );
+ $database->query("Insert into ".TB_PREFIX."admin_log values (0,".$uid.",'Changed Debug Error Log settings',".time().")");
+ break;
+
+ case 'toggle':
+ $active = (int)($_POST['active'] ?? 0);
+ $database->setDebugMode($active, $uid);
+ $database->query("Insert into ".TB_PREFIX."admin_log values (0,".$uid.",'".($active ? 'Enabled' : 'Disabled')." Debug Error Log',".time().")");
+ break;
+
+ case 'clear':
+ @file_put_contents($logFile, '');
+ @unlink($logFile . '.1');
+ $database->query("Insert into ".TB_PREFIX."admin_log values (0,".$uid.",'Cleared Debug Error Log',".time().")");
+ break;
+
+ case 'download':
+ if (is_file($logFile) && filesize($logFile) > 0) {
+ header('Content-Type: text/plain; charset=UTF-8');
+ header('Content-Disposition: attachment; filename="debug-players-'.date('Ymd-His').'.log"');
+ header('Content-Length: ' . filesize($logFile));
+ readfile($logFile);
+ } else {
+ header('Content-Type: text/plain; charset=UTF-8');
+ echo "The debug log is empty.";
+ }
+ exit;
+}
+
+header("Location: ../../../Admin/admin.php?p=debug_log");
+exit;
diff --git a/GameEngine/Database.php b/GameEngine/Database.php
index d294f4e9..45fae4fe 100755
--- a/GameEngine/Database.php
+++ b/GameEngine/Database.php
@@ -8878,12 +8878,70 @@ public function setMaintenance($active, $uid=0) {
$active = (int)$active;
$uid = (int)$uid;
// REPLACE creează rândul dacă nu există
- return $this->query("REPLACE INTO ".TB_PREFIX."maintenance
- (id, active, started_by, started_at)
+ return $this->query("REPLACE INTO ".TB_PREFIX."maintenance
+ (id, active, started_by, started_at)
VALUES (1, $active, $uid, $time)");
}
-
+/**
+ * Debug error-log mode (admin-controlled, transparent to players).
+ * Returns the single config row, falling back to safe defaults when the
+ * table does not exist yet (so deploying the code before creating the table
+ * never produces a blank page).
+ */
+public function getDebugMode() {
+ $default = [
+ 'active' => 0,
+ 'lvl_warning' => 1,
+ 'lvl_notice' => 1,
+ 'lvl_deprecated' => 1,
+ 'lvl_fatal' => 1,
+ 'max_size_mb' => 5,
+ 'auto_off_hours' => 6,
+ 'started_by' => null,
+ 'started_at' => null,
+ ];
+ try {
+ $res = @mysqli_query($this->dblink, "SELECT * FROM ".TB_PREFIX."debug_log WHERE id=1 LIMIT 1");
+ if (!$res) {
+ return $default;
+ }
+ $row = mysqli_fetch_assoc($res);
+ return $row ?: $default;
+ } catch (\Throwable $e) {
+ return $default;
+ }
+}
+
+/**
+ * Toggle the debug mode on/off (stamps who/when on activation).
+ */
+public function setDebugMode($active, $uid = 0) {
+ $active = (int)$active;
+ $uid = (int)$uid;
+ $time = time();
+ return $this->query("UPDATE ".TB_PREFIX."debug_log
+ SET active = $active, started_by = $uid, started_at = $time
+ WHERE id = 1");
+}
+
+/**
+ * Persist the debug capture parameters (levels, size cap, auto-off window).
+ */
+public function setDebugSettings($warning, $notice, $deprecated, $fatal, $maxSizeMb, $autoOffHours) {
+ $warning = $warning ? 1 : 0;
+ $notice = $notice ? 1 : 0;
+ $deprecated = $deprecated ? 1 : 0;
+ $fatal = $fatal ? 1 : 0;
+ $maxSizeMb = max(1, (int)$maxSizeMb);
+ $autoOffHours = max(0, (int)$autoOffHours);
+ return $this->query("UPDATE ".TB_PREFIX."debug_log
+ SET lvl_warning = $warning, lvl_notice = $notice, lvl_deprecated = $deprecated,
+ lvl_fatal = $fatal, max_size_mb = $maxSizeMb, auto_off_hours = $autoOffHours
+ WHERE id = 1");
+}
+
+
/**
* Changed the actual capital with a new one
*
diff --git a/GameEngine/Lang/en.php b/GameEngine/Lang/en.php
index c7386ab3..24a92d1f 100755
--- a/GameEngine/Lang/en.php
+++ b/GameEngine/Lang/en.php
@@ -1932,6 +1932,8 @@ tz_def('TZ_CONSTRUCT_WAREHOUSE', 'Construct Warehouse.');
tz_def('TZ_CP_DAY', 'CP/day');
tz_def('TZ_CROP_25_5_GOLD', 'Crop +25% (5 gold)');
tz_def('TZ_DATE_AND_TIME', 'Date and time');
+tz_def('TZ_DEBUG_OFF', 'Debug log OFF');
+tz_def('TZ_DEBUG_ON', 'Debug log ON');
tz_def('TZ_DECLARE_WAR', 'declare war');
tz_def('TZ_DEFAULT', 'Default:');
tz_def('TZ_DELETE_ACCOUNT', 'Delete account?');
diff --git a/GameEngine/Lang/fr.php b/GameEngine/Lang/fr.php
index b8560dd1..0c173ec4 100644
--- a/GameEngine/Lang/fr.php
+++ b/GameEngine/Lang/fr.php
@@ -1931,6 +1931,8 @@ define('TZ_CONSTRUCT_WAREHOUSE', 'Construisez un entrepôt.');
define('TZ_CP_DAY', 'PC/jour');
define('TZ_CROP_25_5_GOLD', 'Céréales +25% (5 or)');
define('TZ_DATE_AND_TIME', 'Date et heure');
+define('TZ_DEBUG_OFF', 'Journal debug DÉSACTIVÉ');
+define('TZ_DEBUG_ON', 'Journal debug ACTIVÉ');
define('TZ_DECLARE_WAR', 'déclarer la guerre');
define('TZ_DEFAULT', 'Par défaut :');
define('TZ_DELETE_ACCOUNT', 'Supprimer le compte ?');
diff --git a/GameEngine/Lang/ro.php b/GameEngine/Lang/ro.php
index eedd5c0a..99e85ae9 100644
--- a/GameEngine/Lang/ro.php
+++ b/GameEngine/Lang/ro.php
@@ -1926,6 +1926,8 @@ define('TZ_CONSTRUCT_WAREHOUSE', 'Construiește un depozit.');
define('TZ_CP_DAY', 'PC/zi');
define('TZ_CROP_25_5_GOLD', 'Grâne +25% (5 aur)');
define('TZ_DATE_AND_TIME', 'Data și ora');
+define('TZ_DEBUG_OFF', 'Jurnal debug OPRIT');
+define('TZ_DEBUG_ON', 'Jurnal debug PORNIT');
define('TZ_DECLARE_WAR', 'declară război');
define('TZ_DEFAULT', 'Implicit:');
define('TZ_DELETE_ACCOUNT', 'Ștergi contul?');
diff --git a/GameEngine/Session.php b/GameEngine/Session.php
index be9c20ef..4d00a052 100755
--- a/GameEngine/Session.php
+++ b/GameEngine/Session.php
@@ -129,6 +129,21 @@ function __construct() {
}
}
+ // === DEBUG ERROR LOG (admin-controlled, transparent to players) ===
+ // When enabled from the admin panel, capture the selected PHP errors of
+ // every player into var/log/debug-players.log. Auto-disables itself after
+ // the configured window so a forgotten debug session cannot run forever.
+ $dbg = $database->getDebugMode();
+ if (!empty($dbg['active'])) {
+ $autoOff = (int)($dbg['auto_off_hours'] ?? 0);
+ if ($autoOff > 0 && !empty($dbg['started_at'])
+ && ($dbg['started_at'] + $autoOff * 3600) < $this->time) {
+ $database->setDebugMode(0, $this->uid ?? 0);
+ } else {
+ \App\Utils\DebugErrorLogger::enable($dbg, $this->uid ?? 0, $this->username ?? '');
+ }
+ }
+
$this->referrer = $_SESSION['url'] ?? "/";
$this->url = $_SESSION['url'] = $_SERVER['PHP_SELF'];
diff --git a/Templates/debug_status.tpl b/Templates/debug_status.tpl
new file mode 100644
index 00000000..fb2636af
--- /dev/null
+++ b/Templates/debug_status.tpl
@@ -0,0 +1,49 @@
+setDebugMode($newState, $session->uid);
+ // redirect to clean the URL
+ $cleanUrl = strtok($_SERVER["REQUEST_URI"], '?');
+ header("Location: $cleanUrl");
+ exit;
+ }
+
+ $dbg = $database->getDebugMode();
+
+ if(!empty($dbg['active'])) {
+ $started = !empty($dbg['started_at']) ? date('H:i d.m.Y', $dbg['started_at']) : '-';
+ ?>
+
+
+
+
+
+
+
+
diff --git a/Templates/menu.tpl b/Templates/menu.tpl
index 8be9ab80..e9f1f94a 100644
--- a/Templates/menu.tpl
+++ b/Templates/menu.tpl
@@ -195,6 +195,11 @@ $idUser = isset($_SESSION['id_user']) ? (int)$_SESSION['id_user'] : 0;
*/
include("Templates/maintenance_status.tpl");
+ /**
+ * Debug Error Log quick toggle for admins
+ */
+ include("Templates/debug_status.tpl");
+
?>
diff --git a/src/Utils/DebugErrorLogger.php b/src/Utils/DebugErrorLogger.php
new file mode 100644
index 00000000..e1be7caf
--- /dev/null
+++ b/src/Utils/DebugErrorLogger.php
@@ -0,0 +1,176 @@
+ Which severities to capture. */
+ private static $capture = [];
+
+ /** @var string Per-request context appended to every line. */
+ private static $context = '';
+
+ /** @var bool Guards against double registration. */
+ private static $registered = false;
+
+ /**
+ * Turn the capture on for the current request.
+ *
+ * @param array $cfg The debug_log config row (levels + max_size_mb).
+ * @param int $uid Current player id (0 when not logged in).
+ * @param string $name Current player username (for context).
+ */
+ public static function enable(array $cfg, $uid = 0, $name = '') {
+ if (self::$registered) {
+ return;
+ }
+ self::$registered = true;
+
+ // Resolve the project root (max 5 levels up), like AccessLogger.
+ $autoprefix = '';
+ for ($i = 0; $i < 5; $i++) {
+ $autoprefix = str_repeat('../', $i);
+ if (file_exists($autoprefix . 'autoloader.php')) {
+ break;
+ }
+ }
+
+ self::$file = $autoprefix . 'var/log/debug-players.log';
+ self::$maxBytes = max(1, (int)($cfg['max_size_mb'] ?? 5)) * 1024 * 1024;
+
+ self::$capture = [
+ 'WARNING' => !empty($cfg['lvl_warning']),
+ 'NOTICE' => !empty($cfg['lvl_notice']),
+ 'DEPRECATED' => !empty($cfg['lvl_deprecated']),
+ 'FATAL' => !empty($cfg['lvl_fatal']),
+ ];
+
+ $page = $_SERVER['PHP_SELF'] ?? 'cli';
+ self::$context = 'uid=' . (int)$uid
+ . ($name !== '' ? ' (' . $name . ')' : '')
+ . ' | page=' . $page;
+
+ set_error_handler([self::class, 'handleError']);
+ register_shutdown_function([self::class, 'handleShutdown']);
+ }
+
+ /**
+ * Map a PHP error constant to one of our four severity buckets.
+ */
+ private static function bucket($errno) {
+ switch ($errno) {
+ case E_WARNING:
+ case E_USER_WARNING:
+ case E_CORE_WARNING:
+ case E_COMPILE_WARNING:
+ return 'WARNING';
+ case E_NOTICE:
+ case E_USER_NOTICE:
+ return 'NOTICE';
+ case E_DEPRECATED:
+ case E_USER_DEPRECATED:
+ return 'DEPRECATED';
+ default:
+ // E_ERROR, E_PARSE, E_RECOVERABLE_ERROR, E_USER_ERROR, ...
+ return 'FATAL';
+ }
+ }
+
+ /**
+ * Runtime error handler. Returns false so PHP's default handling still
+ * runs (respecting display_errors/error_reporting), keeping behaviour
+ * unchanged. Suppressed errors (the @ operator) are ignored on purpose.
+ */
+ public static function handleError($errno, $errstr, $errfile = '', $errline = 0) {
+ // Respect the @ operator: in PHP 8 error_reporting() returns 0 inside
+ // the handler for a suppressed error.
+ if (error_reporting() === 0) {
+ return false;
+ }
+
+ $bucket = self::bucket($errno);
+ if (!empty(self::$capture[$bucket])) {
+ self::write($bucket, $errstr, $errfile, $errline);
+ }
+
+ return false;
+ }
+
+ /**
+ * Shutdown handler: catches fatal errors that the error handler cannot
+ * (E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR).
+ */
+ public static function handleShutdown() {
+ if (empty(self::$capture['FATAL'])) {
+ return;
+ }
+
+ $err = error_get_last();
+ if ($err === null) {
+ return;
+ }
+
+ $fatal = E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR;
+ if (($err['type'] & $fatal) === 0) {
+ return;
+ }
+
+ self::write('FATAL', $err['message'], $err['file'], $err['line']);
+ }
+
+ /**
+ * Append one formatted line, rotating the file first if it grew past the
+ * size cap. Never throws — logging must not break the game.
+ */
+ private static function write($severity, $message, $file, $line) {
+ try {
+ if (self::$file === null) {
+ return;
+ }
+
+ // Size-cap rotation: keep one ".1" backup, then start fresh.
+ if (@file_exists(self::$file) && @filesize(self::$file) >= self::$maxBytes) {
+ @rename(self::$file, self::$file . '.1');
+ }
+
+ $message = str_replace(["\r", "\n"], ' ', (string)$message);
+ $entry = '[' . date('Y-m-d H:i:s') . '] '
+ . $severity . ': ' . $message
+ . ' in ' . $file . ':' . $line
+ . ' | ' . self::$context . "\n";
+
+ @file_put_contents(self::$file, $entry, FILE_APPEND | LOCK_EX);
+ } catch (\Throwable $e) {
+ // swallow: a logging failure must never surface to players
+ }
+ }
+}
diff --git a/var/db/struct.sql b/var/db/struct.sql
index 6cc55bc1..6897a85d 100644
--- a/var/db/struct.sql
+++ b/var/db/struct.sql
@@ -1828,3 +1828,28 @@ CREATE TABLE IF NOT EXISTS `%PREFIX%maintenance` (
--
INSERT INTO `%PREFIX%maintenance` (`id`, `active`, `message`, `started_by`, `started_at`) VALUES
(1, 0, 'Server in maintenance', NULL, NULL);
+
+--
+-- Table structure for table `%PREFIX%debug_log`
+-- Admin-controlled PHP error capture (transparent to players). One row (id=1).
+--
+
+CREATE TABLE IF NOT EXISTS `%PREFIX%debug_log` (
+ `id` tinyint(1) NOT NULL DEFAULT 1,
+ `active` tinyint(1) NOT NULL DEFAULT 0,
+ `lvl_warning` tinyint(1) NOT NULL DEFAULT 1,
+ `lvl_notice` tinyint(1) NOT NULL DEFAULT 1,
+ `lvl_deprecated` tinyint(1) NOT NULL DEFAULT 1,
+ `lvl_fatal` tinyint(1) NOT NULL DEFAULT 1,
+ `max_size_mb` int(11) NOT NULL DEFAULT 5,
+ `auto_off_hours` int(11) NOT NULL DEFAULT 6,
+ `started_by` int(11) DEFAULT NULL,
+ `started_at` int(11) DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+--
+-- Dumping data for table `%PREFIX%debug_log`
+--
+INSERT INTO `%PREFIX%debug_log` (`id`, `active`, `lvl_warning`, `lvl_notice`, `lvl_deprecated`, `lvl_fatal`, `max_size_mb`, `auto_off_hours`, `started_by`, `started_at`) VALUES
+(1, 0, 1, 1, 1, 1, 5, 6, NULL, NULL);