diff --git a/GameEngine/Automation.php b/GameEngine/Automation.php index f54ec828..a2795ac0 100644 --- a/GameEngine/Automation.php +++ b/GameEngine/Automation.php @@ -49,6 +49,7 @@ include_once("Database.php"); include_once("Data/buidata.php"); include_once("Data/unitdata.php"); include_once("Data/hero_full.php"); +include_once("Data/cp.php"); include_once("Units.php"); include_once("Battle.php"); include_once("Technology.php"); diff --git a/GameEngine/Automation/AutomationAccountMaintenance.php b/GameEngine/Automation/AutomationAccountMaintenance.php index 36d53685..eb4db6dc 100644 --- a/GameEngine/Automation/AutomationAccountMaintenance.php +++ b/GameEngine/Automation/AutomationAccountMaintenance.php @@ -329,6 +329,10 @@ trait AutomationAccountMaintenance { $database->cacheResourceLevels(array_keys($vilIDs)); foreach($villages as $village){ + // HOTFIX warning "$attri undefined": $attri se seteaza doar daca satul are + // un camp tip 18 (Stonemason); fara initializare, valoarea "scapa" din satul + // anterior (rezultatul final era acelasi, fiind un max, dar cu warning in log) + $attri = 0; $field = $database->getResourceLevel($village['wref'], false); for($i = 19; $i <= 40; $i++){ if($field['f'.$i.'t'] == 18){ diff --git a/GameEngine/Automation/AutomationBattleResolution.php b/GameEngine/Automation/AutomationBattleResolution.php index 23ee761b..009d8828 100644 --- a/GameEngine/Automation/AutomationBattleResolution.php +++ b/GameEngine/Automation/AutomationBattleResolution.php @@ -495,9 +495,14 @@ trait AutomationBattleResolution { $expArray = $database->getVillageFields($from['wref'], 'exp1, exp2, exp3'); $villexp = ($expArray['exp1'] == 0) ? 0 : (($expArray['exp2'] == 0) ? 1 : (($expArray['exp3'] == 0) ? 2 : 3)); + // HOTFIX "$cp0 undefined" (acelasi tipar ca FIX U1 din Units.php): tabelele $cp0..$cpN + // vin din Data/cp.php, incarcat pana acum doar de Session.php - deci pe traseul de CRON + // (fara Session) verificarea de CP la cucerire era OCOLITA silentios. Data/cp.php e + // acum inclus si in Automation.php; gardul ?? PHP_INT_MAX ramane fail-closed daca + // tabelul lipseste totusi (fara date de CP nu se poate cuceri), identic cu Settlers(). $mode = CP; - $cp_mode = $GLOBALS['cp' . $mode]; - $need_cps = $cp_mode[count($varray1) + 1]; + $cp_mode = $GLOBALS['cp' . $mode] ?? []; + $need_cps = $cp_mode[count($varray1) + 1] ?? PHP_INT_MAX; $user_cps = $database->getUserArray($from['owner'], 1)['cp']; if ($user_cps < $need_cps) { diff --git a/GameEngine/Automation/AutomationVillageUpkeep.php b/GameEngine/Automation/AutomationVillageUpkeep.php index 874c7dc4..a7ce1364 100644 --- a/GameEngine/Automation/AutomationVillageUpkeep.php +++ b/GameEngine/Automation/AutomationVillageUpkeep.php @@ -323,50 +323,81 @@ trait AutomationVillageUpkeep { private function updateStore() { global $database, $bid10, $bid38, $bid11, $bid39; - $result = mysqli_query($database->dblink, 'SELECT * FROM `' . TB_PREFIX . 'fdata`'); + // HOTFIX deadlock (log 20-23 iul): rutina scria toata vdata intr-o SINGURA + // tranzactie lunga, tinand lock-uri pe multe randuri => coliziuni fatale cu + // scrierile jucatorilor. Doua schimbari: (1) commit in transe de 200 de randuri + // (ferestre de lock scurte), (2) reincercare de maximum 3 ori la deadlock / lock + // timeout. Rutina e idempotenta (recalculeaza maxstore/maxcrop de la zero), deci + // o rulare partiala e inofensiva - urmatorul tick o completeaza. + for ($attempt = 1; $attempt <= 3; $attempt++) { + try { + $result = mysqli_query($database->dblink, 'SELECT * FROM `' . TB_PREFIX . 'fdata`'); - mysqli_begin_transaction($database->dblink); - while ($row = mysqli_fetch_assoc($result)) - { - $ress = $crop = 0; - for ($i = 19; $i < 40; ++$i) - { - //Warehouse - if ($row['f' . $i . 't'] == 10) + $pending = 0; + mysqli_begin_transaction($database->dblink); + while ($row = mysqli_fetch_assoc($result)) { - $ress += ((isset($bid10[$row['f' . $i]]) && isset($bid10[$row['f' . $i]]['attri'])) ? $bid10[$row['f' . $i]]['attri'] * STORAGE_MULTIPLIER : 0); - } + $ress = $crop = 0; + for ($i = 19; $i < 40; ++$i) + { + //Warehouse + if ($row['f' . $i . 't'] == 10) + { + $ress += ((isset($bid10[$row['f' . $i]]) && isset($bid10[$row['f' . $i]]['attri'])) ? $bid10[$row['f' . $i]]['attri'] * STORAGE_MULTIPLIER : 0); + } - //Great warehouse - if ($row['f' . $i . 't'] == 38) - { - $ress += ((isset($bid38[$row['f' . $i]]) && isset($bid38[$row['f' . $i]]['attri'])) ? $bid38[$row['f' . $i]]['attri'] * STORAGE_MULTIPLIER : 0); - } + //Great warehouse + if ($row['f' . $i . 't'] == 38) + { + $ress += ((isset($bid38[$row['f' . $i]]) && isset($bid38[$row['f' . $i]]['attri'])) ? $bid38[$row['f' . $i]]['attri'] * STORAGE_MULTIPLIER : 0); + } - //Granary - if ($row['f' . $i . 't'] == 11) - { - $crop += ((isset($bid11[$row['f' . $i]]) && isset($bid11[$row['f' . $i]]['attri'])) ? $bid11[$row['f' . $i]]['attri'] * STORAGE_MULTIPLIER : 0); - } + //Granary + if ($row['f' . $i . 't'] == 11) + { + $crop += ((isset($bid11[$row['f' . $i]]) && isset($bid11[$row['f' . $i]]['attri'])) ? $bid11[$row['f' . $i]]['attri'] * STORAGE_MULTIPLIER : 0); + } - //Great granary - if ($row['f' . $i . 't'] == 39) - { - $crop += ((isset($bid39[$row['f' . $i]]) && isset($bid39[$row['f' . $i]]['attri'])) ? $bid39[$row['f' . $i]]['attri'] * STORAGE_MULTIPLIER : 0); + //Great granary + if ($row['f' . $i . 't'] == 39) + { + $crop += ((isset($bid39[$row['f' . $i]]) && isset($bid39[$row['f' . $i]]['attri'])) ? $bid39[$row['f' . $i]]['attri'] * STORAGE_MULTIPLIER : 0); + } + } + + // no need for update, since we didn't find any warehouses or granaries + // and maximums would have been set to correct values inside prune* functions already + if ($ress == 0 && $crop == 0) continue; + + // maxstore nor maxcrop can go below the minimum threshold + if ($ress < STORAGE_BASE) $ress = STORAGE_BASE; + if ($crop < STORAGE_BASE) $crop = STORAGE_BASE; + + mysqli_query($database->dblink,'UPDATE `' . TB_PREFIX . 'vdata` SET `maxstore` = ' . (int) $ress . ', `maxcrop` = ' . (int) $crop . ' WHERE `wref` = ' . (int) $row['vref']); + + // commit in transe: tranzactii scurte = ferestre de lock mici + if (++$pending >= 200) { + mysqli_commit($database->dblink); + mysqli_begin_transaction($database->dblink); + $pending = 0; + } } - } - - // no need for update, since we didn't find any warehouses or granaries - // and maximums would have been set to correct values inside prune* functions already - if ($ress == 0 && $crop == 0) continue; - - // maxstore nor maxcrop can go below the minimum threshold - if ($ress < STORAGE_BASE) $ress = STORAGE_BASE; - if ($crop < STORAGE_BASE) $crop = STORAGE_BASE; - - mysqli_query($database->dblink,'UPDATE `' . TB_PREFIX . 'vdata` SET `maxstore` = ' . (int) $ress . ', `maxcrop` = ' . (int) $crop . ' WHERE `wref` = ' . (int) $row['vref']); - } mysqli_commit($database->dblink); + return; + } catch (mysqli_sql_exception $e) { + if (!in_array((int) $e->getCode(), [1213, 1205], true)) { + throw $e; // orice alta eroare ramane fatala, ca inainte + } + try { mysqli_rollback($database->dblink); } catch (mysqli_sql_exception $ignored) {} + if ($attempt === 3) { + // dupa 3 incercari renuntam la ACEST tick fara sa-l omoram; + // urmatorul tick reia calculul de la zero + error_log('Automation::updateStore: deadlock persistent dupa 3 incercari, amanat pentru tick-ul urmator'); + return; + } + usleep(150000 * $attempt); + } + } } private function regenerateOasisTroops() { diff --git a/GameEngine/Database/DatabaseBuildingQueries.php b/GameEngine/Database/DatabaseBuildingQueries.php index dc8dc5ec..2c6528ed 100644 --- a/GameEngine/Database/DatabaseBuildingQueries.php +++ b/GameEngine/Database/DatabaseBuildingQueries.php @@ -341,7 +341,14 @@ trait DatabaseBuildingQueries { */ function modifyBData($wid, $field, $levels, $tribe){ - list($wid, $field, $levels, $tribe) = $this->escape_input((int) $wid, (int) $field, (int) $levels, (int) $tribe); + // HOTFIX "array offset on int": $levels e un ARRAY [nivelNou, nivelVechi] (singurul + // apel: AutomationBattleResolution, dupa lovitura de catapulte). Cast-ul (int) de aici + // il transforma in 1, deci $levels[0] era null, "null == 0" era mereu TRUE si ramura + // else (ajustarea nivelului comenzilor din coada) era COD MORT - comenzile de + // constructie erau sterse mereu, chiar daca cladirea nu ajungea la nivelul 0. + // Pastram cast-ul pe celelalte argumente; elementele lui $levels devin int (SQL-safe). + list($wid, $field, $tribe) = $this->escape_input((int) $wid, (int) $field, (int) $tribe); + $levels = array_map('intval', is_array($levels) ? $levels : [$levels, 0]); if($levels[0] == 0){ $q = "SELECT id FROM " .TB_PREFIX. "bdata WHERE wid = $wid AND field = $field"; diff --git a/GameEngine/Database/DatabaseConnectionCore.php b/GameEngine/Database/DatabaseConnectionCore.php index d02eb533..f4079d8d 100644 --- a/GameEngine/Database/DatabaseConnectionCore.php +++ b/GameEngine/Database/DatabaseConnectionCore.php @@ -271,7 +271,24 @@ trait DatabaseConnectionCore { function query($query) { // REFACTORIZAT: contorizare centralizată $this->countQueryType($query); - return mysqli_query($this->dblink,$query); + + // HOTFIX deadlock (log 20-23 iul): pe PHP 8.1+ mysqli arunca mysqli_sql_exception, + // deci un deadlock InnoDB (1213) / lock wait timeout (1205) omora tot tick-ul de + // automation. Reincercam de maximum 3 ori cu pauza crescatoare; orice alta eroare + // se arunca mai departe neschimbata. Sigur fata de tranzactii: singurele tranzactii + // explicite din cod (updateStore, populateWorldData) folosesc mysqli_query direct, + // nu metoda aceasta - verificat pe tot repo-ul. + $attempt = 0; + while (true) { + try { + return mysqli_query($this->dblink, $query); + } catch (mysqli_sql_exception $e) { + if (!in_array((int) $e->getCode(), [1213, 1205], true) || ++$attempt >= 3) { + throw $e; + } + usleep(150000 * $attempt); // 150ms, apoi 300ms + } + } } function RemoveXSS($val) { diff --git a/GameEngine/Database/DatabaseVillageQueries.php b/GameEngine/Database/DatabaseVillageQueries.php index 6eb9de27..0d96cac6 100644 --- a/GameEngine/Database/DatabaseVillageQueries.php +++ b/GameEngine/Database/DatabaseVillageQueries.php @@ -1846,18 +1846,26 @@ trait DatabaseVillageQueries { // clear expansion slots, if this village is an expansion of any other village $this->clearExpansionSlot($wref, 1); + // HOTFIX warning-uri t1..t11/wref/id: pe calea cu array + cache plin, + // getPrisoners() intoarce INTREGUL cache (inclusiv intrari goale [] pentru + // sate fara prizonieri), nu doar satele cerute - de aici cheile lipsa. + // Gardam randurile incomplete; pentru randurile valide comportamentul e identic. + // ANOMALIE de urmarit separat: "return self::$prisonersCache" din getPrisoners() + // poate livra si prizonierii ALTOR sate decat cele cerute. $getprisoners = $this->getPrisoners($wref); foreach($getprisoners as $pris) { + if (!is_array($pris) || !isset($pris['wref'], $pris['id'])) continue; $troops = 0; - for($i = 1; $i < 12; $i++) $troops += $pris['t'.$i]; + for($i = 1; $i < 12; $i++) $troops += (int) ($pris['t'.$i] ?? 0); $this->modifyUnit($pris['wref'], ["99o"], [$troops], [0]); $this->deletePrisoners($pris['id']); } $getprisoners = $this->getPrisoners($wref, 1); foreach($getprisoners as $pris) { + if (!is_array($pris) || !isset($pris['wref'], $pris['id'])) continue; $troops = 0; - for($i = 1; $i < 12; $i++) $troops += $pris['t'.$i]; + for($i = 1; $i < 12; $i++) $troops += (int) ($pris['t'.$i] ?? 0); $this->modifyUnit($pris['wref'], ["99o"], [$troops], [0]); $this->deletePrisoners($pris['id']); }