commit 97211d8bc82b8143d8d24bc2f06aa61518dc3ddb
Author: jens <jens.se@icloud.com>
AuthorDate: Sat Aug 22 09:23:58 2026 +0200
Commit: jens <jens.se@icloud.com>
CommitDate: Sat Aug 22 09:23:58 2026 +0200
Move Multiplayer to the main menu; make the host's local seat optional
Two follow-ups to the multiplayer lobby:
- "Multiplayer" now lives directly on the main menu (New Game,
Multiplayer, Options, ...) instead of buried under Options - same
blank-name-plus-custom-text-label trick as before, just moved.
- The lobby no longer auto-seats the host as a local player the
instant it starts. Player 1 is just as optional as anyone else: the
person at the keyboard can press Space to join locally (the same
G_AddNextLocalPlayer path a second local player would use), or skip
it entirely and join from their own phone via the QR like everyone
else. Enter still starts the game, but is now ignored while the
lobby is completely empty - there would be nobody to render a view
for.
Verified the full flow end-to-end: main menu -> Multiplayer -> episode
-> skill -> empty lobby -> Enter does nothing with 0 players -> Space
seats the host as Player 1 -> Enter starts the game normally.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---
src/doom/d_main.c | 1 +
src/doom/g_game.c | 38 ++++++++++++++++++++++++++++----------
src/doom/m_menu.c | 22 ++++++++++------------
3 files changed, 39 insertions(+), 22 deletions(-)
diff --git a/src/doom/d_main.c b/src/doom/d_main.c
index 6a6269a5..086e3d9b 100644
--- a/src/doom/d_main.c
+++ b/src/doom/d_main.c
@@ -209,6 +209,7 @@ static void D_DrawLobby (void)
D_DrawLobbyText (SCREENWIDTH/2 + 20, 40, "MULTIPLAYER LOBBY");
D_DrawLobbyText (SCREENWIDTH/2 + 20, 60, "SCAN THE QR CODE TO JOIN BY PHONE");
+ D_DrawLobbyText (SCREENWIDTH/2 + 20, 76, "OR PRESS SPACE TO JOIN WITH KEYBOARD");
y = 100;
for (i = 0 ; i < MAXPLAYERS ; i++)
diff --git a/src/doom/g_game.c b/src/doom/g_game.c
index b2cc0e91..84f8ae62 100644
--- a/src/doom/g_game.c
+++ b/src/doom/g_game.c
@@ -957,17 +957,33 @@ static void SetMouseButtons(unsigned int buttons_mask)
boolean G_Responder (event_t* ev)
{
// Multiplayer lobby (see G_DeferedInitLobby, m_menu.c's
- // "Multiplayer" options item, d_main.c's lobby screen): checked
+ // "Multiplayer" main menu item, d_main.c's lobby screen): checked
// before anything else, including HU_Responder/ST_Responder/
// AM_Responder below, none of which make sense yet (chat, cheat
// codes, the automap) while still waiting in the lobby and which
- // would otherwise have first claim on this same Enter keypress.
- // The host presses Enter once everyone who's joining locally or by
- // phone has joined, to stop waiting and start playing.
+ // would otherwise have first claim on these same keypresses.
+ // Nobody is seated automatically here - the host at the keyboard is
+ // just as optional as anyone else, and joins locally the same way a
+ // second local player would (Space), or not at all if they'd rather
+ // play from their own phone. Enter starts the game once at least
+ // one player - local or phone - has joined; ignored while the
+ // lobby is still completely empty, since there would be nobody to
+ // render a view for.
if (lobby_active)
{
- if (ev->type == ev_keydown && ev->data1 == KEY_ENTER)
- lobby_active = false;
+ if (ev->type == ev_keydown && ev->data1 == ' ')
+ {
+ G_AddNextLocalPlayer ();
+ }
+ else if (ev->type == ev_keydown && ev->data1 == KEY_ENTER)
+ {
+ int i, numactive = 0;
+ for (i = 0; i < MAXPLAYERS; i++)
+ if (playeringame[i])
+ numactive++;
+ if (numactive > 0)
+ lobby_active = false;
+ }
return true;
}
@@ -2295,11 +2311,13 @@ void G_DoNewGame (void)
if (starting_lobby)
{
- // G_InitNew loads the level synchronously (ends in
- // G_DoLoadLevel), so gamestate is already GS_LEVEL here -
- // G_AddNextLocalPlayer can spawn the host immediately.
+ // Nobody is seated yet - the host chooses whether to join
+ // locally (press Space in the lobby - see G_Responder) or just
+ // scan the QR with their own phone like anyone else, same as
+ // every other player. G_InitNew loads the level synchronously
+ // (ends in G_DoLoadLevel), so gamestate is already GS_LEVEL
+ // here if either of those wants to add a player immediately.
lobby_active = true;
- G_AddNextLocalPlayer ();
}
gameaction = ga_nothing;
}
diff --git a/src/doom/m_menu.c b/src/doom/m_menu.c
index d1ac897b..6f8d57a7 100644
--- a/src/doom/m_menu.c
+++ b/src/doom/m_menu.c
@@ -235,6 +235,7 @@ static void M_ClearMenus (void);
enum
{
newgame = 0,
+ multiplayer,
options,
loadgame,
savegame,
@@ -246,6 +247,11 @@ enum
menuitem_t MainMenu[]=
{
{1,"M_NGAME",M_NewGame,'n'},
+ // No WAD graphic for this - see the OptionsMenu "Multiplayer" item
+ // comment (this used to live there) for how the generic per-item
+ // draw loop in M_Drawer skips a blank name, and M_DrawMainMenu below
+ // draws a text label in its place instead.
+ {1,"",M_Multiplayer,'p'},
{1,"M_OPTION",M_Options,'o'},
{1,"M_LOADG",M_LoadGame,'l'},
{1,"M_SAVEG",M_SaveGame,'s'},
@@ -335,7 +341,6 @@ menu_t NewDef =
enum
{
endgame,
- multiplayer,
messages,
detail,
scrnsize,
@@ -349,13 +354,6 @@ enum
menuitem_t OptionsMenu[]=
{
{1,"M_ENDGAM", M_EndGame,'e'},
- // No WAD graphic for this - name is deliberately empty, so the
- // generic per-item draw loop in M_Drawer skips it (it already
- // guards on name[0], see the "name[0] && W_CheckNumForName" check)
- // and M_DrawOptions below draws a text label instead, same
- // approach as the "Messages: ON"/"Graphic Detail: HIGH" dynamic
- // labels already do for their own state.
- {1,"", M_Multiplayer,'p'},
{1,"M_MESSG", M_ChangeMessages,'m'},
{1,"M_DETAIL", M_ChangeDetail,'g'},
{2,"M_SCRNSZ", M_SizeDisplay,'s'},
@@ -893,6 +891,10 @@ void M_DrawMainMenu(void)
{
V_DrawPatchDirect(94, 2,
W_CacheLumpName(DEH_String("M_DOOM"), PU_CACHE));
+
+ // No WAD graphic for this item - see its blank name in MainMenu.
+ M_WriteText(MainDef.x, MainDef.y + LINEHEIGHT * multiplayer + 4,
+ "MULTIPLAYER");
}
@@ -1025,10 +1027,6 @@ void M_DrawOptions(void)
V_DrawPatchDirect(108, 15, W_CacheLumpName(DEH_String("M_OPTTTL"),
PU_CACHE));
- // No WAD graphic for this item - see its blank name in OptionsMenu.
- M_WriteText(OptionsDef.x, OptionsDef.y + LINEHEIGHT * multiplayer + 4,
- "MULTIPLAYER");
-
V_DrawPatchDirect(OptionsDef.x + 175, OptionsDef.y + LINEHEIGHT * detail,
W_CacheLumpName(DEH_String(detailNames[detailLevel]),
PU_CACHE));