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
+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);
}
}
?>