Add Cron Jobs for Automation

Add Cron Jobs for Automation
This commit is contained in:
novgorodschi catalin
2026-07-23 09:43:16 +03:00
parent da4f203aa1
commit 6f80be1f4c
10 changed files with 776 additions and 128 deletions
+89
View File
@@ -95,6 +95,95 @@ $editIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke=
</table>
</div>
<!-- CRON / AUTOMATION -->
<?php
// Starea reala a cron-ului, citita din markerul scris de cron.php la fiecare tick.
// Pragul de 90s e acelasi cu cel din GameEngine/Village.php: sub el, paginile
// jucatorilor NU mai declanseaza automation (il face cron-ul); peste el, paginile
// preiau automat ca fallback.
$cronMarkerFile = __DIR__ . '/../../GameEngine/Prevention/cron_active.txt';
$cronLastRun = @is_file($cronMarkerFile) ? (int) @file_get_contents($cronMarkerFile) : 0;
$cronAge = $cronLastRun > 0 ? (time() - $cronLastRun) : -1;
$cronActive = ($cronAge >= 0 && $cronAge < 90);
$cronLoop = defined('CRON_LOOP_SECONDS') ? (int) CRON_LOOP_SECONDS : 300;
$cronTick = defined('CRON_TICK_SECONDS') ? (int) CRON_TICK_SECONDS : 60;
$cronKey = defined('CRON_KEY') ? (string) CRON_KEY : '';
$cronBase = defined('HOMEPAGE') ? rtrim(HOMEPAGE, '/') : '';
$cronUrl = ($cronBase !== '' && $cronKey !== '') ? $cronBase . '/cron.php?key=' . $cronKey : '';
$cronPath = realpath(__DIR__ . '/../../cron.php');
$cronCmd = $cronPath ? '*/5 * * * * /usr/local/bin/php ' . $cronPath . ' >/dev/null 2>&1' : '';
// cheia afisata mascat; se dezvaluie la cerere (evita expunerea in screenshot-uri)
$cronKeyMasked = ($cronKey === '')
? ''
: (strlen($cronKey) > 12 ? substr($cronKey, 0, 6) . str_repeat('&bull;', 8) . substr($cronKey, -4) : str_repeat('&bull;', 8));
?>
<div class="config-card">
<div class="config-head"><span>Cron &amp; Automation</span><a href="admin.php?p=editCronSet" title="Edit Cron &amp; Automation" class="edit-btn"><?php echo $editIcon; ?></a></div>
<table class="config-table">
<tr><td class="b"><?php echo SERV_VARIABLE ?></td><td class="b"><?php echo SERV_VALUE ?></td></tr>
<tr>
<td>Automation source <em class="tooltip">?<span class="classic">Where the game tick (battles, movements, training, construction) is processed from. When the cron job is running, players' page loads no longer carry the processing.</span></em></td>
<td><?php echo $cronActive
? "<span class='badge green'>Cron job</span>"
: "<span class='badge red'>Page loads (fallback)</span>"; ?></td>
</tr>
<tr>
<td>Last cron tick <em class="tooltip">?<span class="classic">Written by cron.php on every tick into GameEngine/Prevention/cron_active.txt. Under 90 seconds means the cron is alive.</span></em></td>
<td><?php
if ($cronLastRun > 0) {
echo date('d.m.Y H:i:s', $cronLastRun) . ' <span style="color:#777">(' . $cronAge . 's ago)</span>';
} else {
echo "<span class='badge red'>Never</span>";
}
?></td>
</tr>
<tr>
<td>Invocation length <em class="tooltip">?<span class="classic">CRON_LOOP_SECONDS - how long one cron.php invocation keeps running. Hosts that only allow a cron every 5 minutes still get a tick every minute this way. 0 = a single tick per invocation.</span></em></td>
<td><?php echo $cronLoop > 0
? $cronLoop . ' s <span style="color:#777">(' . (int) floor($cronLoop / max($cronTick, 1)) . ' ticks per invocation)</span>'
: "<span class='badge blue'>Single tick</span>"; ?></td>
</tr>
<tr>
<td>Tick interval <em class="tooltip">?<span class="classic">CRON_TICK_SECONDS - how often Automation runs inside one invocation. Automation itself expects to run about every 60 seconds.</span></em></td>
<td><?php echo $cronTick; ?> s</td>
</tr>
<tr>
<td>HTTP trigger key <em class="tooltip">?<span class="classic">CRON_KEY - only needed to call cron.php over HTTP (external cron service). Running it from the server's own cron job does not use this key. Generated automatically at install.</span></em></td>
<td><?php if ($cronKey === '') { ?>
<span class='badge red'>Not set</span>
<?php } else { ?>
<span id="cronKeyMask"><?php echo $cronKeyMasked; ?></span>
<span id="cronKeyFull" style="display:none;word-break:break-all"><?php echo htmlspecialchars($cronKey, ENT_QUOTES, 'UTF-8'); ?></span>
<a href="#" style="margin-left:6px" onclick="var m=document.getElementById('cronKeyMask'),f=document.getElementById('cronKeyFull'),u=document.getElementById('cronUrlFull');var hidden=(f.style.display==='none');m.style.display=hidden?'none':'';f.style.display=hidden?'':'none';if(u){u.style.display=hidden?'':'none';}this.textContent=hidden?'hide':'show';return false;">show</a>
<?php } ?></td>
</tr>
<?php if ($cronUrl !== '') { ?>
<tr>
<td>HTTP trigger URL <em class="tooltip">?<span class="classic">Optional. Use this with an external cron service if the host cannot run a local cron job. One HTTP call = one tick.</span></em></td>
<td><span style="color:#777">hidden &ndash; use "show" above</span>
<span id="cronUrlFull" style="display:none;word-break:break-all"><?php echo htmlspecialchars($cronUrl, ENT_QUOTES, 'UTF-8'); ?></span></td>
</tr>
<?php } ?>
<?php if ($cronCmd !== '') { ?>
<tr>
<td>Cron job command <em class="tooltip">?<span class="classic">Add this in cPanel &rarr; Cron Jobs. The path is detected from this installation.</span></em></td>
<td style="word-break:break-all"><code><?php echo htmlspecialchars($cronCmd, ENT_QUOTES, 'UTF-8'); ?></code></td>
</tr>
<?php } ?>
</table>
</div>
<!-- NEW FUNCTIONS -->
<div class="config-card">
<div class="config-head"><span>New Mechanics and Functions</span><a href="admin.php?p=editNewFunctions" title="Edit New Mechanics and Functions" class="edit-btn"><?php echo $editIcon; ?></a></div>
+146
View File
@@ -0,0 +1,146 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename : editCronSet.tpl ##
## Type : Admin Panel Frontend ##
## --------------------------------------------------------------------------- ##
## Contact : cata7007@gmail.com ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
#################################################################################
if (!isset($_SESSION)) {
session_start();
}
if ($_SESSION['access'] < 9) die(ACCESS_DENIED_ADMIN);
$cronMarkerFile = __DIR__ . '/../../GameEngine/Prevention/cron_active.txt';
$cronLastRun = @is_file($cronMarkerFile) ? (int) @file_get_contents($cronMarkerFile) : 0;
$cronAge = $cronLastRun > 0 ? (time() - $cronLastRun) : -1;
$cronActive = ($cronAge >= 0 && $cronAge < 90);
$cronLoop = defined('CRON_LOOP_SECONDS') ? (int) CRON_LOOP_SECONDS : 300;
$cronTick = defined('CRON_TICK_SECONDS') ? (int) CRON_TICK_SECONDS : 60;
$cronKey = defined('CRON_KEY') ? (string) CRON_KEY : '';
$cronPath = realpath(__DIR__ . '/../../cron.php');
$cronCmd = $cronPath ? '*/5 * * * * /usr/local/bin/php ' . $cronPath . ' >> ' . dirname(dirname($cronPath)) . '/cron.log 2>&1' : '';
?>
<style>
.config-wrap{max-width:1100px;margin:0 auto;font-family:system-ui,-apple-system,Segoe UI,Roboto}
.config-title{text-align:center;font-size:20px;font-weight:800;margin:8px 0 12px;color:#ffffff}
.config-card{background:#fff;border:1px solid #e5e7eb;border-radius:8px;margin-bottom:10px;overflow:hidden;box-shadow:0 1px 2px rgba(0,0,0,.04)}
.config-head{display:flex;justify-content:space-between;align-items:center;padding:7px 10px;background:#0f172a;color:#fff;font-weight:600;font-size:13px;line-height:1}
.config-table{width:100%;border-collapse:collapse}
.config-table tr{border-top:1px solid #f1f5f9}
.config-table tr:first-child{border-top:0}
.config-table td{padding:6px 8px;vertical-align:middle;font-size:13px;line-height:1.3}
.config-table td.b{background:#f8fafc;font-weight:700;color:#334155;width:45%}
.config-table td:last-child{color:#0f172a}
.fm{border:1px solid #cbd5e1;border-radius:5px;padding:5px 7px;font-size:13px}
.hint{display:block;color:#64748b;font-size:11px;margin-top:3px;font-weight:400}
.badge{display:inline-block;padding:2px 7px;border-radius:10px;font-size:11px;font-weight:700}
.badge.green{background:#dcfce7;color:#166534}
.badge.red{background:#fee2e2;color:#991b1b}
.badge.blue{background:#dbeafe;color:#1e40af}
.tooltip{position:relative;cursor:help;color:#64748b;border:1px solid #cbd5e1;border-radius:50%;padding:0 5px;font-size:10px;font-style:normal}
.tooltip span{display:none;position:absolute;left:20px;top:-4px;width:280px;background:#0f172a;color:#fff;padding:7px 9px;border-radius:6px;font-size:11px;z-index:9;line-height:1.4}
.tooltip:hover span{display:block}
.config-actions{display:flex;justify-content:space-between;align-items:center;margin-top:12px}
.btn-back,.btn-save{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:6px;font-weight:600;font-size:13px;text-decoration:none;cursor:pointer;transition:.15s;border:1px solid transparent}
.btn-back{background:#f1f5f9;color:#0f172a;border-color:#e5e7eb}
.btn-back:hover{background:#e2e8f0}
.btn-save{background:#0f172a;color:#fff;box-shadow:0 1px 2px rgba(0,0,0,.08)}
.btn-save:hover{background:#1e293b;transform:translateY(-1px)}
.btn-save svg{width:14px;height:14px}
code{background:#f1f5f9;padding:2px 5px;border-radius:4px;font-size:11px;word-break:break-all}
</style>
<div class="config-wrap">
<div class="config-title">Cron &amp; Automation</div>
<form action="../GameEngine/Admin/Mods/editCronSet.php" method="POST">
<?php echo csrf_field(); ?>
<input type="hidden" name="id" id="id" value="<?php echo $_SESSION['id']; ?>">
<div class="config-card">
<div class="config-head"><span>Current status</span></div>
<table class="config-table">
<tr>
<td class="b">Automation source</td>
<td><?php echo $cronActive
? "<span class='badge green'>Cron job</span>"
: "<span class='badge red'>Page loads (fallback)</span>"; ?>
<span class="hint">When the cron job is not running, the game still works: players' page loads process the tick, exactly as before the cron was introduced.</span></td>
</tr>
<tr>
<td class="b">Last cron tick</td>
<td><?php
if ($cronLastRun > 0) {
echo date('d.m.Y H:i:s', $cronLastRun) . ' <span style="color:#777">(' . $cronAge . 's ago)</span>';
} else {
echo "<span class='badge red'>Never</span>";
}
?></td>
</tr>
<?php if ($cronCmd !== '') { ?>
<tr>
<td class="b">Cron job command</td>
<td><code><?php echo htmlspecialchars($cronCmd, ENT_QUOTES, 'UTF-8'); ?></code>
<span class="hint">Add this in cPanel &rarr; Cron Jobs. Writing to cron.log instead of /dev/null lets you see errors if it fails to start.</span></td>
</tr>
<?php } ?>
</table>
</div>
<div class="config-card">
<div class="config-head"><span>Settings</span></div>
<table class="config-table">
<tr>
<td class="b">Invocation length (seconds)
<em class="tooltip">?<span>How long one cron.php invocation keeps working. Most shared hosts only allow a cron every 5 minutes, while Automation expects to run about every minute &mdash; so one invocation runs several ticks in a row. Use 300 for a "*/5" cron. Set 0 only if your host allows a cron every minute.</span></em>
</td>
<td><input class="fm" type="number" name="cron_loop" min="0" max="3300" value="<?php echo $cronLoop; ?>" style="width:120px">
<span class="hint">0 = a single tick per invocation. Maximum 3300 (55 minutes).</span></td>
</tr>
<tr>
<td class="b">Tick interval (seconds)
<em class="tooltip">?<span>How often Automation runs inside one invocation. Automation's own guard expects roughly 60 seconds; going lower mostly adds load without processing anything new.</span></em>
</td>
<td><input class="fm" type="number" name="cron_tick" min="15" max="900" value="<?php echo $cronTick; ?>" style="width:120px">
<span class="hint">Recommended: 60. Allowed range 15&ndash;900.</span></td>
</tr>
<tr>
<td class="b">HTTP trigger key
<em class="tooltip">?<span>Only needed when calling cron.php over HTTP (for example from an external cron service). The server's own cron job does not use it. Regenerating invalidates any URL you configured elsewhere.</span></em>
</td>
<td>
<?php if ($cronKey === '') { ?>
<span class="badge red">Not set</span> &ndash; one will be generated on save.
<?php } else { ?>
<span id="k1"><?php echo htmlspecialchars(substr($cronKey, 0, 6), ENT_QUOTES, 'UTF-8'); ?>&bull;&bull;&bull;&bull;&bull;&bull;&bull;&bull;<?php echo htmlspecialchars(substr($cronKey, -4), ENT_QUOTES, 'UTF-8'); ?></span>
<span id="k2" style="display:none;word-break:break-all"><?php echo htmlspecialchars($cronKey, ENT_QUOTES, 'UTF-8'); ?></span>
<a href="#" style="margin-left:6px" onclick="var a=document.getElementById('k1'),b=document.getElementById('k2');var h=(b.style.display==='none');a.style.display=h?'none':'';b.style.display=h?'':'none';this.textContent=h?'hide':'show';return false;">show</a>
<?php } ?>
<span class="hint"><label><input type="checkbox" name="regenerate_key" value="1"> Generate a new key on save</label></span>
</td>
</tr>
</table>
</div>
<div class="config-actions">
<a href="../Admin/admin.php?p=config" class="btn-back">&lsaquo; <?php echo EDIT_BACK ?></a>
<button type="submit" class="btn-save">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
</svg>
SAVE
</button>
</div>
</form>
</div>
+5
View File
@@ -94,6 +94,7 @@ function admin_validated_page(string $raw): string
'maintenenceResetPlus', 'givePlusRes', 'maintenenceResetPlusBonus',
'addUsers', 'users', 'admin_log', 'config', 'debug_log',
'editServerSet', 'editPlusSet', 'editLogSet', 'editNewsboxSet',
'editCronSet',
'editExtraSet', 'editAdminInfo', 'resetServer', 'player', 'editUser',
'deletion', 'Newmessage', 'editPlus', 'editSitter', 'editPassword',
'editProtection', 'editOverall',
@@ -302,6 +303,10 @@ if ($page !== '') {
$subpage = 'Server Configuration';
break;
case 'editCronSet':
$subpage = 'Cron & Automation';
break;
case 'editPlusSet':
$subpage = 'PLUS Settings';
break;
+45 -2
View File
@@ -39,9 +39,52 @@ if (!function_exists('admin_config_template_path')) {
return admin_config_template_path() !== false;
}
// Raw template contents (placeholders untouched), or false when unavailable.
// Raw template contents, or false when unavailable.
//
// Placeholders are left untouched for the caller to fill in, with ONE
// exception: %CRONKEY%. Every edit*.php mod regenerates config.php from this
// template and replaces only the placeholders it knows about, so a value that
// no mod handles would be written to config.php literally — breaking the key
// used to call cron.php over HTTP. CRON_KEY is not something the admin edits
// in any of those forms, so it is resolved here, once, for all callers:
// - existing server -> keep the current CRON_KEY (survives config saves)
// - key not defined -> provision a fresh random one (servers installed
// before CRON_KEY existed in the template)
function admin_config_template_contents() {
$path = admin_config_template_path();
return $path !== false ? file_get_contents($path) : false;
if ($path === false) {
return false;
}
$text = file_get_contents($path);
if ($text === false) {
return false;
}
if (strpos($text, '%CRONKEY%') !== false) {
$cronKey = (defined('CRON_KEY') && CRON_KEY !== '' && CRON_KEY !== '%CRONKEY%')
? CRON_KEY
: bin2hex(random_bytes(24));
$text = str_replace('%CRONKEY%', $cronKey, $text);
}
// Acelasi tratament pentru durata ciclului si intervalul de tick: sunt
// editate din ACP (editCronSet), deci orice alt mod care regenereaza
// config.php trebuie sa PASTREZE valorile curente, nu sa le reseteze la
// cele din template.
if (strpos($text, '%CRONLOOP%') !== false) {
$cronLoop = defined('CRON_LOOP_SECONDS') ? (int) CRON_LOOP_SECONDS : 300;
$text = str_replace('%CRONLOOP%', (string) $cronLoop, $text);
}
if (strpos($text, '%CRONTICK%') !== false) {
$cronTick = defined('CRON_TICK_SECONDS') ? (int) CRON_TICK_SECONDS : 60;
$text = str_replace('%CRONTICK%', (string) $cronTick, $text);
}
return $text;
}
}
+146
View File
@@ -0,0 +1,146 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename editCronSet.php ##
## Type Admin Panel - Cron & Automation settings ##
## --------------------------------------------------------------------------- ##
## Contact cata7007@gmail.com ##
## Project TravianZ ##
## GitHub https://github.com/Shadowss/TravianZ ##
## License TravianZ Project ##
## Copyright TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
## NOTA DE PROIECTARE - de ce acest mod NU regenereaza tot config.php: ##
## Celelalte module edit*.php reconstruiesc config.php din constant_format ##
## .tpl si trebuie sa reumple TOATE placeholderele (peste 100). Aici avem ##
## de schimbat trei valori, iar o gresala intr-o mapare fara legatura ar ##
## strica setari de joc. De aceea modificam DOAR liniile de cron din ##
## config.php existent; restul fisierului ramane bit-cu-bit neatins. ##
## Daca liniile lipsesc (server instalat inainte de aceasta versiune), ele ##
## sunt inserate dupa AUTOMATION_LOCK_FILE_NAME. ##
#################################################################################
if (!isset($_SESSION)) {
session_start();
}
if ($_SESSION['access'] < 9) {
die(ACCESS_DENIED_ADMIN);
}
// Acest mod primeste POST direct, deci isi verifica singur tokenul CSRF
// (nu trece prin csrf_verify()-ul central din admin.php). Vezi issue #139.
require_once(__DIR__ . '/../csrf.php');
csrf_verify();
include_once("../../Database.php");
$id = (int) $_POST['id'];
// ---------------------------------------------------------------------------
// Validarea valorilor primite
// ---------------------------------------------------------------------------
$loop = isset($_POST['cron_loop']) ? (int) $_POST['cron_loop'] : 300;
$tick = isset($_POST['cron_tick']) ? (int) $_POST['cron_tick'] : 60;
// 0 = o singura rulare per invocare (host care permite cron la fiecare minut).
// Peste 0, limitam la 55 de minute ca sa nu ramana procese agatate.
if ($loop < 0) { $loop = 0; }
if ($loop > 3300) { $loop = 3300; }
// Automation se asteapta sa ruleze pe la 60s; sub 15s doar incarcam serverul.
if ($tick < 15) { $tick = 15; }
if ($tick > 900) { $tick = 900; }
// Un ciclu care nu incape nici macar un tick nu are sens - il tratam ca "o
// singura rulare", ca sa nu iasa o configuratie care pare activa dar nu e.
if ($loop > 0 && $loop < $tick) {
$loop = 0;
}
$regenerateKey = !empty($_POST['regenerate_key']);
$cronKey = (defined('CRON_KEY') && CRON_KEY !== '' && CRON_KEY !== '%CRONKEY%')
? CRON_KEY
: '';
if ($regenerateKey || $cronKey === '') {
$cronKey = bin2hex(random_bytes(24));
}
// ---------------------------------------------------------------------------
// Editarea chirurgicala a fisierului de configuratie
// ---------------------------------------------------------------------------
$configFile = "../../config.php";
$config = @file_get_contents($configFile);
if ($config === false) {
die("<br/><br/><br/>Can't read file: GameEngine\\config.php");
}
/**
* Inlocuieste valoarea unei constante daca linia exista; altfel intoarce false
* ca apelantul sa stie ca trebuie inserata.
*
* $rawValue se scrie exact asa cum e dat (deja citat pentru siruri).
*/
function cron_set_define(&$text, $constant, $rawValue)
{
$pattern = "/define\s*\(\s*(['\"])" . preg_quote($constant, '/') . "\\1\s*,.*?\)\s*;/s";
if (!preg_match($pattern, $text)) {
return false;
}
// str_replace pe potrivirea gasita, ca sa nu interpretam $ sau \ din valoare
// ca referinte de grup (capcana clasica a lui preg_replace).
preg_match($pattern, $text, $m);
$text = str_replace($m[0], "define('" . $constant . "', " . $rawValue . ");", $text);
return true;
}
$missing = array();
if (!cron_set_define($config, 'CRON_LOOP_SECONDS', (string) $loop)) {
$missing[] = "define('CRON_LOOP_SECONDS', " . $loop . ");";
}
if (!cron_set_define($config, 'CRON_TICK_SECONDS', (string) $tick)) {
$missing[] = "define('CRON_TICK_SECONDS', " . $tick . ");";
}
if (!cron_set_define($config, 'CRON_KEY', "'" . addcslashes($cronKey, "'\\") . "'")) {
$missing[] = "define('CRON_KEY', '" . addcslashes($cronKey, "'\\") . "');";
}
// Server instalat inainte ca aceste constante sa existe: le adaugam dupa
// AUTOMATION_LOCK_FILE_NAME (locul lor din template).
if (!empty($missing)) {
$anchorPattern = "/define\s*\(\s*(['\"])AUTOMATION_LOCK_FILE_NAME\\1\s*,.*?\)\s*;/s";
$block = "\n\n//////////////////////////////////\n"
. "// ***** CRON / AUTOMATION *****//\n"
. "//////////////////////////////////\n"
. implode("\n", $missing);
if (preg_match($anchorPattern, $config, $anchor)) {
$config = str_replace($anchor[0], $anchor[0] . $block, $config);
} else {
// fara ancora: adaugam la finalul fisierului, inaintea eventualului
// tag de inchidere PHP (atentie: nu-l scriem literal in comentariu,
// fiindca intr-un comentariu "//" ar inchide chiar blocul PHP curent)
$config = preg_replace('/\?>\s*$/', '', $config) . $block . "\n";
}
}
$fh = @fopen($configFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\\config.php");
fwrite($fh, $config);
fclose($fh);
$database->query(
"Insert into " . TB_PREFIX . "admin_log values (0," . $id . ",'Changed Cron & Automation Settings'," . time() . ")"
);
header("Location: ../../../Admin/admin.php?p=config");
+102 -84
View File
@@ -18,12 +18,12 @@
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
## Refactor D1 : clamp productie in memorie = clamp SQL; 1 UPDATE/hit; ##
## eliminat wid=wref; flock pe lock-ul de automation ##
## Refactor W1 : getProdFor() unic (fara cele 4 metode copy-paste); ##
## scan f1..f38 o singura data; globals trase in constructor ##
## Refactor W2 : constructor fara side effects; tick() + ActionControl() ##
## explicite in bootstrap; garduri statice eliminate ##
## Refactor D1 : in-memory production clamp = SQL clamp; 1 UPDATE/hit; ##
## removed wid=wref; flock on the automation lock ##
## Refactor W1 : single getProdFor() (no 4 copy-paste methods); ##
## scan f1..f38 only once; globals pulled in constructor ##
## Refactor W2 : constructor without side effects; explicit tick() + ##
## ActionControl() in bootstrap; static guards removed ##
#################################################################################
include_once("Session.php");
@@ -52,20 +52,20 @@ class Village {
private $oasisowned, $ocounter = [];
private $oasisFactor = 0.25;
// W1 #6: dependinte trase o singura data din globals, in constructor
// W1 #6: dependencies pulled from globals once, in the constructor
private $db;
private $sess;
private $tech;
private $bids = [];
// W1 #4: index-ul campurilor f1..f38, construit lazy, o data per incarcare
// W1 #4: index of the f1..f38 fields, built lazily, once per load
private $fieldIndex = null;
/**
* W1 #4: configuratia per resursa pentru getProdFor().
* field = tipul campului de productie (si nr. tabelei $bidN de productie),
* bonus = gid-urile cladirilor bonus (Sawmill/Brickyard/Foundry/Grain Mill+Bakery),
* oasis = indexul in $this->ocounter, sessionBonus = flag-ul de bonus 25%.
* W1 #4: per-resource configuration for getProdFor().
* field = the production field type (and the number of the $bidN production table),
* bonus = the gids of the bonus buildings (Sawmill/Brickyard/Foundry/Grain Mill+Bakery),
* oasis = the index into $this->ocounter, sessionBonus = the 25% bonus flag.
*/
private const PROD_CONFIG = [
'wood' => ['field' => 1, 'bonus' => [5], 'oasis' => 0, 'sessionBonus' => 'bonus1'],
@@ -75,11 +75,11 @@ class Village {
];
/**
* W2 #7: constructorul DOAR incarca. Fara scrieri in DB, fara redirect,
* fara tick de productie. "new Village" e sigur de apelat oriunde
* (instante multiple, admin switch). Gardurile statice loadedWid /
* cacheCleared au fost eliminate (W2 #10) - nu mai sunt necesare cu
* side effects explicite.
* W2 #7: the constructor ONLY loads. No DB writes, no redirect,
* no production tick. "new Village" is safe to call anywhere
* (multiple instances, admin switch). The static loadedWid /
* cacheCleared guards were removed (W2 #10) - they are no longer
* needed now that side effects are explicit.
*/
function __construct() {
global $database, $session, $technology,
@@ -91,7 +91,7 @@ class Village {
$this->bids = [
1 => $bid1, 2 => $bid2, 3 => $bid3, 4 => $bid4,
5 => $bid5, 6 => $bid6, 7 => $bid7, 8 => $bid8, 9 => $bid9,
45 => $bid45, // Waterworks (Egipteni) - folosit de getOasisBonusFactor()
45 => $bid45, // Waterworks (Egyptians) - used by getOasisBonusFactor()
];
// set village id
@@ -113,15 +113,15 @@ class Village {
}
/**
* W2 #8: tick explicit - calculeaza si aplica productia. Apelat din
* bootstrap-ul de la finalul fisierului (nu din dorf1/dorf2 individual:
* 28 de pagini includ Village.php si toate au nevoie de resurse la zi).
* W2 #8: explicit tick - computes and applies production. Called from the
* bootstrap at the end of this file (not from dorf1/dorf2 individually:
* 28 pages include Village.php and all of them need up-to-date resources).
*/
public function tick(): void {
// W2 #10: clear-ul de cache era gardat de static::$cacheCleared ca sa
// ruleze o data per request; cu tick explicit (apelat o data, din
// bootstrap) gardul nu mai e necesar. Un clear repetat la o a doua
// instanta inseamna doar cache rece, nu un bug de corectitudine.
// W2 #10: the cache clear used to be guarded by static::$cacheCleared so it
// would run once per request; with an explicit tick (called once, from the
// bootstrap) that guard is no longer needed. A repeated clear on a second
// instance only means a cold cache, not a correctness bug.
$db = get_class($this->db);
$db::clearVillageCache();
@@ -150,8 +150,8 @@ class Village {
/**
* LOAD VILLAGE DATA (single source of truth)
* W2: parametrul mort $second_run eliminat (nu era pasat de nicaieri;
* getResearching primea mereu use_cache = true).
* W2: the dead $second_run parameter was removed (it was not passed from
* anywhere; getResearching always received use_cache = true).
*/
private function LoadTown(): void {
global $logging;
@@ -187,9 +187,10 @@ class Village {
$this->natar = $this->infoarray['natar'];
$this->currentcel = $this->infoarray['celebration'];
$this->currentfestival = $this->infoarray['festival'];
// FIX #2 (Day 1): eliminat "$this->wid = $this->infoarray['wref']" - suprascria
// id-ul satului la mijlocul incarcarii; in flux normal era no-op (wref == wid),
// dar masca orice rand gresit venit din cache. wid ramane sursa de adevar.
// FIX #2 (Day 1): removed "$this->wid = $this->infoarray['wref']" - it overwrote
// the village id in the middle of the load; in the normal flow it was a no-op
// (wref == wid), but it masked any wrong row coming from the cache. wid stays
// the source of truth.
$this->vname = $this->infoarray['name'];
$this->awood = $this->infoarray['wood'];
@@ -208,15 +209,15 @@ class Village {
$this->master = count($this->db->getMasterJobs($this->wid));
// FIX #1a (Day 1): overflow fix doar in memorie - fara updateResource() aici.
// Clamp-ul SQL din modifyResource() (IF > maxstore -> maxstore) corecteaza
// oricum DB-ul in acelasi request, in processProduction(). Scapa un UPDATE.
// FIX #1a (Day 1): overflow fix in memory only - no updateResource() here.
// The SQL clamp in modifyResource() (IF > maxstore -> maxstore) corrects the
// DB in the same request anyway, in processProduction(). Saves one UPDATE.
if ($this->awood > $this->maxstore) $this->awood = $this->maxstore;
if ($this->aclay > $this->maxstore) $this->aclay = $this->maxstore;
if ($this->airon > $this->maxstore) $this->airon = $this->maxstore;
if ($this->acrop > $this->maxcrop) $this->acrop = $this->maxcrop;
// NOTA (pre-existent, nemodificat): $this->atotal e calculat mai sus din
// valorile ne-clamp-uite si inainte de tick-ul de productie.
// NOTE (pre-existing, unchanged): $this->atotal is computed above from the
// un-clamped values and before the production tick.
}
/**
@@ -239,7 +240,7 @@ class Village {
* APPLY PRODUCTION (NO MORE LOADTOWN RELOAD)
*/
private function processProduction(): void {
// hardening: nu lasa un lastupdate din viitor (clock skew) sa produca delta negativ
// hardening: do not let a lastupdate from the future (clock skew) produce a negative delta
$timepast = max(0, time() - $this->infoarray['lastupdate']);
$nwood = min(($this->production['wood'] / 3600) * $timepast, $this->maxstore);
@@ -247,34 +248,34 @@ class Village {
$niron = min(($this->production['iron'] / 3600) * $timepast, $this->maxstore);
$ncrop = min(($this->production['crop'] / 3600) * $timepast, $this->maxcrop);
// FIX #1b (Day 1): un singur UPDATE - lastupdate e setat in aceeasi interogare
// cu resursele (param $setLastupdate in modifyResource); updateVillage() eliminat.
// FIX #1b (Day 1): a single UPDATE - lastupdate is set in the same query as the
// resources (the $setLastupdate param in modifyResource); updateVillage() removed.
$this->db->modifyResource($this->wid, $nwood, $nclay, $niron, $ncrop, 1, true);
// lightweight sync ONLY (no DB reload)
$this->infoarray['lastupdate'] = time();
// FIX #0 (Day 1): sincronizarea in memorie oglindeste exact clamp-ul SQL
// din modifyResource: podea la 0 (inainte, crop-ul negativ din starvation
// putea cobori sub 0 in memorie in timp ce DB-ul ramanea la 0 -> divergenta).
// FIX #0 (Day 1): the in-memory sync mirrors exactly the SQL clamp from
// modifyResource: floor at 0 (before, negative crop from starvation could
// drop below 0 in memory while the DB stayed at 0 -> divergence).
$this->awood = min(max($this->awood + $nwood, 0), $this->maxstore);
$this->aclay = min(max($this->aclay + $nclay, 0), $this->maxstore);
$this->airon = min(max($this->airon + $niron, 0), $this->maxstore);
$this->acrop = min(max($this->acrop + $ncrop, 0), $this->maxcrop);
}
// W1 #4: cele 4 metode raman ca wrappere de o linie - zero schimbari de API,
// toata logica e in getProdFor().
// W1 #4: the 4 methods remain as one-line wrappers - zero API changes,
// all the logic lives in getProdFor().
private function getWoodProd() { return $this->getProdFor('wood'); }
private function getClayProd() { return $this->getProdFor('clay'); }
private function getIronProd() { return $this->getProdFor('iron'); }
private function getCropProd() { return $this->getProdFor('crop'); }
/**
* W1 #4: productia bruta pentru o resursa. Inlocuieste cele 4 metode
* copy-paste (~120 linii). Ordinea operatiilor e pastrata 1:1 cu originalul:
* suma campuri -> bonus oaze -> bonus cladire (%) -> bonus cont (x1.25)
* -> round(x * SPEED).
* W1 #4: gross production for one resource. Replaces the 4 copy-paste
* methods (~120 lines). The order of the operations is kept 1:1 with the
* original: sum of fields -> oasis bonus -> building bonus (%) ->
* account bonus (x1.25) -> round(x * SPEED).
*/
private function getProdFor(string $type): float {
$cfg = self::PROD_CONFIG[$type];
@@ -289,10 +290,11 @@ class Village {
$amount += $amount * $this->oasisFactor * $this->ocounter[$cfg['oasis']];
// NOTA: garda isset() pe 'attri' exista in original doar la crop; aici e
// aplicata peste tot (identic numeric pe date valide, fara warning pe date
// corupte). Nivelul cladirii bonus = valoarea ULTIMULUI camp de acel tip,
// exact ca in original (in date reale exista cel mult o cladire per tip).
// NOTE: the isset() guard on 'attri' existed in the original only for crop;
// here it is applied everywhere (numerically identical on valid data, no
// warning on corrupted data). The bonus building level = the value of the
// LAST field of that type, exactly as in the original (on real data there is
// at most one building per type).
$bonusPct = 0;
foreach ($cfg['bonus'] as $buildingType) {
$level = $index['last'][$buildingType] ?? 0;
@@ -310,11 +312,11 @@ class Village {
}
/**
* W1 #4: scaneaza campurile f1..f38 O SINGURA data per incarcare (in loc de
* 4 treceri) si le bucket-uieste dupa tip.
* 'levels' => [tip => [nivelele fiecarui camp de acel tip]]
* 'last' => [tip => nivelul (raw) al ultimului camp de acel tip] -
* semantica identica cu atribuirile suprascrise din original.
* W1 #4: scans the f1..f38 fields ONLY once per load (instead of 4 passes)
* and buckets them by type.
* 'levels' => [type => [the levels of every field of that type]]
* 'last' => [type => the (raw) level of the last field of that type] -
* identical semantics to the overwritten assignments in the original.
*/
private function getFieldIndex(): array {
if ($this->fieldIndex !== null) {
@@ -337,10 +339,10 @@ class Village {
* OASIS
*/
/**
* Bonusul de baza al oazei (25%) crescut de Waterworks (gid 45, Egipteni):
* attri = +5% relativ per nivel, pana la +100% la nivel 20 (0.25 -> 0.50).
* NOTA: ramane intentionat in afara getFieldIndex() - scaneaza f19..f40
* (include f39/f40, peste limita de 38 a index-ului).
* The base oasis bonus (25%) increased by the Waterworks (gid 45, Egyptians):
* attri = +5% relative per level, up to +100% at level 20 (0.25 -> 0.50).
* NOTE: intentionally kept outside getFieldIndex() - it scans f19..f40
* (includes f39/f40, past the index limit of 38).
*/
private function getOasisBonusFactor(): float {
$wwLevel = 0;
@@ -375,9 +377,9 @@ class Village {
}
/**
* W2 #9: scos din constructor; apelat explicit din bootstrap-ul de mai jos.
* Isi pastreaza singur garda pe build.php, deci efectul e identic cu a-l
* pune la inceputul build.php, dar fara sa editam paginile de intrare.
* W2 #9: moved out of the constructor; called explicitly from the bootstrap
* below. It keeps its own guard on build.php, so the effect is identical to
* putting it at the top of build.php, but without editing the entry pages.
*/
public function ActionControl(): void {
$page = $_SERVER['SCRIPT_NAME'];
@@ -392,32 +394,48 @@ class Village {
}
}
// W2: bootstrap cu side effects EXPLICITE, in aceeasi ordine ca vechiul
// constructor (load -> tick productie -> ActionControl). Village.php e
// include_once-uit, deci blocul ruleaza o singura data per request.
// "new Village" ramane sigur pentru instante suplimentare (admin switch).
// W2: bootstrap with EXPLICIT side effects, in the same order as the old
// constructor (load -> production tick -> ActionControl). Village.php is
// include_once-d, so this block runs only once per request.
// "new Village" stays safe for additional instances (admin switch).
$village = new Village;
$village->tick();
$village->ActionControl();
$building = new Building;
// automation
// FIX #3 (Day 1): file_exists() + file_put_contents() era un TOCTOU clasic - doua
// request-uri simultane treceau amandoua de check si rulau Automation dublu.
// fopen('c') + flock(LOCK_EX|LOCK_NB) e atomic; lock-ul se elibereaza singur si
// daca procesul moare. Se foloseste ACELASI fisier, deci gardul intern din
// Automation.php (calea cron/directa: file_exists + mtime < 60s) ramane functional;
// touch() tine mtime proaspat cat rulam noi.
$__automationLock = @fopen(AUTOMATION_LOCK_FILE_NAME, 'c');
if ($__automationLock !== false) {
if (flock($__automationLock, LOCK_EX | LOCK_NB)) {
define('AUTOMATION_MANUAL_RUN', true);
@touch(AUTOMATION_LOCK_FILE_NAME);
include_once("Automation.php");
// Automation.php sterge singur fisierul la final (comportament pastrat
// pentru calea cron); pe Linux flock ramane valid pe inode pana la fclose.
flock($__automationLock, LOCK_UN);
// P2: if a dedicated cron is running (cron.php), we no longer trigger automation
// from the player pages - cron.php writes a timestamped marker on every run.
// A fresh marker (< 90s) => cron is active, so we skip. Fallback: if the cron
// stops (marker old or missing), the pages take over automatically, as before, so
// the game does not stall if you forget to set up the cron or if it dies.
$__cronMarker = __DIR__ . '/Prevention/cron_active.txt';
$__cronActive = false;
if (@is_file($__cronMarker)) {
$__cronTime = (int)@file_get_contents($__cronMarker);
if ($__cronTime > 0 && (time() - $__cronTime) < 90) {
$__cronActive = true;
}
fclose($__automationLock);
}
?>
if (!$__cronActive) {
// FIX #3 (Day 1): file_exists() + file_put_contents() was a classic TOCTOU - two
// simultaneous requests both passed the check and ran Automation twice.
// fopen('c') + flock(LOCK_EX|LOCK_NB) is atomic; the lock is released by itself
// even if the process dies. It uses the SAME file, so the internal guard in
// Automation.php (cron/direct path: file_exists + mtime < 60s) keeps working;
// touch() keeps mtime fresh while we are running.
$__automationLock = @fopen(AUTOMATION_LOCK_FILE_NAME, 'c');
if ($__automationLock !== false) {
if (flock($__automationLock, LOCK_EX | LOCK_NB)) {
define('AUTOMATION_MANUAL_RUN', true);
@touch(AUTOMATION_LOCK_FILE_NAME);
include_once("Automation.php");
// Automation.php deletes the file itself at the end (behavior kept for the
// cron path); on Linux flock stays valid on the inode until fclose.
flock($__automationLock, LOCK_UN);
}
fclose($__automationLock);
}
}
?>
+2
View File
@@ -265,6 +265,8 @@ Notes:
- World data
- Croppers build
5. After success, access the game root.
6. Installation in cPanel (Cron Jobs → Add New Cron Job, every minute):
- * * * * * /usr/bin/php /home/USER/public_html/cron.php >/dev/null 2>&1 (REPLACE USER WITH YOUR USERNAME)
## Environment Configuration
+214 -42
View File
@@ -3,18 +3,40 @@
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename : cron.php ##
## Type : Standalone automation runner (P2 - performance) ##
## Filename : cron.php ##
## Type : Standalone automation runner (P2 - Performance) ##
## --------------------------------------------------------------------------- ##
## Scop : Ruleaza Automation.php separat de request-urile ##
## jucatorilor. Se apeleaza dintr-un cron job cPanel la ##
## fiecare minut, astfel incat procesarea (batalii, miscari, ##
## antrenamente) nu mai e carata de pagina unui jucator. ##
## Purpose : Runs Automation.php independently from player requests ##
## so processing (battles, troop movements, training, etc.) ##
## is no longer handled by a player's page request. ##
## --------------------------------------------------------------------------- ##
## Instalare cron cPanel (la fiecare minut): ##
## * * * * * /usr/bin/php /home/USER/public_html/cron.php >/dev/null 2>&1 ##
## sau prin wget/curl daca php-cli nu e disponibil: ##
## * * * * * wget -q -O /dev/null "https://DOMENIU/cron.php?key=SECRET" ##
## WHY AN INTERNAL LOOP: ##
## Automation is designed to run approximately every 60 seconds (its ##
## internal guard checks "time() - filemtime > 60"). Many cPanel hosting ##
## providers do not allow 1-minute cron jobs, only a minimum interval of ##
## 5 minutes. If Automation were executed only once every 5 minutes, ##
## attacks and building completions could be delayed by up to 5 minutes. ##
## For this reason, a single cron invocation performs one TICK every 60 ##
## seconds for approximately 5 minutes, then exits and waits for the next ##
## scheduled cron execution. ##
## ##
## Each tick rewrites Prevention/cron_active.txt, keeping the marker file ##
## fresh so player page requests will no longer trigger Automation at all. ##
## --------------------------------------------------------------------------- ##
## cPanel Cron Installation (every 5 minutes - recommended with the ##
## internal loop): ##
##*/5 * * * * /usr/local/bin/php /home/USER/public_html/cron.php>/dev/null 2>&1##
## ##
## If your hosting provider allows a 1-minute cron, set the Minute field to ##
## "*" and add the following to GameEngine/config.php: ##
## define('CRON_LOOP_SECONDS', 0); ##
## (0 = a single tick per invocation, without the internal loop) ##
## --------------------------------------------------------------------------- ##
## Optional configuration in GameEngine/config.php: ##
## define('CRON_LOOP_SECONDS', 300); // Duration of one invocation ##
## // (0 = single tick) ##
## define('CRON_TICK_SECONDS', 60); // Tick interval inside the loop ##
## define('CRON_KEY', 'secret'); // Required only for HTTP execution ##
## --------------------------------------------------------------------------- ##
## Contact : cata7007@gmail.com ##
## Project : TravianZ ##
@@ -22,18 +44,44 @@
#################################################################################
// -----------------------------------------------------------------------------
// 1. Detectare mod de rulare
// - CLI (php cron.php): mereu permis, e apelat doar de cron-ul serverului.
// - HTTP (wget/curl): permis DOAR daca cheia din config se potriveste, ca sa
// nu poata declansa oricine automation-ul de pe internet.
// 1. Execution Mode
// - CLI (php cron.php): Invoked by the server's cron scheduler and always
// allowed.
// - HTTP (wget/curl): Allowed ONLY when the correct CRON_KEY is provided,
// preventing anyone on the Internet from triggering Automation.
// - "--once" / "?once=1": Executes a single tick without the internal loop
// (useful for a "Force Run" button in the admin panel).
// -----------------------------------------------------------------------------
$isCli = (PHP_SAPI === 'cli');
if ($isCli) {
$forceSingleTick = isset($argv) && in_array('--once', $argv, true);
} else {
$forceSingleTick = isset($_GET['once']);
}
// -----------------------------------------------------------------------------
// 2. Bootstrap minimal - NU includem Session.php (porneste sesiune, se leaga de
// request si de un user logat). Automation are nevoie doar de config (constante
// SQL_*, TB_PREFIX, AUTOMATION_LOCK_FILE_NAME) si de $database; restul (buidata,
// unitdata, Units, Battle, etc.) si le include Automation.php singur.
// 1b. IMPORTANT: The server's cron launches this script with the current working
// directory set to the account's HOME directory (e.g. /home/user), NOT the
// game's directory. Without the line below, the relative lookups in step 2
// (file_exists('autoloader.php'), etc.) fail, the script exits with exit(1),
// and the error message is swallowed by ">/dev/null"—making it appear that
// the cron job "does nothing" with no trace in the logs.
// This problem does not occur when accessed via HTTP (where the current
// working directory is the script's directory), which is why running it from
// the browser works while the cron job does not.
// -----------------------------------------------------------------------------
if (@chdir(__DIR__) === false) {
if ($isCli) { fwrite(STDERR, "cron: unable to change to directory " . __DIR__ . "\n"); }
exit(1);
}
// -----------------------------------------------------------------------------
// 2. Minimal Bootstrap
// - We do NOT include Session.php, as it starts a session and is tied to
// HTTP requests and authenticated users.
// - Automation.php loads everything else it needs on its own
// (buidata, unitdata, Units, Battle, etc.).
// -----------------------------------------------------------------------------
$autoprefix = '';
for ($i = 0; $i < 5; $i++) {
@@ -46,7 +94,7 @@ for ($i = 0; $i < 5; $i++) {
require_once($autoprefix . 'autoloader.php');
if (!file_exists($autoprefix . 'GameEngine/config.php')) {
// instalarea nu e finalizata - nu avem ce rula
// Installation is not yet complete - there is nothing to run.
if ($isCli) { fwrite(STDERR, "cron: config.php lipseste, jocul nu e instalat\n"); }
exit(1);
}
@@ -54,61 +102,185 @@ if (!file_exists($autoprefix . 'GameEngine/config.php')) {
include_once($autoprefix . 'GameEngine/config.php');
// -----------------------------------------------------------------------------
// 3. Verificare cheie pentru accesul HTTP (nu se aplica in CLI)
// Defineste in GameEngine/config.php: define('CRON_KEY', 'un-secret-lung');
// Apoi apeleaza: https://domeniu/cron.php?key=un-secret-lung
// Daca CRON_KEY nu e definit, accesul HTTP este refuzat (doar CLI merge).
// 3. Verify the access key for HTTP requests (not applicable in CLI mode)
// -----------------------------------------------------------------------------
if (!$isCli) {
$providedKey = isset($_GET['key']) ? (string)$_GET['key'] : '';
if (!defined('CRON_KEY') || CRON_KEY === '' || !hash_equals(CRON_KEY, $providedKey)) {
header('HTTP/1.1 403 Forbidden');
exit('Forbidden');
}
// nu tinem conexiunea deschisa pentru client - raspundem si continuam in fundal
// Do not keep the client connection open - send the response immediately and continue processing in the background.
ignore_user_abort(true);
@set_time_limit(0);
header('Content-Type: text/plain');
echo "OK\n";
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
// When executed via HTTP, we do not keep the process running for 5 minutes
// (PHP-FPM has its own execution timeouts).
// An HTTP request performs only a single tick.
// The internal loop is intended for CLI execution only.
$forceSingleTick = true;
}
// -----------------------------------------------------------------------------
// 4. Conexiune la baza de date (acelasi mod ca in Database.php - variabila globala
// $database folosita de Automation prin "global $database").
// 4. Loop Parameters
// -----------------------------------------------------------------------------
$loopSeconds = defined('CRON_LOOP_SECONDS') ? (int)CRON_LOOP_SECONDS : 300;
$tickSeconds = defined('CRON_TICK_SECONDS') ? (int)CRON_TICK_SECONDS : 60;
if ($tickSeconds < 15) { $tickSeconds = 15; } // sub 15s nu aduce nimic, doar incarca serverul
if ($loopSeconds < 0) { $loopSeconds = 0; }
if ($forceSingleTick) { $loopSeconds = 0; }
// -----------------------------------------------------------------------------
// 5. Overlap Lock
// If the previous invocation is still running its loop (e.g., on a slow
// server), the next invocation exits immediately instead of processing
// everything twice. The flock() lock is automatically released, even if
// the process terminates unexpectedly.
// -----------------------------------------------------------------------------
$runLockPath = $autoprefix . 'GameEngine/Prevention/cron_running.lock';
$runLockHandle = @fopen($runLockPath, 'c');
if ($runLockHandle === false) {
if ($isCli) { fwrite(STDERR, "cron: nu pot deschide lock-ul ($runLockPath)\n"); }
exit(1);
}
if (!flock($runLockHandle, LOCK_EX | LOCK_NB)) {
fclose($runLockHandle);
if ($isCli) { echo "cron: o invocare anterioara inca ruleaza, ies\n"; }
exit(0);
}
// -----------------------------------------------------------------------------
// 6. Database Connection
// Initializes the database connection ($database), which is accessed by
// Automation.php through the global scope.
// -----------------------------------------------------------------------------
include_once($autoprefix . 'GameEngine/Database.php');
if (!isset($database) || !$database) {
if ($isCli) { fwrite(STDERR, "cron: conexiunea la baza de date a esuat\n"); }
flock($runLockHandle, LOCK_UN);
fclose($runLockHandle);
exit(1);
}
// -----------------------------------------------------------------------------
// 5. Marcam ca suntem sursa oficiala de automation. AUTOMATION_MANUAL_RUN spune
// Automation.php sa NU mai treaca prin gardul de lock file (acela era pentru
// coordonarea request-urilor concurente ale jucatorilor; cron-ul e sursa unica,
// o data pe minut, deci nu are nevoie de el).
//
// In plus scriem un marker cu timestamp, ca Village.php sa stie ca exista un
// cron activ si sa NU mai declanseze automation din paginile jucatorilor
// (fallback: daca cron-ul se opreste > 90s, paginile preiau automat).
// -----------------------------------------------------------------------------
// AUTOMATION_MANUAL_RUN tells Automation.php to bypass its own lock-file guard.
// That guard is designed to coordinate concurrent player requests, whereas
// this script is the only execution source and is already protected by the
// overlap lock implemented above.
if (!defined('AUTOMATION_MANUAL_RUN')) {
define('AUTOMATION_MANUAL_RUN', true);
}
$cronMarker = $autoprefix . 'GameEngine/Prevention/cron_active.txt';
@file_put_contents($cronMarker, (string)time(), LOCK_EX);
$cronMarkerPath = $autoprefix . 'GameEngine/Prevention/cron_active.txt';
/**
* Clears the database class's request-level caches.
*
* The static caches in MYSQLi_DB are designed to live only for the duration
* of a single request. In a process that executes multiple ticks in sequence,
* they would remain populated, causing subsequent ticks to operate on stale
* data (for example, building levels already modified by the previous tick).
* Therefore, the caches are reset between ticks so that each tick starts with
* the same clean state as a new request.
*
* Only static array properties whose names contain "cache" are reset.
*/
function cron_reset_db_caches()
{
if (!class_exists('MYSQLi_DB')) {
return;
}
try {
$ref = new ReflectionClass('MYSQLi_DB');
foreach ($ref->getProperties(ReflectionProperty::IS_STATIC) as $prop) {
if (stripos($prop->getName(), 'cache') === false) {
continue;
}
$prop->setAccessible(true);
if (is_array($prop->getValue())) {
$prop->setValue(null, []);
}
}
} catch (Throwable $e) {
// If reflection fails, it is better to continue than to stop Automation.
}
}
/**
* A tick represents one complete execution of Automation.
*
* The first tick includes Automation.php (the file ends with "new Automation",
* so including it starts the processing automatically). Subsequent ticks simply
* instantiate the Automation class again, since include_once would not execute
* the file a second time.
*/
function cron_run_tick(&$firstTick, $autoprefix, $cronMarkerPath)
{
@file_put_contents($cronMarkerPath, (string)time(), LOCK_EX);
if ($firstTick) {
include_once($autoprefix . 'GameEngine/Automation.php');
$firstTick = false;
return;
}
cron_reset_db_caches();
new Automation();
}
// -----------------------------------------------------------------------------
// 6. Rulam automation-ul propriu-zis. Constructorul din Automation.php face toata
// treaba (procNewClimbers, buildComplete, trainingComplete, batalii, miscari...).
// 7. Execution
// -----------------------------------------------------------------------------
include_once($autoprefix . 'GameEngine/Automation.php');
$firstTick = true;
$startedAt = time();
$ticks = 0;
do {
$tickStart = time();
cron_run_tick($firstTick, $autoprefix, $cronMarkerPath);
$ticks++;
if ($loopSeconds <= 0) {
break;
}
// Is there enough time remaining in the invocation budget for another complete tick?
$elapsed = time() - $startedAt;
if (($elapsed + $tickSeconds) >= $loopSeconds) {
break;
}
// Sleep until the next tick, subtracting the time spent executing the current tick.
$spent = time() - $tickStart;
$sleep = $tickSeconds - $spent;
if ($sleep > 0) {
sleep($sleep);
}
} while (true);
// -----------------------------------------------------------------------------
// 8. Release the Lock
// -----------------------------------------------------------------------------
flock($runLockHandle, LOCK_UN);
fclose($runLockHandle);
if ($isCli) {
echo "cron: automation rulat la " . date('Y-m-d H:i:s') . "\n";
echo "cron: " . $ticks . " tick(uri) in " . (time() - $startedAt) . "s, terminat la " . date('Y-m-d H:i:s') . "\n";
}
+20
View File
@@ -22,6 +22,26 @@ define("ERROR_REPORT","%ERRORREPORT%");
%ERROR%
define('AUTOMATION_LOCK_FILE_NAME', 'automation.lck');
//////////////////////////////////
// ***** CRON / AUTOMATION *****//
//////////////////////////////////
// Automation ruleaza din cron.php (cron job pe server), nu din paginile
// jucatorilor. Vezi comentariile din cron.php pentru instalarea cron job-ului.
//
// CRON_LOOP_SECONDS = cat timp tine o invocare de cron.php.
// Multe hosturi nu permit cron mai des de 5 minute, iar Automation vrea sa
// ruleze la ~60s. De aceea o invocare ruleaza mai multe tick-uri la rand.
// 300 = potrivit pentru un cron "*/5 * * * *". Pune 0 daca hostul tau permite
// cron la fiecare minut (atunci o invocare = un singur tick).
// CRON_TICK_SECONDS = la cat timp se repeta un tick in interiorul invocarii.
define('CRON_LOOP_SECONDS', %CRONLOOP%);
define('CRON_TICK_SECONDS', %CRONTICK%);
// Cheie pentru apelarea cron.php prin HTTP (wget/curl sau un serviciu extern de
// cron). Rularea din linia de comanda (cron job cPanel) NU are nevoie de ea.
// Generata automat la instalare; pastrata la salvarile de configuratie din ACP.
define('CRON_KEY', '%CRONKEY%');
//////////////////////////////////
// ***** SERVER SETTINGS *****//
//////////////////////////////////
+7
View File
@@ -107,6 +107,13 @@ class Process {
$findReplace["%QTYPE%"] = $_POST['qtype'];
$findReplace["%BEGINNER%"] = $_POST['beginner'];
$findReplace["%STARTTIME%"] = time();
// Cheie aleatoare pentru apelul HTTP al cron.php (vezi CRON_KEY in config).
// Generata o singura data, la instalare; ACP-ul o pastreaza la resalvari.
$findReplace["%CRONKEY%"] = bin2hex(random_bytes(24));
// Valori implicite pentru ciclul intern al cron.php; editabile ulterior
// din ACP (Config -> Cron & Automation -> edit).
$findReplace["%CRONLOOP%"] = 300;
$findReplace["%CRONTICK%"] = 60;
$findReplace["%DOMAIN%"] = $_POST['domain'];
$findReplace["%HOMEPAGE%"] = $_POST['homepage'];
$findReplace["%SERVER%"] = $_POST['server'];