mirror of
https://github.com/Shadowss/TravianZ.git
synced 2026-06-28 00:24:23 +00:00
fix(market): harden NPC resource distribution against NaN [#211]
On some servers the NPC ("merchant") distribution screen could lock on
"Rest: NaN", hiding the trade button and making the NPC unusable. The "Rest"
value never returned to 0, so the player could not complete the trade.
Two layers of hardening:
* Client (Templates/Build/17_3.tpl):
- add a safe npcDiv() helper (returns 0 on a 0 divisor or non-finite value)
and route every division in portionOut() through it, so Infinity/NaN can no
longer leak into the inputs;
- in calculateRest(), coerce "Rest"/overall to a finite value (fall back to
the total when org4 cannot be parsed) so the submit button can always
reappear once the distribution is balanced;
- coerce each resource, $totalRes, $maxstore and $maxcrop to a valid,
non-negative integer before they reach the page/JS, so a resource computed
as NaN/INF cannot poison org4/summe in the first place.
* Server (Market::tradeResource): clamp each requested amount to [0, maxstore]
(crop to maxcrop) before persisting, so a forged or corrupted POST can no
longer write out-of-range resources.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+16
-31
@@ -806,35 +806,20 @@ class Market
|
||||
exit;
|
||||
}
|
||||
|
||||
// Prevent selling more resources than available
|
||||
if (
|
||||
(
|
||||
(int)$post['m2'][0] < 0 &&
|
||||
round($village->awood) + (int)$post['m2'][0] < 0
|
||||
) ||
|
||||
(
|
||||
(int)$post['m2'][1] < 0 &&
|
||||
round($village->aclay) + (int)$post['m2'][1] < 0
|
||||
) ||
|
||||
(
|
||||
(int)$post['m2'][2] < 0 &&
|
||||
round($village->airon) + (int)$post['m2'][2] < 0
|
||||
) ||
|
||||
(
|
||||
(int)$post['m2'][3] < 0 &&
|
||||
round($village->acrop) + (int)$post['m2'][3] < 0
|
||||
)
|
||||
) {
|
||||
// Sanitize the requested distribution: never negative, never above the
|
||||
// warehouse / granary capacity. Guards against a forged or NaN-corrupted
|
||||
// POST (issue #211: NPC distribution).
|
||||
$maxstore = (int) $village->maxstore;
|
||||
$maxcrop = (int) $village->maxcrop;
|
||||
|
||||
header('Location: build.php?id=' . $post['id'] . '&t=3');
|
||||
exit;
|
||||
}
|
||||
$m2 = [
|
||||
max(0, min($maxstore, (int)($post['m2'][0] ?? 0))),
|
||||
max(0, min($maxstore, (int)($post['m2'][1] ?? 0))),
|
||||
max(0, min($maxstore, (int)($post['m2'][2] ?? 0))),
|
||||
max(0, min($maxcrop, (int)($post['m2'][3] ?? 0))),
|
||||
];
|
||||
|
||||
$newTotal =
|
||||
(int)$post['m2'][0] +
|
||||
(int)$post['m2'][1] +
|
||||
(int)$post['m2'][2] +
|
||||
(int)$post['m2'][3];
|
||||
$newTotal = $m2[0] + $m2[1] + $m2[2] + $m2[3];
|
||||
|
||||
$currentTotal =
|
||||
round($village->awood) +
|
||||
@@ -853,10 +838,10 @@ class Market
|
||||
$village->wid,
|
||||
['wood', 'clay', 'iron', 'crop'],
|
||||
[
|
||||
$post['m2'][0],
|
||||
$post['m2'][1],
|
||||
$post['m2'][2],
|
||||
$post['m2'][3]
|
||||
$m2[0],
|
||||
$m2[1],
|
||||
$m2[2],
|
||||
$m2[3]
|
||||
]
|
||||
);
|
||||
$this->forget();
|
||||
|
||||
@@ -8,9 +8,23 @@ if ($session->gold <= 2) {
|
||||
}
|
||||
|
||||
$level = (int)$village->resarray['f'.$id];
|
||||
$totalRes = floor($village->awood + $village->aclay + $village->airon + $village->acrop);
|
||||
$maxstore = (int)$village->maxstore;
|
||||
$maxcrop = (int)$village->maxcrop;
|
||||
|
||||
// Defensive: a resource computed as NaN/INF (PHP float) would be echoed as
|
||||
// "NAN"/"INF" into org4/summe and lock the NPC distribution on "Rest: NaN"
|
||||
// (issue #211). Coerce every value the template/JS relies on to a valid,
|
||||
// non-negative integer before it reaches the page.
|
||||
$resSafe = static function ($v) {
|
||||
$v = (float) $v;
|
||||
return (is_finite($v) && $v > 0) ? (int) floor($v) : 0;
|
||||
};
|
||||
$awood = $resSafe($village->awood);
|
||||
$aclay = $resSafe($village->aclay);
|
||||
$airon = $resSafe($village->airon);
|
||||
$acrop = $resSafe($village->acrop);
|
||||
|
||||
$totalRes = $awood + $aclay + $airon + $acrop;
|
||||
$maxstore = max(0, (int)$village->maxstore);
|
||||
$maxcrop = max(0, (int)$village->maxcrop);
|
||||
|
||||
// valori prefill din GET
|
||||
$r = [];
|
||||
@@ -62,6 +76,9 @@ $completed = isset($_GET['c']);
|
||||
document.getElementById("overall").innerHTML=overall+"%";
|
||||
}
|
||||
function normalize() { calculateRes(); resObj=document.getElementsByName("m2"); for (i=0; i<resObj.length; i++) { tmp=parseInt(resObj[i].value); tmp=tmp*(100/overall); resObj[i].value=Math.round(tmp); } calculateRes(); }
|
||||
// Safe integer division: 0 when the divisor is 0 or the value is not finite
|
||||
// (issue #211: avoids Infinity/NaN leaking into the inputs and the "Rest").
|
||||
function npcDiv(a, b) { return (b > 0 && isFinite(a)) ? Math.round(a / b) : 0; }
|
||||
function calculateRest() {
|
||||
resObj=document.getElementsByName("m2[]"); overall=0;
|
||||
for (i=0; i<resObj.length; i++) {
|
||||
@@ -69,10 +86,13 @@ $completed = isset($_GET['c']);
|
||||
if (tmp=="") { tmp="0"; newRes=0; resObj[i].value=""; } else { newRes=parseInt(tmp); if ((i<3) && (newRes>max123)) newRes=max123; if ((i==3) && (newRes>max4)) newRes=max4; resObj[i].value=newRes; }
|
||||
dif=newRes-parseInt(document.getElementById("org"+i).innerHTML); newHTML=dif; if (dif>0) newHTML="+"+dif; document.getElementById("diff"+i).innerHTML=newHTML; overall+=newRes;
|
||||
}
|
||||
document.getElementById("newsum").innerHTML=overall; rest=parseInt(document.getElementById("org4").innerHTML)-overall; document.getElementById("remain").innerHTML=rest; testSum();
|
||||
if (!isFinite(overall)) overall=0;
|
||||
var total=parseInt(document.getElementById("org4").innerHTML); if (!isFinite(total)) total=summe;
|
||||
rest=total-overall; if (!isFinite(rest)) rest=0;
|
||||
document.getElementById("newsum").innerHTML=overall; document.getElementById("remain").innerHTML=rest; testSum();
|
||||
}
|
||||
function fillup(nr) { resObj=document.getElementsByName("m2[]"); if (nr<3) { resObj[nr].value=max123; } else { resObj[nr].value=max4; } calculateRest(); }
|
||||
function portionOut() { /*... cod original neschimbat... */ restRes=parseInt(document.getElementById("remain").innerHTML); rest=restRes; resObj=document.getElementsByName("m2[]"); nullCount=0; notNullCount=0; for (j=0; j<resObj.length; j++) { if ((restRes>0) && (resObj[j].value=="")) nullCount++; if ((restRes<0) && (resObj[j].value!="")) notNullCount++; } nullCount2=0; if (restRes>0) { if (nullCount==0) { for (i=0; i<resObj.length; i++) { free=max123-parseInt(resObj[i].value); resObj[i].value=(parseInt(resObj[i].value)+Math.round(rest/(4-i))); rest=rest-Math.min(free,Math.round(rest/(4-i))); if ((i<3) && (parseInt(resObj[i].value)<max123)) nullCount2++; } } else { for (i=0; i<resObj.length; i++) { if (resObj[i].value=="") { resObj[i].value=Math.round(rest/nullCount); rest=rest-Math.round(rest/nullCount); nullCount--; } if ((i<3) && (parseInt(resObj[i].value)<max123)) nullCount2++; } } } else { for (j=0; j<resObj.length; j++) { if (parseInt(resObj[j].value)>0) { resObj[j].value=(parseInt(resObj[j].value)+Math.round(rest/notNullCount)); rest=rest-Math.round(rest/notNullCount); notNullCount--; } } } calculateRest(); if (rest>0) { if (max123>max4) { for (j=0; j<3; j++) { if (parseInt(resObj[j].value)<max123) { resObj[j].value=(parseInt(resObj[j].value)+Math.round(rest/nullCount2)); rest=rest-Math.round(rest/nullCount2); nullCount2--; } } } else { resObj[3].value=(parseInt(resObj[3].value)+rest); } } calculateRest(); }
|
||||
function portionOut() { restRes=parseInt(document.getElementById("remain").innerHTML); if (!isFinite(restRes)) restRes=0; rest=restRes; resObj=document.getElementsByName("m2[]"); nullCount=0; notNullCount=0; for (j=0; j<resObj.length; j++) { if ((restRes>0) && (resObj[j].value=="")) nullCount++; if ((restRes<0) && (resObj[j].value!="")) notNullCount++; } nullCount2=0; if (restRes>0) { if (nullCount==0) { for (i=0; i<resObj.length; i++) { free=max123-parseInt(resObj[i].value); resObj[i].value=(parseInt(resObj[i].value)+npcDiv(rest,(4-i))); rest=rest-Math.min(free,npcDiv(rest,(4-i))); if ((i<3) && (parseInt(resObj[i].value)<max123)) nullCount2++; } } else { for (i=0; i<resObj.length; i++) { if (resObj[i].value=="") { resObj[i].value=npcDiv(rest,nullCount); rest=rest-npcDiv(rest,nullCount); nullCount--; } if ((i<3) && (parseInt(resObj[i].value)<max123)) nullCount2++; } } } else { for (j=0; j<resObj.length; j++) { if (parseInt(resObj[j].value)>0) { resObj[j].value=(parseInt(resObj[j].value)+npcDiv(rest,notNullCount)); rest=rest-npcDiv(rest,notNullCount); notNullCount--; } } } calculateRest(); if (rest>0) { if (max123>max4) { for (j=0; j<3; j++) { if (parseInt(resObj[j].value)<max123) { resObj[j].value=(parseInt(resObj[j].value)+npcDiv(rest,nullCount2)); rest=rest-npcDiv(rest,nullCount2); nullCount2--; } } } else { resObj[3].value=((parseInt(resObj[3].value)||0)+(isFinite(rest)?rest:0)); } } calculateRest(); }
|
||||
function testSum() { if (document.getElementById("remain").innerHTML!=0) { document.getElementById("submitText").innerHTML="<a href='javascript:portionOut();'><?php echo DISTRIBUTE_RESOURCES;?></a>"; document.getElementById("submitText").style.display="block"; document.getElementById("submitButton").style.display="none"; } else { document.getElementById("submitText").innerHTML=""; document.getElementById("submitText").style.display="none"; document.getElementById("submitButton").style.display="block"; } }
|
||||
</script>
|
||||
<script language="JavaScript">var summe=<?php echo $totalRes;?>;var max123=<?php echo $maxstore;?>;var max4=<?php echo $maxcrop;?>;</script>
|
||||
@@ -88,10 +108,10 @@ $completed = isset($_GET['c']);
|
||||
<tr><th colspan="5"><?php echo NPC_TRADE;?></th></tr>
|
||||
<tr>
|
||||
<?php $resData = [
|
||||
['wood', $village->awood, 'r1', LUMBER],
|
||||
['clay', $village->aclay, 'r2', CLAY],
|
||||
['iron', $village->airon, 'r3', IRON],
|
||||
['crop', $village->acrop, 'r4', CROP],
|
||||
['wood', $awood, 'r1', LUMBER],
|
||||
['clay', $aclay, 'r2', CLAY],
|
||||
['iron', $airon, 'r3', IRON],
|
||||
['crop', $acrop, 'r4', CROP],
|
||||
];
|
||||
foreach ($resData as $idx => $rd):?>
|
||||
<td class="all">
|
||||
|
||||
Reference in New Issue
Block a user