From eb64bba1e7db0cd6d314294604ef1165f92d8134 Mon Sep 17 00:00:00 2001 From: TravianZ Patcher Date: Wed, 10 Jun 2026 15:05:32 +0200 Subject: [PATCH] fix(quest): grant no-tasks shipment reward atomically [#129] The "no tasks" quest mode delivers a Plus + gold shipment on claim (case '91', and the final case '97'). The reward was granted with a non-atomic read-modify-write: $gold = getUserField('gold'); $gold += 15; updateUserField('gold', $gold); and the quest pointer / timer were advanced unconditionally. Under the many concurrent ajax requests the game fires, a request that read gold/ plus before the claim and wrote the user row back after it would clobber the freshly granted reward. The quest_time write (a literal value) still survived, so the UI advanced to the next shipment countdown while the player received neither Plus nor gold -- exactly the sporadic symptom in the report. Fix: gate the grant on an atomic conditional advance (UPDATE ... SET quest = 91 WHERE quest = 90) and apply the reward with in-place SQL increments (gold = gold + 15, plus = IF(plus > now, ...)), matching the idiom already used in Templates/Plus/*.tpl. This makes the claim idempotent (duplicate/concurrent requests grant nothing extra) and immune to the lost-update race. Also fixes a latent case where an expired Plus timestamp was extended in the past instead of reset to now. Applied to both quest_core.tpl and quest_core25.tpl. Co-Authored-By: Claude Opus 4.8 --- Templates/Ajax/quest_core.tpl | 44 ++++++++++++++------------------- Templates/Ajax/quest_core25.tpl | 44 ++++++++++++++------------------- 2 files changed, 36 insertions(+), 52 deletions(-) diff --git a/Templates/Ajax/quest_core.tpl b/Templates/Ajax/quest_core.tpl index 9d5b34e3..90026843 100644 --- a/Templates/Ajax/quest_core.tpl +++ b/Templates/Ajax/quest_core.tpl @@ -344,21 +344,18 @@ if (isset($qact)){ break; case '91': - $database->updateUserField($_SESSION['username'],'quest','91',0); - $database->updateUserField($_SESSION['username'],'quest_time',''.(time()+$skipp_time).'',0); + // Atomic, idempotent claim (issue #129): grant the reward only when the quest + // pointer actually advances 90 -> 91, so a concurrent or duplicated request + // cannot wipe the gold/Plus reward through a stale read-modify-write. + $now = time(); + $claimed = mysqli_query($database->dblink,"UPDATE ".TB_PREFIX."users SET quest = 91 WHERE `username` = '".$user_sanitized."' AND quest = 90") && mysqli_affected_rows($database->dblink) === 1; + if ($claimed) { + $database->updateUserField($_SESSION['username'],'quest_time',''.($now+$skipp_time).'',0); + //Give Reward: 1 day of Plus + 15 gold (atomic increments) + mysqli_query($database->dblink,"UPDATE ".TB_PREFIX."users SET gold = gold + 15, plus = IF(plus > $now, plus + 86400, $now + 86400) WHERE `username` = '".$user_sanitized."'"); + } $_SESSION['qst']= 91; $_SESSION['qst_time'] = time()+$skipp_time; - //Give Reward - if(!$session->plus){ - mysqli_query($database->dblink,"UPDATE ".TB_PREFIX."users set plus = ('".mktime(date("H"),date("i"), date("s"),date("m") , date("d"), date("Y"))."')+86400 where `username`='".$user_sanitized."'") or die(mysqli_error()); - } else { - $plus=$database->getUserField($_SESSION['username'],'plus','username'); - $plus+=86400; - $database->updateUserField($_SESSION['username'],'plus',$plus,0); - } - $gold=$database->getUserField($_SESSION['username'],'gold','username'); - $gold+=15; - $database->updateUserField($_SESSION['username'],'gold',$gold,0); break; case '92': @@ -407,21 +404,16 @@ if (isset($qact)){ break; case '97': - $database->updateUserField($_SESSION['username'],'quest','97',0); - $database->updateUserField($_SESSION['username'],'quest_time',''.(time()).'',0); + // Atomic, idempotent claim (issue #129): advance 96 -> 97 exactly once. + $now = time(); + $claimed = mysqli_query($database->dblink,"UPDATE ".TB_PREFIX."users SET quest = 97 WHERE `username` = '".$user_sanitized."' AND quest = 96") && mysqli_affected_rows($database->dblink) === 1; + if ($claimed) { + $database->updateUserField($_SESSION['username'],'quest_time',''.$now.'',0); + //Give Reward: 2 days of Plus + 20 gold (atomic increments) + mysqli_query($database->dblink,"UPDATE ".TB_PREFIX."users SET gold = gold + 20, plus = IF(plus > $now, plus + 172800, $now + 172800) WHERE `username` = '".$user_sanitized."'"); + } $_SESSION['qst_time'] = time(); $_SESSION['qst']= 97; - //Give Reward 20 gold + 2 days plus - if(!$session->plus){ - mysqli_query($database->dblink,"UPDATE ".TB_PREFIX."users set plus = ('".mktime(date("H"),date("i"), date("s"),date("m") , date("d"), date("Y"))."')+172800 where `username`='".$user_sanitized."'") or die(mysqli_error()); - } else { - $plus=$database->getUserField($_SESSION['username'],'plus','username'); - $plus+=172800; - $database->updateUserField($_SESSION['username'],'plus',$plus,0); - } - $gold=$database->getUserField($_SESSION['username'],'gold','username'); - $gold+=20; - $database->updateUserField($_SESSION['username'],'gold',$gold,0); break; } } diff --git a/Templates/Ajax/quest_core25.tpl b/Templates/Ajax/quest_core25.tpl index 6f11d6d8..5cdfa968 100644 --- a/Templates/Ajax/quest_core25.tpl +++ b/Templates/Ajax/quest_core25.tpl @@ -324,21 +324,18 @@ if (isset($qact)){ break; case '91': - $database->updateUserField($_SESSION['username'],'quest','91',0); - $database->updateUserField($_SESSION['username'],'quest_time',''.(time()+$skipp_time).'',0); + // Atomic, idempotent claim (issue #129): grant the reward only when the quest + // pointer actually advances 90 -> 91, so a concurrent or duplicated request + // cannot wipe the gold/Plus reward through a stale read-modify-write. + $now = time(); + $claimed = mysqli_query($database->dblink,"UPDATE ".TB_PREFIX."users SET quest = 91 WHERE `username` = '".$user_sanitized."' AND quest = 90") && mysqli_affected_rows($database->dblink) === 1; + if ($claimed) { + $database->updateUserField($_SESSION['username'],'quest_time',''.($now+$skipp_time).'',0); + //Give Reward: 1 day of Plus + 15 gold (atomic increments) + mysqli_query($database->dblink,"UPDATE ".TB_PREFIX."users SET gold = gold + 15, plus = IF(plus > $now, plus + 86400, $now + 86400) WHERE `username` = '".$user_sanitized."'"); + } $_SESSION['qst']= 91; $_SESSION['qst_time'] = time()+$skipp_time; - //Give Reward - if(!$session->plus){ - mysqli_query($database->dblink,"UPDATE ".TB_PREFIX."users set plus = ('".mktime(date("H"),date("i"), date("s"),date("m") , date("d"), date("Y"))."')+86400 where `username`='".$user_sanitized."'") or die(mysqli_error($database->dblink)); - } else { - $plus=$database->getUserField($_SESSION['username'],'plus',1); - $plus+=86400; - $database->updateUserField($_SESSION['username'],'plus',$plus,0); - } - $gold=$database->getUserField($_SESSION['username'],'gold',1); - $gold+=15; - $database->updateUserField($_SESSION['username'],'gold',$gold,0); break; case '92': @@ -387,21 +384,16 @@ if (isset($qact)){ break; case '97': - $database->updateUserField($_SESSION['username'],'quest','97',0); - $database->updateUserField($_SESSION['username'],'quest_time',''.(time()).'',0); + // Atomic, idempotent claim (issue #129): advance 96 -> 97 exactly once. + $now = time(); + $claimed = mysqli_query($database->dblink,"UPDATE ".TB_PREFIX."users SET quest = 97 WHERE `username` = '".$user_sanitized."' AND quest = 96") && mysqli_affected_rows($database->dblink) === 1; + if ($claimed) { + $database->updateUserField($_SESSION['username'],'quest_time',''.$now.'',0); + //Give Reward: 2 days of Plus + 20 gold (atomic increments) + mysqli_query($database->dblink,"UPDATE ".TB_PREFIX."users SET gold = gold + 20, plus = IF(plus > $now, plus + 172800, $now + 172800) WHERE `username` = '".$user_sanitized."'"); + } $_SESSION['qst_time'] = time(); $_SESSION['qst']= 97; - //Give Reward 20 gold + 2 days plus - if(!$session->plus){ - mysqli_query($database->dblink,"UPDATE ".TB_PREFIX."users set plus = ('".mktime(date("H"),date("i"), date("s"),date("m") , date("d"), date("Y"))."')+172800 where `username`='".$user_sanitized."'") or die(mysqli_error($database->dblink)); - } else { - $plus=$database->getUserField($_SESSION['username'],'plus',1); - $plus+=172800; - $database->updateUserField($_SESSION['username'],'plus',$plus,0); - } - $gold=$database->getUserField($_SESSION['username'],'gold',1); - $gold+=20; - $database->updateUserField($_SESSION['username'],'gold',$gold,0); break; } }