commit 7bc4bbae57292a13998c7c9c97f219fef66012fc
Author: jens <jens.se@icloud.com>
AuthorDate: Thu Aug 20 20:45:40 2026 +0200
Commit: jens <jens.se@icloud.com>
CommitDate: Thu Aug 20 20:45:40 2026 +0200
Add dynamic splitscreen (up to 25 players) and phone-based QR join
Raises MAXPLAYERS to 25 and reworks the renderer to tile an
aspect-correct viewport per active player in a single process
(R_RenderSplitViews/R_ComputeSplitLayout/R_FitAspectRect in r_main.c),
instead of the vanilla single fixed view.
Adds an embedded HTTP/WebSocket server (i_webinput.c/h) so phones can
join as controllers by scanning a QR code (rendered with vendored
qrcodegen) rather than needing a keyboard or gamepad. Phone input is
fed through the same gamekeydown/joystick plumbing local players use
(G_SetRemoteInputState in g_game.c), including analog turning via a
twin-stick virtual joystick UI served to the phone. -nolocal lets
every player, including player 1, join this way instead of assuming a
local keyboard player always exists.
Also fixes several latent single-player assumptions that only became
reachable once multiple/zero-player states were possible:
- P_LookForPlayers (p_enemy.c) infinite-looped when no players are in
the game yet (-nolocal at level start).
- S_StartSound/S_UpdateSounds (s_sound.c) only checked audibility
against the console player; now checks every active player.
- The classic status bar has no room in a splitscreen tile, so it's
replaced by a compact per-tile health/ammo readout (st_stuff.c)
that's wired back up to the Screen Size setting (hidden at max size,
shown otherwise) so that setting isn't inert in splitscreen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---
src/d_loop.c | 61 ++-
src/d_loop.h | 12 +
src/doom/CMakeLists.txt | 2 +
src/doom/d_main.c | 148 ++++++-
src/doom/d_net.c | 75 +++-
src/doom/doomdef.h | 9 +-
src/doom/doomstat.h | 16 +-
src/doom/g_game.c | 952 +++++++++++++++++++++++++++++++------------
src/doom/g_game.h | 38 +-
src/doom/i_webinput.c | 936 ++++++++++++++++++++++++++++++++++++++++++
src/doom/i_webinput.h | 52 +++
src/doom/p_enemy.c | 17 +
src/doom/p_mobj.c | 2 +-
src/doom/p_mobj.h | 7 +-
src/doom/p_setup.c | 64 ++-
src/doom/p_setup.h | 5 +
src/doom/p_tick.c | 2 +-
src/doom/qrcodegen.c | 1027 +++++++++++++++++++++++++++++++++++++++++++++++
src/doom/qrcodegen.h | 385 ++++++++++++++++++
src/doom/r_defs.h | 32 +-
src/doom/r_draw.c | 107 +++--
src/doom/r_draw.h | 11 +
src/doom/r_main.c | 350 ++++++++++++++--
src/doom/r_main.h | 16 +
src/doom/r_plane.c | 10 +-
src/doom/r_things.c | 8 +-
src/doom/s_sound.c | 104 +++--
src/doom/s_sound.h | 2 +-
src/doom/st_stuff.c | 108 ++++-
src/doom/st_stuff.h | 6 +
src/doom/wi_stuff.c | 35 +-
src/i_main.c | 6 +
src/i_video.h | 29 +-
src/m_config.c | 149 +++++++
src/m_controls.c | 10 +-
src/m_controls.h | 2 +-
src/net_defs.h | 4 +-
37 files changed, 4372 insertions(+), 427 deletions(-)
diff --git a/src/d_loop.c b/src/d_loop.c
index b963054a..d8905ad4 100644
--- a/src/d_loop.c
+++ b/src/d_loop.c
@@ -186,8 +186,47 @@ static boolean BuildNewTic(void)
NET_CL_SendTiccmd(&cmd, maketic);
}
- ticdata[maketic % BACKUPTICS].cmds[localplayer] = cmd;
- ticdata[maketic % BACKUPTICS].ingame[localplayer] = true;
+ {
+ boolean split_ingame[NET_MAXPLAYERS];
+ boolean is_splitscreen = loop_interface->GetSplitscreenPlayers != NULL
+ && loop_interface->GetSplitscreenPlayers(split_ingame);
+
+ if (is_splitscreen)
+ {
+ int i;
+
+ // Local splitscreen: build an independent ticcmd per local
+ // player slot, reading that slot's own input devices (see
+ // G_BuildTiccmd). Slot localplayer's ticcmd was already
+ // built above (into 'cmd') via the classic single-slot
+ // BuildTiccmd path - reuse it rather than building it twice.
+ for (i = 0; i < NET_MAXPLAYERS; ++i)
+ {
+ if (split_ingame[i])
+ {
+ ticcmd_t splitcmd;
+
+ if (i == localplayer)
+ {
+ splitcmd = cmd;
+ }
+ else
+ {
+ memset(&splitcmd, 0, sizeof(ticcmd_t));
+ loop_interface->BuildSplitTiccmd(&splitcmd, maketic, i);
+ }
+
+ ticdata[maketic % BACKUPTICS].cmds[i] = splitcmd;
+ ticdata[maketic % BACKUPTICS].ingame[i] = true;
+ }
+ }
+ }
+ else
+ {
+ ticdata[maketic % BACKUPTICS].cmds[localplayer] = cmd;
+ ticdata[maketic % BACKUPTICS].ingame[localplayer] = true;
+ }
+ }
++maketic;
@@ -659,10 +698,16 @@ static void TicdupSquash(ticcmd_set_t *set)
static void SinglePlayerClear(ticcmd_set_t *set)
{
unsigned int i;
+ boolean split_ingame[NET_MAXPLAYERS];
+ boolean is_splitscreen;
+
+ memset(split_ingame, 0, sizeof(split_ingame));
+ is_splitscreen = loop_interface->GetSplitscreenPlayers != NULL
+ && loop_interface->GetSplitscreenPlayers(split_ingame);
for (i = 0; i < NET_MAXPLAYERS; ++i)
{
- if (i != localplayer)
+ if (i != localplayer && !(is_splitscreen && split_ingame[i]))
{
set->ingame[i] = false;
}
@@ -753,6 +798,16 @@ void TryRunTics (void)
return;
}
+ // This loop can legitimately run for a while (up to
+ // MAX_NETGAME_STALL_TICS worth of real time above, before
+ // giving up for this call) without ever reaching
+ // BuildNewTic - the only other place input/events normally
+ // get processed. Do it here too, each iteration, so a
+ // phone join or a keypress like ESC doesn't have to wait
+ // out that entire stall before being noticed.
+ I_StartTic ();
+ loop_interface->ProcessEvents ();
+
I_Sleep(1);
}
}
diff --git a/src/d_loop.h b/src/d_loop.h
index c154be3a..4536168b 100644
--- a/src/d_loop.h
+++ b/src/d_loop.h
@@ -48,6 +48,18 @@ typedef struct
// Run the menu (runs independently of the game).
void (*RunMenu)();
+
+ // Optional (may be NULL). Fills 'ingame' (sized NET_MAXPLAYERS) with
+ // which additional local player slots are part of local splitscreen,
+ // and returns true. Returns false (or is NULL) when splitscreen is
+ // not active, in which case only the usual single localplayer slot
+ // is driven via BuildTiccmd above.
+ boolean (*GetSplitscreenPlayers)(boolean *ingame);
+
+ // Optional (may be NULL, but must be set if GetSplitscreenPlayers
+ // can return true). Builds an independent ticcmd for one local
+ // splitscreen player slot, reading that slot's own input devices.
+ void (*BuildSplitTiccmd)(ticcmd_t *cmd, int maketic, int localslot);
} loop_interface_t;
// Register callback functions for the main loop code to use.
diff --git a/src/doom/CMakeLists.txt b/src/doom/CMakeLists.txt
index 6e878a0f..cd115c61 100644
--- a/src/doom/CMakeLists.txt
+++ b/src/doom/CMakeLists.txt
@@ -26,7 +26,9 @@ add_library(doom STATIC
g_game.c g_game.h
hu_lib.c hu_lib.h
hu_stuff.c hu_stuff.h
+ i_webinput.c i_webinput.h
info.c info.h
+ qrcodegen.c qrcodegen.h
m_menu.c m_menu.h
m_random.c m_random.h
p_ceilng.c
diff --git a/src/doom/d_main.c b/src/doom/d_main.c
index 014e4872..298067be 100644
--- a/src/doom/d_main.c
+++ b/src/doom/d_main.c
@@ -58,6 +58,7 @@
#include "i_system.h"
#include "i_timer.h"
#include "i_video.h"
+#include "i_webinput.h"
#include "g_game.h"
@@ -136,10 +137,12 @@ void D_ProcessEvents (void)
{
event_t* ev;
+ I_WebInputTic();
+
// IF STORE DEMO, DO NOT ACCEPT INPUT
if (storedemo)
return;
-
+
while ((ev = D_PopEvent()) != NULL)
{
if (M_Responder (ev))
@@ -166,15 +169,85 @@ boolean D_Display (void)
static boolean inhelpscreensstate = false;
static boolean fullscreen = false;
static gamestate_t oldgamestate = -1;
+ static boolean old_show_split = false;
+ static boolean old_waiting_for_players = false;
static int borderdrawcount;
int y;
boolean wipe;
boolean redrawsbar;
-
+ boolean waiting_for_players;
+ boolean show_split;
+
redrawsbar = false;
-
+
+ // -nolocal (see i_webinput.c): nobody local is ever going to spawn
+ // a player, so there is no player_t/mobj to render a view from
+ // until the first phone joins - R_RenderPlayerView dereferences
+ // players[displayplayer].mo, which would be NULL here and crash.
+ // Show a fullscreen join QR instead of a view whenever that's the
+ // situation, same idea as R_RenderSplitViews's empty-cell QR but
+ // for "zero active players" rather than "grid has room".
+ //
+ // show_split covers everything R_RenderSplitViews will actually
+ // tile - true splitscreen (2+ players), but also, with -qrstart,
+ // exactly 1 player once R_RenderSplitViews reserves a QR cell next
+ // to them (see its comment) rather than going fullscreen. This is
+ // deliberately its own flag rather than reusing splitscreen: ticcmd
+ // building (GetSplitscreenPlayers in d_net.c) and other non-render
+ // consumers must keep treating a reserved QR cell as "still just 1
+ // real player", not a phantom second one needing input of its own.
+ {
+ int i, numactive = 0;
+ for (i = 0; i < MAXPLAYERS; i++)
+ if (playeringame[i])
+ numactive++;
+ waiting_for_players = (numactive == 0) && I_WebInputNoLocalEnabled ();
+ show_split = splitscreen
+ || (numactive == 1 && I_WebInputQrEnabled ());
+ }
+
+ // Entering or leaving splitscreen (e.g. G_AddLocalPlayer/
+ // G_RemoveLocalPlayer joining or leaving mid-level) switches between
+ // entirely different screen layouts without a gamestate change, so
+ // the usual "oldgamestate != GS_LEVEL" redraw trigger below never
+ // fires for it. Force the same background/border redraw and a full
+ // status bar repaint here, or leftover pixels from the old layout
+ // (extra splitscreen tiles, or the classic border) linger in areas
+ // the new layout doesn't happen to touch every frame.
+ //
+ // Leaving splitscreen also needs setsizeneeded: scaledviewwidth/
+ // viewwindowx/viewwindowy/viewheight are the same globals
+ // R_RenderSplitViews's R_RepositionBuffer/R_SetSplitscreenViewSize
+ // just finished setting for the last splitscreen tile: without
+ // forcing R_ExecuteSetViewSize to recompute them for the classic
+ // single view below, R_FillBackScreen/R_DrawViewBorder run with
+ // stale tile geometry and can walk off the edge of the screen.
+ if (show_split != old_show_split
+ || waiting_for_players != old_waiting_for_players)
+ {
+ oldgamestate = -1;
+ borderdrawcount = 3;
+ redrawsbar = true;
+ old_show_split = show_split;
+ old_waiting_for_players = waiting_for_players;
+
+ if (!show_split)
+ setsizeneeded = true;
+
+ // R_RenderSplitViews only runs while show_split is true, so it
+ // cannot itself notice "we just dropped to 1 player with no QR
+ // reserved" and reset its cached tile size - do it here so the
+ // *next* time show_split turns back on (e.g. G_AddLocalPlayer
+ // rejoining), it recomputes fresh tile geometry instead of
+ // trusting a count left over from before we dropped out.
+ R_InvalidateSplitViewCache ();
+ }
+
// change the view size if needed
- if (setsizeneeded)
+ // (show_split's tile sizing is managed entirely by R_RenderSplitViews;
+ // running the classic setblocks-based resize over it would clobber
+ // the tile-sized tables with full/status-bar-shrunk ones)
+ if (setsizeneeded && !show_split)
{
R_ExecuteSetViewSize ();
oldgamestate = -1; // force background redraw
@@ -190,22 +263,29 @@ boolean D_Display (void)
else
wipe = false;
- if (gamestate == GS_LEVEL && gametic)
+ if (gamestate == GS_LEVEL && gametic && !waiting_for_players)
HU_Erase();
-
+
// do buffered drawing
switch (gamestate)
{
case GS_LEVEL:
if (!gametic)
break;
- if (automapactive)
+ if (automapactive && !waiting_for_players)
AM_Drawer ();
if (wipe || (viewheight != SCREENHEIGHT && fullscreen))
redrawsbar = true;
if (inhelpscreensstate && !inhelpscreens)
redrawsbar = true; // just put away the help screen
- ST_Drawer (viewheight == SCREENHEIGHT, redrawsbar );
+ // The classic status bar assumes a single, centered player view and
+ // reserves SBARHEIGHT of screen space for it; neither applies to
+ // show_split's tiled views (real splitscreen or 1 player plus a
+ // reserved QR cell), nor is there a player to show stats for while
+ // waiting_for_players (see above). Skip it in both cases (see
+ // R_RenderSplitViews).
+ if (!show_split && !waiting_for_players)
+ ST_Drawer (viewheight == SCREENHEIGHT, redrawsbar );
fullscreen = viewheight == SCREENHEIGHT;
break;
@@ -227,9 +307,22 @@ boolean D_Display (void)
// draw the view directly
if (gamestate == GS_LEVEL && !automapactive && gametic)
- R_RenderPlayerView (&players[displayplayer]);
+ {
+ if (waiting_for_players)
+ {
+ // No player exists yet to render a view from (see the
+ // waiting_for_players computation above) - show a fullscreen
+ // join QR instead of an actual view.
+ V_DrawFilledBox (0, 0, SCREENWIDTH, SCREENHEIGHT, 0);
+ I_WebInputDrawJoinQR (0, 0, SCREENWIDTH, SCREENHEIGHT);
+ }
+ else if (show_split)
+ R_RenderSplitViews ();
+ else
+ R_RenderPlayerView (&players[displayplayer]);
+ }
- if (gamestate == GS_LEVEL && gametic)
+ if (gamestate == GS_LEVEL && gametic && !waiting_for_players)
HU_Drawer ();
// clean up border stuff
@@ -237,14 +330,21 @@ boolean D_Display (void)
I_SetPalette (W_CacheLumpName (DEH_String("PLAYPAL"),PU_CACHE));
// see if the border needs to be initially drawn
- if (gamestate == GS_LEVEL && oldgamestate != GS_LEVEL)
+ // (show_split's tiles always fill their whole allocated rect - there
+ // is no shrunk-view decorative border to draw, unlike the classic
+ // status-bar view size setting; waiting_for_players' fullscreen QR
+ // is the same story - nothing shrunk, nothing to border)
+ if (gamestate == GS_LEVEL && oldgamestate != GS_LEVEL && !show_split
+ && !waiting_for_players)
{
viewactivestate = false; // view was not active
R_FillBackScreen (); // draw the pattern into the back screen
}
// see if the border needs to be updated to the screen
- if (gamestate == GS_LEVEL && !automapactive && scaledviewwidth != SCREENWIDTH)
+ if (gamestate == GS_LEVEL && !automapactive && !show_split
+ && !waiting_for_players
+ && scaledviewwidth != SCREENWIDTH)
{
if (menuactive || menuactivestate || !viewactivestate)
borderdrawcount = 3;
@@ -363,6 +463,7 @@ void D_BindVariables(void)
M_BindIntVariable("vanilla_demo_limit", &vanilla_demo_limit);
M_BindIntVariable("show_endoom", &show_endoom);
M_BindIntVariable("show_diskicon", &show_diskicon);
+ M_BindIntVariable("splitscreen_players", &splitscreen_players);
// Multiplayer chat macros
@@ -409,6 +510,20 @@ void D_RunFrame()
static int wipestart;
static boolean wipe;
+ // Poll input and drain the event queue once per rendered frame,
+ // independent of wipe state or the gametic simulation rate below
+ // (TryRunTics/BuildNewTic normally do this, but only on real tic
+ // boundaries - ~35/sec in principle, though that can fall far
+ // behind under load, and the wipe branch below skips them entirely
+ // for its duration either way). Both a phone join (I_WebInputTic,
+ // reached via D_ProcessEvents) and basic things like ESC opening
+ // the menu need to keep working promptly regardless, so neither is
+ // tied to that path exclusively. D_PopEvent's queue means calling
+ // this more often than strictly necessary just drains it sooner -
+ // no event is read or acted on twice.
+ I_StartTic ();
+ D_ProcessEvents ();
+
if (wipe)
{
do
@@ -432,7 +547,7 @@ void D_RunFrame()
TryRunTics (); // will run at least one tic
- S_UpdateSounds (players[consoleplayer].mo);// move positional sounds
+ S_UpdateSounds ();// move positional sounds (checks every active player - see S_BestListenerParams)
// Update display, next frame, with current state if no profiling is on
if (screenvisible && !nodrawers)
@@ -1931,6 +2046,13 @@ void D_DoomMain (void)
DEH_printf("D_CheckNetGame: Checking network game status.\n");
D_CheckNetGame ();
+ // Spike/proof of concept: let a phone browser join a local
+ // splitscreen slot over the LAN - see i_webinput.c. Independent of
+ // -splitscreen/splitscreen_players; a phone joining is what flips
+ // splitscreen on if this starts with only 1 local player, exactly
+ // like pressing the local join key.
+ I_WebInputInit (5029);
+
PrintGameVersion();
DEH_printf("HU_Init: Setting up heads up display.\n");
diff --git a/src/doom/d_net.c b/src/doom/d_net.c
index f8969fe0..efaa8069 100644
--- a/src/doom/d_net.c
+++ b/src/doom/d_net.c
@@ -91,11 +91,37 @@ static void RunTic(ticcmd_t *cmds, boolean *ingame)
G_Ticker ();
}
+// Reports the local splitscreen players (see doomstat.h's splitscreen)
+// to d_loop.c, so it drives every splitscreen slot's ticcmd/ingame state
+// instead of just the single localplayer.
+static boolean GetSplitscreenPlayers(boolean *ingame)
+{
+ int i;
+
+ if (!splitscreen)
+ return false;
+
+ for (i = 0; i < MAXPLAYERS; i++)
+ ingame[i] = playeringame[i];
+
+ return true;
+}
+
+// Adapts G_BuildTiccmd's (cmd, maketic, localslot) signature to the
+// classic (cmd, maketic) BuildTiccmd interface slot, always for the
+// primary local player (slot 0).
+static void BuildTiccmdSlot0(ticcmd_t *cmd, int maketic)
+{
+ G_BuildTiccmd(cmd, maketic, 0);
+}
+
static loop_interface_t doom_loop_interface = {
D_ProcessEvents,
- G_BuildTiccmd,
+ BuildTiccmdSlot0,
RunTic,
- M_Ticker
+ M_Ticker,
+ GetSplitscreenPlayers,
+ G_BuildTiccmd
};
@@ -258,6 +284,51 @@ void D_CheckNetGame (void)
D_StartNetGame(&settings, NULL);
LoadGameSettings(&settings);
+ //!
+ // @category net
+ //
+ // No local (keyboard/mouse) player - everybody, including the
+ // first player, joins via phone (see i_webinput.c). Overrides
+ // -splitscreen/splitscreen_players: slot 0, which LoadGameSettings
+ // just defaulted to in-game above, is forced back out instead.
+ //
+
+ if (M_ParmExists("-nolocal"))
+ {
+ playeringame[0] = false;
+ }
+ else
+ //!
+ // @arg <n>
+ // @category net
+ //
+ // Start a local splitscreen game with n predetermined local players
+ // (up to MAXPLAYERS), all sharing this machine's screen and input
+ // devices, instead of the usual single local player. Overrides the
+ // splitscreen_players config setting for this run without changing
+ // its saved value. Concept-step scaffolding for local multiplayer;
+ // see doomstat.h's splitscreen.
+ //
+
+ {
+ int p = M_CheckParmWithArgs("-splitscreen", 1);
+ int num_local_players = (p > 0) ? atoi(myargv[p + 1])
+ : splitscreen_players;
+
+ if (num_local_players > 1)
+ {
+ int i;
+
+ if (num_local_players > MAXPLAYERS)
+ num_local_players = MAXPLAYERS;
+
+ for (i = 0; i < num_local_players; i++)
+ playeringame[i] = true;
+
+ splitscreen = true;
+ }
+ }
+
DEH_printf("startskill %i deathmatch: %i startmap: %i startepisode: %i\n",
startskill, deathmatch, startmap, startepisode);
diff --git a/src/doom/doomdef.h b/src/doom/doomdef.h
index 62d729dd..b455cbc9 100644
--- a/src/doom/doomdef.h
+++ b/src/doom/doomdef.h
@@ -42,7 +42,14 @@
#define RANGECHECK
// The maximum number of players, multiplayer/networking.
-#define MAXPLAYERS 4
+// Raised from vanilla's 4 to support local splitscreen with up to 25
+// players - chosen because it's a perfect 5x5 grid (see
+// R_ComputeSplitLayout), the largest count with no empty/wasted cell.
+// See MF_TRANSLATION (p_mobj.h) and R_InitTranslationTables (r_draw.c),
+// which must be able to represent MAXPLAYERS-1 color ramps (cycling
+// through a smaller set of visually distinct colors, since the palette
+// doesn't have 24 distinct ones to spare).
+#define MAXPLAYERS 25
// The current state of the game: whether we are
// playing, gazing at the intermission screen,
diff --git a/src/doom/doomstat.h b/src/doom/doomstat.h
index 689f5950..d72e6790 100644
--- a/src/doom/doomstat.h
+++ b/src/doom/doomstat.h
@@ -104,6 +104,18 @@ extern boolean netgame;
// 0=Cooperative; 1=Deathmatch; 2=Altdeath
extern int deathmatch;
+// True when more than one local player is in-game (splitscreen). When
+// true, the renderer draws one tiled viewport per in-game player instead
+// of a single fullscreen view for displayplayer. Set by the local
+// multiplayer setup code, independent of netgame.
+extern boolean splitscreen;
+
+// How many local players (1-MAXPLAYERS) to bring into the game
+// automatically at startup - see D_CheckNetGame in d_net.c. Persisted
+// config setting (see D_BindVariables); the -splitscreen command line
+// parameter overrides it for one run without changing the saved value.
+extern int splitscreen_players;
+
// -------------------------
// Internal parameters for sound rendering.
// These have been taken from the DOS version,
@@ -221,7 +233,9 @@ extern boolean playeringame[MAXPLAYERS];
// Player spawn spots for deathmatch.
-#define MAX_DM_STARTS 10
+// Must match MAX_DEATHMATCH_STARTS in p_setup.c, which actually defines
+// the array below (this is only the extern declaration other files see).
+#define MAX_DM_STARTS 32
extern mapthing_t deathmatchstarts[MAX_DM_STARTS];
extern mapthing_t* deathmatch_p;
diff --git a/src/doom/g_game.c b/src/doom/g_game.c
index 42587121..a188ff22 100644
--- a/src/doom/g_game.c
+++ b/src/doom/g_game.c
@@ -118,9 +118,11 @@ int starttime; // for comparative timing purposes
boolean viewactive;
-int deathmatch; // only if started as net death
-boolean netgame; // only true if packets are broadcast
-boolean playeringame[MAXPLAYERS];
+int deathmatch; // only if started as net death
+boolean netgame; // only true if packets are broadcast
+boolean splitscreen; // local splitscreen (see doomstat.h)
+int splitscreen_players = 1; // persisted config (see doomstat.h)
+boolean playeringame[MAXPLAYERS];
player_t players[MAXPLAYERS];
boolean turbodetected[MAXPLAYERS];
@@ -198,29 +200,49 @@ static const struct
#define NUMKEYS 256
#define MAX_JOY_BUTTONS 20
-static boolean gamekeydown[NUMKEYS];
-static int turnheld; // for accelerative turning
-
+// Indexed by local player slot (0 = primary keyboard+mouse player, see
+// G_BuildTiccmd) so each splitscreen player tracks its own held keys.
+static boolean gamekeydown[MAXPLAYERS][NUMKEYS];
+static int turnheld; // for accelerative turning
+
static boolean mousearray[MAX_MOUSE_BUTTONS + 1];
static boolean *mousebuttons = &mousearray[1]; // allow [-1]
-// mouse values are used once
+// mouse values are used once, and only ever drive local slot 0 - there
+// is only one mouse.
int mousex;
-int mousey;
+int mousey;
static int dclicktime;
static boolean dclickstate;
-static int dclicks;
+static int dclicks;
static int dclicktime2;
static boolean dclickstate2;
static int dclicks2;
-// joystick values are repeated
-static int joyxmove;
-static int joyymove;
-static int joystrafemove;
-static boolean joyarray[MAX_JOY_BUTTONS + 1];
-static boolean *joybuttons = &joyarray[1]; // allow [-1]
+// joystick values are repeated, indexed by local player slot (see
+// i_joystick.c for how each slot's physical device is chosen).
+static int joyxmove[MAXPLAYERS];
+static int joyymove[MAXPLAYERS];
+static int joystrafemove[MAXPLAYERS];
+static boolean joyarray[MAXPLAYERS][MAX_JOY_BUTTONS + 1];
+static boolean *joybuttons[MAXPLAYERS]; // allow [-1] per slot
+static boolean joybuttons_initialized;
+
+// joyarray/joybuttons need a runtime init loop (not a static initializer)
+// to set up each slot's "allow [-1]" pointer offset.
+static void InitJoyButtonPointers(void)
+{
+ int i;
+
+ if (joybuttons_initialized)
+ return;
+
+ for (i = 0; i < MAXPLAYERS; i++)
+ joybuttons[i] = &joyarray[i][1];
+
+ joybuttons_initialized = true;
+}
static int savegameslot;
static char savedescription[32];
@@ -322,183 +344,288 @@ static int G_NextWeapon(int direction)
return weapon_order_table[i].weapon_num;
}
+// Predetermined keys for bringing the next available local splitscreen
+// player slot into a running game on the fly, or taking the most
+// recently joined one back out - see G_AddLocalPlayer/
+// G_RemoveLocalPlayer. Neither collides with slot 0's (arrows/
+// punctuation/modifiers) or slot 1's (WASD zone) own bindings. Only
+// slots 0 and 1 have any keyboard binding of their own (see
+// GetLocalKeybinds) - slots 2+ join with no input source yet (no
+// gamepad support built) and will just stand there until one exists,
+// which is enough to exercise the join/leave/render/spawn machinery for
+// more than 2 players without waiting on that.
+#define KEY_JOIN_PLAYER 'j'
+#define KEY_LEAVE_PLAYER 'l'
+
+// Predetermined keyboard bindings used by local splitscreen slots beyond
+// slot 0 (which keeps using the normal, user-configurable key_* cvars).
+// Only slot 1 (a second, WASD-based keyboard zone) is defined for now;
+// slots 2+ are expected to be driven by their own joystick/gamepad
+// instead (see i_joystick.c) and never receive routed key events (see
+// G_Responder), so their entry here is unreachable in practice.
+typedef struct
+{
+ int right, left, up, down;
+ int strafeleft, straferight;
+ int fire, use, strafe, speed;
+} splitscreen_binds_t;
+
+static const splitscreen_binds_t splitscreen_zone1_binds =
+{
+ 'd', 'a', 'w', 's',
+ 'q', 'e',
+ 'f', 'g', 'c', 'v'
+};
+
+static void GetLocalKeybinds(int localslot, splitscreen_binds_t *binds)
+{
+ if (localslot == 0)
+ {
+ binds->right = key_right;
+ binds->left = key_left;
+ binds->up = key_up;
+ binds->down = key_down;
+ binds->strafeleft = key_strafeleft;
+ binds->straferight = key_straferight;
+ binds->fire = key_fire;
+ binds->use = key_use;
+ binds->strafe = key_strafe;
+ binds->speed = key_speed;
+ }
+ else
+ {
+ *binds = splitscreen_zone1_binds;
+ }
+}
+
+// Routes an incoming key event to the local player slot whose keyboard
+// zone owns it: slot 1 if splitscreen is active and the key is one of
+// zone 1's predetermined WASD-zone bindings, slot 0 (the normal,
+// user-configurable cvars) otherwise. Since zone 1's bindings are plain
+// letter keys distinct from every default player-1 binding (which are
+// all arrows/punctuation/modifiers - see m_controls.c), the two zones
+// never contend for the same physical key.
+static int GetKeyLocalSlot(int doomkey)
+{
+ const splitscreen_binds_t *z1;
+
+ if (!splitscreen)
+ return 0;
+
+ z1 = &splitscreen_zone1_binds;
+
+ if (doomkey == z1->right || doomkey == z1->left
+ || doomkey == z1->up || doomkey == z1->down
+ || doomkey == z1->strafeleft || doomkey == z1->straferight
+ || doomkey == z1->fire || doomkey == z1->use
+ || doomkey == z1->strafe || doomkey == z1->speed)
+ {
+ return 1;
+ }
+
+ return 0;
+}
+
//
// G_BuildTiccmd
// Builds a ticcmd from all of the available inputs
-// or reads it from the demo buffer.
-// If recording a demo, write it out
-//
-void G_BuildTiccmd (ticcmd_t* cmd, int maketic)
-{
- int i;
+// or reads it from the demo buffer.
+// If recording a demo, write it out
+//
+// localslot selects which local player's input state to read: their
+// gamekeydown[] row, joystick slot, and (slot 0 only) the shared mouse.
+// See GetLocalKeybinds for how each slot's keyboard bindings resolve.
+//
+void G_BuildTiccmd (ticcmd_t* cmd, int maketic, int localslot)
+{
+ int i;
boolean strafe;
- boolean bstrafe;
+ boolean bstrafe;
int speed;
- int tspeed;
+ int tspeed;
int forward;
int side;
+ boolean use_mouse;
+ boolean *keys;
+ boolean *jbuttons;
+ int jx, jy, jstrafe;
+ splitscreen_binds_t kb;
+
+ InitJoyButtonPointers();
+
+ use_mouse = (localslot == 0);
+ keys = gamekeydown[localslot];
+ jbuttons = joybuttons[localslot];
+ jx = joyxmove[localslot];
+ jy = joyymove[localslot];
+ jstrafe = joystrafemove[localslot];
+ GetLocalKeybinds(localslot, &kb);
memset(cmd, 0, sizeof(ticcmd_t));
- cmd->consistancy =
- consistancy[consoleplayer][maketic%BACKUPTICS];
-
- strafe = gamekeydown[key_strafe] || mousebuttons[mousebstrafe]
- || joybuttons[joybstrafe];
+ cmd->consistancy =
+ consistancy[localslot][maketic%BACKUPTICS];
+
+ strafe = keys[kb.strafe] || (use_mouse && mousebuttons[mousebstrafe])
+ || jbuttons[joybstrafe];
// fraggle: support the old "joyb_speed = 31" hack which
// allowed an autorun effect
- speed = key_speed >= NUMKEYS
+ speed = kb.speed >= NUMKEYS
|| joybspeed >= MAX_JOY_BUTTONS
- || gamekeydown[key_speed]
- || joybuttons[joybspeed]
- || mousebuttons[mousebspeed];
-
+ || keys[kb.speed]
+ || jbuttons[joybspeed]
+ || (use_mouse && mousebuttons[mousebspeed]);
+
forward = side = 0;
-
+
// use two stage accelerative turning
// on the keyboard and joystick
- if (joyxmove < 0
- || joyxmove > 0
- || gamekeydown[key_right]
- || gamekeydown[key_left]
- || mousebuttons[mousebturnright]
- || mousebuttons[mousebturnleft])
- turnheld += ticdup;
- else
- turnheld = 0;
+ if (jx < 0
+ || jx > 0
+ || keys[kb.right]
+ || keys[kb.left]
+ || (use_mouse && mousebuttons[mousebturnright])
+ || (use_mouse && mousebuttons[mousebturnleft]))
+ turnheld += ticdup;
+ else
+ turnheld = 0;
- if (turnheld < SLOWTURNTICS)
- tspeed = 2; // slow turn
- else
+ if (turnheld < SLOWTURNTICS)
+ tspeed = 2; // slow turn
+ else
tspeed = speed;
-
+
// let movement keys cancel each other out
- if (strafe)
- {
- if (gamekeydown[key_right] || mousebuttons[mousebturnright])
+ if (strafe)
+ {
+ if (keys[kb.right] || (use_mouse && mousebuttons[mousebturnright]))
{
// fprintf(stderr, "strafe right\n");
- side += sidemove[speed];
+ side += sidemove[speed];
}
- if (gamekeydown[key_left] || mousebuttons[mousebturnleft])
+ if (keys[kb.left] || (use_mouse && mousebuttons[mousebturnleft]))
{
// fprintf(stderr, "strafe left\n");
- side -= sidemove[speed];
+ side -= sidemove[speed];
}
- if (use_analog && joyxmove)
+ if (use_analog && jx)
{
- joyxmove = joyxmove * joystick_move_sensitivity / 10;
- joyxmove = (joyxmove > FRACUNIT) ? FRACUNIT : joyxmove;
- joyxmove = (joyxmove < -FRACUNIT) ? -FRACUNIT : joyxmove;
- side += FixedMul(sidemove[speed], joyxmove);
+ jx = jx * joystick_move_sensitivity / 10;
+ jx = (jx > FRACUNIT) ? FRACUNIT : jx;
+ jx = (jx < -FRACUNIT) ? -FRACUNIT : jx;
+ side += FixedMul(sidemove[speed], jx);
}
else if (joystick_move_sensitivity)
{
- if (joyxmove > 0)
+ if (jx > 0)
side += sidemove[speed];
- if (joyxmove < 0)
+ if (jx < 0)
side -= sidemove[speed];
}
- }
- else
- {
- if (gamekeydown[key_right] || mousebuttons[mousebturnright])
- cmd->angleturn -= angleturn[tspeed];
- if (gamekeydown[key_left] || mousebuttons[mousebturnleft])
- cmd->angleturn += angleturn[tspeed];
- if (use_analog && joyxmove)
+ }
+ else
+ {
+ if (keys[kb.right] || (use_mouse && mousebuttons[mousebturnright]))
+ cmd->angleturn -= angleturn[tspeed];
+ if (keys[kb.left] || (use_mouse && mousebuttons[mousebturnleft]))
+ cmd->angleturn += angleturn[tspeed];
+ if (use_analog && jx)
{
// Cubic response curve allows for finer control when stick
// deflection is small.
- joyxmove = FixedMul(FixedMul(joyxmove, joyxmove), joyxmove);
- joyxmove = joyxmove * joystick_turn_sensitivity / 10;
- cmd->angleturn -= FixedMul(angleturn[1], joyxmove);
+ jx = FixedMul(FixedMul(jx, jx), jx);
+ jx = jx * joystick_turn_sensitivity / 10;
+ cmd->angleturn -= FixedMul(angleturn[1], jx);
}
else if (joystick_turn_sensitivity)
{
- if (joyxmove > 0)
+ if (jx > 0)
cmd->angleturn -= angleturn[tspeed];
- if (joyxmove < 0)
+ if (jx < 0)
cmd->angleturn += angleturn[tspeed];
}
- }
-
- if (gamekeydown[key_up])
+ }
+
+ if (keys[kb.up])
{
// fprintf(stderr, "up\n");
- forward += forwardmove[speed];
+ forward += forwardmove[speed];
}
- if (gamekeydown[key_down])
+ if (keys[kb.down])
{
// fprintf(stderr, "down\n");
- forward -= forwardmove[speed];
+ forward -= forwardmove[speed];
}
- if (use_analog && joyymove)
+ if (use_analog && jy)
{
- joyymove = joyymove * joystick_move_sensitivity / 10;
- joyymove = (joyymove > FRACUNIT) ? FRACUNIT : joyymove;
- joyymove = (joyymove < -FRACUNIT) ? -FRACUNIT : joyymove;
- forward -= FixedMul(forwardmove[speed], joyymove);
+ jy = jy * joystick_move_sensitivity / 10;
+ jy = (jy > FRACUNIT) ? FRACUNIT : jy;
+ jy = (jy < -FRACUNIT) ? -FRACUNIT : jy;
+ forward -= FixedMul(forwardmove[speed], jy);
}
else if (joystick_move_sensitivity)
{
- if (joyymove < 0)
+ if (jy < 0)
forward += forwardmove[speed];
- if (joyymove > 0)
+ if (jy > 0)
forward -= forwardmove[speed];
}
- if (gamekeydown[key_strafeleft]
- || joybuttons[joybstrafeleft]
- || mousebuttons[mousebstrafeleft])
+ if (keys[kb.strafeleft]
+ || jbuttons[joybstrafeleft]
+ || (use_mouse && mousebuttons[mousebstrafeleft]))
{
side -= sidemove[speed];
}
- if (gamekeydown[key_straferight]
- || joybuttons[joybstraferight]
- || mousebuttons[mousebstraferight])
+ if (keys[kb.straferight]
+ || jbuttons[joybstraferight]
+ || (use_mouse && mousebuttons[mousebstraferight]))
{
- side += sidemove[speed];
+ side += sidemove[speed];
}
- if (use_analog && joystrafemove)
+ if (use_analog && jstrafe)
{
- joystrafemove = joystrafemove * joystick_move_sensitivity / 10;
- joystrafemove = (joystrafemove > FRACUNIT) ? FRACUNIT : joystrafemove;
- joystrafemove = (joystrafemove < -FRACUNIT) ? -FRACUNIT : joystrafemove;
- side += FixedMul(sidemove[speed], joystrafemove);
+ jstrafe = jstrafe * joystick_move_sensitivity / 10;
+ jstrafe = (jstrafe > FRACUNIT) ? FRACUNIT : jstrafe;
+ jstrafe = (jstrafe < -FRACUNIT) ? -FRACUNIT : jstrafe;
+ side += FixedMul(sidemove[speed], jstrafe);
}
else if (joystick_move_sensitivity)
{
- if (joystrafemove < 0)
+ if (jstrafe < 0)
side -= sidemove[speed];
- if (joystrafemove > 0)
+ if (jstrafe > 0)
side += sidemove[speed];
}
// buttons
- cmd->chatchar = HU_dequeueChatChar();
-
- if (gamekeydown[key_fire] || mousebuttons[mousebfire]
- || joybuttons[joybfire])
- cmd->buttons |= BT_ATTACK;
-
- if (gamekeydown[key_use]
- || joybuttons[joybuse]
- || mousebuttons[mousebuse])
- {
+ cmd->chatchar = (use_mouse) ? HU_dequeueChatChar() : 0;
+
+ if (keys[kb.fire] || (use_mouse && mousebuttons[mousebfire])
+ || jbuttons[joybfire])
+ cmd->buttons |= BT_ATTACK;
+
+ if (keys[kb.use]
+ || jbuttons[joybuse]
+ || (use_mouse && mousebuttons[mousebuse]))
+ {
cmd->buttons |= BT_USE;
- // clear double clicks if hit use button
- dclicks = 0;
- }
+ // clear double clicks if hit use button
+ dclicks = 0;
+ }
// If the previous or next weapon button is pressed, the
// next_weapon variable is set to change weapons when
- // we generate a ticcmd. Choose a new weapon.
+ // we generate a ticcmd. Choose a new weapon. Weapon-cycle keys are
+ // only ever bound for slot 0 (see GetLocalKeybinds/G_Responder), and
+ // this always runs for slot 0 first each tic, consuming and
+ // resetting next_weapon before any other slot is processed - so this
+ // cannot leak player 1's weapon switch onto other splitscreen slots.
if (gamestate == GS_LEVEL && next_weapon != 0)
{
@@ -514,7 +641,7 @@ void G_BuildTiccmd (ticcmd_t* cmd, int maketic)
{
int key = *weapon_keys[i];
- if (gamekeydown[key])
+ if (keys[key])
{
cmd->buttons |= BT_CHANGE;
cmd->buttons |= i<<BT_WEAPONSHIFT;
@@ -525,110 +652,113 @@ void G_BuildTiccmd (ticcmd_t* cmd, int maketic)
next_weapon = 0;
- // mouse
- if (mousebuttons[mousebforward])
- {
- forward += forwardmove[speed];
- }
- if (mousebuttons[mousebbackward])
+ // mouse (slot 0 only - there is only one mouse)
+ if (use_mouse)
{
- forward -= forwardmove[speed];
- }
+ if (mousebuttons[mousebforward])
+ {
+ forward += forwardmove[speed];
+ }
+ if (mousebuttons[mousebbackward])
+ {
+ forward -= forwardmove[speed];
+ }
- if (dclick_use)
- {
- // forward double click
- if (mousebuttons[mousebforward] != dclickstate && dclicktime > 1 )
- {
- dclickstate = mousebuttons[mousebforward];
- if (dclickstate)
- dclicks++;
- if (dclicks == 2)
- {
- cmd->buttons |= BT_USE;
- dclicks = 0;
- }
- else
- dclicktime = 0;
- }
- else
- {
- dclicktime += ticdup;
- if (dclicktime > 20)
- {
- dclicks = 0;
- dclickstate = 0;
- }
+ if (dclick_use)
+ {
+ // forward double click
+ if (mousebuttons[mousebforward] != dclickstate && dclicktime > 1 )
+ {
+ dclickstate = mousebuttons[mousebforward];
+ if (dclickstate)
+ dclicks++;
+ if (dclicks == 2)
+ {
+ cmd->buttons |= BT_USE;
+ dclicks = 0;
+ }
+ else
+ dclicktime = 0;
+ }
+ else
+ {
+ dclicktime += ticdup;
+ if (dclicktime > 20)
+ {
+ dclicks = 0;
+ dclickstate = 0;
+ }
+ }
+
+ // strafe double click
+ bstrafe =
+ mousebuttons[mousebstrafe]
+ || jbuttons[joybstrafe];
+ if (bstrafe != dclickstate2 && dclicktime2 > 1 )
+ {
+ dclickstate2 = bstrafe;
+ if (dclickstate2)
+ dclicks2++;
+ if (dclicks2 == 2)
+ {
+ cmd->buttons |= BT_USE;
+ dclicks2 = 0;
+ }
+ else
+ dclicktime2 = 0;
+ }
+ else
+ {
+ dclicktime2 += ticdup;
+ if (dclicktime2 > 20)
+ {
+ dclicks2 = 0;
+ dclickstate2 = 0;
+ }
+ }
}
-
- // strafe double click
- bstrafe =
- mousebuttons[mousebstrafe]
- || joybuttons[joybstrafe];
- if (bstrafe != dclickstate2 && dclicktime2 > 1 )
- {
- dclickstate2 = bstrafe;
- if (dclickstate2)
- dclicks2++;
- if (dclicks2 == 2)
- {
- cmd->buttons |= BT_USE;
- dclicks2 = 0;
- }
- else
- dclicktime2 = 0;
- }
- else
- {
- dclicktime2 += ticdup;
- if (dclicktime2 > 20)
- {
- dclicks2 = 0;
- dclickstate2 = 0;
- }
- }
+
+ forward += mousey;
+
+ if (strafe)
+ side += mousex*2;
+ else
+ cmd->angleturn -= mousex*0x8;
+
+ if (mousex == 0)
+ {
+ // No movement in the previous frame
+
+ testcontrols_mousespeed = 0;
+ }
+
+ mousex = mousey = 0;
}
- forward += mousey;
+ if (forward > MAXPLMOVE)
+ forward = MAXPLMOVE;
+ else if (forward < -MAXPLMOVE)
+ forward = -MAXPLMOVE;
+ if (side > MAXPLMOVE)
+ side = MAXPLMOVE;
+ else if (side < -MAXPLMOVE)
+ side = -MAXPLMOVE;
- if (strafe)
- side += mousex*2;
- else
- cmd->angleturn -= mousex*0x8;
+ cmd->forwardmove += forward;
+ cmd->sidemove += side;
- if (mousex == 0)
+ // special buttons (slot 0 only - pause/save are whole-game actions)
+ if (use_mouse && sendpause)
{
- // No movement in the previous frame
+ sendpause = false;
+ cmd->buttons = BT_SPECIAL | BTS_PAUSE;
+ }
- testcontrols_mousespeed = 0;
+ if (use_mouse && sendsave)
+ {
+ sendsave = false;
+ cmd->buttons = BT_SPECIAL | BTS_SAVEGAME | (savegameslot<<BTS_SAVESHIFT);
}
-
- mousex = mousey = 0;
-
- if (forward > MAXPLMOVE)
- forward = MAXPLMOVE;
- else if (forward < -MAXPLMOVE)
- forward = -MAXPLMOVE;
- if (side > MAXPLMOVE)
- side = MAXPLMOVE;
- else if (side < -MAXPLMOVE)
- side = -MAXPLMOVE;
-
- cmd->forwardmove += forward;
- cmd->sidemove += side;
-
- // special buttons
- if (sendpause)
- {
- sendpause = false;
- cmd->buttons = BT_SPECIAL | BTS_PAUSE;
- }
-
- if (sendsave)
- {
- sendsave = false;
- cmd->buttons = BT_SPECIAL | BTS_SAVEGAME | (savegameslot<<BTS_SAVESHIFT);
- }
// low-res turning
@@ -649,7 +779,7 @@ void G_BuildTiccmd (ticcmd_t* cmd, int maketic)
carry = desired_angleturn - cmd->angleturn;
}
-}
+}
//
@@ -707,15 +837,43 @@ void G_DoLoadLevel (void)
memset (players[i].frags,0,sizeof(players[i].frags));
}
- P_SetupLevel (gameepisode, gamemap, 0, gameskill);
- displayplayer = consoleplayer; // view the guy you are playing
- gameaction = ga_nothing;
+ P_SetupLevel (gameepisode, gamemap, 0, gameskill);
+ displayplayer = consoleplayer; // view the guy you are playing
+
+ // -nolocal (see i_webinput.c): the console player has no local
+ // input source, so it doesn't spawn at level start (see
+ // D_CheckNetGame in d_net.c) - which means ST_Start/HU_Start,
+ // normally only triggered from inside P_SpawnPlayer when the
+ // console player specifically spawns, never ran. Both are safe to
+ // call with no player yet - they only touch plain player_t fields,
+ // never ->mo - so call them here too; if/when the first phone
+ // joins slot 0, P_SpawnPlayer's own call re-runs them harmlessly.
+ if (!playeringame[consoleplayer])
+ {
+ ST_Start ();
+ HU_Start ();
+
+ // Skip the demo-to-level wipe/melt transition for this specific
+ // entry: it's a real-time-paced animation with nothing useful to
+ // show while there's no player yet (just the join QR, drawn
+ // fresh every frame regardless), and D_RunFrame doesn't process
+ // webinput ticcmds/joins at all while a wipe is in progress -
+ // see I_WebInputTic's new call from D_RunFrame's wipe branch,
+ // which covers any *other* wipe that might still start later,
+ // but this one is better avoided outright since it gates the
+ // very first join.
+ wipegamestate = gamestate;
+ }
+
+ gameaction = ga_nothing;
Z_CheckHeap ();
// clear cmd building stuff
memset (gamekeydown, 0, sizeof(gamekeydown));
- joyxmove = joyymove = joystrafemove = 0;
+ memset (joyxmove, 0, sizeof(joyxmove));
+ memset (joyymove, 0, sizeof(joyymove));
+ memset (joystrafemove, 0, sizeof(joystrafemove));
mousex = mousey = 0;
sendpause = sendsave = paused = false;
memset(mousearray, 0, sizeof(mousearray));
@@ -727,9 +885,20 @@ void G_DoLoadLevel (void)
}
}
-static void SetJoyButtons(unsigned int buttons_mask)
+// localslot identifies which local player's joystick posted this event.
+// TODO(splitscreen): i_joystick.c currently only ever opens/reports one
+// physical device, so this is always called with slot 0 for now: every
+// splitscreen slot beyond the keyboard zones (see GetLocalKeybinds) is
+// still unfed. Widening i_joystick.c to open one device per gamepad slot
+// and tag ev_joystick events with their origin is the remaining piece of
+// Fas 2 (see MEMORY/plan notes) - the per-slot joyxmove/joyymove/
+// joystrafemove/joybuttons arrays here are already shaped for it.
+static void SetJoyButtons(int localslot, unsigned int buttons_mask)
{
int i;
+ boolean *slot_buttons = joybuttons[localslot];
+
+ InitJoyButtonPointers();
for (i=0; i<MAX_JOY_BUTTONS; ++i)
{
@@ -737,7 +906,7 @@ static void SetJoyButtons(unsigned int buttons_mask)
// Detect button press:
- if (!joybuttons[i] && button_on)
+ if (!slot_buttons[i] && button_on)
{
// Weapon cycling:
@@ -751,7 +920,7 @@ static void SetJoyButtons(unsigned int buttons_mask)
}
}
- joybuttons[i] = button_on;
+ slot_buttons[i] = button_on;
}
}
@@ -862,40 +1031,64 @@ boolean G_Responder (event_t* ev)
next_weapon = 1;
}
- switch (ev->type)
- {
- case ev_keydown:
- if (ev->data1 == key_pause)
- {
- sendpause = true;
+ // Local splitscreen: bring the next available player slot into the
+ // game on the fly, or take the most recently joined one back out
+ // (never slot 0 - that is the primary/host player).
+ if (ev->type == ev_keydown && ev->data1 == KEY_JOIN_PLAYER)
+ {
+ int slot;
+ for (slot = 0; slot < MAXPLAYERS; slot++)
+ {
+ if (G_AddLocalPlayer (slot))
+ break;
+ }
+ }
+ else if (ev->type == ev_keydown && ev->data1 == KEY_LEAVE_PLAYER)
+ {
+ int slot;
+ for (slot = MAXPLAYERS - 1; slot >= 1; slot--)
+ {
+ if (G_RemoveLocalPlayer (slot))
+ break;
+ }
+ }
+
+ switch (ev->type)
+ {
+ case ev_keydown:
+ if (ev->data1 == key_pause)
+ {
+ sendpause = true;
}
- else if (ev->data1 <NUMKEYS)
+ else if (ev->data1 <NUMKEYS)
{
- gamekeydown[ev->data1] = true;
+ gamekeydown[GetKeyLocalSlot(ev->data1)][ev->data1] = true;
}
- return true; // eat key down events
-
- case ev_keyup:
- if (ev->data1 <NUMKEYS)
- gamekeydown[ev->data1] = false;
- return false; // always let key up events filter down
-
- case ev_mouse:
+ return true; // eat key down events
+
+ case ev_keyup:
+ if (ev->data1 <NUMKEYS)
+ gamekeydown[GetKeyLocalSlot(ev->data1)][ev->data1] = false;
+ return false; // always let key up events filter down
+
+ case ev_mouse:
SetMouseButtons(ev->data1);
- mousex = ev->data2*(mouseSensitivity+5)/10;
- mousey = ev->data3*(mouseSensitivity+5)/10;
- return true; // eat events
-
- case ev_joystick:
- SetJoyButtons(ev->data1);
- joyxmove = ev->data2;
- joyymove = ev->data3;
- joystrafemove = ev->data4;
- return true; // eat events
-
- default:
- break;
+ mousex = ev->data2*(mouseSensitivity+5)/10;
+ mousey = ev->data3*(mouseSensitivity+5)/10;
+ return true; // eat events
+
+ case ev_joystick:
+ // TODO(splitscreen): always slot 0 until i_joystick.c supports
+ // multiple devices - see SetJoyButtons.
+ SetJoyButtons(0, ev->data1);
+ joyxmove[0] = ev->data2;
+ joyymove[0] = ev->data3;
+ joystrafemove[0] = ev->data4;
+ return true; // eat events
+
+ default:
+ break;
}
return false;
@@ -1059,12 +1252,25 @@ void G_Ticker (void)
// do main actions
switch (gamestate)
{
- case GS_LEVEL:
- P_Ticker ();
- ST_Ticker ();
- AM_Ticker ();
- HU_Ticker ();
- break;
+ case GS_LEVEL:
+ P_Ticker ();
+ // ST_Ticker/HU_Ticker read from st_stuff.c's/hu_stuff.c's file-local
+ // "plr" globals, which only get pointed at &players[consoleplayer]
+ // by ST_Start/HU_Start - and those are only called from
+ // P_SpawnPlayer when the console player itself spawns (see
+ // p_mobj.c). Under -nolocal (see i_webinput.c), nobody spawns at
+ // level start, so plr stays NULL until the console player's slot
+ // gets a phone - calling either Ticker before that dereferences a
+ // NULL pointer. AM_Ticker doesn't need the same guard: it already
+ // returns immediately unless automapactive, which nothing can set
+ // without an active player to send the toggle keypress.
+ if (playeringame[consoleplayer])
+ {
+ ST_Ticker ();
+ HU_Ticker ();
+ }
+ AM_Ticker ();
+ break;
case GS_INTERMISSION:
WI_Ticker ();
@@ -1347,12 +1553,206 @@ void G_DoReborn (int playernum)
}
// he's going to be inside something. Too bad.
}
- P_SpawnPlayer (&playerstarts[playernum]);
- }
-}
-
-
-void G_ScreenShot (void)
+ P_SpawnPlayer (&playerstarts[playernum]);
+ }
+}
+
+//
+// G_AddLocalPlayer
+// Brings a new local splitscreen player into the currently running game
+// and spawns them immediately, instead of requiring every player to be
+// configured at launch (see the -splitscreen parameter in d_net.c).
+// Triggered by key_join_player2 in G_Responder. Once splitscreen is
+// active, R_RenderSplitViews (rendering) and GetSplitscreenPlayers
+// (input/ticcmd) both already pick up playeringame[] changes on their
+// own each frame/tic - this only needs to flip that flag and place the
+// player in the world.
+//
+// Returns false (leaving playeringame[playernum] unchanged) if the
+// player is already in-game, no level is currently running, or (coop
+// only) the map has no start position to fall back to.
+//
+boolean G_AddLocalPlayer (int playernum)
+{
+ boolean spawned;
+ int count, i;
+
+ if (playernum < 0 || playernum >= MAXPLAYERS)
+ return false;
+
+ if (gamestate != GS_LEVEL || !usergame || demoplayback)
+ {
+ printf("[G_AddLocalPlayer] slot %d rejected: gamestate=%d usergame=%d demoplayback=%d\n",
+ playernum, gamestate, usergame, demoplayback);
+ return false;
+ }
+
+ if (playeringame[playernum])
+ return false;
+
+ playeringame[playernum] = true;
+
+ // Tiled splitscreen rendering only once there are 2+ active
+ // players - e.g. the first phone to join under -nolocal (see
+ // i_webinput.c) still gets the normal fullscreen view, same as any
+ // other lone player.
+ count = 0;
+ for (i = 0; i < MAXPLAYERS; i++)
+ if (playeringame[i])
+ count++;
+ if (count >= 2)
+ splitscreen = true;
+
+ // Same "force (re)initialization" as G_InitNew does for every player
+ // at the start of a fresh game, so P_SpawnPlayer runs G_PlayerReborn
+ // (starting health/weapons/ammo) instead of treating this as an
+ // already-alive player with a zeroed-out player_t.
+ players[playernum].playerstate = PST_REBORN;
+
+ if (deathmatch)
+ {
+ // Always "succeeds" (falls back to playerstarts[playernum] if
+ // every random attempt fails - see G_DeathMatchSpawnPlayer).
+ G_DeathMatchSpawnPlayer (playernum);
+ spawned = true;
+ }
+ else
+ {
+ spawned = P_SpawnFallbackPlayer (playernum);
+ }
+
+ if (!spawned)
+ {
+ printf("[G_AddLocalPlayer] slot %d: spawn failed (no fallback start position)\n", playernum);
+ playeringame[playernum] = false;
+ }
+
+ return spawned;
+}
+
+//
+// G_RemoveLocalPlayer
+// The mirror of G_AddLocalPlayer: takes local splitscreen player
+// playernum out of the running game. This is the local stand-in for
+// what will eventually be triggered by a mobile controller's connection
+// dropping, once that orchestration layer exists - see G_AddLocalPlayer
+// for why that mapping (which connection owns which slot) belongs
+// outside the engine, not in it. The engine's side of "someone left" is
+// just this: flip playeringame[] and get out of the way.
+//
+// Unlike vanilla netgame disconnect (see PlayerQuitGame in d_net.c),
+// which just leaves the body standing there un-driven, this despawns it
+// (with the same teleport-fog effect used elsewhere for spawns) - a
+// player choosing to leave a local splitscreen game should vanish
+// cleanly, not litter the map with an idle corpse-like body.
+// R_RenderSplitViews/GetSplitscreenPlayers both react to the
+// playeringame[] change on their own, next frame/tic - nothing else
+// needs telling.
+//
+// Returns false if the player wasn't in-game to begin with.
+//
+boolean G_RemoveLocalPlayer (int playernum)
+{
+ int i;
+ int remaining;
+
+ if (playernum < 0 || playernum >= MAXPLAYERS)
+ return false;
+
+ if (!playeringame[playernum])
+ return false;
+
+ playeringame[playernum] = false;
+
+ if (players[playernum].mo != NULL)
+ {
+ mobj_t *fog = P_SpawnMobj (players[playernum].mo->x,
+ players[playernum].mo->y,
+ players[playernum].mo->z,
+ MT_TFOG);
+ S_StartSound (fog, sfx_telept);
+
+ P_RemoveMobj (players[playernum].mo);
+ players[playernum].mo = NULL;
+ }
+
+ remaining = 0;
+ for (i = 0; i < MAXPLAYERS; i++)
+ {
+ if (playeringame[i])
+ remaining++;
+ }
+
+ if (remaining <= 1)
+ {
+ splitscreen = false;
+ }
+
+ return true;
+}
+
+// G_SetRemoteInputState
+// Feeds a remotely-sourced (e.g. phone) held-button bitmask into slot
+// localslot's gamekeydown[] row, using the same key identities
+// GetLocalKeybinds hands out to every non-zero local slot. G_BuildTiccmd
+// then picks it up exactly as if a keyboard zone had set it - no other
+// code path needs to know or care that this slot isn't physically local.
+// See g_game.h for the REMOTE_BIT_* flags and i_webinput.c for the
+// network side that calls this once per tic per connected slot.
+void G_SetRemoteInputState (int localslot, unsigned int bits, int turn_axis)
+{
+ splitscreen_binds_t kb;
+
+ // Used to exclude slot 0 (it was always the local keyboard/mouse
+ // player, never remote) - but under -nolocal (i_webinput.c) the
+ // first phone to join legitimately owns slot 0, and there is no
+ // local keyboard user to clobber in that case (the join search
+ // only ever tries slot 0 when nolocal is active), so it's included
+ // here too now.
+ if (localslot < 0 || localslot >= MAXPLAYERS)
+ return;
+
+ GetLocalKeybinds(localslot, &kb);
+
+ // For localslot 0, kb's fields come straight from the user's
+ // configurable key_* cvars (see GetLocalKeybinds) rather than the
+ // fixed, always-in-range zone1 letter constants every other slot
+ // gets - and G_BuildTiccmd already treats a key_* value >= NUMKEYS
+ // as that control's "unbound" sentinel (see its "kb.speed >=
+ // NUMKEYS" check). Guard every write here the same way instead of
+ // assuming it's always a valid gamekeydown[] index.
+#define SET_REMOTE_KEY(key, bit) \
+ do { if ((key) >= 0 && (key) < NUMKEYS) \
+ gamekeydown[localslot][key] = (bits & (bit)) != 0; } while (0)
+
+ SET_REMOTE_KEY(kb.up, REMOTE_BIT_FORWARD);
+ SET_REMOTE_KEY(kb.down, REMOTE_BIT_BACK);
+ SET_REMOTE_KEY(kb.strafeleft, REMOTE_BIT_STRAFELEFT);
+ SET_REMOTE_KEY(kb.straferight, REMOTE_BIT_STRAFERIGHT);
+ SET_REMOTE_KEY(kb.fire, REMOTE_BIT_FIRE);
+ SET_REMOTE_KEY(kb.use, REMOTE_BIT_USE);
+ SET_REMOTE_KEY(kb.speed, REMOTE_BIT_SPEED);
+
+ // Turning goes through the same joyxmove[] slot a real analog
+ // joystick's x-axis would (see G_Responder's ev_joystick case and
+ // G_BuildTiccmd's "use_analog && jx" branch) instead of a
+ // left/right bit, so it gets that branch's cubic response curve -
+ // proportional to how far the stick is pushed, with gentle
+ // fine-control near center - rather than snapping straight to full
+ // turn speed the moment a threshold is crossed. use_analog is
+ // forced on once in I_WebInputInit for exactly this; -127..127
+ // matches the wire format (see i_webinput.c), scaled to the same
+ // +-FRACUNIT range i_joystick.c normalizes real hardware axes to.
+ if (turn_axis < -127) turn_axis = -127;
+ if (turn_axis > 127) turn_axis = 127;
+ joyxmove[localslot] = turn_axis * FRACUNIT / 127;
+
+#undef SET_REMOTE_KEY
+}
+
+
+
+void G_ScreenShot (void)
{
gameaction = ga_screenshot;
}
@@ -1793,20 +2193,33 @@ G_DeferedInitNew
}
-void G_DoNewGame (void)
+void G_DoNewGame (void)
{
- demoplayback = false;
+ int i;
+
+ demoplayback = false;
netdemo = false;
netgame = false;
deathmatch = false;
- playeringame[1] = playeringame[2] = playeringame[3] = 0;
+
+ // Starting a fresh game from the menu drops any netgame players
+ // back to just the local one - but local splitscreen players are
+ // not netgame players, so leave their slots alone (this used to be
+ // hardcoded to indices 1-3 for vanilla's MAXPLAYERS==4; generalized
+ // here since MAXPLAYERS is now larger).
+ if (!splitscreen)
+ {
+ for (i = 1; i < MAXPLAYERS; i++)
+ playeringame[i] = false;
+ }
+
respawnparm = false;
fastparm = false;
nomonsters = false;
consoleplayer = 0;
- G_InitNew (d_skill, d_episode, d_map);
- gameaction = ga_nothing;
-}
+ G_InitNew (d_skill, d_episode, d_map);
+ gameaction = ga_nothing;
+}
void
@@ -2039,7 +2452,7 @@ void G_WriteDemoTiccmd (ticcmd_t* cmd)
{
byte *demo_start;
- if (gamekeydown[key_demo_quit]) // press q to end demo recording
+ if (gamekeydown[0][key_demo_quit]) // press q to end demo recording
G_CheckDemoStatus ();
demo_start = demo_p;
@@ -2374,11 +2787,16 @@ boolean G_CheckDemoStatus (void)
if (demoplayback)
{
W_ReleaseLumpName(defdemoname);
- demoplayback = false;
+ demoplayback = false;
netdemo = false;
netgame = false;
deathmatch = false;
- playeringame[1] = playeringame[2] = playeringame[3] = 0;
+ if (!splitscreen)
+ {
+ int i;
+ for (i = 1; i < MAXPLAYERS; i++)
+ playeringame[i] = false;
+ }
respawnparm = false;
fastparm = false;
nomonsters = false;
diff --git a/src/doom/g_game.h b/src/doom/g_game.h
index 1e8729d1..192e1f5a 100644
--- a/src/doom/g_game.h
+++ b/src/doom/g_game.h
@@ -64,8 +64,42 @@ void G_SecretExitLevel (void);
void G_WorldDone (void);
// Read current data from inputs and build a player movement command.
-
-void G_BuildTiccmd (ticcmd_t *cmd, int maketic);
+// localslot selects which local player's input devices/keybinds to read
+// (0 for the primary keyboard+mouse player; see g_game.c for the
+// splitscreen keyboard-zone/joystick-slot assignment).
+
+void G_BuildTiccmd (ticcmd_t *cmd, int maketic, int localslot);
+
+// Brings local splitscreen player playernum into the running game on
+// the fly (see g_game.c). Returns false if it couldn't (already in-game,
+// no level running, or no start position to fall back to).
+boolean G_AddLocalPlayer (int playernum);
+
+// The mirror of G_AddLocalPlayer: takes playernum back out of the
+// running game. Returns false if they weren't in-game.
+boolean G_RemoveLocalPlayer (int playernum);
+
+// Remote (e.g. phone) input for one local player slot: a held-button
+// bitmask (feeds the same gamekeydown[] state a local splitscreen
+// keyboard zone would - see GetLocalKeybinds in g_game.c) plus a
+// separate analog turn axis. Any slot other than 0 can be driven this
+// way regardless of what, if anything, physically joins it. See
+// i_webinput.c for the network side.
+#define REMOTE_BIT_FORWARD 0x001
+#define REMOTE_BIT_BACK 0x002
+#define REMOTE_BIT_STRAFELEFT 0x010
+#define REMOTE_BIT_STRAFERIGHT 0x020
+#define REMOTE_BIT_FIRE 0x040
+#define REMOTE_BIT_USE 0x080
+#define REMOTE_BIT_SPEED 0x100
+
+// turn_axis is -127..127 (a phone's turn-stick deflection, left to
+// right) rather than a bit, so turning gets the same cubic response
+// curve/acceleration feel as a real analog joystick (see G_BuildTiccmd's
+// "use_analog && jx" branch) instead of the on/off snap a button would
+// give it - see G_SetRemoteInputState's comment for how this reaches
+// that code path.
+void G_SetRemoteInputState (int localslot, unsigned int bits, int turn_axis);
void G_Ticker (void);
boolean G_Responder (event_t* ev);
diff --git a/src/doom/i_webinput.c b/src/doom/i_webinput.c
new file mode 100644
index 00000000..f612708e
--- /dev/null
+++ b/src/doom/i_webinput.c
@@ -0,0 +1,936 @@
+//
+// Copyright(C) 2026
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// DESCRIPTION:
+// Embedded HTTP/WebSocket server letting a phone browser join a
+// local splitscreen slot and drive it over the LAN. See i_webinput.h.
+//
+// Spike scope only: plain ws:// (no TLS), no auth, a fixed compact
+// binary protocol (one bitmask byte per frame - see REMOTE_BIT_* in
+// g_game.h), and a hard cap on concurrent phones (MAX_REMOTE_SLOTS).
+// Good enough to prove a phone can drive a splitscreen slot over the
+// LAN; not meant to survive the open internet.
+//
+
+#include "i_webinput.h"
+
+#ifndef _WIN32
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <arpa/inet.h>
+#include <ifaddrs.h>
+#include <net/if.h>
+#include <signal.h>
+
+#include "SDL.h"
+
+#include "doomtype.h"
+#include "g_game.h"
+#include "doomdef.h"
+#include "sha1.h"
+#include "m_argv.h"
+#include "v_video.h"
+#include "qrcodegen.h"
+#include "w_wad.h"
+#include "z_zone.h"
+#include "deh_str.h"
+#include "i_joystick.h"
+
+// One tracked TCP connection per player slot. Normally only
+// MAXPLAYERS-2 slots are ever up for grabs by a phone (0 is the host,
+// 1 is the local WASD zone - see G_SetRemoteInputState/
+// GetLocalKeybinds in g_game.c), but under -nolocal all MAXPLAYERS
+// slots are - see nolocal_enabled - so size for the larger case; the
+// struct is a few bytes, the extra headroom costs nothing.
+#define MAX_REMOTE_SLOTS MAXPLAYERS
+#define HTTP_HEADER_MAX 4096
+#define WS_PAYLOAD_MAX 256
+
+typedef struct
+{
+ boolean active; // socket connected, thread running
+ boolean pending_join; // handshake just completed, needs a player slot
+ boolean pending_leave; // connection just dropped, needs its slot freed
+ int player_slot; // -1 until bound by I_WebInputTic
+ unsigned int input_bits;
+ int turn_axis; // -127..127, see G_SetRemoteInputState
+} remote_client_t;
+
+static remote_client_t remote_clients[MAX_REMOTE_SLOTS];
+static SDL_mutex *remote_lock = NULL;
+static boolean webinput_enabled = false;
+
+// -qrstart: show a join QR code in place of any empty splitscreen grid
+// cell (see I_WebInputDrawJoinQR, called from R_RenderSplitViews).
+static boolean qrstart_enabled = false;
+
+// -nolocal: no local (keyboard/mouse) player at all - every player,
+// including the first, joins via phone. Slots 0 and 1 (normally
+// reserved for the local host and WASD zone - see MAX_REMOTE_SLOTS)
+// become joinable too, and qrstart_enabled is forced on: with zero
+// local players there would otherwise be no way to invite anyone,
+// since nobody's at the keyboard to read a join URL off the console.
+static boolean nolocal_enabled = false;
+
+static const char websocket_guid[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+// The join page: two twin-stick virtual joysticks (movement left,
+// turn right - see makeStick), each continuously tracked while
+// dragged rather than fixed-position buttons, plus FIRE/USE. Movement
+// packs its held state into a bitmask (see REMOTE_BIT_* in g_game.h);
+// turning is a separate signed -127..127 axis byte for a smooth,
+// proportional feel (see G_SetRemoteInputState) instead of an on/off
+// snap. Both are sent over the WebSocket whenever either changes.
+//
+// IMPORTANT: this whole page is generated by joining adjacent C
+// string literals with no separator, which strips every line break -
+// harmless for HTML/CSS, but a "//" JS comment then swallows
+// everything after it (no newline left to end the comment at) up to
+// the next literal newline, i.e. nothing, i.e. the rest of the
+// script. Never put a "//" line comment in the embedded <script>
+// here - use /* */ if a comment is truly needed.
+static const char join_page[] =
+"<!doctype html><html><head><meta charset='utf-8'>"
+"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>"
+"<title>Doom Controller</title><style>"
+"html,body{margin:0;height:100%;background:#111;overflow:hidden;"
+"font-family:sans-serif;user-select:none;-webkit-user-select:none}"
+"#status{position:fixed;top:4px;left:8px;color:#888;font-size:12px;z-index:10}"
+"#layout{position:fixed;top:0;left:0;right:0;bottom:84px;display:flex}"
+".stickzone{flex:1;min-width:0;display:flex;align-items:center;justify-content:center;touch-action:none}"
+".stickbase{width:82%;max-width:260px;aspect-ratio:1;"
+"border-radius:50%;background:#222;position:relative}"
+".stickknob{width:42%;height:42%;border-radius:50%;background:#555;"
+"position:absolute;top:29%;left:29%}"
+".stickknob.on{background:#888}"
+"#buttons{position:fixed;left:0;right:0;bottom:0;height:84px;"
+"display:flex;gap:6px;padding:0 6px 6px;box-sizing:border-box}"
+".btn{border-radius:12px;background:#333;color:#eee;display:flex;"
+"align-items:center;justify-content:center;font-size:24px;touch-action:none;flex:1}"
+".btn.on{background:#666}"
+"#fire{background:#711}"
+"#use{background:#173}"
+"</style></head><body>"
+"<div id='status'>connecting...</div>"
+"<div id='layout'>"
+"<div class='stickzone' id='movezone'>"
+"<div class='stickbase'><div class='stickknob' id='moveknob'></div></div>"
+"</div>"
+"<div class='stickzone' id='turnzone'>"
+"<div class='stickbase'><div class='stickknob' id='turnknob'></div></div>"
+"</div>"
+"</div>"
+"<div id='buttons'>"
+"<div class='btn' id='use' data-bit='128'>USE</div>"
+"<div class='btn' id='fire' data-bit='64'>FIRE</div>"
+"</div>"
+"<script>"
+"var bits=0,turnAxis=0,sentBits=-1,sentTurn=-999;"
+"var ws=new WebSocket('ws://'+location.host+'/');"
+"var st=document.getElementById('status');"
+"ws.onopen=function(){st.textContent='connected';};"
+"ws.onclose=function(){st.textContent='disconnected';};"
+"ws.binaryType='arraybuffer';"
+"function send(){"
+"if((bits!==sentBits || turnAxis!==sentTurn) && ws.readyState===1){"
+"ws.send(new Uint8Array([bits, turnAxis & 0xff]));"
+"sentBits=bits;sentTurn=turnAxis;"
+"}"
+"}"
+"document.querySelectorAll('.btn').forEach(function(el){"
+"var bit=parseInt(el.getAttribute('data-bit'));"
+"function on(e){e.preventDefault();bits|=bit;el.classList.add('on');send();}"
+"function off(e){e.preventDefault();bits&=~bit;el.classList.remove('on');send();}"
+"el.addEventListener('touchstart',on);el.addEventListener('touchend',off);"
+"el.addEventListener('touchcancel',off);"
+"el.addEventListener('mousedown',on);el.addEventListener('mouseup',off);"
+"el.addEventListener('mouseleave',off);"
+"});"
+"function makeStick(zoneId,knobId,onUpdate){"
+"var zone=document.getElementById(zoneId);"
+"var knob=document.getElementById(knobId);"
+"var base=knob.parentElement;"
+"var dragging=false,touchId=null,cx=0,cy=0,maxR=1;"
+"function begin(x,y){"
+"var r=base.getBoundingClientRect();"
+"cx=r.left+r.width/2;cy=r.top+r.height/2;maxR=r.width/2;"
+"dragging=true;"
+"move(x,y);"
+"}"
+"function move(x,y){"
+"if(!dragging)return;"
+"var dx=x-cx,dy=y-cy;"
+"var dist=Math.sqrt(dx*dx+dy*dy);"
+"if(dist>maxR){dx=dx/dist*maxR;dy=dy/dist*maxR;dist=maxR;}"
+"knob.style.transform='translate('+dx+'px,'+dy+'px)';"
+"knob.classList.toggle('on',dist>maxR*0.3);"
+"onUpdate(dx/maxR,dy/maxR);"
+"}"
+"function end(){"
+"dragging=false;touchId=null;"
+"knob.style.transform='';"
+"knob.classList.remove('on');"
+"onUpdate(0,0);"
+"}"
+"zone.addEventListener('touchstart',function(e){"
+"e.preventDefault();var t=e.changedTouches[0];touchId=t.identifier;begin(t.clientX,t.clientY);"
+"});"
+"zone.addEventListener('touchmove',function(e){"
+"e.preventDefault();"
+"for(var i=0;i<e.changedTouches.length;i++){"
+"var t=e.changedTouches[i];"
+"if(t.identifier===touchId){move(t.clientX,t.clientY);break;}"
+"}"
+"});"
+"function touchDone(e){"
+"e.preventDefault();"
+"for(var i=0;i<e.changedTouches.length;i++){"
+"if(e.changedTouches[i].identifier===touchId){end();break;}"
+"}"
+"}"
+"zone.addEventListener('touchend',touchDone);"
+"zone.addEventListener('touchcancel',touchDone);"
+"zone.addEventListener('mousedown',function(e){begin(e.clientX,e.clientY);});"
+"window.addEventListener('mousemove',function(e){if(dragging)move(e.clientX,e.clientY);});"
+"window.addEventListener('mouseup',function(){if(dragging)end();});"
+"}"
+"var DEAD=0.35;"
+"var FORWARD=1,BACK=2,STRAFELEFT=16,STRAFERIGHT=32;"
+"makeStick('movezone','moveknob',function(nx,ny){"
+"bits&=~(FORWARD|BACK|STRAFELEFT|STRAFERIGHT);"
+"if(ny<-DEAD)bits|=FORWARD;"
+"if(ny>DEAD)bits|=BACK;"
+"if(nx<-DEAD)bits|=STRAFELEFT;"
+"if(nx>DEAD)bits|=STRAFERIGHT;"
+"send();"
+"});"
+"var TURN_DEAD=0.08;"
+"makeStick('turnzone','turnknob',function(nx,ny){"
+"var v=nx;"
+"if(Math.abs(v)<TURN_DEAD)v=0;"
+"else v=(v-(v>0?1:-1)*TURN_DEAD)/(1-TURN_DEAD);"
+"turnAxis=Math.max(-127,Math.min(127,Math.round(v*127)));"
+"send();"
+"});"
+"setInterval(function(){if(ws.readyState===1)send();},1000);"
+"</script></body></html>";
+
+static void Base64Encode(const byte *data, int len, char *out)
+{
+ static const char tbl[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ int i, o = 0;
+
+ for (i = 0; i < len; i += 3)
+ {
+ unsigned int v = data[i] << 16;
+ if (i + 1 < len) v |= data[i + 1] << 8;
+ if (i + 2 < len) v |= data[i + 2];
+
+ out[o++] = tbl[(v >> 18) & 0x3f];
+ out[o++] = tbl[(v >> 12) & 0x3f];
+ out[o++] = (i + 1 < len) ? tbl[(v >> 6) & 0x3f] : '=';
+ out[o++] = (i + 2 < len) ? tbl[v & 0x3f] : '=';
+ }
+ out[o] = '\0';
+}
+
+// Case-insensitive substring search (avoids relying on the
+// platform-specific availability of strcasestr).
+static const char *FindHeaderCI(const char *haystack, const char *needle)
+{
+ size_t hlen = strlen(haystack), nlen = strlen(needle);
+ size_t i;
+
+ if (nlen == 0 || nlen > hlen)
+ return NULL;
+
+ for (i = 0; i + nlen <= hlen; i++)
+ {
+ if (strncasecmp(haystack + i, needle, nlen) == 0)
+ return haystack + i;
+ }
+ return NULL;
+}
+
+static boolean RecvExact(int fd, void *buf, int len)
+{
+ int got = 0;
+ while (got < len)
+ {
+ int n = recv(fd, (char *) buf + got, len - got, 0);
+ if (n <= 0)
+ return false;
+ got += n;
+ }
+ return true;
+}
+
+static boolean SendAll(int fd, const void *buf, int len)
+{
+ int sent = 0;
+ while (sent < len)
+ {
+ int n = send(fd, (const char *) buf + sent, len - sent, 0);
+ if (n <= 0)
+ return false;
+ sent += n;
+ }
+ return true;
+}
+
+// Reads a raw HTTP request one byte at a time up to the blank line that
+// ends its headers, so we never overread into the WebSocket frame
+// stream that immediately follows on the same connection.
+static int ReadHttpRequest(int fd, char *buf, int bufsize)
+{
+ int len = 0;
+
+ while (len < bufsize - 1)
+ {
+ char c;
+ if (recv(fd, &c, 1, 0) <= 0)
+ return -1;
+
+ buf[len++] = c;
+ buf[len] = '\0';
+
+ if (len >= 4 && strcmp(buf + len - 4, "\r\n\r\n") == 0)
+ return len;
+ }
+ return -1;
+}
+
+static void ServeJoinPage(int fd)
+{
+ char header[256];
+ int hlen = snprintf(header, sizeof(header),
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Type: text/html; charset=utf-8\r\n"
+ "Content-Length: %d\r\n"
+ "Connection: close\r\n\r\n",
+ (int) strlen(join_page));
+
+ SendAll(fd, header, hlen);
+ SendAll(fd, join_page, (int) strlen(join_page));
+}
+
+static boolean DoHandshake(int fd, const char *request)
+{
+ const char *key_hdr = FindHeaderCI(request, "Sec-WebSocket-Key:");
+ char key[128], accept_src[256], accept_b64[64], response[512];
+ sha1_context_t ctx;
+ sha1_digest_t digest;
+ int i, rlen;
+
+ if (key_hdr == NULL)
+ return false;
+
+ key_hdr += strlen("Sec-WebSocket-Key:");
+ while (*key_hdr == ' ')
+ key_hdr++;
+
+ for (i = 0; i < (int) sizeof(key) - 1 && key_hdr[i] != '\r'
+ && key_hdr[i] != '\n' && key_hdr[i] != '\0'; i++)
+ {
+ key[i] = key_hdr[i];
+ }
+ key[i] = '\0';
+
+ snprintf(accept_src, sizeof(accept_src), "%s%s", key, websocket_guid);
+
+ SHA1_Init(&ctx);
+ SHA1_Update(&ctx, (byte *) accept_src, strlen(accept_src));
+ SHA1_Final(digest, &ctx);
+
+ Base64Encode(digest, sizeof(sha1_digest_t), accept_b64);
+
+ rlen = snprintf(response, sizeof(response),
+ "HTTP/1.1 101 Switching Protocols\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Accept: %s\r\n\r\n",
+ accept_b64);
+
+ return SendAll(fd, response, rlen);
+}
+
+// Reads and decodes one client->server WebSocket frame. Returns false
+// on close/error (caller should drop the connection). On true, sets
+// *bits_updated if this was a data frame carrying a payload (in which
+// case *out_bits holds byte 0, the held-button bitmask, and *out_turn
+// holds byte 1 - a signed -127..127 turn-stick deflection, see
+// G_SetRemoteInputState - if the frame was at least 2 bytes; otherwise
+// *out_turn is left untouched) - ping/pong/other frames return true
+// without touching either, so callers don't mistake "no news" for "all
+// buttons released and stick centered".
+static boolean ReadWsFrame(int fd, unsigned int *out_bits, int *out_turn, boolean *bits_updated)
+{
+ byte hdr[2];
+ byte maskkey[4];
+ byte payload[WS_PAYLOAD_MAX];
+ int opcode, masked;
+ uint64_t paylen;
+
+ if (!RecvExact(fd, hdr, 2))
+ return false;
+
+ opcode = hdr[0] & 0x0f;
+ masked = hdr[1] & 0x80;
+ paylen = hdr[1] & 0x7f;
+
+ if (paylen == 126)
+ {
+ byte ext[2];
+ if (!RecvExact(fd, ext, 2))
+ return false;
+ paylen = (ext[0] << 8) | ext[1];
+ }
+ else if (paylen == 127)
+ {
+ byte ext[8];
+ int i;
+ if (!RecvExact(fd, ext, 8))
+ return false;
+ paylen = 0;
+ for (i = 0; i < 8; i++)
+ paylen = (paylen << 8) | ext[i];
+ }
+
+ if (masked && !RecvExact(fd, maskkey, 4))
+ return false;
+
+ if (paylen > WS_PAYLOAD_MAX)
+ return false; // spike protocol never sends more than 1 byte
+
+ if (paylen > 0 && !RecvExact(fd, payload, (int) paylen))
+ return false;
+
+ if (masked)
+ {
+ uint64_t i;
+ for (i = 0; i < paylen; i++)
+ payload[i] ^= maskkey[i % 4];
+ }
+
+ if (opcode == 0x8) // close
+ return false;
+
+ if ((opcode == 0x1 || opcode == 0x2) && paylen >= 1)
+ {
+ *out_bits = payload[0];
+ if (paylen >= 2)
+ *out_turn = (int) (int8_t) payload[1];
+ *bits_updated = true;
+ }
+
+ return true;
+}
+
+typedef struct
+{
+ int fd;
+ int slot_index;
+} client_thread_args_t;
+
+static int ClientThreadFunc(void *data)
+{
+ client_thread_args_t *args = (client_thread_args_t *) data;
+ int fd = args->fd;
+ int idx = args->slot_index;
+ char request[HTTP_HEADER_MAX];
+ int reqlen;
+
+ free(args);
+
+ reqlen = ReadHttpRequest(fd, request, sizeof(request));
+ if (reqlen < 0)
+ {
+ close(fd);
+ return 0;
+ }
+
+ if (FindHeaderCI(request, "Upgrade: websocket") == NULL
+ || !DoHandshake(fd, request))
+ {
+ ServeJoinPage(fd);
+ close(fd);
+ return 0;
+ }
+
+ SDL_LockMutex(remote_lock);
+ remote_clients[idx].pending_join = true;
+ SDL_UnlockMutex(remote_lock);
+
+ printf("[webinput] phone connected (slot %d), awaiting player assignment\n", idx);
+
+ for (;;)
+ {
+ unsigned int bits = 0;
+ int turn;
+ boolean bits_updated = false;
+
+ SDL_LockMutex(remote_lock);
+ turn = remote_clients[idx].turn_axis;
+ SDL_UnlockMutex(remote_lock);
+
+ if (!ReadWsFrame(fd, &bits, &turn, &bits_updated))
+ break;
+
+ if (bits_updated)
+ {
+ SDL_LockMutex(remote_lock);
+ remote_clients[idx].input_bits = bits;
+ remote_clients[idx].turn_axis = turn;
+ SDL_UnlockMutex(remote_lock);
+ }
+ }
+
+ close(fd);
+
+ SDL_LockMutex(remote_lock);
+ remote_clients[idx].pending_leave = true;
+ remote_clients[idx].input_bits = 0;
+ remote_clients[idx].turn_axis = 0;
+ SDL_UnlockMutex(remote_lock);
+
+ printf("[webinput] phone disconnected (slot %d)\n", idx);
+
+ return 0;
+}
+
+// The join URL the QR cell encodes (see I_WebInputDrawJoinQR). Cached
+// once at startup from whichever interface PickBestLanAddress prefers -
+// good enough for the common single-NIC-plus-maybe-a-VPN home network
+// this was tested on; not a guarantee of picking the "right" interface
+// on more exotic setups.
+static char cached_join_url[64];
+
+// Home-network LAN ranges scored above anything else (VPN/virtual
+// adapters tend to hand out other ranges) - see PrintJoinURL.
+static int ScoreLanAddress(const char *ipstr)
+{
+ if (strncmp(ipstr, "192.168.", 8) == 0) return 3;
+ if (strncmp(ipstr, "10.", 3) == 0) return 2;
+ if (strncmp(ipstr, "172.", 4) == 0) return 1;
+ return 0;
+}
+
+static void PrintJoinURL(int port)
+{
+ struct ifaddrs *ifap, *ifa;
+ boolean found = false;
+ int best_score = -1;
+
+ if (getifaddrs(&ifap) != 0)
+ return;
+
+ for (ifa = ifap; ifa != NULL; ifa = ifa->ifa_next)
+ {
+ struct sockaddr_in *sin;
+ char ipstr[INET_ADDRSTRLEN];
+ int score;
+
+ if (ifa->ifa_addr == NULL || ifa->ifa_addr->sa_family != AF_INET)
+ continue;
+ if (ifa->ifa_flags & IFF_LOOPBACK)
+ continue;
+
+ sin = (struct sockaddr_in *) ifa->ifa_addr;
+ inet_ntop(AF_INET, &sin->sin_addr, ipstr, sizeof(ipstr));
+
+ printf("[webinput] join from a phone on this network at: http://%s:%d/\n",
+ ipstr, port);
+ found = true;
+
+ score = ScoreLanAddress(ipstr);
+ if (score > best_score)
+ {
+ best_score = score;
+ snprintf(cached_join_url, sizeof(cached_join_url),
+ "http://%s:%d/", ipstr, port);
+ }
+ }
+
+ freeifaddrs(ifap);
+
+ if (!found)
+ {
+ printf("[webinput] listening on port %d (no LAN IP found to print - "
+ "check your network connection)\n", port);
+ }
+}
+
+// I_WebInputQrEnabled
+// Whether R_RenderSplitViews should draw a join QR into any empty
+// splitscreen grid cell (see I_WebInputDrawJoinQR). False if -qrstart
+// wasn't passed, or if no LAN address was found to encode.
+boolean I_WebInputQrEnabled(void)
+{
+ return qrstart_enabled && cached_join_url[0] != '\0';
+}
+
+// I_WebInputNoLocalEnabled
+// Whether -nolocal was passed - see D_Display (d_main.c), which uses
+// this to show a fullscreen join QR instead of trying to render a
+// nonexistent player's view while nobody has joined yet.
+boolean I_WebInputNoLocalEnabled(void)
+{
+ return nolocal_enabled;
+}
+
+// Lazily-encoded, cached module matrix for cached_join_url - encoding
+// (Reed-Solomon + mask search) only needs to happen once, since the
+// join URL never changes after startup.
+static uint8_t qr_tempbuf[qrcodegen_BUFFER_LEN_MAX];
+static uint8_t qr_matrix[qrcodegen_BUFFER_LEN_MAX];
+static boolean qr_matrix_ready = false;
+
+// Palette index closest to the most saturated red in the loaded
+// PLAYPAL, used as the dark-module color when drawing the join QR
+// (background is black, palette index 0 - already used elsewhere in
+// this codebase e.g. R_RenderSplitViews's screen clear - colors
+// reversed from the usual black-on-white: see I_WebInputDrawJoinQR).
+static int FindRedPaletteIndex(void)
+{
+ byte *playpal = W_CacheLumpName(DEH_String("PLAYPAL"), PU_CACHE);
+ int i, best = 0, best_score = -1;
+
+ for (i = 0; i < 256; i++)
+ {
+ int r = playpal[i * 3];
+ int g = playpal[i * 3 + 1];
+ int b = playpal[i * 3 + 2];
+ int score = 2 * r - g - b;
+
+ if (score > best_score)
+ {
+ best_score = score;
+ best = i;
+ }
+ }
+
+ return best;
+}
+
+// I_WebInputDrawJoinQR
+// Draws a join QR code, fit and centered, into the screen-buffer
+// rectangle (x, y, w, h) - meant for whatever empty splitscreen grid
+// cell R_RenderSplitViews hands it. No-op if there's nothing to encode.
+void I_WebInputDrawJoinQR(int x, int y, int w, int h)
+{
+ int size, quiet, scale, total, px_size, ox, oy;
+ int mx, my;
+ static int red_index = -1;
+
+ if (!I_WebInputQrEnabled())
+ return;
+
+ if (!qr_matrix_ready)
+ {
+ if (!qrcodegen_encodeText(cached_join_url, qr_tempbuf, qr_matrix,
+ qrcodegen_Ecc_LOW,
+ qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX,
+ qrcodegen_Mask_AUTO, true))
+ {
+ return;
+ }
+ qr_matrix_ready = true;
+ }
+
+ if (red_index < 0)
+ red_index = FindRedPaletteIndex();
+
+ size = qrcodegen_getSize(qr_matrix);
+ quiet = 4; // standard recommended quiet zone, in modules
+ total = size + 2 * quiet;
+
+ scale = (w < h ? w : h) / total;
+ if (scale < 1)
+ scale = 1;
+
+ px_size = total * scale;
+ ox = x + (w - px_size) / 2;
+ oy = y + (h - px_size) / 2;
+
+ // Colors reversed from the usual QR convention: black background
+ // (light modules - palette index 0, matching R_RenderSplitViews's
+ // own screen clear elsewhere), red dark modules instead of black.
+ V_DrawFilledBox(ox, oy, px_size, px_size, 0);
+
+ for (my = 0; my < size; my++)
+ {
+ for (mx = 0; mx < size; mx++)
+ {
+ if (qrcodegen_getModule(qr_matrix, mx, my))
+ {
+ V_DrawFilledBox(ox + (quiet + mx) * scale,
+ oy + (quiet + my) * scale,
+ scale, scale, red_index);
+ }
+ }
+ }
+}
+
+static int ListenerThreadFunc(void *data)
+{
+ int port = (int)(intptr_t) data;
+ int listen_fd;
+ struct sockaddr_in addr;
+ int opt = 1;
+
+ listen_fd = socket(AF_INET, SOCK_STREAM, 0);
+ if (listen_fd < 0)
+ {
+ printf("[webinput] socket() failed, phone join disabled\n");
+ return 0;
+ }
+
+ setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_addr.s_addr = INADDR_ANY;
+ addr.sin_port = htons(port);
+
+ if (bind(listen_fd, (struct sockaddr *) &addr, sizeof(addr)) < 0)
+ {
+ printf("[webinput] bind() on port %d failed, phone join disabled\n", port);
+ close(listen_fd);
+ return 0;
+ }
+
+ if (listen(listen_fd, 8) < 0)
+ {
+ printf("[webinput] listen() failed, phone join disabled\n");
+ close(listen_fd);
+ return 0;
+ }
+
+ PrintJoinURL(port);
+
+ for (;;)
+ {
+ struct sockaddr_in client_addr;
+ socklen_t addrlen = sizeof(client_addr);
+ int client_fd = accept(listen_fd, (struct sockaddr *) &client_addr, &addrlen);
+ int idx, opt2 = 1;
+
+ if (client_fd < 0)
+ continue;
+
+ setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &opt2, sizeof(opt2));
+
+ SDL_LockMutex(remote_lock);
+ for (idx = 0; idx < MAX_REMOTE_SLOTS; idx++)
+ {
+ if (!remote_clients[idx].active)
+ {
+ remote_clients[idx].active = true;
+ remote_clients[idx].player_slot = -1;
+ remote_clients[idx].input_bits = 0;
+ remote_clients[idx].turn_axis = 0;
+ break;
+ }
+ }
+ SDL_UnlockMutex(remote_lock);
+
+ if (idx == MAX_REMOTE_SLOTS)
+ {
+ close(client_fd); // full up, spike cap reached
+ continue;
+ }
+
+ {
+ client_thread_args_t *args = malloc(sizeof(client_thread_args_t));
+ SDL_Thread *t;
+ args->fd = client_fd;
+ args->slot_index = idx;
+ t = SDL_CreateThread(ClientThreadFunc, "webinput-client", args);
+ if (t != NULL)
+ SDL_DetachThread(t);
+ }
+ }
+
+ return 0;
+}
+
+void I_WebInputInit(int port)
+{
+ int i;
+
+ // A phone can drop its TCP connection at any moment; without this,
+ // the next send() to that socket raises SIGPIPE, whose default
+ // disposition kills the whole process (not just the failing
+ // connection thread). recv()/send() already check for and handle
+ // the resulting EPIPE return individually.
+ signal(SIGPIPE, SIG_IGN);
+
+ //!
+ // @category net
+ //
+ // Show a join QR code in place of any empty splitscreen grid cell
+ // (see I_WebInputDrawJoinQR), so bystanders can scan to join
+ // instead of needing to be told the join URL out of band.
+ //
+
+ qrstart_enabled = M_ParmExists("-qrstart");
+
+ //!
+ // @category net
+ //
+ // No local (keyboard/mouse) player - everybody, including the
+ // first player, joins via phone. Implies -qrstart, since otherwise
+ // there would be no local player to read a join URL off the
+ // console for. See D_CheckNetGame (d_net.c) for where this also
+ // keeps slot 0 from auto-joining at startup.
+ //
+
+ nolocal_enabled = M_ParmExists("-nolocal");
+ if (nolocal_enabled)
+ qrstart_enabled = true;
+
+ for (i = 0; i < MAX_REMOTE_SLOTS; i++)
+ {
+ remote_clients[i].active = false;
+ remote_clients[i].pending_join = false;
+ remote_clients[i].pending_leave = false;
+ remote_clients[i].player_slot = -1;
+ remote_clients[i].input_bits = 0;
+ remote_clients[i].turn_axis = 0;
+ }
+
+ // Turning for a phone-driven slot goes through joyxmove[] (see
+ // G_SetRemoteInputState) to get the same cubic response curve a
+ // real analog joystick's x-axis gets in G_BuildTiccmd, rather than
+ // an on/off snap-to-full-speed turn. That branch is gated on
+ // use_analog, which is otherwise a user preference for their own
+ // hardware (default off) - force it on globally here since it only
+ // affects players whose turning comes through joyxmove/joyymove at
+ // all (a keyboard-only local player's are always 0, so this is a
+ // no-op for them).
+ use_analog = 1;
+
+ remote_lock = SDL_CreateMutex();
+ if (remote_lock == NULL)
+ return;
+
+ if (SDL_CreateThread(ListenerThreadFunc, "webinput-listener",
+ (void *)(intptr_t) port) == NULL)
+ {
+ printf("[webinput] failed to start listener thread, phone join disabled\n");
+ return;
+ }
+
+ webinput_enabled = true;
+}
+
+void I_WebInputTic(void)
+{
+ int i;
+
+ if (!webinput_enabled)
+ return;
+
+ SDL_LockMutex(remote_lock);
+
+ for (i = 0; i < MAX_REMOTE_SLOTS; i++)
+ {
+ remote_client_t *rc = &remote_clients[i];
+ boolean do_join = rc->pending_join;
+ boolean do_leave = rc->pending_leave;
+ unsigned int bits = rc->input_bits;
+ int turn = rc->turn_axis;
+
+ rc->pending_join = false;
+ rc->pending_leave = false;
+
+ if (do_leave)
+ {
+ SDL_UnlockMutex(remote_lock);
+ if (rc->player_slot >= 0)
+ G_RemoveLocalPlayer(rc->player_slot);
+ SDL_LockMutex(remote_lock);
+ rc->player_slot = -1;
+ rc->active = false;
+ continue;
+ }
+
+ if (do_join && rc->player_slot < 0)
+ {
+ int slot;
+ // Slots 0/1 are normally reserved for a local host/WASD
+ // zone (see MAX_REMOTE_SLOTS); under -nolocal there is no
+ // local player to reserve them for, so the first phone can
+ // take slot 0 itself.
+ int start_slot = nolocal_enabled ? 0 : 2;
+ SDL_UnlockMutex(remote_lock);
+ for (slot = start_slot; slot < MAXPLAYERS; slot++)
+ {
+ if (G_AddLocalPlayer(slot))
+ break;
+ }
+ SDL_LockMutex(remote_lock);
+
+ if (slot < MAXPLAYERS)
+ {
+ rc->player_slot = slot;
+ printf("[webinput] phone bound to player slot %d\n", slot);
+ }
+ }
+
+ if (rc->player_slot >= 0)
+ {
+ SDL_UnlockMutex(remote_lock);
+ G_SetRemoteInputState(rc->player_slot, bits, turn);
+ SDL_LockMutex(remote_lock);
+ }
+ }
+
+ SDL_UnlockMutex(remote_lock);
+}
+
+#else // _WIN32
+
+void I_WebInputInit(int port)
+{
+ // Not implemented on Windows yet - native POSIX sockets only for
+ // this spike (see i_webinput.c).
+}
+
+void I_WebInputTic(void)
+{
+}
+
+boolean I_WebInputQrEnabled(void)
+{
+ return false;
+}
+
+boolean I_WebInputNoLocalEnabled(void)
+{
+ return false;
+}
+
+void I_WebInputDrawJoinQR(int x, int y, int w, int h)
+{
+}
+
+#endif
diff --git a/src/doom/i_webinput.h b/src/doom/i_webinput.h
new file mode 100644
index 00000000..f9e12df9
--- /dev/null
+++ b/src/doom/i_webinput.h
@@ -0,0 +1,52 @@
+//
+// Copyright(C) 2026
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// DESCRIPTION:
+// Embedded HTTP/WebSocket server letting a phone browser join a
+// local splitscreen slot and drive it over the LAN. Spike/proof of
+// concept for the QR-code mobile-controller join flow - see
+// G_SetRemoteInputState (g_game.h) for how received input reaches
+// the game.
+//
+
+#ifndef __I_WEBINPUT__
+#define __I_WEBINPUT__
+
+#include "doomtype.h"
+
+// Starts the listener thread on the given TCP port. Safe to call once
+// at startup; does nothing on platforms without a POSIX sockets
+// implementation (currently Windows).
+void I_WebInputInit (int port);
+
+// Call once per game tic (see D_ProcessEvents). Binds newly-connected
+// phones to local player slots, releases slots whose phone disconnected,
+// and pushes each connected slot's latest input state into the game via
+// G_SetRemoteInputState.
+void I_WebInputTic (void);
+
+// Whether -qrstart is active and there's a join URL to show (see
+// i_webinput.c's PrintJoinURL). Checked by R_RenderSplitViews to
+// decide whether an empty splitscreen grid cell should get a join QR
+// instead of staying plain black.
+boolean I_WebInputQrEnabled (void);
+
+// Draws a join QR code, fit and centered, into the screen-buffer
+// rectangle (x, y, w, h). No-op if I_WebInputQrEnabled() is false.
+void I_WebInputDrawJoinQR (int x, int y, int w, int h);
+
+// Whether -nolocal was passed (no local keyboard/mouse player - every
+// player, including the first, joins via phone).
+boolean I_WebInputNoLocalEnabled (void);
+
+#endif
diff --git a/src/doom/p_enemy.c b/src/doom/p_enemy.c
index c6acf390..a0060b1d 100644
--- a/src/doom/p_enemy.c
+++ b/src/doom/p_enemy.c
@@ -501,6 +501,23 @@ P_LookForPlayers
angle_t an;
fixed_t dist;
+ // actor->lastlook is masked to 0-3 below, so this loop can only
+ // ever make progress by landing on one of the first 4 player
+ // slots - every "not in game" slot hits its lone continue, which
+ // skips straight past the only loop-exit check, without an actor
+ // ever actually being examined. Vanilla Doom could never call this
+ // with zero players in slots 0-3 (there was always at least one,
+ // from level start), so this was never reachable there. It is
+ // reachable now: -nolocal (i_webinput.c) starts a level with no
+ // one in the game yet, and a monster's very first think() call
+ // hits this and spins forever. Bail out up front for that one
+ // case; everything below is untouched vanilla logic otherwise.
+ if (!playeringame[0] && !playeringame[1]
+ && !playeringame[2] && !playeringame[3])
+ {
+ return false;
+ }
+
c = 0;
stop = (actor->lastlook-1)&3;
diff --git a/src/doom/p_mobj.c b/src/doom/p_mobj.c
index 481c5282..69b4152e 100644
--- a/src/doom/p_mobj.c
+++ b/src/doom/p_mobj.c
@@ -767,7 +767,7 @@ void P_SpawnMapThing (mapthing_t* mthing)
// count deathmatch start positions
if (mthing->type == 11)
{
- if (deathmatch_p < &deathmatchstarts[10])
+ if (deathmatch_p < &deathmatchstarts[MAX_DM_STARTS])
{
memcpy (deathmatch_p, mthing, sizeof(*mthing));
deathmatch_p++;
diff --git a/src/doom/p_mobj.h b/src/doom/p_mobj.h
index 90ed764b..04d28687 100644
--- a/src/doom/p_mobj.h
+++ b/src/doom/p_mobj.h
@@ -188,9 +188,10 @@ typedef enum
// Player sprites in multiplayer modes are modified
// using an internal color lookup table for re-indexing.
- // If 0x4 0x8 or 0xc,
- // use a translation table for player colormaps
- MF_TRANSLATION = 0xc000000,
+ // Widened from 2 bits (0xc000000, players 1-4) to 5 bits to cover
+ // up to 24 players (needs values 0-23); bits 27-30 were unused by
+ // any other flag, and bit 31 (the sign bit of this int) stays clear.
+ MF_TRANSLATION = 0x7c000000,
// Hmm ???.
MF_TRANSSHIFT = 26
diff --git a/src/doom/p_setup.c b/src/doom/p_setup.c
index fbb1a180..09748543 100644
--- a/src/doom/p_setup.c
+++ b/src/doom/p_setup.c
@@ -37,6 +37,7 @@
#include "doomdef.h"
#include "p_local.h"
#include "p_rejectpad.h"
+#include "p_setup.h"
#include "s_sound.h"
@@ -44,6 +45,7 @@
void P_SpawnMapThing (mapthing_t* mthing);
+void P_SpawnPlayer (mapthing_t* mthing);
//
@@ -104,7 +106,11 @@ byte* rejectmatrix;
// Maintain single and multi player starting spots.
-#define MAX_DEATHMATCH_STARTS 10
+// Some headroom above MAXPLAYERS: maps often define more deathmatch
+// starts than the vanilla 4-player cap needed, and having more than
+// MAXPLAYERS available gives G_DeathMatchSpawnPlayer's random retry loop
+// more spots to try before falling back to a fixed one.
+#define MAX_DEATHMATCH_STARTS 32
mapthing_t deathmatchstarts[MAX_DEATHMATCH_STARTS];
mapthing_t* deathmatch_p;
@@ -354,7 +360,17 @@ void P_LoadThings (int lump)
data = W_CacheLumpNum (lump,PU_STATIC);
numthings = W_LumpLength (lump) / sizeof(mapthing_t);
-
+
+ // Reset here (start of level load), not after: P_SpawnFallbackPlayer
+ // (used both by the loop below and by a player joining local
+ // splitscreen mid-level - see G_AddLocalPlayer) needs this to still
+ // reflect the CURRENT level's player starts for as long as the level
+ // is running, not just during this function.
+ for (i = 0; i < MAXPLAYERS; i++)
+ {
+ playerstartsingame[i] = false;
+ }
+
mt = (mapthing_t *)data;
for (i=0 ; i<numthings ; i++, mt++)
{
@@ -394,19 +410,59 @@ void P_LoadThings (int lump)
if (!deathmatch)
{
+ // Vanilla maps only define player starts for doomednums 1-4
+ // (players 1-4). For splitscreen players 5-10, there is no
+ // map-defined start to fall back on, so reuse one of the
+ // vanilla starts with a small position offset per extra
+ // player, rather than erroring out.
for (i = 0; i < MAXPLAYERS; i++)
{
if (playeringame[i] && !playerstartsingame[i])
{
- I_Error("P_LoadThings: Player %d start missing (vanilla crashes here)", i + 1);
+ if (!P_SpawnFallbackPlayer(i))
+ {
+ I_Error("P_LoadThings: no player starts in map, "
+ "cannot place player %d", i + 1);
+ }
}
- playerstartsingame[i] = false;
}
}
W_ReleaseLumpNum(lump);
}
+//
+// P_SpawnFallbackPlayer
+// Spawns playernum at a fallback start position, reusing one of the
+// map's own defined player starts (doomednum 1-4, tracked in
+// playerstartsingame[]) with a small position offset so extras don't
+// stack on top of each other. Used both by the loop above (players 5-10
+// at level load) and by a player joining local splitscreen mid-level
+// (see G_AddLocalPlayer in g_game.c). Returns false if the map has no
+// player start at all to fall back to.
+//
+boolean P_SpawnFallbackPlayer (int playernum)
+{
+ int j;
+ mapthing_t spawnthing;
+
+ for (j = 0; j < 4; j++)
+ {
+ if (playerstartsingame[j])
+ {
+ spawnthing = playerstarts[j];
+ spawnthing.type = playernum + 1;
+ spawnthing.x += (playernum - j) * 24;
+ spawnthing.y += (playernum - j) * 24;
+ playerstarts[playernum] = spawnthing;
+ P_SpawnPlayer(&spawnthing);
+ return true;
+ }
+ }
+
+ return false;
+}
+
//
// P_LoadLineDefs
diff --git a/src/doom/p_setup.h b/src/doom/p_setup.h
index 97ca104e..d7696e1a 100644
--- a/src/doom/p_setup.h
+++ b/src/doom/p_setup.h
@@ -36,4 +36,9 @@ P_SetupLevel
// Called by startup code.
void P_Init (void);
+// Spawns playernum at a fallback coop start position derived from the
+// current level's own player starts. See p_setup.c for details. Used by
+// G_AddLocalPlayer to bring in a local splitscreen player mid-level.
+boolean P_SpawnFallbackPlayer (int playernum);
+
#endif
diff --git a/src/doom/p_tick.c b/src/doom/p_tick.c
index 5a93b80f..eaee4541 100644
--- a/src/doom/p_tick.c
+++ b/src/doom/p_tick.c
@@ -143,7 +143,7 @@ void P_Ticker (void)
for (i=0 ; i<MAXPLAYERS ; i++)
if (playeringame[i])
P_PlayerThink (&players[i]);
-
+
P_RunThinkers ();
P_UpdateSpecials ();
P_RespawnSpecials ();
diff --git a/src/doom/qrcodegen.c b/src/doom/qrcodegen.c
new file mode 100644
index 00000000..34f10025
--- /dev/null
+++ b/src/doom/qrcodegen.c
@@ -0,0 +1,1027 @@
+/*
+ * QR Code generator library (C)
+ *
+ * Copyright (c) Project Nayuki. (MIT License)
+ * https://www.nayuki.io/page/qr-code-generator-library
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ * - The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * - The Software is provided "as is", without warranty of any kind, express or
+ * implied, including but not limited to the warranties of merchantability,
+ * fitness for a particular purpose and noninfringement. In no event shall the
+ * authors or copyright holders be liable for any claim, damages or other
+ * liability, whether in an action of contract, tort or otherwise, arising from,
+ * out of or in connection with the Software or the use or other dealings in the
+ * Software.
+ */
+
+#include <assert.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+#include "qrcodegen.h"
+
+#ifndef QRCODEGEN_TEST
+ #define testable static // Keep functions private
+#else
+ #define testable // Expose private functions
+#endif
+
+
+/*---- Forward declarations for private functions ----*/
+
+// Regarding all public and private functions defined in this source file:
+// - They require all pointer/array arguments to be not null unless the array length is zero.
+// - They only read input scalar/array arguments, write to output pointer/array
+// arguments, and return scalar values; they are "pure" functions.
+// - They don't read mutable global variables or write to any global variables.
+// - They don't perform I/O, read the clock, print to console, etc.
+// - They allocate a small and constant amount of stack memory.
+// - They don't allocate or free any memory on the heap.
+// - They don't recurse or mutually recurse. All the code
+// could be inlined into the top-level public functions.
+// - They run in at most quadratic time with respect to input arguments.
+// Most functions run in linear time, and some in constant time.
+// There are no unbounded loops or non-obvious termination conditions.
+// - They are completely thread-safe if the caller does not give the
+// same writable buffer to concurrent calls to these functions.
+
+testable void appendBitsToBuffer(unsigned int val, int numBits, uint8_t buffer[], int *bitLen);
+
+testable void addEccAndInterleave(uint8_t data[], int version, enum qrcodegen_Ecc ecl, uint8_t result[]);
+testable int getNumDataCodewords(int version, enum qrcodegen_Ecc ecl);
+testable int getNumRawDataModules(int ver);
+
+testable void reedSolomonComputeDivisor(int degree, uint8_t result[]);
+testable void reedSolomonComputeRemainder(const uint8_t data[], int dataLen,
+ const uint8_t generator[], int degree, uint8_t result[]);
+testable uint8_t reedSolomonMultiply(uint8_t x, uint8_t y);
+
+testable void initializeFunctionModules(int version, uint8_t qrcode[]);
+static void drawLightFunctionModules(uint8_t qrcode[], int version);
+static void drawFormatBits(enum qrcodegen_Ecc ecl, enum qrcodegen_Mask mask, uint8_t qrcode[]);
+testable int getAlignmentPatternPositions(int version, uint8_t result[7]);
+static void fillRectangle(int left, int top, int width, int height, uint8_t qrcode[]);
+
+static void drawCodewords(const uint8_t data[], int dataLen, uint8_t qrcode[]);
+static void applyMask(const uint8_t functionModules[], uint8_t qrcode[], enum qrcodegen_Mask mask);
+static long getPenaltyScore(const uint8_t qrcode[]);
+static int finderPenaltyCountPatterns(const int runHistory[7], int qrsize);
+static int finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, int runHistory[7], int qrsize);
+static void finderPenaltyAddHistory(int currentRunLength, int runHistory[7], int qrsize);
+
+testable bool getModuleBounded(const uint8_t qrcode[], int x, int y);
+testable void setModuleBounded(uint8_t qrcode[], int x, int y, bool isDark);
+testable void setModuleUnbounded(uint8_t qrcode[], int x, int y, bool isDark);
+static bool getBit(int x, int i);
+
+testable int calcSegmentBitLength(enum qrcodegen_Mode mode, size_t numChars);
+testable int getTotalBits(const struct qrcodegen_Segment segs[], size_t len, int version);
+static int numCharCountBits(enum qrcodegen_Mode mode, int version);
+
+
+
+/*---- Private tables of constants ----*/
+
+// The set of all legal characters in alphanumeric mode, where each character
+// value maps to the index in the string. For checking text and encoding segments.
+static const char *ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
+
+// Sentinel value for use in only some functions.
+#define LENGTH_OVERFLOW -1
+
+// For generating error correction codes.
+testable const int8_t ECC_CODEWORDS_PER_BLOCK[4][41] = {
+ // Version: (note that index 0 is for padding, and is set to an illegal value)
+ //0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
+ {-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Low
+ {-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28}, // Medium
+ {-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Quartile
+ {-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // High
+};
+
+#define qrcodegen_REED_SOLOMON_DEGREE_MAX 30 // Based on the table above
+
+// For generating error correction codes.
+testable const int8_t NUM_ERROR_CORRECTION_BLOCKS[4][41] = {
+ // Version: (note that index 0 is for padding, and is set to an illegal value)
+ //0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error correction level
+ {-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low
+ {-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium
+ {-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile
+ {-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High
+};
+
+// For automatic mask pattern selection.
+static const int PENALTY_N1 = 3;
+static const int PENALTY_N2 = 3;
+static const int PENALTY_N3 = 40;
+static const int PENALTY_N4 = 10;
+
+
+
+/*---- High-level QR Code encoding functions ----*/
+
+// Public function - see documentation comment in header file.
+bool qrcodegen_encodeText(const char *text, uint8_t tempBuffer[], uint8_t qrcode[],
+ enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl) {
+
+ size_t textLen = strlen(text);
+ if (textLen == 0)
+ return qrcodegen_encodeSegmentsAdvanced(NULL, 0, ecl, minVersion, maxVersion, mask, boostEcl, tempBuffer, qrcode);
+ size_t bufLen = (size_t)qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion);
+
+ struct qrcodegen_Segment seg;
+ if (qrcodegen_isNumeric(text)) {
+ if (qrcodegen_calcSegmentBufferSize(qrcodegen_Mode_NUMERIC, textLen) > bufLen)
+ goto fail;
+ seg = qrcodegen_makeNumeric(text, tempBuffer);
+ } else if (qrcodegen_isAlphanumeric(text)) {
+ if (qrcodegen_calcSegmentBufferSize(qrcodegen_Mode_ALPHANUMERIC, textLen) > bufLen)
+ goto fail;
+ seg = qrcodegen_makeAlphanumeric(text, tempBuffer);
+ } else {
+ if (textLen > bufLen)
+ goto fail;
+ for (size_t i = 0; i < textLen; i++)
+ tempBuffer[i] = (uint8_t)text[i];
+ seg.mode = qrcodegen_Mode_BYTE;
+ seg.bitLength = calcSegmentBitLength(seg.mode, textLen);
+ if (seg.bitLength == LENGTH_OVERFLOW)
+ goto fail;
+ seg.numChars = (int)textLen;
+ seg.data = tempBuffer;
+ }
+ return qrcodegen_encodeSegmentsAdvanced(&seg, 1, ecl, minVersion, maxVersion, mask, boostEcl, tempBuffer, qrcode);
+
+fail:
+ qrcode[0] = 0; // Set size to invalid value for safety
+ return false;
+}
+
+
+// Public function - see documentation comment in header file.
+bool qrcodegen_encodeBinary(uint8_t dataAndTemp[], size_t dataLen, uint8_t qrcode[],
+ enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl) {
+
+ struct qrcodegen_Segment seg;
+ seg.mode = qrcodegen_Mode_BYTE;
+ seg.bitLength = calcSegmentBitLength(seg.mode, dataLen);
+ if (seg.bitLength == LENGTH_OVERFLOW) {
+ qrcode[0] = 0; // Set size to invalid value for safety
+ return false;
+ }
+ seg.numChars = (int)dataLen;
+ seg.data = dataAndTemp;
+ return qrcodegen_encodeSegmentsAdvanced(&seg, 1, ecl, minVersion, maxVersion, mask, boostEcl, dataAndTemp, qrcode);
+}
+
+
+// Appends the given number of low-order bits of the given value to the given byte-based
+// bit buffer, increasing the bit length. Requires 0 <= numBits <= 16 and val < 2^numBits.
+testable void appendBitsToBuffer(unsigned int val, int numBits, uint8_t buffer[], int *bitLen) {
+ assert(0 <= numBits && numBits <= 16 && (unsigned long)val >> numBits == 0);
+ for (int i = numBits - 1; i >= 0; i--, (*bitLen)++)
+ buffer[*bitLen >> 3] |= ((val >> i) & 1) << (7 - (*bitLen & 7));
+}
+
+
+
+/*---- Low-level QR Code encoding functions ----*/
+
+// Public function - see documentation comment in header file.
+bool qrcodegen_encodeSegments(const struct qrcodegen_Segment segs[], size_t len,
+ enum qrcodegen_Ecc ecl, uint8_t tempBuffer[], uint8_t qrcode[]) {
+ return qrcodegen_encodeSegmentsAdvanced(segs, len, ecl,
+ qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true, tempBuffer, qrcode);
+}
+
+
+// Public function - see documentation comment in header file.
+bool qrcodegen_encodeSegmentsAdvanced(const struct qrcodegen_Segment segs[], size_t len, enum qrcodegen_Ecc ecl,
+ int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl, uint8_t tempBuffer[], uint8_t qrcode[]) {
+ assert(segs != NULL || len == 0);
+ assert(qrcodegen_VERSION_MIN <= minVersion && minVersion <= maxVersion && maxVersion <= qrcodegen_VERSION_MAX);
+ assert(0 <= (int)ecl && (int)ecl <= 3 && -1 <= (int)mask && (int)mask <= 7);
+
+ // Find the minimal version number to use
+ int version, dataUsedBits;
+ for (version = minVersion; ; version++) {
+ int dataCapacityBits = getNumDataCodewords(version, ecl) * 8; // Number of data bits available
+ dataUsedBits = getTotalBits(segs, len, version);
+ if (dataUsedBits != LENGTH_OVERFLOW && dataUsedBits <= dataCapacityBits)
+ break; // This version number is found to be suitable
+ if (version >= maxVersion) { // All versions in the range could not fit the given data
+ qrcode[0] = 0; // Set size to invalid value for safety
+ return false;
+ }
+ }
+ assert(dataUsedBits != LENGTH_OVERFLOW);
+
+ // Increase the error correction level while the data still fits in the current version number
+ for (int i = (int)qrcodegen_Ecc_MEDIUM; i <= (int)qrcodegen_Ecc_HIGH; i++) { // From low to high
+ if (boostEcl && dataUsedBits <= getNumDataCodewords(version, (enum qrcodegen_Ecc)i) * 8)
+ ecl = (enum qrcodegen_Ecc)i;
+ }
+
+ // Concatenate all segments to create the data bit string
+ memset(qrcode, 0, (size_t)qrcodegen_BUFFER_LEN_FOR_VERSION(version) * sizeof(qrcode[0]));
+ int bitLen = 0;
+ for (size_t i = 0; i < len; i++) {
+ const struct qrcodegen_Segment *seg = &segs[i];
+ appendBitsToBuffer((unsigned int)seg->mode, 4, qrcode, &bitLen);
+ appendBitsToBuffer((unsigned int)seg->numChars, numCharCountBits(seg->mode, version), qrcode, &bitLen);
+ for (int j = 0; j < seg->bitLength; j++) {
+ int bit = (seg->data[j >> 3] >> (7 - (j & 7))) & 1;
+ appendBitsToBuffer((unsigned int)bit, 1, qrcode, &bitLen);
+ }
+ }
+ assert(bitLen == dataUsedBits);
+
+ // Add terminator and pad up to a byte if applicable
+ int dataCapacityBits = getNumDataCodewords(version, ecl) * 8;
+ assert(bitLen <= dataCapacityBits);
+ int terminatorBits = dataCapacityBits - bitLen;
+ if (terminatorBits > 4)
+ terminatorBits = 4;
+ appendBitsToBuffer(0, terminatorBits, qrcode, &bitLen);
+ appendBitsToBuffer(0, (8 - bitLen % 8) % 8, qrcode, &bitLen);
+ assert(bitLen % 8 == 0);
+
+ // Pad with alternating bytes until data capacity is reached
+ for (uint8_t padByte = 0xEC; bitLen < dataCapacityBits; padByte ^= 0xEC ^ 0x11)
+ appendBitsToBuffer(padByte, 8, qrcode, &bitLen);
+
+ // Compute ECC, draw modules
+ addEccAndInterleave(qrcode, version, ecl, tempBuffer);
+ initializeFunctionModules(version, qrcode);
+ drawCodewords(tempBuffer, getNumRawDataModules(version) / 8, qrcode);
+ drawLightFunctionModules(qrcode, version);
+ initializeFunctionModules(version, tempBuffer);
+
+ // Do masking
+ if (mask == qrcodegen_Mask_AUTO) { // Automatically choose best mask
+ long minPenalty = LONG_MAX;
+ for (int i = 0; i < 8; i++) {
+ enum qrcodegen_Mask msk = (enum qrcodegen_Mask)i;
+ applyMask(tempBuffer, qrcode, msk);
+ drawFormatBits(ecl, msk, qrcode);
+ long penalty = getPenaltyScore(qrcode);
+ if (penalty < minPenalty) {
+ mask = msk;
+ minPenalty = penalty;
+ }
+ applyMask(tempBuffer, qrcode, msk); // Undoes the mask due to XOR
+ }
+ }
+ assert(0 <= (int)mask && (int)mask <= 7);
+ applyMask(tempBuffer, qrcode, mask); // Apply the final choice of mask
+ drawFormatBits(ecl, mask, qrcode); // Overwrite old format bits
+ return true;
+}
+
+
+
+/*---- Error correction code generation functions ----*/
+
+// Appends error correction bytes to each block of the given data array, then interleaves
+// bytes from the blocks and stores them in the result array. data[0 : dataLen] contains
+// the input data. data[dataLen : rawCodewords] is used as a temporary work area and will
+// be clobbered by this function. The final answer is stored in result[0 : rawCodewords].
+testable void addEccAndInterleave(uint8_t data[], int version, enum qrcodegen_Ecc ecl, uint8_t result[]) {
+ // Calculate parameter numbers
+ assert(0 <= (int)ecl && (int)ecl < 4 && qrcodegen_VERSION_MIN <= version && version <= qrcodegen_VERSION_MAX);
+ int numBlocks = NUM_ERROR_CORRECTION_BLOCKS[(int)ecl][version];
+ int blockEccLen = ECC_CODEWORDS_PER_BLOCK [(int)ecl][version];
+ int rawCodewords = getNumRawDataModules(version) / 8;
+ int dataLen = getNumDataCodewords(version, ecl);
+ int numShortBlocks = numBlocks - rawCodewords % numBlocks;
+ int shortBlockDataLen = rawCodewords / numBlocks - blockEccLen;
+
+ // Split data into blocks, calculate ECC, and interleave
+ // (not concatenate) the bytes into a single sequence
+ uint8_t rsdiv[qrcodegen_REED_SOLOMON_DEGREE_MAX];
+ reedSolomonComputeDivisor(blockEccLen, rsdiv);
+ const uint8_t *dat = data;
+ for (int i = 0; i < numBlocks; i++) {
+ int datLen = shortBlockDataLen + (i < numShortBlocks ? 0 : 1);
+ uint8_t *ecc = &data[dataLen]; // Temporary storage
+ reedSolomonComputeRemainder(dat, datLen, rsdiv, blockEccLen, ecc);
+ for (int j = 0, k = i; j < datLen; j++, k += numBlocks) { // Copy data
+ if (j == shortBlockDataLen)
+ k -= numShortBlocks;
+ result[k] = dat[j];
+ }
+ for (int j = 0, k = dataLen + i; j < blockEccLen; j++, k += numBlocks) // Copy ECC
+ result[k] = ecc[j];
+ dat += datLen;
+ }
+}
+
+
+// Returns the number of 8-bit codewords that can be used for storing data (not ECC),
+// for the given version number and error correction level. The result is in the range [9, 2956].
+testable int getNumDataCodewords(int version, enum qrcodegen_Ecc ecl) {
+ int v = version, e = (int)ecl;
+ assert(0 <= e && e < 4);
+ return getNumRawDataModules(v) / 8
+ - ECC_CODEWORDS_PER_BLOCK [e][v]
+ * NUM_ERROR_CORRECTION_BLOCKS[e][v];
+}
+
+
+// Returns the number of data bits that can be stored in a QR Code of the given version number, after
+// all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8.
+// The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table.
+testable int getNumRawDataModules(int ver) {
+ assert(qrcodegen_VERSION_MIN <= ver && ver <= qrcodegen_VERSION_MAX);
+ int result = (16 * ver + 128) * ver + 64;
+ if (ver >= 2) {
+ int numAlign = ver / 7 + 2;
+ result -= (25 * numAlign - 10) * numAlign - 55;
+ if (ver >= 7)
+ result -= 36;
+ }
+ assert(208 <= result && result <= 29648);
+ return result;
+}
+
+
+
+/*---- Reed-Solomon ECC generator functions ----*/
+
+// Computes a Reed-Solomon ECC generator polynomial for the given degree, storing in result[0 : degree].
+// This could be implemented as a lookup table over all possible parameter values, instead of as an algorithm.
+testable void reedSolomonComputeDivisor(int degree, uint8_t result[]) {
+ assert(1 <= degree && degree <= qrcodegen_REED_SOLOMON_DEGREE_MAX);
+ // Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1.
+ // For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array {255, 8, 93}.
+ memset(result, 0, (size_t)degree * sizeof(result[0]));
+ result[degree - 1] = 1; // Start off with the monomial x^0
+
+ // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}),
+ // drop the highest monomial term which is always 1x^degree.
+ // Note that r = 0x02, which is a generator element of this field GF(2^8/0x11D).
+ uint8_t root = 1;
+ for (int i = 0; i < degree; i++) {
+ // Multiply the current product by (x - r^i)
+ for (int j = 0; j < degree; j++) {
+ result[j] = reedSolomonMultiply(result[j], root);
+ if (j + 1 < degree)
+ result[j] ^= result[j + 1];
+ }
+ root = reedSolomonMultiply(root, 0x02);
+ }
+}
+
+
+// Computes the Reed-Solomon error correction codeword for the given data and divisor polynomials.
+// The remainder when data[0 : dataLen] is divided by divisor[0 : degree] is stored in result[0 : degree].
+// All polynomials are in big endian, and the generator has an implicit leading 1 term.
+testable void reedSolomonComputeRemainder(const uint8_t data[], int dataLen,
+ const uint8_t generator[], int degree, uint8_t result[]) {
+ assert(1 <= degree && degree <= qrcodegen_REED_SOLOMON_DEGREE_MAX);
+ memset(result, 0, (size_t)degree * sizeof(result[0]));
+ for (int i = 0; i < dataLen; i++) { // Polynomial division
+ uint8_t factor = data[i] ^ result[0];
+ memmove(&result[0], &result[1], (size_t)(degree - 1) * sizeof(result[0]));
+ result[degree - 1] = 0;
+ for (int j = 0; j < degree; j++)
+ result[j] ^= reedSolomonMultiply(generator[j], factor);
+ }
+}
+
+#undef qrcodegen_REED_SOLOMON_DEGREE_MAX
+
+
+// Returns the product of the two given field elements modulo GF(2^8/0x11D).
+// All inputs are valid. This could be implemented as a 256*256 lookup table.
+testable uint8_t reedSolomonMultiply(uint8_t x, uint8_t y) {
+ // Russian peasant multiplication
+ uint8_t z = 0;
+ for (int i = 7; i >= 0; i--) {
+ z = (uint8_t)((z << 1) ^ ((z >> 7) * 0x11D));
+ z ^= ((y >> i) & 1) * x;
+ }
+ return z;
+}
+
+
+
+/*---- Drawing function modules ----*/
+
+// Clears the given QR Code grid with light modules for the given
+// version's size, then marks every function module as dark.
+testable void initializeFunctionModules(int version, uint8_t qrcode[]) {
+ // Initialize QR Code
+ int qrsize = version * 4 + 17;
+ memset(qrcode, 0, (size_t)((qrsize * qrsize + 7) / 8 + 1) * sizeof(qrcode[0]));
+ qrcode[0] = (uint8_t)qrsize;
+
+ // Fill horizontal and vertical timing patterns
+ fillRectangle(6, 0, 1, qrsize, qrcode);
+ fillRectangle(0, 6, qrsize, 1, qrcode);
+
+ // Fill 3 finder patterns (all corners except bottom right) and format bits
+ fillRectangle(0, 0, 9, 9, qrcode);
+ fillRectangle(qrsize - 8, 0, 8, 9, qrcode);
+ fillRectangle(0, qrsize - 8, 9, 8, qrcode);
+
+ // Fill numerous alignment patterns
+ uint8_t alignPatPos[7];
+ int numAlign = getAlignmentPatternPositions(version, alignPatPos);
+ for (int i = 0; i < numAlign; i++) {
+ for (int j = 0; j < numAlign; j++) {
+ // Don't draw on the three finder corners
+ if (!((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) || (i == numAlign - 1 && j == 0)))
+ fillRectangle(alignPatPos[i] - 2, alignPatPos[j] - 2, 5, 5, qrcode);
+ }
+ }
+
+ // Fill version blocks
+ if (version >= 7) {
+ fillRectangle(qrsize - 11, 0, 3, 6, qrcode);
+ fillRectangle(0, qrsize - 11, 6, 3, qrcode);
+ }
+}
+
+
+// Draws light function modules and possibly some dark modules onto the given QR Code, without changing
+// non-function modules. This does not draw the format bits. This requires all function modules to be previously
+// marked dark (namely by initializeFunctionModules()), because this may skip redrawing dark function modules.
+static void drawLightFunctionModules(uint8_t qrcode[], int version) {
+ // Draw horizontal and vertical timing patterns
+ int qrsize = qrcodegen_getSize(qrcode);
+ for (int i = 7; i < qrsize - 7; i += 2) {
+ setModuleBounded(qrcode, 6, i, false);
+ setModuleBounded(qrcode, i, 6, false);
+ }
+
+ // Draw 3 finder patterns (all corners except bottom right; overwrites some timing modules)
+ for (int dy = -4; dy <= 4; dy++) {
+ for (int dx = -4; dx <= 4; dx++) {
+ int dist = abs(dx);
+ if (abs(dy) > dist)
+ dist = abs(dy);
+ if (dist == 2 || dist == 4) {
+ setModuleUnbounded(qrcode, 3 + dx, 3 + dy, false);
+ setModuleUnbounded(qrcode, qrsize - 4 + dx, 3 + dy, false);
+ setModuleUnbounded(qrcode, 3 + dx, qrsize - 4 + dy, false);
+ }
+ }
+ }
+
+ // Draw numerous alignment patterns
+ uint8_t alignPatPos[7];
+ int numAlign = getAlignmentPatternPositions(version, alignPatPos);
+ for (int i = 0; i < numAlign; i++) {
+ for (int j = 0; j < numAlign; j++) {
+ if ((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) || (i == numAlign - 1 && j == 0))
+ continue; // Don't draw on the three finder corners
+ for (int dy = -1; dy <= 1; dy++) {
+ for (int dx = -1; dx <= 1; dx++)
+ setModuleBounded(qrcode, alignPatPos[i] + dx, alignPatPos[j] + dy, dx == 0 && dy == 0);
+ }
+ }
+ }
+
+ // Draw version blocks
+ if (version >= 7) {
+ // Calculate error correction code and pack bits
+ int rem = version; // version is uint6, in the range [7, 40]
+ for (int i = 0; i < 12; i++)
+ rem = (rem << 1) ^ ((rem >> 11) * 0x1F25);
+ long bits = (long)version << 12 | rem; // uint18
+ assert(bits >> 18 == 0);
+
+ // Draw two copies
+ for (int i = 0; i < 6; i++) {
+ for (int j = 0; j < 3; j++) {
+ int k = qrsize - 11 + j;
+ setModuleBounded(qrcode, k, i, (bits & 1) != 0);
+ setModuleBounded(qrcode, i, k, (bits & 1) != 0);
+ bits >>= 1;
+ }
+ }
+ }
+}
+
+
+// Draws two copies of the format bits (with its own error correction code) based
+// on the given mask and error correction level. This always draws all modules of
+// the format bits, unlike drawLightFunctionModules() which might skip dark modules.
+static void drawFormatBits(enum qrcodegen_Ecc ecl, enum qrcodegen_Mask mask, uint8_t qrcode[]) {
+ // Calculate error correction code and pack bits
+ assert(0 <= (int)mask && (int)mask <= 7);
+ static const int table[] = {1, 0, 3, 2};
+ int data = table[(int)ecl] << 3 | (int)mask; // errCorrLvl is uint2, mask is uint3
+ int rem = data;
+ for (int i = 0; i < 10; i++)
+ rem = (rem << 1) ^ ((rem >> 9) * 0x537);
+ int bits = (data << 10 | rem) ^ 0x5412; // uint15
+ assert(bits >> 15 == 0);
+
+ // Draw first copy
+ for (int i = 0; i <= 5; i++)
+ setModuleBounded(qrcode, 8, i, getBit(bits, i));
+ setModuleBounded(qrcode, 8, 7, getBit(bits, 6));
+ setModuleBounded(qrcode, 8, 8, getBit(bits, 7));
+ setModuleBounded(qrcode, 7, 8, getBit(bits, 8));
+ for (int i = 9; i < 15; i++)
+ setModuleBounded(qrcode, 14 - i, 8, getBit(bits, i));
+
+ // Draw second copy
+ int qrsize = qrcodegen_getSize(qrcode);
+ for (int i = 0; i < 8; i++)
+ setModuleBounded(qrcode, qrsize - 1 - i, 8, getBit(bits, i));
+ for (int i = 8; i < 15; i++)
+ setModuleBounded(qrcode, 8, qrsize - 15 + i, getBit(bits, i));
+ setModuleBounded(qrcode, 8, qrsize - 8, true); // Always dark
+}
+
+
+// Calculates and stores an ascending list of positions of alignment patterns
+// for this version number, returning the length of the list (in the range [0,7]).
+// Each position is in the range [0,177), and are used on both the x and y axes.
+// This could be implemented as lookup table of 40 variable-length lists of unsigned bytes.
+testable int getAlignmentPatternPositions(int version, uint8_t result[7]) {
+ if (version == 1)
+ return 0;
+ int numAlign = version / 7 + 2;
+ int step = (version * 8 + numAlign * 3 + 5) / (numAlign * 4 - 4) * 2;
+ for (int i = numAlign - 1, pos = version * 4 + 10; i >= 1; i--, pos -= step)
+ result[i] = (uint8_t)pos;
+ result[0] = 6;
+ return numAlign;
+}
+
+
+// Sets every module in the range [left : left + width] * [top : top + height] to dark.
+static void fillRectangle(int left, int top, int width, int height, uint8_t qrcode[]) {
+ for (int dy = 0; dy < height; dy++) {
+ for (int dx = 0; dx < width; dx++)
+ setModuleBounded(qrcode, left + dx, top + dy, true);
+ }
+}
+
+
+
+/*---- Drawing data modules and masking ----*/
+
+// Draws the raw codewords (including data and ECC) onto the given QR Code. This requires the initial state of
+// the QR Code to be dark at function modules and light at codeword modules (including unused remainder bits).
+static void drawCodewords(const uint8_t data[], int dataLen, uint8_t qrcode[]) {
+ int qrsize = qrcodegen_getSize(qrcode);
+ int i = 0; // Bit index into the data
+ // Do the funny zigzag scan
+ for (int right = qrsize - 1; right >= 1; right -= 2) { // Index of right column in each column pair
+ if (right == 6)
+ right = 5;
+ for (int vert = 0; vert < qrsize; vert++) { // Vertical counter
+ for (int j = 0; j < 2; j++) {
+ int x = right - j; // Actual x coordinate
+ bool upward = ((right + 1) & 2) == 0;
+ int y = upward ? qrsize - 1 - vert : vert; // Actual y coordinate
+ if (!getModuleBounded(qrcode, x, y) && i < dataLen * 8) {
+ bool dark = getBit(data[i >> 3], 7 - (i & 7));
+ setModuleBounded(qrcode, x, y, dark);
+ i++;
+ }
+ // If this QR Code has any remainder bits (0 to 7), they were assigned as
+ // 0/false/light by the constructor and are left unchanged by this method
+ }
+ }
+ }
+ assert(i == dataLen * 8);
+}
+
+
+// XORs the codeword modules in this QR Code with the given mask pattern
+// and given pattern of function modules. The codeword bits must be drawn
+// before masking. Due to the arithmetic of XOR, calling applyMask() with
+// the same mask value a second time will undo the mask. A final well-formed
+// QR Code needs exactly one (not zero, two, etc.) mask applied.
+static void applyMask(const uint8_t functionModules[], uint8_t qrcode[], enum qrcodegen_Mask mask) {
+ assert(0 <= (int)mask && (int)mask <= 7); // Disallows qrcodegen_Mask_AUTO
+ int qrsize = qrcodegen_getSize(qrcode);
+ for (int y = 0; y < qrsize; y++) {
+ for (int x = 0; x < qrsize; x++) {
+ if (getModuleBounded(functionModules, x, y))
+ continue;
+ bool invert;
+ switch ((int)mask) {
+ case 0: invert = (x + y) % 2 == 0; break;
+ case 1: invert = y % 2 == 0; break;
+ case 2: invert = x % 3 == 0; break;
+ case 3: invert = (x + y) % 3 == 0; break;
+ case 4: invert = (x / 3 + y / 2) % 2 == 0; break;
+ case 5: invert = x * y % 2 + x * y % 3 == 0; break;
+ case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break;
+ case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break;
+ default: assert(false); return;
+ }
+ bool val = getModuleBounded(qrcode, x, y);
+ setModuleBounded(qrcode, x, y, val ^ invert);
+ }
+ }
+}
+
+
+// Calculates and returns the penalty score based on state of the given QR Code's current modules.
+// This is used by the automatic mask choice algorithm to find the mask pattern that yields the lowest score.
+static long getPenaltyScore(const uint8_t qrcode[]) {
+ int qrsize = qrcodegen_getSize(qrcode);
+ long result = 0;
+
+ // Adjacent modules in row having same color, and finder-like patterns
+ for (int y = 0; y < qrsize; y++) {
+ bool runColor = false;
+ int runX = 0;
+ int runHistory[7] = {0};
+ for (int x = 0; x < qrsize; x++) {
+ if (getModuleBounded(qrcode, x, y) == runColor) {
+ runX++;
+ if (runX == 5)
+ result += PENALTY_N1;
+ else if (runX > 5)
+ result++;
+ } else {
+ finderPenaltyAddHistory(runX, runHistory, qrsize);
+ if (!runColor)
+ result += finderPenaltyCountPatterns(runHistory, qrsize) * PENALTY_N3;
+ runColor = getModuleBounded(qrcode, x, y);
+ runX = 1;
+ }
+ }
+ result += finderPenaltyTerminateAndCount(runColor, runX, runHistory, qrsize) * PENALTY_N3;
+ }
+ // Adjacent modules in column having same color, and finder-like patterns
+ for (int x = 0; x < qrsize; x++) {
+ bool runColor = false;
+ int runY = 0;
+ int runHistory[7] = {0};
+ for (int y = 0; y < qrsize; y++) {
+ if (getModuleBounded(qrcode, x, y) == runColor) {
+ runY++;
+ if (runY == 5)
+ result += PENALTY_N1;
+ else if (runY > 5)
+ result++;
+ } else {
+ finderPenaltyAddHistory(runY, runHistory, qrsize);
+ if (!runColor)
+ result += finderPenaltyCountPatterns(runHistory, qrsize) * PENALTY_N3;
+ runColor = getModuleBounded(qrcode, x, y);
+ runY = 1;
+ }
+ }
+ result += finderPenaltyTerminateAndCount(runColor, runY, runHistory, qrsize) * PENALTY_N3;
+ }
+
+ // 2*2 blocks of modules having same color
+ for (int y = 0; y < qrsize - 1; y++) {
+ for (int x = 0; x < qrsize - 1; x++) {
+ bool color = getModuleBounded(qrcode, x, y);
+ if ( color == getModuleBounded(qrcode, x + 1, y) &&
+ color == getModuleBounded(qrcode, x, y + 1) &&
+ color == getModuleBounded(qrcode, x + 1, y + 1))
+ result += PENALTY_N2;
+ }
+ }
+
+ // Balance of dark and light modules
+ int dark = 0;
+ for (int y = 0; y < qrsize; y++) {
+ for (int x = 0; x < qrsize; x++) {
+ if (getModuleBounded(qrcode, x, y))
+ dark++;
+ }
+ }
+ int total = qrsize * qrsize; // Note that size is odd, so dark/total != 1/2
+ // Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= (55+5k)%
+ int k = (int)((labs(dark * 20L - total * 10L) + total - 1) / total) - 1;
+ assert(0 <= k && k <= 9);
+ result += k * PENALTY_N4;
+ assert(0 <= result && result <= 2568888L); // Non-tight upper bound based on default values of PENALTY_N1, ..., N4
+ return result;
+}
+
+
+// Can only be called immediately after a light run is added, and
+// returns either 0, 1, or 2. A helper function for getPenaltyScore().
+static int finderPenaltyCountPatterns(const int runHistory[7], int qrsize) {
+ int n = runHistory[1];
+ assert(n <= qrsize * 3); (void)qrsize;
+ bool core = n > 0 && runHistory[2] == n && runHistory[3] == n * 3 && runHistory[4] == n && runHistory[5] == n;
+ // The maximum QR Code size is 177, hence the dark run length n <= 177.
+ // Arithmetic is promoted to int, so n*4 will not overflow.
+ return (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0)
+ + (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0);
+}
+
+
+// Must be called at the end of a line (row or column) of modules. A helper function for getPenaltyScore().
+static int finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, int runHistory[7], int qrsize) {
+ if (currentRunColor) { // Terminate dark run
+ finderPenaltyAddHistory(currentRunLength, runHistory, qrsize);
+ currentRunLength = 0;
+ }
+ currentRunLength += qrsize; // Add light border to final run
+ finderPenaltyAddHistory(currentRunLength, runHistory, qrsize);
+ return finderPenaltyCountPatterns(runHistory, qrsize);
+}
+
+
+// Pushes the given value to the front and drops the last value. A helper function for getPenaltyScore().
+static void finderPenaltyAddHistory(int currentRunLength, int runHistory[7], int qrsize) {
+ if (runHistory[0] == 0)
+ currentRunLength += qrsize; // Add light border to initial run
+ memmove(&runHistory[1], &runHistory[0], 6 * sizeof(runHistory[0]));
+ runHistory[0] = currentRunLength;
+}
+
+
+
+/*---- Basic QR Code information ----*/
+
+// Public function - see documentation comment in header file.
+int qrcodegen_getSize(const uint8_t qrcode[]) {
+ assert(qrcode != NULL);
+ int result = qrcode[0];
+ assert((qrcodegen_VERSION_MIN * 4 + 17) <= result
+ && result <= (qrcodegen_VERSION_MAX * 4 + 17));
+ return result;
+}
+
+
+// Public function - see documentation comment in header file.
+bool qrcodegen_getModule(const uint8_t qrcode[], int x, int y) {
+ assert(qrcode != NULL);
+ int qrsize = qrcode[0];
+ return (0 <= x && x < qrsize && 0 <= y && y < qrsize) && getModuleBounded(qrcode, x, y);
+}
+
+
+// Returns the color of the module at the given coordinates, which must be in bounds.
+testable bool getModuleBounded(const uint8_t qrcode[], int x, int y) {
+ int qrsize = qrcode[0];
+ assert(21 <= qrsize && qrsize <= 177 && 0 <= x && x < qrsize && 0 <= y && y < qrsize);
+ int index = y * qrsize + x;
+ return getBit(qrcode[(index >> 3) + 1], index & 7);
+}
+
+
+// Sets the color of the module at the given coordinates, which must be in bounds.
+testable void setModuleBounded(uint8_t qrcode[], int x, int y, bool isDark) {
+ int qrsize = qrcode[0];
+ assert(21 <= qrsize && qrsize <= 177 && 0 <= x && x < qrsize && 0 <= y && y < qrsize);
+ int index = y * qrsize + x;
+ int bitIndex = index & 7;
+ int byteIndex = (index >> 3) + 1;
+ if (isDark)
+ qrcode[byteIndex] |= 1 << bitIndex;
+ else
+ qrcode[byteIndex] &= (1 << bitIndex) ^ 0xFF;
+}
+
+
+// Sets the color of the module at the given coordinates, doing nothing if out of bounds.
+testable void setModuleUnbounded(uint8_t qrcode[], int x, int y, bool isDark) {
+ int qrsize = qrcode[0];
+ if (0 <= x && x < qrsize && 0 <= y && y < qrsize)
+ setModuleBounded(qrcode, x, y, isDark);
+}
+
+
+// Returns true iff the i'th bit of x is set to 1. Requires x >= 0 and 0 <= i <= 14.
+static bool getBit(int x, int i) {
+ return ((x >> i) & 1) != 0;
+}
+
+
+
+/*---- Segment handling ----*/
+
+// Public function - see documentation comment in header file.
+bool qrcodegen_isNumeric(const char *text) {
+ assert(text != NULL);
+ for (; *text != '\0'; text++) {
+ if (*text < '0' || *text > '9')
+ return false;
+ }
+ return true;
+}
+
+
+// Public function - see documentation comment in header file.
+bool qrcodegen_isAlphanumeric(const char *text) {
+ assert(text != NULL);
+ for (; *text != '\0'; text++) {
+ if (strchr(ALPHANUMERIC_CHARSET, *text) == NULL)
+ return false;
+ }
+ return true;
+}
+
+
+// Public function - see documentation comment in header file.
+size_t qrcodegen_calcSegmentBufferSize(enum qrcodegen_Mode mode, size_t numChars) {
+ int temp = calcSegmentBitLength(mode, numChars);
+ if (temp == LENGTH_OVERFLOW)
+ return SIZE_MAX;
+ assert(0 <= temp && temp <= INT16_MAX);
+ return ((size_t)temp + 7) / 8;
+}
+
+
+// Returns the number of data bits needed to represent a segment
+// containing the given number of characters using the given mode. Notes:
+// - Returns LENGTH_OVERFLOW on failure, i.e. numChars > INT16_MAX
+// or the number of needed bits exceeds INT16_MAX (i.e. 32767).
+// - Otherwise, all valid results are in the range [0, INT16_MAX].
+// - For byte mode, numChars measures the number of bytes, not Unicode code points.
+// - For ECI mode, numChars must be 0, and the worst-case number of bits is returned.
+// An actual ECI segment can have shorter data. For non-ECI modes, the result is exact.
+testable int calcSegmentBitLength(enum qrcodegen_Mode mode, size_t numChars) {
+ // All calculations are designed to avoid overflow on all platforms
+ if (numChars > (unsigned int)INT16_MAX)
+ return LENGTH_OVERFLOW;
+ long result = (long)numChars;
+ if (mode == qrcodegen_Mode_NUMERIC)
+ result = (result * 10 + 2) / 3; // ceil(10/3 * n)
+ else if (mode == qrcodegen_Mode_ALPHANUMERIC)
+ result = (result * 11 + 1) / 2; // ceil(11/2 * n)
+ else if (mode == qrcodegen_Mode_BYTE)
+ result *= 8;
+ else if (mode == qrcodegen_Mode_KANJI)
+ result *= 13;
+ else if (mode == qrcodegen_Mode_ECI && numChars == 0)
+ result = 3 * 8;
+ else { // Invalid argument
+ assert(false);
+ return LENGTH_OVERFLOW;
+ }
+ assert(result >= 0);
+ if (result > INT16_MAX)
+ return LENGTH_OVERFLOW;
+ return (int)result;
+}
+
+
+// Public function - see documentation comment in header file.
+struct qrcodegen_Segment qrcodegen_makeBytes(const uint8_t data[], size_t len, uint8_t buf[]) {
+ assert(data != NULL || len == 0);
+ struct qrcodegen_Segment result;
+ result.mode = qrcodegen_Mode_BYTE;
+ result.bitLength = calcSegmentBitLength(result.mode, len);
+ assert(result.bitLength != LENGTH_OVERFLOW);
+ result.numChars = (int)len;
+ if (len > 0)
+ memcpy(buf, data, len * sizeof(buf[0]));
+ result.data = buf;
+ return result;
+}
+
+
+// Public function - see documentation comment in header file.
+struct qrcodegen_Segment qrcodegen_makeNumeric(const char *digits, uint8_t buf[]) {
+ assert(digits != NULL);
+ struct qrcodegen_Segment result;
+ size_t len = strlen(digits);
+ result.mode = qrcodegen_Mode_NUMERIC;
+ int bitLen = calcSegmentBitLength(result.mode, len);
+ assert(bitLen != LENGTH_OVERFLOW);
+ result.numChars = (int)len;
+ if (bitLen > 0)
+ memset(buf, 0, ((size_t)bitLen + 7) / 8 * sizeof(buf[0]));
+ result.bitLength = 0;
+
+ unsigned int accumData = 0;
+ int accumCount = 0;
+ for (; *digits != '\0'; digits++) {
+ char c = *digits;
+ assert('0' <= c && c <= '9');
+ accumData = accumData * 10 + (unsigned int)(c - '0');
+ accumCount++;
+ if (accumCount == 3) {
+ appendBitsToBuffer(accumData, 10, buf, &result.bitLength);
+ accumData = 0;
+ accumCount = 0;
+ }
+ }
+ if (accumCount > 0) // 1 or 2 digits remaining
+ appendBitsToBuffer(accumData, accumCount * 3 + 1, buf, &result.bitLength);
+ assert(result.bitLength == bitLen);
+ result.data = buf;
+ return result;
+}
+
+
+// Public function - see documentation comment in header file.
+struct qrcodegen_Segment qrcodegen_makeAlphanumeric(const char *text, uint8_t buf[]) {
+ assert(text != NULL);
+ struct qrcodegen_Segment result;
+ size_t len = strlen(text);
+ result.mode = qrcodegen_Mode_ALPHANUMERIC;
+ int bitLen = calcSegmentBitLength(result.mode, len);
+ assert(bitLen != LENGTH_OVERFLOW);
+ result.numChars = (int)len;
+ if (bitLen > 0)
+ memset(buf, 0, ((size_t)bitLen + 7) / 8 * sizeof(buf[0]));
+ result.bitLength = 0;
+
+ unsigned int accumData = 0;
+ int accumCount = 0;
+ for (; *text != '\0'; text++) {
+ const char *temp = strchr(ALPHANUMERIC_CHARSET, *text);
+ assert(temp != NULL);
+ accumData = accumData * 45 + (unsigned int)(temp - ALPHANUMERIC_CHARSET);
+ accumCount++;
+ if (accumCount == 2) {
+ appendBitsToBuffer(accumData, 11, buf, &result.bitLength);
+ accumData = 0;
+ accumCount = 0;
+ }
+ }
+ if (accumCount > 0) // 1 character remaining
+ appendBitsToBuffer(accumData, 6, buf, &result.bitLength);
+ assert(result.bitLength == bitLen);
+ result.data = buf;
+ return result;
+}
+
+
+// Public function - see documentation comment in header file.
+struct qrcodegen_Segment qrcodegen_makeEci(long assignVal, uint8_t buf[]) {
+ struct qrcodegen_Segment result;
+ result.mode = qrcodegen_Mode_ECI;
+ result.numChars = 0;
+ result.bitLength = 0;
+ if (assignVal < 0)
+ assert(false);
+ else if (assignVal < (1 << 7)) {
+ memset(buf, 0, 1 * sizeof(buf[0]));
+ appendBitsToBuffer((unsigned int)assignVal, 8, buf, &result.bitLength);
+ } else if (assignVal < (1 << 14)) {
+ memset(buf, 0, 2 * sizeof(buf[0]));
+ appendBitsToBuffer(2, 2, buf, &result.bitLength);
+ appendBitsToBuffer((unsigned int)assignVal, 14, buf, &result.bitLength);
+ } else if (assignVal < 1000000L) {
+ memset(buf, 0, 3 * sizeof(buf[0]));
+ appendBitsToBuffer(6, 3, buf, &result.bitLength);
+ appendBitsToBuffer((unsigned int)(assignVal >> 10), 11, buf, &result.bitLength);
+ appendBitsToBuffer((unsigned int)(assignVal & 0x3FF), 10, buf, &result.bitLength);
+ } else
+ assert(false);
+ result.data = buf;
+ return result;
+}
+
+
+// Calculates the number of bits needed to encode the given segments at the given version.
+// Returns a non-negative number if successful. Otherwise returns LENGTH_OVERFLOW if a segment
+// has too many characters to fit its length field, or the total bits exceeds INT16_MAX.
+testable int getTotalBits(const struct qrcodegen_Segment segs[], size_t len, int version) {
+ assert(segs != NULL || len == 0);
+ long result = 0;
+ for (size_t i = 0; i < len; i++) {
+ int numChars = segs[i].numChars;
+ int bitLength = segs[i].bitLength;
+ assert(0 <= numChars && numChars <= INT16_MAX);
+ assert(0 <= bitLength && bitLength <= INT16_MAX);
+ int ccbits = numCharCountBits(segs[i].mode, version);
+ assert(0 <= ccbits && ccbits <= 16);
+ if (numChars >= (1L << ccbits))
+ return LENGTH_OVERFLOW; // The segment's length doesn't fit the field's bit width
+ result += 4L + ccbits + bitLength;
+ if (result > INT16_MAX)
+ return LENGTH_OVERFLOW; // The sum might overflow an int type
+ }
+ assert(0 <= result && result <= INT16_MAX);
+ return (int)result;
+}
+
+
+// Returns the bit width of the character count field for a segment in the given mode
+// in a QR Code at the given version number. The result is in the range [0, 16].
+static int numCharCountBits(enum qrcodegen_Mode mode, int version) {
+ assert(qrcodegen_VERSION_MIN <= version && version <= qrcodegen_VERSION_MAX);
+ int i = (version + 7) / 17;
+ switch (mode) {
+ case qrcodegen_Mode_NUMERIC : { static const int temp[] = {10, 12, 14}; return temp[i]; }
+ case qrcodegen_Mode_ALPHANUMERIC: { static const int temp[] = { 9, 11, 13}; return temp[i]; }
+ case qrcodegen_Mode_BYTE : { static const int temp[] = { 8, 16, 16}; return temp[i]; }
+ case qrcodegen_Mode_KANJI : { static const int temp[] = { 8, 10, 12}; return temp[i]; }
+ case qrcodegen_Mode_ECI : return 0;
+ default: assert(false); return -1; // Dummy value
+ }
+}
+
+
+#undef LENGTH_OVERFLOW
diff --git a/src/doom/qrcodegen.h b/src/doom/qrcodegen.h
new file mode 100644
index 00000000..6bbc1576
--- /dev/null
+++ b/src/doom/qrcodegen.h
@@ -0,0 +1,385 @@
+/*
+ * QR Code generator library (C)
+ *
+ * Copyright (c) Project Nayuki. (MIT License)
+ * https://www.nayuki.io/page/qr-code-generator-library
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ * - The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * - The Software is provided "as is", without warranty of any kind, express or
+ * implied, including but not limited to the warranties of merchantability,
+ * fitness for a particular purpose and noninfringement. In no event shall the
+ * authors or copyright holders be liable for any claim, damages or other
+ * liability, whether in an action of contract, tort or otherwise, arising from,
+ * out of or in connection with the Software or the use or other dealings in the
+ * Software.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+/*
+ * This library creates QR Code symbols, which is a type of two-dimension barcode.
+ * Invented by Denso Wave and described in the ISO/IEC 18004 standard.
+ * A QR Code structure is an immutable square grid of dark and light cells.
+ * The library provides functions to create a QR Code from text or binary data.
+ * The library covers the QR Code Model 2 specification, supporting all versions (sizes)
+ * from 1 to 40, all 4 error correction levels, and 4 character encoding modes.
+ *
+ * Ways to create a QR Code object:
+ * - High level: Take the payload data and call qrcodegen_encodeText() or qrcodegen_encodeBinary().
+ * - Low level: Custom-make the list of segments and call
+ * qrcodegen_encodeSegments() or qrcodegen_encodeSegmentsAdvanced().
+ * (Note that all ways require supplying the desired error correction level and various byte buffers.)
+ */
+
+
+/*---- Enum and struct types----*/
+
+/*
+ * The error correction level in a QR Code symbol.
+ */
+enum qrcodegen_Ecc {
+ // Must be declared in ascending order of error protection
+ // so that an internal qrcodegen function works properly
+ qrcodegen_Ecc_LOW = 0 , // The QR Code can tolerate about 7% erroneous codewords
+ qrcodegen_Ecc_MEDIUM , // The QR Code can tolerate about 15% erroneous codewords
+ qrcodegen_Ecc_QUARTILE, // The QR Code can tolerate about 25% erroneous codewords
+ qrcodegen_Ecc_HIGH , // The QR Code can tolerate about 30% erroneous codewords
+};
+
+
+/*
+ * The mask pattern used in a QR Code symbol.
+ */
+enum qrcodegen_Mask {
+ // A special value to tell the QR Code encoder to
+ // automatically select an appropriate mask pattern
+ qrcodegen_Mask_AUTO = -1,
+ // The eight actual mask patterns
+ qrcodegen_Mask_0 = 0,
+ qrcodegen_Mask_1,
+ qrcodegen_Mask_2,
+ qrcodegen_Mask_3,
+ qrcodegen_Mask_4,
+ qrcodegen_Mask_5,
+ qrcodegen_Mask_6,
+ qrcodegen_Mask_7,
+};
+
+
+/*
+ * Describes how a segment's data bits are interpreted.
+ */
+enum qrcodegen_Mode {
+ qrcodegen_Mode_NUMERIC = 0x1,
+ qrcodegen_Mode_ALPHANUMERIC = 0x2,
+ qrcodegen_Mode_BYTE = 0x4,
+ qrcodegen_Mode_KANJI = 0x8,
+ qrcodegen_Mode_ECI = 0x7,
+};
+
+
+/*
+ * A segment of character/binary/control data in a QR Code symbol.
+ * The mid-level way to create a segment is to take the payload data
+ * and call a factory function such as qrcodegen_makeNumeric().
+ * The low-level way to create a segment is to custom-make the bit buffer
+ * and initialize a qrcodegen_Segment struct with appropriate values.
+ * Even in the most favorable conditions, a QR Code can only hold 7089 characters of data.
+ * Any segment longer than this is meaningless for the purpose of generating QR Codes.
+ * Moreover, the maximum allowed bit length is 32767 because
+ * the largest QR Code (version 40) has 31329 modules.
+ */
+struct qrcodegen_Segment {
+ // The mode indicator of this segment.
+ enum qrcodegen_Mode mode;
+
+ // The length of this segment's unencoded data. Measured in characters for
+ // numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode.
+ // Always zero or positive. Not the same as the data's bit length.
+ int numChars;
+
+ // The data bits of this segment, packed in bitwise big endian.
+ // Can be null if the bit length is zero.
+ uint8_t *data;
+
+ // The number of valid data bits used in the buffer. Requires
+ // 0 <= bitLength <= 32767, and bitLength <= (capacity of data array) * 8.
+ // The character count (numChars) must agree with the mode and the bit buffer length.
+ int bitLength;
+};
+
+
+
+/*---- Macro constants and functions ----*/
+
+#define qrcodegen_VERSION_MIN 1 // The minimum version number supported in the QR Code Model 2 standard
+#define qrcodegen_VERSION_MAX 40 // The maximum version number supported in the QR Code Model 2 standard
+
+// Calculates the number of bytes needed to store any QR Code up to and including the given version number,
+// as a compile-time constant. For example, 'uint8_t buffer[qrcodegen_BUFFER_LEN_FOR_VERSION(25)];'
+// can store any single QR Code from version 1 to 25 (inclusive). The result fits in an int (or int16).
+// Requires qrcodegen_VERSION_MIN <= n <= qrcodegen_VERSION_MAX.
+#define qrcodegen_BUFFER_LEN_FOR_VERSION(n) ((((n) * 4 + 17) * ((n) * 4 + 17) + 7) / 8 + 1)
+
+// The worst-case number of bytes needed to store one QR Code, up to and including
+// version 40. This value equals 3918, which is just under 4 kilobytes.
+// Use this more convenient value to avoid calculating tighter memory bounds for buffers.
+#define qrcodegen_BUFFER_LEN_MAX qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)
+
+
+
+/*---- Functions (high level) to generate QR Codes ----*/
+
+/*
+ * Encodes the given text string to a QR Code, returning true if successful.
+ * If the data is too long to fit in any version in the given range
+ * at the given ECC level, then false is returned.
+ *
+ * The input text must be encoded in UTF-8 and contain no NULs.
+ * Requires 1 <= minVersion <= maxVersion <= 40.
+ *
+ * The smallest possible QR Code version within the given range is automatically
+ * chosen for the output. Iff boostEcl is true, then the ECC level of the result
+ * may be higher than the ecl argument if it can be done without increasing the
+ * version. The mask is either between qrcodegen_Mask_0 to 7 to force that mask, or
+ * qrcodegen_Mask_AUTO to automatically choose an appropriate mask (which may be slow).
+ *
+ * About the arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion):
+ * - Before calling the function:
+ * - The array ranges tempBuffer[0 : len] and qrcode[0 : len] must allow
+ * reading and writing; hence each array must have a length of at least len.
+ * - The two ranges must not overlap (aliasing).
+ * - The initial state of both ranges can be uninitialized
+ * because the function always writes before reading.
+ * - After the function returns:
+ * - Both ranges have no guarantee on which elements are initialized and what values are stored.
+ * - tempBuffer contains no useful data and should be treated as entirely uninitialized.
+ * - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule().
+ *
+ * If successful, the resulting QR Code may use numeric,
+ * alphanumeric, or byte mode to encode the text.
+ *
+ * In the most optimistic case, a QR Code at version 40 with low ECC
+ * can hold any UTF-8 string up to 2953 bytes, or any alphanumeric string
+ * up to 4296 characters, or any digit string up to 7089 characters.
+ * These numbers represent the hard upper limit of the QR Code standard.
+ *
+ * Please consult the QR Code specification for information on
+ * data capacities per version, ECC level, and text encoding mode.
+ */
+bool qrcodegen_encodeText(const char *text, uint8_t tempBuffer[], uint8_t qrcode[],
+ enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl);
+
+
+/*
+ * Encodes the given binary data to a QR Code, returning true if successful.
+ * If the data is too long to fit in any version in the given range
+ * at the given ECC level, then false is returned.
+ *
+ * Requires 1 <= minVersion <= maxVersion <= 40.
+ *
+ * The smallest possible QR Code version within the given range is automatically
+ * chosen for the output. Iff boostEcl is true, then the ECC level of the result
+ * may be higher than the ecl argument if it can be done without increasing the
+ * version. The mask is either between qrcodegen_Mask_0 to 7 to force that mask, or
+ * qrcodegen_Mask_AUTO to automatically choose an appropriate mask (which may be slow).
+ *
+ * About the arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion):
+ * - Before calling the function:
+ * - The array ranges dataAndTemp[0 : len] and qrcode[0 : len] must allow
+ * reading and writing; hence each array must have a length of at least len.
+ * - The two ranges must not overlap (aliasing).
+ * - The input array range dataAndTemp[0 : dataLen] should normally be
+ * valid UTF-8 text, but is not required by the QR Code standard.
+ * - The initial state of dataAndTemp[dataLen : len] and qrcode[0 : len]
+ * can be uninitialized because the function always writes before reading.
+ * - After the function returns:
+ * - Both ranges have no guarantee on which elements are initialized and what values are stored.
+ * - dataAndTemp contains no useful data and should be treated as entirely uninitialized.
+ * - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule().
+ *
+ * If successful, the resulting QR Code will use byte mode to encode the data.
+ *
+ * In the most optimistic case, a QR Code at version 40 with low ECC can hold any byte
+ * sequence up to length 2953. This is the hard upper limit of the QR Code standard.
+ *
+ * Please consult the QR Code specification for information on
+ * data capacities per version, ECC level, and text encoding mode.
+ */
+bool qrcodegen_encodeBinary(uint8_t dataAndTemp[], size_t dataLen, uint8_t qrcode[],
+ enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl);
+
+
+/*---- Functions (low level) to generate QR Codes ----*/
+
+/*
+ * Encodes the given segments to a QR Code, returning true if successful.
+ * If the data is too long to fit in any version at the given ECC level,
+ * then false is returned.
+ *
+ * The smallest possible QR Code version is automatically chosen for
+ * the output. The ECC level of the result may be higher than the
+ * ecl argument if it can be done without increasing the version.
+ *
+ * About the byte arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX):
+ * - Before calling the function:
+ * - The array ranges tempBuffer[0 : len] and qrcode[0 : len] must allow
+ * reading and writing; hence each array must have a length of at least len.
+ * - The two ranges must not overlap (aliasing).
+ * - The initial state of both ranges can be uninitialized
+ * because the function always writes before reading.
+ * - The input array segs can contain segments whose data buffers overlap with tempBuffer.
+ * - After the function returns:
+ * - Both ranges have no guarantee on which elements are initialized and what values are stored.
+ * - tempBuffer contains no useful data and should be treated as entirely uninitialized.
+ * - Any segment whose data buffer overlaps with tempBuffer[0 : len]
+ * must be treated as having invalid values in that array.
+ * - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule().
+ *
+ * Please consult the QR Code specification for information on
+ * data capacities per version, ECC level, and text encoding mode.
+ *
+ * This function allows the user to create a custom sequence of segments that switches
+ * between modes (such as alphanumeric and byte) to encode text in less space.
+ * This is a low-level API; the high-level API is qrcodegen_encodeText() and qrcodegen_encodeBinary().
+ */
+bool qrcodegen_encodeSegments(const struct qrcodegen_Segment segs[], size_t len,
+ enum qrcodegen_Ecc ecl, uint8_t tempBuffer[], uint8_t qrcode[]);
+
+
+/*
+ * Encodes the given segments to a QR Code, returning true if successful.
+ * If the data is too long to fit in any version in the given range
+ * at the given ECC level, then false is returned.
+ *
+ * Requires 1 <= minVersion <= maxVersion <= 40.
+ *
+ * The smallest possible QR Code version within the given range is automatically
+ * chosen for the output. Iff boostEcl is true, then the ECC level of the result
+ * may be higher than the ecl argument if it can be done without increasing the
+ * version. The mask is either between qrcodegen_Mask_0 to 7 to force that mask, or
+ * qrcodegen_Mask_AUTO to automatically choose an appropriate mask (which may be slow).
+ *
+ * About the byte arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX):
+ * - Before calling the function:
+ * - The array ranges tempBuffer[0 : len] and qrcode[0 : len] must allow
+ * reading and writing; hence each array must have a length of at least len.
+ * - The two ranges must not overlap (aliasing).
+ * - The initial state of both ranges can be uninitialized
+ * because the function always writes before reading.
+ * - The input array segs can contain segments whose data buffers overlap with tempBuffer.
+ * - After the function returns:
+ * - Both ranges have no guarantee on which elements are initialized and what values are stored.
+ * - tempBuffer contains no useful data and should be treated as entirely uninitialized.
+ * - Any segment whose data buffer overlaps with tempBuffer[0 : len]
+ * must be treated as having invalid values in that array.
+ * - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule().
+ *
+ * Please consult the QR Code specification for information on
+ * data capacities per version, ECC level, and text encoding mode.
+ *
+ * This function allows the user to create a custom sequence of segments that switches
+ * between modes (such as alphanumeric and byte) to encode text in less space.
+ * This is a low-level API; the high-level API is qrcodegen_encodeText() and qrcodegen_encodeBinary().
+ */
+bool qrcodegen_encodeSegmentsAdvanced(const struct qrcodegen_Segment segs[], size_t len, enum qrcodegen_Ecc ecl,
+ int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl, uint8_t tempBuffer[], uint8_t qrcode[]);
+
+
+/*
+ * Tests whether the given string can be encoded as a segment in numeric mode.
+ * A string is encodable iff each character is in the range 0 to 9.
+ */
+bool qrcodegen_isNumeric(const char *text);
+
+
+/*
+ * Tests whether the given string can be encoded as a segment in alphanumeric mode.
+ * A string is encodable iff each character is in the following set: 0 to 9, A to Z
+ * (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon.
+ */
+bool qrcodegen_isAlphanumeric(const char *text);
+
+
+/*
+ * Returns the number of bytes (uint8_t) needed for the data buffer of a segment
+ * containing the given number of characters using the given mode. Notes:
+ * - Returns SIZE_MAX on failure, i.e. numChars > INT16_MAX or the internal
+ * calculation of the number of needed bits exceeds INT16_MAX (i.e. 32767).
+ * - Otherwise, all valid results are in the range [0, ceil(INT16_MAX / 8)], i.e. at most 4096.
+ * - It is okay for the user to allocate more bytes for the buffer than needed.
+ * - For byte mode, numChars measures the number of bytes, not Unicode code points.
+ * - For ECI mode, numChars must be 0, and the worst-case number of bytes is returned.
+ * An actual ECI segment can have shorter data. For non-ECI modes, the result is exact.
+ */
+size_t qrcodegen_calcSegmentBufferSize(enum qrcodegen_Mode mode, size_t numChars);
+
+
+/*
+ * Returns a segment representing the given binary data encoded in
+ * byte mode. All input byte arrays are acceptable. Any text string
+ * can be converted to UTF-8 bytes and encoded as a byte mode segment.
+ */
+struct qrcodegen_Segment qrcodegen_makeBytes(const uint8_t data[], size_t len, uint8_t buf[]);
+
+
+/*
+ * Returns a segment representing the given string of decimal digits encoded in numeric mode.
+ */
+struct qrcodegen_Segment qrcodegen_makeNumeric(const char *digits, uint8_t buf[]);
+
+
+/*
+ * Returns a segment representing the given text string encoded in alphanumeric mode.
+ * The characters allowed are: 0 to 9, A to Z (uppercase only), space,
+ * dollar, percent, asterisk, plus, hyphen, period, slash, colon.
+ */
+struct qrcodegen_Segment qrcodegen_makeAlphanumeric(const char *text, uint8_t buf[]);
+
+
+/*
+ * Returns a segment representing an Extended Channel Interpretation
+ * (ECI) designator with the given assignment value.
+ */
+struct qrcodegen_Segment qrcodegen_makeEci(long assignVal, uint8_t buf[]);
+
+
+/*---- Functions to extract raw data from QR Codes ----*/
+
+/*
+ * Returns the side length of the given QR Code, assuming that encoding succeeded.
+ * The result is in the range [21, 177]. Note that the length of the array buffer
+ * is related to the side length - every 'uint8_t qrcode[]' must have length at least
+ * qrcodegen_BUFFER_LEN_FOR_VERSION(version), which equals ceil(size^2 / 8 + 1).
+ */
+int qrcodegen_getSize(const uint8_t qrcode[]);
+
+
+/*
+ * Returns the color of the module (pixel) at the given coordinates, which is false
+ * for light or true for dark. The top left corner has the coordinates (x=0, y=0).
+ * If the given coordinates are out of bounds, then false (light) is returned.
+ */
+bool qrcodegen_getModule(const uint8_t qrcode[], int x, int y);
+
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/doom/r_defs.h b/src/doom/r_defs.h
index d87ca38a..c4653a04 100644
--- a/src/doom/r_defs.h
+++ b/src/doom/r_defs.h
@@ -429,16 +429,28 @@ typedef struct
int maxx;
// leave pads for [minx-1]/[maxx+1]
-
- byte pad1;
- // Here lies the rub for all
- // dynamic resize/change of resolution.
- byte top[SCREENWIDTH];
- byte pad2;
- byte pad3;
- // See above.
- byte bottom[SCREENWIDTH];
- byte pad4;
+ //
+ // Historically byte (see the "dynamic resize/change of resolution"
+ // comment id Software left here) - widened to unsigned short since a
+ // resolution taller than 255 pixels would silently truncate the y
+ // coordinates stored here (see R_RenderSegLoop, which writes into
+ // these from int locals up to viewheight-1). Sentinel value is
+ // 0xffff, not 0xff - see R_CheckPlane/R_DrawPlanes.
+ //
+ // The pads MUST be the same width as top[]/bottom[]'s elements: code
+ // deliberately reads/writes top[minx-1] and top[maxx+1] (and the
+ // equivalent for bottom[]), relying on those out-of-bounds-by-one
+ // accesses landing exactly on the adjacent pad field rather than
+ // corrupting height/picnum/lightlevel or the next array. A narrower
+ // pad type leaves the compiler free to insert its own (uninitialized,
+ // differently-sized) alignment padding instead, which is exactly what
+ // silently broke this the first time top[]/bottom[] were widened here.
+ unsigned short pad1;
+ unsigned short top[SCREENWIDTH];
+ unsigned short pad2;
+ unsigned short pad3;
+ unsigned short bottom[SCREENWIDTH];
+ unsigned short pad4;
} visplane_t;
diff --git a/src/doom/r_draw.c b/src/doom/r_draw.c
index 4fc41b5d..193ec2a8 100644
--- a/src/doom/r_draw.c
+++ b/src/doom/r_draw.c
@@ -511,27 +511,63 @@ void R_DrawTranslatedColumnLow (void)
// Assumes a given structure of the PLAYPAL.
// Could be read from a lump instead.
//
+// One translation table per non-native player color (players 2..MAXPLAYERS).
+// Player 1 uses the sprites' native green and needs no translation.
+#define NUM_TRANSLATION_TABLES (MAXPLAYERS - 1)
+
+// Base palette index (low nibble varies per source color) for each of
+// the distinguishable player color ramps DOOM1.WAD's PLAYPAL has to
+// offer: gray, brown, red (vanilla players 2-4), then blue, purple,
+// orange, silver, olive, peach. There are only this many visually
+// distinct 16-color ramps available - with MAXPLAYERS now well past
+// that, translation tables beyond NUM_BASE_COLORS cycle back through
+// the same set rather than inventing muddier, harder-to-tell-apart
+// colors. Shared with R_PlayerBorderColor below, so splitscreen tile
+// borders always match their player's sprite color.
+#define NUM_BASE_COLORS 9
+static const byte translation_bases[NUM_BASE_COLORS] =
+{
+ 0x60, 0x40, 0x20, 0xc0, 0xf0, 0xd0, 0x50, 0xe0, 0x30
+};
+
+// A representative solid palette color for playernum (0-based), for use
+// as e.g. a splitscreen tile border - a mid shade of that player's
+// translated color ramp, or native green for player 0.
+byte R_PlayerBorderColor (int playernum)
+{
+ if (playernum <= 0)
+ return 0x78;
+
+ return translation_bases[(playernum - 1) % NUM_BASE_COLORS] + 8;
+}
+
void R_InitTranslationTables (void)
{
int i;
-
- translationtables = Z_Malloc (256*3, PU_STATIC, 0);
-
+ int t;
+ byte base;
+
+ translationtables = Z_Malloc (256*NUM_TRANSLATION_TABLES, PU_STATIC, 0);
+
// translate just the 16 green colors
for (i=0 ; i<256 ; i++)
{
if (i >= 0x70 && i<= 0x7f)
{
- // map green ramp to gray, brown, red
- translationtables[i] = 0x60 + (i&0xf);
- translationtables [i+256] = 0x40 + (i&0xf);
- translationtables [i+512] = 0x20 + (i&0xf);
+ // map green ramp to each player's target color range,
+ // cycling through the base colors once MAXPLAYERS exceeds
+ // NUM_BASE_COLORS (see translation_bases above)
+ for (t=0 ; t<NUM_TRANSLATION_TABLES ; t++)
+ {
+ base = translation_bases[t % NUM_BASE_COLORS];
+ translationtables[i + 256*t] = base + (i&0xf);
+ }
}
else
{
// Keep all other colors as is.
- translationtables[i] = translationtables[i+256]
- = translationtables[i+512] = i;
+ for (t=0 ; t<NUM_TRANSLATION_TABLES ; t++)
+ translationtables[i + 256*t] = i;
}
}
}
@@ -760,30 +796,53 @@ void R_DrawSpanLow (void)
void
R_InitBuffer
( int width,
- int height )
-{
- int i;
+ int height )
+{
+ int windowx;
+ int windowy;
// Handle resize,
// e.g. smaller view windows
// with border and/or status bar.
- viewwindowx = (SCREENWIDTH-width) >> 1;
+ windowx = (SCREENWIDTH-width) >> 1;
+
+ if (width == SCREENWIDTH)
+ windowy = 0;
+ else
+ windowy = (SCREENHEIGHT-SBARHEIGHT-height) >> 1;
+
+ R_RepositionBuffer (windowx, windowy, width, height);
+}
+
+//
+// R_RepositionBuffer
+// Cheaply re-targets the column/row lookup tables built by R_InitBuffer
+// at a new top-left position, without recomputing the (much more
+// expensive) view-size-dependent tables in R_ExecuteSetViewSize. Used to
+// draw several same-sized player viewports into different regions of the
+// same screen buffer for splitscreen, one R_RenderPlayerView call at a
+// time.
+void
+R_RepositionBuffer
+( int x,
+ int y,
+ int width,
+ int height )
+{
+ int i;
+
+ viewwindowx = x;
+ viewwindowy = y;
// Column offset. For windows.
- for (i=0 ; i<width ; i++)
+ for (i=0 ; i<width ; i++)
columnofs[i] = viewwindowx + i;
- // Samw with base row offset.
- if (width == SCREENWIDTH)
- viewwindowy = 0;
- else
- viewwindowy = (SCREENHEIGHT-SBARHEIGHT-height) >> 1;
-
// Preclaculate all row offsets.
- for (i=0 ; i<height ; i++)
- ylookup[i] = I_VideoBuffer + (i+viewwindowy)*SCREENWIDTH;
-}
-
+ for (i=0 ; i<height ; i++)
+ ylookup[i] = I_VideoBuffer + (i+viewwindowy)*SCREENWIDTH;
+}
+
diff --git a/src/doom/r_draw.h b/src/doom/r_draw.h
index 5b0c818b..a8029102 100644
--- a/src/doom/r_draw.h
+++ b/src/doom/r_draw.h
@@ -86,11 +86,22 @@ R_InitBuffer
( int width,
int height );
+void
+R_RepositionBuffer
+( int x,
+ int y,
+ int width,
+ int height );
+
// Initialize color translation tables,
// for player rendering etc.
void R_InitTranslationTables (void);
+// A representative solid palette color for playernum (0-based) - see
+// r_draw.c for details. Used for splitscreen tile borders.
+byte R_PlayerBorderColor (int playernum);
+
// Rendering function.
diff --git a/src/doom/r_main.c b/src/doom/r_main.c
index 970bd19b..6662c05d 100644
--- a/src/doom/r_main.c
+++ b/src/doom/r_main.c
@@ -27,13 +27,17 @@
#include "doomdef.h"
+#include "doomstat.h"
#include "d_loop.h"
#include "m_bbox.h"
#include "m_menu.h"
+#include "v_video.h"
#include "r_local.h"
#include "r_sky.h"
+#include "i_webinput.h"
+#include "st_stuff.h"
@@ -621,7 +625,8 @@ void R_InitLightTables (void)
startmap = ((LIGHTLEVELS-1-i)*2)*NUMCOLORMAPS/LIGHTLEVELS;
for (j=0 ; j<MAXLIGHTZ ; j++)
{
- scale = FixedDiv ((SCREENWIDTH/2*FRACUNIT), (j+1)<<LIGHTZSHIFT);
+ // Relative to ORIGWIDTH, not SCREENWIDTH - see pspritescale above.
+ scale = FixedDiv ((ORIGWIDTH/2*FRACUNIT), (j+1)<<LIGHTZSHIFT);
scale >>= LIGHTSCALESHIFT;
level = startmap - scale/DISTMAP;
@@ -661,33 +666,25 @@ R_SetViewSize
//
-// R_ExecuteSetViewSize
+// R_SetupViewSizeTables
+// Shared core of R_ExecuteSetViewSize / R_SetSplitscreenViewSize: rebuilds
+// every table that depends on the current scaledviewwidth/viewheight/
+// detailshift. Callers are responsible for setting those three globals
+// first - this only differs from the historical R_ExecuteSetViewSize body
+// in being callable with view dimensions that didn't come from the
+// setblocks status-bar-size setting (i.e. splitscreen tiles).
//
-void R_ExecuteSetViewSize (void)
+static void R_SetupViewSizeTables (void)
{
fixed_t cosadj;
fixed_t dy;
int i;
int j;
int level;
- int startmap;
-
- setsizeneeded = false;
+ int startmap;
- if (setblocks == 11)
- {
- scaledviewwidth = SCREENWIDTH;
- viewheight = SCREENHEIGHT;
- }
- else
- {
- scaledviewwidth = setblocks*32;
- viewheight = (setblocks*168/10)&~7;
- }
-
- detailshift = setdetail;
viewwidth = scaledviewwidth>>detailshift;
-
+
centery = viewheight/2;
centerx = viewwidth/2;
centerxfrac = centerx<<FRACBITS;
@@ -714,8 +711,11 @@ void R_ExecuteSetViewSize (void)
R_InitTextureMapping ();
// psprite scales
- pspritescale = FRACUNIT*viewwidth/SCREENWIDTH;
- pspriteiscale = FRACUNIT*SCREENWIDTH/viewwidth;
+ // Relative to ORIGWIDTH (the resolution weapon sprites were authored
+ // for), not SCREENWIDTH (the internal framebuffer size, which can
+ // now be larger - see i_video.h). These used to be the same number.
+ pspritescale = FRACUNIT*viewwidth/ORIGWIDTH;
+ pspriteiscale = FRACUNIT*ORIGWIDTH/viewwidth;
// thing clipping
for (i=0 ; i<viewwidth ; i++)
@@ -742,7 +742,8 @@ void R_ExecuteSetViewSize (void)
startmap = ((LIGHTLEVELS-1-i)*2)*NUMCOLORMAPS/LIGHTLEVELS;
for (j=0 ; j<MAXLIGHTSCALE ; j++)
{
- level = startmap - j*SCREENWIDTH/(viewwidth<<detailshift)/DISTMAP;
+ // Relative to ORIGWIDTH, not SCREENWIDTH - see pspritescale above.
+ level = startmap - j*ORIGWIDTH/(viewwidth<<detailshift)/DISTMAP;
if (level < 0)
level = 0;
@@ -755,6 +756,47 @@ void R_ExecuteSetViewSize (void)
}
}
+//
+// R_ExecuteSetViewSize
+//
+void R_ExecuteSetViewSize (void)
+{
+ setsizeneeded = false;
+
+ if (setblocks == 11)
+ {
+ scaledviewwidth = SCREENWIDTH;
+ viewheight = SCREENHEIGHT;
+ }
+ else
+ {
+ scaledviewwidth = setblocks*32;
+ viewheight = (setblocks*168/10)&~7;
+ }
+
+ detailshift = setdetail;
+
+ R_SetupViewSizeTables ();
+}
+
+//
+// R_SetSplitscreenViewSize
+// Like R_ExecuteSetViewSize, but for a splitscreen tile of an explicit
+// pixel size instead of one derived from the setblocks status-bar-size
+// setting. Always full detail (no low-detail mode). Only needs to be
+// called when the tile size changes (i.e. the active player count
+// changes), not every frame - R_RepositionBuffer handles repositioning
+// same-sized tiles cheaply.
+//
+void R_SetSplitscreenViewSize (int width, int height)
+{
+ scaledviewwidth = width;
+ viewheight = height;
+ detailshift = 0;
+
+ R_SetupViewSizeTables ();
+}
+
//
@@ -886,5 +928,267 @@ void R_RenderPlayerView (player_t* player)
R_DrawMasked ();
// Check for new console commands.
- NetUpdate ();
+ NetUpdate ();
+}
+
+//
+// R_ComputeSplitLayout
+// Computes a near-square grid (cols x rows, cols*rows >= numplayers) used
+// to tile numplayers viewports into the screen for splitscreen.
+//
+void R_ComputeSplitLayout (int numplayers, int *cols, int *rows)
+{
+ if (numplayers < 1)
+ numplayers = 1;
+
+ *cols = (int) ceil (sqrt ((double) numplayers));
+ *rows = (numplayers + *cols - 1) / *cols;
+}
+
+// The vanilla fullscreen aspect ratio (setblocks==11 in
+// R_ExecuteSetViewSize). Several parts of the renderer that vanilla only
+// ever exercised at this ratio - most visibly R_DrawPSprite's weapon
+// sprite placement, which measures from a hardcoded SCREENWIDTH/2 center
+// and BASEYCENTER (SCREENHEIGHT/2) rather than the current tile's actual
+// width/height - assume it, rather than deriving correctly from
+// whatever the current view's width:height happens to be. A splitscreen
+// grid cell is generally NOT this ratio (e.g. a 2-player 2x1 layout
+// gives cells twice as wide as they are tall relative to a full view),
+// and stretching the rendered view to fill such a cell throws off that
+// hardcoded weapon-sprite math. So instead we fit an aspect-correct view
+// inside each cell and letterbox/pillarbox the remainder, rather than
+// widening the handful of vanilla formulas that assume this ratio.
+#define SPLITVIEW_ASPECT_NUM 320
+#define SPLITVIEW_ASPECT_DEN 200
+
+static void
+R_FitAspectRect
+( int cellx,
+ int celly,
+ int cellw,
+ int cellh,
+ int* vx,
+ int* vy,
+ int* vw,
+ int* vh )
+{
+ int fitw;
+
+ fitw = cellh * SPLITVIEW_ASPECT_NUM / SPLITVIEW_ASPECT_DEN;
+
+ if (fitw <= cellw)
+ {
+ *vh = cellh;
+ *vw = fitw;
+ }
+ else
+ {
+ *vw = cellw;
+ *vh = cellw * SPLITVIEW_ASPECT_DEN / SPLITVIEW_ASPECT_NUM;
+ }
+
+ *vx = cellx + (cellw - *vw) / 2;
+ *vy = celly + (cellh - *vh) / 2;
+}
+
+//
+// R_GetSplitscreenTile
+// If splitscreen is active (more than one player in-game) and playernum
+// is in-game, computes the screen-buffer rectangle that player's
+// aspect-correct viewport occupies (see R_FitAspectRect) and returns
+// true. Otherwise returns false, meaning the caller should fall back to
+// the normal single fullscreen view. Shared by R_RenderSplitViews and
+// the per-tile HUD in d_main.c so both agree on the same layout.
+//
+boolean
+R_GetSplitscreenTile
+( int playernum,
+ int* x,
+ int* y,
+ int* w,
+ int* h )
+{
+ int active[MAXPLAYERS];
+ int numactive;
+ int index;
+ int cols, rows;
+ int cellw, cellh;
+ int i;
+ boolean qr_slot;
+ int layout_count;
+
+ if (!playeringame[playernum])
+ return false;
+
+ numactive = 0;
+ index = -1;
+
+ for (i=0 ; i<MAXPLAYERS ; i++)
+ {
+ if (playeringame[i])
+ {
+ if (i == playernum)
+ index = numactive;
+ active[numactive++] = i;
+ }
+ }
+
+ // Keep in step with R_RenderSplitViews's own reserved-QR-cell
+ // layout (see its comment) - otherwise this would report a plain
+ // fullscreen tile for the 1-active-player-plus-QR case that
+ // function actually renders as a 2-cell grid.
+ qr_slot = I_WebInputQrEnabled () && numactive < MAXPLAYERS;
+
+ if (numactive <= 1 && !qr_slot)
+ return false;
+
+ layout_count = numactive + (qr_slot ? 1 : 0);
+ R_ComputeSplitLayout (layout_count, &cols, &rows);
+
+ cellw = SCREENWIDTH / cols;
+ cellh = SCREENHEIGHT / rows;
+
+ R_FitAspectRect ((index % cols) * cellw, (index / cols) * cellh,
+ cellw, cellh, x, y, w, h);
+
+ return true;
+}
+
+// Thickness in pixels of the per-player colored border drawn around each
+// splitscreen tile (see R_PlayerBorderColor).
+#define SPLITVIEW_BORDER_THICKNESS 3
+
+static void R_DrawSplitBorder (int x, int y, int w, int h, byte color)
+{
+ int t;
+
+ if (w <= 2*SPLITVIEW_BORDER_THICKNESS || h <= 2*SPLITVIEW_BORDER_THICKNESS)
+ return;
+
+ for (t = 0; t < SPLITVIEW_BORDER_THICKNESS; t++)
+ V_DrawBox (x+t, y+t, w-2*t, h-2*t, color);
+}
+
+//
+// R_RenderSplitViews
+// Renders one viewport per in-game player into its own tile of the
+// screen buffer. Called instead of R_RenderPlayerView when splitscreen
+// is active (see d_main.c).
+//
+// Tracks the player count R_RenderSplitViews last sized its tiles for,
+// so it only re-runs the (relatively expensive) R_SetSplitscreenViewSize
+// when that count actually changes. File-level rather than a function-
+// local static so R_InvalidateSplitViewCache (see below) can reset it
+// from outside: D_Display stops calling R_RenderSplitViews at all once
+// splitscreen goes false (it calls R_RenderPlayerView instead), so this
+// function never gets a chance to notice "we dropped to 1 player" and
+// reset the cache itself - see R_InvalidateSplitViewCache's callers.
+static int split_last_numactive = -1;
+
+void R_InvalidateSplitViewCache (void)
+{
+ split_last_numactive = -1;
+}
+
+void R_RenderSplitViews (void)
+{
+ int numactive;
+ int i;
+ int index;
+ int cols, rows;
+ int cellw, cellh;
+ int x, y, w, h;
+ boolean qr_slot;
+ int layout_count;
+
+ numactive = 0;
+ for (i=0 ; i<MAXPLAYERS ; i++)
+ if (playeringame[i])
+ numactive++;
+
+ if (numactive == 0)
+ return;
+
+ // Reserve one extra grid cell for a join QR whenever -qrstart wants
+ // one and the grid isn't already full (see I_WebInputQrEnabled) -
+ // including at exactly 1 active player, so the very first player to
+ // join still sees an invite to bring in the next one instead of a
+ // plain fullscreen view with no way to tell anyone how to join. As
+ // more players join, this shrinks by one slot each time exactly
+ // like any other cell the qr-fill check below reclaims, until the
+ // grid is completely full and there's no room left to reserve.
+ qr_slot = I_WebInputQrEnabled () && numactive < MAXPLAYERS;
+
+ if (numactive == 1 && !qr_slot)
+ {
+ // Only one active player and no QR to make room for - render
+ // fullscreen as usual.
+ R_RenderPlayerView (&players[displayplayer]);
+ return;
+ }
+
+ layout_count = numactive + (qr_slot ? 1 : 0);
+ R_ComputeSplitLayout (layout_count, &cols, &rows);
+ cellw = SCREENWIDTH / cols;
+ cellh = SCREENHEIGHT / rows;
+
+ if (numactive != split_last_numactive)
+ {
+ R_FitAspectRect (0, 0, cellw, cellh, &x, &y, &w, &h);
+ R_SetSplitscreenViewSize (w, h);
+ split_last_numactive = numactive;
+ }
+
+ // Blacken the whole screen first, not just each active player's own
+ // cell: cols*rows can exceed numactive (e.g. 5 players is a 3x2
+ // grid with one empty cell), and integer division can leave a
+ // sliver uncovered by any cell (e.g. 640/3 rounds down). Neither is
+ // ever touched by the per-player loop below, so without this they
+ // keep showing whatever an earlier, differently-shaped layout last
+ // rendered there (splitscreen skips R_FillBackScreen, which would
+ // normally paint over stale pixels between frames).
+ V_DrawFilledBox (0, 0, SCREENWIDTH, SCREENHEIGHT, 0);
+
+ index = 0;
+
+ for (i=0 ; i<MAXPLAYERS ; i++)
+ {
+ int cellx, celly;
+
+ if (!playeringame[i])
+ continue;
+
+ cellx = (index % cols) * cellw;
+ celly = (index / cols) * cellh;
+
+ R_FitAspectRect (cellx, celly, cellw, cellh, &x, &y, &w, &h);
+ R_RepositionBuffer (x, y, w, h);
+ R_RenderPlayerView (&players[i]);
+
+ // Border hugs the actual (aspect-correct) viewport, not the
+ // whole cell - letterbox/pillarbox bars stay plain black.
+ R_DrawSplitBorder (x, y, w, h, R_PlayerBorderColor (i));
+
+ // The classic status bar setting (Options > Screen Size) has
+ // nothing to drive while show_split is active - R_ExecuteSetViewSize
+ // (which reads it) is skipped in favor of R_SetSplitscreenViewSize
+ // (see d_main.c). Wire it back up as the on/off switch for this
+ // mini HUD instead, same as vanilla: max size (blocks==11) hides
+ // it for an unobstructed view, anything below shows it.
+ if (screenblocks != 11)
+ ST_DrawMiniHud (&players[i], x, y, w, h);
+
+ index++;
+ }
+
+ // qr_slot being true guarantees cols*rows > index (the grid was
+ // deliberately sized with room for it - see layout_count above),
+ // so there's always a cell here left over from the per-player loop
+ // to put it in.
+ if (qr_slot)
+ {
+ int cellx = (index % cols) * cellw;
+ int celly = (index / cols) * cellh;
+ I_WebInputDrawJoinQR (cellx, celly, cellw, cellh);
+ }
}
diff --git a/src/doom/r_main.h b/src/doom/r_main.h
index f97bd817..3bd15ff8 100644
--- a/src/doom/r_main.h
+++ b/src/doom/r_main.h
@@ -160,6 +160,22 @@ void R_Init (void);
void R_SetViewSize (int blocks, int detail);
void R_ExecuteSetViewSize(void);
+void R_SetSplitscreenViewSize(int width, int height);
+
+// Renders one viewport per in-game player into its own tile of the
+// screen buffer, for local splitscreen. Called instead of
+// R_RenderPlayerView when splitscreen is active. See d_main.c.
+void R_RenderSplitViews(void);
+
+// Forces the next R_RenderSplitViews call to recompute tile view sizing
+// regardless of player count. Call whenever splitscreen is toggled on or
+// off (see D_Display) - R_RenderSplitViews stops being called at all
+// while splitscreen is off, so it cannot notice and reset this itself.
+void R_InvalidateSplitViewCache(void);
+
+// Grid layout shared by R_RenderSplitViews and the per-tile HUD.
+void R_ComputeSplitLayout(int numplayers, int *cols, int *rows);
+boolean R_GetSplitscreenTile(int playernum, int *x, int *y, int *w, int *h);
#endif
diff --git a/src/doom/r_plane.c b/src/doom/r_plane.c
index b63b20fd..7fecb311 100644
--- a/src/doom/r_plane.c
+++ b/src/doom/r_plane.c
@@ -167,7 +167,7 @@ R_MapPlane
ds_x2 = x2;
// high or low detail
- spanfunc ();
+ spanfunc ();
}
@@ -290,7 +290,7 @@ R_CheckPlane
}
for (x=intrl ; x<= intrh ; x++)
- if (pl->top[x] != 0xff)
+ if (pl->top[x] != 0xffff)
break;
if (x > intrh)
@@ -431,9 +431,9 @@ void R_DrawPlanes (void)
planezlight = zlight[light];
- pl->top[pl->maxx+1] = 0xff;
- pl->top[pl->minx-1] = 0xff;
-
+ pl->top[pl->maxx+1] = 0xffff;
+ pl->top[pl->minx-1] = 0xffff;
+
stop = pl->maxx + 1;
for (x=pl->minx ; x<= stop ; x++)
diff --git a/src/doom/r_things.c b/src/doom/r_things.c
index 5533fd9b..38b1f27b 100644
--- a/src/doom/r_things.c
+++ b/src/doom/r_things.c
@@ -38,7 +38,10 @@
#define MINZ (FRACUNIT*4)
-#define BASEYCENTER (SCREENHEIGHT/2)
+// Relative to ORIGHEIGHT (the resolution weapon sprites were authored
+// for), not SCREENHEIGHT (the internal framebuffer size, which can now
+// be larger - see i_video.h and R_SetupViewSizeTables's pspritescale).
+#define BASEYCENTER (ORIGHEIGHT/2)
//void R_DrawColumn (void);
//void R_DrawFuzzColumn (void);
@@ -665,7 +668,8 @@ void R_DrawPSprite (pspdef_t* psp)
flip = (boolean)sprframe->flip[0];
// calculate edges of the shape
- tx = psp->sx-(SCREENWIDTH/2)*FRACUNIT;
+ // Relative to ORIGWIDTH, not SCREENWIDTH - see pspritescale.
+ tx = psp->sx-(ORIGWIDTH/2)*FRACUNIT;
tx -= spriteoffset[lump];
x1 = (centerxfrac + FixedMul (tx,pspritescale) ) >>FRACBITS;
diff --git a/src/doom/s_sound.c b/src/doom/s_sound.c
index 76748892..0659b5d3 100644
--- a/src/doom/s_sound.c
+++ b/src/doom/s_sound.c
@@ -426,11 +426,75 @@ static int Clamp(int x)
return x;
}
+// Computes the best (loudest) volume+separation for a sound at
+// `origin`, checking every active local player as a possible listener
+// instead of just one. Vanilla only ever measured this against
+// players[consoleplayer] - the only listener that could ever exist.
+// Local splitscreen shares one speaker output across every active
+// player though (see G_AddLocalPlayer), so a sound near ANY of them
+// should be heard - otherwise anything happening near a splitscreen
+// player other than consoleplayer (their own weapon fire included)
+// goes silent the moment they wander away from consoleplayer, and an
+// already-playing sound would get cut off again by S_UpdateSounds the
+// next frame even if S_StartSound let it through. Returns false (and
+// leaves *out_volume/*out_sep untouched) if origin isn't within
+// earshot of any active player, or there's no active player at all
+// (see waiting_for_players in d_main.c) to hear anything.
+static boolean S_BestListenerParams(mobj_t *origin, int base_volume,
+ int *out_volume, int *out_sep)
+{
+ int i;
+ boolean audible = false;
+ int best_volume = 0;
+ int best_sep = NORM_SEP;
+
+ for (i = 0; i < MAXPLAYERS; i++)
+ {
+ int this_volume, this_sep;
+ boolean at_this_listener;
+
+ if (!playeringame[i] || !players[i].mo)
+ continue;
+
+ at_this_listener = (origin == players[i].mo)
+ || (origin->x == players[i].mo->x
+ && origin->y == players[i].mo->y);
+
+ if (at_this_listener)
+ {
+ this_volume = base_volume;
+ this_sep = NORM_SEP;
+ }
+ else
+ {
+ this_volume = base_volume;
+ if (!S_AdjustSoundParams(players[i].mo, origin,
+ &this_volume, &this_sep))
+ {
+ continue;
+ }
+ }
+
+ if (!audible || this_volume > best_volume)
+ {
+ best_volume = this_volume;
+ best_sep = this_sep;
+ audible = true;
+ }
+ }
+
+ if (!audible)
+ return false;
+
+ *out_volume = best_volume;
+ *out_sep = best_sep;
+ return true;
+}
+
void S_StartSound(void *origin_p, int sfx_id)
{
sfxinfo_t *sfx;
mobj_t *origin;
- int rc;
int sep;
int pitch;
int cnum;
@@ -466,22 +530,12 @@ void S_StartSound(void *origin_p, int sfx_id)
}
- // Check to see if it is audible,
- // and if not, modify the params
- if (origin && origin != players[consoleplayer].mo)
+ // Check to see if it is audible, and if not, modify the params -
+ // see S_BestListenerParams for why this checks every active player
+ // instead of just consoleplayer.
+ if (origin)
{
- rc = S_AdjustSoundParams(players[consoleplayer].mo,
- origin,
- &volume,
- &sep);
-
- if (origin->x == players[consoleplayer].mo->x
- && origin->y == players[consoleplayer].mo->y)
- {
- sep = NORM_SEP;
- }
-
- if (!rc)
+ if (!S_BestListenerParams(origin, volume, &volume, &sep))
{
return;
}
@@ -554,9 +608,8 @@ void S_ResumeSound(void)
// Updates music & sounds
//
-void S_UpdateSounds(mobj_t *listener)
+void S_UpdateSounds(void)
{
- int audible;
int cnum;
int volume;
int sep;
@@ -593,15 +646,14 @@ void S_UpdateSounds(mobj_t *listener)
}
// check non-local sounds for distance clipping
- // or modify their params
- if (c->origin && listener != c->origin)
+ // or modify their params - see S_BestListenerParams
+ // for why this checks every active player instead of
+ // just one (also handles there being none at all, e.g.
+ // waiting_for_players in d_main.c, the same way).
+ if (c->origin)
{
- audible = S_AdjustSoundParams(listener,
- c->origin,
- &volume,
- &sep);
-
- if (!audible)
+ if (!S_BestListenerParams(c->origin, volume,
+ &volume, &sep))
{
S_StopChannel(cnum);
}
diff --git a/src/doom/s_sound.h b/src/doom/s_sound.h
index bbd100a0..68962123 100644
--- a/src/doom/s_sound.h
+++ b/src/doom/s_sound.h
@@ -78,7 +78,7 @@ void S_ResumeSound(void);
//
// Updates music & sounds
//
-void S_UpdateSounds(mobj_t *listener);
+void S_UpdateSounds(void);
void S_SetMusicVolume(int volume);
void S_SetSfxVolume(int volume);
diff --git a/src/doom/st_stuff.c b/src/doom/st_stuff.c
index da9b8bcf..b05256d4 100644
--- a/src/doom/st_stuff.c
+++ b/src/doom/st_stuff.c
@@ -24,6 +24,7 @@
#include <ctype.h>
#include "i_system.h"
+#include "i_swap.h"
#include "i_video.h"
#include "z_zone.h"
#include "m_misc.h"
@@ -46,6 +47,7 @@
#include "am_map.h"
#include "m_cheat.h"
+#include "hu_stuff.h"
#include "s_sound.h"
@@ -78,7 +80,14 @@
#define ST_X2 104
#define ST_FX 143
-#define ST_FY 169
+// Relative to ST_Y (which already correctly tracks SCREENHEIGHT), not a
+// hardcoded absolute row: these were all originally calibrated assuming
+// SCREENHEIGHT==200 (so ST_Y==168), silently baking that in as e.g.
+// "169" instead of "ST_Y+1". At a taller SCREENHEIGHT (see i_video.h)
+// ST_Y moves down but these didn't, eventually landing above ST_Y and
+// hitting STlib_drawNum's "n->y - ST_Y < 0" I_Error. Preserves the exact
+// original vanilla appearance at 320x200 (168+offset == the old literal).
+#define ST_FY (ST_Y + 1)
// Number of status faces.
#define ST_NUMPAINFACES 5
@@ -102,7 +111,7 @@
#define ST_DEADFACE (ST_GODFACE+1)
#define ST_FACESX 143
-#define ST_FACESY 168
+#define ST_FACESY ST_Y
#define ST_EVILGRINCOUNT (2*TICRATE)
#define ST_STRAIGHTFACECOUNT (TICRATE/2)
@@ -124,73 +133,73 @@
// AMMO number pos.
#define ST_AMMOWIDTH 3
#define ST_AMMOX 44
-#define ST_AMMOY 171
+#define ST_AMMOY (ST_Y + 3)
// HEALTH number pos.
-#define ST_HEALTHWIDTH 3
+#define ST_HEALTHWIDTH 3
#define ST_HEALTHX 90
-#define ST_HEALTHY 171
+#define ST_HEALTHY (ST_Y + 3)
// Weapon pos.
#define ST_ARMSX 111
-#define ST_ARMSY 172
+#define ST_ARMSY (ST_Y + 4)
#define ST_ARMSBGX 104
-#define ST_ARMSBGY 168
+#define ST_ARMSBGY ST_Y
#define ST_ARMSXSPACE 12
#define ST_ARMSYSPACE 10
// Frags pos.
#define ST_FRAGSX 138
-#define ST_FRAGSY 171
+#define ST_FRAGSY (ST_Y + 3)
#define ST_FRAGSWIDTH 2
// ARMOR number pos.
#define ST_ARMORWIDTH 3
#define ST_ARMORX 221
-#define ST_ARMORY 171
+#define ST_ARMORY (ST_Y + 3)
// Key icon positions.
#define ST_KEY0WIDTH 8
#define ST_KEY0HEIGHT 5
#define ST_KEY0X 239
-#define ST_KEY0Y 171
+#define ST_KEY0Y (ST_Y + 3)
#define ST_KEY1WIDTH ST_KEY0WIDTH
#define ST_KEY1X 239
-#define ST_KEY1Y 181
+#define ST_KEY1Y (ST_Y + 13)
#define ST_KEY2WIDTH ST_KEY0WIDTH
#define ST_KEY2X 239
-#define ST_KEY2Y 191
+#define ST_KEY2Y (ST_Y + 23)
// Ammunition counter.
#define ST_AMMO0WIDTH 3
#define ST_AMMO0HEIGHT 6
#define ST_AMMO0X 288
-#define ST_AMMO0Y 173
+#define ST_AMMO0Y (ST_Y + 5)
#define ST_AMMO1WIDTH ST_AMMO0WIDTH
#define ST_AMMO1X 288
-#define ST_AMMO1Y 179
+#define ST_AMMO1Y (ST_Y + 11)
#define ST_AMMO2WIDTH ST_AMMO0WIDTH
#define ST_AMMO2X 288
-#define ST_AMMO2Y 191
+#define ST_AMMO2Y (ST_Y + 23)
#define ST_AMMO3WIDTH ST_AMMO0WIDTH
#define ST_AMMO3X 288
-#define ST_AMMO3Y 185
+#define ST_AMMO3Y (ST_Y + 17)
// Indicate maximum ammunition.
// Only needed because backpack exists.
#define ST_MAXAMMO0WIDTH 3
#define ST_MAXAMMO0HEIGHT 5
#define ST_MAXAMMO0X 314
-#define ST_MAXAMMO0Y 173
+#define ST_MAXAMMO0Y (ST_Y + 5)
#define ST_MAXAMMO1WIDTH ST_MAXAMMO0WIDTH
#define ST_MAXAMMO1X 314
-#define ST_MAXAMMO1Y 179
+#define ST_MAXAMMO1Y (ST_Y + 11)
#define ST_MAXAMMO2WIDTH ST_MAXAMMO0WIDTH
#define ST_MAXAMMO2X 314
-#define ST_MAXAMMO2Y 191
+#define ST_MAXAMMO2Y (ST_Y + 23)
#define ST_MAXAMMO3WIDTH ST_MAXAMMO0WIDTH
#define ST_MAXAMMO3X 314
-#define ST_MAXAMMO3Y 185
+#define ST_MAXAMMO3Y (ST_Y + 17)
// Dimensions given in characters.
#define ST_MSGWIDTH 52
@@ -1365,3 +1374,62 @@ void ST_Init (void)
st_backing_screen = (pixel_t *) Z_Malloc(ST_WIDTH * ST_HEIGHT * sizeof(*st_backing_screen), PU_STATIC, 0);
}
+//
+// ST_DrawMiniHud
+// Compact health/ammo readout for one splitscreen tile, drawn with the
+// small message font (hu_font) instead of the classic status bar
+// graphics: the real status bar assumes a single full-width view and
+// doesn't fit in a splitscreen cell, so R_RenderSplitViews calls this
+// per player tile instead of running ST_Drawer at all (see d_main.c).
+// Skipped for tiles too small to show it legibly.
+//
+#define MINIHUD_MARGIN 2
+
+void ST_DrawMiniHud (player_t *player, int x, int y, int w, int h)
+{
+ char buf[16];
+ int fx, fy, i, len;
+ int health;
+ boolean hasammo;
+
+ if (w < 48 || h < 28 || !player->mo)
+ return;
+
+ health = player->health > 0 ? player->health : 0;
+ hasammo = weaponinfo[player->readyweapon].ammo != am_noammo;
+
+ if (hasammo)
+ {
+ M_snprintf(buf, sizeof(buf), "%d/%d", health,
+ player->ammo[weaponinfo[player->readyweapon].ammo]);
+ }
+ else
+ {
+ M_snprintf(buf, sizeof(buf), "%d", health);
+ }
+
+ len = strlen(buf);
+
+ fy = y + h - SHORT(hu_font[0]->height) - MINIHUD_MARGIN;
+ fx = x + MINIHUD_MARGIN;
+
+ for (i = 0; i < len; i++)
+ {
+ unsigned char c = toupper(buf[i]);
+ int cw;
+
+ if (c < HU_FONTSTART || c > HU_FONTEND)
+ {
+ fx += 4;
+ continue;
+ }
+
+ cw = SHORT(hu_font[c - HU_FONTSTART]->width);
+ if (fx + cw > x + w)
+ break;
+
+ V_DrawPatch(fx, fy, hu_font[c - HU_FONTSTART]);
+ fx += cw;
+ }
+}
+
diff --git a/src/doom/st_stuff.h b/src/doom/st_stuff.h
index 0cf28bb6..efcc83d3 100644
--- a/src/doom/st_stuff.h
+++ b/src/doom/st_stuff.h
@@ -23,6 +23,7 @@
#include "doomtype.h"
#include "d_event.h"
+#include "d_player.h"
#include "m_cheat.h"
// Size of statusbar.
@@ -51,6 +52,11 @@ void ST_Start (void);
// Called by startup code.
void ST_Init (void);
+// Compact per-tile health/ammo readout for a splitscreen cell - see
+// R_RenderSplitViews (r_main.c), which calls this once per player tile
+// instead of running ST_Drawer.
+void ST_DrawMiniHud (player_t *player, int x, int y, int w, int h);
+
extern pixel_t *st_backing_screen;
diff --git a/src/doom/wi_stuff.c b/src/doom/wi_stuff.c
index 965cc557..8d2ac54d 100644
--- a/src/doom/wi_stuff.c
+++ b/src/doom/wi_stuff.c
@@ -89,6 +89,15 @@
#define NG_SPACINGX 64
+// The netgame-stats and deathmatch-matrix screens below were laid out for
+// vanilla's MAXPLAYERS==4 and have no more room on the 320x200 intermission
+// screen: their fixed Y spacing (WI_SPACINGY) only fits 4 rows before
+// running off SCREENHEIGHT, which would otherwise hit V_DrawPatch's range
+// check. Cap both screens' visible rows/columns at 4 (all MAXPLAYERS
+// players are still simulated and scored correctly - only this summary
+// display is limited) until they get a real layout for higher player
+// counts.
+#define WI_MAXVISIBLEPLAYERS 4
// DEATHMATCH STUFF
#define DM_MATRIXX 42
@@ -1026,14 +1035,14 @@ void WI_drawDeathmatchStats(void)
x = DM_MATRIXX + DM_SPACINGX;
y = DM_MATRIXY;
- for (i=0 ; i<MAXPLAYERS ; i++)
+ for (i=0 ; i<MAXPLAYERS && i<WI_MAXVISIBLEPLAYERS ; i++)
{
- if (playeringame[i])
+ if (playeringame[i] && p[i] != NULL)
{
V_DrawPatch(x-SHORT(p[i]->width)/2,
DM_MATRIXY - WI_SPACINGY,
p[i]);
-
+
V_DrawPatch(DM_MATRIXX-SHORT(p[i]->width)/2,
y,
p[i]);
@@ -1064,13 +1073,13 @@ void WI_drawDeathmatchStats(void)
y = DM_MATRIXY+10;
w = SHORT(num[0]->width);
- for (i=0 ; i<MAXPLAYERS ; i++)
+ for (i=0 ; i<MAXPLAYERS && i<WI_MAXVISIBLEPLAYERS ; i++)
{
x = DM_MATRIXX + DM_SPACINGX;
if (playeringame[i])
{
- for (j=0 ; j<MAXPLAYERS ; j++)
+ for (j=0 ; j<MAXPLAYERS && j<WI_MAXVISIBLEPLAYERS ; j++)
{
if (playeringame[j])
WI_drawNum(x+w, y, dm_frags[i][j], 2);
@@ -1301,16 +1310,19 @@ void WI_drawNetgameStats(void)
// draw stats
y = NG_STATSY + SHORT(kills->height);
- for (i=0 ; i<MAXPLAYERS ; i++)
+ for (i=0 ; i<MAXPLAYERS && i<WI_MAXVISIBLEPLAYERS ; i++)
{
if (!playeringame[i])
continue;
x = NG_STATSX;
- V_DrawPatch(x-SHORT(p[i]->width), y, p[i]);
+ if (p[i] != NULL)
+ {
+ V_DrawPatch(x-SHORT(p[i]->width), y, p[i]);
- if (i == me)
- V_DrawPatch(x-SHORT(p[i]->width), y, star);
+ if (i == me)
+ V_DrawPatch(x-SHORT(p[i]->width), y, star);
+ }
x += NG_SPACINGX;
WI_drawPercent(x-pwidth, y+10, cnt_kills[i]); x += NG_SPACINGX;
@@ -1674,7 +1686,10 @@ static void WI_loadUnloadData(load_callback_t callback)
// "total"
callback(DEH_String("WIMSTT"), &total);
- for (i=0 ; i<MAXPLAYERS ; i++)
+ // Vanilla IWADs only ship "P1".."P4" face graphics (STPB0-3/WIBP1-4).
+ // Splitscreen players 5-10 have no matching lumps, so leave p[]/bp[]
+ // NULL for them - draw call sites must guard against that.
+ for (i=0 ; i<MAXPLAYERS && i<4 ; i++)
{
// "1,2,3,4"
DEH_snprintf(name, 9, "STPB%d", i);
diff --git a/src/i_main.c b/src/i_main.c
index 851f0e9d..b54286da 100644
--- a/src/i_main.c
+++ b/src/i_main.c
@@ -40,6 +40,12 @@ void D_DoomMain (void);
int main(int argc, char **argv)
{
+ // stdout is fully buffered rather than line-buffered whenever it's
+ // not a terminal (e.g. redirected to a log file), which otherwise
+ // delays console output - including crash-adjacent diagnostics -
+ // until process exit or a large-enough buffer fill.
+ setvbuf(stdout, NULL, _IOLBF, 0);
+
// save arguments
myargc = argc;
diff --git a/src/i_video.h b/src/i_video.h
index d98b8f50..5a8c4ffe 100644
--- a/src/i_video.h
+++ b/src/i_video.h
@@ -23,13 +23,34 @@
#include "doomtype.h"
// Screen width and height.
-
-#define SCREENWIDTH 320
-#define SCREENHEIGHT 200
+//
+// Raised 2x from vanilla's 320x200 so splitscreen tiles (which each get
+// only a fraction of this budget - see R_RenderSplitViews) have more
+// actual pixels before being upscaled to fill their tile. The 3D
+// renderer and i_video.c's window upscaling are already parameterized
+// on these constants and adapt automatically; various absolute-pixel UI
+// screens (menu, intermission, status bar) are NOT yet aspect/scale-
+// aware and will look small/offset until updated - see doomstat.h's
+// splitscreen for where this most matters (the classic status bar is
+// already skipped there).
+
+#define SCREENWIDTH 640
+#define SCREENHEIGHT 400
+
+// The resolution weapon-sprite (psprite) graphics were authored for, and
+// which a handful of scale/reference calculations (pspritescale,
+// BASEYCENTER, the light falloff table) still assume - independent of
+// SCREENWIDTH/SCREENHEIGHT above. These used to always be the same
+// number (320x200 was vanilla's only resolution), so nothing needed to
+// distinguish "the internal framebuffer size" from "the size those
+// formulas were calibrated for" until SCREENWIDTH/SCREENHEIGHT was
+// raised for splitscreen. See R_SetupViewSizeTables and R_DrawPSprite.
+#define ORIGWIDTH 320
+#define ORIGHEIGHT 200
// Screen height used when aspect_ratio_correct=true.
-#define SCREENHEIGHT_4_3 240
+#define SCREENHEIGHT_4_3 480
typedef boolean (*grabmouse_callback_t)(void);
diff --git a/src/m_config.c b/src/m_config.c
index e1a0260a..86f05947 100644
--- a/src/m_config.c
+++ b/src/m_config.c
@@ -512,6 +512,19 @@ static default_t doom_defaults_list[] =
CONFIG_VARIABLE_INT(screenblocks),
+ //!
+ // @game doom
+ //
+ // Number of local splitscreen players (1-MAXPLAYERS) to bring into
+ // the game automatically at startup. 1 (the default) behaves like
+ // vanilla - just the one local player, no splitscreen. See
+ // G_AddLocalPlayer/G_RemoveLocalPlayer for joining or leaving more
+ // players once the game is already running. The -splitscreen
+ // command line parameter overrides this for a single run.
+ //
+
+ CONFIG_VARIABLE_INT(splitscreen_players),
+
//!
// @game strife
//
@@ -2066,6 +2079,142 @@ static default_t extra_defaults_list[] =
//
CONFIG_VARIABLE_KEY(key_multi_msgplayer8),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 9 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer9),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 10 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer10),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 11 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer11),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 12 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer12),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 13 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer13),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 14 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer14),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 15 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer15),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 16 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer16),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 17 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer17),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 18 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer18),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 19 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer19),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 20 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer20),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 21 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer21),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 22 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer22),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 23 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer23),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 24 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer24),
+
+ //!
+ // @game doom
+ //
+ // Key to send a message to player 25 during multiplayer games.
+ //
+
+ CONFIG_VARIABLE_KEY(key_multi_msgplayer25),
};
static default_collection_t extra_defaults =
diff --git a/src/m_controls.c b/src/m_controls.c
index 1a5f520e..982ea688 100644
--- a/src/m_controls.c
+++ b/src/m_controls.c
@@ -132,7 +132,15 @@ int key_spy = KEY_F12;
// Multiplayer chat keys:
int key_multi_msg = 't';
-int key_multi_msgplayer[8];
+// Sized to Doom's MAXPLAYERS (see doom/doomdef.h), the largest caller of
+// M_BindChatControls(); other games' smaller MAXPLAYERS just leave the
+// tail unused. Only the first 10 (key_multi_msgplayer1..10) have a
+// config file entry in m_config.c - slots beyond that just keep
+// whatever M_BindChatControls sets them to in memory each run instead
+// of being user-configurable/persisted, which matters little since
+// per-player chat targeting is not really meaningful for local
+// splitscreen anyway.
+int key_multi_msgplayer[25];
// Weapon selection keys:
diff --git a/src/m_controls.h b/src/m_controls.h
index 7a8a2eaf..c462c148 100644
--- a/src/m_controls.h
+++ b/src/m_controls.h
@@ -56,7 +56,7 @@ extern int key_message_refresh;
extern int key_pause;
extern int key_multi_msg;
-extern int key_multi_msgplayer[8];
+extern int key_multi_msgplayer[25];
extern int key_weapon1;
extern int key_weapon2;
diff --git a/src/net_defs.h b/src/net_defs.h
index ab852c08..480663ea 100644
--- a/src/net_defs.h
+++ b/src/net_defs.h
@@ -28,13 +28,13 @@
// NET_MAXPLAYERS, as there may be observers that are not participating
// (eg. left/right monitors)
-#define MAXNETNODES 16
+#define MAXNETNODES 32
// The maximum number of players, multiplayer/networking.
// This is the maximum supported by the networking code; individual games
// have their own values for MAXPLAYERS that can be smaller.
-#define NET_MAXPLAYERS 8
+#define NET_MAXPLAYERS 25
// Maximum length of a player's name.