WIP: feat(coop): add consent-based player presence and isolated save playtests #355

Draft
mooki wants to merge 120 commits from mooki/Longshot:coop-playtest-r9 into main
Member

Why

Friends should be able to see one another, agree on what their room shares, and try co-op without risking their usual saves. This is the whole experimental playtest line above #68, kept together so testers can use one matching build.

Work in progress. The tested head is 2f01e1b4bdb6e1629c54a998f0403519fe4e5c74 (r12), Linux source 05329d46. The pull request's branch is still named coop-playtest-r9; that name is historical. Build and report the commit, not the branch suffix.

The base is unchanged from r10: pull request 68 at 4a9b96060ad71a3b48e05ac0269fc7e0e1a4a626, on main cf605c85bbaeaaa78176b933b3bc1a3366e13a78. The 80 line commits and three r10 fixture/documentation repairs remain, followed by the three commits below. Main is still three commits later at 0caf06184b391a5259d3741bb006ef5826eb6417: documentation, Windows lift workers and button prompts. This is not current main. The text-field parent remains #51.

What is measured on r12: the whole game-free suite and all six short two-game Linux proofs, each passing at its first attempt with both complete logs checked for unexpected body removal or recreation. Linux builds and runs; the Windows executable and renderer cross-build and link. Not run on r12: Windows, two machines or a 30-minute session. Earlier LAN, soak and native Windows results retain their actual commits below, including the failed r10 continuity result and a separate open Windows body-disappearance report. This build cannot start a fight.

What changed

Pull request 68 already supplies the relay, sessions, signed progress, backups and the Co-op tab. Its work is a dependency, not new work to review twice. Above that pinned parent, the same pieces remain integrated in this order:

Piece What a player gets; where to read On by default?
S00: numbered signed rules Room features have stable numbered permissions; unknown signed rules survive forwarding and a missing rule grants nothing. host/coop_state.c, host/coop_proto.c. The rule format is used whenever this build joins; co-op itself stays off. Matching builds are required.
S01: signed preview and consent Read the maker's terms and identities before the second Accept press. Changed rules or members cancel an old press without replacing the joiner's local preferences. Co-op and PvP setup have different, explicit help. host/coop_menu.c, gpu/vk/vk_menu.cpp. Mandatory when accepting; room mode defaults to Co-op, not a fight.
S02: one owner for game changes Leaving or losing permission invalidates already queued changes as well as future messages. Progress, presentation and temporary effects have separate permissions. host/coop_gameplay.h, host/oot_coop.c. The checks are always used; they grant nothing by themselves.
S03: child bodies An accepted friend's child body can appear in the same place, with locally generated assets and guarded lifetime. host/oot_coop_players.c, nx2/coop_players_assets.py. Off: both local body display and the signed age permission are needed.
Child pose and prompt repair The remote child uses the current animation frame and a full-body standing/walking binding rather than a fixed arms-out pose. Existing menu-prompt checks are adapted without weakening them. Automatic when authorized animated bodies are enabled; no new opt-in.
S07: temporary profile Temporary save copies saved checkpoints, restarts into a working copy, then offers Return to normal saves. It never copies progress back. host/coop_profile.c, host/nn_fs.c, gpu/vk/vk_menu.cpp. Off: a deliberate two-press action outside a room.
Separate copy banner and refusal The copy reminder no longer hides an actionable room refusal. Both fit at the tested menu scales. gpu/vk/vk_menu.cpp. Automatic on a temporary copy.
S08: signed checkpoint transfer A selected checkpoint can be offered to one explicitly consenting recipient, verified and stored separately. It is not loaded or imported. The save picker is not included; this is available through the local control API. host/coop_save.c, host/coop_save_runtime.c, host/coop_profile.c. Off: separate identity, owner and exact-checkpoint approvals are required.
S13: native combat observations Optional records distinguish a requested health change from a committed loss. Receiving a record cannot hurt another player. host/oot_coop_hits.c. Off: signed observation permission and local tracing are separate switches.
Complete hit trace Independent bounded records for health changes and attack activations can establish a closed observation interval; missing critical records invalidate it. Optional details are counted separately. host/oot_coop_hits.c. Only while tracing is enabled; tracing stays off.
Position privacy Positions go only to currently accepted recipient sessions; revocation also discards queued poses. This is not private encryption from other holders of the room secret. host/coop.c, host/oot_coop_players.c. Automatic for authorized presentation; no extra disclosure is enabled.
Adult bodies, sword/shield display and adult binding Separate generated adult body/animation assets and cosmetic sword/shield state. Unsupported equipment is hidden rather than guessed. host/oot_coop_players.c, nx2/coop_players_assets.py. Off: additional signed age/gear bits are needed. Generated adult animation and full gear behavior are not yet shown in game; the earlier Windows standing-model report is separate.
Friend names, health, place and arrows Names over visible friends; separately permitted health/place in the list and arrows towards same-map friends off screen. Unknown or stale values say unknown. host/oot_coop_players.c, gpu/vk/vk_menu.cpp. Names are selected locally by default, but require accepted visible bodies; health/place/arrows remain off in the signed permissions.
Prompt and damage-scaling integration Existing input prompts and local Damage taken behavior keep their checks beside the expanded co-op interfaces. No new permission or default; not remote damage.
Go to friend A two-press trip towards an accepted friend's current position, limited to Kokiri Forest and Hyrule Field in the player's existing age. It cannot pull another player and refuses on native temporary copies. host/oot_coop_warp.c. Off: maker permission plus local confirmation. Live arrival safety is not yet proven.
Room chat and notices Accepted players can type short messages, mute a member locally and choose join/item notices. Typing is fenced from game input; no chat history is saved. host/coop_chat.c, host/coop_chat_runtime.c, gpu/vk/vk_menu.cpp. Signed chat off; local notices default to Everything, subject to acceptance and actual shared events.
Temporary sword-practice kit Separately confirm a child sword/shield pair or an adult pair on a native temporary copy. Actual equipment readback, not an icon, decides readiness. host/oot_coop_kit.c. Off: signed catalog/mask, copy presence and another two-press action are needed. No other item families.
Arena readiness and separate duel scores Diagnostic Ready explains missing native safety/contact/damage support. The game-free round/score logic is separate from real health and saves; no production contact binding starts it. host/coop_duel.c, host/coop_duel_menu.c. Fighting is disabled. The diagnostic row is hidden unless tracing and PvP setup are selected.
Seeing and talking on copies Accept to see and talk grants presentation on the currently mounted copy, not progress, pools, downloads, inventory or travel. Leaving or changed terms revoke it. host/oot_coop.c, host/coop_menu.c. Off until a fresh explicit acceptance; ordinary acceptance never carries across Copy or Return.
Combined presentation and kit consent A kit checks the current copy-only presentation acceptance and its own independently confirmed offer. No automatic kit grant.
Reliable retry and retirement counts The existing coop: stats: line now names reliable retries and events given up. Deliberate cancellation contributes to given-up counts; they are not a packet-loss percentage. host/coop.c. Counters follow the existing active-room statistics; no new switch.
Cross-piece integration repairs The combined menu, input, renderer interface and source-reading tests use the same contracts. Local display choices remain separate from signed rules. No additional feature is enabled.
Transfer-on-copy regression coverage The actual room offer on a temporary copy is available to the separate download consent path. Tests retain the no-presence/no-progress boundary. tests/test_coop_save_copy.py. No new default or permission.
r9: native export-path fix Linux checkpoint export converts the native mount's Windows separators before filesystem lookup, and rejects parent-directory components. The regression reaches the real mount/export boundary. host/coop_profile.c, tests/test_coop_export_path.py. Automatic at the storage boundary; consent and Windows valid-path handling are unchanged.

No shared enemy/boss simulation, playable PvP, arbitrary travel, save-owner picker, progress return from a copy, shared clock, invite service or mod-loading interface is included. Models and animations are generated from each player's own game; none is distributed with the source or sent over the network.

Earlier r10 replay (1ed75e61): at that head the co-op core was byte-identical to r9: protocol, signed consent, progress/presence/download separation, save profiles and transfers, body adapters, chat, travel, kits and trace queues have no new behavior. Shared files retain the newer base's input toggles, audio/Intel menu rows, sampled game state, filesystem handling and logs/ / cache/ layout. The observer hooks are composed with the base's full pre-clamp damage-policy argument; the actual game-code translation was regenerated and both products rebuilt, rather than relabelling an r9 binary.

The three fixture/documentation commits that completed r10 were:

  • 7f4dcaf4, tests/test_menu_feature_rules.py: provide the newer menu parser's Plus/Minus exclusions, keeping every rule-registry and saved-key assertion.
  • 44c6f99a, tests/test_menu_text_field.py: combine the real pad and text-release callback declarations and retain the correct input-polling sections beside chat, without losing text, repeat, toggle or restart checks.
  • 1ed75e61, tests/test_oot_coop_hits.py: use the base's three-argument damage-policy interface and add overkill checks for both native update paths, retaining the original request, scaled committed life and transmitted result checks.

Those three fixture commits changed no production code. The two presence fixes below do; no permission, filesystem refusal or test assertion was removed to obtain the results.

Three commits after r10:

  • 2be4e92335a722468631d19d7fac52e12f970d5e, fix(coop): preserve presence across unchanged roster heartbeats: an identical periodic roster is no longer published as a changed room view. A change of view could briefly close otherwise valid presentation permission before its consumer caught up. Real member, session and rule changes still revoke old permission; no grace period or consent bypass was added.
  • 15a68a5ebaafc7638e6dfa7fb6e6de969c8a16e0, docs(coop): list the line's switches in the switch reference: adds the 25 missing direct switch entries and completes automatic-acceptance documentation, with defaults, who sets them, and test/development scope. It changes only docs/development.md and the README's Teleport note.
  • 2f01e1b4bdb6e1629c54a998f0403519fe4e5c74, fix(coop): apply joined views after the native engine starts: a joined room view stays pending until it has actually been applied to an existing native engine. The next ordinary session update can consume it without waiting for another roster publication.

Why both fixes are needed: the r10 30-minute session at 1ed75e61 contained an unrequested hide/delete/recreate in one game despite the room remaining accepted. Investigation reproduced the unchanged-roster publication race in both r9 and r10; it is consistent with that interruption, although the original log did not record the exact cross-thread version pair. The intermediate head 15a68a5e, containing only the first fix and the documentation, then failed its first two-game proof before signed-room readiness and was never pushed. It exposed a second fault: the initial view could be marked handled before the native engine existed, and the old repeated heartbeat had accidentally retried it. The second fix retains that view until applied, while keeping the unchanged-heartbeat repair.

Two game-free regressions cover the distinct boundaries: tests/test_coop_roster_presence.py forces the publication/consumption race and preserves revocation for real changes; tests/test_coop_join_publication.py drives a real join, signed identity/charter exchange and explicit acceptance with engine creation and callback registration in different orders. Both pass with GCC and Clang on the corrected code. The join test compiles and fails on the intermediate head with both compilers. The first race test began with an existing engine and accepted room, so its success alone had not established startup joining.

Flags and settings

Defaults below are the unchanged co-op defaults of r12, not a recommendation to enable every feature. Signed defaults initialize a maker's draft; an absent permission is still off. New terms require fresh acceptance. All co-op remains opt-in, and all participants need the same compatible build. The Untested on column describes this r12 head; the separately labelled r9/r10 LAN, soak and Windows evidence below does not turn those into r12 results.

The inherited connection settings are coop / HOS_COOP (off), coop_host / HOS_COOP_HOST (off), coop_server / HOS_COOP_SERVER (unset; relay port 24680), coop_port / HOS_COOP_PORT (0, a free client port), and coop_name / HOS_COOP_NAME (a generated player name). Make/Join enables the session for that run. Only the maker enables hosting; Join never hosts. No router or firewall is changed automatically.

The inherited ordinary-save sharing defaults are Shared for coop_progress, coop_items, coop_songs and coop_quest; Pool for coop_keys; Own for coop_rupees and coop_ammo. They apply only after ordinary consent and a completed backup. A native temporary copy never shares this progress, even if its room displays Shared. For a first playtest, use disposable save roots and Own categories.

The seven category environment variables below are inputs to the maker's signed room rules, not separate permission to write a peer's game. With Shared progress, Items/Songs/Quest are normalized to Shared; with Own progress, Keys is normalized to Own. An unset or empty category override uses the saved key, then the stated fallback. Changing the accepted rules needs fresh consent. Test-only switches are not recommended player settings, and the two debug Teleport overrides already exist in the base; Go to friend merely adds a refusal when they are set. These rows document source behavior, not new runtime coverage.

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
HOS_COOP_PROGRESS, HOS_COOP_ITEMS, HOS_COOP_SONGS, HOS_COOP_QUEST Signed room-rule inputs: choose shared or own for progress, items, songs and quest items respectively; a nonempty environment value overrides the corresponding coop_* key in the maker's draft Unset: use the saved key; fallback shared for each Prefer Own on disposable first-playtest saves; review the signed result Deliberately accepted ordinary-save sharing No progress on native temporary copies or in PvP setup; no grant before consent/backup Each environment-override combination in live r12 play; Windows gameplay
HOS_COOP_KEYS Signed room-rule input: pool or own for dungeon small keys, over coop_keys Unset: use the saved key; fallback pool, normalized to own when progress is Own Use Own for the first playtest Agreed small-key pooling on ordinary saves No pooling on native copies; no independent keys pool with Own progress Live r12 key-pool/override cases; Windows gameplay
HOS_COOP_RUPEES, HOS_COOP_AMMO Signed room-rule inputs: pool or own for rupees and ammunition respectively, over coop_rupees and coop_ammo Unset: use the saved keys; fallback own for both Keep Own unless everyone explicitly wants the pools Agreed resource sharing on ordinary saves No pools on native temporary copies; no permission from joining alone Live r12 resource-pool/override cases; Windows gameplay
HOS_COOP_AUTO Test/automation switch: create or join requests a room at startup while co-op is enabled, then recreates or waits for that room after relay loss; it is not acceptance Unset, empty or off: no automatic request Leave unset for human playtests; use Make/Join Repeatable connection tests Does not switch co-op on by itself or grant consent; Copy/Return suppresses inherited automatic joining Live r12 reconnect/loss cases; Windows gameplay
HOS_COOP_ROOM Test/automation switch: private room secret for automatic create/join; also selects the secret used by the menu's Make action Unset: Make/create generates a fresh secret; automatic join requires a valid supplied secret Leave unset for human playtests; never publish its value Connecting controlled test peers to a known room Does not enable co-op, accept anyone or bypass an already-open-room refusal Live r12 known-secret edge cases; Windows gameplay
HOS_COOP_ACCEPT=always Test-only switch: automatically accepts a valid ordinary room offer instead of asking for the local menu confirmation; signature/identity and backup checks still apply Unset or any value other than always: manual acceptance Leave unset; never use on a normal playing save Deliberate automatic-consent tests on disposable saves Does not grant copy-only see-and-talk, downloads or a kit; Copy/Return clears inherited automatic acceptance Full automatic-consent matrix on r12; Windows gameplay
HOS_COOP_DROP Test-only switch: randomly discards the requested percentage of incoming co-op datagrams, capped at 100 0 percent Keep 0 unless deliberately testing loss Receiver-side loss simulation Not a real-network loss measurement or a delivery guarantee Live r12 induced-loss scenarios; Windows gameplay
HOS_COOP_DEMO Test-only switch: drives the Co-op menu with a made-up room; its menu actions start no network session Off Leave off for every gameplay or network proof Menu screenshots and offline menu checks Does not establish connection, consent or gameplay with a real peer Windows execution and physical input for the demo
HOS_COOP_PROBE Development diagnostic: after a normal save reaches play, logs the flag table and compares its reads with the game's getter; by itself it writes nothing and needs no room Off Leave off except for a requested diagnostic; keep generated flag details private Investigating the local progress reader Not progress sharing, native copy support or evidence of a natural pickup Live r12 probe-only use; Windows execution
HOS_COOP_TEST_SET Test-only switch: schedules local Boolean/integer flag writes through the game's setters as hash=value@frame, up to 8 entries; these writes can become saved progress Unset: no scheduled writes Leave unset; use only disposable test roots, never a normal playing save Controlled local grant tests while the co-op/probe update is active Not natural acquisition or consent; the native temporary-copy path never runs this aid Live r12 scheduled-grant scenarios; Windows execution
HOS_OOT_TELEPORT_EVENT Existing development override for the local debug Teleport event name; this line adds a Go to friend refusal while a nonempty override is set Unset/empty: the built-in map-change event Leave unset; not a co-op travel workaround Focused debug-travel investigation Any nonempty value disables Go to friend rather than changing its event; no remote event selection Live r12 override behavior and safe travel; Windows gameplay
HOS_OOT_TELEPORT_STARTER Existing development override for the local debug Teleport request: exactly 0 omits Link as its starter; other values retain him. Go to friend refuses any nonempty override Unset: Link is the starter Leave unset; not a co-op travel workaround Focused debug-travel investigation Even 0 or 1 makes Go to friend refuse; no network-selected starter or new permission Live r12 override behavior and safe travel; Windows gameplay
coop_room_mode / Room rules: Mode 0 Co-op; 1 PvP setup; -1 refuses acceptance 0 Keep Co-op for playtests Matching consenting peers Does not enable fighting Windows and cross-PC
Accept the room / Accept to see and talk Two-press review of the current signed offer; the latter is copy-only presentation Not accepted Always review identities and terms Both save domains No automatic consent after restart or changed terms Physical input and copy-presence runtime
coop_presence_permissions / Friend information Add bits: 1 child, 2 adult, 4 health, 8 arrows, 16 gear, 64 place 0 Start with 1; add only disclosures both players want Same-map presence; optional friend information Does not grant inventory or contact Adult/gear/HUD runtime; Windows and cross-PC
coop_players / HOS_COOP_PLAYERS Local body display and authorized pose sending 0 Set 1 on each test game Authorized child bodies No effect without consent, age permission or a matching build Windows and cross-PC
HOS_COOP_PLAYERS_ANIM Local animation override Automatic with generated assets Leave unset Standing/walking bodies Does not supply missing assets Adult/full pose parity
HOS_COOP_PLAYERS_ACTOR, HOS_COOP_PLAYERS_TEST Local diagnostic actor/feed overrides Unset Leave unset Focused diagnostics Not real-peer evidence or consent This playtest
coop_names / Friend names Signed permission for names over visible accepted bodies 1 Keep; locally hide if unwanted Accepted same-map friends No body or position grant Live name placement; Windows
coop_labels / Friend labels 0 Off, 1 Names, 2 Names and arrows 1 Names first; arrows only with signed bit 8 Local readability/navigation Hiding labels does not revoke disclosure Live arrows and multi-player readability
Temporary save / Return to normal saves Copy/restart and return without copying progress back Inactive Use on a disposable root for this WIP Isolated play Does not merge or import progress Windows filesystem/restart behavior
HOS_COOP_PROFILE_TICKET Internal one-use restart state Absent on an ordinary start Never set or copy by hand Native Copy/Return Not a user-selected save or transferable permission Windows execution
Local coop_save actions: status, identity, offer, accept, cancel Separate signed checkpoint download consent and storage No transfer Not part of the short menu-only playtest Explicit owner and recipient No picker, import, load or copy-back Same-room term-change cancellation live; Windows
coop_warp / Go to friend Signed permission and separate two-press local travel 0 Leave off for the first copy-only session Normal disposable saves, forest/field Native temporary copies and other maps Actual arrival/hazards and Windows
coop_social_permissions / Room chat Bit 0 permits accepted-member text 0 Enable only for an agreed chat trial Room communication No progress or identity authority Two-game chat rendering/input
Chat / key_chat Open text entry; Enter sends, Esc cancels T Keep unless it conflicts with a binding Keyboard input; controller paste No new controller character picker Physical typing/paste/controller input
Mute chat Mute/unmute an accepted member locally Nobody muted Use as needed Local feed Does not remove a member or change others' screens Live mute/unmute
coop_notices / Notices 0 Off, 1 joins/leaves, 2 all including shared-item notices 2 Prefer 1 for a copy-only test Accepted room transitions An item notice is not proof of its original finder Live notices and catch-up
coop_hit_events / Native hit observations Share committed native loss observations 0 Leave off Diagnostic observation Never applies remote damage Networked loss observations
HOS_COOP_HITS_TRACE Local attack/health trace and diagnostic Ready row 0 Leave off except for an observer test Closed native observation intervals No collision or sword-owner proof Windows; long-session trace bounds
HOS_COOP_ARENA_FILE Local diagnostic arena description Unset Leave unset Private arena validation Cannot enable a fight Native safe-area certification
coop_loadout_version, coop_loadout_preset Practice catalog and display label Both 0 Leave 0 for the first session; catalog 1 is sword practice Separately accepted temporary kit No complete item palette Actual kit use
coop_loadout_age, coop_loadout_bits_0 Explicit age and kit mask Both 0; no kit Catalog 1 only: age 0/mask 3 or age 1/mask 12 Child or adult sword/shield pair No age change; wrong age refuses Both live kit families
coop_loadout_bits_1 through _7 Remaining item masks All 0 Keep 0; unsupported bits refuse Reserved terms only Other item families Unsupported by this kit
coop_loadout_magic, coop_loadout_seeds, coop_loadout_arrows, coop_loadout_bombs, coop_loadout_bombchus, coop_loadout_nuts, coop_loadout_sticks, coop_loadout_magic_start Signed magic/ammunition terms All 0 Keep 0 Reserved terms only No magic/ammunition kit Unsupported by this kit
coop_loadout_bottles, coop_loadout_bottle_0 through _4 Signed bottle terms All 0 Keep 0 Reserved terms only No bottle kit Unsupported by this kit
Sword-practice kit Independent two-press give/equip action on an accepted copy Inactive Separate test only A matching live kit offer Normal saves; presence alone Usable sword/shield proof
coop_combat_mode, coop_damage_model Duel permission and receiver-native damage model 0 and 1 Keep combat off Diagnostic terms Neither installs native contact/damage support Playable combat
coop_pvp_damage, coop_pvp_items, coop_pvp_grace, coop_pvp_shield Damage percent, allowed attack mask, grace in ms, shield mode 100, 1, 700, 0 Leave defaults; fighting remains disabled Game-free duel contract No remote damage fallback Native combat
coop_duel_hearts, coop_duel_rounds, coop_duel_healing, coop_duel_revive Separate hearts, best-of rounds, healing and revival terms 10, 3, 0, 0 Leave defaults Game-free round accounting Does not change native health or death Live rounds
coop_arena, coop_arena_hazards, coop_arena_time Arena and environment terms All 0 Leave unset/off Diagnostic readiness No surveyed arena is shipped Native arena safety
Ready for duel Diagnostic two-press preview; reports unavailable Hidden without tracing/PvP setup Do not expect a fight Explaining missing capabilities Cannot start a round Playable PvP
HOS_COOP_TRACE Existing packet/progress diagnostics, now also body details 0 Off normally; opt in for a requested log Finding connection/presentation faults Not a performance measurement Cross-PC trace collection
RX_VK_MENU_SCRIPT with CHAT Diagnostic entry into the same chat text field Unset Leave unset for human play Reproducible input checks Cannot bypass room consent Physical input is separate

No new public build flag or command-line launch flag is required. The existing --user selects a separate test save root. Do not put HOS_COOP* switches in the shareable longshot_env.txt: they are deliberately ignored there. The detailed reference is docs/coop.md; commit 15a68a5e, included here, now supplies the missing named switch entries in docs/development.md and the README Teleport warning. Internal callback/option numbers are not user settings.

Newer-base startup differences, not new co-op permissions: ordinary file logging is now logs/longshot_log.txt, with rotated logs in the same directory. Runtime caches are in cache/, including longshot_vk_pipelines* and generated cache/prompts/ files. Terminal or redirected launches keep their actual output stream instead of necessarily creating that log file. On Windows Intel, keep intel_shader_compiles=0 (one compiler thread at a time) and leave RX_VK_NO_INTEL_SERIAL unset; do not disable that safety default to start a test. The inherited hold-to-toggle option stays off and Mono output stays Auto by default. The updated tester guide is #355 (comment).

Measurements

These are functional results, not a performance comparison. The base below is the pinned ancestry, not current main. No main-versus-r12 timing pair was run; older off-cost or frame-duration observations do not measure this corrected head.

main: cf605c85bb (2026-11-14)
this PR: coop-playtest-r9 @ 2f01e1b4bd
Hardware: NVIDIA RTX 4090, driver 615.78.08; Core i9-13900K, 96 GB RAM; Linux 7.2.15-1-omarchy x86-64.
Scene: child play in Kokiri Forest and Link's house; requested output and captures 1280x720, frame generation off, cap 60; windowless. The renderer reports an initial 1920x1080 offscreen target. No swapchain/display claim.
Method: six separately started two-game sessions on the exact r12 build, with a local relay and disposable saves; every proof passed its first attempt. Each requires the original consent/save/pose/observer checks, inspected new captures, both normal exits and a scan of both complete stopped-game logs, including coop: puppet: and coop: players:, for unexpected body hiding, deletion or recreation. Other compilation was kept out; this is not a performance experiment.
How to reproduce: build commit 2f01e1b4bdb6e1629c54a998f0403519fe4e5c74 from each player's own supported game and use separate save roots and matching peers. Check signed terms/stale acceptance, body consent and Leave, reciprocal child steps, native Copy/autosave/Return, an explicitly approved checkpoint transfer with a genuinely partial cancellation, and five visible sword swings with closed health/activation records. Account for every body creation and removal against the requested action, not only pictures at the endpoints. The short human guide uses menu consent, not the control-API transfer procedure.

Result main @ cf605c85b this PR @ 2f01e1b4
Whole game-free suite Recorded baseline: 540 passed, 3 failed, 16 skipped; not rerun for this round 580 passed, 3 failed, 16 skipped; exit 1
Six two-game proofs Not run on main; these features are not all there All six pass at the first attempt, including complete-log body-lifetime scans

No performance/off-cost result exists for r12.

The suite's exact final line:

580 passed, 3 failed, 16 skipped in 187 s (linux, 6 at once)

It ran once with strict prerequisites. All 54 line-added-or-modified drivers pass, including both new presence/join regressions. The three failures are still the pinned main's:

  • tests/test_framegen_resources.py: its extracted fixture lacks the Intel driver-guard type and shader/compute declarations.
  • tests/test_module_ids.py: its source-extraction marker no longer matches the function it seeks.
  • tests/test_pad_layout.py, PadLayoutTests.test_presets: its extracted fixture lacks HoldToggle.

That is one additional pass over intermediate 15a68a5e, two over r10 1ed75e61, with no new failure or skip; it is not an all-green suite. The translation failure from the older r9 base no longer occurs. The separately recorded GCC-only test_menu_keys.py declaration failure remains earlier focused-check evidence; the full suite uses Clang for that driver and passes it.

Proof on r12 Held time, including shutdown Result and complete-log boundary
S08: signed checkpoint transfer 321.8 s Pass: the joiner now identifies the maker, consumes the signed charter and receives a nonzero offer; fresh download-only consent, immutable owner export and exact recipient approval complete. A second transfer is cancelled after 8,192 of 160,019 bytes with no usable partial or mount change. Neither game creates a remote body.
S02: body consent and Leave 373.9 s Pass: no body before consent, then exactly one remote body created in each game. Both retire only after actual Leave and do not return; no intervening hide/delete/recreation.
S07: native temporary save 370.1 s Pass: both Copies and Returns, A's natural autosave and retained changed working checkpoint, both normal saves and immutable originals unchanged after exit. No remote-body lifecycle event in this out-of-room proof.
S01: signed terms and acceptance 303.2 s Pass: matching signed terms, tab/close cancellation, stale disarm and fresh PvP-setup acceptance, with no native progress applied and no body created.
S03: child standing and walking 125.0 s Pass: full arms-down bodies and reciprocal visible steps, then standing again. Exactly one remote body per game, removed only when signed presence is switched off; no intervening recreation.
S13: native health observer 127.7 s Pass: five visible swings beside a still B, complete critical records and zero committed player loss through the instrumented path. Exactly one initial body per game, no recreation, and only explained shutdown retirement after the final records. Not contact or PvP.

All 12 games exited 0. Both complete logs for every proof pass the lifecycle review; a generic “map change or a load” reason is not accepted as an explanation without the corresponding requested action. All six runs have complete kernel coverage without reported driver faults. Six present intervals above one second occur only in S08's initial normal generations: four during entry, two near native Copy/restart; neither temporary generation has one. Their precise cause was not isolated. These short successes do not establish 30-minute continuity.

Earlier evidence keeps its head: the r10 short-proof round at 1ed75e61 retained a failed first S03 picture-framing attempt, then passed with the same input/camera route while other compilation was excluded. That history is still in #355 (comment); it is not an r12 failure or an r12 speed result. The intermediate 15a68a5e failure at signed joining and the failed r10 long session remain failures, not renamed passes.

Earlier export-path repair, still present: the r8 failure came from Windows separators reaching Linux filesystem calls. The r9 fix normalizes the profile/export boundary and rejects parent components; its earlier 92-outcome compiler matrix and 5 Windows-target syntax units remain r9 evidence. The exact r12 whole suite passes the regression and the new S08 run proves actual transfer without weakening owner selection, signed descriptions, recipient consent or path refusals. R8/r10 focused matrices and syntax checks are not described as rerun on r12.

Testing

Tested: the exact r12 Linux executable and renderer build and run, source 05329d46. All seven locally generated child/adult actor, animation and shared-table files are present and agree between the two builds. All six short proofs above pass, including post-shutdown scans of both complete logs; child standing/walking, native Copy/autosave/Return, signed transfer/partial cancellation and the bounded complete health-observer interval have new r12 evidence. The whole r12 suite has 580 passes and the three named inherited failures. Both race and join regressions pass with GCC and Clang; the join regression fails on intermediate 15a68a5e before the second fix. The switch-reference commit 15a68a5e passed its seven selected documentation/source-reader checks before and after, not a separate r12 runtime test.

Built for Windows at this head, not run: the exact r12 Windows x86-64 executable and Vulkan renderer cross-built and linked on Linux with llvm-mingw 20261104 / Clang 23.1.5, release/PGO settings, exit 0, with no compiler/linker failure or source workaround. Validated unchanged generated game code from r10 was reused; the full products contain the corrected host code and v3.0.0-rc.3-179-g2f01e1b4b (source 05329d46). The production warning-suppression flags remain, so this is not warning-clean evidence. Static dependency checks and embedded version checks are not Windows execution, visible-menu proof or Windows UTF-8 runtime-path coverage. No r12 Windows process has run in this evidence.

Earlier heads only:

  • LAN, 0ec9d0b0: Windows hosted through the game and Linux joined; signed acceptance, reciprocal child bodies/walking, chat and Leave worked, with normal exits and unchanged disposable saves. Linux hosting in the reverse direction was blocked by its firewall, not a successful reverse-direction game test. This was short play on normal disposable saves, not native temporary copies, shared progress, transfer or kits: #355 (comment).
  • First 30-minute session, 0ec9d0b0: one Linux PC, game-hosted loopback traffic. Connection, observed bodies/names/chat and unchanged normal saves held, but the presence readings missed the predeclared sampling cadence. It remains qualified, not an unqualified pass: #355 (comment).
  • Later 30-minute session, 1ed75e61: the completed restart met its sampling schedule but failed body continuity: one A-side hide/delete/recreate while the room remained accepted. The first attempt's earlier undetermined stop and the openly revised swap criterion remain in its record. Normal exits and unchanged saves do not turn it into a pass, and neither new fix retroactively repairs that result: #355 (comment).
  • Native Windows report, 1ed75e61: Windows 11 Pro 26300, Core i9-12900K, 64 GB RAM, RTX 3080 10 GB / NVIDIA 621.14. The tester reports a native build and suite of 556 passed, 6 failed, 35 skipped, against main 0caf0618 with 521 passed, 6 failed, 34 skipped. The same six failures are test_pad_layout.py, test_release.py (OpenSSL path), test_tested_configurations.py (line endings), test_oot_state_tap.py (local control connection), test_module_ids.py and test_framegen_resources.py; this is not an all-green Windows suite. Two windowed games on that one PC joined a separate loopback relay with automatic acceptance, made backups and left both saves unchanged. Adult bodies were reported as standing-model fallbacks because generated animation assets were missing, so this is not adult walking-animation proof. The repeated body disappearance remains open below. No native Copy/Return, real NTFS junction-refusal case, game-hosted relay, firewall prompt or two-PC session was tested in that report: #355 (comment).
  • Earlier limited Windows title start, 1ed75e61: the cross-built r10 folder reached the title and quit on Intel Arc / driver 32.0.101.9175 with co-op off, on-demand shaders, pipeline-cache load/save disabled and native float atomic add forced. Its nonfatal programs (10 failed) counter stays recorded. It is not an r12 start or completed preparation.

Not tested: on r12, any Windows runtime or execution of its changed C/C++ fixtures, a two-machine session, game-hosted LAN/internet/NAT gameplay, a 30-minute session or longer play, and windowed presentation. A new r12 long session is planned separately; no result is claimed here. Also not settled: AMD/Steam Deck, physical input, more than two live games, active-room performance/off-cost or continuous frame-fault coverage. The six r12 proofs use a separate local relay. Generated adult animation/full gear, live arrows/health/place, safe Go to friend arrival, copy-only see-and-talk presence, full chat focus/mute/notices behavior and usable practice kits need their own coverage. S08 tests old-generation refusal, not same-room signed-term changes during a live transfer. Downloads are stored, not loaded or imported. The bounded native health trace does not prove a hit, shield block, every health writer or playable PvP. Broader ordinary progress/chest/boss behavior is not re-proven by these six cases.

Known open problem, found by the Windows tester at 1ed75e61: with two games sharing one GPU over its video-memory allowance, one game fell to 1 to 8 frames a second, and at each such collapse it hid and later re-made the other player's body while the peer logged in no map: seven times in about two and a half minutes, none in the stretches at 37 to 52 frames a second (#355 (comment)). This is the play check working as written: a game whose play counter has not ticked for 2.5 seconds counts as not being played, removes the bodies it shows, and asks for them again only 2 seconds after it recovers. It is too quick for a real hitch (a starved GPU, a long shader compile). The two fixes in this head do not change that rule; what to change is still to be decided.

Needs other testers:

  • Windows with a Linux partner on two PCs: pin this r12 commit, use the game's hosting and separate save copies, then Make/Join/review/Accept, move, leave and return. Expect no body before consent and no unexplained removal while accepted; test both hosting directions with only the intended network permissions (T-ADK4; the base's request is T-SR3D).
  • Windows: rerun the changed fixtures on r12, particularly both new presence/join regressions, and test native Copy/Return, export, locks, real junction refusal and UTF-8 runtime paths. Copy/Return needs a real Windows window. Earlier Windows results remain at 1ed75e61 (T-QF6F).
  • Windows body continuity: with each game on its own GPU, or one game per PC, check that the other player's body stays through ordinary play; and note any hitch of more than two seconds, because the play check removes bodies after 2.5 seconds without a tick (the open problem under Not tested) (part of T-KQUM).
  • Other GPUs and physical input: generated child/adult presentation, labels/arrows, chat focus and copy/Return with logs and pictures (T-KQUM).

Screenshots

These eight pictures were taken on 1ed75e61 (r10) and keep their original attachment links and captions. Freshly inspected r12 captures showed the same illustrated states in all six proofs, as recorded by the new proof review; room fingerprints, idle poses and foliage phases naturally differ. They remain r10 illustrations, not relabelled r12 captures. R12's own signed replies, file/mount checks and complete lifecycle logs establish the new results; stills alone do not prove transfer, save isolation or uninterrupted body lifetime.

Both games show matching signed Co-op terms and read-only help before acceptance.
The joiner's first press is armed, then changed room rules cancel it.
The new PvP setup is read and accepted afresh; the room says no progress is shared and no fight starts.
A left, B right: local-only before consent, both complete bodies after acceptance, local-only again after actual Leave.
Successful r10 retry: each local child steps and the other game's received body visibly steps too, with both full bodies in view.
Native temporary copies, A's opened chest and Saving indicator, then both titles after Return; mount/file checks establish unchanged normal saves.
Download-only consent, verified storage and partial cancellation with only local bodies; signed replies and independent hashes establish the transfer result.
Both views before, during the fifth swing and after all five, with full pictured hearts; complete native records establish only the bounded no-loss result.

Risk and rollback

This is a large change across save handling, permissions, networking, input, generated presentation and the host/renderer interface. A permission or path mistake could affect a save; incorrect body lifetime could leave a wrong actor; stale input could turn a chat key into a game action. Network loss or the host leaving can stop the room. An invited holder of the secret can lie about cosmetic poses or chat; this is not an anti-cheat system. A verified checkpoint hash proves its bytes, not that an untrusted save is safe for the game's parser.

Use a separate --user root from the first launch, containing only a copy of your own saves, and keep the normal installation closed. Native Temporary save adds another isolated working copy. Return switches back to that test root without importing progress. An ordinary restart after a crash selects the normal root for that launch, so never omit --user when resuming this test. Copies stay for recovery and are not automatically pruned.

Leave the room or set coop=0 / HOS_COOP=0 to stop further sharing; coop_host=0 / HOS_COOP_HOST=0 disables hosting and coop_players=0 / HOS_COOP_PLAYERS=0 disables local bodies. Revoke the signed presence bits to withdraw disclosure; merely hiding labels is not revocation. Turning co-op off or reverting source cannot undo progress already merged into an ordinary save. Do not copy a test save back into the usual save folder. To stop using the line, keep its test folder separate and return to the unchanged normal installation.

Known wording limits remain: the human-readable signed-rule log is abbreviated; pending PvP log prose can mention progress even though the apply gate stays closed; a body-retirement reason can call Leave a map change/load; the copy reminder can be shortened at 1280x720. Read the actual signed Rules page and acceptance state, not those shortened diagnostic sentences, when deciding what to accept.

The other player's body being hidden and re-made when one game stalls for more than 2.5 seconds, found on Windows at 1ed75e61, is a known open problem of the play check itself. These roster and start-up fixes do not repair it, and the short Linux passes did not exercise it.

Reviewer notes

Start with the consent and separate save-domain boundaries, then native Copy/export and body lifetime. The later presentation, travel, chat and practice pieces deliberately do not reuse download consent as permission to affect a game. The native damage/contact connection is absent rather than replaced by guessed remote damage.

Review question: how would you like a line of this size cut for review now that the line is on the current pull request 68 head: signed rules/consent and ownership first, then bodies, save isolation/transfer, and the optional social/travel/practice pieces, or another boundary? This is one WIP tracking pull request, not a request to merge every piece at once. The existing core-versus-module discussion remains at #42; no module interface is built here.

The corrected r12 head has passed the new six-proof round. Next are its own continuous 30-minute session and two-PC/Windows runtime checks, neither claimed here. The r9 qualified soak, failed r10 soak, r9 LAN session and native r10 Windows report retain their original commits and outcomes. Main remains three commits beyond the pinned base; the current-main checkbox and review-splitting question stay open.

Issues

See #68 for the co-op base and earlier proof discussion.
See #51 for its shared text-field dependency.
See #42 for the unresolved core/module direction.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main. Replayed onto pull request 68 at 4a9b9606 / main cf605c85b, but main has since advanced three commits to 0caf06184.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above). The exact r12 run has 580 passes and the three named failures of its pinned main base, not an all-green result.
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). The three requests are open: T-ADK4, T-QF6F, T-KQUM.
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. No timing/off-cost comparison exists for the full line.
  • No game files, dumps, build output or generated files. The complete line and its inherited base were reviewed at 2f01e1b4: all changed files are text, and the body assets are generated locally from each player's own game rather than committed.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead). This line does not change third_party/; its inherited base only adds unmodified Monocypher 4.0.2 with its licence and provenance, and all 12 recorded part hashes match. That version is to be updated to 4.0.3 in the base, since the library's own download page lists 4.0.2 among versions with a possible timing leak in public-key operations.
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. The source and copied passages were reviewed at 2f01e1b4: the additions are GPL-3.0-or-later contributions or compatible material with preserved notices, and the native bindings contain interoperability facts rather than decompiled game bodies.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Flags noted, and the switch reference updated (docs/development.md, the README). Commit 15a68a5e, included in this head, adds all 25 missing named entries and completes HOS_COOP_ACCEPT=always, with the README Teleport warning.
  • Screenshots for a change that shows on screen.
  • MKWVK_API_VERSION bumped (gpu/vk/vk_api.h's MkwVkHost or MkwVkRenderer changed).
  • A game-free test added for the fix, failing on main and passing here. The export-path regression is fail-before on r8, its actual feature-bearing baseline; plain main has no export layer to exercise.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. At exact head 2f01e1b4, the native Linux executable/renderer build, link and run; the Windows executable/renderer cross-build and link with llvm-mingw, exit 0, from a build path containing a space and accented character. Nothing from this head has run on Windows; Windows runtime, changed-fixture execution and UTF-8 runtime-path behavior remain untested.
  • Docs updated.

New on this forge; not opened on GitHub.

## Why Friends should be able to see one another, agree on what their room shares, and try co-op without risking their usual saves. This is the whole experimental playtest line above https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/68, kept together so testers can use one matching build. **Work in progress.** The tested head is `2f01e1b4bdb6e1629c54a998f0403519fe4e5c74` (r12), Linux source `05329d46`. The pull request's branch is still named `coop-playtest-r9`; that name is historical. Build and report the commit, not the branch suffix. The base is unchanged from r10: pull request 68 at `4a9b96060ad71a3b48e05ac0269fc7e0e1a4a626`, on main `cf605c85bbaeaaa78176b933b3bc1a3366e13a78`. The 80 line commits and three r10 fixture/documentation repairs remain, followed by the three commits below. Main is still three commits later at `0caf06184b391a5259d3741bb006ef5826eb6417`: documentation, Windows lift workers and button prompts. This is not current main. The text-field parent remains https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/51. **What is measured on r12:** the whole game-free suite and all six short two-game Linux proofs, each passing at its first attempt with both complete logs checked for unexpected body removal or recreation. Linux builds and runs; the Windows executable and renderer cross-build and link. **Not run on r12:** Windows, two machines or a 30-minute session. Earlier LAN, soak and native Windows results retain their actual commits below, including the failed r10 continuity result and a separate open Windows body-disappearance report. This build cannot start a fight. ## What changed Pull request 68 already supplies the relay, sessions, signed progress, backups and the Co-op tab. Its work is a dependency, not new work to review twice. Above that pinned parent, the same pieces remain integrated in this order: | Piece | What a player gets; where to read | On by default? | |---|---|---| | S00: numbered signed rules | Room features have stable numbered permissions; unknown signed rules survive forwarding and a missing rule grants nothing. `host/coop_state.c`, `host/coop_proto.c`. | The rule format is used whenever this build joins; co-op itself stays off. Matching builds are required. | | S01: signed preview and consent | Read the maker's terms and identities before the second Accept press. Changed rules or members cancel an old press without replacing the joiner's local preferences. Co-op and PvP setup have different, explicit help. `host/coop_menu.c`, `gpu/vk/vk_menu.cpp`. | Mandatory when accepting; room mode defaults to Co-op, not a fight. | | S02: one owner for game changes | Leaving or losing permission invalidates already queued changes as well as future messages. Progress, presentation and temporary effects have separate permissions. `host/coop_gameplay.h`, `host/oot_coop.c`. | The checks are always used; they grant nothing by themselves. | | S03: child bodies | An accepted friend's child body can appear in the same place, with locally generated assets and guarded lifetime. `host/oot_coop_players.c`, `nx2/coop_players_assets.py`. | Off: both local body display and the signed age permission are needed. | | Child pose and prompt repair | The remote child uses the current animation frame and a full-body standing/walking binding rather than a fixed arms-out pose. Existing menu-prompt checks are adapted without weakening them. | Automatic when authorized animated bodies are enabled; no new opt-in. | | S07: temporary profile | Temporary save copies saved checkpoints, restarts into a working copy, then offers Return to normal saves. It never copies progress back. `host/coop_profile.c`, `host/nn_fs.c`, `gpu/vk/vk_menu.cpp`. | Off: a deliberate two-press action outside a room. | | Separate copy banner and refusal | The copy reminder no longer hides an actionable room refusal. Both fit at the tested menu scales. `gpu/vk/vk_menu.cpp`. | Automatic on a temporary copy. | | S08: signed checkpoint transfer | A selected checkpoint can be offered to one explicitly consenting recipient, verified and stored separately. It is not loaded or imported. The save picker is not included; this is available through the local control API. `host/coop_save.c`, `host/coop_save_runtime.c`, `host/coop_profile.c`. | Off: separate identity, owner and exact-checkpoint approvals are required. | | S13: native combat observations | Optional records distinguish a requested health change from a committed loss. Receiving a record cannot hurt another player. `host/oot_coop_hits.c`. | Off: signed observation permission and local tracing are separate switches. | | Complete hit trace | Independent bounded records for health changes and attack activations can establish a closed observation interval; missing critical records invalidate it. Optional details are counted separately. `host/oot_coop_hits.c`. | Only while tracing is enabled; tracing stays off. | | Position privacy | Positions go only to currently accepted recipient sessions; revocation also discards queued poses. This is not private encryption from other holders of the room secret. `host/coop.c`, `host/oot_coop_players.c`. | Automatic for authorized presentation; no extra disclosure is enabled. | | Adult bodies, sword/shield display and adult binding | Separate generated adult body/animation assets and cosmetic sword/shield state. Unsupported equipment is hidden rather than guessed. `host/oot_coop_players.c`, `nx2/coop_players_assets.py`. | Off: additional signed age/gear bits are needed. Generated adult animation and full gear behavior are not yet shown in game; the earlier Windows standing-model report is separate. | | Friend names, health, place and arrows | Names over visible friends; separately permitted health/place in the list and arrows towards same-map friends off screen. Unknown or stale values say unknown. `host/oot_coop_players.c`, `gpu/vk/vk_menu.cpp`. | Names are selected locally by default, but require accepted visible bodies; health/place/arrows remain off in the signed permissions. | | Prompt and damage-scaling integration | Existing input prompts and local Damage taken behavior keep their checks beside the expanded co-op interfaces. | No new permission or default; not remote damage. | | Go to friend | A two-press trip towards an accepted friend's current position, limited to Kokiri Forest and Hyrule Field in the player's existing age. It cannot pull another player and refuses on native temporary copies. `host/oot_coop_warp.c`. | Off: maker permission plus local confirmation. Live arrival safety is not yet proven. | | Room chat and notices | Accepted players can type short messages, mute a member locally and choose join/item notices. Typing is fenced from game input; no chat history is saved. `host/coop_chat.c`, `host/coop_chat_runtime.c`, `gpu/vk/vk_menu.cpp`. | Signed chat off; local notices default to Everything, subject to acceptance and actual shared events. | | Temporary sword-practice kit | Separately confirm a child sword/shield pair or an adult pair on a native temporary copy. Actual equipment readback, not an icon, decides readiness. `host/oot_coop_kit.c`. | Off: signed catalog/mask, copy presence and another two-press action are needed. No other item families. | | Arena readiness and separate duel scores | Diagnostic Ready explains missing native safety/contact/damage support. The game-free round/score logic is separate from real health and saves; no production contact binding starts it. `host/coop_duel.c`, `host/coop_duel_menu.c`. | Fighting is disabled. The diagnostic row is hidden unless tracing and PvP setup are selected. | | Seeing and talking on copies | Accept to see and talk grants presentation on the currently mounted copy, not progress, pools, downloads, inventory or travel. Leaving or changed terms revoke it. `host/oot_coop.c`, `host/coop_menu.c`. | Off until a fresh explicit acceptance; ordinary acceptance never carries across Copy or Return. | | Combined presentation and kit consent | A kit checks the current copy-only presentation acceptance and its own independently confirmed offer. | No automatic kit grant. | | Reliable retry and retirement counts | The existing `coop: stats:` line now names reliable retries and events given up. Deliberate cancellation contributes to given-up counts; they are not a packet-loss percentage. `host/coop.c`. | Counters follow the existing active-room statistics; no new switch. | | Cross-piece integration repairs | The combined menu, input, renderer interface and source-reading tests use the same contracts. Local display choices remain separate from signed rules. | No additional feature is enabled. | | Transfer-on-copy regression coverage | The actual room offer on a temporary copy is available to the separate download consent path. Tests retain the no-presence/no-progress boundary. `tests/test_coop_save_copy.py`. | No new default or permission. | | r9: native export-path fix | Linux checkpoint export converts the native mount's Windows separators before filesystem lookup, and rejects parent-directory components. The regression reaches the real mount/export boundary. `host/coop_profile.c`, `tests/test_coop_export_path.py`. | Automatic at the storage boundary; consent and Windows valid-path handling are unchanged. | No shared enemy/boss simulation, playable PvP, arbitrary travel, save-owner picker, progress return from a copy, shared clock, invite service or mod-loading interface is included. Models and animations are generated from each player's own game; none is distributed with the source or sent over the network. **Earlier r10 replay (`1ed75e61`):** at that head the co-op core was byte-identical to r9: protocol, signed consent, progress/presence/download separation, save profiles and transfers, body adapters, chat, travel, kits and trace queues have no new behavior. Shared files retain the newer base's input toggles, audio/Intel menu rows, sampled game state, filesystem handling and `logs/` / `cache/` layout. The observer hooks are composed with the base's full pre-clamp damage-policy argument; the actual game-code translation was regenerated and both products rebuilt, rather than relabelling an r9 binary. The three fixture/documentation commits that completed r10 were: - `7f4dcaf4`, `tests/test_menu_feature_rules.py`: provide the newer menu parser's Plus/Minus exclusions, keeping every rule-registry and saved-key assertion. - `44c6f99a`, `tests/test_menu_text_field.py`: combine the real pad and text-release callback declarations and retain the correct input-polling sections beside chat, without losing text, repeat, toggle or restart checks. - `1ed75e61`, `tests/test_oot_coop_hits.py`: use the base's three-argument damage-policy interface and add overkill checks for both native update paths, retaining the original request, scaled committed life and transmitted result checks. Those three fixture commits changed no production code. The two presence fixes below do; no permission, filesystem refusal or test assertion was removed to obtain the results. **Three commits after r10:** - `2be4e92335a722468631d19d7fac52e12f970d5e`, `fix(coop): preserve presence across unchanged roster heartbeats`: an identical periodic roster is no longer published as a changed room view. A change of view could briefly close otherwise valid presentation permission before its consumer caught up. Real member, session and rule changes still revoke old permission; no grace period or consent bypass was added. - `15a68a5ebaafc7638e6dfa7fb6e6de969c8a16e0`, `docs(coop): list the line's switches in the switch reference`: adds the 25 missing direct switch entries and completes automatic-acceptance documentation, with defaults, who sets them, and test/development scope. It changes only `docs/development.md` and the README's Teleport note. - `2f01e1b4bdb6e1629c54a998f0403519fe4e5c74`, `fix(coop): apply joined views after the native engine starts`: a joined room view stays pending until it has actually been applied to an existing native engine. The next ordinary session update can consume it without waiting for another roster publication. **Why both fixes are needed:** the r10 30-minute session at `1ed75e61` contained an unrequested hide/delete/recreate in one game despite the room remaining accepted. Investigation reproduced the unchanged-roster publication race in both r9 and r10; it is consistent with that interruption, although the original log did not record the exact cross-thread version pair. The intermediate head `15a68a5e`, containing only the first fix and the documentation, then failed its first two-game proof before signed-room readiness and was never pushed. It exposed a second fault: the initial view could be marked handled before the native engine existed, and the old repeated heartbeat had accidentally retried it. The second fix retains that view until applied, while keeping the unchanged-heartbeat repair. Two game-free regressions cover the distinct boundaries: `tests/test_coop_roster_presence.py` forces the publication/consumption race and preserves revocation for real changes; `tests/test_coop_join_publication.py` drives a real join, signed identity/charter exchange and explicit acceptance with engine creation and callback registration in different orders. Both pass with GCC and Clang on the corrected code. The join test compiles and fails on the intermediate head with both compilers. The first race test began with an existing engine and accepted room, so its success alone had not established startup joining. ## Flags and settings Defaults below are the unchanged co-op defaults of r12, not a recommendation to enable every feature. Signed defaults initialize a maker's draft; an absent permission is still off. New terms require fresh acceptance. All co-op remains opt-in, and all participants need the same compatible build. The Untested on column describes this r12 head; the separately labelled r9/r10 LAN, soak and Windows evidence below does not turn those into r12 results. The inherited connection settings are `coop` / `HOS_COOP` (off), `coop_host` / `HOS_COOP_HOST` (off), `coop_server` / `HOS_COOP_SERVER` (unset; relay port 24680), `coop_port` / `HOS_COOP_PORT` (0, a free client port), and `coop_name` / `HOS_COOP_NAME` (a generated player name). Make/Join enables the session for that run. Only the maker enables hosting; Join never hosts. No router or firewall is changed automatically. The inherited ordinary-save sharing defaults are Shared for `coop_progress`, `coop_items`, `coop_songs` and `coop_quest`; Pool for `coop_keys`; Own for `coop_rupees` and `coop_ammo`. They apply only after ordinary consent and a completed backup. A native temporary copy never shares this progress, even if its room displays Shared. For a first playtest, use disposable save roots and Own categories. The seven category environment variables below are inputs to the maker's signed room rules, not separate permission to write a peer's game. With Shared progress, Items/Songs/Quest are normalized to Shared; with Own progress, Keys is normalized to Own. An unset or empty category override uses the saved key, then the stated fallback. Changing the accepted rules needs fresh consent. Test-only switches are not recommended player settings, and the two debug Teleport overrides already exist in the base; Go to friend merely adds a refusal when they are set. These rows document source behavior, not new runtime coverage. | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `HOS_COOP_PROGRESS`, `HOS_COOP_ITEMS`, `HOS_COOP_SONGS`, `HOS_COOP_QUEST` | Signed room-rule inputs: choose `shared` or `own` for progress, items, songs and quest items respectively; a nonempty environment value overrides the corresponding `coop_*` key in the maker's draft | Unset: use the saved key; fallback `shared` for each | Prefer Own on disposable first-playtest saves; review the signed result | Deliberately accepted ordinary-save sharing | No progress on native temporary copies or in PvP setup; no grant before consent/backup | Each environment-override combination in live r12 play; Windows gameplay | | `HOS_COOP_KEYS` | Signed room-rule input: `pool` or `own` for dungeon small keys, over `coop_keys` | Unset: use the saved key; fallback `pool`, normalized to `own` when progress is Own | Use Own for the first playtest | Agreed small-key pooling on ordinary saves | No pooling on native copies; no independent keys pool with Own progress | Live r12 key-pool/override cases; Windows gameplay | | `HOS_COOP_RUPEES`, `HOS_COOP_AMMO` | Signed room-rule inputs: `pool` or `own` for rupees and ammunition respectively, over `coop_rupees` and `coop_ammo` | Unset: use the saved keys; fallback `own` for both | Keep Own unless everyone explicitly wants the pools | Agreed resource sharing on ordinary saves | No pools on native temporary copies; no permission from joining alone | Live r12 resource-pool/override cases; Windows gameplay | | `HOS_COOP_AUTO` | Test/automation switch: `create` or `join` requests a room at startup while co-op is enabled, then recreates or waits for that room after relay loss; it is not acceptance | Unset, empty or `off`: no automatic request | Leave unset for human playtests; use Make/Join | Repeatable connection tests | Does not switch co-op on by itself or grant consent; Copy/Return suppresses inherited automatic joining | Live r12 reconnect/loss cases; Windows gameplay | | `HOS_COOP_ROOM` | Test/automation switch: private room secret for automatic create/join; also selects the secret used by the menu's Make action | Unset: Make/create generates a fresh secret; automatic join requires a valid supplied secret | Leave unset for human playtests; never publish its value | Connecting controlled test peers to a known room | Does not enable co-op, accept anyone or bypass an already-open-room refusal | Live r12 known-secret edge cases; Windows gameplay | | `HOS_COOP_ACCEPT=always` | Test-only switch: automatically accepts a valid ordinary room offer instead of asking for the local menu confirmation; signature/identity and backup checks still apply | Unset or any value other than `always`: manual acceptance | Leave unset; never use on a normal playing save | Deliberate automatic-consent tests on disposable saves | Does not grant copy-only see-and-talk, downloads or a kit; Copy/Return clears inherited automatic acceptance | Full automatic-consent matrix on r12; Windows gameplay | | `HOS_COOP_DROP` | Test-only switch: randomly discards the requested percentage of incoming co-op datagrams, capped at 100 | 0 percent | Keep 0 unless deliberately testing loss | Receiver-side loss simulation | Not a real-network loss measurement or a delivery guarantee | Live r12 induced-loss scenarios; Windows gameplay | | `HOS_COOP_DEMO` | Test-only switch: drives the Co-op menu with a made-up room; its menu actions start no network session | Off | Leave off for every gameplay or network proof | Menu screenshots and offline menu checks | Does not establish connection, consent or gameplay with a real peer | Windows execution and physical input for the demo | | `HOS_COOP_PROBE` | Development diagnostic: after a normal save reaches play, logs the flag table and compares its reads with the game's getter; by itself it writes nothing and needs no room | Off | Leave off except for a requested diagnostic; keep generated flag details private | Investigating the local progress reader | Not progress sharing, native copy support or evidence of a natural pickup | Live r12 probe-only use; Windows execution | | `HOS_COOP_TEST_SET` | Test-only switch: schedules local Boolean/integer flag writes through the game's setters as `hash=value@frame`, up to 8 entries; these writes can become saved progress | Unset: no scheduled writes | Leave unset; use only disposable test roots, never a normal playing save | Controlled local grant tests while the co-op/probe update is active | Not natural acquisition or consent; the native temporary-copy path never runs this aid | Live r12 scheduled-grant scenarios; Windows execution | | `HOS_OOT_TELEPORT_EVENT` | Existing development override for the local debug Teleport event name; this line adds a Go to friend refusal while a nonempty override is set | Unset/empty: the built-in map-change event | Leave unset; not a co-op travel workaround | Focused debug-travel investigation | Any nonempty value disables Go to friend rather than changing its event; no remote event selection | Live r12 override behavior and safe travel; Windows gameplay | | `HOS_OOT_TELEPORT_STARTER` | Existing development override for the local debug Teleport request: exactly `0` omits Link as its starter; other values retain him. Go to friend refuses any nonempty override | Unset: Link is the starter | Leave unset; not a co-op travel workaround | Focused debug-travel investigation | Even `0` or `1` makes Go to friend refuse; no network-selected starter or new permission | Live r12 override behavior and safe travel; Windows gameplay | | `coop_room_mode` / Room rules: Mode | 0 Co-op; 1 PvP setup; -1 refuses acceptance | 0 | Keep Co-op for playtests | Matching consenting peers | Does not enable fighting | Windows and cross-PC | | Accept the room / Accept to see and talk | Two-press review of the current signed offer; the latter is copy-only presentation | Not accepted | Always review identities and terms | Both save domains | No automatic consent after restart or changed terms | Physical input and copy-presence runtime | | `coop_presence_permissions` / Friend information | Add bits: 1 child, 2 adult, 4 health, 8 arrows, 16 gear, 64 place | 0 | Start with 1; add only disclosures both players want | Same-map presence; optional friend information | Does not grant inventory or contact | Adult/gear/HUD runtime; Windows and cross-PC | | `coop_players` / `HOS_COOP_PLAYERS` | Local body display and authorized pose sending | 0 | Set 1 on each test game | Authorized child bodies | No effect without consent, age permission or a matching build | Windows and cross-PC | | `HOS_COOP_PLAYERS_ANIM` | Local animation override | Automatic with generated assets | Leave unset | Standing/walking bodies | Does not supply missing assets | Adult/full pose parity | | `HOS_COOP_PLAYERS_ACTOR`, `HOS_COOP_PLAYERS_TEST` | Local diagnostic actor/feed overrides | Unset | Leave unset | Focused diagnostics | Not real-peer evidence or consent | This playtest | | `coop_names` / Friend names | Signed permission for names over visible accepted bodies | 1 | Keep; locally hide if unwanted | Accepted same-map friends | No body or position grant | Live name placement; Windows | | `coop_labels` / Friend labels | 0 Off, 1 Names, 2 Names and arrows | 1 | Names first; arrows only with signed bit 8 | Local readability/navigation | Hiding labels does not revoke disclosure | Live arrows and multi-player readability | | Temporary save / Return to normal saves | Copy/restart and return without copying progress back | Inactive | Use on a disposable root for this WIP | Isolated play | Does not merge or import progress | Windows filesystem/restart behavior | | `HOS_COOP_PROFILE_TICKET` | Internal one-use restart state | Absent on an ordinary start | Never set or copy by hand | Native Copy/Return | Not a user-selected save or transferable permission | Windows execution | | Local `coop_save` actions: status, identity, offer, accept, cancel | Separate signed checkpoint download consent and storage | No transfer | Not part of the short menu-only playtest | Explicit owner and recipient | No picker, import, load or copy-back | Same-room term-change cancellation live; Windows | | `coop_warp` / Go to friend | Signed permission and separate two-press local travel | 0 | Leave off for the first copy-only session | Normal disposable saves, forest/field | Native temporary copies and other maps | Actual arrival/hazards and Windows | | `coop_social_permissions` / Room chat | Bit 0 permits accepted-member text | 0 | Enable only for an agreed chat trial | Room communication | No progress or identity authority | Two-game chat rendering/input | | Chat / `key_chat` | Open text entry; Enter sends, Esc cancels | T | Keep unless it conflicts with a binding | Keyboard input; controller paste | No new controller character picker | Physical typing/paste/controller input | | Mute chat | Mute/unmute an accepted member locally | Nobody muted | Use as needed | Local feed | Does not remove a member or change others' screens | Live mute/unmute | | `coop_notices` / Notices | 0 Off, 1 joins/leaves, 2 all including shared-item notices | 2 | Prefer 1 for a copy-only test | Accepted room transitions | An item notice is not proof of its original finder | Live notices and catch-up | | `coop_hit_events` / Native hit observations | Share committed native loss observations | 0 | Leave off | Diagnostic observation | Never applies remote damage | Networked loss observations | | `HOS_COOP_HITS_TRACE` | Local attack/health trace and diagnostic Ready row | 0 | Leave off except for an observer test | Closed native observation intervals | No collision or sword-owner proof | Windows; long-session trace bounds | | `HOS_COOP_ARENA_FILE` | Local diagnostic arena description | Unset | Leave unset | Private arena validation | Cannot enable a fight | Native safe-area certification | | `coop_loadout_version`, `coop_loadout_preset` | Practice catalog and display label | Both 0 | Leave 0 for the first session; catalog 1 is sword practice | Separately accepted temporary kit | No complete item palette | Actual kit use | | `coop_loadout_age`, `coop_loadout_bits_0` | Explicit age and kit mask | Both 0; no kit | Catalog 1 only: age 0/mask 3 or age 1/mask 12 | Child or adult sword/shield pair | No age change; wrong age refuses | Both live kit families | | `coop_loadout_bits_1` through `_7` | Remaining item masks | All 0 | Keep 0; unsupported bits refuse | Reserved terms only | Other item families | Unsupported by this kit | | `coop_loadout_magic`, `coop_loadout_seeds`, `coop_loadout_arrows`, `coop_loadout_bombs`, `coop_loadout_bombchus`, `coop_loadout_nuts`, `coop_loadout_sticks`, `coop_loadout_magic_start` | Signed magic/ammunition terms | All 0 | Keep 0 | Reserved terms only | No magic/ammunition kit | Unsupported by this kit | | `coop_loadout_bottles`, `coop_loadout_bottle_0` through `_4` | Signed bottle terms | All 0 | Keep 0 | Reserved terms only | No bottle kit | Unsupported by this kit | | Sword-practice kit | Independent two-press give/equip action on an accepted copy | Inactive | Separate test only | A matching live kit offer | Normal saves; presence alone | Usable sword/shield proof | | `coop_combat_mode`, `coop_damage_model` | Duel permission and receiver-native damage model | 0 and 1 | Keep combat off | Diagnostic terms | Neither installs native contact/damage support | Playable combat | | `coop_pvp_damage`, `coop_pvp_items`, `coop_pvp_grace`, `coop_pvp_shield` | Damage percent, allowed attack mask, grace in ms, shield mode | 100, 1, 700, 0 | Leave defaults; fighting remains disabled | Game-free duel contract | No remote damage fallback | Native combat | | `coop_duel_hearts`, `coop_duel_rounds`, `coop_duel_healing`, `coop_duel_revive` | Separate hearts, best-of rounds, healing and revival terms | 10, 3, 0, 0 | Leave defaults | Game-free round accounting | Does not change native health or death | Live rounds | | `coop_arena`, `coop_arena_hazards`, `coop_arena_time` | Arena and environment terms | All 0 | Leave unset/off | Diagnostic readiness | No surveyed arena is shipped | Native arena safety | | Ready for duel | Diagnostic two-press preview; reports unavailable | Hidden without tracing/PvP setup | Do not expect a fight | Explaining missing capabilities | Cannot start a round | Playable PvP | | `HOS_COOP_TRACE` | Existing packet/progress diagnostics, now also body details | 0 | Off normally; opt in for a requested log | Finding connection/presentation faults | Not a performance measurement | Cross-PC trace collection | | `RX_VK_MENU_SCRIPT` with `CHAT` | Diagnostic entry into the same chat text field | Unset | Leave unset for human play | Reproducible input checks | Cannot bypass room consent | Physical input is separate | No new public build flag or command-line launch flag is required. The existing `--user` selects a separate test save root. Do not put `HOS_COOP*` switches in the shareable `longshot_env.txt`: they are deliberately ignored there. The detailed reference is `docs/coop.md`; commit `15a68a5e`, included here, now supplies the missing named switch entries in `docs/development.md` and the README Teleport warning. Internal callback/option numbers are not user settings. **Newer-base startup differences, not new co-op permissions:** ordinary file logging is now `logs/longshot_log.txt`, with rotated logs in the same directory. Runtime caches are in `cache/`, including `longshot_vk_pipelines*` and generated `cache/prompts/` files. Terminal or redirected launches keep their actual output stream instead of necessarily creating that log file. On Windows Intel, keep `intel_shader_compiles=0` (one compiler thread at a time) and leave `RX_VK_NO_INTEL_SERIAL` unset; do not disable that safety default to start a test. The inherited hold-to-toggle option stays off and Mono output stays Auto by default. The updated tester guide is https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17211. ## Measurements These are functional results, not a performance comparison. The base below is the pinned ancestry, not current main. No main-versus-r12 timing pair was run; older off-cost or frame-duration observations do not measure this corrected head. main: cf605c85bbaeaaa78176b933b3bc1a3366e13a78 (2026-11-14) this PR: coop-playtest-r9 @ 2f01e1b4bdb6e1629c54a998f0403519fe4e5c74 Hardware: NVIDIA RTX 4090, driver 615.78.08; Core i9-13900K, 96 GB RAM; Linux 7.2.15-1-omarchy x86-64. Scene: child play in Kokiri Forest and Link's house; requested output and captures 1280x720, frame generation off, cap 60; windowless. The renderer reports an initial 1920x1080 offscreen target. No swapchain/display claim. Method: six separately started two-game sessions on the exact r12 build, with a local relay and disposable saves; every proof passed its first attempt. Each requires the original consent/save/pose/observer checks, inspected new captures, both normal exits and a scan of both complete stopped-game logs, including `coop: puppet:` and `coop: players:`, for unexpected body hiding, deletion or recreation. Other compilation was kept out; this is not a performance experiment. How to reproduce: build commit `2f01e1b4bdb6e1629c54a998f0403519fe4e5c74` from each player's own supported game and use separate save roots and matching peers. Check signed terms/stale acceptance, body consent and Leave, reciprocal child steps, native Copy/autosave/Return, an explicitly approved checkpoint transfer with a genuinely partial cancellation, and five visible sword swings with closed health/activation records. Account for every body creation and removal against the requested action, not only pictures at the endpoints. The short human guide uses menu consent, not the control-API transfer procedure. | Result | main @ cf605c85b | this PR @ 2f01e1b4 | |---|---|---| | Whole game-free suite | Recorded baseline: 540 passed, 3 failed, 16 skipped; not rerun for this round | 580 passed, 3 failed, 16 skipped; exit 1 | | Six two-game proofs | Not run on main; these features are not all there | All six pass at the first attempt, including complete-log body-lifetime scans | No performance/off-cost result exists for r12. The suite's exact final line: ```text 580 passed, 3 failed, 16 skipped in 187 s (linux, 6 at once) ``` It ran once with strict prerequisites. All 54 line-added-or-modified drivers pass, including both new presence/join regressions. The three failures are still the pinned main's: - `tests/test_framegen_resources.py`: its extracted fixture lacks the Intel driver-guard type and shader/compute declarations. - `tests/test_module_ids.py`: its source-extraction marker no longer matches the function it seeks. - `tests/test_pad_layout.py`, `PadLayoutTests.test_presets`: its extracted fixture lacks `HoldToggle`. That is one additional pass over intermediate `15a68a5e`, two over r10 `1ed75e61`, with no new failure or skip; it is not an all-green suite. The translation failure from the older r9 base no longer occurs. The separately recorded GCC-only `test_menu_keys.py` declaration failure remains earlier focused-check evidence; the full suite uses Clang for that driver and passes it. | Proof on r12 | Held time, including shutdown | Result and complete-log boundary | |---|---:|---| | S08: signed checkpoint transfer | 321.8 s | Pass: the joiner now identifies the maker, consumes the signed charter and receives a nonzero offer; fresh download-only consent, immutable owner export and exact recipient approval complete. A second transfer is cancelled after 8,192 of 160,019 bytes with no usable partial or mount change. Neither game creates a remote body. | | S02: body consent and Leave | 373.9 s | Pass: no body before consent, then exactly one remote body created in each game. Both retire only after actual Leave and do not return; no intervening hide/delete/recreation. | | S07: native temporary save | 370.1 s | Pass: both Copies and Returns, A's natural autosave and retained changed working checkpoint, both normal saves and immutable originals unchanged after exit. No remote-body lifecycle event in this out-of-room proof. | | S01: signed terms and acceptance | 303.2 s | Pass: matching signed terms, tab/close cancellation, stale disarm and fresh PvP-setup acceptance, with no native progress applied and no body created. | | S03: child standing and walking | 125.0 s | Pass: full arms-down bodies and reciprocal visible steps, then standing again. Exactly one remote body per game, removed only when signed presence is switched off; no intervening recreation. | | S13: native health observer | 127.7 s | Pass: five visible swings beside a still B, complete critical records and zero committed player loss through the instrumented path. Exactly one initial body per game, no recreation, and only explained shutdown retirement after the final records. Not contact or PvP. | All 12 games exited 0. Both complete logs for every proof pass the lifecycle review; a generic “map change or a load” reason is not accepted as an explanation without the corresponding requested action. All six runs have complete kernel coverage without reported driver faults. Six present intervals above one second occur only in S08's initial normal generations: four during entry, two near native Copy/restart; neither temporary generation has one. Their precise cause was not isolated. These short successes do not establish 30-minute continuity. **Earlier evidence keeps its head:** the r10 short-proof round at `1ed75e61` retained a failed first S03 picture-framing attempt, then passed with the same input/camera route while other compilation was excluded. That history is still in https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17525; it is not an r12 failure or an r12 speed result. The intermediate `15a68a5e` failure at signed joining and the failed r10 long session remain failures, not renamed passes. **Earlier export-path repair, still present:** the r8 failure came from Windows separators reaching Linux filesystem calls. The r9 fix normalizes the profile/export boundary and rejects parent components; its earlier 92-outcome compiler matrix and 5 Windows-target syntax units remain r9 evidence. The exact r12 whole suite passes the regression and the new S08 run proves actual transfer without weakening owner selection, signed descriptions, recipient consent or path refusals. R8/r10 focused matrices and syntax checks are not described as rerun on r12. ## Testing **Tested:** the exact r12 Linux executable and renderer build and run, source `05329d46`. All seven locally generated child/adult actor, animation and shared-table files are present and agree between the two builds. All six short proofs above pass, including post-shutdown scans of both complete logs; child standing/walking, native Copy/autosave/Return, signed transfer/partial cancellation and the bounded complete health-observer interval have new r12 evidence. The whole r12 suite has 580 passes and the three named inherited failures. Both race and join regressions pass with GCC and Clang; the join regression fails on intermediate `15a68a5e` before the second fix. The switch-reference commit `15a68a5e` passed its seven selected documentation/source-reader checks before and after, not a separate r12 runtime test. **Built for Windows at this head, not run:** the exact r12 Windows x86-64 executable and Vulkan renderer cross-built and linked on Linux with llvm-mingw 20261104 / Clang 23.1.5, release/PGO settings, exit 0, with no compiler/linker failure or source workaround. Validated unchanged generated game code from r10 was reused; the full products contain the corrected host code and `v3.0.0-rc.3-179-g2f01e1b4b (source 05329d46)`. The production warning-suppression flags remain, so this is not warning-clean evidence. Static dependency checks and embedded version checks are not Windows execution, visible-menu proof or Windows UTF-8 runtime-path coverage. No r12 Windows process has run in this evidence. **Earlier heads only:** - **LAN, `0ec9d0b0`:** Windows hosted through the game and Linux joined; signed acceptance, reciprocal child bodies/walking, chat and Leave worked, with normal exits and unchanged disposable saves. Linux hosting in the reverse direction was blocked by its firewall, not a successful reverse-direction game test. This was short play on normal disposable saves, not native temporary copies, shared progress, transfer or kits: https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17499. - **First 30-minute session, `0ec9d0b0`:** one Linux PC, game-hosted loopback traffic. Connection, observed bodies/names/chat and unchanged normal saves held, but the presence readings missed the predeclared sampling cadence. It remains qualified, not an unqualified pass: https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17281. - **Later 30-minute session, `1ed75e61`:** the completed restart met its sampling schedule but failed body continuity: one A-side hide/delete/recreate while the room remained accepted. The first attempt's earlier undetermined stop and the openly revised swap criterion remain in its record. Normal exits and unchanged saves do not turn it into a pass, and neither new fix retroactively repairs that result: https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17694. - **Native Windows report, `1ed75e61`:** Windows 11 Pro 26300, Core i9-12900K, 64 GB RAM, RTX 3080 10 GB / NVIDIA 621.14. The tester reports a native build and suite of **556 passed, 6 failed, 35 skipped**, against main `0caf0618` with **521 passed, 6 failed, 34 skipped**. The same six failures are `test_pad_layout.py`, `test_release.py` (OpenSSL path), `test_tested_configurations.py` (line endings), `test_oot_state_tap.py` (local control connection), `test_module_ids.py` and `test_framegen_resources.py`; this is not an all-green Windows suite. Two windowed games on that one PC joined a separate loopback relay with automatic acceptance, made backups and left both saves unchanged. Adult bodies were reported as standing-model fallbacks because generated animation assets were missing, so this is not adult walking-animation proof. The repeated body disappearance remains open below. No native Copy/Return, real NTFS junction-refusal case, game-hosted relay, firewall prompt or two-PC session was tested in that report: https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17726. - **Earlier limited Windows title start, `1ed75e61`:** the cross-built r10 folder reached the title and quit on Intel Arc / driver 32.0.101.9175 with co-op off, on-demand shaders, pipeline-cache load/save disabled and native float atomic add forced. Its nonfatal `programs (10 failed)` counter stays recorded. It is not an r12 start or completed preparation. **Not tested:** on r12, any Windows runtime or execution of its changed C/C++ fixtures, a two-machine session, game-hosted LAN/internet/NAT gameplay, a 30-minute session or longer play, and windowed presentation. A new r12 long session is planned separately; no result is claimed here. Also not settled: AMD/Steam Deck, physical input, more than two live games, active-room performance/off-cost or continuous frame-fault coverage. The six r12 proofs use a separate local relay. Generated adult animation/full gear, live arrows/health/place, safe Go to friend arrival, copy-only see-and-talk presence, full chat focus/mute/notices behavior and usable practice kits need their own coverage. S08 tests old-generation refusal, not same-room signed-term changes during a live transfer. Downloads are stored, not loaded or imported. The bounded native health trace does not prove a hit, shield block, every health writer or playable PvP. Broader ordinary progress/chest/boss behavior is not re-proven by these six cases. **Known open problem, found by the Windows tester at `1ed75e61`:** with two games sharing one GPU over its video-memory allowance, one game fell to 1 to 8 frames a second, and at each such collapse it hid and later re-made the other player's body while the peer logged `in no map`: seven times in about two and a half minutes, none in the stretches at 37 to 52 frames a second (https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17764). This is the play check working as written: a game whose play counter has not ticked for 2.5 seconds counts as not being played, removes the bodies it shows, and asks for them again only 2 seconds after it recovers. It is too quick for a real hitch (a starved GPU, a long shader compile). The two fixes in this head do not change that rule; what to change is still to be decided. **Needs other testers:** - Windows with a Linux partner on two PCs: pin this r12 commit, use the game's hosting and separate save copies, then Make/Join/review/Accept, move, leave and return. Expect no body before consent and no unexplained removal while accepted; test both hosting directions with only the intended network permissions (T-ADK4; the base's request is T-SR3D). - Windows: rerun the changed fixtures on r12, particularly both new presence/join regressions, and test native Copy/Return, export, locks, real junction refusal and UTF-8 runtime paths. Copy/Return needs a real Windows window. Earlier Windows results remain at `1ed75e61` (T-QF6F). - Windows body continuity: with each game on its own GPU, or one game per PC, check that the other player's body stays through ordinary play; and note any hitch of more than two seconds, because the play check removes bodies after 2.5 seconds without a tick (the open problem under Not tested) (part of T-KQUM). - Other GPUs and physical input: generated child/adult presentation, labels/arrows, chat focus and copy/Return with logs and pictures (T-KQUM). ## Screenshots These eight pictures were taken on **`1ed75e61` (r10)** and keep their original attachment links and captions. Freshly inspected r12 captures showed the same illustrated states in all six proofs, as recorded by the new proof review; room fingerprints, idle poses and foliage phases naturally differ. They remain r10 illustrations, not relabelled r12 captures. R12's own signed replies, file/mount checks and complete lifecycle logs establish the new results; stills alone do not prove transfer, save isolation or uninterrupted body lifetime. ![Both games show matching signed Co-op terms and read-only help before acceptance.](https://git.magicbean.dev/attachments/173b89ac-95ee-49ab-ac6b-62b2467665d1) ![The joiner's first press is armed, then changed room rules cancel it.](https://git.magicbean.dev/attachments/195d41ed-8161-43a1-a820-e8fe4327b5cd) ![The new PvP setup is read and accepted afresh; the room says no progress is shared and no fight starts.](https://git.magicbean.dev/attachments/1c593cbf-5489-4c25-a9e4-bd65270f5271) ![A left, B right: local-only before consent, both complete bodies after acceptance, local-only again after actual Leave.](https://git.magicbean.dev/attachments/f44b15f7-f734-4a3b-96ef-b4e623134a3f) ![Successful r10 retry: each local child steps and the other game's received body visibly steps too, with both full bodies in view.](https://git.magicbean.dev/attachments/4cd9f47f-0c5a-4040-a43a-542f3a6203f1) ![Native temporary copies, A's opened chest and Saving indicator, then both titles after Return; mount/file checks establish unchanged normal saves.](https://git.magicbean.dev/attachments/e25d66f2-7d3a-4c29-bb95-562b26994d39) ![Download-only consent, verified storage and partial cancellation with only local bodies; signed replies and independent hashes establish the transfer result.](https://git.magicbean.dev/attachments/b5eb169d-b60e-4da0-acb2-1a0a3aeeea53) ![Both views before, during the fifth swing and after all five, with full pictured hearts; complete native records establish only the bounded no-loss result.](https://git.magicbean.dev/attachments/c181965a-8f28-468b-bcb8-bffd40809615) ## Risk and rollback This is a large change across save handling, permissions, networking, input, generated presentation and the host/renderer interface. A permission or path mistake could affect a save; incorrect body lifetime could leave a wrong actor; stale input could turn a chat key into a game action. Network loss or the host leaving can stop the room. An invited holder of the secret can lie about cosmetic poses or chat; this is not an anti-cheat system. A verified checkpoint hash proves its bytes, not that an untrusted save is safe for the game's parser. Use a separate `--user` root from the first launch, containing only a copy of your own saves, and keep the normal installation closed. Native Temporary save adds another isolated working copy. Return switches back to that test root without importing progress. An ordinary restart after a crash selects the normal root for that launch, so never omit `--user` when resuming this test. Copies stay for recovery and are not automatically pruned. Leave the room or set `coop=0` / `HOS_COOP=0` to stop further sharing; `coop_host=0` / `HOS_COOP_HOST=0` disables hosting and `coop_players=0` / `HOS_COOP_PLAYERS=0` disables local bodies. Revoke the signed presence bits to withdraw disclosure; merely hiding labels is not revocation. Turning co-op off or reverting source cannot undo progress already merged into an ordinary save. Do not copy a test save back into the usual save folder. To stop using the line, keep its test folder separate and return to the unchanged normal installation. Known wording limits remain: the human-readable signed-rule log is abbreviated; pending PvP log prose can mention progress even though the apply gate stays closed; a body-retirement reason can call Leave a map change/load; the copy reminder can be shortened at 1280x720. Read the actual signed Rules page and acceptance state, not those shortened diagnostic sentences, when deciding what to accept. The other player's body being hidden and re-made when one game stalls for more than 2.5 seconds, found on Windows at `1ed75e61`, is a known open problem of the play check itself. These roster and start-up fixes do not repair it, and the short Linux passes did not exercise it. ## Reviewer notes Start with the consent and separate save-domain boundaries, then native Copy/export and body lifetime. The later presentation, travel, chat and practice pieces deliberately do not reuse download consent as permission to affect a game. The native damage/contact connection is absent rather than replaced by guessed remote damage. **Review question:** how would you like a line of this size cut for review now that the line is on the current pull request 68 head: signed rules/consent and ownership first, then bodies, save isolation/transfer, and the optional social/travel/practice pieces, or another boundary? This is one WIP tracking pull request, not a request to merge every piece at once. The existing core-versus-module discussion remains at https://git.magicbean.dev/MagicBeanDev/Longshot/issues/42; no module interface is built here. The corrected r12 head has passed the new six-proof round. Next are its own continuous 30-minute session and two-PC/Windows runtime checks, neither claimed here. The r9 qualified soak, failed r10 soak, r9 LAN session and native r10 Windows report retain their original commits and outcomes. Main remains three commits beyond the pinned base; the current-main checkbox and review-splitting question stay open. ## Issues See https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/68 for the co-op base and earlier proof discussion. See https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/51 for its shared text-field dependency. See https://git.magicbean.dev/MagicBeanDev/Longshot/issues/42 for the unresolved core/module direction. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [ ] Rebased on current `main`. Replayed onto pull request 68 at `4a9b9606` / main `cf605c85b`, but main has since advanced three commits to `0caf06184`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). The exact r12 run has 580 passes and the three named failures of its pinned main base, not an all-green result. - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). The three requests are open: T-ADK4, T-QF6F, T-KQUM. - [ ] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. No timing/off-cost comparison exists for the full line. - [x] No game files, dumps, build output or generated files. The complete line and its inherited base were reviewed at `2f01e1b4`: all changed files are text, and the body assets are generated locally from each player's own game rather than committed. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). This line does not change `third_party/`; its inherited base only adds unmodified Monocypher 4.0.2 with its licence and provenance, and all 12 recorded part hashes match. That version is to be updated to 4.0.3 in the base, since the library's own download page lists 4.0.2 among versions with a possible timing leak in public-key operations. - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. The source and copied passages were reviewed at `2f01e1b4`: the additions are GPL-3.0-or-later contributions or compatible material with preserved notices, and the native bindings contain interoperability facts rather than decompiled game bodies. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Flags noted, and the switch reference updated (docs/development.md, the README). Commit `15a68a5e`, included in this head, adds all 25 missing named entries and completes `HOS_COOP_ACCEPT=always`, with the README Teleport warning. - [x] Screenshots for a change that shows on screen. - [x] `MKWVK_API_VERSION` bumped (`gpu/vk/vk_api.h`'s `MkwVkHost` or `MkwVkRenderer` changed). - [x] A game-free test added for the fix, failing on `main` and passing here. The export-path regression is fail-before on r8, its actual feature-bearing baseline; plain main has no export layer to exercise. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. At exact head `2f01e1b4`, the native Linux executable/renderer build, link and run; the Windows executable/renderer cross-build and link with llvm-mingw, exit 0, from a build path containing a space and accented character. Nothing from this head has run on Windows; Windows runtime, changed-fixture execution and UTF-8 runtime-path behavior remain untested. - [x] Docs updated. --- New on this forge; not opened on GitHub.
mooki added 111 commits 2026-11-14 04:15:44 +00:00
The Insert menu could not take typed text: a setting held as text, such as an
output size of the player's own, could only be set by editing
longshot_options.ini.

A text field is a row whose value is typed. Characters come from the window
(WM_CHAR on Windows, SDL's text input on Linux, turned on only while a field
has the keyboard) through a new MkwVkHost member, take_text, appended last
(MKWVK_API_VERSION 20, from 18; 19 is claimed by an open pull request). Left,
Right, Home, End, Backspace and Delete edit,
Ctrl+V and Shift+Insert paste the clipboard's first line, Enter sets the
text, Esc puts it back; a controller's confirm and back are Enter and Esc,
its D-pad moves the caret, a free face button pastes. A field holds printable
ASCII, as the menu's glyphs are baked; other characters are left out. While a
field has the keyboard no key does anything else, and the game gets none, as
for the whole menu. RX_VK_MENU_SCRIPT gains TYPE:<text> and the editing keys.

The first field is Custom output size on the Graphics tab: the Output
resolution of the player's own (output_res=), typed and checked by the same
rules, with the reason shown when a size is refused. As the row itself, it
leaves the Quality preset alone, and a size typed is the player's own from
then on, also in a run whose HOS_OUTPUT_RES holds the row (env_release).

Main's prompts_live (version 17) stays before take_text in both the interface
and the host table. tests/test_menu_text_field.py compiles the editor and the
host's queue cut from the source, and checks the wiring in the source: the
interface's take_text in its place in the host's table, WM_CHAR and SDL's text
input, the menu telling the host whether a field has the keyboard, TYPE.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Windows clang with Microsoft headers rejected getenv from hos_wide_size.h and sscanf from the extracted output parser as deprecated under -Werror. Put _CRT_SECURE_NO_WARNINGS on the first line of both generated C and C++ sources, matching the existing capture fixture convention. The screensaver fixture also needs the guard: its injected window procedure calls getenv twice. Runtime code, warning flags and every behavioral check stay unchanged.

Linux g++ and clang++ execute all three text-field checks; GCC and Clang pass all 18 mocked-window-procedure cases. Native LLVM-MinGW 23.1.5 Windows-target syntax with -Wall -Wextra -Werror -Wdeprecated-declarations and Microsoft-style getenv/sscanf annotations fails before and passes after. The three generated co-op tab fixtures pass that audit unchanged; restart-slot checks generate no C or C++. This is not a Windows runtime test. Refs #2594.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Main took version 20 for screen_size and version 21 for nvn_program_created while this rebase was being prepared. Append take_text after screen_size at version 22 and keep its fixture version check in step. Preserve both merged callbacks and their history comments. Reuse the SDL2 SDL_free lookup now supplied by the merged controller diagnostics instead of loading the same callback twice.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Record the two windowless runs built at 4914fdec on main 8cf9a524. The native build reused a completed lift only after both actual source-tree lift keys matched the recorded donor key. Keep the tested commit named explicitly: the later clean rebase onto main 15d4ae8c is checked game-free, not represented as another game run.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
fix(menu): complete custom output size controls and translations
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
891631dfba
Keep monitor-following for typed 16:9 sizes. Allow pad-only character entry and removal, translate the field and its prompts, ignore repeated paste shortcuts, fold full-width size input and hold the list still during editing. Keep the host API append check extensible without weakening positional wiring.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Co-op needs a way for the games of several players to find each other and talk without anyone on the path, the relay
included, reading or forging what they send. This is the game-free first part: the relay and the wire format both
ends speak. The game does not use it yet.

- scripts/coop_relay.py: a UDP relay, standard library only. Rooms of up to 8 players named by a locator derived from
  a secret the relay never sees; a cookie challenge before it answers anything larger than it got; rejoin tokens;
  limits per member, per network, per room and in all.
- host/coop_proto.c/.h: the same format in C: room secrets (18 characters, 85 bits), keys derived with keyed BLAKE2b,
  datagram tags (keyed BLAKE2b-64), the client and relay messages, and the players' messages sealed with
  XChaCha20-Poly1305; EdDSA for what only one player may make; the POS message for showing other players later.
- third_party/monocypher: Monocypher 4.0.2, unmodified (BSD-2-Clause / CC0), compiled with the runtime by
  nx2/build.py and shipped in the AppImage's sources.
- docs/coop.md: running a relay, rooms and secrets, the wire.

Tests: tests/test_coop_relay.py (60 cases: the codec, the relay's rooms and limits on a fake clock, hostile clients,
real UDP); tests/test_coop_proto.py (the C codec against every vector of tests/coop_vectors.txt, which the Python is
checked against too, known answers, sealing, signatures, 400000 fuzzed datagrams under ASan/UBSan, the POS counter).

Carried from the author's own co-op for another port of this game (files he wrote; nothing of that port's code), with
the domain strings renamed and the datagram tag changed from HMAC-SHA256 to keyed BLAKE2b, so that Monocypher is the
only cryptography.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Progress sync needs one rule for how the players' states combine that holds under lost, repeated and reordered
messages and against a player who sends what they should not. This is that engine, game-free; the game does not use
it yet.

- host/coop_state.c/.h: flags as (category, 32-bit hash, value) registers merged by keeping the highest value; small
  keys, rupees and ammunition as signed per-player records in shared pools; digests and sections between the players;
  identities, acceptance by key, the room maker's signed charter (rules, admissions, removals, pool resets). Every
  value is checked against the game's own table before it is stored, passed on or applied.
- tests/coop_sync_test.c with tests/test_coop_sync.py: the engine between simulated games on a simulated network
  (merging in any order, 30 % loss, late joiners, rules, pools, carry-over between rooms and saves, hostile members and
  an attacker with the room's secret, a randomised run against a ledger), under ASan (leaks too) and UBSan on Linux.
  The sanitizers found the engine's apply queue never freed; coop_sync_free frees it now.

Carried from the author's own co-op for another port of this game (files he wrote, no code of that port), with the
signing roles renamed to hookshot-coop and the test's old-engine shim removed.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-op needs the game itself to reach a relay and stay in a room: this adds the
session layer, one UDP socket on one host thread, off unless switched on, that
the menu's Co-op tab and progress sync build on. It never touches game memory.

- host/coop.c, host/coop.h: the session (the relay's cookie exchange, CONNECT
  bursts with backoff, keep-alive PINGs, rejoin after the relay goes silent or
  restarts, the roster, sealed player messages with replay windows, removal by
  the room's maker, BYE and LEAVE when leaving) and the process's session:
  longshot_options.ini's coop, coop_name, coop_server and coop_port, with
  HOS_COOP, HOS_COOP_NAME, HOS_COOP_SERVER and HOS_COOP_PORT over them;
  HOS_COOP_AUTO=create|join with HOS_COOP_ROOM, HOS_COOP_DROP and
  HOS_COOP_TRACE for tests (HOS_COOP and HOS_COOP_TRACE are on/off switches by
  env_on's rule: =0 is off); coop_configure for the menu. The thread exists
  only while co-op is on: switching it off, or the game closing, asks it to
  end and waits for it at most 0.4 s, with the socket closed; switching it on
  starts it again. The room's secret and the relay's address stay out of the
  log. A sender that has used up its 32-bit message counter leaves the room
  rather than reuse a nonce; a new join draws a new session.
- host/coop_net.c, host/coop_net.h: the operating system's part: a dual-stack
  UDP socket, the name lookup, random bytes (BCryptGenRandom, getrandom), a
  lock, an event, a once and a thread that is waited for, on Winsock and on
  BSD sockets.
- host/hos_main.c: coop_init() beside hos_discord_start() on Windows and
  Linux; no name that begins HOS_COOP is taken from longshot_env.txt, a file
  players pass around.
- host/hos_relaunch.c, host/control.c: BYE and LEAVE when the window closes,
  and when the control bridge's quit ends the game. Co-op installs no
  signal handler: a game that is killed says nothing, and the relay drops it
  after 15 s of silence (docs/coop.md says what the others see).
- host/nn_applet.c: hos_ini_text, the options file's text values; its line
  buffer is 256 bytes, for a relay address of up to 159.
- nx2/build.py: -lws2_32 -lbcrypt in the Windows host libraries.
- tests/coop_session_test.c, tests/test_coop_session.py: the sockets on
  loopback, coop_server_parse, co-op off (no thread, no socket), the menu
  switching it on and off (off, on, off cycles with a socket open: no thread
  and no socket left), and two sessions in one process through a hostile
  relay of the test's own and through scripts/coop_relay.py (restarted
  midway), at 0 % and 30 % loss, with the process's session and, on Linux, a
  child ended by SIGTERM that the relay drops for its silence; ASan and UBSan
  on Linux, the two unit runs also under ThreadSanitizer; send_sealed cut
  from the source and run with the real sealing at the counter's end; and the
  wiring in the source (the start beside the Discord status, both ways out,
  the Windows libraries, hos_ini_text).
- tests/test_coop_env_file.py: longshot_env.txt sets no HOS_COOP name.
- tests/test_coop_options_text.py: a long coop_server= line is read whole.
- tests/display_startup_failure_test.cpp: an inert coop_init beside the
  inert hos_discord_start (its fixture compiles main()).
- docs/coop.md: the settings, the log lines, a game that ends without a
  goodbye, the tests; docs/development.md: the options file's co-op keys.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Players in a co-op room should share a chest, a boss, a song or an item one of them gets, in their own games with their
own saves. This is the game side of that, on the merge engine of the commit before: the game's flags read between frames,
the room's values given to the game through its own set routines, and the save copied before anything of a room reaches
it.

- host/oot_coop.c/.h: at the end of each present (host/nvn_native.c, one call, on the presenting game thread with its
  core held), the game's flag database (GameDataMgr) read in quarters and its shared flags classified at run time from
  each entry's reset kinds and save file and the game's own groups (item counts, first-get markers, dungeon keys,
  capacities), with a few dozen hashes for what the groups cannot tell; no list of the game's flags in the source. The
  table is checked against the game's own getter before anything is shared. Values are applied through SetBool and
  SetInt (never a write of the table), only raised, within the game's own table, and only after the player accepted the
  room and its save was copied. Nothing is read or set on the title screen, while a save loads, a scene changes or the
  play clock stands. With co-op off the call reads one value and returns. Sync is on whenever co-op is: from the start
  with HOS_COOP=1 or coop=1 in the options file, or from the session's first callback when co-op is switched on later
  (the session's hooks are registered either way). The offer, acceptance and the maker's remedies for the Co-op tab:
  hos_oot_coop_offer/accept/is_maker/reset_pools/remove_key/contributors. HOS_COOP, HOS_COOP_PROBE and
  HOS_COOP_TRACE are on/off switches by env_on's rule (=0 is off).
- host/nn_fs.c: hos_save_copy, the save folder copied to <user>/coop-backup/ (first-<time> kept, the newest eight
  others), under a lock that the save mount's changes take, only when no save file is open for writing and nothing
  changed for half a second; a save waits for a copy under way. The save folder's path is kept once, by the
  save mounts under that lock, and the control bridge's hos_fs_save_root reads it there.
- tests/test_env_file.py: HOS_COOP_ACCEPT, HOS_COOP_TEST_SET and HOS_COOP_NAME in longshot_env.txt set nothing
  (host/hos_main.c is not changed here: the commit before refuses every HOS_COOP name from that file).
- Tests: tests/test_oot_coop.py (the reader, classification, apply rules, guest calls, table check and gate on a made-up
  flag database, and the switch), tests/test_save_copy.py (a writer racing the copies, ThreadSanitizer; the bridge's save root).
- docs/coop.md: what joining a room means for your save, going back, progress sync, what is shared, the room's rules.

Carried from the author's own co-op for another port of this game (his coop_game.c: the logic; every call into that
port's runtime rewritten against Hookshot's).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Co-op could only be started from the environment (HOS_COOP_AUTO and HOS_COOP_ROOM) or the options file, and a room
could only be accepted from a test switch; a player needs to make or join a room, see who is in it and accept it from
the game.

The Insert menu gets a Co-op tab, the strip's last. Your name and Relay are text fields saved as longshot_options.ini's
coop_name= and coop_server= (which host/coop.c reads at the start); Make a room and Join a room (the secret typed or
pasted) switch co-op on for this run with them (coop_configure, then coop_command). In a room the tab shows its secret
(masked until Enter), a status box with the state, the player count, the relay's round trip and the session's own
words, and a line per member: number, name, you, maker, away, and its identity key's fingerprint. Accept the room
(press twice) takes progress sync's offer (hos_oot_coop_accept: the save is copied first); the game holding the
maker's key also gets Remove a player (Left and Right choose) and Reset the pools (hos_oot_coop_remove_key,
hos_oot_coop_reset_pools). Leave the room, press twice. Rows come and go with the room's state and the selection stays
on its row. While the room waits to be accepted the remedies and Room rules stand back, and the members' lines move
closer together when the room is full, so that every key being accepted shows above the prompts where it can.

Room rules opens a page of its own, as Motion does on Controls: what a room this game makes shares. Progress, Items,
Songs, Quest items (Shared or Per player), Small keys, Rupees, Ammunition (One pool or Per player) are the options
file's coop_progress .. coop_ammo, which host/oot_coop.c reads when the room's maker signs its rules, with the
engine's values (0 per player, 1 shared, 2 one pool) and defaults; under a row the page says what a room cannot have
(with progress shared, items, songs and quest items are shared; with progress per player, small keys are). Co-op at
start is the file's coop=, which host/coop.c reads at the start. The rows' keys are not their names, so they are read
and written through coop_options beside ini_key. The rows take no numeric option id (rx_option): the runtime reads
them by their keys, so they need none of the ids main and other pull requests use.

The interface: vk_api.h version 21, MkwVkCoopView and MkwVkHost.coop_view and coop_command after take_text (version 20).
host/coop_menu.c is the host's side: the session's view and progress sync's offer (keys, token, acceptance) and pool
list (the player's own key) in the tab's terms, the tab's commands into the session's and progress sync's calls. Make
a room uses HOS_COOP_ROOM's secret when the environment has one (test sessions whose second game types it).
HOS_COOP_DEMO=1 runs the tab on a made-up room that walks through every state (members with and without keys, an
offer, accepted, a member away, removed, left) without starting the session.

RX_VK_MENU_SCRIPT gains ROW:<name>: the shown tab's or page's row of that name is selected, if it shows. The tab's
rows come and go with the room's state (and the Graphics tab's with the GPU), so a scripted run that counted DOWN
presses pressed the wrong row; a session driven from the tab chooses Accept the room by name.

The control bridge (host/control.c) lists and sets the Room rules rows like the other settings, under their
coop_ keys (row_key), and names the Co-op tab's and the Room rules page's rows' tabs (co-op, room rules).

save() writes the Room rules rows under their keys with saved_value, as every row, so a run's environment does
not stay in the file; HOS_COOP_DEMO is an on/off switch (env_on). tests/test_menu_env_overrides.py, which compiles
save() cut from the source, gets the tab's part of it as stand-ins; so does tests/test_menu_keys.py for the tab's
parts of menu_tick's Esc block and of menu_init's reading of the options file.

tests/test_menu_coop_tab.py drives the tab's code cut from vk_menu.cpp (rows per state, presses, fields, selection,
the remedies, what the rules page says under a row), compiles host/coop_menu.c against the real secret code with the
session and progress sync stubbed (and the made-up room on a made-up clock), checks the rules page's rows against
host/coop_state.c itself (keys, modes, defaults), and checks the wiring.

docs/coop.md says what the tab does (a section of its own; the lines that said the tab was to come), the readme's menu
table has its rows, docs/development.md has ROW:<name>, HOS_COOP_DEMO and the options file's co-op keys.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Adopt Kuba’s bind_tables rename from his Windows build/play report on #2274, avoiding the bind declaration imported by winsock2.h. Rename the internal IN macro to COOP_FLAG_IN as well, because minwindef.h defines IN. Migrate the existing fixture callers and extraction markers without removing a check. Document the reported Windows read-only hard-link and headless forced-stop behavior.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>

Co-authored-by: Kuba <[email protected]>
A pthread join can finish before the joined Linux TID disappears from /proc/self/task, especially with ThreadSanitizer. The warm-up helper then inflates the baseline from two tasks to three and every exact menu-thread check uses the wrong baseline. Retain each fixture helper’s TID until its join, wait at most five seconds for that specific task to disappear, and only then let the baseline be counted. Keep every existing exact thread/socket-count check and all production code unchanged.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
MinGW GCC checks the session logger’s format(printf) attribute against Microsoft’s format dialect, which rejects %zu even when the fixture requests MinGW ANSI stdio. Print the three wire byte counts with %u and explicit unsigned casts; all are bounded by COOP_MAX_DGRAM. Apply the same portable format to the protocol fixture’s maximum-payload diagnostic. Keep format attributes and -Werror intact, with no build-wide stdio override or runtime behavior change.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
This is a progress-sync fix found by warp, independent of the warp and feature-rule changes. It is intended to be cherry-picked onto the six-commit stack by itself.

Use the save total-play-time counter rather than seconds spent in the current area. The old counter resets in the common area-change completion path, so ordinary doors and loading zones that change area spuriously re-offer an accepted room and require another backup on reacceptance. The Players s2 field-and-back session and Warp s1 silently reaccepted under HOS_COOP_ACCEPT=always; the manual-menu stack proof and other stationary proofs did not exercise that boundary.

Keep all existing load detection unchanged. Independent area and total counters now exercise each transition flag through the real merge engine: area changes preserve the accepted offer and existing copy; another slot, the same slot loaded again with unchanged time, and a new game each invalidate the old offer and require a new acceptance and copy before application. No existing checks removed or weakened. GCC and Clang pass; the extended fixture fails against the prior binding with both compilers. No new GPU run.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Raise the default member limits to 480 forwarded copies and 256 KiB per second, with double bursts and command-line options. Charge every broadcast copy to both budgets.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Reseal missing event fragments until receipt, bind them to accepted sender and recipient sessions, and dispatch each whole event once in per-sender FIFO order. Keep state datagrams independent.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Publish the progress engine consent decision to the session without a game dependency. Refresh it after identity, removal, roster and save changes, and reject bindings for another member session.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
List allocated and reserved kinds, state and event calls, traffic limits, receiver checks, and a ten-line integration example.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Run the single-room relay on its own socket and thread. Make connects the local session over loopback; friends use the maker’s reachable UDP address. Keep cookie proof, sealed forwarding, bounded budgets, and joined shutdown.

Exercise the shared relay cases, session scenarios, settings and lifecycle without the game. Document the single-room limits and the port-forwarding requirement.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Replace a cached refusal after a charged change of reason, as the Python relay does. A request that changes back must pay again. The new test fails before this change and passes against both relays.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Treat each sent event id as the oldest live id for its recipient, so a cancelled partial cannot block later events. Bind acknowledgement masks to the receiver assembly epoch and retain consent transitions between socket steps. Keep completed receipts and reject delayed older-epoch acknowledgements.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
The settings fixture sets HOS_COOP_HOST to an empty value and expects
"off". A Windows process cannot hold an empty variable: _putenv_s(name,
"") removes it, so the game reads the ini value there, as it should,
and the Windows checks failed on "host switch environment precedence"
and "off override does not host" for that one mode. On Windows the
fixture now expects the unset behaviour for the empty mode; elsewhere
the empty value is still set and still means off.

Tested: Linux, GCC 16.2.1 and Clang 23.1.3: tests/test_coop_host_relay.py
passes as before. The fixture compiled for the Windows target (clang
--target=x86_64-w64-mingw32, syntax only): no error, no warning.
Not tested: run on Windows (the project's Windows job does that; it is
the job that showed the failure).

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
The fixture's helper was named accept. On Windows the host's network
header brings in winsock2.h, which declares accept(), so the Windows
checks could not compile the fixture ("conflicting types for 'accept'")
and the event tests never ran there. The helper is named grant.

Tested: all eight C fixtures this branch adds or changes compiled for
the Windows target (clang --target=x86_64-w64-mingw32, -Wall -Wextra,
syntax only): this fixture had 15 errors before and has none now; the
four other co-op fixtures show none (three fixtures that take their
sources or defines from their Python drivers cannot be judged by that
pass). Linux, GCC 16.2.1 and Clang 23.1.3: tests/test_coop_events.py
passes as before.
Not tested: run on Windows (the project's Windows job does that).

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
"Adding a layer" told layer authors to heal damage state with one
running total merged by highest-wins. That loses a hit when two players
strike at once: totals of 2 and 2 merge to 2, not 4. The rule that
works is a running total per player and enemy, each merged by the
highest value seen, then the players' totals added together. The
sentence says so now.

Docs only.

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Windows permits a same-user specific-address bind beside a default wildcard
bind, including the IPv4 half of a dual-stack socket. Another program could
receive datagrams meant for the game's session or hosted relay, or send from
an overlapping local endpoint and disturb the room. It cannot decrypt sealed
player messages without the room secret; relay-control metadata is not sealed.
The Windows session CI failure exposed this as cross-test Alice/Bob traffic,
not a reason to serialize the fixture or relax its roster/message checks.

Set SO_EXCLUSIVEADDRUSE before binding native Windows co-op sockets and the
Python relay. Close the socket and fail when exclusivity cannot be obtained.
A relay bound to a specific address owns that requested address, not unrelated
interfaces. Linux enables neither SO_REUSEADDR nor SO_REUSEPORT and already
rejects overlapping binds; no reuse option is added there.

Keep all concurrent session cases. Add real wildcard/specific IPv4 bind probes
in both orders, including a competing reuse request, on Windows and Linux.
Compile the actual Windows open function against a call recorder to check
option ordering and failure cleanup without a Windows machine. The recorder
and Python option check fail on the old source and pass on the fix; real
Windows kernel execution remains required in the fork workflow. Preserve
-Werror and add the fixture's secure-CRT guards before includes.

Binding rules: https://learn.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
The local Windows preflight found that several co-op drivers declared the
CRT policy in only their fixture, or not until after an included header.
Their separately compiled game sources and the forced environment header
therefore still produced secure-CRT deprecation diagnostics.

Set the existing project policy before those headers, use compile-wide
Windows flags for multi-unit fixtures, and let existing fixture
prefixes coexist with that flag. Cover all three Co-op menu compilations,
not just the first one reached before the capture stops at execution.
Keep the existing compiler warnings, assertions, and concurrent modes.
No game or protocol behavior changes here.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Keep the existing Longshot domains and pin their exact bytes against future branding changes. Regenerate sealed payloads with the C reference fixture and the vector file with test_coop_relay.py --write-vectors. Document the Hookshot/Longshot interoperability boundary.

The same reference with only the old domain inputs reproduces the original vector file byte-for-byte; the original port commits are unchanged.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
The shared predicate continues with the co-op keys instead of ending at auto_steps. Keep every Auto persistence key assertion.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
main's third-party check now requires a tree-sha256 line in each part's PROVENANCE.md; written by 'python -I scripts/check_third_party.py --update monocypher'. No file of the library changes.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
fix(coop): integrate the reviewed text-field controls and fixtures
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
Website / Website on Cloudflare (pull_request) Has been cancelled
f841f54400
Keep the parent text-field implementation and its appendable API checks. Show character-picker prompts only where a field supplies that picker; Co-op text fields retain caret, removal, paste and confirmation controls. Exercise both prompt sets from the real menu block.

Update the Co-op fixture key definitions, extract the actual edit helpers without pulling in unrelated room actions, and give the upscaler save/load fixture the same inert Co-op stand-ins as the existing menu fixtures. Keep all parent pad/full-width/paste/shape and upscaler checks.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Port the reviewed parked rule and menu changes onto the Longshot relay foundation. Keep Longshot signature domains, format 5 and one API24 metadata callback; retain the existing EVENT helper and current output-size and Auto persistence behavior.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Keep all wire-domain assertions. Only the charter signing role advanced with numbered features; the protocol fixture must pin Longshot format5 for both engine and authored signed fixture.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
The rebased menu parser records whether output_res is present. Supply that existing startup-local flag in the extracted feature-rules fixture; all registry and persistence assertions remain unchanged.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Keep received terms separate from local defaults, pin both confirmation presses, refuse unsupported modes and format4, and keep PvP setup incapable of starting gameplay.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Named settings consult the current room even while the menu is closed. The mode leads both local drafts and received read-only terms; local defaults remain separate.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
The real menu tick cleared the pending Accept as soon as its preview selected a Rules row. Preserve that pending action only while reading the signed terms; the existing stale-offer, changed-selection and closed-menu paths still disarm it.

The fixture now executes the actual tick cancellation block and Back transition between presses. It fails on a9cc1bea before this correction.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Keep the no-focus wiring assertion and require its narrowly scoped reading-terms exception. The compiled GCC and Clang behavior already passed; this updates the additional old-expression source assertion.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Execute the actual tab-navigation and closed-menu cleanup blocks after arming Accept. Returning and reopening must require a new first press, never complete the cancelled one. Production code is unchanged.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Keep exact-offer cancellation while distinguishing signed membership revisions from rule and pool changes. Show PvP-only acceptance help and logs, and omit Change on read-only Rules. Add source-backed signed admission fixtures and catalogue coverage; reserve API 27 for the expanded received view.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Retain the opaque-change assertion while making its authored roster match the host view contract; duplicate zero IDs had falsely resembled changed membership.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Separate presentation, temporary and durable writers. Exact version and size checks fail closed; queued tickets bind current context and accepted sessions. Route progress through its sole writer under the existing lock, and make switching co-op off stop guest reads.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Port the existing private body generator and bounded pose adapter onto Longshot. Require signed presence permission and current accepted tickets before native effects, defer request storage until permitted, refuse adult body poses in this family, and preserve native actor-generation guards. No outside multiplayer code, tables or assets are imported.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
A new presentation grant starts a fresh pose history while retaining the session replay floor. Publish only validated capabilities and keep the child-only contract explicit.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Remove the unused adult native actor path from this first body family while retaining explicit adult-pose refusal and codec coverage.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
An authored timestamp can place the receiver offset at INT32_MAX. Keep the next same-offset sample bounded, and retain recovery when a smaller offset arrives. The game-free fixture exercises both cases.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Preserve startup allocation refusal after deferred storage, normalize received rotations, clamp interpolation weights and avoid overflowing finite animation endpoints. Authored fixtures cover each boundary.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Require the generated actor pack and exact source-version actor/resource tables by one extensible list instead of the obsolete child-plus-adult file count. Preserve user-mod precedence and disable animation on replacements; exercise the real loader against authored generator output.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
The acceptance-log extraction now includes the child visibility helpers used by publish. Supply the existing default-option host stub in the neighboring real mod-routing fixture.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Read the same per-slot phase as the native current-frame query. Keep the duration binding in the game-free fixture as a distinct value so a mistaken function address cannot pass again.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Generate a separately named local AS graph whose primary slot uses the source full-body mask. Keep other partials and overlays, do not import the real player controller, and require the generated graph through the shared asset contract. Authored fixtures check the binding, dynamic mask lookup, unchanged source and fail-closed output.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Use the Windows CRT environment calls in the synthetic native adapter fixture; retain the same POSIX behavior and every pose assertion.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
The S03 finish-stage actor generator was accidentally routed through the prompt fixture drawing stub, causing 46 false cache failures. Keep the complete production prompt branch and every existing assertion; reject any unrelated generator in the stub. Production prompt code is unchanged.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Copy under the native save barrier, select the private root only at restart, and return by selecting the unchanged normal root. Keep interrupted copies and refuse incomplete or altered restart tickets.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Keep automatic room consent off across copy/return restarts, preserve read-only RomFS paths, and guard temporary backup paths. Exercise the native write fence and temporary progress gate.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Refuse linked temporary backup paths, keep the read overlay away from native user mounts, and exercise writable/read-only saves, existence and cache enumeration through their real mount functions. Use wide Windows copy paths.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Update the existing startup/relaunch wiring checks without dropping their shutdown, environment or ordering requirements. The native mount fixture keeps UTF-8 filename coverage without relying on a Windows UTF-8 argv manifest.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
The expanded native mount fixture uses the real cache-storage refusal path; supply its filesystem result constant without changing any guard or assertion.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
The mount helper receives native backslash-separated subpaths. Keep the two-entry enumeration assertion and construct the first cache with that same contract.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Use API 26 for the combined received-terms and temporary-save view, retaining rule-change command 9 and copy/return command 10. Refuse durable writer capability for a temporary profile even when called directly, with a recording boundary regression.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Keep the two-press copy/return action on the same host command and Linux close/relaunch path when no SDL window exists. Poll close requests while the game thread runs, retaining the preparation worker wait. Exercise windowless request delivery, idle waits, and the actual menu action with authored fixtures.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
The full build found that the temporary-save action used a no-argument fixture stand-in instead of the renderer API. Use force=true as ordinary Restart does. Extract the actual vk_draw.h declaration into the behavioral fixture and pin its function type so future signature drift fails before a game build.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Exercise the real menu pending-action cleanup between first and second presses, including changed selection and refused copy. This action stays on its own row; no additional renderer defect was found.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Keep the save-domain note on its own line so it cannot hide a refusal. Exercise the extracted header rendering and box bounds at three scales.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Separate download consent from progress acceptance, bind offers and receipts to identities and sessions, and stream two fixed file kinds through the existing reliable-event helper. Keep partials unmountable, retain signed manifests, and cancel only the transfer scope on deadlines, revocation or disk failure.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Keep the complete versioned signature domain, preserve the verified manifest on disk, recheck permission and clocks after filesystem work, and bound terminal control retries. Add independent disk-hash and publication-revocation coverage.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
A valid opposite-direction offer can choose the same transfer ID. Match its manifest as well so its signed cancellation cannot be hidden by the outgoing offer. Show the received charter terms in the local download preview, and retain the before-fix behavioral regression.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Keep warnings as errors while explicitly discarding the parameter made unused by the mutation. Its failure must come from the behavioral scope assertion, not compilation.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Carry every numbered received feature and the room admission reason into the download preview. Exercise missing/off/unknown/old-format and stale-token refusal at the actual download consent boundary, separately from native progress acceptance.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Exercise a real simulated network/update interval after accepting the exact download preview, before checking the independent member mask and closed progress gates. Unlike menu Accept this path has no pending renderer confirmation.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Port the original committed-hit observer onto the signed-rule foundation of pull request 68. Observe native setup, instance renewal and sensor-off operations without applying any remote damage. Retain pre-clamp aggregate and original requested life through Protect hearts, count successful atomic writes only, and drain bounded scalar observations outside native workers. Reuse EVENT rather than a separate retry loop.

Game-free behavioral fixtures cover native hooks, protected overkill, receiver admission and bounded delivery; execution is recorded separately. No game runtime or playable-duel claim.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
The synthetic sensor-off body must use its context argument even when the old lift patch adds no observer. Keep strict compiler warnings and let missing native observation fail behavioral assertions, not compilation. This fixture-only annotation does not change production code; negative execution remains pending quiet-window admission.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Restore the frozen line ordering while keeping the reviewed parent text-field prompts and Damage taken policy.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Keep native loss requests and every setup/renewal header independent of optional diagnostic detail and the room mutex. Report cumulative accounting, pending publication and sticky overflow with explicit no-drain bounds; preserve observation-only semantics.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Snapshot accepted recipients as unicasts and recheck consent under the send lock. A revoke/reaccept cannot revive queued positions. Receiver admission is unchanged.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Use age-specific local actor packs and the existing sole body writer. Carry bounded cosmetic equipment in POS v3, resolve only local supported pieces, and read lasting native hand state. Generated animation follows the local display switch unless explicitly overridden.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Extend the shared generator/loader contract to both actor and animation pairs. Exercise both ages in authored full-body and missing-mask checks while retaining passive equipment and fail-closed loader coverage. Adult runtime remains unproven.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Reuse submitted-camera name projection with bounded offscreen arrows, independently permissioned map and heart disclosure, fresh accepted snapshots and a live local display setting. No health authority or game run.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Retain all existing room action checks and separately assert the local label row. Match the production C++ static-assert spelling and prerequisite header order.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Assert Names defaults, names-and-arrows reload and Off save without duplicate keys. Keep explicit CRT policy before fixture headers for Windows syntax checks.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Normalize direction before the bounded square root instead of deprecated MinGW hypot. Supply native fixture environment helpers on Windows and remove the unused inherited helper from the rule-persistence fixture.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
HudNames added viewport publication and its off-path query inside the extracted present block. Supply both fixture boundaries and exercise labels off, full viewport and crop coordinates without removing the geometry and bounded-log checks.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Preserve the keyboard footer fixture context and run the actual Damage taken implementation through both observed life-write paths.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Reuse the native map request and character-controller placement, with explicit signed permission, accepted pose freshness and cancellation at the native map boundary. Keep unreviewed entrances closed.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Use a fixed-size copy between disjoint name buffers; keep all old menu checks alongside the new rows.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Share strict UTF-8 decoding with scripted text; keep persistence fixture callbacks aligned with API 27.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Reuse the existing row scroller with space reserved for the member list; show unavailable chat explicitly.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Use signed semantic kit masks, native pouch giving and worn-equipment selection with capacity and live-slot readback. Refuse unsupported families and normal mounts; retain one-shot setup across cancellation without progress export.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Keep native combat fail-closed until receiver contact, damage and private arena surveys are proven. Implement the game-free Ready/countdown, descriptor and round contracts without game health or save writes.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Keep ordinary acceptance, progress, pools and save-download consent isolated from copy-only see-and-talk permission. Bind presentation to the exact signed offer and sessions, retire it on lifecycle changes, and retain native owner tickets and the off-path early return.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Run the new copy-presence menu case alongside the other independent cases instead of before the existing off-room baseline. Keep every original refusal assertion.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Use the fixture compiler flag required by the Windows-target checker before all headers, retaining strict diagnostics and every behavioral assertion.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Unify the presentation mask, preserve per-session pose send fences, and keep kit effects behind a separate exact-copy two-press command. Use menu API29 for every added view and callback. Explain refused position versions instead of silently omitting a body.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Keep retry attempts and unresolved recipient retirements in the session stats line without changing retry policy. Cover no-loss completion, lost ACKs, revoke, deadline, cancellation and reset, including full-width counter formatting.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
The combined translation unit already has an incoming event_discard helper. Give the stats-aware outgoing retirement path its own name and retain the cancellation mutation anchor.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Keep recorder include ordering, distinct controller-picker and Unicode fields, local notice preset exemption, remote-only duel consent and exact kit/menu/version checks.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Exercise the combined overlays, dynamic menu rows, complete damage hook and unified scrolling while preserving the reviewed parent assertions.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Keep the real-engine no-durable/download boundaries, independent presence and kit admission, combined privacy-mask and native off-path checks.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Use the existing C-assert spelling bridge for C++ fixtures, start received-term extraction after its forward declaration, and retain all seven categories after removing both local preferences.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Exercise the shared engine, host tick/publication and save runtime with an actual temporary profile and authored checkpoints. Retain isolated normal-save behavior and catch restored missing-offer, identity, terms, manifest, signature and presentation-boundary defects.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Document the existing engine-owned offer used by both the menu and save bridge, independent download and presentation consent, and the regression that restores the old empty-offer gate.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Retain a verified maker/owner charter while a third member remains unidentified past the wait interval. Require a real nonzero offer, then refuse consent and export; removing the unidentified guard must fail under both compilers.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
fix(coop): normalize native save roots before profile I/O
All checks were successful
Website / Website on Cloudflare (pull_request_target) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 14s
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 8s
0ec9d0b024
Normalize external user and export roots at the profile boundary before POSIX I/O, reusing the same separator rule for path validation. Reject parent components before following them. Exercise actual save-mount and export functions with their native separators, exact signed manifests and filesystem refusals.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Author
Member

Two-player co-op playtest: r12

This is an experimental test, not a finished multiplayer release. Use commit 2f01e1b4bdb6e1629c54a998f0403519fe4e5c74 (r12) on both PCs, Linux source 05329d46. The pull request's branch is still named coop-playtest-r9 for historical reasons; pin the commit, not the branch suffix. All six short proofs passed on this r12 head with two games on one Linux PC. Windows runtime, two-machine and 30-minute results already posted on the pull request belong to earlier heads, not r12. No Windows runtime, two-machine session or 30-minute result is claimed for this head.

Plan for 10 to 15 minutes of play after building and initial shader preparation. Loading and preparation are not part of that estimate. Start with child saves. One tester hosts on Linux, the other joins on Windows; the Windows/network part is what this test is meant to find out.

What each tester needs

  • Their own supported Switch 2 game dump and their own save. Do not exchange a built game, save, generated body assets, game files or keys.
  • A PC that can run Longshot, with enough memory and a supported Vulkan driver. Record Windows/Linux version, GPU and driver, CPU and RAM. Allow about 60 GB for building, as the source guide specifies.
  • The same source commit, built separately for each platform. A current release or pull request 68 alone is not this line.
  • A keyboard for entering names, the relay address and chat. The controller has confirm/back/paste controls, but this line adds no controller character picker.
  • A private way to exchange the host address, room secret and displayed identity fingerprints. Do not put the room secret in a public report or screenshot.

Obtain the playtest branch and pin it:

git clone --branch coop-playtest-r9 --single-branch https://git.magicbean.dev/mooki/Longshot.git coop-playtest-source
git -C coop-playtest-source checkout 2f01e1b4bdb6e1629c54a998f0403519fe4e5c74

Follow that checkout's CONTRIBUTING.md, Development setup and Building. Windows uses Python and llvm-mingw, with LONGSHOT_MINGW pointing to it and the documented Builder prerequisites. Linux needs the documented compiler, headers and Python dependencies; its packaged build instructions use Ubuntu 22.04 or the provided container route. Do not update an ordinary playing installation to test this branch.

From the prepared source checkout, build into a new game folder using your own dump, for example:

python -I nx2/make_game.py --nup "your-game.nup" --out "../coop-playtest-r12-game" --work "work/coop-r12"

Replace your-game.nup with your own file. This is the normal full build, including locally generated co-op bodies. Keep its executable and renderer together; do not mix products from different commits. The reference instructions are https://git.magicbean.dev/mooki/Longshot/src/branch/coop-playtest-r9/CONTRIBUTING.md and https://git.magicbean.dev/mooki/Longshot/src/branch/coop-playtest-r9/docs/coop.md.

Keep the normal save out of the test

  1. Close your ordinary game. Make an offline backup of your own saves.
  2. Inside the new test game folder, create a new folder named coop-playtest-user. Copy only your own save directory, retaining its numbered subfolders, into it. Do not copy someone else's user folder or the coop-keys directory. Each player must have a separate test root and identity. A new disposable game is also fine, but is not a quick substitute for a prepared checkpoint.
  3. Always start the actual game executable with --user "./coop-playtest-user". A source build normally has longshot.exe or longshot directly in its output folder. Packaged builds put the real executable in bin; the outer launcher does not forward --user.
  4. Check the startup fs: mounted lines and the chosen save before accepting a room. They must refer to this test root, never the usual save directory. If they do not, stop without accepting.
  5. The test then uses Temporary save as a second layer of isolation. Its restart creates a working copy below this test root's coop-sessions, and Return to normal saves returns to this test root, not the original installation. Normal and copy acceptance do not carry across those restarts.

For a source-built game, run from its new game folder:

.\longshot.exe --user ".\coop-playtest-user"
./longshot --user "./coop-playtest-user"

For a packaged game use bin\longshot.exe or bin/longshot instead. Keep this explicit --user argument on every manual restart, especially after a crash. An ordinary restart does not automatically resume an interrupted native temporary copy. Do not import the test's progress back into the usual save folder.

Windows Copy/Return still needs a real game window. Do not use HOS_HEADLESS=1 for this route. A restart creates a new Windows process; the first process ending is not proof of a completed Return. Wait for the replacement window and check its new fs: mounted lines before continuing. Never create or copy a restart ticket by hand.

R12 keeps runtime program/pipeline/preparation caches under cache/, Vulkan pipeline files as cache/longshot_vk_pipelines*, and generated prompt files under cache/prompts/. Use a new writable r12 game folder; do not copy Linux driver caches into Windows or move files out of a running r9 folder. Initial preparation is still needed for the target PC. The game's shipped data and longshot_options.ini stay at their documented locations.

Settings before starting

Edit only the test game's longshot_options.ini, with that game closed. Set these on both PCs:

coop=0
coop_players=1
coop_room_mode=0
coop_progress=0
coop_items=0
coop_songs=0
coop_quest=0
coop_keys=0
coop_rupees=0
coop_ammo=0
coop_presence_permissions=1
coop_names=1
coop_labels=1
coop_social_permissions=1
coop_notices=1
coop_warp=0
coop_hit_events=0
coop_combat_mode=0
coop_loadout_version=0

Here category value 0 means Own. Presence value 1 permits child bodies only; it does not enable fighting, gear, health, place or arrows. Chat is enabled here for an explicitly agreed trial, not because it was already proven on r12. coop=0 prevents automatic connection; Make or Join turns it on for the current run. The maker's signed rules, not the joiner's local draft, govern the room.

The Linux host additionally sets coop_host=1; the Windows joiner sets coop_host=0. Remove any inherited co-op environment overrides, automatic joining/acceptance, diagnostic feeds or loss simulation from the test launch. Do not use HOS_COOP_ACCEPT=always. HOS_COOP* values written in longshot_env.txt are ignored; these settings belong in the options file or the process's own environment.

On Windows Intel Arc, keep the newer base's intel_shader_compiles=0, which admits one compiler thread at a time. Leave RX_VK_NO_INTEL_SERIAL unset; do not use it to bypass that safety default. The inherited hold-to-toggle controls default to off and Mono output to Auto; neither needs changing for this route. If a follow-up asks for diagnostic commands, use scripts/longshot_ctl.py from this exact r12 checkout: it now reports sampled game state, which can remain old at the title, during a pause or while loading. An advancing reply counter alone does not establish fresh play state.

Host and join using the game itself

No separate relay program is needed. The firewall example below is retained from the previous head 0ec9d0b0; it does not claim a two-machine r12 session. A new executable path on Windows can require a fresh firewall decision for the intended private network.

  • Host: leave Relay empty for the default UDP port 24680, then use Make a room. The log says hosting on this PC and prints hosting: players on this network can try .... The host's own client connects over loopback.
  • Joiner on the same LAN: put the host's printed LAN address and port in Relay. Do not use 127.0.0.1 on the second PC. Choose Join a room and enter the secret shared privately by the host.
  • The host's firewall, also on a home network: the host's PC has to allow incoming UDP 24680 from the joiner's PC. On Windows the first hosting brings up Windows' own firewall prompt; allowing it for private networks is enough. On Linux with a firewall that blocks incoming traffic, allow that one port for the other PC. If the port is blocked the joiner only sees Connecting: no answer from the relay, and the host sits alone in its room; nothing else is wrong. The host's log can print more than one address: use the home-network one. Tried between a Linux PC and a Windows 11 PC: #355 (comment)
  • Across the internet: the host needs that one UDP port reachable through the router and the intended firewall network; the joiner uses the public address and port. The game does not open ports, discover the public address or bypass carrier-grade NAT. If it is unreachable, report that rather than disabling the firewall or changing unrelated security settings. A separately arranged reachable relay is a different test.
  • Use the ordinary LAN or internet path, not a VPN/tailnet, for this test. Record which was used. The first LAN session does not prove internet/NAT behavior.
  • When the host leaves or closes the game, its hosted room ends. The other player can keep playing locally, but the room cannot exchange data until hosted again.

The 10-to-15-minute route

Use menu labels, not a remembered row number: rows appear, disappear and scroll with room state. Insert opens the menu, and the Co-op tab is at the end. Follow the displayed keyboard/controller prompts.

Play time What both testers do Expected result; what to report
0 to 3 minutes With no room open, load the copied child checkpoint, save normally and wait until Saving ends. Select Temporary save and confirm twice. Wait for restart, load the checkpoint again and check the copy reminder. The game is now on a native temporary copy. Keep the fs: mounted lines showing the new copy root. A failed copy must not be bypassed by accepting on the ordinary save.
3 to 6 minutes Host makes a room; joiner enters its address and secret. Set distinct test names. Compare both displayed fingerprints privately. Host selects Accept to see and talk, reads Room rules, goes Back and confirms. Once the membership/terms settle, the joiner does the same. Before acceptance there should be no remote body or chat. Both should see the same signed maker and terms. A changed offer cancels a pending press; read it again, do not repeatedly confirm blindly. Copy-only presence is itself a new live test.
6 to 8 minutes Meet in the same child area, stay on clear ground and take turns standing, turning and walking a short distance while the other watches. Look for both complete bodies, normal colours, arms-down standing and visible steps rather than sliding. Note missing bodies, wrong age, stuck poses or labels covering the screen. Remote sword/shield display is not enabled in this minimal configuration.
8 to 10 minutes Try one short chat message each with T or Chat. Cancel another with Esc. Optionally mute, send one more line, then unmute. If both agree, the maker can change Friend information from 1 to 77 and both freshly accept; select Names and arrows, then walk briefly out of one another's view in the same map. Chat should reach only accepted peers and typing/cancelling should not move or attack with Link. Mute is local. Value 77 is child + health + arrows + place; these extra displays are tests, not existing runtime guarantees. Changed signed terms must require a new acceptance. A maker on a copy must first accept see-and-talk before editing rules.
10 to 12 minutes Make one ordinary local change in A's copy, such as an available chest pickup, and let it autosave while B watches its own world. Do not use the practice kit for this check. A's copy may change; B must not receive progress from copy-only consent. The normal saves must not change. A missing available pickup is not a failure: record that this step was not tried.
12 to 13 minutes Joiner selects Leave the room and confirms twice. Watch both views before making any new room. Remote bodies should disappear and chat should stop. Rejoining is optional; it needs current identity/terms and fresh acceptance where offered. Do not treat a reconnect as automatically accepted.
13 to 15 minutes Both use Return to normal saves through the Temporary save row and confirm twice. After restart, check the test-root mount and the original copied checkpoint, then quit normally. Return must select the disposable normal root without importing copy progress. Keep the changed temporary copy for diagnosis. The original installation's save is outside the test throughout. Save the logs before later launches rotate them away.

If anything unexpected changes the save, the game crashes, or either player cannot tell which save domain is active, stop that session and preserve the logs. Do not delete the copy or try to repair it by importing another save.

What is not part of this short test

  • Fighting is disabled. Ready and sword swings do not prove a hit or a block. The six proofs observed no committed player health loss during one bounded five-swing interval; that is not playable PvP.
  • Progress is not shared unless ordinary-save room terms explicitly allow it and both players accept with a completed backup. Native temporary copies never share ordinary progress, even if their signed Rules page displays Shared. This guide uses Own categories and copies deliberately.
  • Go to friend is unavailable on native temporary copies. A later, separately agreed test can use disposable normal roots with coop_warp=1; only Kokiri Forest and Hyrule Field are supported. Safe actual arrivals are not proven by the six proofs. Do not return to the real save just to make the row available.
  • Adult bodies, cosmetic sword/shield display and the separately confirmed practice kit need their own tests. The kit supports only the documented child or adult sword/shield pair, not a full item palette; it grants no permission to fight.
  • No shared enemies/bosses, shared time, save-owner picker, loading a downloaded checkpoint, or returning progress from a temporary copy. The signed-transfer proof used separate local control-API approvals; there is no ordinary save-transfer menu to look for.
  • The room can describe up to 8 members, but only two simultaneous games were proven here. The short tests do not settle long sessions, packet loss, relay restart, internet reachability or performance.
  • Some diagnostic prose is shortened or misleading: pending PvP can mention progress; a body-retirement line can call Leave a map change. Use the actual signed Rules page and observed state, and include the suspect line in the report.

What to send back

Send a short result from each tester: exact commit/source line, OS, GPU/driver, CPU/RAM, host or joiner, LAN or internet, duration, which route steps worked/failed/not tried, and the first unexpected action. Say whether the normal save stayed unchanged and how you checked. A picture or short video of both views is useful; mask the room secret and unrelated private information.

The documented default game log is now logs/longshot_log.txt in the game folder. The four preceding runs are logs/longshot_log.1.txt through logs/longshot_log.4.txt. Copy/Return restart the process, so retain the rotated files too. A game started from a terminal prints there instead; an explicitly redirected launch keeps its chosen output file and may leave logs/ empty. Retain that actual output, rather than assuming a missing default file means nothing happened. Preserve output from every restarted process. The README documents these names at https://git.magicbean.dev/mooki/Longshot/src/branch/coop-playtest-r9/README.md.

Keep the complete relevant logs, plus these lines around the failure:

  • The first version/source line and vk: device identifying the GPU and driver.
  • fs: mounted lines before Copy, on the temporary copy and after Return. Redact personal path prefixes when posting, but keep enough to distinguish the save domains.
  • coop: hosting / hosting: players on this network can try ..., coop: connecting to the relay, the relay answered, created, joined room, and any no room: refusal. Share network addresses privately when needed, not in a public screenshot.
  • coop: sync: acceptance and changed-offer lines; coop: players: creation, hiding/deletion or asset/matching-build refusals.
  • Every coop: stats: line, especially reliable retries, events given up, outbox full and round trip. Given-up events include intentional cancellations, so do not label that total packet loss.
  • coop: relay lost; rejoining, is away, is back, left room, and the final shutdown or error lines.
  • coop: kit: only if a separate kit test was explicitly agreed. Chat text is not logged: describe a failed message without expecting to find its contents in the log.

HOS_COOP_TRACE=1 can add packet and body details when a focused follow-up asks for them; it also logs relay addresses and can make a large log. It is not needed for the first attempt. Do not send saves, identity keys, room secrets, game files, generated assets or a whole user folder. Return the report to the WIP line or #68 while that remains the collection point.

# Two-player co-op playtest: r12 This is an experimental test, not a finished multiplayer release. Use **commit `2f01e1b4bdb6e1629c54a998f0403519fe4e5c74` (r12)** on both PCs, Linux source **`05329d46`**. The pull request's branch is still named **`coop-playtest-r9`** for historical reasons; pin the commit, not the branch suffix. All six short proofs passed on this r12 head with two games on one Linux PC. Windows runtime, two-machine and 30-minute results already posted on the pull request belong to earlier heads, not r12. No Windows runtime, two-machine session or 30-minute result is claimed for this head. Plan for **10 to 15 minutes of play after building and initial shader preparation**. Loading and preparation are not part of that estimate. Start with child saves. One tester hosts on Linux, the other joins on Windows; the Windows/network part is what this test is meant to find out. ## What each tester needs - Their own supported Switch 2 game dump and their own save. Do not exchange a built game, save, generated body assets, game files or keys. - A PC that can run Longshot, with enough memory and a supported Vulkan driver. Record Windows/Linux version, GPU and driver, CPU and RAM. Allow about 60 GB for building, as the source guide specifies. - The same source commit, built separately for each platform. A current release or pull request 68 alone is not this line. - A keyboard for entering names, the relay address and chat. The controller has confirm/back/paste controls, but this line adds no controller character picker. - A private way to exchange the host address, room secret and displayed identity fingerprints. Do not put the room secret in a public report or screenshot. Obtain the playtest branch and pin it: ```text git clone --branch coop-playtest-r9 --single-branch https://git.magicbean.dev/mooki/Longshot.git coop-playtest-source git -C coop-playtest-source checkout 2f01e1b4bdb6e1629c54a998f0403519fe4e5c74 ``` Follow that checkout's `CONTRIBUTING.md`, Development setup and Building. Windows uses Python and llvm-mingw, with `LONGSHOT_MINGW` pointing to it and the documented Builder prerequisites. Linux needs the documented compiler, headers and Python dependencies; its packaged build instructions use Ubuntu 22.04 or the provided container route. Do not update an ordinary playing installation to test this branch. From the prepared source checkout, build into a **new** game folder using your own dump, for example: ```text python -I nx2/make_game.py --nup "your-game.nup" --out "../coop-playtest-r12-game" --work "work/coop-r12" ``` Replace `your-game.nup` with your own file. This is the normal full build, including locally generated co-op bodies. Keep its executable and renderer together; do not mix products from different commits. The reference instructions are https://git.magicbean.dev/mooki/Longshot/src/branch/coop-playtest-r9/CONTRIBUTING.md and https://git.magicbean.dev/mooki/Longshot/src/branch/coop-playtest-r9/docs/coop.md. ## Keep the normal save out of the test 1. Close your ordinary game. Make an offline backup of your own saves. 2. Inside the new test game folder, create a new folder named `coop-playtest-user`. Copy **only your own `save` directory**, retaining its numbered subfolders, into it. Do not copy someone else's user folder or the `coop-keys` directory. Each player must have a separate test root and identity. A new disposable game is also fine, but is not a quick substitute for a prepared checkpoint. 3. Always start the **actual game executable** with `--user "./coop-playtest-user"`. A source build normally has `longshot.exe` or `longshot` directly in its output folder. Packaged builds put the real executable in `bin`; the outer launcher does not forward `--user`. 4. Check the startup `fs: mounted` lines and the chosen save before accepting a room. They must refer to this test root, never the usual save directory. If they do not, stop without accepting. 5. The test then uses **Temporary save** as a second layer of isolation. Its restart creates a working copy below this test root's `coop-sessions`, and **Return to normal saves** returns to this test root, not the original installation. Normal and copy acceptance do not carry across those restarts. For a source-built game, run from its new game folder: ```powershell .\longshot.exe --user ".\coop-playtest-user" ``` ```sh ./longshot --user "./coop-playtest-user" ``` For a packaged game use `bin\longshot.exe` or `bin/longshot` instead. Keep this explicit `--user` argument on every manual restart, especially after a crash. An ordinary restart does not automatically resume an interrupted native temporary copy. Do not import the test's progress back into the usual save folder. **Windows Copy/Return still needs a real game window.** Do not use `HOS_HEADLESS=1` for this route. A restart creates a new Windows process; the first process ending is not proof of a completed Return. Wait for the replacement window and check its new `fs: mounted` lines before continuing. Never create or copy a restart ticket by hand. R12 keeps runtime program/pipeline/preparation caches under `cache/`, Vulkan pipeline files as `cache/longshot_vk_pipelines*`, and generated prompt files under `cache/prompts/`. Use a new writable r12 game folder; do not copy Linux driver caches into Windows or move files out of a running r9 folder. Initial preparation is still needed for the target PC. The game's shipped data and `longshot_options.ini` stay at their documented locations. ## Settings before starting Edit only the test game's `longshot_options.ini`, with that game closed. Set these on **both** PCs: ```ini coop=0 coop_players=1 coop_room_mode=0 coop_progress=0 coop_items=0 coop_songs=0 coop_quest=0 coop_keys=0 coop_rupees=0 coop_ammo=0 coop_presence_permissions=1 coop_names=1 coop_labels=1 coop_social_permissions=1 coop_notices=1 coop_warp=0 coop_hit_events=0 coop_combat_mode=0 coop_loadout_version=0 ``` Here category value 0 means **Own**. Presence value 1 permits **child bodies only**; it does not enable fighting, gear, health, place or arrows. Chat is enabled here for an explicitly agreed trial, not because it was already proven on r12. `coop=0` prevents automatic connection; Make or Join turns it on for the current run. The maker's signed rules, not the joiner's local draft, govern the room. The **Linux host** additionally sets `coop_host=1`; the Windows joiner sets `coop_host=0`. Remove any inherited co-op environment overrides, automatic joining/acceptance, diagnostic feeds or loss simulation from the test launch. Do not use `HOS_COOP_ACCEPT=always`. `HOS_COOP*` values written in `longshot_env.txt` are ignored; these settings belong in the options file or the process's own environment. On **Windows Intel Arc**, keep the newer base's `intel_shader_compiles=0`, which admits one compiler thread at a time. Leave `RX_VK_NO_INTEL_SERIAL` unset; do not use it to bypass that safety default. The inherited hold-to-toggle controls default to off and Mono output to Auto; neither needs changing for this route. If a follow-up asks for diagnostic commands, use `scripts/longshot_ctl.py` from this exact r12 checkout: it now reports sampled game state, which can remain old at the title, during a pause or while loading. An advancing reply counter alone does not establish fresh play state. ## Host and join using the game itself No separate relay program is needed. The firewall example below is retained from the previous head `0ec9d0b0`; it does not claim a two-machine r12 session. A new executable path on Windows can require a fresh firewall decision for the intended private network. - **Host:** leave **Relay** empty for the default UDP port **24680**, then use **Make a room**. The log says `hosting on this PC` and prints `hosting: players on this network can try ...`. The host's own client connects over loopback. - **Joiner on the same LAN:** put the host's printed LAN address and port in **Relay**. Do not use `127.0.0.1` on the second PC. Choose **Join a room** and enter the secret shared privately by the host. - **The host's firewall, also on a home network:** the host's PC has to allow incoming UDP 24680 from the joiner's PC. On Windows the first hosting brings up Windows' own firewall prompt; allowing it for private networks is enough. On Linux with a firewall that blocks incoming traffic, allow that one port for the other PC. If the port is blocked the joiner only sees `Connecting: no answer from the relay`, and the host sits alone in its room; nothing else is wrong. The host's log can print more than one address: use the home-network one. Tried between a Linux PC and a Windows 11 PC: https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17499 - **Across the internet:** the host needs that one UDP port reachable through the router and the intended firewall network; the joiner uses the public address and port. The game does not open ports, discover the public address or bypass carrier-grade NAT. If it is unreachable, report that rather than disabling the firewall or changing unrelated security settings. A separately arranged reachable relay is a different test. - Use the ordinary LAN or internet path, not a VPN/tailnet, for this test. Record which was used. The first LAN session does not prove internet/NAT behavior. - When the host leaves or closes the game, its hosted room ends. The other player can keep playing locally, but the room cannot exchange data until hosted again. ## The 10-to-15-minute route Use menu labels, not a remembered row number: rows appear, disappear and scroll with room state. **Insert** opens the menu, and the Co-op tab is at the end. Follow the displayed keyboard/controller prompts. | Play time | What both testers do | Expected result; what to report | |---|---|---| | 0 to 3 minutes | With no room open, load the copied child checkpoint, save normally and wait until Saving ends. Select **Temporary save** and confirm twice. Wait for restart, load the checkpoint again and check the copy reminder. | The game is now on a native temporary copy. Keep the `fs: mounted` lines showing the new copy root. A failed copy must not be bypassed by accepting on the ordinary save. | | 3 to 6 minutes | Host makes a room; joiner enters its address and secret. Set distinct test names. Compare both displayed fingerprints privately. Host selects **Accept to see and talk**, reads Room rules, goes Back and confirms. Once the membership/terms settle, the joiner does the same. | Before acceptance there should be no remote body or chat. Both should see the same signed maker and terms. A changed offer cancels a pending press; read it again, do not repeatedly confirm blindly. Copy-only presence is itself a new live test. | | 6 to 8 minutes | Meet in the same child area, stay on clear ground and take turns standing, turning and walking a short distance while the other watches. | Look for both complete bodies, normal colours, arms-down standing and visible steps rather than sliding. Note missing bodies, wrong age, stuck poses or labels covering the screen. Remote sword/shield display is not enabled in this minimal configuration. | | 8 to 10 minutes | Try one short chat message each with **T** or **Chat**. Cancel another with Esc. Optionally mute, send one more line, then unmute. If both agree, the maker can change **Friend information** from 1 to **77** and both freshly accept; select **Names and arrows**, then walk briefly out of one another's view in the same map. | Chat should reach only accepted peers and typing/cancelling should not move or attack with Link. Mute is local. Value 77 is child + health + arrows + place; these extra displays are tests, not existing runtime guarantees. Changed signed terms must require a new acceptance. A maker on a copy must first accept see-and-talk before editing rules. | | 10 to 12 minutes | Make one ordinary local change in A's copy, such as an available chest pickup, and let it autosave while B watches its own world. Do not use the practice kit for this check. | A's copy may change; B must not receive progress from copy-only consent. The normal saves must not change. A missing available pickup is not a failure: record that this step was not tried. | | 12 to 13 minutes | Joiner selects **Leave the room** and confirms twice. Watch both views before making any new room. | Remote bodies should disappear and chat should stop. Rejoining is optional; it needs current identity/terms and fresh acceptance where offered. Do not treat a reconnect as automatically accepted. | | 13 to 15 minutes | Both use **Return to normal saves** through the Temporary save row and confirm twice. After restart, check the test-root mount and the original copied checkpoint, then quit normally. | Return must select the disposable normal root without importing copy progress. Keep the changed temporary copy for diagnosis. The original installation's save is outside the test throughout. Save the logs before later launches rotate them away. | If anything unexpected changes the save, the game crashes, or either player cannot tell which save domain is active, stop that session and preserve the logs. Do not delete the copy or try to repair it by importing another save. ## What is not part of this short test - **Fighting is disabled.** Ready and sword swings do not prove a hit or a block. The six proofs observed no committed player health loss during one bounded five-swing interval; that is not playable PvP. - **Progress is not shared unless ordinary-save room terms explicitly allow it and both players accept with a completed backup.** Native temporary copies never share ordinary progress, even if their signed Rules page displays Shared. This guide uses Own categories and copies deliberately. - **Go to friend is unavailable on native temporary copies.** A later, separately agreed test can use disposable normal roots with `coop_warp=1`; only Kokiri Forest and Hyrule Field are supported. Safe actual arrivals are not proven by the six proofs. Do not return to the real save just to make the row available. - Adult bodies, cosmetic sword/shield display and the separately confirmed practice kit need their own tests. The kit supports only the documented child or adult sword/shield pair, not a full item palette; it grants no permission to fight. - No shared enemies/bosses, shared time, save-owner picker, loading a downloaded checkpoint, or returning progress from a temporary copy. The signed-transfer proof used separate local control-API approvals; there is no ordinary save-transfer menu to look for. - The room can describe up to 8 members, but only two simultaneous games were proven here. The short tests do not settle long sessions, packet loss, relay restart, internet reachability or performance. - Some diagnostic prose is shortened or misleading: pending PvP can mention progress; a body-retirement line can call Leave a map change. Use the actual signed Rules page and observed state, and include the suspect line in the report. ## What to send back Send a short result from **each** tester: exact commit/source line, OS, GPU/driver, CPU/RAM, host or joiner, LAN or internet, duration, which route steps worked/failed/not tried, and the first unexpected action. Say whether the normal save stayed unchanged and how you checked. A picture or short video of both views is useful; mask the room secret and unrelated private information. The documented default game log is now **`logs/longshot_log.txt` in the game folder**. The four preceding runs are **`logs/longshot_log.1.txt` through `logs/longshot_log.4.txt`**. Copy/Return restart the process, so retain the rotated files too. A game started from a terminal prints there instead; an explicitly redirected launch keeps its chosen output file and may leave `logs/` empty. Retain that actual output, rather than assuming a missing default file means nothing happened. Preserve output from every restarted process. The README documents these names at https://git.magicbean.dev/mooki/Longshot/src/branch/coop-playtest-r9/README.md. Keep the complete relevant logs, plus these lines around the failure: - The first version/source line and `vk: device` identifying the GPU and driver. - `fs: mounted` lines before Copy, on the temporary copy and after Return. Redact personal path prefixes when posting, but keep enough to distinguish the save domains. - `coop: hosting` / `hosting: players on this network can try ...`, `coop: connecting to the relay`, `the relay answered`, `created`, `joined room`, and any `no room:` refusal. Share network addresses privately when needed, not in a public screenshot. - `coop: sync:` acceptance and changed-offer lines; `coop: players:` creation, hiding/deletion or asset/matching-build refusals. - Every `coop: stats:` line, especially `reliable retries`, `events given up`, `outbox full` and `round trip`. Given-up events include intentional cancellations, so do not label that total packet loss. - `coop: relay lost; rejoining`, `is away`, `is back`, `left room`, and the final shutdown or error lines. - `coop: kit:` only if a separate kit test was explicitly agreed. Chat text is not logged: describe a failed message without expecting to find its contents in the log. `HOS_COOP_TRACE=1` can add packet and body details when a focused follow-up asks for them; it also logs relay addresses and can make a large log. It is not needed for the first attempt. Do not send saves, identity keys, room secrets, game files, generated assets or a whole user folder. Return the report to the WIP line or https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/68 while that remains the collection point.
Author
Member

One continuous 30-minute two-game session on this branch's build (0ec9d0b0), two windowless games on one Linux PC (RTX 4090, NVIDIA 615.78.08), hosted by the game itself, one acceptance each, then standing, short walks towards and away from each other, a chat line every five minutes and idle stretches.

What held for the whole 1,803 seconds:

Check Result
Connection One create, one join, one acceptance per game; no disconnect, rejoin or room change until the planned shutdown
Bodies Both full bodies in both views in every inspected state, from the first picture to the last
Chat All seven lines (minutes 1, 5, 10, 15, 20, 25, 29) visibly delivered in both games, with the friend's name above the body
Counters 32 statistics lines per game, all increasing; 0 retried fragments, 0 given up, 0 refused, 0 outbox-full
Memory Growth from minute 2 to the end: 63.8 MiB and 36.7 MiB (limit set beforehand: 512 MiB); no swap
Frames No interval above one second in either game's frame record (121,856 and 118,215 frames)
Saves Both normal saves and the original test save byte-identical afterwards
Exits Both games exit 0; no driver line in the kernel log
Minute A sent / accepted B sent / accepted Retries A / B RSS MiB A / B
0 1291 / 395 649 / 1584 0 / 0 9577.3 / 9553.0
10 10811 / 10055 10190 / 11222 0 / 0 9589.3 / 9594.7
20 20323 / 19679 19692 / 20851 0 / 0 9599.8 / 9624.8
30 29830 / 29298 29196 / 30477 0 / 0 9629.6 / 9625.5

What the run did not meet: the plan, written before the run, wanted each game's presence read every five seconds. The reader managed 265 paired readings at a median of 6.6 s, with gaps up to 18.9 s while pictures were being taken. That is the test's own shortfall, not something the games did (their frame records kept advancing through those gaps), but it means presence was not observed at the planned rate. By the plan's rule the soak is therefore not an unqualified pass, and it was not run a second time.

Minute 15: A types a line, it appears in both games, both bodies and both names stay in view.
Minute 30: both games still show both bodies.

Not covered by this run: two machines, Windows, a map change, items or shared progress, the save copy and return (shown separately in the six proofs), adult bodies and equipment. It is an ordinary functional run, not a speed or smoothness measurement. The traffic was on one PC over the loopback address.

One continuous 30-minute two-game session on this branch's build (`0ec9d0b0`), two windowless games on one Linux PC (RTX 4090, NVIDIA 615.78.08), hosted by the game itself, one acceptance each, then standing, short walks towards and away from each other, a chat line every five minutes and idle stretches. **What held for the whole 1,803 seconds:** | Check | Result | |---|---| | Connection | One create, one join, one acceptance per game; no disconnect, rejoin or room change until the planned shutdown | | Bodies | Both full bodies in both views in every inspected state, from the first picture to the last | | Chat | All seven lines (minutes 1, 5, 10, 15, 20, 25, 29) visibly delivered in both games, with the friend's name above the body | | Counters | 32 statistics lines per game, all increasing; 0 retried fragments, 0 given up, 0 refused, 0 outbox-full | | Memory | Growth from minute 2 to the end: 63.8 MiB and 36.7 MiB (limit set beforehand: 512 MiB); no swap | | Frames | No interval above one second in either game's frame record (121,856 and 118,215 frames) | | Saves | Both normal saves and the original test save byte-identical afterwards | | Exits | Both games exit 0; no driver line in the kernel log | | Minute | A sent / accepted | B sent / accepted | Retries A / B | RSS MiB A / B | |---:|---:|---:|---:|---:| | 0 | 1291 / 395 | 649 / 1584 | 0 / 0 | 9577.3 / 9553.0 | | 10 | 10811 / 10055 | 10190 / 11222 | 0 / 0 | 9589.3 / 9594.7 | | 20 | 20323 / 19679 | 19692 / 20851 | 0 / 0 | 9599.8 / 9624.8 | | 30 | 29830 / 29298 | 29196 / 30477 | 0 / 0 | 9629.6 / 9625.5 | **What the run did not meet:** the plan, written before the run, wanted each game's presence read every five seconds. The reader managed 265 paired readings at a median of 6.6 s, with gaps up to 18.9 s while pictures were being taken. That is the test's own shortfall, not something the games did (their frame records kept advancing through those gaps), but it means presence was not observed at the planned rate. By the plan's rule the soak is therefore not an unqualified pass, and it was not run a second time. ![Minute 15: A types a line, it appears in both games, both bodies and both names stay in view.](https://git.magicbean.dev/attachments/486a0f15-dbc8-489a-b06f-10be4e8d2dab) ![Minute 30: both games still show both bodies.](https://git.magicbean.dev/attachments/df1c9274-1976-494d-801e-3d6756da4079) **Not covered by this run:** two machines, Windows, a map change, items or shared progress, the save copy and return (shown separately in the six proofs), adult bodies and equipment. It is an ordinary functional run, not a speed or smoothness measurement. The traffic was on one PC over the loopback address.
Author
Member

Two machines: a Linux PC and a Windows 11 PC on a home network. Until now every proof here ran two games on one PC over the loopback address. This is the first session across a real network and across operating systems, on head 0ec9d0b0 on both.

Linux PC Windows 11 PC
Hardware i9-13900K, RTX 4090, NVIDIA 615.78.08 Core Ultra 9 185H, Intel Arc, driver 32.0.101.9175
Build Linux build of 0ec9d0b0 Windows build of 0ec9d0b0, cross-built
Save and identity its own disposable copy of a test save, its own identity its own disposable copy, its own identity

Settings: Co-op mode, every progress category per player, child bodies on, chat on; nothing shared, no fighting, no transfer. The game's own hosting on UDP 24680, home-network addresses. Joining was scripted with a private room secret; consent and Leave went through the real menu rows on each machine.

Windows hosts, Linux joins: works

Step Linux (joiner) Windows (host)
Connection the relay answers at the Windows PC's home-network address, joined hosting on UDP 24680, joined
Signed terms maker fingerprint 1a52-512e-5836-45fd, same rules the same fingerprint and rules
Acceptance once, through the menu once, through the menu
The other's body the Windows player's child body shown, then six walking moves played the Linux player's child body shown, then six walking moves played
Chat its line sent, the Windows line shown the Linux line shown, its line sent
Leave body hidden and removed, 0 failed body hidden and removed, 0 failed, notice "CrossLinux left"
Exit and saves exit 0, all 14 save files unchanged exit 0 after 391 s, all 14 save files unchanged

Last counters of each game:

Linux:   coop: stats: sent 2395, accepted 2368, thrown away 0 (tag 0, sender 0, not co-op 0, replay 0), dropped by the test 0, outbox full 0, reliable retries 0, events given up 0, round trip 1 ms
Windows: coop: stats: sent 2296, accepted 2459, thrown away 0 (tag 0, sender 0, not co-op 0, replay 0), dropped by the test 0, outbox full 0, reliable retries 0, events given up 0, round trip 1 ms

The Linux game logged a round trip of 1 to 4 ms to the Windows host over the session.

The signed, read-only rules on both machines: the same maker fingerprint and the same per-player categories. Left Linux, right the Windows window.

After both accepted: each machine shows the other player as a complete, normally coloured child body.

Walking crosses the wire both ways: the Windows player mid-step on Linux, the Linux player mid-step on Windows (different moments, from an earlier run of the same session).

One chat line each way, shown on the sender and on the receiver.

After Leave: only the local player remains on each machine; Windows shows the notice that the other left.

Linux hosts, Windows joins: blocked by the host PC's firewall, not by the game

The Linux game hosted and sat in its room alone; the Windows game sent its join packets to the Linux PC's home-network address and showed "Connecting: no answer from the relay; trying again in 15 s". The Linux PC's firewall logged those exact packets as blocked (incoming UDP 24680). Three attempts, the same each time; no firewall was changed for this test. So a host has to allow incoming UDP 24680 from the other player's PC. On Windows the first hosting brings up Windows' own firewall prompt, and allowing it for private networks was enough here.

What each side shows when the host's firewall drops the port: the host alone in its room, the joiner with "no answer from the relay", both playing on locally.

One thing a player cannot tell from that message is which side is at fault; a hint about the host's firewall in that text would save a search.

What this does not cover

  • Short sessions of about six minutes on normal disposable saves. The save-copy route (play on a copy, autosave, Return), the save transfer, kits, fighting and Go to friend were not run on Windows; the six proofs above are Linux only.
  • A scripted join with a private secret, not a person typing the address and the secret.
  • One Windows PC with one GPU, a home network only: no connection across the internet, no other Windows or GPU setup.
  • No claim about speed or smoothness, and nothing about Intel rendering beyond these pictures.
  • The Windows test build shows "Longshot unknown" in the menu's corner because it was cross-built without a version stamp; it is the same commit.
**Two machines: a Linux PC and a Windows 11 PC on a home network.** Until now every proof here ran two games on one PC over the loopback address. This is the first session across a real network and across operating systems, on head `0ec9d0b0` on both. | | Linux PC | Windows 11 PC | |---|---|---| | Hardware | i9-13900K, RTX 4090, NVIDIA 615.78.08 | Core Ultra 9 185H, Intel Arc, driver 32.0.101.9175 | | Build | Linux build of `0ec9d0b0` | Windows build of `0ec9d0b0`, cross-built | | Save and identity | its own disposable copy of a test save, its own identity | its own disposable copy, its own identity | Settings: Co-op mode, every progress category per player, child bodies on, chat on; nothing shared, no fighting, no transfer. The game's own hosting on UDP 24680, home-network addresses. Joining was scripted with a private room secret; consent and Leave went through the real menu rows on each machine. ### Windows hosts, Linux joins: works | Step | Linux (joiner) | Windows (host) | |---|---|---| | Connection | the relay answers at the Windows PC's home-network address, joined | hosting on UDP 24680, joined | | Signed terms | maker fingerprint `1a52-512e-5836-45fd`, same rules | the same fingerprint and rules | | Acceptance | once, through the menu | once, through the menu | | The other's body | the Windows player's child body shown, then six walking moves played | the Linux player's child body shown, then six walking moves played | | Chat | its line sent, the Windows line shown | the Linux line shown, its line sent | | Leave | body hidden and removed, 0 failed | body hidden and removed, 0 failed, notice "CrossLinux left" | | Exit and saves | exit 0, all 14 save files unchanged | exit 0 after 391 s, all 14 save files unchanged | Last counters of each game: ```text Linux: coop: stats: sent 2395, accepted 2368, thrown away 0 (tag 0, sender 0, not co-op 0, replay 0), dropped by the test 0, outbox full 0, reliable retries 0, events given up 0, round trip 1 ms Windows: coop: stats: sent 2296, accepted 2459, thrown away 0 (tag 0, sender 0, not co-op 0, replay 0), dropped by the test 0, outbox full 0, reliable retries 0, events given up 0, round trip 1 ms ``` The Linux game logged a round trip of 1 to 4 ms to the Windows host over the session. ![The signed, read-only rules on both machines: the same maker fingerprint and the same per-player categories. Left Linux, right the Windows window.](https://git.magicbean.dev/attachments/08f0649c-c606-4dbc-b22b-16a2e131144d) ![After both accepted: each machine shows the other player as a complete, normally coloured child body.](https://git.magicbean.dev/attachments/97ad6594-9f1e-4965-ad44-562c0e70ba78) ![Walking crosses the wire both ways: the Windows player mid-step on Linux, the Linux player mid-step on Windows (different moments, from an earlier run of the same session).](https://git.magicbean.dev/attachments/1ca742bc-e252-4362-88cc-6cf746cad3a9) ![One chat line each way, shown on the sender and on the receiver.](https://git.magicbean.dev/attachments/298d7068-297d-4252-b0bd-1ba1530d159e) ![After Leave: only the local player remains on each machine; Windows shows the notice that the other left.](https://git.magicbean.dev/attachments/e95c29f8-7164-40c9-9548-5f7bdfbea568) ### Linux hosts, Windows joins: blocked by the host PC's firewall, not by the game The Linux game hosted and sat in its room alone; the Windows game sent its join packets to the Linux PC's home-network address and showed "Connecting: no answer from the relay; trying again in 15 s". The Linux PC's firewall logged those exact packets as blocked (incoming UDP 24680). Three attempts, the same each time; no firewall was changed for this test. So a host has to allow incoming UDP 24680 from the other player's PC. On Windows the first hosting brings up Windows' own firewall prompt, and allowing it for private networks was enough here. ![What each side shows when the host's firewall drops the port: the host alone in its room, the joiner with "no answer from the relay", both playing on locally.](https://git.magicbean.dev/attachments/6deebf94-4070-4535-90f2-27aba7165d6c) One thing a player cannot tell from that message is which side is at fault; a hint about the host's firewall in that text would save a search. ### What this does not cover - Short sessions of about six minutes on normal disposable saves. The save-copy route (play on a copy, autosave, Return), the save transfer, kits, fighting and Go to friend were not run on Windows; the six proofs above are Linux only. - A scripted join with a private secret, not a person typing the address and the secret. - One Windows PC with one GPU, a home network only: no connection across the internet, no other Windows or GPU setup. - No claim about speed or smoothness, and nothing about Intel rendering beyond these pictures. - The Windows test build shows "Longshot unknown" in the menu's corner because it was cross-built without a version stamp; it is the same commit.
mooki force-pushed coop-playtest-r9 from 0ec9d0b024
All checks were successful
Website / Website on Cloudflare (pull_request_target) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 14s
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 8s
to 1ed75e61b5
All checks were successful
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
Third-party parts / As their notes say (pull_request) Successful in 9s
CI / Conventional Commit title (pull_request) Successful in 4s
PR description / Description follows the template (pull_request_target) Successful in 13s
2026-11-14 06:35:21 +00:00
Compare
Author
Member

Updated to 1ed75e61b5bc083247950a262b1a48230f44909a (r10), source c058db8c, replayed onto pull request 68's current head 4a9b9606: #68. The branch name coop-playtest-r9 is historical; both testers should pin the new commit. Its base is main cf605c85b, not today's 0caf06184, which is three commits later.

All 80 line commits remain, and the co-op core is unchanged. Three separate fixture fixes follow the replay:

  • test_menu_feature_rules.py: supply the newer parser's Plus/Minus exclusions.
  • test_menu_text_field.py: combine pad/text callbacks and retain the correct input sections beside chat.
  • test_oot_coop_hits.py: use the newer damage-policy interface and check overkill on both native update paths.

The new head was rebuilt from a fresh game-code translation. The whole suite reports 578 passed, 3 failed, 16 skipped; the failures are the pinned main's test_framegen_resources.py, test_module_ids.py and test_pad_layout.py. All 26 added drivers pass within the 52 added-or-modified drivers, all of which pass. No check was weakened or removed.

All six two-game proofs pass on r10: signed terms and fresh consent; bodies appearing and leaving; reciprocal child standing/walking; native save copy, autosave and Return; signed checkpoint transfer and partial cancellation; five visible sword swings with complete health records and no committed player loss through that observed path. The description now has eight pictures from this build.

The first S03 attempt remains a failed visual proof: other compilation was running, and the same 72-frame walks lasted 1.85 and 2.03 seconds instead of about 1.20 seconds, moving the bodies outside the cameras. With other compilation kept out, the unchanged route passed; no game code, input length or camera was changed, and this is not a controlled performance comparison.

The r10 Windows build links and has reached the title, but has not shown Windows gameplay. The two-machine LAN result and the qualified 30-minute session remain evidence for the previous head 0ec9d0b0 only: #355 (comment) and #355 (comment). No result from the new r10 30-minute session is claimed. This stays WIP; fighting remains disabled, and the updated guide names the new commit, logs/ and cache/ locations and Windows startup limits.

Updated to `1ed75e61b5bc083247950a262b1a48230f44909a` (r10), source `c058db8c`, replayed onto pull request 68's current head `4a9b9606`: https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/68. The branch name `coop-playtest-r9` is historical; both testers should pin the new commit. Its base is main `cf605c85b`, not today's `0caf06184`, which is three commits later. All 80 line commits remain, and the co-op core is unchanged. Three separate fixture fixes follow the replay: - `test_menu_feature_rules.py`: supply the newer parser's Plus/Minus exclusions. - `test_menu_text_field.py`: combine pad/text callbacks and retain the correct input sections beside chat. - `test_oot_coop_hits.py`: use the newer damage-policy interface and check overkill on both native update paths. The new head was rebuilt from a fresh game-code translation. The whole suite reports **578 passed, 3 failed, 16 skipped**; the failures are the pinned main's `test_framegen_resources.py`, `test_module_ids.py` and `test_pad_layout.py`. All 26 added drivers pass within the 52 added-or-modified drivers, all of which pass. No check was weakened or removed. **All six two-game proofs pass on r10:** signed terms and fresh consent; bodies appearing and leaving; reciprocal child standing/walking; native save copy, autosave and Return; signed checkpoint transfer and partial cancellation; five visible sword swings with complete health records and no committed player loss through that observed path. The description now has eight pictures from this build. The first S03 attempt remains a failed visual proof: other compilation was running, and the same 72-frame walks lasted 1.85 and 2.03 seconds instead of about 1.20 seconds, moving the bodies outside the cameras. With other compilation kept out, the unchanged route passed; no game code, input length or camera was changed, and this is not a controlled performance comparison. The r10 Windows build links and has reached the title, but has not shown Windows gameplay. The two-machine LAN result and the qualified 30-minute session remain evidence for **the previous head `0ec9d0b0` only**: https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17499 and https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17281. No result from the new r10 30-minute session is claimed. This stays WIP; fighting remains disabled, and the updated guide names the new commit, `logs/` and `cache/` locations and Windows startup limits.
Author
Member

R10: thirty minutes completed, but body continuity failed

Tested commit 1ed75e61b5bc083247950a262b1a48230f44909a, source c058db8c, with two windowless games on one Linux PC, RTX 4090 and NVIDIA 615.78.08. The game hosted the room over loopback. Each player accepted once, then took short walks, stood idle and sent the seven planned chat lines. Other compilation was kept out.

The completed session is not a pass. Around 18 minutes 14 to 19 seconds, game A logged hiding and deleting B's body, followed by creating it again. B has no corresponding body removal. The room stayed connected and accepted, and no load, travel or Leave was requested.

A's native log says:

coop: puppet: member 2 is hidden (this game stopped being played: a map change or a load)
coop: players: this game stopped being played: a map change or a load: made 1, deleted 1 (1 by this game, 0 gone with their map), 0 still shown, 0 requests failed
coop: players: nobody is shown now: made 1, deleted 1 (1 by this game, 0 gone with their map), 0 still shown, 0 requests failed

It then reports the child body shown again. Both changes fall within one five-second native-clock bracket; the exact missing duration is unknown. Nearby fresh readings still show the same place with no event or loading. The surrounding pictures show both bodies before and after, not the missing instant. The reason for that brief change is not established. The live check missed the actual hide line; reviewing the whole log caught it, so normal exits are not being counted as a pass.

Both attempts remain on record

The first attempt stopped at minute 4 when its combined same-process/zero-swap check failed. The failing value was not saved before the check, so it cannot be recovered. The logs show one native start and one socket generation per game, with no evidence of a process change. Its cause remains undetermined.

I chose the single restart on that undetermined stop, not on a proven test-only failure. Before restarting, I also changed the zero-swap criterion openly: this PC already held about 12 GiB of other programs' pages in compressed zram, with swappiness 150. Swap became a recorded observation rather than a stop by itself, and memory growth became resident plus swapped memory, keeping the predeclared 512 MiB final-growth limit. Process identity remained a separate hard check. All measurements were written before checking them, with the PC's memory and paging counters alongside them.

The restarted games happened to report zero swapped memory in all 31 samples each. That does not explain the first attempt's lost value. No third attempt was made.

Results beside the previous head

The earlier result remains #355 (comment).

Check Previous head 0ec9d0b0 R10 completed restart 1ed75e61
Continuous accepted interval 1,803.5 s 1,800.1 s
Disconnects or rejoins 0 0
Reliable retries / events given up 0 / 0 in both games 0 / 0 in both games
Logged relay round trip 1 ms 1 ms
Growth from minute 2, A / B 63.8 / 36.7 MiB; swap 0 25.9 / 27.3 MiB including reported swap; swap 0
Game-present gaps above one second 0 0
State and room readings 265 pairs; median 6.6 s, maximum 18.9 s 361 pairs; median 5.000000 s, maximum 5.000186 s
Native body continuity No in-session removal in either game's log One A-side hide/delete/recreate; fails the rule
Chat All seven lines shown in both games All seven lines shown in both games
Normal saves and original test save Byte-identical afterward Byte-identical afterward
Exits / kernel driver lines Both 0 / none Both 0 / none

The first r10 attempt separately had four gaps above one second during startup/entry, none in its reached accepted interval. It must not be confused with the completed restart's zero gaps. The different memory values are observations from individual runs, not a controlled claim that r10 is faster or uses less memory.

The five-second schedule is now met, including during walking and pictures. Its local-state and room readings cannot, by themselves, prove the remote body exists between them; the native lifetime records remain necessary.

Not covered: two PCs, Windows, internet traffic, physical input, map changes, items or shared progress, save copy/Return, transfers, adult equipment or fighting. Copy/Return belongs to the separate six-proof result. This remains an ordinary functional test, not a speed, smoothness or general leak-free claim.

The cause of that one hide and recreate is being looked for in the code that decides a game has stopped being played; the result follows here.

## R10: thirty minutes completed, but body continuity failed Tested commit `1ed75e61b5bc083247950a262b1a48230f44909a`, source `c058db8c`, with two windowless games on one Linux PC, RTX 4090 and NVIDIA 615.78.08. The game hosted the room over loopback. Each player accepted once, then took short walks, stood idle and sent the seven planned chat lines. Other compilation was kept out. **The completed session is not a pass.** Around 18 minutes 14 to 19 seconds, game A logged hiding and deleting B's body, followed by creating it again. B has no corresponding body removal. The room stayed connected and accepted, and no load, travel or Leave was requested. A's native log says: ```text coop: puppet: member 2 is hidden (this game stopped being played: a map change or a load) coop: players: this game stopped being played: a map change or a load: made 1, deleted 1 (1 by this game, 0 gone with their map), 0 still shown, 0 requests failed coop: players: nobody is shown now: made 1, deleted 1 (1 by this game, 0 gone with their map), 0 still shown, 0 requests failed ``` It then reports the child body shown again. Both changes fall within one five-second native-clock bracket; the exact missing duration is unknown. Nearby fresh readings still show the same place with no event or loading. The surrounding pictures show both bodies before and after, not the missing instant. The reason for that brief change is not established. The live check missed the actual hide line; reviewing the whole log caught it, so normal exits are not being counted as a pass. ### Both attempts remain on record The first attempt stopped at minute 4 when its combined same-process/zero-swap check failed. The failing value was not saved before the check, so it cannot be recovered. The logs show one native start and one socket generation per game, with no evidence of a process change. Its cause remains undetermined. **I chose the single restart on that undetermined stop, not on a proven test-only failure.** Before restarting, I also changed the zero-swap criterion openly: this PC already held about 12 GiB of other programs' pages in compressed zram, with swappiness 150. Swap became a recorded observation rather than a stop by itself, and memory growth became **resident plus swapped memory**, keeping the predeclared 512 MiB final-growth limit. Process identity remained a separate hard check. All measurements were written before checking them, with the PC's memory and paging counters alongside them. The restarted games happened to report **zero swapped memory in all 31 samples each**. That does not explain the first attempt's lost value. No third attempt was made. ### Results beside the previous head The earlier result remains https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17281. | Check | Previous head `0ec9d0b0` | R10 completed restart `1ed75e61` | |---|---|---| | Continuous accepted interval | 1,803.5 s | 1,800.1 s | | Disconnects or rejoins | 0 | 0 | | Reliable retries / events given up | 0 / 0 in both games | 0 / 0 in both games | | Logged relay round trip | 1 ms | 1 ms | | Growth from minute 2, A / B | 63.8 / 36.7 MiB; swap 0 | 25.9 / 27.3 MiB including reported swap; swap 0 | | Game-present gaps above one second | 0 | 0 | | State and room readings | 265 pairs; median 6.6 s, maximum 18.9 s | 361 pairs; median 5.000000 s, maximum 5.000186 s | | Native body continuity | No in-session removal in either game's log | One A-side hide/delete/recreate; fails the rule | | Chat | All seven lines shown in both games | All seven lines shown in both games | | Normal saves and original test save | Byte-identical afterward | Byte-identical afterward | | Exits / kernel driver lines | Both 0 / none | Both 0 / none | The first r10 attempt separately had four gaps above one second during startup/entry, none in its reached accepted interval. It must not be confused with the completed restart's zero gaps. The different memory values are observations from individual runs, not a controlled claim that r10 is faster or uses less memory. The five-second schedule is now met, including during walking and pictures. Its local-state and room readings cannot, by themselves, prove the remote body exists between them; the native lifetime records remain necessary. **Not covered:** two PCs, Windows, internet traffic, physical input, map changes, items or shared progress, save copy/Return, transfers, adult equipment or fighting. Copy/Return belongs to the separate six-proof result. This remains an ordinary functional test, not a speed, smoothness or general leak-free claim. The cause of that one hide and recreate is being looked for in the code that decides a game has stopped being played; the result follows here.
Contributor

Windows results at r10 1ed75e61 (source as checked out, not a cross-build), for the "Windows" lines under Needs other testers. One PC, so no two-PC session from here.

Hardware: Windows 11 Pro 26300, i9-12900K, 64 GB, RTX 3080 10 GB (NVIDIA 621.14). Toolchain: llvm-mingw from the Longshot Builder's tools, Python 3.12.

1. The game-free suite, run natively on Windows (python -I -B tests/run_all.py, 24 at once): 556 passed, 6 failed, 35 skipped. Main 0caf0618 on the same PC, same run: 521 passed, 6 failed, 34 skipped. The 6 failures are the same tests on both, so this line adds none, and the 35 extra tests it brings all pass on Windows, including the co-op fixtures that compile and run here:

fails on both why (on main too)
test_pad_layout.py HoldToggle undeclared in the fixture since #137 (#331 fixes it)
test_release.py openssl not on the PATH (#144 has run_all find Git's); passes with it on the PATH
test_tested_configurations.py \r\n from the CLI's ASCII pipe on Windows
test_oot_state_tap.py no game listens on longshot-oot-test-<n> (Invalid argument)
test_module_ids.py, test_framegen_resources.py fail on main 0caf0618 on Windows too; not looked into

2. A native Windows build: nx2/lift.py --standalone --write and nx2/build.py --standalone at 1ed75e61, exit 0 in 610 s, no build error.

3. Two games on one Windows PC, each in a window that never took focus, with their own game folder (B a full copy of A's) and their own user folder. Each user folder had a copy of one played save (adult Link, Desert Colossus). Relay: scripts/coop_relay.py --host 127.0.0.1 --port 0, loopback only, so Windows Firewall's prompt and the game's own hosting were not tested. A: HOS_COOP_AUTO=create, coop_presence_permissions=3 in its longshot_options.ini; B: HOS_COOP_AUTO=join; both HOS_COOP_PLAYERS=1, HOS_COOP_ACCEPT=always, the same 18-character secret. The script loaded both saves, walked B forward 90 frames, then A, waited, quit B, then quit A.

  • Room: A room RSY… created (member 1), B joined room RSY… as member 2. Relay: WinA joined as member 1, WinB joined as member 2, then WinB (member 2) left (1/8) and WinA (member 1) left (0/8). A logged WinB (member 2) left the room, and both logged left room (the game is closing).
  • Consent and the save: each logged room accepted (HOS_COOP_ACCEPT=always); the save is copied first, then made coop-backup\first-20261114-0846xxZ before applying anything. the flag table reads as the game's getter does (28 flags checked). A kept its signing key under userA\coop-keys.
  • Normal saves unchanged: SHA-256 of all 14 files in each save\ before and after: identical for A and for B. The user folders gained only coop-backup, coop-keys (A) and temp.
  • Bodies: each game showed the other as PouchPlayer_Adult at the other's position (member 2 is shown as PouchPlayer_Adult at -4.01 100.13 0.06 (made in 104 ms)). A person watching both windows says each could see the other's Link. B was attacked by the Colossus enemies, which A doesn't show (expected: only players are shared), but A did show B being knocked back by the hits.
  • Counters: thrown away 0, reliable retries 0, events given up 0, round trip 1 ms, on both. No CRASH, no device loss; both exited on quit.
  • Animation: players: locally built animation assets missing or incomplete → standing models, in this dev build (no Builder asset step); so no walk animation was checked.

One thing to look at: the other body was shown and hidden repeatedly while both games stayed in Desert Colossus and in play (no control: state change at those points). In about 2.5 minutes, A logged 7 pairs of member 2 is shown as PouchPlayer_Adult ... / member 2 is hidden (this game stopped being played: a map change or a load). B logged member 1 is hidden (in no map) in step with them, made 7 times. So each game seems to decide every so often that it isn't being played, and tells the other "no map". Each body was re-made each time (34-1285 ms). I couldn't tie it to a cause from the logs. It might be the enemy hits and knockback (B was being attacked), or the windows being clicked by the person watching, since the log shows the foreground switching between them. But there's no line that says so.

Not done here: native Copy/Return (Temporary save) and the junction refusal on a real NTFS junction in the game. Both need the Co-op tab's two presses, which I can drive only by timed menu keys, so I'd do that as a separate run if it's still wanted. Also not done: hosting through the game, the firewall prompt, more than two players, and two PCs. Shots stay on this PC (game imagery), so none are attached.

Windows results at r10 **1ed75e61** (source as checked out, not a cross-build), for the "Windows" lines under Needs other testers. One PC, so no two-PC session from here. Hardware: Windows 11 Pro 26300, i9-12900K, 64 GB, RTX 3080 10 GB (NVIDIA 621.14). Toolchain: llvm-mingw from the Longshot Builder's tools, Python 3.12. **1. The game-free suite, run natively on Windows** (`python -I -B tests/run_all.py`, 24 at once): **556 passed, 6 failed, 35 skipped**. Main 0caf0618 on the same PC, same run: 521 passed, 6 failed, 34 skipped. The 6 failures are the same tests on both, so this line adds none, and the 35 extra tests it brings all pass on Windows, including the co-op fixtures that compile and run here: | fails on both | why (on main too) | |---|---| | `test_pad_layout.py` | `HoldToggle` undeclared in the fixture since #137 (#331 fixes it) | | `test_release.py` | `openssl` not on the PATH (#144 has run_all find Git's); passes with it on the PATH | | `test_tested_configurations.py` | `\r\n` from the CLI's ASCII pipe on Windows | | `test_oot_state_tap.py` | `no game listens on longshot-oot-test-<n> (Invalid argument)` | | `test_module_ids.py`, `test_framegen_resources.py` | fail on main 0caf0618 on Windows too; not looked into | **2. A native Windows build:** `nx2/lift.py --standalone --write` and `nx2/build.py --standalone` at 1ed75e61, exit 0 in 610 s, no build error. **3. Two games on one Windows PC**, each in a window that never took focus, with their own game folder (B a full copy of A's) and their own user folder. Each user folder had a copy of one played save (adult Link, Desert Colossus). Relay: `scripts/coop_relay.py --host 127.0.0.1 --port 0`, loopback only, so Windows Firewall's prompt and the game's own hosting were **not** tested. A: `HOS_COOP_AUTO=create`, `coop_presence_permissions=3` in its `longshot_options.ini`; B: `HOS_COOP_AUTO=join`; both `HOS_COOP_PLAYERS=1`, `HOS_COOP_ACCEPT=always`, the same 18-character secret. The script loaded both saves, walked B forward 90 frames, then A, waited, quit B, then quit A. - **Room:** A `room RSY… created (member 1)`, B `joined room RSY… as member 2`. Relay: `WinA joined as member 1`, `WinB joined as member 2`, then `WinB (member 2) left (1/8)` and `WinA (member 1) left (0/8)`. A logged `WinB (member 2) left the room`, and both logged `left room (the game is closing)`. - **Consent and the save:** each logged `room accepted (HOS_COOP_ACCEPT=always); the save is copied first`, then made `coop-backup\first-20261114-0846xxZ` before applying anything. `the flag table reads as the game's getter does (28 flags checked)`. A kept its signing key under `userA\coop-keys`. - **Normal saves unchanged:** SHA-256 of all 14 files in each `save\` before and after: identical for A and for B. The user folders gained only `coop-backup`, `coop-keys` (A) and `temp`. - **Bodies:** each game showed the other as `PouchPlayer_Adult` at the other's position (`member 2 is shown as PouchPlayer_Adult at -4.01 100.13 0.06 (made in 104 ms)`). A person watching both windows says each could see the other's Link. B was attacked by the Colossus enemies, which A doesn't show (expected: only players are shared), but A did show B being knocked back by the hits. - **Counters:** `thrown away 0`, `reliable retries 0`, `events given up 0`, round trip 1 ms, on both. No `CRASH`, no device loss; both exited on `quit`. - **Animation:** `players: locally built animation assets missing or incomplete` → standing models, in this dev build (no Builder asset step); so no walk animation was checked. **One thing to look at: the other body was shown and hidden repeatedly** while both games stayed in Desert Colossus and in play (no `control: state` change at those points). In about 2.5 minutes, A logged 7 pairs of `member 2 is shown as PouchPlayer_Adult ...` / `member 2 is hidden (this game stopped being played: a map change or a load)`. B logged `member 1 is hidden (in no map)` in step with them, made 7 times. So each game seems to decide every so often that it isn't being played, and tells the other "no map". Each body was re-made each time (34-1285 ms). I couldn't tie it to a cause from the logs. It might be the enemy hits and knockback (B was being attacked), or the windows being clicked by the person watching, since the log shows the foreground switching between them. But there's no line that says so. Not done here: native Copy/Return (Temporary save) and the junction refusal on a real NTFS junction in the game. Both need the Co-op tab's two presses, which I can drive only by timed menu keys, so I'd do that as a separate run if it's still wanted. Also not done: hosting through the game, the firewall prompt, more than two players, and two PCs. Shots stay on this PC (game imagery), so none are attached.
Author
Member

Thank you, this is exactly what the Windows lines were missing: the suite natively on Windows with the line's fixtures compiling and running there, a native build, and two games sharing a room with saves unchanged.

On the body being shown and hidden repeatedly: that needs explaining, and it is not what the guide or the proofs lead anyone to expect.

  • A 30-minute session here on the same head had one such hide and recreate (#355 (comment)). Its cause is a race in this line: a periodic roster update could briefly look like revoked permission. A fix for that is built and being proven now; the head will move when it passes.
  • Yours looks different in one way that matters. In that session only the viewing game logged a hide and the other game logged nothing. In yours B logs member 1 is hidden (in no map) in step with A, so game A itself reported that it was not in play, seven times in two and a half minutes. Two games in windows on one PC, one of them always in the background, and one player being hit, are both things the Linux proofs never had.

The code that makes that decision is being read against your description now. Three things from your run would narrow it, if the logs are still there:

  1. From A's log, the lines from two seconds before to two seconds after one member 2 is hidden line, and the same stretch of B's log.
  2. Whether the hide times line up with clicking from one window to the other, or with B being hit.
  3. Whether the game in the background kept its frame rate (the FPS counter in its corner) or dropped or froze while the other was in front.

Copy/Return and the junction refusal on Windows are still wanted, but they can wait for the next head so they are not spent on a commit that is about to be replaced. Your suite and build results stand for 1ed75e61 as posted.

Thank you, this is exactly what the Windows lines were missing: the suite natively on Windows with the line's fixtures compiling and running there, a native build, and two games sharing a room with saves unchanged. **On the body being shown and hidden repeatedly:** that needs explaining, and it is not what the guide or the proofs lead anyone to expect. - A 30-minute session here on the same head had one such hide and recreate (https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17694). Its cause is a race in this line: a periodic roster update could briefly look like revoked permission. A fix for that is built and being proven now; the head will move when it passes. - Yours looks different in one way that matters. In that session only the viewing game logged a hide and the other game logged nothing. In yours B logs `member 1 is hidden (in no map)` in step with A, so game A itself reported that it was not in play, seven times in two and a half minutes. Two games in windows on one PC, one of them always in the background, and one player being hit, are both things the Linux proofs never had. The code that makes that decision is being read against your description now. Three things from your run would narrow it, if the logs are still there: 1. From A's log, the lines from two seconds before to two seconds after one `member 2 is hidden` line, and the same stretch of B's log. 2. Whether the hide times line up with clicking from one window to the other, or with B being hit. 3. Whether the game in the background kept its frame rate (the FPS counter in its corner) or dropped or froze while the other was in front. Copy/Return and the junction refusal on Windows are still wanted, but they can wait for the next head so they are not spent on a commit that is about to be replaced. Your suite and build results stand for `1ed75e61` as posted.
Contributor

The logs are still here. Short version: every hide came during a frame-rate collapse, which two games sharing one 10 GB RTX 3080 caused. They were over the driver's video-memory allowance. Window switching doesn't line up.

3. Frame rate. A's vi: ... fps over the last 5 s lines against its hides (log line numbers):

2565  52.3 fps
2578  vk: over the driver's video-memory allowance (4.5 of 4.4 GB)
2672   9.9 fps      2722  17.7 fps      2743   8.2 fps
2745  member 2 is hidden (this game stopped being played: a map change or a load)
2748  vk: over the driver's video-memory allowance (4.3 of 4.1 GB)
2760   2.4 fps over 6.4 s
2768  hidden        2772   3.1 fps
2782  hidden        2786   4.4 fps      2805   1.2 fps over 6.5 s      2810   1.6 fps
2820  hidden        2847   5.4 fps
2942  37.0 fps
2951  hidden        2954  vk: over the driver's video-memory allowance (4.3 of 3.8 GB)
2970   4.8 fps
2981  hidden        2988   6.4 fps      3003   8.0 fps      3053  20.6     3099  21.3
3119  vk: over the driver's video-memory allowance (4.3 of 4.3 GB)
3149  22.6 fps
3153  hidden

There's no hide in A's stretches at 37-52 fps. B ran at 17-60 fps. Its own single member 1 is hidden (this game stopped being played ...) (line 2662) came as it fell from 16.4 to 9.1 fps. Its member 1 is hidden (in no map) lines are A's reports arriving. Around A's first hide, the capture path was stalling too: vk: a capture's wait for write-back 24013 timed out repeated, nvn: GPU waits in 5 s: 33 sync waits (1447 ms).

So my reading: A's game thread went seconds between frames, the "being played" check took that for a load, and B was told "in no map". It's an artefact of this test setup, two games on one GPU over its memory budget. But it would also show for a real player whose game stalls for a second or two (a slow PC, a shader compile, a hitch), so the check may want a longer grace before it reports a load.

1. The stretch around the first hide. The above is A's, with the renderer's own lines left out. Untrimmed, A from 4 lines before to 4 after:

[hos Presentation Thread] vi: 3418 frames presented (8.2 fps over the last 5.1 s, interval 1)
[hos Presentation Thread] vi: present intervals: <15.5 ms 0, 15.5-17.8 0, 17.8-22 1, 22-30 3, 30-40 4, over 40 34
[hos BaseProcExecutor0] coop: puppet: member 2 is hidden (this game stopped being played: a map change or a load)
[hos BaseProcExecutor0] coop: players: this game stopped being played: a map change or a load: made 1, deleted 1 (1 by this game, 0 gone with their map), 0 still shown, 0 requests failed
[hos Presentation Thread] vk: over the driver's video-memory allowance (4.3 of 4.1 GB)
...
[hos sead::MainThread] vk: a capture's wait for write-back 24013 timed out (done 24009)
[hos sead::MainThread] nvn: GPU waits in 5 s: 10 sync waits (1298 ms), 0 polls timed out (0 ms), 0 queue finishes (0 ms)
[hos Presentation Thread] vi: 3433 frames presented (2.4 fps over the last 6.4 s, interval 1)

B at the same time logged member 1 is hidden (in no map) next to vi: ... 47.4 fps.

2. Window switches and hits. Window switches don't line up: A's foreground changes are at lines 1156, 1750, 1910, 1979, 2824 and 2938, and only the last is near a hide (2951, 13 lines later). B was being hit in that stretch, but the log has no line for a hit (heart protection was on), so I can't say which hits were when.

I'll do Copy/Return and the junction refusal on your next head. For the bodies I'll run it with one game on the RTX 3080 and the other on the iGPU, so neither runs out of video memory.

The logs are still here. Short version: **every hide came during a frame-rate collapse**, which two games sharing one 10 GB RTX 3080 caused. They were over the driver's video-memory allowance. Window switching doesn't line up. **3. Frame rate.** A's `vi: ... fps over the last 5 s` lines against its hides (log line numbers): ``` 2565 52.3 fps 2578 vk: over the driver's video-memory allowance (4.5 of 4.4 GB) 2672 9.9 fps 2722 17.7 fps 2743 8.2 fps 2745 member 2 is hidden (this game stopped being played: a map change or a load) 2748 vk: over the driver's video-memory allowance (4.3 of 4.1 GB) 2760 2.4 fps over 6.4 s 2768 hidden 2772 3.1 fps 2782 hidden 2786 4.4 fps 2805 1.2 fps over 6.5 s 2810 1.6 fps 2820 hidden 2847 5.4 fps 2942 37.0 fps 2951 hidden 2954 vk: over the driver's video-memory allowance (4.3 of 3.8 GB) 2970 4.8 fps 2981 hidden 2988 6.4 fps 3003 8.0 fps 3053 20.6 3099 21.3 3119 vk: over the driver's video-memory allowance (4.3 of 4.3 GB) 3149 22.6 fps 3153 hidden ``` There's no hide in A's stretches at 37-52 fps. B ran at 17-60 fps. Its own single `member 1 is hidden (this game stopped being played ...)` (line 2662) came as it fell from 16.4 to 9.1 fps. Its `member 1 is hidden (in no map)` lines are A's reports arriving. Around A's first hide, the capture path was stalling too: `vk: a capture's wait for write-back 24013 timed out` repeated, `nvn: GPU waits in 5 s: 33 sync waits (1447 ms)`. So my reading: A's game thread went seconds between frames, the "being played" check took that for a load, and B was told "in no map". It's an artefact of this test setup, two games on one GPU over its memory budget. But it would also show for a real player whose game stalls for a second or two (a slow PC, a shader compile, a hitch), so the check may want a longer grace before it reports a load. **1. The stretch around the first hide.** The above is A's, with the renderer's own lines left out. Untrimmed, A from 4 lines before to 4 after: ``` [hos Presentation Thread] vi: 3418 frames presented (8.2 fps over the last 5.1 s, interval 1) [hos Presentation Thread] vi: present intervals: <15.5 ms 0, 15.5-17.8 0, 17.8-22 1, 22-30 3, 30-40 4, over 40 34 [hos BaseProcExecutor0] coop: puppet: member 2 is hidden (this game stopped being played: a map change or a load) [hos BaseProcExecutor0] coop: players: this game stopped being played: a map change or a load: made 1, deleted 1 (1 by this game, 0 gone with their map), 0 still shown, 0 requests failed [hos Presentation Thread] vk: over the driver's video-memory allowance (4.3 of 4.1 GB) ... [hos sead::MainThread] vk: a capture's wait for write-back 24013 timed out (done 24009) [hos sead::MainThread] nvn: GPU waits in 5 s: 10 sync waits (1298 ms), 0 polls timed out (0 ms), 0 queue finishes (0 ms) [hos Presentation Thread] vi: 3433 frames presented (2.4 fps over the last 6.4 s, interval 1) ``` B at the same time logged `member 1 is hidden (in no map)` next to `vi: ... 47.4 fps`. **2. Window switches and hits.** Window switches don't line up: A's foreground changes are at lines 1156, 1750, 1910, 1979, 2824 and 2938, and only the last is near a hide (2951, 13 lines later). B was being hit in that stretch, but the log has no line for a hit (heart protection was on), so I can't say which hits were when. I'll do Copy/Return and the junction refusal on your next head. For the bodies I'll run it with one game on the RTX 3080 and the other on the iGPU, so neither runs out of video memory.
Author
Member

Follow-up on the repeated show and hide, after reading the code against your description.

What decides it. A game stops counting as played, removes the bodies it shows and tells the others it is in no map, when one of these fails: a save is loaded, the player object is valid and in the world, no load or transition flag is set, and the game's own play-time counter has ticked normally within the last 2.5 seconds. A change of that counter other than a small step forward is taken for another save having been loaded. Separately, a game that has not managed to read its own player's pose for 2.5 seconds also reports itself as not in the world. After a removal there is a 2-second wait before the body is asked for again. Being hit has no branch of its own in any of this, and neither does window focus: the game is always told it is in focus, and losing focus only resets input.

What that suggests, not established: anything that holds one game still for a moment could trip the counter rule or the 2.5-second rule. On Windows, pressing on a window's title bar or border blocks that window's thread until the button is released, and a covered window can be held back by the compositor. That would fit clicking between two windows; it is a candidate, not a finding. The fix that is being proven now is for a different fault and does not touch these rules.

For comparison: in the two-PC session on the earlier head, the Windows game ran in its own real window for about three minutes and each game's complete log has exactly one show and one hide (the Leave). That was one game per PC, never covered or clicked.

If you run it again, HOS_COOP_TRACE=1 on both games makes the reason visible. The lines to look for around the first hide and the recovery:

coop: sync: game paused ... / coop: sync: game running ...
coop: sync: Link ... the world
hid: game window in front / not in front

and whether each hide follows a click on a title bar or a switch of the front window. A minute with nobody touching either window, then a minute of deliberate switching, would separate the two.

Here, the same is being tried on Linux by stopping one game for a fraction of a second, one second and three seconds with the trace on; the result follows.

Follow-up on the repeated show and hide, after reading the code against your description. **What decides it.** A game stops counting as played, removes the bodies it shows and tells the others it is in no map, when one of these fails: a save is loaded, the player object is valid and in the world, no load or transition flag is set, and the game's own play-time counter has ticked normally within the last 2.5 seconds. A change of that counter other than a small step forward is taken for another save having been loaded. Separately, a game that has not managed to read its own player's pose for 2.5 seconds also reports itself as not in the world. After a removal there is a 2-second wait before the body is asked for again. Being hit has no branch of its own in any of this, and neither does window focus: the game is always told it is in focus, and losing focus only resets input. **What that suggests, not established:** anything that holds one game still for a moment could trip the counter rule or the 2.5-second rule. On Windows, pressing on a window's title bar or border blocks that window's thread until the button is released, and a covered window can be held back by the compositor. That would fit clicking between two windows; it is a candidate, not a finding. The fix that is being proven now is for a different fault and does not touch these rules. **For comparison:** in the two-PC session on the earlier head, the Windows game ran in its own real window for about three minutes and each game's complete log has exactly one show and one hide (the Leave). That was one game per PC, never covered or clicked. **If you run it again,** `HOS_COOP_TRACE=1` on both games makes the reason visible. The lines to look for around the first hide and the recovery: ```text coop: sync: game paused ... / coop: sync: game running ... coop: sync: Link ... the world hid: game window in front / not in front ``` and whether each hide follows a click on a title bar or a switch of the front window. A minute with nobody touching either window, then a minute of deliberate switching, would separate the two. Here, the same is being tried on Linux by stopping one game for a fraction of a second, one second and three seconds with the trace on; the result follows.
Contributor

That fits the 2.5-second rule: in my comment above (frame rates against the hides), game A ran at 1.2-8 fps over 5-6.5 s windows at every hide, with present intervals almost all over 40 ms (34 of 42 in the window before the first hide), on one RTX 3080 shared by both games over the driver's video-memory allowance. So a counter stalled for over 2.5 s explains all seven, with no window clicks needed. A real player with a hitch that long would see the same, so a longer grace or a frame-count-based rule might be worth a look. I'll redo the bodies with one game on the iGPU so neither starves.

That fits the 2.5-second rule: in my comment above (frame rates against the hides), game A ran at 1.2-8 fps over 5-6.5 s windows at every hide, with present intervals almost all over 40 ms (34 of 42 in the window before the first hide), on one RTX 3080 shared by both games over the driver's video-memory allowance. So a counter stalled for over 2.5 s explains all seven, with no window clicks needed. A real player with a hitch that long would see the same, so a longer grace or a frame-count-based rule might be worth a look. I'll redo the bodies with one game on the iGPU so neither starves.
Author
Member

That settles it better than anything here could: every hide sits in a stretch at 1 to 8 frames a second, none in the stretches at 37 to 52, and the window switching does not line up. Thank you for going back through the logs.

So this is the rule working as written on a game that was starved, and you are right that the rule is too quick for a real hitch: a slow PC, a long shader compile or a memory squeeze would make the other player vanish, and after a removal the body is not asked for again for two seconds. It is being tried here directly, by stopping one game for 0.3, 1 and 3 seconds with the trace on, to see which lengths remove a body and by which of the two rules (the 2.5-second one, or a jump of the play counter being taken for a load). What to do about it (a longer grace before a body is removed, or judging by frames instead of seconds as you suggest, and not treating a jump after a stall as another save) will be proposed here with that result. It is not in the head that is about to be pushed: that one, 2f01e1b4, fixes the race from the 30-minute session and has passed the six proofs.

One game on the iGPU so neither starves is a good next run; the bodies with walking animation would need the animation assets built, which your development build did not have.

That settles it better than anything here could: every hide sits in a stretch at 1 to 8 frames a second, none in the stretches at 37 to 52, and the window switching does not line up. Thank you for going back through the logs. So this is the rule working as written on a game that was starved, and you are right that the rule is too quick for a real hitch: a slow PC, a long shader compile or a memory squeeze would make the other player vanish, and after a removal the body is not asked for again for two seconds. It is being tried here directly, by stopping one game for 0.3, 1 and 3 seconds with the trace on, to see which lengths remove a body and by which of the two rules (the 2.5-second one, or a jump of the play counter being taken for a load). What to do about it (a longer grace before a body is removed, or judging by frames instead of seconds as you suggest, and not treating a jump after a stall as another save) will be proposed here with that result. It is not in the head that is about to be pushed: that one, `2f01e1b4`, fixes the race from the 30-minute session and has passed the six proofs. One game on the iGPU so neither starves is a good next run; the bodies with walking animation would need the animation assets built, which your development build did not have.
Author
Member

Updated to 2f01e1b4bdb6e1629c54a998f0403519fe4e5c74 (r12), Linux source 05329d46. The fork branch still has its historical name coop-playtest-r9. The base remains pull request 68 at 4a9b9606, on main cf605c85b, three commits behind 0caf06184.

The failed 30-minute session on r10 led to two presence fixes and the completed switch reference:

  • 2be4e9233: an unchanged roster heartbeat no longer looks like a changed room view that briefly withdraws permission to show the other player.
  • 15a68a5eb: the missing switch-reference entries, defaults and test/development limits are documented.
  • 2f01e1b4b: a joined room view stays pending until the native engine exists and actually receives it.

The intermediate head 15a68a5e failed its first two-game proof before signed-room readiness and was never pushed. Removing the repeated heartbeat exposed a second fault: the first room view had been marked handled before the native engine existed, and the old heartbeat had been retrying it by accident.

Both new regressions pass with GCC and Clang; the real join-to-accepted-room test fails on that intermediate head. On r12 the whole suite reports 580 passed, 3 failed, 16 skipped: the failures remain the pinned main's test_framegen_resources.py, test_module_ids.py and test_pad_layout.py.

All six short two-game proofs pass at the first attempt on r12: signed terms/consent, body consent and Leave, reciprocal child poses, native Copy/autosave/Return, signed transfer/partial cancellation, and the bounded five-swing health-observer test. Both complete logs were also scanned in every proof for unexpected body hiding, deletion or recreation; none occurred. The eight existing pictures remain labelled r10 illustrations; fresh r12 captures showed the same views.

Linux builds and runs. The exact r12 Windows executable and renderer cross-build and link, but have not run on Windows; no r12 two-machine or 30-minute result is claimed. The qualified 30-minute session and LAN result remain at 0ec9d0b0; the failed later 30-minute session and native Windows tester's results remain at 1ed75e61. The repeated show and hide the Windows tester found is a separate matter and is not repaired here: the tester's logs show every hide during a frame-rate collapse (#355 (comment)), which is the play check's 2.5-second rule working as written and being too quick for a real hitch. What to change there is still to be decided.

Testers: pin 2f01e1b4; results already posted for 1ed75e61 stand for that commit. This stays WIP and fighting remains disabled.

Updated to **`2f01e1b4bdb6e1629c54a998f0403519fe4e5c74` (r12)**, Linux source `05329d46`. The fork branch still has its historical name `coop-playtest-r9`. The base remains pull request 68 at `4a9b9606`, on main `cf605c85b`, three commits behind `0caf06184`. The failed 30-minute session on r10 led to two presence fixes and the completed switch reference: - `2be4e9233`: an unchanged roster heartbeat no longer looks like a changed room view that briefly withdraws permission to show the other player. - `15a68a5eb`: the missing switch-reference entries, defaults and test/development limits are documented. - `2f01e1b4b`: a joined room view stays pending until the native engine exists and actually receives it. The intermediate head `15a68a5e` failed its first two-game proof before signed-room readiness and was never pushed. Removing the repeated heartbeat exposed a second fault: the first room view had been marked handled before the native engine existed, and the old heartbeat had been retrying it by accident. Both new regressions pass with GCC and Clang; the real join-to-accepted-room test fails on that intermediate head. On r12 the whole suite reports **580 passed, 3 failed, 16 skipped**: the failures remain the pinned main's `test_framegen_resources.py`, `test_module_ids.py` and `test_pad_layout.py`. **All six short two-game proofs pass at the first attempt on r12:** signed terms/consent, body consent and Leave, reciprocal child poses, native Copy/autosave/Return, signed transfer/partial cancellation, and the bounded five-swing health-observer test. Both complete logs were also scanned in every proof for unexpected body hiding, deletion or recreation; none occurred. The eight existing pictures remain labelled r10 illustrations; fresh r12 captures showed the same views. Linux builds and runs. The exact r12 Windows executable and renderer cross-build and link, but have not run on Windows; no r12 two-machine or 30-minute result is claimed. The qualified 30-minute session and LAN result remain at `0ec9d0b0`; the failed later 30-minute session and native Windows tester's results remain at `1ed75e61`. The repeated show and hide the Windows tester found is a separate matter and is not repaired here: the tester's logs show every hide during a frame-rate collapse (https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17764), which is the play check's 2.5-second rule working as written and being too quick for a real hitch. What to change there is still to be decided. **Testers:** pin `2f01e1b4`; results already posted for `1ed75e61` stand for that commit. This stays WIP and fighting remains disabled.
Only publish a new session view when the roster fields actually change. Repeated heartbeat publication exposed a newer view version before the consent callback could consume it, allowing the game thread to revoke an unchanged presentation context.

Exercise that exact interleaving with the real session publication and native context code. Both compiler families fail on r9 and r10 and pass with the fix; real roster changes retain the stale-view fence.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Document co-op connection, signed-rule, test and development switches with their defaults and consent limits. Clarify internal restart state and the debug Teleport overrides without changing runtime behavior.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
fix(coop): apply joined views after the native engine starts
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 7s
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
2f01e1b4bd
A view arriving before engine creation was marked consumed without initializing the signed room. Keep it pending until view_changed applies it under the engine lock, so the next session tick recovers it without republishing unchanged roster heartbeats.

Drive real JOINED and ROSTER parsing, queued encrypted loopback messages, signed identity and charter delivery, and explicit two-member acceptance in three startup orders. The new integration fixture fails on r11 and passes here with GCC and Clang; the existing presence-race fixture still passes with both.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Author
Member

The freeze experiment promised above, on head 2f01e1b4 (two windowless games on one Linux PC, RTX 4090, both accepted, standing in sight of each other, trace on). One game's whole process was stopped and released again three times, with more than 42 seconds of untouched play between; nothing else was changed and no input was sent.

Game A stopped for What happened to the bodies
0.300 s nothing: no pause line, no hide, no re-creation on either game
1.000 s nothing
3.000 s both removed once and shown again without any input

In the 3-second case there are two separate reactions, as the code read suggested:

  • The other game, B, hid A as not heard shortly before A was released: it had received nothing from A for 3 seconds.
  • The stopped game, A, on its first frame afterwards logged game paused (its play counter had not ticked for more than 2.5 seconds), removed B's body, logged game running one counter step later, and asked for the body again.
  • Both bodies were back about 2.06 seconds after that, which is the 2-second wait before a removed body is requested again plus building it.
  • Neither log has an in no map line or another save was loaded. So on this PC a clean 3-second stop shows as not heard on the peer; the Windows report's in no map is the same check seen from a game that kept sending while starved.

Before the 3-second stop both games show both bodies; after it each game shows only its own player; a little later both show both again, with no input.

What this means for a player: a hitch of up to a second does nothing. One of about 2.5 to 3 seconds or more makes the other player disappear on both sides and come back roughly two seconds after the game recovers. Nothing is lost and no consent has to be given again, but it looks like the other person left.

Possible changes, to be decided, none made: keep a body through a short not-in-play reading and remove it only if the reading lasts (for example ten seconds), or freeze it in place with a mark instead of removing it; ask for a removed body again at once when play resumes rather than after the 2-second wait; judge liveness by frames as well as seconds, so a game that is slow but still running is not taken for one that stopped. Each of these touches when another player's body may be shown, so it needs the same care as the consent rules.

One observation session, not a proof with a pass or fail; exits 0 on both games; the stop was applied to the game process only.

The freeze experiment promised above, on head `2f01e1b4` (two windowless games on one Linux PC, RTX 4090, both accepted, standing in sight of each other, trace on). One game's whole process was stopped and released again three times, with more than 42 seconds of untouched play between; nothing else was changed and no input was sent. | Game A stopped for | What happened to the bodies | |---|---| | 0.300 s | nothing: no pause line, no hide, no re-creation on either game | | 1.000 s | nothing | | 3.000 s | both removed once and shown again without any input | In the 3-second case there are two separate reactions, as the code read suggested: - **The other game, B,** hid A as `not heard` shortly before A was released: it had received nothing from A for 3 seconds. - **The stopped game, A,** on its first frame afterwards logged `game paused` (its play counter had not ticked for more than 2.5 seconds), removed B's body, logged `game running` one counter step later, and asked for the body again. - Both bodies were back about 2.06 seconds after that, which is the 2-second wait before a removed body is requested again plus building it. - Neither log has an `in no map` line or `another save was loaded`. So on this PC a clean 3-second stop shows as `not heard` on the peer; the Windows report's `in no map` is the same check seen from a game that kept sending while starved. ![Before the 3-second stop both games show both bodies; after it each game shows only its own player; a little later both show both again, with no input.](https://git.magicbean.dev/attachments/3d833bac-b22c-4926-980c-e3d8511e96f1) **What this means for a player:** a hitch of up to a second does nothing. One of about 2.5 to 3 seconds or more makes the other player disappear on both sides and come back roughly two seconds after the game recovers. Nothing is lost and no consent has to be given again, but it looks like the other person left. **Possible changes, to be decided, none made:** keep a body through a short not-in-play reading and remove it only if the reading lasts (for example ten seconds), or freeze it in place with a mark instead of removing it; ask for a removed body again at once when play resumes rather than after the 2-second wait; judge liveness by frames as well as seconds, so a game that is slow but still running is not taken for one that stopped. Each of these touches when another player's body may be shown, so it needs the same care as the consent rules. One observation session, not a proof with a pass or fail; exits 0 on both games; the stop was applied to the game process only.
Author
Member

R12: thirty minutes of body continuity passed, but the save-byte rule failed

Tested commit 2f01e1b4bdb6e1629c54a998f0403519fe4e5c74, source 05329d46, with two windowless games on one Linux PC, RTX 4090 and NVIDIA 615.78.08. The game hosted the room over loopback. Each player accepted once, then stood, took short walks and sent the seven planned chat lines.

The accepted session lasted 1,800.129 seconds with no disconnect, rejoin or unexpected body removal. It is nevertheless not an overall pass: the rule written beforehand required every normal-save file to remain byte-identical, and each disposable test copy's option.sav changed.

What passed

  • Both complete logs contain exactly one initial request and creation per game. Neither body was hidden, deleted or recreated during the 30 minutes. After the interval, B deliberately left through the menu; each game then removed its one remote body, with no revival.
  • All 361 paired checks arrived on the five-second schedule. The largest gap between either game's checks was 5.000668 seconds. Both games kept their original process and room identities.
  • Both bodies remained visible in the inspected standing and walking phases. All seven chat lines appeared in both games with the friend's name. The initial join notice and final Leave notice were visible, and Notices remained Everything.
  • All 32 statistics lines per game kept reliable retries, given-up events, refused packets, test drops and full outboxes at 0. The logged loopback round trip was 1 ms.
  • Resident plus reported swapped memory grew by 28.0 MiB and 23.4 MiB from minute 2, below the predeclared 512 MiB final limit. Maximum sampled growth was 46.6 MiB and 36.1 MiB, below the 1,024 MiB peak limit. Both games reported zero swapped memory in all 31 samples each.
  • Both games exited 0. The kernel record contains no driver fault or other driver line.

There were four game-present intervals longer than one second during startup, two per game. None occurred during the accepted interval or shutdown. Those startup intervals remain part of the result; this was not a speed or smoothness measurement.

What failed

Each normal save tree contained 14 files. Only option.sav changed, by 99 bytes on each side. All 13 other files per game and all files in the original test-save source remained byte-identical. These were disposable copies, not the ordinary playing installation.

The retained file times are 05:00:00.732 and 05:00:01.053 local, on 14 November 2026, around minute 14 of the accepted session. Both games were idle then, well before the planned final Leave. Both files kept the same size and layout; one changed value equals the timestamp of 05:00:00. The log does not identify the writer, and some changed values have not been given meanings.

So the byte check genuinely failed, but the evidence does not yet establish whether this was expected native bookkeeping or a multiplayer defect. It does not establish corruption. A clock-related update is a possibility, not a demonstrated cause. The failed requirement is not being changed afterward to ignore the options file, and no second r12 soak was run.

Beside the earlier sessions

Check Previous 0ec9d0b0 Completed r10 restart 1ed75e61 R12 2f01e1b4
Accepted interval 1,803.5 s 1,800.1 s 1,800.1 s
Five-second schedule Missed: 265 pairs Met: 361 pairs Met: 361 pairs
Unexpected native body lifetime None in either complete log One A-side hide/delete/recreate None in either complete log
Disconnect or rejoin None None None
Normal-save bytes after exit Unchanged Unchanged Only option.sav changed on each side
Original source / native exits Unchanged / both 0 Unchanged / both 0 Unchanged / both 0
Overall Sampling rule failed Body-continuity rule failed Save-byte rule failed

The earlier r10 first attempt stopped at minute 4 on an undetermined same-process/zero-swap check whose failing value was not recorded. I chose its single restart on that undetermined stop, not a proven test-only trigger. Before restarting, I changed zero-swap to recorded swap with resident-plus-swap growth because this PC already used compressed zram at swappiness 150. R12 used that openly amended memory rule from the outset, without a restart. The earlier records remain:

#355 (comment)

#355 (comment)

Not covered by this session: Windows, two PCs, internet traffic, other GPUs, physical input, map changes, save copy/Return, transfers, progress-changing actions, adult equipment, fighting or deliberately stalled games. In particular, this does not resolve the separate Windows starvation and 2.5-second play-clock problem described at #355 (comment). The line stays WIP.

Whether a single game with co-op off writes the same file at the same clock time is being checked next; the answer follows here.

## R12: thirty minutes of body continuity passed, but the save-byte rule failed Tested commit `2f01e1b4bdb6e1629c54a998f0403519fe4e5c74`, source `05329d46`, with two windowless games on one Linux PC, RTX 4090 and NVIDIA 615.78.08. The game hosted the room over loopback. Each player accepted once, then stood, took short walks and sent the seven planned chat lines. **The accepted session lasted 1,800.129 seconds with no disconnect, rejoin or unexpected body removal. It is nevertheless not an overall pass:** the rule written beforehand required every normal-save file to remain byte-identical, and each disposable test copy's `option.sav` changed. ### What passed - Both complete logs contain exactly one initial request and creation per game. Neither body was hidden, deleted or recreated during the 30 minutes. After the interval, B deliberately left through the menu; each game then removed its one remote body, with no revival. - All 361 paired checks arrived on the five-second schedule. The largest gap between either game's checks was 5.000668 seconds. Both games kept their original process and room identities. - Both bodies remained visible in the inspected standing and walking phases. All seven chat lines appeared in both games with the friend's name. The initial join notice and final Leave notice were visible, and Notices remained Everything. - All 32 statistics lines per game kept reliable retries, given-up events, refused packets, test drops and full outboxes at 0. The logged loopback round trip was 1 ms. - Resident plus reported swapped memory grew by **28.0 MiB and 23.4 MiB** from minute 2, below the predeclared 512 MiB final limit. Maximum sampled growth was 46.6 MiB and 36.1 MiB, below the 1,024 MiB peak limit. Both games reported zero swapped memory in all 31 samples each. - Both games exited 0. The kernel record contains no driver fault or other driver line. There were four game-present intervals longer than one second during startup, two per game. None occurred during the accepted interval or shutdown. Those startup intervals remain part of the result; this was not a speed or smoothness measurement. ### What failed Each normal save tree contained 14 files. **Only `option.sav` changed, by 99 bytes on each side.** All 13 other files per game and all files in the original test-save source remained byte-identical. These were disposable copies, not the ordinary playing installation. The retained file times are **05:00:00.732 and 05:00:01.053 local, on 14 November 2026**, around minute 14 of the accepted session. Both games were idle then, well before the planned final Leave. Both files kept the same size and layout; one changed value equals the timestamp of 05:00:00. The log does not identify the writer, and some changed values have not been given meanings. So the byte check genuinely failed, but **the evidence does not yet establish whether this was expected native bookkeeping or a multiplayer defect**. It does not establish corruption. A clock-related update is a possibility, not a demonstrated cause. The failed requirement is not being changed afterward to ignore the options file, and no second r12 soak was run. ### Beside the earlier sessions | Check | Previous `0ec9d0b0` | Completed r10 restart `1ed75e61` | R12 `2f01e1b4` | |---|---|---|---| | Accepted interval | 1,803.5 s | 1,800.1 s | 1,800.1 s | | Five-second schedule | Missed: 265 pairs | Met: 361 pairs | Met: 361 pairs | | Unexpected native body lifetime | None in either complete log | One A-side hide/delete/recreate | None in either complete log | | Disconnect or rejoin | None | None | None | | Normal-save bytes after exit | Unchanged | Unchanged | Only `option.sav` changed on each side | | Original source / native exits | Unchanged / both 0 | Unchanged / both 0 | Unchanged / both 0 | | Overall | Sampling rule failed | Body-continuity rule failed | Save-byte rule failed | The earlier r10 first attempt stopped at minute 4 on an undetermined same-process/zero-swap check whose failing value was not recorded. **I chose its single restart on that undetermined stop, not a proven test-only trigger.** Before restarting, I changed zero-swap to recorded swap with resident-plus-swap growth because this PC already used compressed zram at swappiness 150. R12 used that openly amended memory rule from the outset, without a restart. The earlier records remain: https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17281 https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17694 **Not covered by this session:** Windows, two PCs, internet traffic, other GPUs, physical input, map changes, save copy/Return, transfers, progress-changing actions, adult equipment, fighting or deliberately stalled games. In particular, this does not resolve the separate Windows starvation and 2.5-second play-clock problem described at https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17764. The line stays WIP. Whether a single game with co-op off writes the same file at the same clock time is being checked next; the answer follows here.
Author
Member

Answer to the open point of the 30-minute session above (#355 (comment)).

The options-save change is the game's own daily bookkeeping, not something that requires co-op.

I ran one game at a time on Linux/RTX 4090, using disposable copies of the same checkpoint and the existing guest-clock override. The PC's clock and time zone were not changed. Each successful test had loaded the save and reached quiet play more than three minutes before the boundary, then received no gameplay input.

Build, co-op off Local guest-clock crossing option.sav result
r12 2f01e1b4 05:00 Rewritten; 85 bytes changed
Plain main 0caf06184 05:00 Rewritten; the same 85 offsets changed
Plain main 0caf06184 06:00 Byte-identical throughout

The native code uses a day boundary at 05:00 local, not every full hour. It saves a rollover timestamp, buffered event and position history, and their four counters. The relevant native functions are unchanged between these builds.

That also accounts for the original 99 bytes: the stationary tests have one new position record; the soak has two, accounting for the extra 14 changed bytes. All eight new event records have the same non-time contents in main, r12-off and both soak games. It was not merely one harmless timestamp that the byte check noticed.

All three tests exited normally; every other save file and the original checkpoint stayed unchanged, with no driver faults. One earlier attempt never passed startup preparation and is retained as unexercised, not a negative result.

The original soak still failed its predeclared byte-invariance rule. For future sessions, keep the whole-file hashes and compare these identified bookkeeping fields separately, checking the rollover time, counters and appended records while requiring every other byte to stay unchanged. Do not ignore option.sav. No game code or soak criterion was changed here. Windows and other clock boundaries remain untested.

So for the session on 2f01e1b4: what it was run for held (30 minutes with both bodies continuously present, no retries, saves' progress untouched), and the one rule it failed was the test's own assumption that an idle game never writes its options save. The session crossed 05:00; the two earlier ones did not.

Answer to the open point of the 30-minute session above (https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/355#issuecomment-17887). The options-save change is the game's own daily bookkeeping, not something that requires co-op. I ran one game at a time on Linux/RTX 4090, using disposable copies of the same checkpoint and the existing guest-clock override. The PC's clock and time zone were not changed. Each successful test had loaded the save and reached quiet play more than three minutes before the boundary, then received no gameplay input. | Build, co-op off | Local guest-clock crossing | `option.sav` result | |---|---|---| | r12 `2f01e1b4` | 05:00 | Rewritten; 85 bytes changed | | Plain main `0caf06184` | 05:00 | Rewritten; the same 85 offsets changed | | Plain main `0caf06184` | 06:00 | Byte-identical throughout | The native code uses a day boundary at **05:00 local**, not every full hour. It saves a rollover timestamp, buffered event and position history, and their four counters. The relevant native functions are unchanged between these builds. That also accounts for the original **99 bytes**: the stationary tests have one new position record; the soak has two, accounting for the extra 14 changed bytes. All eight new event records have the same non-time contents in main, r12-off and both soak games. It was not merely one harmless timestamp that the byte check noticed. All three tests exited normally; every other save file and the original checkpoint stayed unchanged, with no driver faults. One earlier attempt never passed startup preparation and is retained as unexercised, not a negative result. The original soak still failed its predeclared byte-invariance rule. For future sessions, keep the whole-file hashes and compare these identified bookkeeping fields separately, checking the rollover time, counters and appended records while requiring every other byte to stay unchanged. **Do not ignore `option.sav`.** No game code or soak criterion was changed here. Windows and other clock boundaries remain untested. **So for the session on `2f01e1b4`:** what it was run for held (30 minutes with both bodies continuously present, no retries, saves' progress untouched), and the one rule it failed was the test's own assumption that an idle game never writes its options save. The session crossed 05:00; the two earlier ones did not.
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 7s
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
This pull request has changes conflicting with the target branch.
  • docs/development.md
  • gpu/vk/menu_i18n.h
  • gpu/vk/vk_api.h
  • gpu/vk/vk_menu.cpp
  • gpu/vk/vk_menu.h
  • gpu/vk/vk_replay.cpp
  • host/hos_relaunch.c
  • host/nn_hid.c
  • host/rx_backend.c
  • tests/input_background_test.c
  • tests/test_input_background.py
  • tests/test_menu_i18n.py
  • tests/test_menu_keys.py
  • tests/test_menu_text_field.py
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'coop-playtest-r9:mooki-coop-playtest-r9'
git switch 'mooki-coop-playtest-r9'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!355
No description provided.