diff --git a/GameEngine/Automation/AutomationBattleResolution.php b/GameEngine/Automation/AutomationBattleResolution.php index e0e362a2..28711bd5 100644 --- a/GameEngine/Automation/AutomationBattleResolution.php +++ b/GameEngine/Automation/AutomationBattleResolution.php @@ -70,7 +70,15 @@ trait AutomationBattleResolution { // update all that needs updating $database->setVillageLevel($data['to'], $fieldsToSet, $fieldValuesToSet); - + + // BUG FIXED: $tbgid is 0 when the catapult targets an already-empty field + // slot (nothing ever built there) - $newLevel==0 still runs this whole + // "destroyed" branch even though tblevel was already 0 (see the guard right + // above only skips the build-queue update, not this block). There is no + // "bid0" data array (building type IDs start at 1), so $GLOBALS["bid0"] + // doesn't exist -> "Undefined global variable $bid0". Null-coalescing keeps + // the exact same downstream result ($buildarray stays effectively empty, + // so the isset() check below is still false) without the warning. $buildarray = $GLOBALS["bid".$tbgid] ?? null; if ( is_array($buildarray) && isset( $buildarray[$newLevel] ) ) { @@ -317,6 +325,13 @@ 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 { @@ -623,6 +638,11 @@ 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; @@ -660,6 +680,11 @@ 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) { @@ -692,6 +717,14 @@ 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) { @@ -1216,6 +1249,25 @@ 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 @@ -1312,6 +1364,21 @@ 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']; @@ -1773,6 +1840,13 @@ trait AutomationBattleResolution { global $database, $units; $DefenderUserData = $this->getCachedUser($database->getVillageField($data['to'],"owner"),1); + // BUG FIXED: when the target village was razed mid-batch (issue #298 scenario), + // getVillageField(...,"owner") returns 0 -> getCachedUser(0) finds no user -> + // $DefenderUserData is null, and getVillage() below also returns null (no vdata + // row). Every read from them then threw "Trying to access array offset on null". + // The razed-target bounce-home check downstream (empty($to['wref'])) already + // handles this case correctly - it just needs to survive getting there without + // warnings, so null-coalesce each read here; values stay null exactly as before. $DefenderID = $DefenderUserData["id"] ?? null; $targettribe = $DefenderUserData["tribe"] ?? null; $targetally = $DefenderUserData["alliance"] ?? null; @@ -1939,7 +2013,8 @@ trait AutomationBattleResolution { $targettribe, $att_tribe, $Attacker, $AttackerHeroID, $Defender, $DefendersHeroID, $toF, $from, $targetally, $ownally, - &$heroxp, &$defheroxp + &$heroxp, &$defheroxp, + array $defensePointShares = [] ) { global $database; @@ -1988,11 +2063,101 @@ trait AutomationBattleResolution { // we don't need these two variables anymore unset($AttackerHeroID, $DefendersHeroID); - $database->modifyPoints( - $toF['owner'], - ['dpall', 'dp'], - [$totalpoint_def, $totalpoint_def] - ); + // BUG FIXED (defense points awarded to the wrong player(s)): $totalpoint_def + // is the total defense-point value for the WHOLE battle (every defender's + // troops combined) - it was previously credited entirely to $toF['owner'] + // (the attacked village's owner) even when other players' reinforcements + // did some or all of the actual defending. $defensePointShares (from + // Battle::computeDefenderForces(), threaded through calculateBattle()'s + // result as 'DefensePointShares') maps owner id -> that owner's own + // dp+cdp contribution to the battle. Split $totalpoint_def proportionally + // by each owner's share of the total, and credit each owner (and each + // owner's alliance) individually instead of dumping it all on one player. + // + // Fallback: if the breakdown is empty or sums to 0 (e.g. a pure scout + // skirmish with no measurable dp/cdp, or the caller didn't pass one), + // preserve the exact previous behavior - credit $toF['owner']/$targetally + // in full - rather than silently discarding the points. + $totalDefenseShare = array_sum($defensePointShares); + + if ($totalDefenseShare <= 0) { + + $database->modifyPoints( + $toF['owner'], + ['dpall', 'dp'], + [$totalpoint_def, $totalpoint_def] + ); + + $database->modifyPointsAlly( + $targetally, + ['Adp', 'dp'], + [$totalpoint_def, $totalpoint_def] + ); + + } else { + + $perAlliancePoints = []; + $allianceCache = []; + $creditedTotal = 0; + + // filter down to valid, positive shares FIRST, so "last owner gets + // the rounding remainder" below always lands on someone who is + // actually credited - picking it from the unfiltered list could + // land on a skipped (invalid or zero-share) entry and lose the + // remainder entirely. + $validShares = array_filter( + $defensePointShares, + function ($share, $ownerId) { return (int) $ownerId > 0 && $share > 0; }, + ARRAY_FILTER_USE_BOTH + ); + + $ownerIds = array_keys($validShares); + $lastOwnerId = end($ownerIds); + + foreach ($validShares as $ownerId => $share) { + + $ownerId = (int) $ownerId; + + // last owner in the split gets the rounding remainder, so the + // sum of everything credited always equals $totalpoint_def + // exactly (no points lost or invented to floating-point rounding) + if ($ownerId === $lastOwnerId) { + $ownerPoints = $totalpoint_def - $creditedTotal; + } else { + $ownerPoints = (int) round(($share / $totalDefenseShare) * $totalpoint_def); + } + + $creditedTotal += $ownerPoints; + + if ($ownerPoints == 0) { + continue; + } + + $database->modifyPoints( + $ownerId, + ['dpall', 'dp'], + [$ownerPoints, $ownerPoints] + ); + + if (!isset($allianceCache[$ownerId])) { + $allianceCache[$ownerId] = (int) $database->getUserField($ownerId, "alliance", 1); + } + + $ownerAlliance = $allianceCache[$ownerId]; + + if ($ownerAlliance > 0) { + $perAlliancePoints[$ownerAlliance] = ($perAlliancePoints[$ownerAlliance] ?? 0) + $ownerPoints; + } + } + + foreach ($perAlliancePoints as $allianceId => $alliPoints) { + $database->modifyPointsAlly( + $allianceId, + ['Adp', 'dp'], + [$alliPoints, $alliPoints] + ); + } + } $database->modifyPoints( $from['owner'], @@ -2000,12 +2165,6 @@ trait AutomationBattleResolution { [$totalpoint_att, $totalpoint_att] ); - $database->modifyPointsAlly( - $targetally, - ['Adp', 'dp'], - [$totalpoint_def, $totalpoint_def] - ); - $database->modifyPointsAlly( $ownally, ['Aap', 'ap'], @@ -2067,6 +2226,11 @@ trait AutomationBattleResolution { $this->pruneResource(); $villageData = $database->getVillageFields($conqureby, 'clay, iron, wood, crop', false); + // BUG FIXED: $conqureby (the village about to receive the oasis) can itself + // have been razed earlier in the same batch, in which case getVillageFields() + // returns null and every read below threw "Trying to access array offset on + // null". Null-coalescing to 0 keeps the same effective result as before + // (intval(null / 10) already evaluated to 0) without the warning. $totclay = intval(($villageData['clay'] ?? 0) / 10); $totiron = intval(($villageData['iron'] ?? 0) / 10); $totwood = intval(($villageData['wood'] ?? 0) / 10); @@ -2146,6 +2310,8 @@ 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; } @@ -2336,7 +2502,7 @@ trait AutomationBattleResolution { $database->modifyPointsAlly($targetally, 'RR', $totalstolentaken); $database->modifyPointsAlly($ownally, 'RR', $totalstolengain); } - }else{ + }else{ //fix by ronix if only 1 chief left to conqured - don't add with zero enforces 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]); } @@ -2463,7 +2629,26 @@ 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 + // + // BUG FIXED: setMovementProc() lives on $database (DatabaseMovementQueries), + // not on Automation - "$this->" was throwing "Call to undefined method + // Automation::setMovementProc()" (fatal, killed the whole automation tick) + // every time this razed-target bounce path was hit. Every other call site + // in this file already uses $database->setMovementProc(). 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; @@ -2492,8 +2677,7 @@ 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']; @@ -2681,6 +2865,12 @@ trait AutomationBattleResolution { $alldead = []; for($i = 1; $i <= 90; $i++) $alldead[$i] = 0; + // BUG FIXED: 'hero' key was only defensively set AFTER applyReinforcementCasualties() + // ran (see the guard a few lines below), but that function itself does + // "$alldead['hero'] += $dead['hero']" (by reference) as soon as a reinforcement + // hero dies - triggering "Undefined array key 'hero'" on every such battle. + // Initializing it here, like the numeric 1..90 keys above, removes the warning + // with zero behavior change (0 + amount === the previous auto-vivified null + amount). $alldead['hero'] = 0; //kill own defence — extracted to applyOwnDefenceCasualties() [#155] @@ -2704,6 +2894,8 @@ 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']); @@ -2758,13 +2950,18 @@ trait AutomationBattleResolution { $troopsdead11 = $dead11; // hero XP, player points and alliance points — extracted to calculateHeroXpAndPoints() [#155] + // BUG FIXED: pass the per-owner defense breakdown through so + // defense points get split across village owner + reinforcers + // instead of all going to $toF['owner'] alone (see the fix + // inside calculateHeroXpAndPoints() itself for the full story). $totaldead_def = $this->calculateHeroXpAndPoints( $alldead, $owndead, [$dead1, $dead2, $dead3, $dead4, $dead5, $dead6, $dead7, $dead8, $dead9, $dead10, $dead11], $targettribe, $att_tribe, $Attacker, $AttackerHeroID, $Defender, $DefendersHeroID, $toF, $from, $targetally, $ownally, - $heroxp, $defheroxp + $heroxp, $defheroxp, + $battlepart['DefensePointShares'] ?? [] ); // resources lootable after cranny protection — extracted to resolveResourcesAfterBattle() [#155] @@ -2833,6 +3030,12 @@ 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; } @@ -3024,7 +3227,13 @@ 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; - + + // BUG FIXED: defensive fallback in case a caller ever passes a razed + // village's null resource array (updateRes() now guards its own call + // path, but this closes the loop here too) - treat as "no fields" so + // production is simply 0, instead of "Trying to access array offset + // on null" (+ the resulting null used as an array key below, which + // additionally raised "Deprecated: Using null as an array offset"). if (!is_array($resArray)) $resArray = []; $prodBid = [1 => $bid1, 2 => $bid2, 3 => $bid3, 4 => $bid4][$resourceType]; diff --git a/GameEngine/Battle.php b/GameEngine/Battle.php index bc30161a..4ca6a9c6 100644 --- a/GameEngine/Battle.php +++ b/GameEngine/Battle.php @@ -557,7 +557,8 @@ class Battle { $defForces = $this->computeDefenderForces( $Defender, $def_ab, $type, $defender_artefact, $defenderhero, - $DefenderWref, $defReinforcements + $DefenderWref, $defReinforcements, + $DefenderID ); $dp = $defForces['dp']; @@ -623,6 +624,10 @@ class Battle { $result['Attack_points'] = $rap; $result['Defend_points'] = $rdp; + // BUG FIXED (defense points): per-owner dp+cdp breakdown, for splitting + // Defend_points proportionally in AutomationBattleResolution.php instead of + // crediting the whole battle's defense points to the village owner alone. + $result['DefensePointShares'] = $defForces['perOwnerDp']; $winner = ($rap > $rdp); @@ -888,7 +893,8 @@ class Battle { private function computeDefenderForces( $Defender, $def_ab, $type, $defender_artefact, $defenderhero, - $DefenderWref, $defReinforcements + $DefenderWref, $defReinforcements, + $DefenderID = 0 ) { global $database; @@ -898,6 +904,12 @@ class Battle { $involve = 0; $detected = false; $defHeroUnit = null; + // BUG FIXED (defense points): tracks how much of the total dp+cdp pool each + // owner contributed (village owner's own troops + each reinforcement sender's + // troops), keyed by owner id. Purely additive - only consumed by the new + // 'perOwnerDp' return key below, doesn't change $dp/$cdp or anything else. + $perOwnerDp = []; + $DefenderID = (int) $DefenderID; /****************************************************************** * DEFENDER BASE FORCES @@ -914,6 +926,10 @@ class Battle { $detected = $datadefScout['detect']; } + if ($DefenderID > 0) { + $perOwnerDp[$DefenderID] = ($perOwnerDp[$DefenderID] ?? 0) + $datadefScout['dp'] + $datadefScout['cdp']; + } + } else { $datadef = $this->getDataDef($Defender, $def_ab); @@ -943,6 +959,10 @@ class Battle { $dp += $own_dp; $cdp += $own_cdp; + + if ($DefenderID > 0) { + $perOwnerDp[$DefenderID] = ($perOwnerDp[$DefenderID] ?? 0) + $own_dp + $own_cdp; + } } /****************************************************************** @@ -1003,6 +1023,10 @@ class Battle { $detected = $datadefScout['detect']; } + if ($owner > 0) { + $perOwnerDp[$owner] = ($perOwnerDp[$owner] ?? 0) + $datadefScout['dp'] + $datadefScout['cdp']; + } + } else { $datadef = $this->getDataDef($defenders, $def_ab); @@ -1029,6 +1053,10 @@ class Battle { $dp += $reinf_dp; $cdp += $reinf_cdp; + + if ($owner > 0) { + $perOwnerDp[$owner] = ($perOwnerDp[$owner] ?? 0) + $reinf_dp + $reinf_cdp; + } } } } @@ -1039,7 +1067,11 @@ class Battle { 'involve' => $involve, 'detected' => $detected, 'def_hero_unit' => $defHeroUnit, - 'DefendersAll' => $DefendersAll + 'DefendersAll' => $DefendersAll, + // BUG FIXED (defense points): owner id -> dp+cdp contributed, so callers + // can split battle-outcome defense points proportionally per defender + // instead of awarding the whole battle's points to the village owner. + 'perOwnerDp' => $perOwnerDp ]; } diff --git a/GameEngine/Database.php b/GameEngine/Database.php index 5749030a..cfce26ef 100755 --- a/GameEngine/Database.php +++ b/GameEngine/Database.php @@ -211,6 +211,12 @@ class MYSQLi_DB implements IDbConnection { */ $usersCountCache = [], + /** + * @var array Cache of whether the server has been won (fdata f99/f99t + * check). Only ever holds a TRUE value - see isThereAWinner(). + */ + $serverFinishedCache = [], + /** * @var array Cache of alliances count. */ diff --git a/GameEngine/Database/DatabaseStatisticsQueries.php b/GameEngine/Database/DatabaseStatisticsQueries.php index e75ee3b6..bb087b32 100644 --- a/GameEngine/Database/DatabaseStatisticsQueries.php +++ b/GameEngine/Database/DatabaseStatisticsQueries.php @@ -77,10 +77,28 @@ trait DatabaseStatisticsQueries { return mysqli_query($this->dblink,$q); } - function isThereAWinner(){ + function isThereAWinner($use_cache = true){ + // BUG FIXED: this now runs on every logged-in page load (see isWinner() + // in Session.php), not just on winner.php - added the same request-scope + // cache pattern as countUser() above. Only a TRUE result is cached: once + // someone finishes the Wonder it can never flip back to false, so there + // is nothing to gain by re-querying again this request; a FALSE result + // is deliberately left uncached so every request keeps checking until + // the moment it does flip. Routed through $this->query() (deadlock / + // connection-loss retry) since it's now a much hotter path than before. + if ($use_cache && ($cachedValue = self::returnCachedContent(self::$serverFinishedCache, 0)) && !is_null($cachedValue)) { + return $cachedValue; + } + $q = "SELECT Count(*) as Total FROM ".TB_PREFIX."fdata WHERE f99 = 100 and f99t = 40"; - $result = mysqli_fetch_array(mysqli_query($this->dblink, $q), MYSQLI_ASSOC); - return $result['Total'] > 0; + $result = mysqli_fetch_array($this->query($q), MYSQLI_ASSOC); + $finished = $result['Total'] > 0; + + if ($finished) { + self::$serverFinishedCache[0] = $finished; + } + + return $finished; } // no need to cache this method diff --git a/GameEngine/Session.php b/GameEngine/Session.php index 954543fd..56df6d0d 100755 --- a/GameEngine/Session.php +++ b/GameEngine/Session.php @@ -271,6 +271,16 @@ function __construct() { $_SERVER['HTTP_USER_AGENT'] ?? '' ); + // BUG FIXED: land directly on winner.php after login once the server has + // been won, instead of dorf1.php (or nachrichten.php for uid 1) and + // relying on isWinner() to bounce the very next page load - same end + // result either way (isWinner() still covers every other entry point), + // just without the extra redirect hop for the single most common one. + if ($database->isThereAWinner()) { + header("Location: winner.php"); + exit; + } + if ($dbarray['id'] == 1) { header("Location: nachrichten.php"); exit; @@ -379,26 +389,44 @@ function __construct() { /** * FIXED: winner condition bug (safe parentheses + logic) + * + * BUG FIXED (game-lock after server win): this only protected 3 pages + * (build.php, plus1.php, plus.php?id>=7). Every other gameplay-changing + * endpoint - most importantly a2b.php (attacks/reinforcements/troop + * movements), but also dorf1.php/dorf2.php/dorf3.php, karte.php, resource + * transfers, troop training, etc. - stayed fully playable after the server + * was won, including actually executing on the backend (INSERT/UPDATE + * attack/reinforce) before any page ever redirected. Widened to a blanket + * lock: everything except winner.php itself and logout.php now redirects, + * for every logged-in request. This method is called from checkLogin(), + * which runs at the very start of Session::__construct() - itself the very + * first include in Village.php, which every gameplay page includes before + * any of its own logic - so the lock (and the exit; that comes with it) + * fires before a single gameplay DB write can happen, not just as a + * frontend redirect. checkLogin() only calls this while $user is set (see + * call site above), so anonymous visitors can still reach login.php to + * authenticate; $this->inAdmin mirrors the same admin exemption already + * used in SurfControl(), so the operator isn't locked out of Admin/. */ function isWinner() { global $database; + if ($this->inAdmin) { + return; + } + + if (!$database->isThereAWinner()) { + return; + } + $requiredPage = basename($_SERVER['PHP_SELF']); - $idParam = isset($_GET['id']) ? (int)$_GET['id'] : 0; - - if ( - $database->isThereAWinner() && - ( - in_array($requiredPage, ['build.php', 'plus1.php']) || - ( - $requiredPage === 'plus.php' && $idParam >= 7 - ) - ) - ) { - header('Location: winner.php'); - exit; + if (in_array($requiredPage, ['winner.php', 'logout.php'], true)) { + return; } + + header('Location: winner.php'); + exit; } /**