Skip to content

fix(coop): host re-validate craft capacity in SHARED (#121) - #123

Merged
NonPolynomialTim merged 1 commit into
mainfrom
claude/openxcom-coop-issue-121-623851
Aug 1, 2026
Merged

fix(coop): host re-validate craft capacity in SHARED (#121)#123
NonPolynomialTim merged 1 commit into
mainfrom
claude/openxcom-coop-issue-121-623851

Conversation

@NonPolynomialTim

Copy link
Copy Markdown
Collaborator

Closes #121.

Problem

In SHARED co-op either player can command/edit any craft, so the client-side capacity gate is not enough. A stale-view concurrent edit passes the initiator's gate, reaches the host, and pre-fix the host applied it without re-checking craft capacity — producing an over-capacity craft that can cause a coop desync or a battlescape/deployment fault.

Two host validators were missing the re-check:

  • craftRearmValidate validated base/craft/slot/weapon-slot only — none of the four CraftWeaponsState capacity gates (cargo / HWP / storage-items / storage-space).
  • soldierArmorValidate validated base/soldier/armor existence only — not Craft::validateArmorChange (STR_NOT_ENOUGH_CRAFT_SPACE).

Fix

  • Factored the four inline capacity checks in CraftWeaponsState::lstWeaponsClick into a pure static CraftWeaponsState::equipCapacityError(...) (math is byte-identical to the old block, so host and client can never disagree), and call it from both the UI and craftRearmValidate.
  • soldierArmorValidate now re-runs Craft::validateArmorChange host-side, mirroring SoldierArmorState::lstArmorClick.

Both validators funnel host-origin and replica-origin commands through processHostCmd, so the gate now protects every path; a rejected request emits shared_fail and mutates nothing.

Harness fidelity + regression test

  • harnessEquip / harnessSetArmor now run the client gate (surfaced as resp["gate"]), matching a real player's screen. A new force param bypasses it to model a stale replica whose gate passed.
  • New tools/coop_test/test_shared_capacity_host_gate.py (throwaway mod: an over-capacity craft weapon + a zero-large-capacity Skyranger + a size-2 armor) checks each gate twice:
    • client gate (un-forced): the over-capacity change is refused locally and never sent.
    • host gate (forced past the client gate): the host rejects it (STR_NOT_ENOUGH_CARGO_SPACE / STR_NOT_ENOUGH_CRAFT_SPACE) and both worlds stay unchanged and equal.

Testing

  • python tools/coop_test/test_shared_capacity_host_gate.py — both sections green.
  • python tools/coop_test/test_shared_equip2.py — all three sections green (no regression from the harness signature change).
  • Serial x64 Release build clean.

🤖 Generated with Claude Code

In SHARED either player can edit any craft, so the client-side capacity
gate is not enough: a stale-view request that passes the initiator's gate
reached the host, which applied it without re-checking -> an over-capacity
craft that can desync or fault a later deployment.

- Factor CraftWeaponsState's four inline capacity checks into a pure
  static equipCapacityError() (byte-identical math) and call it from both
  lstWeaponsClick and craftRearmValidate.
- soldierArmorValidate now re-runs Craft::validateArmorChange host-side,
  mirroring SoldierArmorState::lstArmorClick.

Harness fidelity: harnessEquip/harnessSetArmor now run the client gate
(surfaced as resp["gate"]); a new `force` param bypasses it to model a
stale replica whose gate passed. New regression test
test_shared_capacity_host_gate.py drives an over-capacity craft_rearm /
soldier_armor from a replica with force and asserts the host REJECTS it
(shared_fail reason) with both worlds left unchanged and equal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@NonPolynomialTim
NonPolynomialTim merged commit 9b65d67 into main Aug 1, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SHARED: host validators don't re-check craft capacity (craftRearmValidate/soldierArmorValidate)

1 participant