Skip to content

Classic Mode 2-Player Co-op - #229

Merged
JRickey merged 9 commits into
mainfrom
feature/classic-coop
Jun 10, 2026
Merged

JRickey merged 9 commits into
mainfrom
feature/classic-coop

Conversation

@JRickey

@JRickey JRickey commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

Adds 2-player co-op to Classic (1P) mode, gated behind a default-ON "Classic Co-op (Needs reload)" toggle. With the toggle on, Classic mode opens the VS-mode character select screen so a second player can join the entire run — every battle stage, Break the Targets / Board the Platforms (shared completion in P1's character's map), Race to the Finish, and Master Hand. Challenger duels stay P1-solo. Full design notes in docs/classic_coop.md.

Highlights:

  • CSS: VS CSS serves Classic mode with a difficulty selector replacing the team toggle, stock clamp 1–5, slots 3/4 locked, solo-start allowed (falls back to stock solo Classic), B returns to the 1P menu.
  • Game rules: run ends only when both players are out of stocks; a continue revives both; an eliminated partner rejoins the next stage on their last life; run score sums both players' KOs; records stay keyed to P1.
  • Friendly fire off by default, opt-in via a "Co-op Friendly Fire" checkbox (applies from the next stage, also wired for bonus stages).
  • Intro screens show P2 posed beside P1 on every battle stage (reusing the Mario Bros. two-fighter card layout); P2 spawns side by side with P1 on stages without authored ally spawn points.
  • When the toggle is off, or only one human confirms, every code path is byte-identical to stock 1P mode (single predicate gate: sc1PManagerIsCoopActive()).

Bonus fixes that rode along:

  • Crash handler double-fault — the FP-walk readability probe wrote to /dev/null, which never copies from the source buffer, so it approved every address and the handler SIGBUSed on coroutine guard pages, losing the original backtrace. Now probes via self-pipe (docs/bugs/crash_handler_devnull_probe_2026-06-10.md).
  • Multi-gamepad port routing (libultraship) — routing no longer resets on device events, the Nth pad auto-assigns to the first free port, ports 2-4 get default mappings, and identical controllers get unique input-editor IDs + #1/#2 labels (docs/bugs/controller_port_routing_2026-06-10.md).

Submodule bumps: decomp → f96117676 (pushed to port-patches), libultraship → b58e7063 (pushed to ssb64).

Test plan

  • Headless smoke matrix (crash-free, 1200–1800 frames each): co-op direct boots of stages 0, 1, 3, 4, 5, 6, 11, 12, 13 with P2 on port 1; stages 0, 4, 6 with P2 on port 2; solo controls; VS CSS / 1P menu boots.
  • Hands-on with two DualSense pads: CSS flow, both-pads routing, co-op battle + bonus stages, next-stage revive, friendly-fire toggle.
  • Toggle-off regression: stock 1P CSS and solo run unchanged.

🤖 Generated with Claude Code

JRickey and others added 9 commits June 10, 2026 09:25
gEnhancements.ClassicCoop (default ON) is latched once at boot so the
"Classic Co-op (Needs reload)" checkbox only applies on the next
launch, plus the classic-context flag the VS CSS uses to know which
mode it is serving.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four-commit sequence on decomp feature/classic-coop: P2 handoff fields
and scrubs; VS CSS serving Classic mode; manager enrollment / lose
rule / continue; per-stage setup, bonus stages, and co-op death rules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…elf-pipe

ProbeReadable wrote the probed bytes to /dev/null, whose driver (XNU
nullwrite / Linux write_null) discards the request without copying from
the source buffer — the probe reported every address readable. On any
game-coroutine crash, WalkFPChain then stepped onto the adjacent
coroutine stack's PROT_NONE guard page at the chain's base frame and
SIGBUSed inside the signal handler, losing the original backtrace and
re-raising the wrong signal (see docs/bugs/macos_crash_2026-06-10.txt).

Probe via a self-pipe instead: pipe writes copyin the source bytes and
genuinely fail with EFAULT on unreadable memory. Fds are created at
watchdog init (pipe(2) is not async-signal-safe) with O_NONBLOCK +
FD_CLOEXEC, and each successful probe drains its bytes so the pipe
never fills. Verified by re-triggering the original crash: one CRASH
line, full 13-frame coroutine-stack backtrace with the true faulting
function at frame 0, process exits with the original signal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second-pad-dead family fixed: routing no longer reverts on device
events, the Nth new pad auto-assigns to the first free port, and ports
2-4 get default gamepad mappings at init. Write-up in
docs/bugs/controller_port_routing_2026-06-10.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gEnhancements.ClassicCoopFriendlyFire (default OFF) with a "Co-op
Friendly Fire" checkbox under the Classic Co-op toggle; read live at
each stage setup so flipping it applies from the next stage. Decomp
bump brings the CSS slot lock, wider spawn separation, P2 on the intro
screen, and the team-attack overrides.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…endly fire)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JRickey
JRickey merged commit 62b513e into main Jun 10, 2026
@JRickey
JRickey deleted the feature/classic-coop branch June 17, 2026 03:12
TechnicallyComputers pushed a commit to TechnicallyComputers/BattleShip that referenced this pull request Jul 10, 2026
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.

1 participant