From 1bab6640d018df7d4cd9c6da5d424cec3e79e6b9 Mon Sep 17 00:00:00 2001 From: novgorodschi catalin Date: Thu, 3 Sep 2026 13:51:05 +0300 Subject: [PATCH] Some hot fixes!!! Some hot fixes!!! --- .../AutomationAccountMaintenance.php | 7 - .../Automation/AutomationBattleResolution.php | 125 ++++-------------- .../Automation/AutomationBuildQueue.php | 7 - .../Automation/AutomationStarvation.php | 7 +- .../Automation/AutomationVillageUpkeep.php | 11 ++ .../Database/DatabaseConnectionCore.php | 21 ++- GameEngine/Database/DatabaseTroopQueries.php | 28 +++- 7 files changed, 87 insertions(+), 119 deletions(-) diff --git a/GameEngine/Automation/AutomationAccountMaintenance.php b/GameEngine/Automation/AutomationAccountMaintenance.php index 341cd8f6..69faaff6 100644 --- a/GameEngine/Automation/AutomationAccountMaintenance.php +++ b/GameEngine/Automation/AutomationAccountMaintenance.php @@ -86,10 +86,6 @@ trait AutomationAccountMaintenance { if(AUTO_DEL_INACTIVE) { $time = time() - UN_ACT_TIME; - - // BUG REPARAT: lista era 1,2,3 - conturile inactive ale triburilor noi - // (huni, egipteni, spartani, vikingi) nu se stergeau niciodata si - // ramaneau pe harta la nesfarsit. $q = "INSERT INTO ".TB_PREFIX."deleting SELECT id, UNIX_TIMESTAMP() FROM ".TB_PREFIX."users WHERE timestamp < $time AND tribe IN(1, 2, 3, 6, 7, 8, 9)"; $database->query($q); } @@ -324,9 +320,6 @@ 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++){ diff --git a/GameEngine/Automation/AutomationBattleResolution.php b/GameEngine/Automation/AutomationBattleResolution.php index f806d49a..e0e362a2 100644 --- a/GameEngine/Automation/AutomationBattleResolution.php +++ b/GameEngine/Automation/AutomationBattleResolution.php @@ -70,10 +70,10 @@ trait AutomationBattleResolution { // update all that needs updating $database->setVillageLevel($data['to'], $fieldsToSet, $fieldValuesToSet); - - $buildarray = $GLOBALS["bid".$tbgid]; + + $buildarray = $GLOBALS["bid".$tbgid] ?? null; - if ( isset( $buildarray[$newLevel] ) ) { + if ( is_array($buildarray) && isset( $buildarray[$newLevel] ) ) { // (great) warehouse level was changed if ($tbgid == 10 || $tbgid == 38) { $database->setMaxStoreForVillage($data['to'], $buildarray[$newLevel]['attri']); @@ -317,13 +317,6 @@ trait AutomationBattleResolution { if ($i == 41) $i = 99; if ($bdo['f'.$i] > 0 && $i != 40) $list[] = $i; } - // FIX (PHP log): daca $list e gol (niciun target alternativ - // cu nivel>0), rand(0, count($list)-1) devine rand(0,-1). - // PHP NU arunca eroare aici (verificat pe 8.3) - intoarce - // pseudo-random 0 sau -1, ambele chei inexistente in $list, - // deci "Undefined array key -1"/0 si $catapultTarget2=null. - // Pastram fallback-ul 99 folosit deja mai sus (linia ~301) - // pentru cazul "niciun target gasit". if (!empty($list)) { $catapultTarget2 = $list[ rand(0, count($list) - 1) ]; } else { @@ -630,11 +623,6 @@ 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] ?? PHP_INT_MAX; @@ -672,11 +660,6 @@ trait AutomationBattleResolution { $reducedLoyalty /= $battlepart['moralBonus']; - // Bug fix: Brewery (35) is capital-only but empire-wide — its effect - // must be checked on the attacker's CAPITAL, not on $data['from'] (the - // launching village, which may not be the capital at all), and only - // while a Mead-Festival is actually active there, not just because - // the Brewery has been built (it has no permanent effect). if ($owntribe == 2) { $attackerCapital = $database->getVillage($from['owner'], 3); if ($attackerCapital && (int)$attackerCapital['festival'] > $time && $this->getTypeLevel(35, $attackerCapital['wref']) > 0) { @@ -709,14 +692,6 @@ trait AutomationBattleResolution { $database->setVillageFields($data['to'], ['loyalty', 'owner'], [0, $database->getVillageField($data['from'], 'owner')]); - // Milestones: first WW village ever conquered, and — separately — - // first village ever conquered FROM ANOTHER PLAYER (not from - // Natars). $to is this function's own parameter (not re-fetched), - // so $to['natar']/$to['owner'] still reflect the village's state - // from BEFORE this conquest, which is exactly what we need here. - // natar==1 marks one of the 13 pre-built WW conquest targets (see - // Artifacts::createWWVillages()) — Natars' capital and artifact/ - // plan villages are natar=0, so this check cannot misfire on those. if (defined('NEW_FUNCTIONS_MILESTONES') && NEW_FUNCTIONS_MILESTONES) { $newOwner = $database->getVillageField($data['from'], 'owner'); if ((int)($to['natar'] ?? 0) === 1) { @@ -1241,25 +1216,6 @@ trait AutomationBattleResolution { if ($unitlist) { $owndead['hero'] = (isset($battlepart['deadherodef']) ? $battlepart['deadherodef'] : ''); - - /** - * BUG FIXED (issue #379): units.hero ended up at -1. - * - * The value is still reported (it feeds the battle report and the - * points), but it must NOT be subtracted from units.hero here any - * more. Battle::applyHeroBattleDamage() already clears the column - * itself ("UPDATE units u JOIN vdata v SET u.hero = 0 WHERE - * v.owner = ") the moment the hero dies, and that runs - * inside calculateBattle(), i.e. BEFORE this method. - * - * So the column went 1 -> 0 there and 0 - 1 -> -1 here: the - * village kept showing "-1 Hero" in the troop list while the - * Hero's Mansion correctly offered a revive. - * - * One writer only: the death is owned by applyHeroBattleDamage(), - * which is also the only place that knows about the hero's other - * possible locations (reinforcements, another village). - */ } // modify units in DB @@ -1356,21 +1312,6 @@ trait AutomationBattleResolution { } if ($enforce['hero'] > 0) { - /** - * BUG FIXED (issue #379), same double subtraction as in - * applyOwnDefenceCasualties(): a reinforcement hero who died - * was already removed by Battle::applyHeroBattleDamage() - * ("UPDATE enforcement e JOIN vdata v SET e.hero = 0"), so - * subtracting him again here drove enforcement.hero to -1. - * - * $enforce still reads 1 because getEnforceVillage() answers - * from the request cache filled before the battle, so the - * stale row hid the problem instead of preventing it. - * - * The dead count below is kept: it is what the reinforcement - * report and $wrong (which decides whether the wiped-out - * reinforcement row is deleted) are built from. - */ $dead['hero'] = $battlepart['deadheroref'][$enforce['id']]; $alldead['hero'] += $dead['hero']; $wrong = $dead['hero'] != $enforce['hero']; @@ -1832,19 +1773,19 @@ trait AutomationBattleResolution { global $database, $units; $DefenderUserData = $this->getCachedUser($database->getVillageField($data['to'],"owner"),1); - $DefenderID = $DefenderUserData["id"]; - $targettribe = $DefenderUserData["tribe"]; - $targetally = $DefenderUserData["alliance"]; + $DefenderID = $DefenderUserData["id"] ?? null; + $targettribe = $DefenderUserData["tribe"] ?? null; + $targetally = $DefenderUserData["alliance"] ?? null; $to = $database->getMInfo($data['to']); $toF = $database->getVillage($data['to']); $conqureby = 0; - $NatarCapital = ($toF['owner'] == 3 && $toF['capital'] == 1); + $NatarCapital = (($toF['owner'] ?? null) == 3 && ($toF['capital'] ?? null) == 1); if(!isset($to['name']) || empty($to['name'])) $to['name'] = "[?]"; $DefenderUnit = $database->getUnit($data['to']); - $evasion = $toF["evasion"]; - $maxevasion = $DefenderUserData["maxevasion"]; - $gold = $DefenderUserData["gold"]; + $evasion = $toF["evasion"] ?? null; + $maxevasion = $DefenderUserData["maxevasion"] ?? null; + $gold = $DefenderUserData["gold"] ?? null; $cannotsend = false; $movements = $database->getMovement(34, $data['to'], 1); @@ -2126,10 +2067,10 @@ trait AutomationBattleResolution { $this->pruneResource(); $villageData = $database->getVillageFields($conqureby, 'clay, iron, wood, crop', false); - $totclay = intval($villageData['clay'] / 10); - $totiron = intval($villageData['iron'] / 10); - $totwood = intval($villageData['wood'] / 10); - $totcrop = intval($villageData['crop'] / 10); + $totclay = intval(($villageData['clay'] ?? 0) / 10); + $totiron = intval(($villageData['iron'] ?? 0) / 10); + $totwood = intval(($villageData['wood'] ?? 0) / 10); + $totcrop = intval(($villageData['crop'] ?? 0) / 10); }else{ // work out available resources. $this->updateORes($data['to']); @@ -2205,8 +2146,6 @@ trait AutomationBattleResolution { if ($canqured == 3 && $troopcount == 0) { if ($type == 3) { $Oloyaltybefore = intval($to['loyalty']); - //$database->modifyOasisLoyalty($data['to']); - //$OasisInfo = $database->getOasisInfo($data['to']); $Oloyaltynow = intval($database->modifyOasisLoyalty($data['to']));//intval($OasisInfo['loyalty']); $info_hero = $hero_pic.",".rc_tok('RC_HERO_REDUCED_OASIS_LOYALTY', $Oloyaltynow, $Oloyaltybefore).$xp; } @@ -2397,7 +2336,7 @@ trait AutomationBattleResolution { $database->modifyPointsAlly($targetally, 'RR', $totalstolentaken); $database->modifyPointsAlly($ownally, 'RR', $totalstolengain); } - }else{ //fix by ronix if only 1 chief left to conqured - don't add with zero enforces + }else{ if($totalsend_att - ($totaldead_att + (isset($totaltraped_att) ? $totaltraped_att : 0)) > 1){ $database->addEnforce2($data, $owntribe, $troopsdead[1], $troopsdead[2], $troopsdead[3], $troopsdead[4], $troopsdead[5], $troopsdead[6], $troopsdead[7], $troopsdead[8], $troopsdead[9], $troopsdead[10], $troopsdead[11]); } @@ -2524,21 +2463,8 @@ trait AutomationBattleResolution { $tblevel = $vt['tblevel']; $stonemason = $vt['stonemason']; - // Issue #298: the target village no longer exists — it was razed - // either earlier in this same batch ($razedTargets), or in an - // earlier tick whose still-in-flight follow-up waves DelVillage() - // failed to bounce. getMInfo() then returns NULL vdata columns - // ($to['wref'] is NULL), so resolving a battle here would fight a - // phantom village and compute the return trip from NULL coordinates - // — a bogus arrival time that strands the troops in an endless loop - // (report against "[?]"). Bounce the whole army straight home - // instead, exactly like DelVillage() does for in-flight attacks, - // and mark the movement processed so it stops being re-fetched. if (isset($razedTargets[$data['to']]) || empty($to['wref'])) { - // only own the bounce if DelVillage() hasn't already handled it - // (setMovementProc() returns true only when it flips proc 0->1), - // so we never create a duplicate return movement - if ($this->setMovementProc($data['moveid'])) { + if ($database->setMovementProc($data['moveid'])) { $bounceTime = $units->getWalkingTroopsTime($from['wref'], $data['to'], $from['owner'], $owntribe, $data, 1, 't'); $bounceEnd = $database->getArtifactsValueInfluence($from['owner'], $from['wref'], 2, $bounceTime) + $AttackArrivalTime; $database->addMovement(4, $data['to'], $from['wref'], $data['ref'], $AttackArrivalTime, $bounceEnd); @@ -2566,7 +2492,8 @@ trait AutomationBattleResolution { $spy_pic = $atkUnits['spy_pic']; $hero_pic = $atkUnits['hero_pic']; - }else{ //It's an oasis + }else{ + //It's an oasis // target + battle environment — extracted to resolveOasisTarget() [#155] $ot = $this->resolveOasisTarget($data, $dataarray, $data_num, $owntribe); $DefenderID = $ot['DefenderID']; @@ -2754,6 +2681,7 @@ trait AutomationBattleResolution { $alldead = []; for($i = 1; $i <= 90; $i++) $alldead[$i] = 0; + $alldead['hero'] = 0; //kill own defence — extracted to applyOwnDefenceCasualties() [#155] $owndead = $this->applyOwnDefenceCasualties($data, $targettribe, $battlepart); @@ -2776,8 +2704,6 @@ trait AutomationBattleResolution { if (empty($alldead['hero'])) $alldead['hero'] = 0; if (empty($owndead['hero'])) $owndead['hero'] = 0; - // sursa autoritativa: rezultatul luptei (owndead['hero'] se poate pierde - // pe unele cai de executie, iar raportul afisa eroul aparator ca viu) $deadhero = (int)(isset($battlepart['deadherodef']) && $battlepart['deadherodef'] > 0 ? $battlepart['deadherodef'] : $owndead['hero']); @@ -2907,12 +2833,6 @@ trait AutomationBattleResolution { // delete the target village if it was destroyed — extracted to handleVillageDestruction() [#155] $this->handleVillageDestruction($village_destroyed, $can_destroy, $data, $to, $varray); - // remember the razed tile so any follow-up wave still queued in - // this same batch bounces home instead of fighting a phantom - // (now-deleted) village. Needed on top of the $to['wref'] check - // because getMInfo() is cached: a same-batch wave would still see - // the stale "alive" village row — see the guard right after - // resolveVillageTarget() (issue #298). if ($village_destroyed == 1 && $can_destroy == 1) { $razedTargets[$data['to']] = true; } @@ -3104,6 +3024,8 @@ trait AutomationBattleResolution { // the gold-club +25% crop bonus (b4) keyed on the village owner. private function bountyGetResourceProd($resArray, $oasisNumber, $resourceType) { global $bid1, $bid2, $bid3, $bid4, $bid5, $bid6, $bid7, $bid8, $bid9, $database; + + if (!is_array($resArray)) $resArray = []; $prodBid = [1 => $bid1, 2 => $bid2, 3 => $bid3, 4 => $bid4][$resourceType]; $boosterBid = [5 => $bid5, 6 => $bid6, 7 => $bid7, 8 => $bid8, 9 => $bid9]; @@ -3114,8 +3036,9 @@ trait AutomationBattleResolution { $holders = []; $boosterLevels = array_fill_keys($boosterTypes, 0); for($i = 1; $i <= 38; $i++) { - if($resArray['f'.$i.'t'] == $resourceType) $holders[] = 'f'.$i; - if(isset($boosterLevels[$resArray['f'.$i.'t']])) $boosterLevels[$resArray['f'.$i.'t']] = $resArray['f'.$i]; + $fieldType = $resArray['f'.$i.'t'] ?? null; + if($fieldType == $resourceType) $holders[] = 'f'.$i; + if($fieldType !== null && isset($boosterLevels[$fieldType])) $boosterLevels[$fieldType] = $resArray['f'.$i] ?? 0; } foreach($holders as $holder) $prod += $prodBid[$resArray[$holder]]['prod']; diff --git a/GameEngine/Automation/AutomationBuildQueue.php b/GameEngine/Automation/AutomationBuildQueue.php index b84c0002..aa407a80 100644 --- a/GameEngine/Automation/AutomationBuildQueue.php +++ b/GameEngine/Automation/AutomationBuildQueue.php @@ -219,13 +219,6 @@ trait AutomationBuildQueue { $buildarray = $GLOBALS["bid".$type]; - // FIX: capacitatea de depozitare se RECALCULEAZA din cladirile - // ramase, dupa ce nivelul nou e scris mai jos. Scaderea de - // dinainte lua `attri` al nivelului, dar `attri` e capacitatea - // TOTALA la acel nivel (1200, 1700, 2300...), nu incrementul - - // deci taia prea mult. Ignora si STORAGE_MULTIPLIER. Efectul era - // ascuns de pragul STORAGE_BASE si "reparat" abia la urmatoarea - // rulare a lui updateStore(). $needsStorageRecalc = in_array($type, [10, 11, 38, 39]); if ($level == 1) $clear = ",f".$vil['buildnumber']."t=0"; diff --git a/GameEngine/Automation/AutomationStarvation.php b/GameEngine/Automation/AutomationStarvation.php index c680f1da..7ca1fabc 100644 --- a/GameEngine/Automation/AutomationStarvation.php +++ b/GameEngine/Automation/AutomationStarvation.php @@ -261,7 +261,12 @@ trait AutomationStarvation { } break; case 2: // own units - $database->modifyUnit($starv['wref'], array_keys($killedUnits), array_values($killedUnits), [0]); + // BUG FIXED: modifyUnit() indexes $array_mode[$i] per unit (no scalar + // broadcast), but only one mode element was passed here regardless of how + // many unit types starved -> "Undefined array key 1"/"2" whenever more than + // one troop type died of starvation in the same tick. All deaths use mode 0 + // (subtract), so fill one 0 per unit - identical resulting SQL, no warning. + $database->modifyUnit($starv['wref'], array_keys($killedUnits), array_values($killedUnits), array_fill(0, count($killedUnits), 0)); break; case 3: // moving attacks. if($totalKilledUnits < $totalUnits){ diff --git a/GameEngine/Automation/AutomationVillageUpkeep.php b/GameEngine/Automation/AutomationVillageUpkeep.php index 39b8aec2..1442d7b4 100644 --- a/GameEngine/Automation/AutomationVillageUpkeep.php +++ b/GameEngine/Automation/AutomationVillageUpkeep.php @@ -353,6 +353,17 @@ trait AutomationVillageUpkeep { //Get village infos $villageInfoArray = $database->getVillage($bountywid); + + // BUG FIXED: $bountywid can be a village razed earlier in the same batch + // (either the attack target itself, or the conqureby owner of a captured + // oasis - both call updateRes() from AutomationBattleResolution.php). + // getVillage() then returns null and every read below ('natar', 'pop', + // 'lastupdate') threw "Trying to access array offset on null", cascading + // into bountyGetResourceProd() with a null $resArray too. There is + // nothing meaningful to update for a village that no longer exists, so + // skip entirely - modifyResource()/updateVillage() below would just be + // a harmless no-op UPDATE matching zero rows anyway. + if (!is_array($villageInfoArray)) return; //Get building and resource fields array $resArray = $database->getResourceLevel($bountywid, false); diff --git a/GameEngine/Database/DatabaseConnectionCore.php b/GameEngine/Database/DatabaseConnectionCore.php index f4079d8d..97ebadf1 100644 --- a/GameEngine/Database/DatabaseConnectionCore.php +++ b/GameEngine/Database/DatabaseConnectionCore.php @@ -278,12 +278,31 @@ trait DatabaseConnectionCore { // 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. + // + // HOTFIX "MySQL server has gone away" (log 1-3 sep, cod 2006/CR_SERVER_GONE_ERROR + // sau 2013/CR_SERVER_LOST): conexiunea poate cadea intre tick-urile ciclului intern + // al cron.php (pana la CRON_LOOP_SECONDS=300s), la fel de fatal pe PHP 8.1+. Cod + // separat de cel de deadlock (reconectam, nu doar reincercam): $this->reconnect() + // (deja existent in acest trait) inchide si redeschide $this->dblink inainte de a + // repeta interogarea. Maxim 2 reconectari, dupa care aruncam eroarea mai departe. $attempt = 0; + $reconnectAttempt = 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) { + $code = (int) $e->getCode(); + + if (in_array($code, [2006, 2013], true)) { + if (++$reconnectAttempt > 2) { + throw $e; + } + $this->reconnect(); + usleep(150000 * $reconnectAttempt); + continue; + } + + if (!in_array($code, [1213, 1205], true) || ++$attempt >= 3) { throw $e; } usleep(150000 * $attempt); // 150ms, apoi 300ms diff --git a/GameEngine/Database/DatabaseTroopQueries.php b/GameEngine/Database/DatabaseTroopQueries.php index 33792ba4..fe31432c 100644 --- a/GameEngine/Database/DatabaseTroopQueries.php +++ b/GameEngine/Database/DatabaseTroopQueries.php @@ -692,7 +692,11 @@ trait DatabaseTroopQueries { // return a single value if (!$array_passed) { - self::$villageFromReinforcementsCache[$vid[0].$from[0]] = $result[0]; + // BUG FIXED: $result is empty (no reinforcement row for this vid/from + // pair - a normal, common outcome) -> $result[0] doesn't exist -> + // "Undefined array key 0". Null-coalesce; the cached value stays null + // exactly as before, just without the warning on every miss. + self::$villageFromReinforcementsCache[$vid[0].$from[0]] = $result[0] ?? null; } else { if ($result && count($result)) { foreach ( $result as $record ) { @@ -762,7 +766,13 @@ trait DatabaseTroopQueries { } else if ($mode == 3) { $q = "SELECT e.*,o.conqured,o.wref,o.high, o.owner as ownero, v.owner as ownerv FROM ".TB_PREFIX."enforcement as e LEFT JOIN ".TB_PREFIX."odata as o ON e.vref=o.wref LEFT JOIN ".TB_PREFIX."vdata as v ON e.from=v.wref where o.conqured IN(".implode(', ', $ref).") AND o.owner=v.owner"; } - $result = $this->mysqli_fetch_all(mysqli_query($this->dblink,$q)); + // BUG FIXED (fatal, log 3 sep): raw mysqli_query() here bypassed $this->query(), + // the only place with retry/reconnect logic - when the connection drops between + // cron.php's internal ticks (cod 2006/2013, "MySQL server has gone away"), this + // call threw an uncaught mysqli_sql_exception straight from Technology::getUpkeep(), + // killing the whole automation tick. Routed through $this->query() (identical + // return value) so it now benefits from the reconnect-and-retry added there. + $result = $this->mysqli_fetch_all($this->query($q)); // return a single value if (!$array_passed) { @@ -897,6 +907,20 @@ trait DatabaseTroopQueries { $mode = [(int) $mode]; } + // BUG FIXED: this only normalized $amt/$mode into arrays when $unit itself + // wasn't one. AutomationStarvation.php calls this with $unit/$amt as arrays + // (array_keys/array_values($killedUnits)) but $mode as a bare scalar (0, + // meaning "subtract, for all of them") - $mode stayed an int, so + // "(int) $mode[$index]" below tried to array-offset an int ("Trying to + // access array offset on int"). Broadcast a scalar $amt/$mode to match + // $unit's length; already-matching arrays are left untouched. + if (!is_array($amt)) { + $amt = array_fill(0, count($unit), (int) $amt); + } + if (!is_array($mode)) { + $mode = array_fill(0, count($unit), (int) $mode); + } + foreach ($unit as $index => $unitType) { $unitType = ($unitType != 'hero' ? 'u' . $this->escape($unitType) : $unitType);