Some important fixes!

Some important fixes!
This commit is contained in:
novgorodschi catalin
2026-09-04 12:39:23 +03:00
parent a7bb354015
commit 7e3ffc4c2e
5 changed files with 330 additions and 37 deletions
@@ -70,7 +70,15 @@ trait AutomationBattleResolution {
// update all that needs updating // update all that needs updating
$database->setVillageLevel($data['to'], $fieldsToSet, $fieldValuesToSet); $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; $buildarray = $GLOBALS["bid".$tbgid] ?? null;
if ( is_array($buildarray) && isset( $buildarray[$newLevel] ) ) { if ( is_array($buildarray) && isset( $buildarray[$newLevel] ) ) {
@@ -317,6 +325,13 @@ trait AutomationBattleResolution {
if ($i == 41) $i = 99; if ($i == 41) $i = 99;
if ($bdo['f'.$i] > 0 && $i != 40) $list[] = $i; 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)) { if (!empty($list)) {
$catapultTarget2 = $list[ rand(0, count($list) - 1) ]; $catapultTarget2 = $list[ rand(0, count($list) - 1) ];
} else { } else {
@@ -623,6 +638,11 @@ trait AutomationBattleResolution {
$expArray = $database->getVillageFields($from['wref'], 'exp1, exp2, exp3'); $expArray = $database->getVillageFields($from['wref'], 'exp1, exp2, exp3');
$villexp = ($expArray['exp1'] == 0) ? 0 : (($expArray['exp2'] == 0) ? 1 : (($expArray['exp3'] == 0) ? 2 : 3)); $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; $mode = CP;
$cp_mode = $GLOBALS['cp' . $mode] ?? []; $cp_mode = $GLOBALS['cp' . $mode] ?? [];
$need_cps = $cp_mode[count($varray1) + 1] ?? PHP_INT_MAX; $need_cps = $cp_mode[count($varray1) + 1] ?? PHP_INT_MAX;
@@ -660,6 +680,11 @@ trait AutomationBattleResolution {
$reducedLoyalty /= $battlepart['moralBonus']; $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) { if ($owntribe == 2) {
$attackerCapital = $database->getVillage($from['owner'], 3); $attackerCapital = $database->getVillage($from['owner'], 3);
if ($attackerCapital && (int)$attackerCapital['festival'] > $time && $this->getTypeLevel(35, $attackerCapital['wref']) > 0) { 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')]); $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) { if (defined('NEW_FUNCTIONS_MILESTONES') && NEW_FUNCTIONS_MILESTONES) {
$newOwner = $database->getVillageField($data['from'], 'owner'); $newOwner = $database->getVillageField($data['from'], 'owner');
if ((int)($to['natar'] ?? 0) === 1) { if ((int)($to['natar'] ?? 0) === 1) {
@@ -1216,6 +1249,25 @@ trait AutomationBattleResolution {
if ($unitlist) { if ($unitlist) {
$owndead['hero'] = (isset($battlepart['deadherodef']) ? $battlepart['deadherodef'] : ''); $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 = <hero 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 // modify units in DB
@@ -1312,6 +1364,21 @@ trait AutomationBattleResolution {
} }
if ($enforce['hero'] > 0) { 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']]; $dead['hero'] = $battlepart['deadheroref'][$enforce['id']];
$alldead['hero'] += $dead['hero']; $alldead['hero'] += $dead['hero'];
$wrong = $dead['hero'] != $enforce['hero']; $wrong = $dead['hero'] != $enforce['hero'];
@@ -1773,6 +1840,13 @@ trait AutomationBattleResolution {
global $database, $units; global $database, $units;
$DefenderUserData = $this->getCachedUser($database->getVillageField($data['to'],"owner"),1); $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; $DefenderID = $DefenderUserData["id"] ?? null;
$targettribe = $DefenderUserData["tribe"] ?? null; $targettribe = $DefenderUserData["tribe"] ?? null;
$targetally = $DefenderUserData["alliance"] ?? null; $targetally = $DefenderUserData["alliance"] ?? null;
@@ -1939,7 +2013,8 @@ trait AutomationBattleResolution {
$targettribe, $att_tribe, $targettribe, $att_tribe,
$Attacker, $AttackerHeroID, $Defender, $DefendersHeroID, $Attacker, $AttackerHeroID, $Defender, $DefendersHeroID,
$toF, $from, $targetally, $ownally, $toF, $from, $targetally, $ownally,
&$heroxp, &$defheroxp &$heroxp, &$defheroxp,
array $defensePointShares = []
) { ) {
global $database; global $database;
@@ -1988,11 +2063,101 @@ trait AutomationBattleResolution {
// we don't need these two variables anymore // we don't need these two variables anymore
unset($AttackerHeroID, $DefendersHeroID); unset($AttackerHeroID, $DefendersHeroID);
$database->modifyPoints( // BUG FIXED (defense points awarded to the wrong player(s)): $totalpoint_def
$toF['owner'], // is the total defense-point value for the WHOLE battle (every defender's
['dpall', 'dp'], // troops combined) - it was previously credited entirely to $toF['owner']
[$totalpoint_def, $totalpoint_def] // (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( $database->modifyPoints(
$from['owner'], $from['owner'],
@@ -2000,12 +2165,6 @@ trait AutomationBattleResolution {
[$totalpoint_att, $totalpoint_att] [$totalpoint_att, $totalpoint_att]
); );
$database->modifyPointsAlly(
$targetally,
['Adp', 'dp'],
[$totalpoint_def, $totalpoint_def]
);
$database->modifyPointsAlly( $database->modifyPointsAlly(
$ownally, $ownally,
['Aap', 'ap'], ['Aap', 'ap'],
@@ -2067,6 +2226,11 @@ trait AutomationBattleResolution {
$this->pruneResource(); $this->pruneResource();
$villageData = $database->getVillageFields($conqureby, 'clay, iron, wood, crop', false); $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); $totclay = intval(($villageData['clay'] ?? 0) / 10);
$totiron = intval(($villageData['iron'] ?? 0) / 10); $totiron = intval(($villageData['iron'] ?? 0) / 10);
$totwood = intval(($villageData['wood'] ?? 0) / 10); $totwood = intval(($villageData['wood'] ?? 0) / 10);
@@ -2146,6 +2310,8 @@ trait AutomationBattleResolution {
if ($canqured == 3 && $troopcount == 0) { if ($canqured == 3 && $troopcount == 0) {
if ($type == 3) { if ($type == 3) {
$Oloyaltybefore = intval($to['loyalty']); $Oloyaltybefore = intval($to['loyalty']);
//$database->modifyOasisLoyalty($data['to']);
//$OasisInfo = $database->getOasisInfo($data['to']);
$Oloyaltynow = intval($database->modifyOasisLoyalty($data['to']));//intval($OasisInfo['loyalty']); $Oloyaltynow = intval($database->modifyOasisLoyalty($data['to']));//intval($OasisInfo['loyalty']);
$info_hero = $hero_pic.",".rc_tok('RC_HERO_REDUCED_OASIS_LOYALTY', $Oloyaltynow, $Oloyaltybefore).$xp; $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($targetally, 'RR', $totalstolentaken);
$database->modifyPointsAlly($ownally, 'RR', $totalstolengain); $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){ 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]); $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']; $tblevel = $vt['tblevel'];
$stonemason = $vt['stonemason']; $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'])) { 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'])) { if ($database->setMovementProc($data['moveid'])) {
$bounceTime = $units->getWalkingTroopsTime($from['wref'], $data['to'], $from['owner'], $owntribe, $data, 1, 't'); $bounceTime = $units->getWalkingTroopsTime($from['wref'], $data['to'], $from['owner'], $owntribe, $data, 1, 't');
$bounceEnd = $database->getArtifactsValueInfluence($from['owner'], $from['wref'], 2, $bounceTime) + $AttackArrivalTime; $bounceEnd = $database->getArtifactsValueInfluence($from['owner'], $from['wref'], 2, $bounceTime) + $AttackArrivalTime;
@@ -2492,8 +2677,7 @@ trait AutomationBattleResolution {
$spy_pic = $atkUnits['spy_pic']; $spy_pic = $atkUnits['spy_pic'];
$hero_pic = $atkUnits['hero_pic']; $hero_pic = $atkUnits['hero_pic'];
}else{ }else{ //It's an oasis
//It's an oasis
// target + battle environment — extracted to resolveOasisTarget() [#155] // target + battle environment — extracted to resolveOasisTarget() [#155]
$ot = $this->resolveOasisTarget($data, $dataarray, $data_num, $owntribe); $ot = $this->resolveOasisTarget($data, $dataarray, $data_num, $owntribe);
$DefenderID = $ot['DefenderID']; $DefenderID = $ot['DefenderID'];
@@ -2681,6 +2865,12 @@ trait AutomationBattleResolution {
$alldead = []; $alldead = [];
for($i = 1; $i <= 90; $i++) $alldead[$i] = 0; 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; $alldead['hero'] = 0;
//kill own defence — extracted to applyOwnDefenceCasualties() [#155] //kill own defence — extracted to applyOwnDefenceCasualties() [#155]
@@ -2704,6 +2894,8 @@ trait AutomationBattleResolution {
if (empty($alldead['hero'])) $alldead['hero'] = 0; if (empty($alldead['hero'])) $alldead['hero'] = 0;
if (empty($owndead['hero'])) $owndead['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 $deadhero = (int)(isset($battlepart['deadherodef']) && $battlepart['deadherodef'] > 0
? $battlepart['deadherodef'] ? $battlepart['deadherodef']
: $owndead['hero']); : $owndead['hero']);
@@ -2758,13 +2950,18 @@ trait AutomationBattleResolution {
$troopsdead11 = $dead11; $troopsdead11 = $dead11;
// hero XP, player points and alliance points — extracted to calculateHeroXpAndPoints() [#155] // 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( $totaldead_def = $this->calculateHeroXpAndPoints(
$alldead, $owndead, $alldead, $owndead,
[$dead1, $dead2, $dead3, $dead4, $dead5, $dead6, $dead7, $dead8, $dead9, $dead10, $dead11], [$dead1, $dead2, $dead3, $dead4, $dead5, $dead6, $dead7, $dead8, $dead9, $dead10, $dead11],
$targettribe, $att_tribe, $targettribe, $att_tribe,
$Attacker, $AttackerHeroID, $Defender, $DefendersHeroID, $Attacker, $AttackerHeroID, $Defender, $DefendersHeroID,
$toF, $from, $targetally, $ownally, $toF, $from, $targetally, $ownally,
$heroxp, $defheroxp $heroxp, $defheroxp,
$battlepart['DefensePointShares'] ?? []
); );
// resources lootable after cranny protection — extracted to resolveResourcesAfterBattle() [#155] // 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] // delete the target village if it was destroyed — extracted to handleVillageDestruction() [#155]
$this->handleVillageDestruction($village_destroyed, $can_destroy, $data, $to, $varray); $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) { if ($village_destroyed == 1 && $can_destroy == 1) {
$razedTargets[$data['to']] = true; $razedTargets[$data['to']] = true;
} }
@@ -3024,7 +3227,13 @@ trait AutomationBattleResolution {
// the gold-club +25% crop bonus (b4) keyed on the village owner. // the gold-club +25% crop bonus (b4) keyed on the village owner.
private function bountyGetResourceProd($resArray, $oasisNumber, $resourceType) { private function bountyGetResourceProd($resArray, $oasisNumber, $resourceType) {
global $bid1, $bid2, $bid3, $bid4, $bid5, $bid6, $bid7, $bid8, $bid9, $database; 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 = []; if (!is_array($resArray)) $resArray = [];
$prodBid = [1 => $bid1, 2 => $bid2, 3 => $bid3, 4 => $bid4][$resourceType]; $prodBid = [1 => $bid1, 2 => $bid2, 3 => $bid3, 4 => $bid4][$resourceType];
+35 -3
View File
@@ -557,7 +557,8 @@ class Battle {
$defForces = $this->computeDefenderForces( $defForces = $this->computeDefenderForces(
$Defender, $def_ab, $type, $Defender, $def_ab, $type,
$defender_artefact, $defenderhero, $defender_artefact, $defenderhero,
$DefenderWref, $defReinforcements $DefenderWref, $defReinforcements,
$DefenderID
); );
$dp = $defForces['dp']; $dp = $defForces['dp'];
@@ -623,6 +624,10 @@ class Battle {
$result['Attack_points'] = $rap; $result['Attack_points'] = $rap;
$result['Defend_points'] = $rdp; $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); $winner = ($rap > $rdp);
@@ -888,7 +893,8 @@ class Battle {
private function computeDefenderForces( private function computeDefenderForces(
$Defender, $def_ab, $type, $Defender, $def_ab, $type,
$defender_artefact, $defenderhero, $defender_artefact, $defenderhero,
$DefenderWref, $defReinforcements $DefenderWref, $defReinforcements,
$DefenderID = 0
) { ) {
global $database; global $database;
@@ -898,6 +904,12 @@ class Battle {
$involve = 0; $involve = 0;
$detected = false; $detected = false;
$defHeroUnit = null; $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 * DEFENDER BASE FORCES
@@ -914,6 +926,10 @@ class Battle {
$detected = $datadefScout['detect']; $detected = $datadefScout['detect'];
} }
if ($DefenderID > 0) {
$perOwnerDp[$DefenderID] = ($perOwnerDp[$DefenderID] ?? 0) + $datadefScout['dp'] + $datadefScout['cdp'];
}
} else { } else {
$datadef = $this->getDataDef($Defender, $def_ab); $datadef = $this->getDataDef($Defender, $def_ab);
@@ -943,6 +959,10 @@ class Battle {
$dp += $own_dp; $dp += $own_dp;
$cdp += $own_cdp; $cdp += $own_cdp;
if ($DefenderID > 0) {
$perOwnerDp[$DefenderID] = ($perOwnerDp[$DefenderID] ?? 0) + $own_dp + $own_cdp;
}
} }
/****************************************************************** /******************************************************************
@@ -1003,6 +1023,10 @@ class Battle {
$detected = $datadefScout['detect']; $detected = $datadefScout['detect'];
} }
if ($owner > 0) {
$perOwnerDp[$owner] = ($perOwnerDp[$owner] ?? 0) + $datadefScout['dp'] + $datadefScout['cdp'];
}
} else { } else {
$datadef = $this->getDataDef($defenders, $def_ab); $datadef = $this->getDataDef($defenders, $def_ab);
@@ -1029,6 +1053,10 @@ class Battle {
$dp += $reinf_dp; $dp += $reinf_dp;
$cdp += $reinf_cdp; $cdp += $reinf_cdp;
if ($owner > 0) {
$perOwnerDp[$owner] = ($perOwnerDp[$owner] ?? 0) + $reinf_dp + $reinf_cdp;
}
} }
} }
} }
@@ -1039,7 +1067,11 @@ class Battle {
'involve' => $involve, 'involve' => $involve,
'detected' => $detected, 'detected' => $detected,
'def_hero_unit' => $defHeroUnit, '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
]; ];
} }
+6
View File
@@ -211,6 +211,12 @@ class MYSQLi_DB implements IDbConnection {
*/ */
$usersCountCache = [], $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. * @var array Cache of alliances count.
*/ */
@@ -77,10 +77,28 @@ trait DatabaseStatisticsQueries {
return mysqli_query($this->dblink,$q); 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"; $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); $result = mysqli_fetch_array($this->query($q), MYSQLI_ASSOC);
return $result['Total'] > 0; $finished = $result['Total'] > 0;
if ($finished) {
self::$serverFinishedCache[0] = $finished;
}
return $finished;
} }
// no need to cache this method // no need to cache this method
+41 -13
View File
@@ -271,6 +271,16 @@ function __construct() {
$_SERVER['HTTP_USER_AGENT'] ?? '' $_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) { if ($dbarray['id'] == 1) {
header("Location: nachrichten.php"); header("Location: nachrichten.php");
exit; exit;
@@ -379,26 +389,44 @@ function __construct() {
/** /**
* FIXED: winner condition bug (safe parentheses + logic) * 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() { function isWinner() {
global $database; global $database;
if ($this->inAdmin) {
return;
}
if (!$database->isThereAWinner()) {
return;
}
$requiredPage = basename($_SERVER['PHP_SELF']); $requiredPage = basename($_SERVER['PHP_SELF']);
$idParam = isset($_GET['id']) ? (int)$_GET['id'] : 0; if (in_array($requiredPage, ['winner.php', 'logout.php'], true)) {
return;
if (
$database->isThereAWinner() &&
(
in_array($requiredPage, ['build.php', 'plus1.php']) ||
(
$requiredPage === 'plus.php' && $idParam >= 7
)
)
) {
header('Location: winner.php');
exit;
} }
header('Location: winner.php');
exit;
} }
/** /**