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:
Ferywir
2026-06-13 21:38:46 +02:00
parent dc8641c13f
commit dc971770e1
2 changed files with 45 additions and 40 deletions
+16 -31
View File
@@ -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();