Demo playback
How a game plays itself on the title screen or replays a recorded session, by feeding stored inputs or stored world state back through its own code.
A demo is a game playing itself, on the title screen or from a file. Most of the games read here feed stored inputs back through their own input path, from Super Mario Bros.’ script of 21 button bytes1 to Doom’s 4 bytes per player per tic2. Quake records the server’s messages instead3.
- Input scriptSuper Mario Bros. (1985). The title screen's demo is a recording of button presses, played through the ordinary game.1,4,5,6,7
- Recorded inputTetris (NES) (1989). 256 (buttons, frames − 1) pairs and a fixed 256-byte piece list; a recorder for the format is left in the code, switched off.8,9,10,11Doom (1993). A 13-byte header, then 4 bytes per player per tic fed into each tic command; random indices reset at the start.2,12,13,14,15,16
- Recorded stateQuake (1996). The server's messages with the client's view angles, read in place of the network; no server runs.17,3,18
#How it works
An input demo replaces the controller and lets the game run as usual. Super Mario Bros. writes each script byte where the controller routine leaves the pad’s state, then runs the game engine4. Tetris writes held and newly pressed buttons into the same two bytes its controller routine fills9. Doom overwrites each player’s tic command before the player code acts on it12.
What else each game sets before a demo differs. Doom resets both random indices and restores the recorded skill, map, and options before a demo14. Tetris doesn’t use its randomizer for the demo’s pieces; it reads them from a fixed list10. Super Mario Bros. reseeds nothing: its generator’s state when the demo starts depends on the frames since reset7.
Quake records state instead: it shuts down any local server and reads the recorded server messages as if they arrived from the network3.
| Data | Fed to | Kept in step by | Ends when | |
|---|---|---|---|---|
| Super Mario Bros. | 21 (buttons, frames) steps, 1,119 frames (computed)1 | the controller 1 byte, then the game engine4 | nothing reseeded7 | the script ends, Mario’s lose-life routine, or Start or Select6 |
| Tetris (NES) | 256 (buttons, frames − 1) pairs9 | the held and pressed button bytes9 | a fixed piece list10 | the table runs out, or Start11 |
| Doom | 13-byte header, then 4 bytes per player per tic2 | each player’s tic command12 | random indices reset, options restored14 | the 0x80 byte15 |
| Quake | the server’s messages, each with 3 view angles17 | the client’s message parser3 | the messages’ time stamps18 | the recorded disconnect18 |
#Implementations
#Super Mario Bros.
The script pairs a button byte in DemoActionData with a frame count in DemoTimingData; the 21 counts total 1,119 frames, by the atlas’s sum1. The demo waits on the title screen for 24 ticks of a timer that ticks every 21 frames, and pressing Select puts it back5. It ends when the script’s closing 0 comes up, when the engine reaches the lose-life routine, or when a player presses Start or Select, and each of these restarts the title screen6.
#Tetris (NES)
The US build waits on the title screen for 1,025 to 1,280 frames (computed), then plays a game of type 0, A-type in the disassembly’s comment, from level 08. Its button table holds 256 pairs of a controller byte and a count, each held for its count plus one frame9. A recording branch in the same routine writes that format from the live controller, switched on by $FF in $D0, a value the code never writes there9.
The pieces come from a 256-byte list, three bits of each byte indexing the 7-entry spawn table. An index of 7 reads the byte past the table, which is the T piece’s, so a demo T comes from 2 of the 8 index values10. The demo returns to the title when the button table runs out or Start is pressed; the game-over routine has no demo case11.
#Doom
A demo lump starts with the version, skill, episode, map, options, the recording player, and which of the 4 players are in the game. Each tic then holds forward move, side move, turn, and buttons for every player in the game, until a 0x80 byte2. The turn keeps only its high byte, steps of 1/256 of a circle; while recording, the game reads each command back from the buffer, so the recording player plays with the rounded turn too13.
Playback restores the header’s settings and starts the level through G_InitNew, which resets the random indices; a demo of another version is refused14. When one ends, the attract loop moves on through the title, the credits, and demos 1 to 3, with a fourth in The Ultimate Doom; a key opens the menu over a running demo15. A recording ends on Q, a full buffer, a player leaving a net game, or an error16.
#Quake
A .dem file is a line naming a CD track, then one block for each server message other than one-byte keepalives: its length, the client’s three view angles, and the message17. Playback paces the blocks by the time stamps in the messages, and timedemo reads one a frame to measure the frame rate. When the recorded disconnect arrives, the next demo of the startdemos list plays18.
#History
Games
| Game | Year | How | Note |
|---|---|---|---|
| Yoshi's Cookie | 1992 | the attract demo plays from a fixed seed and gives the generator back afterwards | |
| Super Mario Bros. | 1985 | Input script | the title screen's demo is a recording of button presses, played through the ordinary game |
| Tetris (NES) | 1989 | Recorded input | 256 (buttons, frames − 1) pairs and a fixed 256-byte piece list; a recorder for the format is left in the code, switched off |
| Doom | 1993 | Recorded input | a 13-byte header, then 4 bytes per player per tic fed into each tic command; random indices reset at the start |
| Quake | 1996 | Recorded state | the server's messages with the client's view angles, read in place of the network; no server runs |
Every claim about this mechanic, by game (20)
References
- ^ abcdSuper Mario Bros.' demo is a script of 21 controller bytes in
DemoActionData, each held for the number of frames at the same place inDemoTimingData;DemoEnginemoves to the next byte when the count runs out. The 21 counts add up to 1,119 frames (computed), about 18.6 seconds at the NES's 60.1 frames a second (computed).CodeXkeeper0/smb1How we know
459DemoActionData: 460 .db $01, $80, $02, $81, $41, $80, $01 461 .db $42, $c2, $02, $80, $41, $c1, $41, $c1 462 .db $01, $c1, $01, $02, $80, $00 ⋮ 464DemoTimingData: 465 .db $9b, $10, $18, $05, $2c, $20, $24 466 .db $15, $5a, $10, $20, $28, $30, $20, $10 467 .db $80, $20, $30, $30, $01, $ff, $00
src/prg.asm lines 459–467 at 1cc3c92 · upstream ↗ 470 LDX DemoAction ; load current demo action 471 LDA DemoActionTimer ; load current action timer 472 BNE DoAction ; if timer still counting down, skip 473 INX 474 INC DemoAction ; if expired, increment action, X, and ⋮ 476 LDA DemoTimingData-1,x ; get next timer 477 STA DemoActionTimer ; store as current timer ⋮ 480 LDA DemoActionData-1,x ; get and perform action (current or next) 481 STA SavedJoypad1Bits 482 DEC DemoActionTimer ; decrement action timer
src/prg.asm lines 469–485 at 1cc3c92 · upstream ↗ 104A_Button = %10000000 105B_Button = %01000000 ⋮ 110Left_Dir = %00000010 111Right_Dir = %00000001
src/defs.asm lines 104–111 at 1cc3c92 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abcdIn the Linux 1.10 source, a Doom demo starts with a 13-byte header: the version (110), skill, episode, map, the deathmatch, respawn, fast, and no-monsters options, the recording player's number, and one in-game flag for each of the 4 players. Then, for every tic and every player in the game, come 4 bytes: forward move, side move, the high byte of the turn, and the buttons. A byte of
0x80ends it. A one-player demo takes 140 bytes a second at 35 tics a second (computed).Codeid-Software/DOOMHow we know
1553 demo_p = demobuffer; ⋮ 1555 *demo_p++ = VERSION; 1556 *demo_p++ = gameskill; 1557 *demo_p++ = gameepisode; 1558 *demo_p++ = gamemap; 1559 *demo_p++ = deathmatch; 1560 *demo_p++ = respawnparm; 1561 *demo_p++ = fastparm; 1562 *demo_p++ = nomonsters; 1563 *demo_p++ = consoleplayer; ⋮ 1565 for (i=0 ; i<MAXPLAYERS ; i++) 1566 *demo_p++ = playeringame[i];
linuxdoom-1.10/g_game.c lines 1549–1567 at a77dfb9 · upstream ↗ 1488#define DEMOMARKER 0x80 ⋮ 1510 *demo_p++ = cmd->forwardmove; 1511 *demo_p++ = cmd->sidemove; 1512 *demo_p++ = (cmd->angleturn+128)>>8; 1513 *demo_p++ = cmd->buttons;
linuxdoom-1.10/g_game.c lines 1488–1513 at a77dfb9 · upstream ↗ 660 if (playeringame[i]) ⋮ 666 if (demoplayback) 667 G_ReadDemoTiccmd (cmd); 668 if (demorecording) 669 G_WriteDemoTiccmd (cmd);
linuxdoom-1.10/g_game.c lines 660–669 at a77dfb9 · upstream ↗ 1677 if (demorecording) ⋮ 1679 *demo_p++ = DEMOMARKER; 1680 M_WriteFile (demoname, demobuffer, demo_p - demobuffer);
linuxdoom-1.10/g_game.c lines 1677–1680 at a77dfb9 · upstream ↗ 33enum { VERSION = 110 };linuxdoom-1.10/doomdef.h line 33 at a77dfb9 · upstream ↗ 119#define MAXPLAYERS 4 ⋮ 122#define TICRATE 35
linuxdoom-1.10/doomdef.h lines 119–122 at a77dfb9 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abcdA Quake demo records what the server told the client, not the player's inputs. Playing one first disconnects from any server, shutting down a local one, and the client then reads its server messages from the file instead of the network and parses them as live ones; the file's view angles set the view. No server runs during playback; the world arrives as the recorded messages.Codeid-Software/Quake
How we know
28DEMO CODE ⋮ 30When a demo is playing back, all NET_SendMessages are skipped, and 31NET_GetMessages are read from the demo file. ⋮ 33Whenever cl.time gets past the last received message, another message is 34read from the demo file.
WinQuake/cl_demo.c lines 28–34 at bf4ac42 · upstream ↗ 116 // get the next message 117 fread (&net_message.cursize, 4, 1, cls.demofile); 118 VectorCopy (cl.mviewangles[0], cl.mviewangles[1]); ⋮ 122 cl.mviewangles[0][i] = LittleFloat (f); ⋮ 128 r = fread (net_message.data, net_message.cursize, 1, cls.demofile);
WinQuake/cl_demo.c lines 116–128 at bf4ac42 · upstream ↗ 286 CL_Disconnect ();WinQuake/cl_demo.c line 286 at bf4ac42 · upstream ↗ 99void CL_Disconnect (void) ⋮ 107// if running a local server, shut it down 108 if (cls.demoplayback) 109 CL_StopPlayback (); 110 else if (cls.state == ca_connected) ⋮ 123 if (sv.active) 124 Host_ShutdownServer(false);
WinQuake/cl_main.c lines 99–124 at bf4ac42 · upstream ↗ 643 ret = CL_GetMessage (); ⋮ 650 CL_ParseServerMessage ();
WinQuake/cl_main.c lines 641–650 at bf4ac42 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abcThe demo writes each byte into
SavedJoypad1Bits, the byte the interrupt routine fills from controller 1 every frame, and then callsGameCoreRoutine, the same engine a game in play runs, so Mario moves by the ordinary player code.CodeXkeeper0/smb1How we know
74NonMaskableInterrupt:src/prg.asm line 74 at 1cc3c92 · upstream ↗ 116 JSR ReadJoypads ; read joypadssrc/prg.asm line 116 at 1cc3c92 · upstream ↗ 1895 STA SavedJoypadBits,x ; save controller status here alwayssrc/prg.asm line 1895 at 1cc3c92 · upstream ↗ 480 LDA DemoActionData-1,x ; get and perform action (current or next) 481 STA SavedJoypad1Bits
src/prg.asm lines 480–481 at 1cc3c92 · upstream ↗ 343 LDX DemoTimer ; otherwise check demo timer 344 BNE ChkWorldSel ; if demo timer not expired, branch to check world selection 345 STA SelectTimer ; set controller bits here if running demo 346 JSR DemoEngine ; run through the demo actions 347 BCS ResetTitle ; if carry flag set, demo over, thus branch 348 JMP RunDemo ; otherwise, run game engine for demo
src/prg.asm lines 343–348 at 1cc3c92 · upstream ↗ 390RunDemo: 391 JSR GameCoreRoutine ; run game engine
src/prg.asm lines 390–391 at 1cc3c92 · upstream ↗ 31SavedJoypadBits = $06fc 32SavedJoypad1Bits = $06fc
src/ram.asm lines 31–32 at 1cc3c92 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abEach time the title screen starts,
InitializeGamesetsDemoTimerto$18(24). It is one of the interval timers, which the interrupt routine counts down once every 21 frames, so the demo starts about 500 frames after the title appears (computed: 24 × 21 = 504, less up to 20 for the first tick). Pressing Select, or B once world select is enabled, puts the timer back to$18.CodeXkeeper0/smb1How we know
316TitleScreenMode: 317 LDA OperMode_Task 318 JSR JumpEngine ⋮ 320 .dw InitializeGame
src/prg.asm lines 316–323 at 1cc3c92 · upstream ↗ 2131 LDA #$18 ; set demo timer 2132 STA DemoTimer
src/prg.asm lines 2131–2132 at 1cc3c92 · upstream ↗ 74NonMaskableInterrupt:src/prg.asm line 74 at 1cc3c92 · upstream ↗ 127 LDX #$14 ; load end offset for end of frame timers 128 DEC IntervalTimerControl ; decrement interval timer control, 129 BPL DecTimersLoop ; if not expired, only frame timers will decrement 130 LDA #$14 131 STA IntervalTimerControl ; if control for interval timers expired, 132 LDX #$23 ; interval timers will decrement along with frame timers ⋮ 136 DEC Timers,x ; otherwise decrement the current timer
src/prg.asm lines 126–140 at 1cc3c92 · upstream ↗ 60Timers = $0780 ⋮ 82DemoTimer = $07a2
src/ram.asm lines 60–82 at 1cc3c92 · upstream ↗ 356 LDA DemoTimer ; if select or B pressed, check demo timer one last time 357 BEQ ResetTitle ; if demo timer expired, branch to reset title screen mode 358 LDA #$18 ; otherwise reset demo timer 359 STA DemoTimer
src/prg.asm lines 355–359 at 1cc3c92 · upstream ↗ 349ChkWorldSel: 350 LDX WorldSelectEnableFlag ; check to see if world selection has been enabled 351 BEQ NullJoypad 352 CMP #B_Button ; if so, check to see if the B button was pressed
src/prg.asm lines 348–354 at 1cc3c92 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abcThe demo ends, and the title screen restarts from
InitializeGame, in three ways: the script reaches its closing count of 0; the engine reaches its lose-life routine (GameEngineSubroutine6); or a player presses Start (alone or with A) or Select on either controller; B, which the title screen checks for world select, isn't tested while the demo runs.CodeXkeeper0/smb1How we know
474 INC DemoAction ; if expired, increment action, X, and 475 SEC ; set carry by default for demo over 476 LDA DemoTimingData-1,x ; get next timer 477 STA DemoActionTimer ; store as current timer 478 BEQ DemoOver ; if timer already at zero, skip
src/prg.asm lines 474–478 at 1cc3c92 · upstream ↗ 391 JSR GameCoreRoutine ; run game engine 392 LDA GameEngineSubroutine ; check to see if we're running lose life routine 393 CMP #$06 394 BNE ExitMenu ; if not, do not do all the resetting below 395ResetTitle: 396 LDA #$00 ; reset game modes, disable 397 STA OperMode ; sprite 0 check and disable 398 STA OperMode_Task ; screen output
src/prg.asm lines 390–400 at 1cc3c92 · upstream ↗ 4266GameRoutines: ⋮ 4276 .dw PlayerLoseLife
src/prg.asm lines 4266–4276 at 1cc3c92 · upstream ↗ 332 LDA SavedJoypad1Bits ; check to see if either player pressed 333 ORA SavedJoypad2Bits ; only the start button (either joypad) 334 CMP #Start_Button 335 BEQ StartGame 336 CMP #A_Button+Start_Button ; check to see if A + start was pressed ⋮ 341 CMP #Select_Button ; check to see if the select button was pressed 342 BEQ SelectBLogic ; if so, branch reset demo timer
src/prg.asm lines 330–342 at 1cc3c92 · upstream ↗ 343 LDX DemoTimer ; otherwise check demo timer 344 BNE ChkWorldSel ; if demo timer not expired, branch to check world selection
src/prg.asm lines 343–344 at 1cc3c92 · upstream ↗ 349ChkWorldSel: 350 LDX WorldSelectEnableFlag ; check to see if world selection has been enabled ⋮ 352 CMP #B_Button ; if so, check to see if the B button was pressed ⋮ 355SelectBLogic: 356 LDA DemoTimer ; if select or B pressed, check demo timer one last time 357 BEQ ResetTitle ; if demo timer expired, branch to reset title screen mode
src/prg.asm lines 348–357 at 1cc3c92 · upstream ↗ 402ChkContinue: 403 LDY DemoTimer ; if timer for demo has expired, reset modes 404 BEQ ResetTitle
src/prg.asm lines 402–404 at 1cc3c92 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abcThe demo doesn't reseed the random generator.
PseudoRandomBitRegat$07A7gets its seed$A5only at reset, and the interrupt routine shifts it every frame;InitializeGameclears memory only up to$076F. The generator's state when the demo starts therefore depends on the number of frames since reset.CodeXkeeper0/smb1How we know
29ColdBoot: 30 JSR InitializeMemory ; clear memory using pointer in Y ⋮ 33 LDA #$a5 ; set warm boot flag 34 STA WarmBootValidation 35 STA PseudoRandomBitReg ; set seed for pseudorandom register
src/prg.asm lines 29–35 at 1cc3c92 · upstream ↗ 74NonMaskableInterrupt:src/prg.asm line 74 at 1cc3c92 · upstream ↗ 142PauseSkip: ⋮ 145 LDA PseudoRandomBitReg ; get first memory location of LSFR bytes ⋮ 155 ROR PseudoRandomBitReg,x ; rotate carry into d7, and rotate last bit into carry
src/prg.asm lines 142–158 at 1cc3c92 · upstream ↗ 2124 LDY #$6f ; clear all memory as in initialization procedure, 2125 JSR InitializeMemory ; but this time, clear only as far as $076f
src/prg.asm lines 2124–2125 at 1cc3c92 · upstream ↗ 237PseudoRandomBitReg = $07a7src/ram.asm line 237 at 1cc3c92 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abTetris (NES, US build) starts its demo when the title screen has waited for Start until the frame counter's high byte, cleared as the title appears, reaches 5: between 1,025 and 1,280 frames (computed), about 17 to 21 seconds. The demo stores 0 as the game type, which the disassembly's comment names A-type, and 0 as the start level.CodeCelestialAmber/TetrisNESDisasmrevised
How we know
603 lda #$00 604 sta frameCounter+1 ⋮ 609@waitForStartButton: 610 jsr updateAudioWaitForNmiAndResetOamStaging 611 lda newlyPressedButtons_player1 612 cmp #BUTTON_START 613 beq @startButtonPressed 614 lda frameCounter+1 615 cmp #$05 616 beq @timeout ⋮ 627; Start demo 628@timeout: ⋮
main.asm lines 603–633 at 94581c0 · upstream ↗ 263 lda frameCounter 264 clc 265 adc #$01 266 sta frameCounter 267 lda #$00 268 adc frameCounter+1 269 sta frameCounter+1
main.asm lines 263–269 at 94581c0 · upstream ↗ 3735gameMode_startDemo: 3736 lda #$00 3737 sta gameType 3738 sta player1_startLevel ⋮ 3741 lda #$05 3742 sta gameMode
main.asm lines 3735–3743 at 94581c0 · upstream ↗ 132gameMode := $00C0 ; 0=legal, 1=title, 2=type menu, 3=level menu, 4=play and ending and high score, 5=demo, 6=start demo 133gameType := $00C1 ; A=0, B=1
main.asm lines 132–133 at 94581c0 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^ abcdefThe US build's demo plays a table of 256 pairs, a controller byte and a count; the demo code holds each byte for its count plus one frame, about 4,780 frames in all (computed), some 80 seconds. It writes the held buttons and the newly pressed ones into the same two bytes the controller routine fills, so the pieces move by the ordinary play code. The
@recordingbranch of the same routine writes such pairs from the live controller when$D0holds$FF, but the code's only store to$D0writes 0.CodeCelestialAmber/TetrisNESDisasmrevisedHow we know
1; $80-a $40-b $20-select $10-start $08-up $04-down $02-left $01-right $00-no input 2;2nd number is number of frames the input is held for 3demoButtonsTable:
data/demo_data.asm lines 1–3 at 94581c0 · upstream ↗ 69.else 70 .byte $00,$31,$40,$06,$00,$04,$40,$07
data/demo_data.asm lines 69–71 at 94581c0 · upstream ↗ 3637@demoGameMode: 3638 lda $D0 3639 cmp #$FF 3640 beq @recording 3641 jsr pollController ⋮ 3645 lda demo_repeats 3646 beq @finishedMove 3647 dec demo_repeats 3648 jmp @moveInProgress ⋮ 3650@finishedMove: 3651 ldx #$00 3652 lda (demoButtonsAddr,x) ⋮
main.asm lines 3637–3676 at 94581c0 · upstream ↗ 5541diffOldAndNewButtons: ⋮ 5546 eor heldButtons_player1,x 5547 and newlyPressedButtons_player1,x 5548 sta newlyPressedButtons_player1,x 5549 sty heldButtons_player1,x
main.asm lines 5541–5549 at 94581c0 · upstream ↗ 282 jsr pollControllerButtonsmain.asm line 282 at 94581c0 · upstream ↗ 3687@recording: 3688 jsr pollController ⋮ 3690 lda heldButtons_player1 ⋮ 3700 lda heldButtons_player1 3701 cmp demo_heldButtons 3702 beq @buttonsNotChanged 3703 ldx #$00 3704 lda demo_heldButtons 3705 sta (demoButtonsAddr,x) ⋮ 3707 lda demo_repeats 3708 sta (demoButtonsAddr,x) ⋮ 3713 lda heldButtons_player1 ⋮
main.asm lines 3687–3720 at 94581c0 · upstream ↗ 563 sta $D0main.asm line 563 at 94581c0 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^ abcdThe demo's pieces come from a fixed list, not the randomizer: in demo mode
chooseNextTetriminoreads the next byte of the 256-bytedemoTetriminoTypeTable, from 0 at each game start, and takes bits 4-6 as an index into the 7-entry spawn table. An index of 7 reads the byte after the table,$02, the T piece's spawn orientation, so 2 of the 8 index values give a T; 45 of the table's 256 bytes carry a 7 (computed).CodeCelestialAmber/TetrisNESDisasmHow we know
2949 lda gameMode 2950 cmp #$05 2951 bne pickRandomTetrimino 2952 ldx demoIndex 2953 inc demoIndex 2954 lda demoTetriminoTypeTable,x 2955 lsr a 2956 lsr a 2957 lsr a 2958 lsr a 2959 and #$07 2960 tax ⋮
main.asm lines 2949–2962 at 94581c0 · upstream ↗ 3013spawnTable: 3014 .byte tDown, jDown, zHoriz, oFixed, sHoriz, lDown, iHoriz 3015; unused portion of spawnTable: 3016.if NWC = 1 3017 .byte $12 3018.else 3019 .byte $02
main.asm lines 3013–3020 at 94581c0 · upstream ↗ 123tDown = $02constants.asm line 123 at 94581c0 · upstream ↗ 1229 sta allegro 1230 sta demo_heldButtons 1231 sta demo_repeats 1232 sta demoIndex
main.asm lines 1229–1236 at 94581c0 · upstream ↗ 136demoTetriminoTypeTable: 137 .byte $00,$14,$8A,$45,$22,$11,$88,$44
data/demo_data.asm lines 136–137 at 94581c0 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abcThe demo stores 1 as the game mode, which the disassembly's comment names the title screen, when the button table runs out, its read address reaching the piece table's page, or when Start is pressed. The game-over routine has no demo case: a one-player game over, in the demo as in play, stores
$03, which the comment names the level menu.CodeCelestialAmber/TetrisNESDisasmrevisedHow we know
412@checkForDemoDataExhaustion: 413 lda gameMode 414 cmp #$05 415 bne @continue 416 lda demoButtonsAddr+1 417 cmp #>demoTetriminoTypeTable 418 bne @continue ⋮ 423 lda #$01 424 sta gameMode
main.asm lines 412–424 at 94581c0 · upstream ↗ 3642 lda newlyPressedButtons_player1 3643 cmp #BUTTON_START 3644 beq @startButtonPressed
main.asm lines 3642–3644 at 94581c0 · upstream ↗ 3678@startButtonPressed: 3679 lda #>demoButtonsTable 3680 sta demoButtonsAddr+1 3681 lda #$00 3682 sta frameCounter+1 3683 lda #$01 3684 sta gameMode
main.asm lines 3678–3684 at 94581c0 · upstream ↗ 3545gameModeState_handleGameOver: ⋮ 3567@onePlayerGameOver: 3568 lda #$03 ⋮ 3573 jsr handleHighScoreIfNecessary ⋮ 3589 lda #$03 3590 sta gameMode
main.asm lines 3545–3591 at 94581c0 · upstream ↗ 132gameMode := $00C0 ; 0=legal, 1=title, 2=type menu, 3=level menu, 4=play and ending and high score, 5=demo, 6=start demomain.asm line 132 at 94581c0 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^ abcPlayback goes through the same path as play. Each tic,
G_Tickercopies every player's command (ticcmd_t) from the network buffer, then, during a demo, overwrites its move, turn, and buttons from the demo; the player code then acts on that command as on a live one.Codeid-Software/DOOMHow we know
654 // get commands, check consistancy, ⋮ 662 cmd = &players[i].cmd; ⋮ 664 memcpy (cmd, &netcmds[i][buf], sizeof(ticcmd_t)); ⋮ 666 if (demoplayback) 667 G_ReadDemoTiccmd (cmd);
linuxdoom-1.10/g_game.c lines 654–667 at a77dfb9 · upstream ↗ 1491void G_ReadDemoTiccmd (ticcmd_t* cmd) ⋮ 1499 cmd->forwardmove = ((signed char)*demo_p++); 1500 cmd->sidemove = ((signed char)*demo_p++); 1501 cmd->angleturn = ((unsigned char)*demo_p++)<<8; 1502 cmd->buttons = (unsigned char)*demo_p++;
linuxdoom-1.10/g_game.c lines 1491–1503 at a77dfb9 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abA demo keeps only the turn's high byte, rounded, so a recorded turn moves in steps of 1/256 of a full circle, about 1.4 degrees (computed). While recording, the game writes the command and reads it straight back ("make SURE it is exactly the same"), so the recording player also plays with the rounded turn and the recording matches what the player saw.Codeid-Software/DOOM
How we know
1512 *demo_p++ = (cmd->angleturn+128)>>8; 1513 *demo_p++ = cmd->buttons; 1514 demo_p -= 4; ⋮ 1522 G_ReadDemoTiccmd (cmd); // make SURE it is exactly the same
linuxdoom-1.10/g_game.c lines 1506–1522 at a77dfb9 · upstream ↗ 1501 cmd->angleturn = ((unsigned char)*demo_p++)<<8;linuxdoom-1.10/g_game.c line 1501 at a77dfb9 · upstream ↗ 40 short angleturn; // <<16 for angle deltalinuxdoom-1.10/d_ticcmd.h line 40 at a77dfb9 · upstream ↗ 69#define ANG90 0x40000000linuxdoom-1.10/tables.h line 69 at a77dfb9 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abcdPlaying a demo restores the recorded skill, episode, map, and options from its header and starts the level through
G_InitNew, which resets both random indices to 0, as a recording started from the command line also does. A demo whose version byte isn't 110 is refused with "Demo is from a different game version!".Codeid-Software/DOOMHow we know
1588 demobuffer = demo_p = W_CacheLumpName (defdemoname, PU_STATIC); 1589 if ( *demo_p++ != VERSION) ⋮ 1591 fprintf( stderr, "Demo is from a different game version!\n"); ⋮ 1596 skill = *demo_p++; 1597 episode = *demo_p++; 1598 map = *demo_p++; 1599 deathmatch = *demo_p++; ⋮ 1615 G_InitNew (skill, episode, map);
linuxdoom-1.10/g_game.c lines 1586–1615 at a77dfb9 · upstream ↗ 1414 M_ClearRandom ();linuxdoom-1.10/g_game.c line 1414 at a77dfb9 · upstream ↗ 69void M_ClearRandom (void) ⋮ 71 rndindex = prndindex = 0;
linuxdoom-1.10/m_random.c lines 69–72 at a77dfb9 · upstream ↗ 1127 p = M_CheckParm ("-record"); ⋮ 1131 G_RecordDemo (myargv[p+1]); 1132 autostart = true;
linuxdoom-1.10/d_main.c lines 1127–1132 at a77dfb9 · upstream ↗ 1161 if ( gameaction != ga_loadgame ) ⋮ 1163 if (autostart || netgame) 1164 G_InitNew (startskill, startepisode, startmap);
linuxdoom-1.10/d_main.c lines 1161–1164 at a77dfb9 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abcPlayback ends at the
0x80byte. A demo given with-playdemothen quits the game, and one given with-timedemoreports the tics and the real time taken. Otherwise the attract loop moves on: the title picture, demo1, the credits, demo2, a third page, demo3, and, in the retail Ultimate Doom only, demo4. During an attract demo, a key press that the menu doesn't take first, or a mouse or joystick button, opens the menu, and the demo keeps running behind it.Codeid-Software/DOOMHow we know
1493 if (*demo_p == DEMOMARKER) ⋮ 1495 // end of demo data stream 1496 G_CheckDemoStatus (); 1497 return;
linuxdoom-1.10/g_game.c lines 1493–1497 at a77dfb9 · upstream ↗ 1651 if (timingdemo) ⋮ 1654 I_Error ("timed %i gametics in %i realtics",gametic ⋮ 1658 if (demoplayback) ⋮ 1660 if (singledemo) 1661 I_Quit (); ⋮ 1673 D_AdvanceDemo ();
linuxdoom-1.10/g_game.c lines 1651–1674 at a77dfb9 · upstream ↗ 1135 p = M_CheckParm ("-playdemo"); ⋮ 1138 singledemo = true; // quit after one demo ⋮ 1143 p = M_CheckParm ("-timedemo"); ⋮ 1146 G_TimeDemo (myargv[p+1]);
linuxdoom-1.10/d_main.c lines 1135–1146 at a77dfb9 · upstream ↗ 462 if ( gamemode == retail ) 463 demosequence = (demosequence+1)%7; 464 else 465 demosequence = (demosequence+1)%6; ⋮ 472 else ⋮ 475 pagename = "TITLEPIC"; ⋮ 478 else ⋮ 482 G_DeferedPlayDemo ("demo1"); ⋮ 487 pagename = "CREDIT"; ⋮ 490 G_DeferedPlayDemo ("demo2"); ⋮ 497 pagename = "TITLEPIC"; ⋮ 500 else ⋮
linuxdoom-1.10/d_main.c lines 462–518 at a77dfb9 · upstream ↗ 520 // any other key pops up menu if in demos 521 if (gameaction == ga_nothing && !singledemo && 522 (demoplayback || gamestate == GS_DEMOSCREEN) ⋮ 529 M_StartControlPanel ();
linuxdoom-1.10/g_game.c lines 520–530 at a77dfb9 · upstream ↗ 173 if (M_Responder (ev)) 174 continue; // menu ate the event 175 G_Responder (ev);
linuxdoom-1.10/d_main.c lines 173–175 at a77dfb9 · upstream ↗ 138 // pause if in menu and at least one tic has been run 139 if ( !netgame 140 && menuactive 141 && !demoplayback
linuxdoom-1.10/p_tick.c lines 138–144 at a77dfb9 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abRecording (
-record) ends when the player presses Q, when the buffer, 128 KiB unless-maxdemosets another size in KiB, is within 16 bytes of full, when a player leaves a net game, or when the game stops on an error (I_Error); the game then writes the0x80byte and the file<name>.lmp, and exits with "Demo <name>.lmp recorded".Codeid-Software/DOOMHow we know
1508 if (gamekeydown['q']) // press q to end demo recording 1509 G_CheckDemoStatus (); ⋮ 1515 if (demo_p > demoend - 16) ⋮ 1517 // no more space 1518 G_CheckDemoStatus ();
linuxdoom-1.10/g_game.c lines 1508–1518 at a77dfb9 · upstream ↗ 1536 strcpy (demoname, name); 1537 strcat (demoname, ".lmp"); 1538 maxsize = 0x20000; 1539 i = M_CheckParm ("-maxdemo"); 1540 if (i && i<myargc-1) 1541 maxsize = atoi(myargv[i+1])*1024; ⋮ 1543 demoend = demobuffer + maxsize;
linuxdoom-1.10/g_game.c lines 1536–1543 at a77dfb9 · upstream ↗ 162void I_Error (char *error, ...) ⋮ 176 if (demorecording) 177 G_CheckDemoStatus();
linuxdoom-1.10/i_system.c lines 162–177 at a77dfb9 · upstream ↗ 289 strcpy (exitmsg, "Player 1 left the game"); ⋮ 292 if (demorecording) 293 G_CheckDemoStatus ();
linuxdoom-1.10/d_net.c lines 289–293 at a77dfb9 · upstream ↗ 1677 if (demorecording) ⋮ 1679 *demo_p++ = DEMOMARKER; 1680 M_WriteFile (demoname, demobuffer, demo_p - demobuffer); 1681 Z_Free (demobuffer); 1682 demorecording = false; 1683 I_Error ("Demo %s recorded",demoname);
linuxdoom-1.10/g_game.c lines 1677–1683 at a77dfb9 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^ abcA Quake demo (
.dem, WinQuake source) opens with a line of text, the CD track to force or -1, and then holds one block for each message the server sent, except one-byte keepalives (svc_nop), which are dropped: its length in 4 bytes, the client's three view angles as 4-byte floats, and the message itself.stopends the recording with one last block holding a disconnect message.Codeid-Software/QuakerevisedHow we know
244 COM_DefaultExtension (name, ".dem"); ⋮ 255 fprintf (cls.demofile, "%i\n", cls.forcetrack);
WinQuake/cl_demo.c lines 244–255 at bf4ac42 · upstream ↗ 66void CL_WriteDemoMessage (void) ⋮ 72 len = LittleLong (net_message.cursize); 73 fwrite (&len, 4, 1, cls.demofile); 74 for (i=0 ; i<3 ; i++) ⋮ 76 f = LittleFloat (cl.viewangles[i]); 77 fwrite (&f, 4, 1, cls.demofile); ⋮ 79 fwrite (net_message.data, net_message.cursize, 1, cls.demofile);
WinQuake/cl_demo.c lines 66–79 at bf4ac42 · upstream ↗ 145 // discard nop keepalive message 146 if (net_message.cursize == 1 && net_message.data[0] == svc_nop) ⋮ 152 if (cls.demorecording) 153 CL_WriteDemoMessage ();
WinQuake/cl_demo.c lines 145–153 at bf4ac42 · upstream ↗ 177// write a disconnect message to the demo file 178 SZ_Clear (&net_message); 179 MSG_WriteByte (&net_message, svc_disconnect); 180 CL_WriteDemoMessage ();
WinQuake/cl_demo.c lines 177–180 at bf4ac42 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Revised after a check by a second AI model, 2026-10-22.
- ^ abcdPlayback reads the next block only once the client's clock passes the time stamped in the last message (
svc_time), so a demo plays at its recorded speed;timedemoinstead reads one block a frame and reports frames, seconds, and frames per second. The file's closing disconnect ends the game, and while the attract loop runs the next demo of thestartdemoslist, up to 8, starts.Codeid-Software/QuakeHow we know
100 if (cls.timedemo) ⋮ 102 if (host_framecount == cls.td_lastframe) 103 return 0; // allready read this frame's message 104 cls.td_lastframe = host_framecount; ⋮ 110 else if ( /* cl.time > 0 && */ cl.time <= cl.mtime[0]) ⋮ 112 return 0; // don't need another message yet
WinQuake/cl_demo.c lines 98–113 at bf4ac42 · upstream ↗ 773 case svc_time: 774 cl.mtime[1] = cl.mtime[0]; 775 cl.mtime[0] = MSG_ReadFloat ();
WinQuake/cl_parse.c lines 773–775 at bf4ac42 · upstream ↗ 337 Con_Printf ("%i frames %5.1f seconds %5.1f fps\n", frames, time, frames/time);WinQuake/cl_demo.c line 337 at bf4ac42 · upstream ↗ 789 case svc_disconnect: 790 Host_EndGame ("Server disconnected\n");
WinQuake/cl_parse.c lines 789–790 at bf4ac42 · upstream ↗ 106 if (cls.demonum != -1) 107 CL_NextDemo ();
WinQuake/host.c lines 106–107 at bf4ac42 · upstream ↗ 228 if (!cls.demos[cls.demonum][0] || cls.demonum == MAX_DEMOS) ⋮ 230 cls.demonum = 0; ⋮ 239 sprintf (str,"playdemo %s\n", cls.demos[cls.demonum]); 240 Cbuf_InsertText (str); 241 cls.demonum++;
WinQuake/cl_main.c lines 228–241 at bf4ac42 · upstream ↗ 98#define MAX_DEMOS 8WinQuake/client.h line 98 at bf4ac42 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-22.
Checked by a second AI model, 2026-10-22.
- ^At the start of each level
generatePillsReservedeals 128 pills, shared by both players. Each pill is one of 9 color pairs, found by adding a random 0 to 15 to the previous pill's index and subtracting 9 until it fits; the game reads the reserve in a loop, so the same 128 pills come round again.CodeNostaljipi/dr-mario-disassemblyHow we know
170 lda #pillsReserveSize ;Equals to the number of pills to generate ⋮ 180 dex 181 lda rng0 ;Max the rng value at 15 182 and #$0F ⋮ 184 adc pillId ⋮ 187 cmp #colorCombination_right - colorCombination_left 188 bcc @storePill ;If within the array size (9), is a valid pill index ⋮ 191 sbc #colorCombination_right - colorCombination_left ⋮ 194 sta pillsReserve,X ;Starts from the end of the array
prg/drmario_prg_level_init.asm lines 169–194 at ecb9899 · upstream ↗ 124 jsr generatePillsReserve ;Same reserve used by both playersprg/drmario_prg_level_init.asm line 124 at ecb9899 · upstream ↗ 464 ldx currentP_pillsCounter ;If not demo, then use the player's pill reserve 465 lda pillsReserve,X ⋮ 472 inc currentP_pillsCounter ;Increase the pills counter ⋮ 474 and #pillsReserveSize-1 ;Wraparound over $7F because we have a maximum of 128 pills stored
prg/drmario_prg_game_logic.asm lines 464–475 at ecb9899 · upstream ↗ 824colorCombination_left: 825.db $00,$00,$00,$01,$01,$01,$02,$02 826.db $02 827colorCombination_right: 828.db $00,$01,$02,$00,$01,$02,$00,$01 829.db $02
data/drmario_data_game.asm lines 824–829 at ecb9899 · upstream ↗ Checked against the evidence by its author, an AI model, 2026-10-21.
Checked by a second AI model, 2026-10-21.
Sources
Code
- doppelganger and Xkeeper. 2022. “smb1: Super Mario Bros. disassembly for asm6f (Xkeeper0/smb1).” Commit 1cc3c92. Xkeeper0/smb1 @ 1cc3c92 · licence, all uses
- Software, id. 2024. “DOOM: the Linux source release (id-Software/DOOM, linuxdoom-1.10).” Commit a77dfb9. id-Software/DOOM @ a77dfb9 · licence, all uses
- Software, id. 2012. “Quake: the engine source release (id-Software/Quake: WinQuake and QuakeWorld).” Commit bf4ac42. id-Software/Quake @ bf4ac42 · licence, all uses
- (CelestialAmber), Amber Brault and contributors. 2024. “TetrisNESDisasm: a disassembly of Tetris (NES).” Commit 94581c0. CelestialAmber/TetrisNESDisasm @ 94581c0 · licence, all uses
- Nostaljipi. 2021. “Dr. Mario (NES) Disassembly.” Commit ecb9899. Nostaljipi/dr-mario-disassembly @ ecb9899 · licence, all uses