Skip to content

Mac: DM_PAD_PRESS_TO_JOIN=1, a controller becomes a player at its first button press - #1596

Closed
elieteyssedou wants to merge 5 commits into
seedds:mainfrom
elieteyssedou:pads-press-to-join
Closed

elieteyssedou wants to merge 5 commits into
seedds:mainfrom
elieteyssedou:pads-press-to-join

Conversation

@elieteyssedou

Copy link
Copy Markdown
Contributor

What and why

Builds on #1595 (and #859): its commit is the last one here.

Players cannot choose who is player 1 on the Mac: the order follows GameController's list, and for identical pads read from their reports, the USB port. Bluetooth has no port, so nothing chooses it there.

With DM_PAD_PRESS_TO_JOIN=1, a controller is no player until one of its buttons is pressed, then it takes the lowest player free (pad_slot_take_lowest, pad_slots.h), as a console's "press a button" screen does: GameController controllers, raw HID pads and identical pads alike. Not with DM_PAD_MERGE. Off by default, so nothing changes unless it is set.

Checked

  • Tests: make -C dm/host/tests GAME=0400C3F00006E000 run passes; t_hid checks pad_slot_take_lowest (the order of presses, identical names, a freed player 1, no slot left).
  • Game run: Mario Kart World (0400C3F00006E000) 1.8.0 as a Mac app, 33 minutes of split-screen races (the same run as Mac: identical pads that GameController lists as one are each a player of their own (issue #1218) #1595): the players chose their order with two Pro Controllers over USB, and two Xbox Wireless Controllers connected over Bluetooth while the game ran joined as players 3 and 4 at their first press.
  • Seen in that run: the game's own player-count screen opened before the second pad was pressed, and was canceled (2 to 2 players wanted, 1 connected); pressing on both pads first avoids it.
  • Device: M4 Pro, macOS 27, Xcode 27.2.

Docs

  • docs/playing.md, docs/env_switches.tsv
  • No game files, keys or data extracted from a game in the change

🤖 Generated with Claude Code

larsniet and others added 5 commits November 1, 2026 10:19
…says YES

a5281d0 skipped every HID pad for which +[GCController supportsHIDDevice:]
answers YES. It answers YES for the 8BitDo Ultimate 2C Wireless (2dc8:301b),
which GameController then never lists, so the pad got no input from either
path (seedds#637 again). The call is now made only for Microsoft, Sony and Nintendo
pads, the vendors GameController does list; other vendors are read as HID.
… and is read as HID if it never comes

+[GCController supportsHIDDevice:] is only a hint: it answers YES for the 8BitDo Ultimate 2C (2dc8:301b), which GameController then never lists (GitHub issue seedds#965). A device it says YES for now waits HIDPAD_GC_WAIT seconds for a GCController of its name; without one it is read as HID, and dropped again if that GCController connects later. A device it says NO for is read at once, as before. This replaces the Microsoft, Sony and Nintendo vendor list, so a pad of another vendor that GameController does list is not read twice.
… are each a player of their own (issue seedds#1218)

Two Pro Controllers over USB are listed as one by GameController. When IOKit has at least two pads of a
model and more than GameController lists, after a 3 s wait, every pad of that model is read here, by USB
location, each a player of its own:
- a Switch Pro Controller from its report 0x30 (pad_pro.h), with its gyro, and its sticks from its own
  calibration in SPI flash (the user's, else the factory's), asked with subcommand 0x10; until it answers,
  or if it never does, each axis measures its rest and reach itself;
- any other model from its HID elements as pad_hid.h reads them: no gyro, 8BitDo's button numbering
  unless DM_HID_MAP says otherwise.
Built on seedds#859: the devices GameController lists are now kept, unread, to count them. GameController gives a
controller no HID identity, so its controllers of such a model are left unread rather than read twice.
DM_PAD_IDENTICAL=0 turns it off. No rumble or player lights on such pads, and Home does not quit from them.

Tests: t_pad_pro (report 0x30, the SPI read, both calibrations), t_pad_hid (when a model is read here).
Run: Mario Kart World 1.8.0 on an M4 Pro (macOS 27), 33 min of split-screen races, two Pro Controllers
over USB, which GameController lists as one: each was a player of its own, each with its own calibration
(one the factory's, one the user's), read over USB and over Bluetooth; two Xbox controllers over Bluetooth,
which GameController lists both, joined as players 3 and 4 while the game ran. A duplicate player found in
that run (GameController's own Pro Controller also joining) is fixed here; the fix builds and the unit
tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st button press

The players pick their order whatever the port, the order of connection or the link (Bluetooth has no
port), and for identical pads too: each controller is no player until one of its buttons is pressed,
then takes the lowest player free. GameController controllers, raw HID pads and identical pads read
here alike. Not with DM_PAD_MERGE. Test: t_hid (pad_slot_take_lowest). Run in Mario Kart World with the commit before it (see there): the
players chose their order, and two controllers connected while the game ran joined at their first press.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added host The Horizon host: SVCs, services, threads, memory (dm/host) mac The Mac front end and app (dm/mac, front_mac.m) docs Documentation labels Nov 6, 2026
seedds added a commit that referenced this pull request Nov 7, 2026
… player at its first button press, the lowest player free, on the Mac and on iOS (PR #1596)

Players could not choose who is player 1: the order followed GameController's list, and for identical pads read from IOKit, the USB port; Bluetooth has no port. With the setting on, a controller is no player until one of its buttons is pressed, then it takes the lowest free slot (pad_slot_take_lowest, pad_slots.h): GameController controllers, raw HID pads and identical pads alike. The press also reaches the game. Not with DM_PAD_MERGE. Off by default.

Beyond the PR: the same rule in the iOS front end (DmGamepad.m; a controller waiting for its press hides the touch gamepad, as a connected one does), and a launcher row (settings.tsv, Controls: Press a button to join) so both launchers offer it. docs/settings.md and playing.md say to press on every controller before the game asks how many play: the controller applet stand-in (ctrl_support, hle_hid.c) cancels when fewer have joined than the game wants.

Tests: t_hid (pad_slot_take_lowest); host tests pass; DmGamepad.m compiles for iOS. The PR's version ran in Mario Kart World 1.8.0 on the Mac; the iOS side awaits a device test.

Co-authored-by: Elie <8143621+elieteyssedou@users.noreply.github.com>
@seedds

seedds commented Nov 7, 2026

Copy link
Copy Markdown
Owner

Landed in 62be7fa, with you credited as co-author. It sits on top of the reworked #1595 (1ed28ed): pad_is_twin is now hidpad_gc_unread, and a raw Pro is d->twin && hidpad_is_pro(d). Your join logic, pad_slot_take_lowest and its test are unchanged.

Added on top:

  • iOS: the same rule in DmGamepad.m, since iOS shares pad_slots.h and every controller there is Bluetooth. A controller waiting for its first press hides the touch gamepad, as a connected one does today.
  • A launcher setting: a settings.tsv row, Press a button to join under Controls, off by default, so both the iOS launcher and the Mac settings window offer it. DM_PAD_PRESS_TO_JOIN in env.txt still works.
  • Docs: docs/settings.md and playing.md note that the joining press also reaches the game, and say to press on every controller before the game asks how many play. That's the 2 to 2 players wanted, 1 connected cancel you saw. The real fix, the controller applet stand-in waiting for presses, is now its own issue.

Not run with controllers yet; the iOS side needs a device test.

@seedds seedds closed this Nov 7, 2026
seedds added a commit that referenced this pull request Nov 7, 2026
…sks for more players than it sees gets a prompt over the game that holds its thread until enough controllers have joined (a first button with Press a button to join, a connection without) or the player closes it, as the Switch's player screen; without a window or with DM_KEYS / DM_CTL it is canceled at once as before

Mario Kart World 1.8.0 closed its player screen when the second controller had not pressed yet (pull request #1596). The rules are in dm/host/ctrl_prompt.h (t_ctrl_prompt): B or Escape cancels; with the minimum in, A or Return, the game's maximum, or no connected controller left to join finishes it; the press that makes a controller join counts for neither; no time limit and no close on a focus loss. The game gets no input while it is up, nor until the A or B that closed it is let go. hle_hid.c ctrl_support calls the new front_ctrl_support (front.h) and counts the players again after it (dm_players_reported). The Mac draws it on the Home prompt's card (hq_card_view), iOS as a card with Done and Cancel buttons (DmGamepad.m). The docs no longer ask players to press on every controller first.

Front-ends: a front end may pass the controller prompt's texts in the player's language as DM_CTRL_PROMPT_TEXT, one per line: title, hint, count, done, cancel, with {min}, {max} and {joined} replaced by the numbers (docs/frontends.md#starting-a-game)

Fixes #1640
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
… player at its first button press, the lowest player free, on the Mac and on iOS (PR seedds#1596)

Players could not choose who is player 1: the order followed GameController's list, and for identical pads read from IOKit, the USB port; Bluetooth has no port. With the setting on, a controller is no player until one of its buttons is pressed, then it takes the lowest free slot (pad_slot_take_lowest, pad_slots.h): GameController controllers, raw HID pads and identical pads alike. The press also reaches the game. Not with DM_PAD_MERGE. Off by default.

Beyond the PR: the same rule in the iOS front end (DmGamepad.m; a controller waiting for its press hides the touch gamepad, as a connected one does), and a launcher row (settings.tsv, Controls: Press a button to join) so both launchers offer it. docs/settings.md and playing.md say to press on every controller before the game asks how many play: the controller applet stand-in (ctrl_support, hle_hid.c) cancels when fewer have joined than the game wants.

Tests: t_hid (pad_slot_take_lowest); host tests pass; DmGamepad.m compiles for iOS. The PR's version ran in Mario Kart World 1.8.0 on the Mac; the iOS side awaits a device test.

Co-authored-by: Elie <8143621+elieteyssedou@users.noreply.github.com>
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
…sks for more players than it sees gets a prompt over the game that holds its thread until enough controllers have joined (a first button with Press a button to join, a connection without) or the player closes it, as the Switch's player screen; without a window or with DM_KEYS / DM_CTL it is canceled at once as before

Mario Kart World 1.8.0 closed its player screen when the second controller had not pressed yet (pull request seedds#1596). The rules are in dm/host/ctrl_prompt.h (t_ctrl_prompt): B or Escape cancels; with the minimum in, A or Return, the game's maximum, or no connected controller left to join finishes it; the press that makes a controller join counts for neither; no time limit and no close on a focus loss. The game gets no input while it is up, nor until the A or B that closed it is let go. hle_hid.c ctrl_support calls the new front_ctrl_support (front.h) and counts the players again after it (dm_players_reported). The Mac draws it on the Home prompt's card (hq_card_view), iOS as a card with Done and Cancel buttons (DmGamepad.m). The docs no longer ask players to press on every controller first.

Front-ends: a front end may pass the controller prompt's texts in the player's language as DM_CTRL_PROMPT_TEXT, one per line: title, hint, count, done, cancel, with {min}, {max} and {joined} replaced by the numbers (docs/frontends.md#starting-a-game)

Fixes seedds#1640
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation host The Horizon host: SVCs, services, threads, memory (dm/host) mac The Mac front end and app (dm/mac, front_mac.m)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants