GitHub avatar

Fox's Blog

Super Mario Bros.: Level Format, Pointers, and the 256 Glitch Worlds

How 128 levels × 4 zone types fit in 40KB of ROM, why the Minus World exists, and how a Tennis NES match can load glitch worlds.

Introduction

Super Mario Bros. is 40 kilobytes of ROM. Eight worlds, 32 levels, enemies, music, power-ups, everything fits in there.

But if you open an emulator and fiddle with the right bytes, you can load level 36-1. Or 255-1. Or land in a world where everything is made of Bowser sprites and pipes leading nowhere.

Those glitch worlds exist for a simple reason: SMB1's level storage system is a marvel of 8-bit optimization, and when you force the game to read where it shouldn't, you get fascinating results.

Retro Game Mechanics Explained made a 4-video series on it -- let's compile it into a single dive into the 6502 code of the best-selling game of its era.

GLITCH OBJECTS -- the RGMechEx series title on SMB1's hidden mechanics

World 9-1 -- the first glitch world reachable via the Tennis cart swap

Warm start: why Tennis RAM survives into SMB1

Before talking level storage, you need to understand how SMB1 boots. Because the NES Tennis cart-swap glitch rests entirely on the game's warm start / cold start detection system.

The 41 preserved bytes

When SMB1 detects a cold start (first power-on or power off/on), it wipes all RAM. But when it detects a warm start (reset button, no power cut), it preserves a 41-byte memory zone:

; The 41 RAM bytes preserved on warm start
; Addresses $075F-$0787
;
; $075F : boot byte (world - 1)    [1 byte]
; $0760 : world select flag (B button) [1 byte]
; $0761-$0762 : unused                    [2 bytes]
; $0763-$0768 : timer (6 digits, 3 shown) [6 bytes]
; $0769-$076E : Luigi coins                   [6 bytes]
; $076F-$0774 : Mario coins                   [6 bytes]
; $0775-$077A : Luigi score                   [6 bytes]
; $077B-$0780 : Mario score                   [6 bytes]
; $0781-$0786 : top score (6 digits, 1 hidden) [6 bytes]
; $0787 : the magic byte $A5                 [1 byte]

Those 41 bytes serve a single feature: letting the player continue at the same world after a game over. If you die on 6-3, the game writes world 6 into the boot byte, and at the title screen, holding A + Start restarts you on 6-1.

The 41 RAM bytes preserved on warm start -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, and the magic $A5 byte

The double warm-start check

Cold start vs warm start -- the reset detection diagram

When SMB1 boots, it doesn't check a single criterion but two:

CheckWarmStart:
  ; 1. Check the magic byte $A5 at $0787
  lda $0787
  cmp #$A5
  bne ColdStart        ; not $A5 → cold start

  ; 2. Check the 6 top-score digits ($0781-$0786)
  ;    Each digit must be between 0 and 9
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; digit >= 10 → cold start
  inx
  cpx #6
  bne CheckLoop

  ; If both pass → warm start
  ; RAM is not wiped, the start world is preserved
  jmp WarmStartBoot

Checking the $A5 byte and top-score digits -- the warm start heart

Why a double check? Because the $A5 byte could be there by chance (another game leaving that value, or the RAM chip's default resting state). Checking the top-score digits are valid (0-9) ensures the data is coherent.

Why Tennis is the only game that works

When you first insert SMB1 (cold start), the game:

  1. Wipes all RAM → top score = 0, world byte = 0
  2. Writes $A5 at address $0787

Then you swap to Tennis without powering off. Tennis:

  • Doesn't clean RAM on boot (few NES games do)
  • Doesn't write over the top-score bytes → they stay at 0 (valid)
  • Doesn't touch the $A5 byte → it stays present
  • Uses address $075F for the player footstep counter
; The footstep increment in Tennis:
; On each player step on the court, Tennis increments the byte at $075F.
; That same byte is used by SMB1 as "world number - 1".
;
; 0 steps  → world 0 → SMB1 = world 1
; 1-7 steps → world 1-7 → normal worlds
; 8+ steps → world 8+ → glitch worlds!
;
; The counter only increments when music stops
; (footstep sounds don't play during music).

When you put SMB1 back in:

  1. The $A5 byte is still there (Tennis didn't touch it)
  2. The top-score digits are still 0 (valid)
  3. The world byte is now 8+ (incremented by Tennis steps)
  4. SMB1 detects a warm start → preserves the corrupted world byte
  5. Holding A + Start → world 9-1, world A-1, world 36-1, etc.

Why you must boot Mario before Tennis

A subtlety: you must first boot SMB1, then Tennis, then SMB1 again. If you started directly with Tennis, the $A5 byte would never be written (Tennis doesn't write $A5), so warm-start detection would fail and RAM would be wiped.

Tennis footstep counter: each footstep increments the world byte

Access Glitch Worlds via NES Tennis -- the cart swap explainer video

How SMB1 stores its levels in 40KB

Nintendo R&D4 had a simple-looking problem to solve: representing horizontally scrolling levels with tiles, enemies, items, all in an ultra-tight ROM budget.

The solution is a split into two completely independent data layers:

The tile layout (the level map)

Each level is defined by a pointer to a compressed ROM tile structure. The compression is crude but brilliant: a "control" byte followed by 1-3 data bytes.

The tile format uses a run system (RLE-like):

; SMB1 tile format (simplified)
; Each "command" is a control byte:
;
; $00-$7F: place one tile, advance 1 column
; $80-$BF: place a tile repeated N times (N = byte - $80 + 1)
; $C0-$FF: special command (end of line, jump, palette change)

Example: to draw 3 consecutive bricks:
  $82 $01    ; repeat tile $01 (brick) 3 times

Each level holds 13 rows of 16 tile columns (13×16 = 208 visible tiles). But the compressed format goes way lower -- e.g. sky and empty columns take almost no space.

The 6502 render loop:

; Tile decompression - main loop
; Input: tile_data pointer in $XX
; Output: level tilemap in PPU RAM

DecompressTile:
  lda (tile_ptr),y      ; read control byte
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F: single tile
  cmp #$C0
  bcc RunLength         ; $80-$BF: run-length
  jmp SpecialCommand    ; $C0-$FF: special command

SingleTile:
  sta PPU_DATA          ; write the tile directly
  jmp Next

RunLength:
  sec
  sbc #$7E              ; N = control - $7E
  tax
  lda (tile_ptr),y      ; read the tile to repeat
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

The sprite layout (enemies and objects)

In parallel, enemies and objects (blocks ?, pipes, goombas, koopas) live in a completely separate structure. Each spawn is defined by 2 bytes:

; SMB1 sprite format
; Byte 0: X position (in columns)
; Byte 1: sprite type + Y page bits
; Y is derived from the index in the sequence

A sprite sequence:
  $01 $4B    ; goomba at column 1
  $09 $4B    ; goomba at column 9
  $10 $61    ; ? block at column 16 (holds coin)
  $15 $54    ; green koopa at column 21
  $FF        ; end of sequence

Each level can reference up to 5 sprite pages (well, 5 "screens" of 16 columns), but in practice most levels use only 2-3.

The pointer table

The design's genius is the pointer table. Each level is stored as a pair of ROM addresses:

// Internal structure (simplified) of the World Map
struct LevelPointer {
    uint16_t tile_ptr;   // ROM address of tile data
    uint16_t sprite_ptr; // ROM address of sprite data
};

// 4 separate tables, one per AreaType:
// 0 = Water, 1 = Overworld, 2 = Underground, 3 = Castle
LevelPointer level_table[4][128];

128 entries per table. 4 zone types. 512 possible combos, but only a fraction is used by the official game. The rest is uninitialized RAM or data interpreted as pointers.

When the game loads a level, it does this:

; Loading a level
; A = AreaType (0-3), X = LevelID (0-127)

LoadLevel:
  sta AREA_TYPE
  asl                  ; *2 for 16-bit table offset
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; pointer to tiles
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; pointer to sprites
  jsr DecompressTiles

The game places total trust in pointers: it reads the address in the table and decompresses whatever sits there, period.

Level ID $06 (Water) -- 9-1, the underwater version of 6-2

The Level ID table: 128 possible entries, 34 assigned

The different tile and sprite pointer orders -- the cause of Frankenstein levels

The 34 unique levels and the 7-bit ID system

The NES RAM chip (MB8416A) -- it's what preserves data when swapping cartridges

SMB1 doesn't have 32 levels, but 34 unique levels. Many levels are duplicates (5-3 = 1-3 but with Bullet Bills) flagged "hard mode". The real unique levels:

  • Water (Type 0): 3 levels (2-2, 7-2, bonus zone 5-2/6-2)
  • Overworld (Type 1): 22 levels (including the 2 bonus cloud rooms)
  • Underground (Type 2): 3 levels (including the underground bonus rooms)
  • Castle (Type 3): 6 levels
  • + 1 cutscene room (before underground/water levels)
  • + 1 warp zone from 4-2

Each level has a 7-bit ID. The 5 low bits = number in the subgroup, the 2 high bits = zone type:

; 7-bit Level ID encoding
; Bits 6-5: Type (00=Water, 01=Overworld, 10=Underground, 11=Castle)
; Bits 4-0: Number in subgroup
;
; Water IDs      : $00-$02  (types 00, numbers 0-2)
; Overworld IDs  : $20-$35  (types 01, numbers 0-21)
; Underground IDs: $40-$42  (types 10, numbers 0-2)
; Castle IDs     : $60-$65  (types 11, numbers 0-5)
;
; ID $25 = %0100101 → type 01 (Overworld), number 5 → 1-1
; ID $23 = %0100011 → type 01 (Overworld), number 3 → 6-2

128 possible IDs ($00-$7F), only 34 assigned to real levels. Unused IDs point to whatever.

Pointer tables: two lists, two orders

Tile and sprite pointers aren't stored in the same order. The code uses two separate 16-bit lists (high byte / low byte in two distinct tables):

Sprite pointer order:
  Index 0-5   : Castle (6 levels)
  Index 6-27  : Overworld (22 levels)
  Index 28-30 : Underground (3 levels)
  Index 31-33 : Water (3 levels)

Tile pointer order:
  Index 0-2   : Water (3 levels)
  Index 3-24  : Overworld (22 levels)
  Index 25-27 : Underground (3 levels)
  Index 28-33 : Castle (6 levels)

Why different orders? No technical reason -- it's probably how data got organized during development. But it creates a fascinating consequence: when a level ID is invalid, tile and sprite pointers load different levels, creating Frankenstein levels.

To navigate between those two lists, the game uses small offset tables (like a table of contents):

; Offset tables by type (Water, Overworld, Underground, Castle)
; Each entry = start index in the matching list

SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; Water=31, Overworld=6, Underground=28, Castle=0

TileOffsetTable:
  .byte $00, $03, $19, $1C    ; Water=0, Overworld=3, Underground=25, Castle=28

To load level 6-2 (ID $23, Overworld number 3):

; 1. Type = 01 (Overworld) → index in offset table = 1
; 2. Sprite offset = SpriteOffsetTable[1] = 6
;    Final index = 6 + 3 (level number) = 9 → 10th sprite pointer
; 3. Tile offset = TileOffsetTable[1] = 3
;    Final index = 3 + 3 = 6 → 7th tile pointer
; 4. Result: tile pointer $A619 + sprite pointer $9ED0 = 6-2 ✓

Now, what happens with an invalid ID like $43 (Underground number 3, which doesn't exist)?

; ID $43, Type = 10 (Underground), number = 3
; Sprite offset = SpriteOffsetTable[2] = $1C = 28
;   Index = 28 + 3 = 31 → 32nd sprite pointer = 5-2 water bonus!
; Tile offset = TileOffsetTable[2] = $19 = 25
;   Index = 25 + 3 = 28 → 29th tile pointer = 1-4 (Castle)!
;
; Result: an underground level with 1-4 tiles
; and the 5-2 water zone Bloopers. A real Frankenstein.

Level ID $43 -- Frankenstein level: 1-4 tiles + 5-2 water sprites

Exploring Glitch Level Pointers -- the offset tables explained

The world index table -- when the world 9 overflow creates a glitch level

The world index table: why world 9 overflows

There's an 8-byte ROM table giving the first level's index of each world (1-8). And right after, the 36 Level IDs table of all levels in play order.

; WorldIndexTable (8 bytes)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   -> World 1 starts at level 0
;   -> World 2 starts at level 5
;   -> World 8 starts at level 33

; LevelIDTable (36 bytes)
  .byte $25, $28, $29, $26, $24, ... ; the 36 Level IDs

When trying to load world 9, the game reads the 9th byte of WorldIndexTable... which doesn't exist. It overflows 1 byte into LevelIDTable, reads value $25, then uses $25 as index into LevelIDTable (37th entry) -- which overflows again 2 bytes into SpriteOffsetTable, and reads value 6.

; World 9:
;   1. WorldIndexTable[8] (overflow) → reads $25 in LevelIDTable
;   2. LevelIDTable[37] (overflow) → reads 2nd byte of SpriteOffsetTable = 6
;   3. ID = 6 → Water level number 6 (doesn't exist)
;   4. Tile pointer = water pointer number 6 = 6-2 tiles
;   5. Sprite pointer = index 31+6 = 37 > 33 → invalid pointer
;   6. Result: 6-2 underwater with glitched sprites
;      → world 9-1!

For world G (16), the overflow goes even further and lands on Level ID $01, which is the cutscene level preceding 1-2:

; World G (16):
;   WorldIndexTable[15] → reads $01 in LevelIDTable
;   LevelIDTable[1] = $29 (1-2 cutscene)
;   → world G-1 = the 1-2 entry cutscene

Why glitch worlds exist

The game has 32 "legit" levels (8 worlds × 4 levels). But the pointer table has 128 entries per zone type. Entries beyond level 32 hold whatever sits in ROM at those addresses -- sometimes another level, sometimes sound data, sometimes RAM, sometimes whatever.

Level ID $01 Water (Minus World) -- tile pointer $AE45, sprite pointer $A171

Level ID $01 + AreaType 0 = Minus World

The most famous glitch world. Level ID $01 in AreaType 0 (water) points to:

  • Tile pointer: $AE45 → the 2-2/7-2 underwater zone
  • Sprite pointer: $A171 → the 2-2/7-2 sprites

Result: a water level looking like 2-2, but looping forever because the flagpole doesn't exist. The level loops forever.

It's level 36-1 (or 36-1 in the $-1 world).

SMB1's warm start check -- it's what lets the Minus World exist

; Why the flagpole is missing in the Minus World:
; The 2-2/7-2 sprites ($A171) have no flagpole
; in their sequence. The game looks for sprite $FD (flagpole)
; but never finds it → infinite loop
;
; The game keeps generating the level forever
; until the timer hits zero.

Pointers pointing to RAM

When the tile or sprite pointer aims at a RAM address ($00-$7F) instead of ROM, the game tries interpreting the constantly changing RAM as tiles:

; Example: Level ID $03 in Water
; Tile Pointer: $A46B (3-3 - valid)
; Sprite Pointer: $009D (points to zero-page RAM!)
;
; Zero-page RAM holds the game's registers,
; Mario's position, counter states...
; The game decompresses that as a sprite sequence,
; and the result is a level with enemies
; that are actually register values.

When zero page changes (because Mario moves, the timer runs, ...), the level's "sprites" change too. That's why some glitch worlds have enemies blinking and constantly transforming.

Level ID $03 Water -- sprite pointer $009D points to RAM, unplayable level

Level ID $36: the empty level (Overworld)

Level ID $36 in Overworld:

  • Tile pointer: $AC35 (1-2)
  • Sprite pointer: $A0D8 (1-2)

Result: nothing. The game loads the level but it's marked "no level" in RGMechEx's catalog. Tiles may be valid but sprites point somewhere producing an empty or non-functional level.

Level ID $1D (Castle): the crash champion

Level ID $1D in Castle:

  • Tile pointer: $A210 (4-4)
  • Sprite pointer: $7EA0 (RAM!)

RAM sprite pointer = undefined sprites. The game tries displaying a Spiny ball or Bullet Bill blaster in the first tile row. It crashes immediately.

; When the sprite pointer aims at RAM,
; the game decompresses ever-changing bytes
; as "spawn" instructions. The result:
; - Spawning nonexistent objects (undefined value)
; - PPU crash when the NES sprite tries displaying an invalid tile
; - Full console freeze

The 256 cataloged glitch worlds

RGMechEx wrote a script generating every level's maps, for all 4 zone types, and all 128 IDs each.

The world counter is 8-bit (0-255). Worlds 1-8 are legit. 248 glitch worlds remain potential. Each glitch world matches that world's first level, and its Level ID computes via the WorldIndexTable overflow mechanism.

Glitch worlds table -- 248 corrupted worlds, 68 reachable first levels

Of the 128 possible IDs, only 68 are a world's "first level" (reachable via the glitch world number). The other 60 are 2nd+ levels or unreachable.

Type Playable unique IDs Crashing IDs Empty IDs
Water (0) ~20 ~60 ~48
Overworld (1) ~30 ~55 ~43
Underground (2) ~15 ~65 ~48
Castle (3) ~25 ~58 ~45

Many IDs lead to the same level because pointers land on the same ROM addresses. Level ID $28 (Overworld) for example -- tile pointer $A7CD (2-1) -- appears in 38 different glitch worlds, because its $9F51 sprite pointer aims at a ROM zone reused as padding/sound data by many IDs.

Level ID $28 (Overworld) map -- 2-1 tiles with normal sprites, 38 glitch worlds

Super Mario Bros. Glitch Levels Explained -- the 3rd video

The 6 truly unique glitch levels

Of the 19 reachable glitch level IDs, only 6 don't immediately crash on load:

World Level ID Description
E-1 (224) $50 A single ? block above a pit. Mario dies instantly.
W $57 Mario spawns stuck, unable to move.
42 (133) $50 Cloud tunnel trapping Mario if he goes far enough.
62 (131, 240) $4D Frozen castle: Mario spawns on top, can't fall → stuck.
127 $4B Underground tunnel, but crashes if you go too far.
137 $4B Enables cutscene autoscrolling. Mario meets a single brick block blocking him forever.

Level ID $50 (cloud tunnel) -- glitch world 42-1 and E-1 Level ID $4D (castle) -- world 62-1, Mario stuck at spawn Level ID $4B (tunnel) -- world 127-1, crashes if you go too far

Six glitch worlds out of 248 producing something genuinely new. The rest are normal levels with the wrong zone type, or black screens.

The level format in detail

Zooming into the exact level data format, to understand why glitch levels hold together (or not).

The level header: 2 bytes, 6 properties

Each level starts with a 2-byte header controlling 6 properties:

; Byte 0: timer + Y start + modifier
;   Bits 7-6: timer (00=unchanged, 01=200, 10=300, 11=400)
;   Bits 5-3: Mario Y start (111/110 = autowalk)
;   Bits 2-0: level type modifier
;              000=default, 001=waves,
;              010=brick wall, 011=water bottom,
;              100=night, 101=snow,
;              110=snow night, 111=gray night

; Byte 1: platform + background + floor pattern
;   Bits 7-6: special platform (00=tree, 01=mushroom,
;                                 10=Bullet Bill, 11=cloud)
;   Bits 5-4: background (00=none, 01=clouds,
;                           10=mountains, 11=fences)
;   Bits 3-0: initial floor pattern (0-15)

The type modifier controls visual variations: waves atop water levels, the 8-3 brick background, the 4-3 night palette, 6-2 snow, etc.

Tile objects: 2 bytes, Next Screen Flag, 3-slot queue

After the header comes a list of tile objects, each 2 bytes. Byte $FD marks the list end.

; Tile object format (16 bits):
; Byte 0:
;   Bits 7-4: X position (column 0-15)
;   Bits 3-0: Y position
;     Y=0-11  : normal Y position
;     Y=12    : special objects (holes, bridges, rope, ? blocks)
;     Y=13    : screen skip / special objects 2
;     Y=14    : modifier/scenery/floor change
;     Y=15    : special objects 3 (castle, stairs, big pipe)

; Byte 1:
;   Bit  7   : NEXT SCREEN FLAG
;   Bits 6-4 : object type (0-7)
;   Bits 3-0 : width/height / subtype

When the "next screen" bit is set, the current working column increments by 1. That places objects beyond the first 16 columns. Objects must be listed in order (left to right) because the game loads them sequentially:

; The load routine has TWO phases per column:
; Phase 1: find new objects starting on this column
;            and add them to the queue
; Phase 2: process each object in the queue by drawing tiles,
;            and remove those ending on this column

The queue is exactly 3 slots. Direct consequence: you can't have more than 3 objects starting on the same column. If the queue is full, the 4th object is ignored and never loads.

That's why well-designed levels avoid stacking too many objects. Example in 1-2: the column with the 1up block in the ceiling + bricks next to it are split into two distinct objects to respect the 3 limit.

Special Y positions: 12, 13, 14, 15

When Y=12, the object has no Y position (it's hardcoded by type):

; Y=12: objects with no Y position
;   Type 0: hole (removes ground)
;   Type 1: moving platform rope
;   Types 2-4: bridges at fixed Y
;   Type 5: hole with water/lava
;   Types 6-7: ? block rows

When Y=13, two subgroups. If byte 1's bit 6 is 1:

; Y=13, bit6=1: special objects
;   0 = L-pipe (cutscene), 1 = flagpole, 2-3 = bridge/axe/hammer (castle end)
;   4 = stop screen, 5 = random enemies, 6 = loop level, 7+ = possible crash

If bit6=0, the 5 low bits encode a screen skip (jumping straight to screen N, without going one by one via next screen flag).

When Y=14: same principle with bit6=1 to change the type modifier, bit6=0 to change background + floor pattern.

Floor patterns: 16 ground motifs

Level ground isn't made of individual objects. SMB1 uses floor patterns, a background motif applying to all columns until the next change:

; Floor patterns (4 bits = 16 possibilities)
;   0 = total void
;   1 = 2-tile-high ground
;   2 = 1-tile-high ground
;   3 = ground + bottom
;   4 = ground + bottom 2
;   5 = 1/2 tile ground
;   6 = 3/4 ground
;   ... up to 15 = fully filled (ground + ceiling)

That's why holes are objects: they override the floor pattern on one specific column, without changing the pattern for everything else.

The 256-byte limit and repeat

All tile data of a level fits in 256 bytes max. The 6502's Y register serves as index, and it's 8-bit. If the game reaches the data end without meeting the $FD byte, it loops back to the start and repeats the 256 bytes forever:

; Y index = 8 bits → max 256 bytes of tile data
; If Y overflows (255 → 0) without meeting $FD → repeat
; Same for sprites, but pipe objects (3 bytes)
; shift index parity on each load.

Some glitch levels exploit that repeat to generate "endless" levels.

The sprite system: 2 bytes + pipe transitions

Sprites follow a similar format, but with no header and a few key differences. Byte $FF marks the list end.

; Sprite format (2 bytes):
; Byte 0: X position (column)
; Byte 1:
;   Bit 7: NEXT SCREEN FLAG
;   Bits 6-0: sprite type
;       Some types include: goomba, koopa, Blooper,
;       Bullet Bill, Lakitu, Spiny, platforms,
;       warp zone command, toad/princess,
;       enemy group spawn commands

The low bit of byte 1 is the hard level flag: if set to 1, the sprite only appears in levels ≥ 5-3. That's how "hard mode" levels are created.

Y position 15 = screen skip (identical to tiles). Y position 14 = pipe transition (3 bytes):

; Sprite Y=14: pipe/vine transition (3 bytes!)
;   Byte 0: X position
;   Byte 1: bits 6-0 = destination 7-bit Level ID
;   Byte 2: bits 4-0 = destination screen
;            bits 7-5 = world where this transition is valid
;
; Why a world? Bonus rooms are reused across worlds.
; Example: the 1-1 bonus room is also used by 2-1 and 7-1.
; That room has 3 transitions, one per world, so Mario
; reappears at the right place.

Sprites have no queue system. The only limit is no more than 4 loaded sprites at once in the spawn zone (just off-screen right). Beyond that, sprites are ignored.

How to reach glitch worlds

There are two main methods.

The classic method: wall clip

Wall clip (passing through walls) lets you leave the normal level and walk to the hidden war zone. By manipulating the world counter via RAM, you can load any Level ID.

The technique:

  1. World 1-2: go into the hidden end pipe
  2. Wall-clip through the right wall
  3. Walk through the void to the warp zone
  4. The game interprets values as worlds

But that method only reaches a small share of glitch worlds.

The extreme method: NES Tennis cart swap

See the "warm start" section above for full detail. In short: Tennis's footstep counter writes to the same RAM byte as SMB1's start world, and warm-start detection preserves that value.

The tinkers' corner: code to explore everything

If you want to explore all glitches yourself in an emulator, you can patch the Level ID directly:

; Patch for FCEUX / Mesen:
; RAM address $075F = current Level ID
; RAM address $0760 = Area Type (0=Water, 1=Overworld, 2=Underground, 3=Castle)

; Example: load Level 57 (0x39) in Overworld
; In the emulator, open the memory tracer and write:
; $075F = 0x39
; $0760 = 0x01
; Then enter a warp pipe or die and restart
; → The game loads level ID $39 in Overworld

RGMechEx published the full list of 128 levels × 4 types with auto-generated maps at rgmechex.com. Each entry shows the tile pointer, sprite pointer, and a visual map of the level.

The weirdest levels

Level ID $1F (Water): 15 glitch worlds in one

Tile pointer $A302 (3-4) combined with sprite pointer $02A0 gives 15 different glitch worlds (D-1, J-1, Y-1, Z-1, 55-1, 73-1...). Explanation: the sprite pointer aims at a ROM zone holding data close enough to valid sprites to produce playable results, but combining 3-4 castle tiles with overworld sprites creates an absurd render.

Level ID $28 (Overworld): 38 glitch worlds = record

The absolute record. 38 glitch world entries point at the same level (2-1 tiles + $9F51 sprites). Why? Because sprite pointer $9F51 lands in a ROM zone used as padding/sound data reused by many IDs.

Level ID $49 (Underground): the FDS level

Tile pointer $76AE + sprite pointer $1C9D. The tile pointer aims at the ROM zone reserved for the Famicom Disk System version. Result: a level with tiles that don't exist in the standard cartridge. It's the level making levels 52-1 and 196-1 appear.

Level IDs $00-$02: the real bonus levels

Those IDs are used by the game's legit sub-levels:

  • $00: 5-2/6-2 underwater zone (used by H-1, 39-1)
  • $01: 2-2/7-2 water (the Minus World, 36-1)
  • $02: 8-4 sub-level (136-1, 151-1, 215-1)

The difference between a "bonus" level reachable normally and a glitch world is that warp zones check the current world:

; Warp zone check (simplified)
; The game checks the target world is between 1 and 8
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; < 1 → refused
  cmp #9
  bcs InvalidWarp       ; > 8 → refused
  ; valid world only between 1 and 8
  jmp DoWarp

Glitch worlds numbered > 8 or 0 can't be reached through normal pipes. You need the wall clip or cart swap.

Why some levels crash: jump tables

When the game loads a tile object, it uses its type as an index into a jump table:

; Standard tile object jump table (types 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; type 0: ? block, hidden, flagpole...
  .word Obj_Platform      ; type 1: special platform
  .word Obj_BrickRow      ; type 2: brick row
  .word Obj_BlockRow      ; type 3: block row
  .word Obj_CoinRow       ; type 4: coin row
  .word Obj_BrickCol      ; type 5: brick column
  .word Obj_BlockCol      ; type 6: block column
  .word Obj_Pipe          ; type 7: pipe
  .word Obj_8             ; type 8
  .word Obj_9             ; type 9
  .word Obj_10            ; type 10
  .word Obj_11            ; type 11

Jump tables: why an invalid object type crashes the game

If an object has an invalid type (≥12), the game jumps to a pointer that doesn't exist in that table. 4 possible outcomes:

  1. Valid pointer → the object loads normally
  2. Pointer to another jump table (overlap) → a different object appears. Example: type 12 points to the Y=13 table, giving an L-pipe.
  3. Pointer to executable code → random code execution (likely crash)
  4. Explicit placeholder (NOP) → the object does nothing (some sprites are like that, producing enemies hovering without moving)

Glitch level ID $58: the sprite pointer aims at an invalid address, the game crashes

Glitch level ID $50: the cloud tunnel, a level generated from corrupted data

Glitch level ID $58 (the crashing tunnel): its sprite pointer aims at a memory region that doesn't exist on NES without a ROM mapper. The game tries loading the same Koopa 5 times per frame at position (0,0), saturating the PPU and freezing.

; Why ID $58 crashes:
; Sprite pointer → invalid address (outside standard NES space)
; → The game reads undetermined bytes as sprite types
; → The $00 Koopa (invalid type with no handler) loops in recursive calls
; → 6502 stack overflow → freeze

The pipe warp paradox

Remember the target_world BETWEEN 1 AND 8 check. Even if you find a pipe in a glitch world, the game checks the destination world is between 1 and 8. Glitch worlds have numbers > 8 (36-1, 255-1...), so warping fails.

That's also why the Minus World has no end: the flagpole isn't in the sprites, and pipes lead nowhere.

The 5-objects-in-a-column trick

There's an edge case bypassing the 3-objects-per-column cap. When the queue jams (slots full + next object with no next screen flag), the game "pre-processes" the current column in a loop until it finds an object with next screen flag. During each preprocessing:

; During column preprocessing:
; 1. Objects in the queue see their remaining width
;    decremented on each "fake" column advance
; 2. If an object hits width=0, it leaves the queue
; 3. A freed slot can be filled by a new object
;    added in the same column

; Result: up to 5 objects can be processed on the same column.
; Technique: place 2 objects crossing the screen boundary
; (slots 1 and 2), 1 dummy object at X < previous (jams the queue),
; then 3 objects at X=0 of the next screen (one with next screen flag).

It's called a "queue skip" and romhackers use it to build denser levels than the format normally allows.

Version differences

Famicom Disk System

The FDS version of SMB1 has a different memory map. All level pointers shift, but data is the same. What changes: glitch world indices are completely different:

FDS World 36 → Level ID $09 (water version of 5-3)
  → The flagpole IS there! You can finish the level.
  → Then: $27 (normal 7-3) → $44 (underground 4-4)
  → $44 is finishable → the axe works → end of game!

  So the FDS Minus World is a "bonus world" that can lead
  to game completion, unlike the NES version.

My favorite FDS level: ID $5F, an underground version of 3-3's second half in a low tunnel (shame it's an autoscroller).

The Lost Levels (Japanese Super Mario Bros. 2)

Lost Levels changes a lot:

  1. Identical tile/sprite order: no more Frankenstein levels (tiles and sprites load the same level even with an invalid ID)
  2. A single 16-bit pointer table instead of two separate high/low tables
  3. 4 disk files: the ROM got split for FDS:
    • File 1: worlds 1-4
    • File 2: worlds 5-8
    • File 3: world 9 + sound engine
    • File 4: worlds A-D (completely different pointer table)
  4. Same Level ID = 4 possible levels depending on the loaded file
  5. No more Tennis glitch: the continue option (continue at the same world after game over) makes warm start useless, and the game resets immediately if world > 9
  6. New objects: poison mushroom, invisible block, invisible fire flower, upside down pipes, wind -- but inserted mid-list → backward incompatibility with SMB1
  7. Red Piranha Plants always after world 4, green springboards only in worlds 2/B/3/C/7

Super Mario All-Stars (SNES)

Direct port with the same 6502 routines (the SNES runs NES code in compat mode):

  • Fixed warp zone: no more Minus World (entering the left pipe before the text leads to the right world)
  • Crashing: most glitch levels crash (except ID $6A and 9-1)
  • Added castle objects: made more unique
  • But: the 4-2 wrong warp still works (never patched!)

The 4-2 wrong warp: an object placement bug

In 4-2, there are two pipe transition objects: the vine (warp zone) and the pipe (coin cash room). The first transition object (the vine's) sits way before the vine appears on screen. The second (the pipe's) sits too late in the level.

; Transition timing in 4-2:
; Transition object 1 (vine → warp zone): placed 3 screens before the vine
; Transition object 2 (pipe → coin cash): placed 1 screen after the pipe
;
; Normally the first object deactivates before Mario
; reaches the pipe. But if Mario is fast (or uses
; the B+right block shortcut), the vine transition
; is still active when he touches the pipe!
; → The game loads the warp zone instead of the coin cash.
;
; If the object had been placed just after the vine but before
; the pipe, the bug wouldn't exist.

Loop levels

How do loop levels (8-4, 7-4) work? The level has checkpoints with screen numbers and hardcoded Y positions:

; Checkpoint: {screen_number, vertical_position}
; If Mario passes that checkpoint at the right height → level continues
; Otherwise → warp back 4 screens (64 blocks)
;
; For an infinite loop: vertical_position = $F0
; (below the screen bottom) → impossible to validate.
;
; Checkpoints are simple (one flag) except for world 7
; using triplets (3 flags, must fail at least 1)
;
; The warp back is harsh: tile data offset set to a
; hardcoded value, sprite data offset reset to 0. Present enemies
; instantly unload → firebars disappear.

Changing the format, not the code

One of the most fascinating lessons of that architecture is that SMB1's devs managed to create a very expressive level system without ever touching the 6502 render code. All variation between levels comes from data (pointers, objects, sprites, floor patterns), never code.

The 256 glitch worlds exist because the pointer tables are sized for 128 entries × 4 types, and the game never validates the values it reads. When a pointer lands in RAM, the game interprets Mario's registers as tiles. When a pointer lands in sound data, the game plays music as level design. And when jump tables overflow, the game runs whatever until it crashes.

More Super Mario Bros. Mechanics Explained -- the 4th video

What we can learn from all of that

  1. Tile/sprite separation: total independence of both layers, with different storage orders creating unique Frankenstein levels
  2. RLE compression + object system: levels aren't bitmaps but placed object lists, with floor patterns for ground
  3. 3-slot queue: strict hardware (and level design) cap
  4. Total trust in pointers: the game trusts pointers and jump tables, producing either playable glitches or crashes
  5. 256 bytes max: the 6502 Y register limit, making data repeat if you go too far
  6. Warm start / cold start: a "continue" system that opened the door to Tennis → Mario cart swap

Most beautiful of all: it's 6502 code fitting in 40KB. The code talks directly to hardware and places total trust in every memory access. If the pointer is rotten, the game crashes. And crashes, we call glitch worlds.

The 3 things to remember

  1. Glitch worlds are pointers landing wrong -- The game has 128 IDs × 4 zone types, but only 34 unique levels. When the world number is corrupted (by Tennis or wall clip), the game loads a pointer designed for another level, and the 512 possible combos produce unpredictable results.

  2. The Minus World is a warp bug combined with corruption -- The left pipe in 1-2, if activated before the text appears, loads world 36 (0x24). That world points to Level ID $01 (2-2 water), a level with no flagpole. And since there's no pipe transition for world 36, the level loops forever. The missing check creates the icon.

  3. Tennis → Mario, 15 years before OoT → Paper Mario -- The NES RAM survives a cartridge swap thanks to capacitors and SMB1's warm start / cold start system. Tennis's footstep counter (incrementing a RAM byte while playing footstep sounds) lands exactly on the world number address. The top-score digits must stay at 0, the $A5 byte intact, and the game must detect a warm start -- a perfect coincidence that only worked with Tennis.

Retro Game Mechanics Explained's original videos are a hell of a meticulous job -- the detail level on 6502 disassembly, automatic maps of every level, cart swap and warm start explanations. If you haven't seen the series, watch it, it's short and every minute is dense.

The maps' source code is at rgmechex.com, and SMB1's full disassembly is open source on many repos. 40 years ago, Japanese programmers wrote that level system in 6502 with zero unit tests and zero bug trackers, and we keep learning optimization techniques by opening their code today.

Super Mario Bros. : le format de niveau, les pointeurs et les 256 glitch worlds

Comment 128 niveaux × 4 types de zone tiennent dans 40KB de ROM, pourquoi le Minus World existe, et comment un match de Tennis NES peut charger des glitch worlds.

Introduction

Super Mario Bros., c'est 40 kilooctets de ROM. Huit mondes, 32 niveaux, des ennemis, de la musique, des power-ups, tout tient là-dedans.

Mais si tu ouvres un émulateur et que tu trifouilles les bonnes bytes, tu peux charger le niveau 36-1. Ou le 255-1. Ou atterrir dans un monde où tout est fait de sprites de Bowser et de tuyaux qui mènent nulle part.

Ces glitch worlds existent pour une raison simple : le système de stockage des niveaux de SMB1 est une merveille d'optimisation 8-bit, et quand on force le jeu à lire là où il faut pas, ça donne des résultats fascinants.

Retro Game Mechanics Explained a fait une série de 4 vidéos là-dessus -- on va les compiler en une seule plongée dans le code 6502 du jeu le plus vendu de son époque.

GLITCH OBJECTS -- le titre de la série RGMechEx sur les mécaniques cachées de SMB1

World 9-1 -- l'écran titre du premier glitch world accessible via le cart swap Tennis

Le warm start : pourquoi la RAM de Tennis survit dans SMB1

Avant de parler de stockage de niveaux, il faut comprendre comment SMB1 démarre. Parce que le glitch du cart swap NES Tennis repose entièrement sur le système de détection warm start / cold start du jeu.

Les 41 bytes préservés

Quand SMB1 détecte un cold start (première mise sous tension ou power off/on), il efface toute la RAM. Mais quand il détecte un warm start (reset bouton, pas de coupure d'alimentation), il préserve une zone mémoire de 41 bytes :

; Les 41 bytes préservés en RAM lors d'un warm start
; Adresses $075F-$0787
;
; $075F : byte de démarrage (world - 1)    [1 byte]
; $0760 : flag de sélection de monde (B button) [1 byte]
; $0761-$0762 : inutilisé                    [2 bytes]
; $0763-$0768 : timer (6 digits, 3 affichés) [6 bytes]
; $0769-$076E : coins Luigi                   [6 bytes]
; $076F-$0774 : coins Mario                   [6 bytes]
; $0775-$077A : score Luigi                   [6 bytes]
; $077B-$0780 : score Mario                   [6 bytes]
; $0781-$0786 : top score (6 digits, 1 caché) [6 bytes]
; $0787 : le byte magique $A5                 [1 byte]

Ces 41 bytes servent à une seule fonctionnalité : permettre au joueur de continuer au même monde après un game over. Si tu meurs en 6-3, le jeu écrit le monde 6 dans le byte de démarrage, et au title screen, si tu maintiens A + Start, tu recommences en 6-1.

Les 41 bytes préservés en RAM lors d'un warm start -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, et le byte magique $A5

La double vérification du warm start

Cold start vs warm start -- le diagramme de détection du reset

Quand SMB1 boote, il ne vérifie pas un seul critère mais deux :

CheckWarmStart:
  ; 1. Vérifier le byte magique $A5 à $0787
  lda $0787
  cmp #$A5
  bne ColdStart        ; pas $A5 → cold start

  ; 2. Vérifier les 6 digits du top score ($0781-$0786)
  ;    Chaque digit doit être entre 0 et 9
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; digit >= 10 → cold start
  inx
  cpx #6
  bne CheckLoop

  ; Si les deux conditions passent → warm start
  ; La RAM n'est pas effacée, le monde de départ est préservé
  jmp WarmStartBoot

La vérification du byte $A5 et des digits du top score -- le coeur du warm start

Pourquoi une double vérification ? Parce que le byte $A5 pourrait être présent par hasard (un autre jeu qui laisse cette valeur, ou l'état de repos par défaut du chip RAM). En vérifiant que les digits du top score sont valides (0-9), on s'assure que les données sont cohérentes.

Pourquoi Tennis est le seul jeu qui marche

Quand on insère SMB1 pour la première fois (cold start), le jeu :

  1. Efface toute la RAM → top score = 0, world byte = 0
  2. Écrit $A5 à l'adresse $0787

Ensuite, on swap sur Tennis sans éteindre la console. Tennis :

  • Ne nettoie pas la RAM au démarrage (peu de jeux NES le font)
  • N'écrit pas sur les bytes du top score → ils restent à 0 (valides)
  • Ne touche pas au byte $A5 → il reste présent
  • Utilise l'adresse $075F pour le compteur de pas du joueur
; Le footstep increment dans Tennis :
; À chaque pas du joueur sur le court, Tennis incrémente le byte à $075F.
; Ce même byte est utilisé par SMB1 comme "world number - 1".
;
; 0 pas  → world 0 → SMB1 = world 1
; 1-7 pas → world 1-7 → worlds normaux
; 8+ pas → world 8+ → glitch worlds !
;
; Le compteur ne s'incrémente que quand la musique s'arrête
; (les footstep sounds ne jouent pas pendant la musique).

Quand on remet SMB1 :

  1. Le byte $A5 est toujours là (Tennis ne l'a pas touché)
  2. Les digits du top score sont toujours 0 (valides)
  3. Le world byte vaut maintenant 8+ (incrémenté par les pas de Tennis)
  4. SMB1 détecte un warm start → préserve le world byte corrompu
  5. Maintenir A + Start → world 9-1, world A-1, world 36-1, etc.

Pourquoi il faut booter Mario avant Tennis

Une subtilité : il faut d'abord booter SMB1, puis Tennis, puis SMB1 à nouveau. Si tu commençais directement par Tennis, le byte $A5 ne serait jamais écrit (Tennis n'écrit pas $A5), donc la détection warm start échouerait et la RAM serait effacée.

Le compteur de pas de Tennis : chaque footstep incrémente le world byte

Access Glitch Worlds via NES Tennis -- la vidéo qui explique le cart swap

Comment SMB1 stocke ses niveaux dans 40KB

Nintendo R&D4 a dû résoudre un problème simple en apparence : représenter des niveaux qui scrollent horizontalement avec des tiles, des ennemis, des items, le tout dans un budget ROM ultra-serré.

La solution, c'est une séparation en deux couches de données complètement indépendantes :

Le tile layout (la carte du niveau)

Chaque niveau est défini par un pointeur vers une structure de tiles compressée en ROM. La compression est rudimentaire mais géniale : un byte "contrôle" suivi de 1-3 bytes de données.

Le format tile utilise un système de runs (RLE-like) :

; Format tile SMB1 (simplifié)
; Chaque "commande" est un byte contrôle :
;
; $00-$7F : pose une tile, avance d'1 colonne
; $80-$BF : pose une tile répétée N fois (N = byte - $80 + 1)
; $C0-$FF : commande spéciale (fin de ligne, saut, changement de palette)

Exemple : pour dessiner 3 briques consécutives :
  $82 $01    ; répète la tile $01 (brick) 3 fois

Chaque niveau contient 13 lignes de 16 colonnes de tiles (13×16 = 208 tiles visibles). Mais le format compressé permet de descendre bien plus bas -- par exemple, le ciel et les colonnes vides ne prennent presque pas de place.

La boucle de rendu en 6502 :

; Décompression tile - loop principale
; Entrée : pointeur tile_data en $XX
; Sortie : tilemap niveau dans la RAM PPU

DecompressTile:
  lda (tile_ptr),y      ; lire byte contrôle
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F : tile unique
  cmp #$C0
  bcc RunLength         ; $80-$BF : run-length
  jmp SpecialCommand    ; $C0-$FF : commande spéciale

SingleTile:
  sta PPU_DATA          ; écrire la tile directement
  jmp Next

RunLength:
  sec
  sbc #$7E              ; N = control - $7E
  tax
  lda (tile_ptr),y      ; lire la tile à répéter
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

Le sprite layout (les ennemis et objets)

Parallèlement, les ennemis et objets (blocs ?, tuyaux, goombas, koopas) sont stockés dans une structure complètement séparée. Chaque spawn est défini par 2 bytes :

; Format sprite SMB1
; Byte 0 : position X (en colonnes)
; Byte 1 : type de sprite + bits de page Y
; Y est dérivé de l'index dans la séquence

Une séquence de sprites :
  $01 $4B    ; goomba à la colonne 1
  $09 $4B    ; goomba à la colonne 9
  $10 $61    ; bloc ? à la colonne 16 (contient pièce)
  $15 $54    ; koopa verte à la colonne 21
  $FF        ; fin de séquence

Chaque niveau peut référencer jusqu'à 5 pages de sprites différentes (enfin, 5 "screens" de 16 colonnes), mais en pratique la plupart des niveaux n'en utilisent que 2-3.

La table des pointeurs

Le génie du design, c'est la table de pointeurs. Chaque niveau est stocké comme une paire d'adresses ROM :

// Structure interne (simplifiée) du World Map
struct LevelPointer {
    uint16_t tile_ptr;   // Adresse ROM des données tiles
    uint16_t sprite_ptr; // Adresse ROM des données sprites
};

// 4 tables séparées, une par AreaType :
// 0 = Water, 1 = Overworld, 2 = Underground, 3 = Castle
LevelPointer level_table[4][128];

128 entrées par table. 4 types de zone. 512 combinaisons possibles, mais seulement une fraction est utilisée par le jeu officiel. Le reste, c'est de la RAM non initialisée ou des données qui sont interprétées comme des pointeurs.

Quand le jeu charge un niveau, il fait ça :

; Chargement d'un niveau
; A = AreaType (0-3), X = LevelID (0-127)

LoadLevel:
  sta AREA_TYPE
  asl                  ; *2 pour offset dans table 16-bit
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; pointeur vers les tiles
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; pointeur vers les sprites
  jsr DecompressTiles

Le jeu accorde une confiance totale aux pointeurs : il lit l'adresse dans la table et décompresse ce qui se trouve à cette adresse, point final.

Level ID $06 (Water) -- 9-1, la version sous-marine de 6-2

La table des Level IDs : 128 entrées possibles, 34 assignées

L'ordre différent des pointeurs tiles et sprites -- la cause des Frankenstein levels

Les 34 niveaux uniques et le système d'ID 7-bit

Le chip RAM de la NES (MB8416A) -- c'est lui qui conserve les données quand on swap les cartouches

SMB1 n'a pas 32 niveaux, mais 34 niveaux uniques. Beaucoup de niveaux sont des doublons (5-3 = 1-3 mais avec des Bullet Bills) marqués par un drapeau "hard mode". Les vrais niveaux uniques :

  • Eau (Type 0) : 3 niveaux (2-2, 7-2, zone bonus 5-2/6-2)
  • Overworld (Type 1) : 22 niveaux (dont les 2 salles nuages bonus)
  • Underground (Type 2) : 3 niveaux (dont les salles bonus souterraines)
  • Castle (Type 3) : 6 niveaux
  • + 1 cutscene room (avant les niveaux sous-terrains/eau)
  • + 1 warp zone de 4-2

Chaque niveau a un ID sur 7 bits. Les 5 bits de poids faible = numéro dans le sous-groupe, les 2 bits de poids fort = type de zone :

; Encodage 7-bit du Level ID
; Bits 6-5 : Type (00=Water, 01=Overworld, 10=Underground, 11=Castle)
; Bits 4-0 : Numéro dans le sous-groupe
;
; Water IDs      : $00-$02  (types 00, numéros 0-2)
; Overworld IDs  : $20-$35  (types 01, numéros 0-21)
; Underground IDs: $40-$42  (types 10, numéros 0-2)
; Castle IDs     : $60-$65  (types 11, numéros 0-5)
;
; ID $25 = %0100101 → type 01 (Overworld), numéro 5 → 1-1
; ID $23 = %0100011 → type 01 (Overworld), numéro 3 → 6-2

128 IDs possibles ($00-$7F), seulement 34 assignés à des vrais niveaux. Les IDs inutilisés pointent vers n'importe quoi.

Les tables de pointeurs : deux listes, deux ordres

Les pointeurs tiles et sprites ne sont pas stockés dans la même ordre. Le code utilise deux listes 16-bit séparées (high byte / low byte dans deux tables distinctes) :

Ordre des pointeurs sprites :
  Index 0-5   : Castle (6 niveaux)
  Index 6-27  : Overworld (22 niveaux)
  Index 28-30 : Underground (3 niveaux)
  Index 31-33 : Water (3 niveaux)

Ordre des pointeurs tiles :
  Index 0-2   : Water (3 niveaux)
  Index 3-24  : Overworld (22 niveaux)
  Index 25-27 : Underground (3 niveaux)
  Index 28-33 : Castle (6 niveaux)

Pourquoi des ordres différents ? Aucune raison technique -- c'est probablement comme ça que les données ont été organisées pendant le développement. Mais ça crée une conséquence fascinante : quand un ID de niveau est invalide, les pointeurs tiles et sprites chargent des niveaux différents, créant des Frankenstein levels.

Pour naviguer entre ces deux listes, le jeu utilise des petites tables d'offset (comme une table des matières) :

; Tables d'offset par type (Water, Overworld, Underground, Castle)
; Chaque entrée = index de début dans la liste correspondante

SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; Water=31, Overworld=6, Underground=28, Castle=0

TileOffsetTable:
  .byte $00, $03, $19, $1C    ; Water=0, Overworld=3, Underground=25, Castle=28

Pour charger le niveau 6-2 (ID $23, Overworld numéro 3) :

; 1. Type = 01 (Overworld) → index dans table d'offset = 1
; 2. Sprite offset = SpriteOffsetTable[1] = 6
;    Index final = 6 + 3 (numéro de niveau) = 9 → 10ème pointeur sprites
; 3. Tile offset = TileOffsetTable[1] = 3
;    Index final = 3 + 3 = 6 → 7ème pointeur tiles
; 4. Résultat : pointeur tiles $A619 + pointeur sprites $9ED0 = 6-2 ✓

Maintenant, que se passe-t-il avec un ID invalide comme $43 (Underground numéro 3, qui n'existe pas) ?

; ID $43, Type = 10 (Underground), numéro = 3
; Sprite offset = SpriteOffsetTable[2] = $1C = 28
;   Index = 28 + 3 = 31 → 32ème pointeur sprites = eau bonus 5-2 !
; Tile offset = TileOffsetTable[2] = $19 = 25
;   Index = 25 + 3 = 28 → 29ème pointeur tiles = 1-4 (Castle) !
;
; Résultat : un niveau souterrain avec les tiles de 1-4
; et les Bloopers de la zone eau de 5-2. Un vrai Frankenstein.

Level ID $43 -- Frankenstein level : tiles 1-4 + sprites eau 5-2

Exploring Glitch Level Pointers -- les tables d'offset expliquées

Le world index table -- quand l'overflow de world 9 crée un glitch level

Le world index table : pourquoi world 9 overflow

Il y a une table ROM de 8 bytes qui donne l'index du premier niveau de chaque monde (1-8). Et juste après, la table des 36 Level IDs de tous les niveaux dans l'ordre de jeu.

; WorldIndexTable (8 bytes)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   -> Monde 1 commence au niveau 0
;   -> Monde 2 commence au niveau 5
;   -> Monde 8 commence au niveau 33

; LevelIDTable (36 bytes)
  .byte $25, $28, $29, $26, $24, ... ; les 36 Level IDs

Quand on essaie de charger world 9, le jeu lit le 9ème byte de WorldIndexTable... qui n'existe pas. Il overflow de 1 byte dans LevelIDTable, lit la valeur $25, puis utilise $25 comme index dans LevelIDTable (37ème entrée) -- ce qui overflow à nouveau de 2 bytes dans SpriteOffsetTable, et lit la valeur 6.

; World 9 :
;   1. WorldIndexTable[8] (overflow) → lit $25 dans LevelIDTable
;   2. LevelIDTable[37] (overflow) → lit le 2ème byte de SpriteOffsetTable = 6
;   3. ID = 6 → Water level number 6 (qui n'existe pas)
;   4. Tile pointer = pointeur water numéro 6 = tiles de 6-2
;   5. Sprite pointer = index 31+6 = 37 > 33 → pointeur invalide
;   6. Résultat : 6-2 sous l'eau avec des sprites glitchés
;      → world 9-1 !

Pour world G (16), l'overflow va encore plus loin et tombe sur le Level ID $01, qui est le niveau cutscene qui précède 1-2 :

; World G (16) :
;   WorldIndexTable[15] → lit $01 dans LevelIDTable
;   LevelIDTable[1] = $29 (cutscene 1-2)
;   → world G-1 = la cutscene d'entrée de 1-2

Pourquoi les glitch worlds existent

Le jeu a 32 niveaux "légitimes" (8 mondes × 4 niveaux). Mais la table de pointeurs fait 128 entrées par type de zone. Les entrées au-delà du niveau 32 contiennent ce qui se trouve en ROM à ces adresses -- parfois un autre niveau, parfois des données sonores, parfois de la RAM, parfois n'importe quoi.

Level ID $01 Water (Minus World) -- tile pointer $AE45, sprite pointer $A171

Level ID $01 + AreaType 0 = Minus World

Le plus célèbre des glitch worlds. Le Level ID $01 en AreaType 0 (eau) pointe vers :

  • Tile pointer : $AE45 → la zone sous-marine de 2-2/7-2
  • Sprite pointer : $A171 → les sprites de 2-2/7-2

Le résultat : un niveau d'eau qui ressemble à 2-2, mais qui boucle à l'infini parce que le flagpole n'existe pas. Le niveau tourne en boucle pour toujours.

C'est le niveau 36-1 (ou 36-1 dans le monde $-1).

Le warm start check de SMB1 -- c'est lui qui permet au Minus World d'exister

; Pourquoi le flagpole manque dans le Minus World :
; Les sprites de 2-2/7-2 ($A171) n'ont pas de flagpole
; dans leur séquence. Le jeu cherche le sprite $FD (flagpole)
; mais ne le trouve jamais → boucle infinie
;
; Le jeu continue de générer le niveau à l'infini
; jusqu'à ce que le timer atteigne zéro.

Les pointeurs qui pointent vers la RAM

Quand le tile pointer ou le sprite pointer pointe vers une adresse en RAM ($00-$7F) plutôt qu'en ROM, le jeu tente d'interpréter les changements constants de la RAM comme des tiles :

; Exemple : Level ID $03 en Water
; Tile Pointer : $A46B (3-3 - valide)
; Sprite Pointer : $009D (pointe vers la RAM page zéro !)
;
; La RAM page zéro contient les registres du jeu,
; la position de Mario, l'état des compteurs...
; Le jeu décompresse ça comme une séquence de sprites,
; et le résultat c'est un niveau avec des ennemis
; qui sont en fait des valeurs de registres.

Quand la page zéro change (parce que Mario bouge, que le timer tourne, etc.), les "sprites" du niveau changent aussi. C'est pour ça que certains glitch worlds ont des ennemis qui clignotent et se transforment constamment.

Level ID $03 Water -- sprite pointer $009D pointe vers la RAM, niveau injouable

Level ID $36 : le niveau vide (Overworld)

Level ID $36 en Overworld :

  • Tile pointer : $AC35 (1-2)
  • Sprite pointer : $A0D8 (1-2)

Résultat : rien. Le jeu charge le niveau mais il est marqué "sans niveau" dans le catalogue de RGMechEx. Les tiles sont peut-être valides mais les sprites pointent vers un endroit qui produit un niveau vide ou non fonctionnel.

Level ID $1D (Castle) : le champion des crashs

Level ID $1D en Castle :

  • Tile pointer : $A210 (4-4)
  • Sprite pointer : $7EA0 (RAM !)

Sprite pointer en RAM = undefined sprites. Le jeu essaie d'afficher un Spiny ball ou un Bullet Bill blaster dans la première ligne de tiles. Ça crashe immédiatement.

; Quand le sprite pointer pointe vers la RAM,
; le jeu décompresse des bytes qui changent tout le temps
; comme des instructions "spawn". Le résultat :
; - Apparition d'objets inexistants (valeur undefined)
; - Crash PPU quand le sprite NES essaie d'afficher un tile invalide
; - Freeze complet de la console

Les 256 glitch worlds catalogués

RGMechEx a écrit un script qui génère les maps de tous les niveaux, pour les 4 types de zone, et les 128 IDs chacun.

Le compteur de monde est sur 8 bits (0-255). Les mondes 1-8 sont légitimes. Il reste 248 glitch worlds potentiels. Chaque glitch world correspond au premier niveau de ce monde, et sa Level ID est calculée par le mécanisme d'overflow de la WorldIndexTable.

Table des glitch worlds -- 248 mondes corrompus, 68 premiers niveaux accessibles

Sur les 128 IDs possibles, seulement 68 sont des "first level" d'un monde (accessibles via le glitch world number). Les 60 autres sont des niveaux 2+ ou inaccessibles.

Type IDs uniques jouables IDs qui crashent IDs vides
Water (0) ~20 ~60 ~48
Overworld (1) ~30 ~55 ~43
Underground (2) ~15 ~65 ~48
Castle (3) ~25 ~58 ~45

Beaucoup de IDs mènent au même niveau à cause des pointeurs qui tombent sur les mêmes adresses ROM. Le Level ID $28 (Overworld) par exemple -- tile pointer $A7CD (2-1) -- apparaît dans 38 glitch worlds différents, parce que son sprite pointer $9F51 pointe vers une zone de la ROM qui est utilisée comme padding/données sonores réutilisé par plein d'IDs.

Carte du niveau ID $28 (Overworld) -- 2-1 tiles avec des sprites normaux, 38 glitch worlds

Super Mario Bros. Glitch Levels Explained -- la 3ème vidéo

Les 6 glitch levels vraiment uniques

Parmi les 19 IDs de glitch level accessibles, seulement 6 ne crashent pas immédiatement au chargement :

World Level ID Description
E-1 (224) $50 Un seul ? block au-dessus d'un gouffre. Mario meurt instantanément.
W $57 Mario spawn bloqué, incapable de bouger.
42 (133) $50 Tunnel de nuages qui piège Mario s'il va assez loin.
62 (131, 240) $4D Château gelé : Mario spawn en haut, ne peut pas tomber → bloqué.
127 $4B Tunnel souterrain, mais crashe si on va trop loin.
137 $4B Active le défilement automatique des cutscenes. Mario rencontre un unique brick block qui le bloque à jamais.

Level ID $50 (cloud tunnel) -- le glitch world 42-1 et E-1 Level ID $4D (castle) -- world 62-1, Mario bloqué au spawn Level ID $4B (tunnel) -- world 127-1, crashe si on va trop loin

Six glitch worlds sur 248 qui produisent quelque chose de vraiment nouveau. Le reste, ce sont des niveaux normaux avec le mauvais type de zone, ou des écrans noirs.

Le format des niveaux en détail

Cap sur le format exact des données de niveau, pour comprendre pourquoi les glitch levels tiennent debout (ou pas).

Le header niveau : 2 bytes, 6 propriétés

Chaque niveau commence par un header de 2 bytes qui contrôle 6 propriétés :

; Byte 0 : timer + Y start + modifier
;   Bits 7-6 : timer (00=inchangé, 01=200, 10=300, 11=400)
;   Bits 5-3 : Y start Mario (111/110 = autowalk)
;   Bits 2-0 : level type modifier
;              000=default, 001=waves, 010=brick wall,
;              011=water bottom, 100=night, 101=snow,
;              110=snow night, 111=gray night

; Byte 1 : platform + background + floor pattern
;   Bits 7-6 : special platform (00=tree, 01=mushroom,
;                                 10=Bullet Bill, 11=cloud)
;   Bits 5-4 : background (00=none, 01=clouds,
;                           10=montains, 11=fences)
;   Bits 3-0 : floor pattern initial (0-15)

Le type modifier contrôle des variations visuelles : les vagues en haut des niveaux d'eau, le fond brique de 8-3, la palette nuit de 4-3, la neige de 6-2, etc.

Les objets tiles : 2 bytes, Next Screen Flag, queue 3 slots

Après le header vient une liste d'objets tile, chaque objet fait 2 bytes. Le byte $FD marque la fin de la liste.

; Format objet tile (16 bits) :
; Byte 0 :
;   Bits 7-4 : X position (colonne 0-15)
;   Bits 3-0 : Y position
;     Y=0-11  : position Y normale
;     Y=12    : objets spéciaux (trous, ponts, rope, ? blocks)
;     Y=13    : screen skip / objets spéciaux 2
;     Y=14    : changement de modifier/scenery/floor
;     Y=15    : objets spéciaux 3 (château, escaliers, gros tuyau)

; Byte 1 :
;   Bit  7   : NEXT SCREEN FLAG
;   Bits 6-4 : type d'objet (0-7)
;   Bits 3-0 : largeur/hauteur / sous-type

Quand le bit "next screen" est mis, la colonne de travail courante est incrémentée de 1. Ça permet de placer des objets au-delà des 16 premières colonnes. Les objets doivent être listés dans l'ordre (gauche à droite) parce que le jeu les charge séquentiellement :

; La routine de chargement a DEUX phases par colonne :
; Phase 1 : chercher les nouveaux objets qui commencent sur cette colonne
;            et les ajouter à la file d'attente (queue)
; Phase 2 : traiter chaque objet dans la queue en dessinant les tiles,
;            et retirer ceux qui finissent sur cette colonne

La queue fait exactement 3 slots. Conséquence directe : on ne peut pas avoir plus de 3 objets qui commencent sur la même colonne. Si la queue est pleine, le 4ème objet est ignoré et ne sera jamais chargé.

C'est pour ça que les niveaux bien conçus évitent d'empiler trop d'objets. Exemple dans 1-2 : la colonne avec le 1up block dans le plafond + les briques à côté sont splitées en deux objets distincts pour respecter la limite de 3.

Y position spéciale : 12, 13, 14, 15

Quand Y=12, l'objet n'a pas de position Y (elle est hardcodée par type) :

; Y=12 : objets sans Y position
;   Type 0 : trou (supprime le sol)
;   Type 1 : rope de plateforme mobile
;   Types 2-4 : ponts à Y fixe
;   Type 5 : trou avec eau/lave
;   Types 6-7 : rangées de ? blocks

Quand Y=13, deux sous-groupes. Si le bit 6 du byte 1 est à 1 :

; Y=13, bit6=1 : objets spéciaux
;   0 = L-pipe (cutscene), 1 = flagpole, 2-3 = pont/axe/hamer (fin château)
;   4 = stop screen, 5 = random enemies, 6 = loop level, 7+ = crash possible

Si bit6=0, les 5 bits de poids faible encodent un screen skip (sauter directement à un écran N, sans passer par le next screen flag un par un).

Quand Y=14 : même principe avec bit6=1 pour changer le type modifier, bit6=0 pour changer le fond + floor pattern.

Les floor patterns : 16 motifs de sol

Le sol des niveaux n'est pas fait d'objets individuels. SMB1 utilise des floor patterns, un motif de fond qui s'applique à toutes les colonnes jusqu'au prochain changement :

; Floor patterns (4 bits = 16 possibilités)
;   0 = vide total
;   1 = sol 2 tiles haut
;   2 = sol 1 tile haut
;   3 = sol + bottom
;   4 = sol + bottom 2
;   5 = sol 1/2 tile
;   6 = 3/4 sol
;   ... jusqu'à 15 = rempli total (sol + plafond)

C'est pour ça que les trous sont des objets : ils override le floor pattern sur une colonne spécifique, sans avoir à changer le pattern pour tout le reste.

La limite des 256 bytes et le repeat

Toutes les données tile d'un niveau tiennent dans 256 bytes maximum. Le Y register du 6502 est utilisé comme index, et il fait 8 bits. Si le jeu arrive à la fin des données sans trouver le byte $FD, il reboucle au début et répète les 256 bytes à l'infini :

; Index Y = 8 bits → max 256 bytes de données tiles
; Si Y overflow (255 → 0) sans rencontrer $FD → repeat
; Même chose pour les sprites, mais les objets pipe (3 bytes)
; décalent la parité de l'index à chaque chargement.

Certains glitch levels exploitent ce repeat pour générer des niveaux qui durent "indéfiniment".

Le système de sprites : 2 bytes + pipe transitions

Les sprites suivent un format similaire, mais sans header et avec quelques différences clés. Le byte $FF marque la fin de la liste.

; Format sprite (2 bytes) :
; Byte 0 : position X (colonne)
; Byte 1 :
;   Bit 7 : NEXT SCREEN FLAG
;   Bits 6-0 : type de sprite
;       Certains types incluent : goomba, koopa, Blooper,
;       Bullet Bill, Lakitu, Spiny, plateformes,
;       commande warp zone, toad/princesse,
;       commandes de spawn de groupes d'ennemis

Le bit de poids faible du byte 1 est le hard level flag : si mis à 1, le sprite n'apparaît que dans les niveaux ≥ 5-3. C'est ainsi que les niveaux "hard mode" sont créés.

Y position 15 = screen skip (identique aux tiles). Y position 14 = pipe transition (3 bytes) :

; Sprite Y=14 : pipe/vine transition (3 bytes !)
;   Byte 0 : position X
;   Byte 1 : bits 6-0 = Level ID 7-bit (destination)
;   Byte 2 : bits 4-0 = screen de destination
;            bits 7-5 = world où cette transition est valide
;
; Pourquoi un world ? Les bonus rooms sont réutilisées entre mondes.
; Exemple : la salle bonus de 1-1 est aussi utilisée par 2-1 et 7-1.
; Cette salle a 3 transitions, une par monde, pour que Mario
; réapparaisse au bon endroit.

Les sprites n'ont pas de queue system. La seule limite est qu'il ne peut pas y avoir plus de 4 sprites chargés simultanément dans la zone de spawn (juste hors-écran à droite). Au delà, les sprites sont ignorés.

Comment accéder aux glitch worlds

Il y a deux méthodes principales.

La méthode classique : le wall clip

Le wall clip (passage à travers les murs) permet de sortir du niveau normal et de marcher jusqu'à la warzone cachée. En manipulant le compteur de monde via la RAM, on peut charger n'importe quel Level ID.

La technique :

  1. World 1-2 : aller dans le tuyau de fin caché
  2. Faire le wall clip sur le mur de droite
  3. Marcher dans le vide jusqu'à la zone warp
  4. Le jeu interprète les valeurs comme des mondes

Mais cette méthode ne donne accès qu'à une petite partie des glitch worlds.

La méthode extreme : NES Tennis cart swap

Voir la section "Le warm start" plus haut pour le détail complet. En résumé : le compteur de pas de Tennis écrit sur le même byte RAM que le monde de départ de SMB1, et la détection warm start préserve cette valeur.

Le coin des bidouilleurs : le code pour tout explorer

Si tu veux explorer tous les glitch toi-même dans un émulateur, tu peux patcher le Level ID directement :

; Patch pour FCEUX / Mesen :
; Adresse RAM $075F = Level ID actuel
; Adresse RAM $0760 = Area Type (0=Water, 1=Overworld, 2=Underground, 3=Castle)

; Exemple : charger le Level 57 (0x39) en Overworld
; Dans l'émulateur, ouvrir le traceur mémoire et écrire :
; $075F = 0x39
; $0760 = 0x01
; Puis entrer dans un tuyau de warp ou mourir et recommencer
; → Le jeu charge le niveau ID $39 en Overworld

RGMechEx a publié la liste complète des 128 niveaux × 4 types avec des maps générées automatiquement sur rgmechex.com. Chaque entrée montre le tile pointer, le sprite pointer, et une carte visuelle du niveau.

Les niveaux les plus wtf

Level ID $1F (Water) : 15 glitch worlds en un

Le tile pointer $A302 (3-4) combiné au sprite pointer $02A0 donne 15 glitch worlds différents (D-1, J-1, Y-1, Z-1, 55-1, 73-1...). Explication : le sprite pointer pointe vers une zone de la ROM qui contient des données suffisamment proches de sprites valides pour produire des résultats jouables, mais la combinaison des tiles de château 3-4 avec des sprites d'overworld crée un rendu absurde.

Level ID $28 (Overworld) : 38 glitch worlds = record

Le record absolu. 38 entrées de glitch world pointent vers le même niveau (2-1 tiles + $9F51 sprites). Pourquoi ? Parce que le sprite pointer $9F51 tombe dans une zone de la ROM qui est utilisée comme padding/données sonores réutilisé par plein d'IDs.

Level ID $49 (Underground) : le niveau FDS

Tile pointer $76AE + sprite pointer $1C9D. Le tile pointer pointe vers la zone de la ROM réservée à la version Famicom Disk System. Résultat : un niveau avec des tiles qui n'existent pas dans la cartouche standard. C'est le niveau qui fait apparaître le niveau 52-1 et 196-1.

Level ID $00-$02 : les vrais niveaux bonus

Ces IDs sont utilisés par des sous-niveaux légitimes du jeu :

  • $00 : zone sous-marine de 5-2/6-2 (utilisé par H-1, 39-1)
  • $01 : l'eau de 2-2/7-2 (le Minus World, 36-1)
  • $02 : sous-niveau de 8-4 (136-1, 151-1, 215-1)

La différence entre un niveau "bonus" accessible normalement et un glitch world, c'est que les warp zones vérifient le monde actuel :

; Vérification warp zone (simplifié)
; Le jeu vérifie que le monde cible est entre 1 et 8
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; < 1 → refusé
  cmp #9
  bcs InvalidWarp       ; > 8 → refusé
  ; world valide entre 1 et 8 uniquement
  jmp DoWarp

Les glitch worlds avec des numéros > 8 ou 0 ne peuvent pas être atteints par des tuyaux normaux. Il faut le wall clip ou le cart swap.

Pourquoi certains niveaux crash : les jump tables

Quand le jeu charge un objet tile, il utilise son type comme index dans une jump table :

; Jump table des objets tiles standards (types 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; type 0 : bloc ?, hidden, flagpole...
  .word Obj_Platform      ; type 1 : plateforme spéciale
  .word Obj_BrickRow      ; type 2 : rangée de briques
  .word Obj_BlockRow      ; type 3 : rangée de blocks
  .word Obj_CoinRow       ; type 4 : rangée de pièces
  .word Obj_BrickCol      ; type 5 : colonne de briques
  .word Obj_BlockCol      ; type 6 : colonne de blocks
  .word Obj_Pipe          ; type 7 : tuyau
  .word Obj_8             ; type 8
  .word Obj_9             ; type 9
  .word Obj_10            ; type 10
  .word Obj_11            ; type 11

Les jump tables : pourquoi un type d'objet invalide fait crasher le jeu

Si un objet a un type invalide (≥12), le jeu saute à un pointeur qui n'existe pas dans cette table. 4 outcomes possibles :

  1. Pointeur valide → l'objet se charge normalement
  2. Pointeur vers une autre jump table (chevauchement) → un objet différent apparaît. Exemple : type 12 pointe vers la table Y=13, ce qui donne un L-pipe.
  3. Pointeur vers de l'exécutable → exécution de code aléatoire (crash probable)
  4. Placeholder explicite (NOP) → l'objet ne fait rien (certains sprites sont comme ça, produisant des ennemis qui volent sur place sans bouger)

Glitch level ID $58 : le sprite pointer pointe vers une adresse invalide, le jeu crashe

Glitch level ID $50 : le cloud tunnel, un niveau généré par des données corrompues

La glitch level ID $58 (le tunnel qui crashe) : son sprite pointer pointe vers une région mémoire qui n'existe pas sur NES sans mapper ROM. Le jeu essaie de charger le même Koopa 5 fois par frame à la position (0,0), ce qui sature la PPU et provoque un freeze.

; Pourquoi ID $58 crashe :
; Sprite pointer → adresse invalide (hors espace NES standard)
; → Le jeu lit des bytes indéterminés comme des types de sprite
; → Le Koopa $00 (type invalide sans handler) boucle en appel récursif
; → Stack overflow 6502 → freeze

Le paradoxe pipe warp

Rappelle-toi le check target_world BETWEEN 1 AND 8. Même si tu trouves un tuyau dans un glitch world, le jeu vérifie que le monde de destination est entre 1 et 8. Les glitch worlds ont des numéros > 8 (36-1, 255-1...), donc la warp échoue.

C'est aussi pour ça que le Minus World n'a pas de fin : le flagpole n'est pas présent dans les sprites, et les tuyaux ne mènent nulle part.

Le trick des 5 objets dans une colonne

Il existe un edge case qui permet d'outrepasser la limite de 3 objets par colonne. Quand la queue se bloque (slots pleins + objet suivant avec next screen flag manquant), le jeu "prétraite" la colonne courante en boucle jusqu'à trouver un objet avec next screen flag. Pendant chaque prétraitement :

; Pendant le prétraitement de colonne :
; 1. Les objets dans la queue voient leur largeur restante
;    décrémentée à chaque "fausse avancée" de colonne
; 2. Si un objet atteint largeur=0, il quitte la queue
; 3. Un slot libéré peut être rempli par un nouvel objet
;    ajouté dans la même colonne

; Résultat : jusqu'à 5 objets peuvent être traités sur la même colonne.
; Technique : placer 2 objets qui traversent la screen boundary
; (slots 1 et 2), 1 objet dummy en X < précédent (bloque la queue),
; puis 3 objets à X=0 de l'écran suivant (dont un avec next screen flag).

C'est ce qu'on appelle un "queue skip" et c'est utilisé par certains romhackers pour créer des niveaux plus denses que ce que le format permet normalement.

Les différences entre versions

Famicom Disk System

La version FDS de SMB1 a une memory map différente. Tous les pointeurs de niveau sont décalés, mais les données sont les mêmes. Ce qui change : les indices des glitch worlds sont complètement différents :

FDS World 36 → Level ID $09 (eau version de 5-3)
  → Le flagpole est présent ! On peut finir le niveau.
  → Ensuite : $27 (7-3 normal) → $44 (4-4 underground)
  → $44 est finissable → la hache fonctionne → fin du jeu !
  
Le Minus World FDS est donc un "bonus world" qui peut mener
à la complétion du jeu, contrairement à la version NES.

Mon niveau FDS préféré : ID $5F, une version souterraine de la deuxième moitié de 3-3 en tunnel bas (dommage que ce soit un autoscroller).

The Lost Levels (Super Mario Bros. 2 japonais)

Lost Levels change beaucoup de choses :

  1. Ordre identique tiles/sprites : plus de Frankenstein levels (tiles et sprites chargent le même niveau même avec un ID invalide)
  2. Une seule table de pointeurs 16-bit au lieu de deux tables séparées high/low
  3. 4 fichiers disque : la ROM a été splitée pour le FDS :
    • Fichier 1 : worlds 1-4
    • Fichier 2 : worlds 5-8
    • Fichier 3 : world 9 + sound engine
    • Fichier 4 : worlds A-D (table de pointeurs complètement différente)
  4. Même Level ID = 4 niveaux possibles selon le fichier chargé
  5. Plus de glitch Tennis : le continue option (continue au même monde après game over) rend le warm start inutile, et le jeu reset immédiatement si world > 9
  6. Nouveaux objets : champignon poison, block invisible, block invisible fire flower, upside down pipes, vent -- mais insérés au milieu des listes existantes → incompatibilité backward avec SMB1
  7. Piranha Plants toujours rouges après world 4, springboards verts seulement en worlds 2/B/3/C/7

Super Mario All-Stars (SNES)

Portage direct avec les mêmes routines 6502 (le SNES exécute le code NES en mode compatible) :

  • Warp zone fixée : plus de Minus World (entrer dans le tuyau gauche avant le texte mène au bon monde)
  • Plantage : la plupart des glitch levels crashent (sauf ID $6A et 9-1)
  • Objets château ajoutés : rendus plus uniques
  • Mais : le 4-2 wrong warp fonctionne encore (pas patché !)

Le 4-2 wrong warp : un bug de placement d'objet

Dans 4-2, il y a deux objets de transition pipe : la vigne (warp zone) et le tuyau (coin cash room). Le premier objet de transition (celui de la vigne) est placé bien avant que la vigne n'apparaisse sur l'écran. Le deuxième (le tuyau) est placé trop tard dans le niveau.

; Timing des transitions dans 4-2 :
; Objet transition 1 (vigne → warp zone) : placé 3 écrans avant la vigne
; Objet transition 2 (tuyau → coin cash) : placé 1 écran après le tuyau
;
; Normalement le premier objet est désactivé avant que Mario
; n'atteigne le tuyau. Mais si Mario va vite (ou utilise
; le raccourci du bloc B+right), la transition de la vigne
; est toujours active quand il touche le tuyau !
; → Le jeu charge la warp zone au lieu du coin cash.
;
; Si l'objet avait été placé juste après la vigne mais avant
; le tuyau, le bug n'existerait pas.

Les niveaux à boucle

Comment fonctionnent les loop (8-4, 7-4) ? Le niveau a des checkpoints avec des numéros d'écran et des positions Y hardcodés :

; Checkpoint : {screen_number, vertical_position}
; Si Mario passe ce checkpoint à la bonne hauteur → niveau continue
; Sinon → warp back de 4 écrans (64 blocks)
;
; Pour faire une boucle infinie : vertical_position = $F0
; (en dessous du bas de l'écran) → impossible de valider.
;
; Les checkpoints sont simples (un seul flag) sauf pour world 7
; qui utilise des triplets (3 flags, il faut en échouer au moins 1)
;
; Le warp back est rude : offset de tile data réglé à une valeur
; hardcodée, offset de sprite data remis à 0. Les ennemis présents
; sont déchargés instantanément → les firebars disparaissent.

Changer le format, pas le code

Une des leçons les plus fascinantes de cette architecture, c'est que les développeurs de SMB1 ont réussi à créer un système de niveau très expressif sans jamais toucher au code de rendu 6502. Toute la variation entre les niveaux vient des données (pointeurs, objets, sprites, floor patterns), pas du code.

Les 256 glitch worlds existent parce que les tables de pointeurs sont dimensionnées pour 128 entrées × 4 types, et que le jeu ne valide jamais les valeurs qu'il lit. Quand un pointeur tombe en RAM, le jeu interprète les registres de Mario comme des tiles. Quand un pointeur tombe dans les données sonores, le jeu joue de la musique sous forme de level design. Et quand les jump tables overflowent, le jeu exécute n'importe quoi jusqu'au crash.

More Super Mario Bros. Mechanics Explained -- la 4ème vidéo

Ce qu'on peut apprendre de tout ça

  1. Séparation tiles/sprites : indépendance totale des deux couches, avec des ordres de stockage différents qui créent des Frankenstein levels uniques
  2. Compression RLE + système d'objets : les niveaux ne sont pas des bitmaps mais des listes d'objets placés, avec des floor patterns pour le sol
  3. Queue 3 slots : limite stricte du hardware (et du design de niveau)
  4. Confiance totale aux pointeurs : le jeu fait confiance aux pointeurs et aux jump tables, ce qui produit soit des glitchs jouables, soit des crashs
  5. 256 bytes max : la limite du Y register 6502, qui fait que les données se répètent si on va trop loin
  6. Warm start / cold start : un système de "continuer" qui a ouvert la porte au cart swap Tennis → Mario

Le plus beau : tout ça, c'est du code 6502 qui tient dans 40KB. Le code dialogue directement avec le hardware et accorde une confiance totale à chaque accès mémoire. Si le pointeur est pourri, le jeu crashe. Et les crashs, on les appelle des glitch worlds.

Les 3 trucs à retenir

  1. Les glitch worlds sont des pointeurs qui tombent mal -- Le jeu a 128 IDs × 4 types de zone, mais seulement 34 niveaux uniques. Quand le world number est corrompu (par Tennis ou wall clip), le jeu charge un pointeur conçu pour un autre niveau, et les 512 combinaisons possibles produisent des résultats imprévisibles.

  2. Le Minus World est un bug de warp combiné à de la corruption -- Le tuyau gauche dans 1-2, si activé avant que le texte n'apparaisse, charge world 36 (0x24). Ce world pointe vers Level ID $01 (eau de 2-2), un niveau sans flagpole. Et comme il n'y a pas de transition pipe pour world 36, le niveau boucle à l'infini. L'absence de vérification crée l'icône.

  3. Tennis → Mario, 15 ans avant OoT → Paper Mario -- La RAM du NES survit à un swap de cartouche grâce aux condensateurs et au système de warm start / cold start de SMB1. Le compteur de pas de Tennis (qui incrémente un byte RAM en jouant le son des pas) tombe pile sur l'adresse du world number. Il faut que les digits du top score restent à 0, que le byte $A5 soit intact, et que le jeu détecte un warm start -- un concours de circonstances parfait qui n'a fonctionné qu'avec Tennis.

Les vidéos originales de Retro Game Mechanics Explained sont un putain de travail de fourmi -- le niveau de détail sur la désassemble 6502, les maps automatiques de tous les niveaux, les explications du cart swap et du warm start. Si t'as pas vu la série, mate-la, elle est courte et chaque minute est dense.

Le code source des maps est dispo sur rgmechex.com, et le désassemble complet de SMB1 est open source sur plein de repos. Y'a 40 ans, des programmeurs japonais ont écrit ce système de niveau en 6502 avec zéro test unitaire et zéro bug tracker, et on continue d'apprendre des techniques d'optimisation en ouvrant leur code aujourd'hui.

超级马里奥:关卡格式、指针和 256 个 glitch 世界

128 关卡 x 4 区域类型怎么塞进 40KB ROM,Minus World 为什么存在,一场 NES 网球赛怎么载出 glitch 世界。

引言

超级马里奥 40KB ROM。八世界,32 关,敌人、音乐、道具,全塞里面。

但模拟器里拨对字节,能载 36-1 关。或 255-1。或掉进全是 Bowser sprite 和通向虚无水管的世界。

glitch 世界存在理由简单:SMB1 关卡存储是 8-bit 优化的艺术品,逼游戏读不该读的地方,结果很迷人。

Retro Game Mechanics Explained 做了 4 期视频讲这个,合一篇 6502 深潜,当年最畅销游戏的代码。

GLITCH OBJECTS,RGMechEx 隐藏机制系列标题

World 9-1,经网球换卡进的第一个 glitch 世界标题屏

warm start:为什么网球的 RAM 在 SMB1 里活下来

讲关卡存储前,先讲 SMB1 启动。因为 NES 网球换卡 glitch 全靠游戏的warm start / cold start 检测系统。

保留的 41 字节

SMB1 检测到 cold start(首次上电或断电再开),全 RAM 清零。检测到 warm start(按 reset,不断电),保留 41 字节内存区:

; warm start 保留的 41 字节 RAM
; 地址 $075F-$0787
;
; $075F:启动字节(world - 1)    [1 字节]
; $0760:选世界 flag(B 键) [1 字节]
; $0761-$0762:未用                    [2 字节]
; $0763-$0768:计时器(6 位,显示 3 位) [6 字节]
; $0769-$076E:Luigi 金币                   [6 字节]
; $076F-$0774:Mario 金币                   [6 字节]
; $0775-$077A:Luigi 分数                   [6 字节]
; $077B-$0780:Mario 分数                   [6 字节]
; $0781-$0786:最高分(6 位,1 位隐藏) [6 字节]
; $0787:魔法字节 $A5                 [1 字节]

41 字节只干一件事:game over 后同世界续关。6-3 死,游戏把世界 6 写启动字节,标题屏按住 A 加 Start 从 6-1 重开。

warm start 保留的 41 字节 RAM,TOP SCORE、MARIO SCORE、TIMER、WORLD SELECT、CONTINUE WORLD、魔法字节 $A5

warm start 双重校验

cold start 对 warm start,reset 检测流程图

SMB1 启动不查一个条件查两个:

CheckWarmStart:
  ; 1. 查 $0787 魔法字节 $A5
  lda $0787
  cmp #$A5
  bne ColdStart        ; 不是 $A5 → cold start

  ; 2. 查最高分 6 位 ($0781-$0786)
  ;    每位必须 0-9
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; 位 >= 10 → cold start
  inx
  cpx #6
  bne CheckLoop

  ; 两条件过 → warm start
  ; RAM 不清,起始世界保留
  jmp WarmStartBoot

查 $A5 字节和最高分位,warm start 核心

为什么双重?$A5 可能碰巧在(别的游戏留这值,或 RAM 芯片默认静置态)。再验最高分位合法(0-9),保证数据自洽。

为什么只有网球行

SMB1 首次插入(cold start),游戏:

  1. 全 RAM 清,最高分 0,world 字节 0
  2. $0787 写 $A5

然后不断电换网球。网球:

  • 启动不清 RAM(少有 NES 游戏清)
  • 不动最高分字节,保持 0(合法)
  • 不动 $A5 字节,留着
  • $075F 地址拿来做玩家脚步计数
; 网球脚步计数:
; 球场走一步 $075F 字节加一。
; SMB1 里同字节是"world number - 1"。
;
; 0 步  → world 0 → SMB1 里 world 1
; 1-7 步 → world 1-7 → 正常世界
; 8+ 步 → world 8+ → glitch 世界!
;
; 计数只在音乐停时涨
; (脚步音效音乐放时不播)。

换回 SMB1:

  1. $A5 还在(网球没碰)
  2. 最高分位还是 0(合法)
  3. world 字节变成 8+(网球脚步加上去的)
  4. SMB1 判 warm start,保留被污染的 world 字节
  5. 按住 A 加 Start,world 9-1、world A-1、world 36-1 等等。

为什么先开马里奥再网球

微妙点:必须先开 SMB1,再网球,再 SMB1。直接从网球起,$A5 字节网球不写,warm start 检测失败,RAM 清掉。

网球脚步计数:每步 world 字节加一

Access Glitch Worlds via NES Tennis,换卡讲解视频

SMB1 怎么把关卡塞进 40KB

任天堂 R&D4 要解看着简单的问题:横版卷轴关卡,tile、敌人、道具,塞进抠门的 ROM 预算。

解法是两层数据完全独立:

tile 布局(关卡地图)

每关 tile 经 ROM 压缩结构指针定义。压缩原始但天才:控制字节加 1-3 数据字节。

tile 格式是 run 系统(类 RLE):

; SMB1 tile 格式(简化)
; 每"命令"是控制字节:
;
; $00-$7F:放一个 tile,前进 1 列
; $80-$BF:tile 重复 N 次(N = 字节 - $80 + 1)
; $C0-$FF:特殊命令(行尾、跳跃、换色板)

例子:画 3 个连续砖块:
  $82 $01    ; tile $01(砖)重复 3 次

每关 13 行 16 列 tile(13x16 = 208 可见 tile)。压缩格式能压很低,天空和空列几乎不占地方。

6502 解压循环:

; tile 解压,主循环
; 输入:tile_data 指针 $XX
; 输出:关卡 tilemap 进 PPU RAM

DecompressTile:
  lda (tile_ptr),y      ; 读控制字节
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F:单个 tile
  cmp #$C0
  bcc RunLength         ; $80-$BF:run-length
  jmp SpecialCommand    ; $C0-$FF:特殊命令

SingleTile:
  sta PPU_DATA          ; 直接写 tile
  jmp Next

RunLength:
  sec
  sbc #$7E              ; N = 控制 - $7E
  tax
  lda (tile_ptr),y      ; 读要重复的 tile
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

sprite 布局(敌人和物件)

并行,敌人和物件(? 块、水管、板栗仔、乌龟)存完全独立结构。每刷怪 2 字节:

; SMB1 sprite 格式
; 字节 0:X 位置(列)
; 字节 1:sprite 类型加 Y 页面位
; Y 从序列 index 推

sprite 序列:
  $01 $4B    ; 列 1 板栗仔
  $09 $4B    ; 列 9 板栗仔
  $10 $61    ; 列 16 ? 块(出金币)
  $15 $54    ; 列 21 绿乌龟
  $FF        ; 序列尾

每关最多引用 5 页 sprite(其实 5 个 16 列"屏"),实际大多数关只用 2-3 页。

指针表

设计天才在指针表。每关存成 一对 ROM 地址:

// 内部结构(简化)World Map
struct LevelPointer {
    uint16_t tile_ptr;   // tile 数据 ROM 地址
    uint16_t sprite_ptr; // sprite 数据 ROM 地址
};

// 4 张独立表,按 AreaType:
 // 0 = 水,1 = 地上,2 = 地下,3 = 城堡
LevelPointer level_table[4][128];

每表 128 项。512 种组合,官方游戏只用一小撮。剩下的读未初始化 RAM 或当指针使的数据。

载关时游戏这么干:

; 载关
; A = AreaType(0-3),X = LevelID(0-127)

LoadLevel:
  sta AREA_TYPE
  asl                  ; x2 做 16 位表偏移
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; tile 指针
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; sprite 指针
  jsr DecompressTiles

游戏无条件信指针:表里地址读出来,解压那个地址的东西,完事。

Level ID $06(水),9-1,6-2 的水下版

Level ID 表:128 可能项,34 已分配

tile 和 sprite 指针顺序不同,Frankenstein 关卡的病因

34 个独立关和 7-bit ID 系统

NES RAM 芯片(MB8416A),换卡时保数据的就是它

SMB1 不是 32 关,是 34 个独立关。很多关复用(5-3 = 1-1 加飞弹,"hard mode" flag 标)。真独立关:

  • 水(类型 0):3 关(2-2、7-2、5-2/6-2 奖励区)
  • 地上(类型 1):22 关(含两云奖励房)
  • 地下(类型 2):3 关(含地下奖励房)
  • 城堡(类型 3):6 关
  • 加 1 cutscene 房(地下/水关前)
  • 1 个 4-2 warp 区

每关 ID 7 位。低 5 位子组编号,高 2 位区域类型:

; 7 位 Level ID 编码
; 位 6-5:类型(00=水,01=地上,10=地下,11=城堡)
; 位 4-0:子组编号
;
; 水 ID       : $00-$02  (类型 00,编号 0-2)
; 地上 ID     : $20-$35  (类型 01,编号 0-21)
; 地下 ID     : $40-$42  (类型 10,编号 0-2)
; 城堡 ID     : $60-$65  (类型 11,编号 0-5)
;
; ID $25 = %0100101 → 类型 01(地上),编号 5 → 1-1
; ID $23 = %0100011 → 类型 01(地上),编号 3 → 6-2

128 可能 ID($00-$7F),34 个有真关卡。没用的 ID 指向随便哪。

指针表:两表,两顺序

tile 指针和 sprite 指针存的顺序不一样。代码用两张独立 16 位表(高字节低字节分两表存):

sprite 指针顺序:
  index 0-5   :城堡(6 关)
  index 6-27  :地上(22 关)
  index 28-30 :地下(3 关)
  index 31-33 :水(3 关)

tile 指针顺序:
  index 0-2   :水(3 关)
  index 3-24  :地上(22 关)
  index 25-27 :地下(3 关)
  index 28-33 :城堡(6 关)

顺序为什么不同?没技术原因,大概开发时数据就这么摆的。但后果迷人:ID 非法时 tile 和 sprite 指针载不同关卡,拼出 Frankenstein 关。

两表之间导航靠小 offset 表(像目录):

; 各类型 offset 表(水、地上、地下、城堡)
; 每项 = 对应表起始 index

SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; 水=31,地上=6,地下=28,城堡=0

TileOffsetTable:
  .byte $00, $03, $19, $1C    ; 水=0,地上=3,地下=25,城堡=28

载 6-2(ID $23,地上编号 3):

; 1. 类型 = 01(地上)→ offset 表 index = 1
; 2. sprite 偏移 = SpriteOffsetTable[1] = 6
;    最终 index = 6 + 3(关编号)= 9 → 第 10 个 sprite 指针
; 3. tile 偏移 = TileOffsetTable[1] = 3
;    最终 index = 3 + 3 = 6 → 第 7 个 tile 指针
; 4. 结果:tile 指针 $A619 加 sprite 指针 $9ED0 = 6-2 ✓

非法 ID $43(地下编号 3,不存在)呢?

; ID $43,类型 = 10(地下),编号 = 3
; sprite 偏移 = SpriteOffsetTable[2] = $1C = 28
;   index = 28 + 3 = 31 → 第 32 个 sprite 指针 = 5-2 水奖励区!
; tile 偏移 = TileOffsetTable[2] = $19 = 25
;   index = 25 + 3 = 28 → 第 29 个 tile 指针 = 1-4(城堡)!
;
; 结果:地下关配 1-4 tile
; 加 5-2 水区 Blooper。真 Frankenstein。

Level ID $43,Frankenstein 关:1-4 tile 加 5-2 水 sprite

Exploring Glitch Level Pointers,offset 表讲解

world index 表,world 9 溢出造出 glitch 关

world index 表:为什么 world 9 溢出

ROM 里 8 字节表给每世界(1-8)首关 index。后面紧跟 36 关 Level ID 全表按游戏顺序。

; WorldIndexTable(8 字节)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   → 世界 1 从关 0 起
;   → 世界 2 从关 5 起
;   → 世界 8 从关 33 起

; LevelIDTable(36 字节)
  .byte $25, $28, $29, $26, $24, ... ; 36 Level ID

载 world 9,游戏读 WorldIndexTable 第 9 字节,不存在。溢 1 字节进 LevelIDTable,读到 $25,再拿 $25 当 LevelIDTable index(第 37 项),再溢 2 字节进 SpriteOffsetTable,读到 6。

; World 9:
;   1. WorldIndexTable[8](溢出)→ LevelIDTable 读到 $25
;   2. LevelIDTable[37](溢出)→ SpriteOffsetTable 第 2 字节 = 6
;   3. ID = 6 → 水关编号 6(不存在)
;   4. tile 指针 = 水指针编号 6 = 6-2 tile
;   5. sprite 指针 = index 31+6 = 37 > 33 → 非法指针
;   6. 结果:水下 6-2 加 glitch sprite
;      → world 9-1!

World G(16)溢更远掉到 Level ID $01,1-2 前 cutscene 关:

; World G(16):
;   WorldIndexTable[15] → LevelIDTable 读到 $01
;   LevelIDTable[1] = $29(1-2 cutscene)
;   → world G-1 = 1-2 入场 cutscene

glitch 世界为什么存在

游戏"正经"关 32 关(8 世界 × 4 关)。指针表每区域类型 128 项。32 关之外的项装 ROM 那地址的东西,可能是别的关,可能是声音数据,可能是 RAM,可能是随便啥。

Level ID $01 水(Minus World),tile 指针 $AE45,sprite 指针 $A171

Level ID $01 加 AreaType 0 = Minus World

最出名的 glitch 世界。Level ID $01 配 AreaType 0(水)指向:

  • tile 指针:$AE45 → 2-2/7-2 水下区
  • sprite 指针:$A171 → 2-2/7-2 sprite

结果:长得像 2-2 的水关,但旗杆没了。关卡无限循环。

就是 36-1 关(或 $-1 世界的 36-1)。

SMB1 的 warm start 检查,Minus World 能存在的根

; Minus World 为什么没旗杆:
; 2-2/7-2 sprite($A171)序列没有旗杆
; 游戏找 $FD sprite(旗杆)
; 永远找不到 → 无限循环
;
; 游戏无限生成关卡
; 直到计时器归零。

指向 RAM 的指针

tile 或 sprite 指针指 RAM 地址($00-$7F)不指 ROM,游戏把 RAM 不停变的字节当 tile 解:

; 例子:Level ID $03 水
; tile 指针:$A46B(3-3,合法)
; sprite 指针:$009D(指零页 RAM!)
;
; 零页 RAM 装游戏寄存器、
; 马里奥位置、计数器状态……
; 游戏解压当 sprite 序列,
; 结果关里敌人其实是寄存器值。

零页一变(马里奥动,计时器走),关里"sprite"跟着变。所以有些 glitch 世界敌人闪烁变形。

Level ID $03 水,sprite 指针 $009D 指 RAM,没法玩

Level ID $36:空关(地上)

Level ID $36 配地上:

  • tile 指针:$AC35(1-2)
  • sprite 指针:$A0D8(1-2)

结果:空。游戏载关但 RGMechEx 目录标"无此关"。tile 可能合法但 sprite 指向产出空关或坏关的地方。

Level ID $1D(城堡):crash 王

Level ID $1D 配城堡:

  • tile 指针:$A210(4-4)
  • sprite 指针:$7EA0(RAM!)

sprite 指针在 RAM = 未定义 sprite。游戏想在首行 tile 显示 Spiny 球或飞弹发射器。直接 crash。

; $1D 为什么 crash:
; sprite 指针指 RAM,
; 游戏把时刻变的字节
; 当"刷怪"指令解
; 结果:
; - 刷出不存在的物件(未定义值)
; - sprite NES 想显示非法 tile
; - PPU crash
; - 整机冻住

256 个 glitch 世界全目录

RGMechEx 写脚本把全部关卡地图生成出来,4 区域类型 × 128 ID。

世界计数 8 位(0-255)。世界 1-8 正经。剩 248 个潜在 glitch 世界。每个 glitch 世界是该世界首关,Level ID 经 WorldIndexTable 溢出机制算。

glitch 世界表,248 个坏世界,前 68 首关可进

128 可能 ID 只有 68 个是某世界"首关"(经 glitch world 号可进)。60 个是第 2 关以后或进不去。

类型 独立可玩 ID crash 的 ID 空 ID
水(0) 约 20 约 60 约 48
地上(1) 约 30 约 55 约 43
地下(2) 约 15 约 65 约 48
城堡(3) 约 25 约 58 约 45

很多 ID 通向同关,因为指针掉 ROM 同地址。Level ID $28(地上)tile 指针 $A7CD(2-1),出现在 38 个不同 glitch 世界,因为 sprite 指针 $9F51 掉 ROM 当 padding/声音数据复用的区,好多 ID 共用。

Level ID $28(地上),2-1 tile 加正常 sprite,38 个 glitch 世界

Super Mario Bros. Glitch Levels Explained,第 3 期

真独特的 6 个 glitch 关

19 个可进 glitch ID 里只有 6 个载入不立刻 crash:

世界 Level ID 说明
E-1 (224) $50 深坑上一块 ? 砖。马里奥出生即死。
W $57 马里奥出生卡住,动不了。
42 (133) $50 云隧道,走远卡死马里奥。
62 (131, 240) $4D 冻住的城堡:马里奥出生在顶,下不来,卡住。
127 $4B 地下隧道,走远 crash。
137 $4B 触发 cutscene 自动卷轴。马里奥遇到单个 brick block 永远挡住。

Level ID $50(云隧道),glitch 世界 42-1 和 E-1 Level ID $4D(城堡),world 62-1,马里奥出生卡住 Level ID $4B(隧道),world 127-1,走远 crash

248 个 glitch 世界产出真新东西的只有六个。剩下是错区域类型的正常关,或黑屏。

关卡格式细看

看精确数据格式,理解 glitch 关为什么有的立得住有的不。

关头:2 字节 6 属性

每关开头 2 字节头定 6 属性:

; 字节 0:计时器 + Y 起点 + 修饰
;   位 7-6:计时器(00=不变,01=200,10=300,11=400)
;   位 5-3:马里奥 Y 起点(111/110 = 自动走)
;   位 2-0:关卡类型修饰
;              000=默认,001=波浪,010=砖墙,
;              011=水底,100=夜,101=雪,
;              110=雪夜,111=灰夜

; 字节 1:平台 + 背景 + 地板 pattern
;   位 7-6:特殊平台(00=树,01=蘑菇,
;                                 10=飞弹,11=云)
;   位 5-4:背景(00=无,01=云,
;                           10=山,11=栅栏)
;   位 3-0:初始地板 pattern(0-15)

类型修饰管视觉变体:水关顶部波浪、8-3 砖底、4-3 夜色板、6-2 雪等。

tile 物件:2 字节,Next Screen 旗,3 槽队列

头后面是 tile 物件列表,每物件 2 字节。字节 $FD 标列表尾。

; tile 物件格式(16 位):
; 字节 0:
;   位 7-4:X 位置(列 0-15)
;   位 3-0:Y 位置
;     Y=0-11  :正常 Y 位置
;     Y=12    :特殊物件(坑、桥、绳、? 块)
;     Y=13    :跳屏 / 特殊物件 2
;     Y=14    :换修饰/布景/地板
;     Y=15    :特殊物件 3(城堡、楼梯、大水管)

; 字节 1:
;   位  7   :NEXT SCREEN 旗
;   位 6-4 :物件类型(0-7)
;   位 3-0 :宽/高 / 子类型

"next screen" 旗一起,当前工作列加 1。物件放 16 列之后就靠这个。物件必须按顺序列(左到右),游戏顺序载:

; 载入 routine 每列两阶段:
; 阶段 1:找本列新开始的物件
;            进队列
; 阶段 2:队列每物件画 tile,
;            本列画完的出队

队列正好 3 槽。直接后果:同列开头物件不能超 3 个。队列满第 4 个忽略,永不载。

所以好关卡不堆物件。1-2 天花板 1up 砖加旁边砖拆两物件,就为守 3 上限。

特殊 Y 位置:12、13、14、15

Y=12 物件没 Y 位置(按类型写死):

; Y=12:无 Y 位置物件
;   类型 0:坑(删地面)
;   类型 1:移动平台绳
;   类型 2-4:固定 Y 桥
;   类型 5:水/岩浆坑
;   类型 6-7:? 块排

Y=13 两亚组。字节 1 位 6 = 1:

; Y=13,位6=1:特殊物件
;   0 = L 形管(cutscene),1 = 旗杆,2-3 = 桥/斧/锤(城堡尾)
;   4 = 停屏,5 = 随机敌人,6 = 循环关,7+ = 可能 crash

位6=0,低 5 位编码跳屏(直接跳屏 N,不一个个 next screen 旗走)。

Y=14:同理位6=1 换类型修饰,位6=0 换背景加地板 pattern。

地板 pattern:16 种地面

关卡地面不是一个个物件。SMB1 用地板 pattern,背景 pattern 铺到下次改:

; 地板 pattern(4 位 = 16 种)
;   0 = 全空
;   1 = 2 tile 高地面
;   2 = 1 tile 高地面
;   3 = 地面加底
;   4 = 地面加底 2
;   5 = 地面 1/2 tile
;   6 = 3/4 地面
;   ... 到 15 = 全填(地面加天花板)

坑是物件的原因:特定列覆盖地板 pattern,不用为剩下改 pattern。

256 字节上限和 repeat

每关 tile 数据最多 256 字节。6502 Y 寄存器做 index,8 位。游戏读完没遇 $FD,绕回开头无限重复 256 字节:

; Y index = 8 位 → tile 数据最多 256 字节
; Y 溢出(255 → 0)没遇 $FD → 重复
; sprite 也一样,但管子物件(3 字节)
; 每次载入错开 index 奇偶。

有些 glitch 关靠 repeat 生成"无限"关。

sprite 系统:2 字节加管子过渡

sprite 格式类似,无头,几点不同。字节 $FF 标列表尾。

; sprite 格式(2 字节):
; 字节 0:X 位置(列)
; 字节 1:
;   位 7:NEXT SCREEN 旗
;   位 6-0:sprite 类型
;       含:板栗仔、乌龟、Blooper、
;       飞弹、Lakitu、Spiny、平台、
;       warp 区命令、蛤蟆/公主、
;       敌群刷怪命令

字节 1 最低位是 hard 关 flag:置 1 sprite 只在 ≥5-3 关出现。"hard mode" 就是这么造的。

Y 位置 15 = 跳屏(tile 一样)。Y 位置 14 = 管子过渡(3 字节):

; sprite Y=14:管/藤过渡(3 字节!)
;   字节 0:X 位置
;   字节 1:位 6-0 = Level ID 7 位(去哪)
;   字节 2:位 4-0 = 去第几屏
;            位 7-5 = 这过渡有效世界
;
; 为什么要世界?奖励房多世界复用。
; 例:1-1 奖励房 2-1 和 7-1 也用。
; 这房 3 过渡,一世界一个,马里奥
; 落回对的地方。

sprite 无队列。唯一限制:刷怪区(右屏外刚好)同时载 sprite 不超 4 个。超了忽略。

怎么进 glitch 世界

两条路。

经典:穿墙

穿墙(wall clip)出正常关,走到藏起来的 warp 区。拨 RAM 世界计数,载任意 Level ID。

步骤:

  1. World 1-2:进隐藏尾管
  2. 右墙穿墙
  3. 虚空走到 warp 区
  4. 游戏读成世界

但这路只到一小撮 glitch 世界。

狠的:NES 网球换卡

上面 warm start 节全讲了。总结:网球脚步计数写 RAM 同字节正是 SMB1 世界号,warm start 检测留住这个值。

玩家的:直接改内存看全部

模拟器自己逛全部 glitch,直接改 Level ID:

; FCEUX / Mesen 补丁:
; RAM 地址 $075F = 当前 Level ID
; RAM 地址 $0760 = Area Type(0=水,1=地上,2=地下,3=城堡)

; 例子:地上载 Level 57(0x39)
; 模拟器内存查看写:
; $075F = 0x39
; $0760 = 0x01
; 然后进 warp 管或死一次重来
; → 游戏载 Level ID $39 地上

RGMechEx 把 128 关 × 4 类型全地图放 rgmechex.com。每项 tile 指针、sprite 指针、可视地图。

最离谱的几关

Level ID $1F(水):一关顶 15 个 glitch 世界

tile 指针 $A302(3-4)加 sprite 指针 $02A0,产出 15 个 glitch 世界(D-1、J-1、Y-1、Z-1、55-1、73-1……)。解释:sprite 指针掉 ROM 区数据凑合像合法 sprite,能玩,但 3-4 城堡 tile 配地上 sprite,渲染荒谬。

Level ID $28(地上):38 个 glitch 世界纪录

纪录保持。38 个 glitch 世界入口同关(2-1 tile 加 $9F51 sprite)。因为 sprite 指针 $9F51 掉 ROM 当 padding/声音数据复用区,好多 ID 共用。

Level ID $49(地下):FDS 关

tile 指针 $76AE 加 sprite 指针 $1C9D。tile 指针掉 FDS 版 ROM 预留区。结果:标准卡带没有的 tile。52-1 和 196-1 就是它。

Level ID $00-$02:真奖励关

这些 ID 正经子关卡用:

  • $00:5-2/6-2 水下区(H-1、39-1 用)
  • $01:2-2/7-2 水(Minus World,36-1)
  • $02:8-4 子关(136-1、151-1、215-1 用)

"奖励关"和 glitch 世界区别在 warp 区验当前世界:

; warp 区检查(简化)
; 游戏验目标世界 1-8
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; < 1 → 拒绝
  cmp #9
  bcs InvalidWarp       ; > 8 → 拒绝
  ; 1-8 才合法
  jmp DoWarp

号 > 8 或 0 的 glitch 世界正常管子到不了。要穿墙或换卡。

有些关为什么 crash:跳转表

游戏载 tile 物件拿类型当 jump table index:

; 标准 tile 物件跳转表(类型 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; 类型 0:? 块、隐藏、旗杆……
  .word Obj_Platform      ; 类型 1:特殊平台
  .word Obj_BrickRow      ; 类型 2:砖排
  .word Obj_BlockRow      ; 类型 3:砖块排
  .word Obj_CoinRow       ; 类型 4:金币排
  .word Obj_BrickCol      ; 类型 5:砖列
  .word Obj_BlockCol      ; 类型 6:砖块列
  .word Obj_Pipe          ; 类型 7:水管
  .word Obj_8             ; 类型 8
  .word Obj_9             ; 类型 9
  .word Obj_10            ; 类型 10
  .word Obj_11            ; 类型 11

跳转表:非法物件类型为什么 crash 游戏

物件类型非法(≥12),游戏跳表里没有的指针。4 种结局:

  1. 合法指针,物件正常载
  2. 跳到别的跳转表(重叠),出别的物件。例:类型 12 指到 Y=13 表,出 L 管。
  3. 跳到可执行代码,随机执行(大概率 crash)
  4. 显式占位(NOP),物件啥不干(有些 sprite 这样,原地打转不动的敌人)

glitch 关 ID $58:sprite 指针指非法地址,游戏 crash

glitch 关 ID $50:云隧道,坏数据生成的关

glitch 关 ID $58(crash 隧道):sprite 指针指 NES 无 mapper ROM 不存在的内存区。游戏每帧同位置(0,0)载同一 Koopa 5 次,PPU 塞爆冻住。

; $58 为什么 crash:
; sprite 指针 → 非法地址(标准 NES 空间外)
; → 游戏把不定字节当 sprite 类型读
; → $00 Koopa(无 handler 非法类型)递归调用
; → 6502 栈溢出 → 冻住

管子 warp 悖论

还记 target_world BETWEEN 1 AND 8 检查吗?glitch 世界管子找到了,游戏验目标世界 1-8。glitch 世界号 > 8(36-1、255-1……),warp 失败。

Minus World 没尾也是这个:sprite 没旗杆,管子去不了 world 36。

同列 5 物件 trick

有绕 3 物件上限的 edge case。队列卡住(槽满加下个物件没 next screen 旗),游戏"预处理"当前列循环找带 next screen 旗的物件。每次预处理:

; 列预处理时:
; 1. 队列物件剩余宽度
;    每"假走"一列减一
; 2. 宽度到 0 出队
; 3. 空槽进新同列物件

; 结果:同列最多塞 5 物件。
; 手法:2 个跨屏边界物件
; (占槽 1、2),1 个 X 比前面小的 dummy 物件(卡队列),
; 再下屏 X=0 处 3 物件(一个带 next screen 旗)。

这叫"queue skip",romhacker 拿它做比格式允许更密的关。

版本差异

Famicom Disk System

FDS 版 SMB1 内存 map 不同。全部关卡指针移位,数据一样。变的:glitch 世界 index 全不同:

FDS World 36 → Level ID $09(5-3 水版)
  → 旗杆在!能过关。
  → 接着:$27(7-3 正常)→ $44(4-4 地下)
  → $44 能过,斧子管用,通关!

FDS Minus World 是能打通的"奖励世界",
NES 版不是。

我最爱的 FDS 关:ID $5F,3-3 后半地下窄隧道版(可惜自动卷轴)。

The Lost Levels(日版超级马里奥 2)

Lost Levels 改很多:

  1. tile sprite 同顺序:非法 ID 也同关载,没 Frankenstein 关
  2. 单 16 位指针表,不高低字节分两表
  3. 4 磁盘文件:ROM 按 FDS 切了:
    • 文件 1:世界 1-4
    • 文件 2:世界 5-8
    • 文件 3:世界 9 加声音引擎
    • 文件 4:世界 A-D(指针表完全不同)
  4. 同 Level ID = 4 种可能关,看载哪个文件
  5. 网球 glitch 没了:续关选项(同世界 game over 续)让 warm start 多余,游戏 world > 9 直接重置
  6. 新物件:毒蘑菇、隐形块、隐形火花、倒管子、风,但插旧表中间,和 SMB1 存档不兼容
  7. 4 世界后食人花全红,弹簧只世界 2/B/3/C/7 绿

超级马里奥全明星(SNES)

直接移植同 6502 routine(SNES 兼容模式跑 NES 代码):

  • warp 区修了:Minus World 没了(文字出来前进左管去对的世界)
  • crash:大多数 glitch 关 crash(ID $6A 和 9-1 外)
  • 城堡物件加了,更独特
  • 但:4-2 wrong warp 还在(没修!)

4-2 wrong warp:物件放错的 bug

4-2 两个管子过渡物件:藤蔓(warp 区)和水管(金币房)。藤蔓过渡物件放太早,藤蔓出屏前 3 屏。水管过渡物件放太晚,水管后 1 屏。

; 4-2 过渡时机:
; 过渡物件 1(藤蔓 → warp 区):藤蔓前 3 屏放
; 过渡物件 2(水管 → 金币房):水管后 1 屏放
;
; 正常第一个物件马里奥
; 到水管前失效。但马里奥够快(或
; 用 B+右捷径),藤蔓过渡
; 到水管还活着!
; → 游戏载 warp 区不载金币房。
;
; 物件放藤蔓后水管前,bug 就没了。

循环关

循环(8-4、7-4)怎么跑?关卡有带屏号和 Y 位置的检查点:

; 检查点:{屏号,纵位置}
; 马里奥过检查点高度对 → 关卡继续
; 否则 warp 回 4 屏(64 块)
;
; 无限循环:纵位置 = $F0
; (屏底下)→ 永不过。
;
; 检查点单 flag,world 7 除外
; 三 flag,至少挂 1 个;
;
; warp 回粗暴:tile 数据偏移硬编码值,
; sprite 数据偏移归零。场上敌人
; 瞬间卸载,火棒消失。

改格式,不改代码

SMB1 架构最迷人一课:开发者做出表达力很强的关卡系统,6502 渲染代码一行没动。关卡差异全来自数据(指针、物件、sprite、地板 pattern),不来自代码。

256 glitch 世界存在因为指针表按 128 项 × 4 类型定,游戏从不校验读的值。指针掉 RAM,游戏把马里奥寄存器当 tile 读。指针掉声音数据,游戏把音乐当关卡玩。跳转表溢出,游戏随机执行到 crash。

More Super Mario Bros. Mechanics Explained,第 4 期

能学到什么

  1. tile sprite 分离:两层完全独立,存的顺序不同拼出独特 Frankenstein 关
  2. 压缩 RLE 加物件系统:关卡不是位图,是放好的物件列表,加地板 pattern 铺地
  3. 3 槽队列:硬件(和关卡设计)硬上限
  4. 无条件信指针:信指针信跳转表,出 glitch 可玩关或 crash
  5. 256 字节上限:6502 Y 寄存器上限,走远数据重复
  6. warm start / cold start:"续关"系统开出网球换马里奥换卡门

最美的:全是 40KB 里 6502 代码。代码直连硬件,每次内存访问无条件信。指针烂游戏 crash。crash 我们叫 glitch 世界。

三句话记住

  1. glitch 世界是指针掉错地方,游戏 128 ID × 4 区域类型,真独立关 34 个。world 号一坏(网球或穿墙),游戏载给别关设计的指针,512 种组合结果不可预测。

  2. Minus World 是 warp bug 加数据污染,1-2 左管文字出来前进载 world 36(0x24)。这世界指 Level ID $01(2-2 水),无旗杆关。world 36 无管子过渡,关卡无限循环。缺校验造出 icon。

  3. 网球 → 马里奥,比 OoT → 纸马早 15 年,NES 换卡 RAM 靠电容和 SMB1 warm start / cold start 活下来。网球脚步计数(播脚步声加 RAM 字节)正好落在世界号地址。最高分位保持 0、$A5 字节完好、warm start 判过,巧合全齐才有这个门,只网球行。

Retro Game Mechanics Explained 原视频是真功夫,6502 反汇编细、全部关卡自动地图、换卡 warm start 讲透。没看过系列去看,短,每分钟都密。

地图源码在 rgmechex.com,SMB1 完整反汇编开源仓库一堆。40 年前日本程序员 6502 手写这套关卡系统,无单元测试无 bug 跟踪,我们今天开源码还在学优化技巧。

スーパーマリオ:面形式とポインタと256のグリッチワールド

128面×4 zone種別が40KB ROMにどう収まるか、Minus Worldがなぜあるか、NES Tennisの試合がグリッチワールドをどう読み込むか。

はじめに

スーパーマリオは40キロバイトのROMだ。8ワールド、32面、敵、音楽、パワーアップ、全部そこに収まる。

でもエミュレータを開けて正しいバイトをいじれば、36-1面を読み込める。255-1もだ。クッパのスプライトとどこにも通じない土管だらけの世界に着地もできる。

このグリッチワールドがあるのは単純な理由だ。SMB1の面保存システムは8ビット最適化の驚異で、ゲームが読んじゃいけないところを読まされると魅惑的な結果になる。

Retro Game Mechanics Explainedが4動画シリーズを作った。全部まとめて、史上いちばん売れたゲームの6502コードへの単一潜入にする。

GLITCH OBJECTS、SMB1隠し仕掛けシリーズのRGMechExタイトル

World 9-1、Tennisカート交換で到達できる最初のグリッチワールドのタイトル画面

ウォームスタート:TennisのRAMがSMB1で生き延びる理由

面保存を話す前に、SMB1の起動を理解しなきゃいけない。NES Tennisカート交換グリッチはゲームのウォームスタートやコールドスタート検出システムに完全依存してるからだ。

保存される41バイト

SMB1がコールドスタート(初回通電か電源オフやオン)を検出すると、全RAMを消す。ウォームスタート(リセットボタン、電源切らず)を検出すると、41バイトのメモリ zone を保存する。

; ウォームスタートでRAM保存される41バイト
; アドレス $075F-$0787
;
; $075F : 起動バイト(world - 1)    [1バイト]
; $0760 : ワールド選択フラグ(Bボタン) [1バイト]
; $0761-$0762 : 未使用                    [2バイト]
; $0763-$0768 : タイマー(6桁、3表示) [6バイト]
; $0769-$076E : ルイージコイン                   [6バイト]
; $076F-$0774 : マリオコイン                   [6バイト]
; $0775-$077A : ルイージスコア                   [6バイト]
; $077B-$0780 : マリオスコア                   [6バイト]
; $0781-$0786 : トップスコア(6桁、1隠し) [6バイト]
; $0787 : 魔法バイト $A5                 [1バイト]

この41バイトは1機能のためだけにある。ゲームオーバー後に同じワールドから続けるためだ。6-3で死んだらゲームはワールド6を起動バイトに書く。タイトル画面でAとStart押しながらなら6-1から再開する。

ウォームスタートでRAM保存される41バイト。TOP SCORE、MARIO SCORE、TIMER、WORLD SELECT、CONTINUE WORLD、魔法バイト$A5

ウォームスタートの二重確認

コールドスタート対ウォームスタート、リセット検出図

SMB1起動時は1基準じゃなく2つ確認する。

CheckWarmStart:
  ; 1. $0787の魔法バイト$A5を確認
  lda $0787
  cmp #$A5
  bne ColdStart        ; $A5じゃない → コールドスタート

  ; 2. トップスコア6桁($0781-$0786)を確認
  ;    各桁は0から9でなきゃいけない
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; 桁10以上 → コールドスタート
  inx
  cpx #6
  bne CheckLoop

  ; 両条件通過 → ウォームスタート
  ; RAMは消さない、開始ワールドは保存する
  jmp WarmStartBoot

$A5バイトとトップスコア桁の確認、ウォームスタートの心臓

なぜ二重確認なのか。$A5バイトは偶然ありうるからだ(他ゲームがこの値を残すとか、RAMチップ休止状態のデフォルト)。トップスコア桁が有効(0-9)か確認して、データ一貫性を確かめる。

なぜTennisだけ動くのか

初めてSMB1を入れると(コールドスタート)、ゲームはこうする。

  1. 全RAMを消す → トップスコア0、ワールドバイト0
  2. アドレス$0787に$A5を書く

次に電源切らずTennisに交換する。Tennisはこうだ。

  • 起動時にRAMを掃除しない(NESゲームで掃除するものは少ない)
  • トップスコアバイトに書かない → 0のまま(有効)
  • $A5バイトに触らない → 残る
  • アドレス$075Fをプレイヤー歩数カウンタに使う
; Tennisの歩数増分:
; コート上の各歩でTennisは$075Fバイトを増やす。
; この同じバイトはSMB1で「world number - 1」として使う。
;
; 0歩  → world 0 → SMB1ではworld 1
; 1-7歩 → world 1-7 → 通常ワールド
; 8歩以上 → world 8以上 → グリッチワールド!
;
; カウンタは音楽停止時だけ増える
; (足音は音楽中鳴らない)。

SMB1に戻すとこうなる。

  1. $A5バイトはまだある(Tennisは触ってない)
  2. トップスコア桁はまだ0だ(有効)
  3. ワールドバイトは8以上になってる(Tennis歩数で増加)
  4. SMB1はウォームスタート検出 → 破損ワールドバイトを保存する
  5. AとStart押しながら → world 9-1、world A-1、world 36-1など

なぜTennis前にマリオ起動が要るのか

微妙な点:まずSMB1を起動して、次にTennis、再びSMB1だ。Tennisから直接始めたら、$A5バイトは書かれない(Tennisは$A5を書かない)。ウォームスタート検出は失敗してRAMは消去される。

Tennisの歩数カウンタ:各足音がワールドバイトを増やす

Access Glitch Worlds via NES Tennis、カート交換を解説する動画

SMB1が40KBに面を保存する方法

任天堂R&D4は一見単純な問題を解かなきゃいけなかった。タイルで水平スクロールする面を、敵もアイテムも全部、超厳しいROM予算で表すことだ。

解は、完全独立の2データ層への分離だ。

タイル配置(面の地図)

各面はROM内圧縮タイル構造へのポインタで定義される。圧縮は素朴だが天才的だ。制御バイトの後ろに1-3データバイトが続く。

タイル形式はrun制(RLE風)を使う。

; SMB1タイル形式(簡略)
; 各「命令」は制御バイト:
;
; $00-$7F : タイル1つ置いて1列進む
; $80-$BF : タイルを指定回繰り返す(N = バイト - $80 + 1)
; $C0-$FF : 特殊命令(行末、跳躍、パレット変更)

例:レンガ3つ連続を描く:
  $82 $01    ; タイル$01(brick)を3回繰り返す

各面は16列13行のタイルを持つ(13×16 = 208可視タイル)。でも圧縮形式でずっと小さくできる。たとえば空と空列は場所をほとんど取らない。

6502レンダー loop:

; タイル伸長 - 主 loop
; 入力:$XXのtile_dataポインタ
; 出力:PPU RAM内の面タイルマップ

DecompressTile:
  lda (tile_ptr),y      ; 制御バイト読む
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F : 単一タイル
  cmp #$C0
  bcc RunLength         ; $80-$BF : ランレングス
  jmp SpecialCommand    ; $C0-$FF : 特殊命令

SingleTile:
  sta PPU_DATA          ; タイル直接書き込み
  jmp Next

RunLength:
  sec
  sbc #$7E              ; N = control - $7E
  tax
  lda (tile_ptr),y      ; 繰り返すタイル読む
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

スプライト配置(敵と物)

並行して、敵と物(?ブロック、土管、クリボー、ノコノコ)は完全別の構造に保存される。各出現は2バイトで定義される。

; SMB1スプライト形式
; バイト0:X位置(列単位)
; バイト1:スプライト種別とYページbit
; Yは並び内indexから導く

スプライト並び:
  $01 $4B    ; 1列目にクリボー
  $09 $4B    ; 9列目にクリボー
  $10 $61    ; 16列目に?ブロック(コイン入り)
  $15 $54    ; 21列目に緑ノコノコ
  $FF        ; 並び終わり

各面は最大5スプライトページを参照できる(正確には16列の5「screen」)。実際は大半の面が2、3しか使わない。

ポインタテーブル

設計の天才はポインタテーブルだ。各面はROMアドレスペアとして保存される。

// World Map内部構造(簡略)
struct LevelPointer {
    uint16_t tile_ptr;   // タイルデータROMアドレス
    uint16_t sprite_ptr; // スプライトデータROMアドレス
};

// AreaTypeごとに4テーブル分離:
// 0 = Water、1 = Overworld、2 = Underground、3 = Castle
LevelPointer level_table[4][128];

テーブルごとに128エントリだ。4 zone種別だ。512組み合わせ可能だが、公式ゲームが使うのは一部だけだ。残りは未初期化RAMかポインタとして解釈されるデータだ。

ゲームが面を読み込むとこうする。

; 面読み込み
; A = AreaType (0-3)、X = LevelID (0-127)

LoadLevel:
  sta AREA_TYPE
  asl                  ; 16ビットテーブル用に*2
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; タイルへのポインタ
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; スプライトへのポインタ
  jsr DecompressTiles

ゲームはポインタを完全信用する。テーブル内アドレスを読んで、そのアドレスにあるものを伸長する。以上だ。

Level ID $06 (Water)、9-1、6-2の水中版

Level IDテーブル:128エントリ可能、34割り当て

タイルとスプライトポインタの違う順序、Frankenstein面の原因

34固有面と7ビットID制

NESのRAMチップ(MB8416A)。カート交換時にデータを保つのはこいつだ

SMB1は32面じゃなく34固有面だ。多くの面は重複だ(5-3は1-3だがBullet Bill付き)。「hard mode」フラグ印付きだ。本当の固有面:

  • 水(Type 0):3面(2-2、7-2、5-2や6-2 bonus zone)
  • 地上(Type 1):22面(2つの雲 bonus 部屋込み)
  • 地下(Type 2):3面(地下 bonus 部屋込み)
  • 城(Type 3):6面
  • 1カットシーン部屋(地下や水面前)
  • 4-2の1ワープ zone

各面は7ビットIDを持つ。下位5bitはサブグループ内番号、上位2bitは zone 種別だ。

; Level IDの7ビット符号化
; ビット6-5:種別(00=Water、01=Overworld、10=Underground、11=Castle)
; ビット4-0:サブグループ内番号
;
; Water ID      : $00-$02  (種別00、番号0-2)
; Overworld ID  : $20-$35  (種別01、番号0-21)
; Underground ID: $40-$42  (種別10、番号0-2)
; Castle ID     : $60-$65  (種別11、番号0-5)
;
; ID $25 = %0100101 → 種別01(Overworld)、番号5 → 1-1
; ID $23 = %0100011 → 種別01(Overworld)、番号3 → 6-2

128 ID可能($00-$7F)で、本物面に割り当ては34だけだ。未使用IDはどこでも指す。

ポインタテーブル:2リスト、2順序

タイルとスプライトのポインタは同じ順序で保存されてない。コードは2つの分離16ビットリストを使う(2つの別テーブルにhigh byteとlow byte)。

スプライトポインタ順序:
  Index 0-5   : Castle(6面)
  Index 6-27  : Overworld(22面)
  Index 28-30 : Underground(3面)
  Index 31-33 : Water(3面)

タイルポインタ順序:
  Index 0-2   : Water(3面)
  Index 3-24  : Overworld(22面)
  Index 25-27 : Underground(3面)
  Index 28-33 : Castle(6面)

なぜ順序が違うのか。技術理由はない。開発中にデータがこう組まれたってだけだろう。でも魅惑的な帰結を生む。無効面IDのとき、タイルとスプライトのポインタは違う面を読み込んで、Frankenstein面を作る。

2リスト間を動くため、ゲームは小さなオフセットテーブルを使う(目次みたいなもの)。

; 種別ごとのオフセットテーブル(Water、Overworld、Underground、Castle)
; 各エントリ = 対応リスト内の開始index

SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; Water=31、Overworld=6、Underground=28、Castle=0

TileOffsetTable:
  .byte $00, $03, $19, $1C    ; Water=0、Overworld=3、Underground=25、Castle=28

6-2(ID $23、Overworld番号3)読み込みはこうだ。

; 1. 種別 = 01(Overworld)→ オフセットテーブル内index = 1
; 2. スプライト offset = SpriteOffsetTable[1] = 6
;    最終Index = 6 + 3(面番号) = 9 → 10番目スプライトポインタ
; 3. タイル offset = TileOffsetTable[1] = 3
;    最終Index = 3 + 3 = 6 → 7番目タイルポインタ
; 4. 結果:タイルポインタ$A619 + スプライトポインタ$9ED0 = 6-2 ✓

じゃあ$43みたいな無効ID(存在しないUnderground番号3)では何が起きる?

; ID $43、種別 = 10(Underground)、番号 = 3
; スプライト offset = SpriteOffsetTable[2] = $1C = 28
;   Index = 28 + 3 = 31 → 32番目スプライトポインタ = 水 bonus 5-2!
; タイル offset = TileOffsetTable[2] = $19 = 25
;   Index = 25 + 3 = 28 → 29番目タイルポインタ = 1-4(Castle)!
;
; 結果:5-2水 zone のBloopers付き1-4タイルの地下レベル
; 本物のFrankensteinだ。

Level ID $43、Frankenstein面:1-4タイルと水5-2スプライト

Exploring Glitch Level Pointers、オフセットテーブル解説

world index table、world 9 overflow がグリッチ面を作るとき

world index table:なぜworld 9が overflow するのか

各ワールド(1-8)の初面indexを与える8バイトROMテーブルがある。その直後に、遊ぶ順の全36面のLevel ID 36個のテーブルがある。

; WorldIndexTable (8 bytes)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   -> ワールド1は面0から
;   -> ワールド2は面5から
;   -> ワールド8は面33から

; LevelIDTable (36 bytes)
  .byte $25, $28, $29, $26, $24, ... ; 36 Level ID

world 9を読み込もうとすると、ゲームはWorldIndexTableの9バイト目を読む。存在しない。LevelIDTableに1バイト overflow して値$25を読む。次に$25をLevelIDTable内のindexとして使う(37番目エントリ)。今度はSpriteOffsetTableに2バイト overflow して値6を読む。

; World 9 :
;   1. WorldIndexTable[8] (overflow) → LevelIDTable内の$25を読む
;   2. LevelIDTable[37] (overflow) → SpriteOffsetTableの2バイト目 = 6を読む
;   3. ID = 6 → 存在しないWater面番号6
;   4. タイルポインタ = waterポインタ番号6 = 6-2タイル
;   5. スプライトポインタ = index 31+6 = 37 > 33 → 無効ポインタ
;   6. 結果:グリッチスプライト付き水中6-2
;      → world 9-1!

world G(16)では overflow がさらに進んでLevel ID $01に落ちる。1-2前のカットシーン面だ。

; World G (16) :
;   WorldIndexTable[15] → LevelIDTable内の$01を読む
;   LevelIDTable[1] = $29 (1-2カットシーン)
;   → world G-1 = 1-2入口カットシーン

なぜグリッチワールドがあるのか

ゲームは32「正規」面を持つ(8ワールド×4面)。でもポインタテーブルは zone 種別ごとに128エントリだ。面32超のエントリは、そのアドレスにあるROM内容だ。ときどき別面、ときどき音データ、ときどきRAM、ときどき何でもだ。

Level ID $01 Water(Minus World)。タイルポインタ$AE45、スプライトポインタ$A171

Level ID $01とAreaType 0はMinus World

いちばん有名なグリッチワールドだ。AreaType 0(水)のLevel ID $01はこう指す。

  • タイルポインタ:$AE45 → 2-2や7-2の水中 zone
  • スプライトポインタ:$A171 → 2-2や7-2のスプライト

結果:2-2っぽい水面だが、flagpoleがないから無限 loop する。永遠に loop する面だ。

これが36-1面だ($-1ワールドの36-1とも)。

SMB1のウォームスタート確認。Minus Worldを存在させるのはこいつだ

; Minus Worldにflagpoleがない理由:
; 2-2/7-2スプライト($A171)に並び内flagpoleがない
; ゲームはスプライト$FD(flagpole)を探す
; でも見つからない → 無限ループ
;
; ゲームはタイマーゼロまで無限に面生成を続ける

RAMを指すポインタ

タイルポインタやスプライトポインタがROMじゃなくRAM($00-$7F)アドレスを指すと、ゲームは常に変わるRAMをタイルとして解釈しようとする。

; 例:WaterのLevel ID $03
; タイルポインタ:$A46B (3-3 - 有効)
; スプライトポインタ:$009D(ゼロページRAMを指す!)
;
; ゼロページRAMはゲームレジスタ、
; マリオ位置、カウンタ状態を含む。
; ゲームはこれをスプライト並びとして伸長する。
; 結果はレジスタ値である敵の面だ。

ゼロページが変わる(マリオ動く、タイマー回るなど)と、面の「スプライト」も変わる。だから一部グリッチワールドは点滅して常に変形する敵がいる。

Level ID $03 Water。スプライトポインタ$009DはRAMを指す。遊べない面

Level ID $36:空面(Overworld)

OverworldのLevel ID $36:

  • タイルポインタ:$AC35 (1-2)
  • スプライトポインタ:$A0D8 (1-2)

結果:無だ。ゲームは面を読み込むが、RGMechExカタログでは「面なし」印だ。タイル有効でもスプライトが空か動かない面を出すところを指すのかも。

Level ID $1D (Castle):クラッシュ王

CastleのLevel ID $1D:

  • タイルポインタ:$A210 (4-4)
  • スプライトポインタ:$7EA0 (RAM!)

RAM内スプライトポインタは未定義スプライトだ。ゲームはタイル1行目にSpiny ballかBullet Bill blasterを表示しようとする。即クラッシュだ。

; スプライトポインタがRAMを指すと、
; ゲームは常に変わるバイトを
; 「出現」命令として伸長する。結果:
; - 存在しない物の出現(未定義値)
; - NESスプライトが無効タイル表示しようとしてPPUクラッシュ
; - 本体完全フリーズ

カタログ化された256グリッチワールド

RGMechExは全部の面のマップを生成するスクリプトを書いた。4 zone種別、ID 128個ずつだ。

ワールドカウンタは8ビット(0-255)だ。ワールド1-8は正規だ。残り248グリッチワールド候補だ。各グリッチワールドはそのワールドの初面に対応して、Level IDはWorldIndexTable overflow 機構で計算される。

グリッチワールドテーブル。248破損ワールド、到達可能初面68

128 ID可能のうち、**68だけがワールドの「初面」**だ(グリッチワールド番号経由で到達可能)。残り60は2面以降か到達不能だ。

種別 遊べる固有ID クラッシュID 空ID
Water (0) 約20 約60 約48
Overworld (1) 約30 約55 約43
Underground (2) 約15 約65 約48
Castle (3) 約25 約58 約45

同じROMアドレスにポインタが落ちるから、多くのIDが同じ面になる。OverworldのLevel ID $28(タイルポインタ$A7CD (2-1))はたとえば38違うグリッチワールドに現れる。スプライトポインタ$9F51が多くのIDが再利用するパディングや音データ zone を指すからだ。

Level ID $28 (Overworld)のマップ。通常スプライト付き2-1タイル、38グリッチワールド

Super Mario Bros. Glitch Levels Explained、3つ目の動画

本当に固有の6グリッチ面

到達可能な19グリッチ面IDのうち、読み込みで即クラッシュしないのは6つだけだ。

ワールド Level ID 説明
E-1 (224) $50 奈落の上に?ブロック1つ。マリオは即死ぬ。
W $57 マリオは詰まって出現、動けない。
42 (133) $50 雲トンネル。遠く行けばマリオが閉じ込められる。
62 (131, 240) $4D 凍った城:マリオは上に出現、落ちられない → 詰み。
127 $4B 地下トンネルだが、遠く行けばクラッシュする。
137 $4B カットシーン自動スクロールが起動。マリオは唯一のレンガブロックに出会って永遠に詰む。

Level ID $50(雲トンネル)。グリッチワールド42-1とE-1 Level ID $4D(城)。world 62-1、出現で詰まるマリオ Level ID $4B(トンネル)。world 127-1、遠く行けばクラッシュ

248グリッチワールドのうち本当に新しいものを出すのは6つだ。残りは違う zone 種別の通常面か、黒画面だ。

面形式の詳細

面データの正確な形式に進んで、グリッチ面が立つ(立たない)理由を理解しよう。

面ヘッダー:2バイト、6特性

各面は6特性を操る2バイトヘッダーから始まる。

; バイト0:タイマーとY開始とmodifier
;   ビット7-6:タイマー(00=不変、01=200、10=300、11=400)
;   ビット5-3:マリオY開始(111/110は自動歩行)
;   ビット2-0:面種別modifier
;              000=デフォルト、001=波、010=レンガ壁、
;              011=水底、100=夜、101=雪、
;              110=雪夜、111=灰夜

; バイト1:platformと背景と床模様
;   ビット7-6:特殊platform(00=木、01=キノコ、
;                                 10=Bullet Bill、11=雲)
;   ビット5-4:背景(00=なし、01=雲、
;                           10=山、11=柵)
;   ビット3-0:初期床模様(0-15)

種別modifierは視覚変化を操る。水面上部の波、8-3のレンガ背景、4-3の夜パレット、6-2の雪などだ。

タイル物:2バイト、Next Screen Flag、3枠待ち行列

ヘッダーの後にタイル物一覧が来る。各物は2バイトだ。バイト$FDが一覧終わり印だ。

; タイル物形式(16ビット):
; バイト0:
;   ビット7-4:X位置(0-15列)
;   ビット3-0:Y位置
;     Y=0-11  : 通常Y位置
;     Y=12    : 特殊物(穴、橋、ロープ、?ブロック)
;     Y=13    : screen skip / 特殊物2
;     Y=14    : modifier/scenery/floor変更
;     Y=15    : 特殊物3(城、階段、大土管)

; バイト1:
;   ビット7   : NEXT SCREEN FLAG
;   ビット6-4 : 物種別(0-7)
;   ビット3-0 : 幅/高さ / 亜種

「next screen」bitが立てば、作業中列は1増える。最初16列超に物を置ける。物は順に(左から右へ)並べなきゃいけない。ゲームは順に読むからだ。

; 読み込みルーチンは列ごとに2段階:
; 段階1:この列から始まる新物を探して
;            待ち行列(queue)に足す
; 段階2:待ち行列内各物をタイル描画で処理して、
;            この列で終わるものを外す

待ち行列は正確に3枠だ。直接の帰結:同じ列から始まる物は3つまでだ。待ち行列いっぱいなら4つ目は無視されて読み込まれない。

だから良い面は物を積みすぎない。1-2の例:天井内1upブロックと隣レンガの列は、3上限を守るため2つの別物に分けられる。

特殊Y位置:12、13、14、15

Y=12のとき、物にY位置はない(種別でハードコード)。

; Y=12 : Y位置なし物
;   種別0 : 穴(床消す)
;   種別1 : 動くplatformのロープ
;   種別2-4 : Y固定の橋
;   種別5 : 水や溶岩付き穴
;   種別6-7 : ?ブロック列

Y=13のとき、2亜群だ。バイト1のbit6が1ならこうだ。

; Y=13、bit6=1 : 特殊物
;   0 = L土管(カットシーン)、1 = flagpole、2-3 = 橋や斧やハンマー(城終わり)
;   4 = 画面停止、5 = ランダム敵、6 = loop面、7以上 = クラッシュ可能

bit6=0なら、下位5bitはscreen skipを符号化する(next screen flagを1つずつ通さず直接N画面へ跳ぶ)。

Y=14のとき:bit6=1で種別変更、bit6=0で背景と床模様変更と同じ仕組みだ。

床模様:16床模様

面の床は個別物で作らない。SMB1は床模様を使う。次変更まで全列に適用される背景模様だ。

; 床模様(4ビット = 16可能)
;   0 = 完全空
;   1 = 2タイル高さ床
;   2 = 1タイル高さ床
;   3 = 床とbottom
;   4 = 床とbottom 2
;   5 = 床1/2タイル
;   6 = 3/4床
;   ... 15 = 完全充填(床と天井)まで

だから穴は物だ。残り全部変えず特定列で床模様を上書きする。

256バイト上限と繰り返し

面のタイルデータ全部は最大256バイトに収まる。6502のYレジスタをindexに使ってて、8ビットだ。$FDバイトなしにデータ終わりに着いたら、ゲームは頭へ** loop **して256バイトを無限繰り返す。

; Index Y = 8ビット → タイルデータ最大256バイト
; Y overflow(255 → 0)で$FDなし → 繰り返し
; スプライトも同じだが、土管物(3バイト)が
; 読み込みごとにindex偶奇をずらす。

一部グリッチ面はこの繰り返しを使って「無限」に続く面を作る。

スプライト制:2バイトと土管遷移

スプライトは似た形式に従う。ヘッダーなしでいくつか違う点付きだ。バイト$FFが一覧終わり印だ。

; スプライト形式(2バイト):
; バイト0:X位置(列)
; バイト1:
;   ビット7:NEXT SCREEN FLAG
;   ビット6-0:スプライト種別
;       一部種別はこう:クリボー、ノコノコ、Blooper、
;       Bullet Bill、ジュゲム、トゲゾー、platform、
;       ワープ zone 命令、キノピオや姫、
;       敵群出現命令

バイト1の下位bitはhard level flagだ。1ならスプライトは5-3以降の面にしか現れない。「hard mode」面はこう作られる。

Y位置15はscreen skip(タイルと同じ)だ。Y位置14は土管遷移(3バイト)だ。

; スプライトY=14 : 土管やツル遷移(3バイト!)
;   バイト0 : X位置
;   バイト1 : ビット6-0 = 7ビットLevel ID(目的地)
;   バイト2 : ビット4-0 = 目的地screen
;            ビット7-5 = この遷移が有効なworld
;
; なぜworldか。 bonus 部屋はワールド間で再利用されるからだ。
; 例:1-1 bonus 部屋は2-1と7-1でも使う。
; この部屋はワールドごとに3遷移あって、マリオが
; 正しい場所に戻るようにする。

スプライトに待ち行列制はない。唯一の上限は、出現 zone(画面右すぐ外)に同時読み込みできるスプライトは4つまでだ。超えたらスプライトは無視される。

グリッチワールドへの行き方

主な方法は2つだ。

古典法:壁抜け

壁抜け(壁通り抜け)で通常面を出て、隠し warzone まで歩く。RAM経由でワールドカウンタを操作して、任意Level IDを読み込める。

手順:

  1. World 1-2:隠し終わり土管に入る
  2. 右壁に壁抜けする
  3. 虚空をワープ zone まで歩く
  4. ゲームが値をワールドとして解釈する

でもこの方法ではグリッチワールドの一部にしか行けない。

極限法:NES Tennisカート交換

上で「ウォームスタート」節に完全詳細あり。要約:Tennisの歩数カウンタはSMB1の開始ワールドと同じRAMバイトに書く。ウォームスタート検出がこの値を保存する。

いじり屋の隅:全部探るコード

エミュレータで全部自分で探りたければ、Level IDを直接いじれる。

; FCEUX / Mesen用パッチ:
; RAMアドレス$075F = 現Level ID
; RAMアドレス$0760 = Area Type (0=Water、1=Overworld、2=Underground、3=Castle)

; 例:Overworldで面57 (0x39)を読み込む
; エミュレータでメモリトレーサを開いて書く:
; $075F = 0x39
; $0760 = 0x01
; 次にワープ土管に入るか死んで再開する
; → OverworldでID $39面を読み込む

RGMechExは128面×4種別の完全一覧をrgmechex.comで公開した。自動生成マップ付きだ。各エントリはタイルポインタ、スプライトポインタ、面の視覚マップを示す。

いちばん変な面

Level ID $1F (Water):1つで15グリッチワールド

タイルポインタ$A302 (3-4)とスプライトポインタ$02A0の組み合わせで15違うグリッチワールドが出る(D-1、J-1、Y-1、Z-1、55-1、73-1など)。説明:スプライトポインタは有効スプライトに十分近いROM zone を指して遊べる結果を出す。でも城3-4タイルと地上スプライトの組み合わせで不条理な描画になる。

Level ID $28 (Overworld):38グリッチワールドで記録

絶対記録だ。38グリッチワールドエントリが同じ面を指す(2-1タイルと$9F51スプライト)。なぜか。スプライトポインタ$9F51が多くのIDが再利用するパディングや音データ zone に落ちるからだ。

Level ID $49 (Underground):FDS面

タイルポインタ$76AEとスプライトポインタ$1C9D。タイルポインタはファミコンディスクシステム版用に予約されたROM zone を指す。結果:標準カートリッジにないタイルの面だ。52-1と196-1面を出す面だ。

Level ID $00-$02:本物 bonus 面

このIDはゲーム正規サブ面が使う。

  • $00:5-2や6-2の水中 zone(H-1、39-1が使う)
  • $01:2-2や7-2の水(Minus World、36-1)
  • $02:8-4のサブ面(136-1、151-1、215-1)

通常到達 bonus 面とグリッチワールドの違いは、ワープ zone が現ワールドを確認することだ。

; ワープ zone 確認(簡略)
; ゲームが対象ワールド1から8か確認する
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; 1未満 → 拒否
  cmp #9
  bcs InvalidWarp       ; 8超 → 拒否
  ; 1から8の有効worldのみ
  jmp DoWarp

8超や0のグリッチワールドには通常土管で到達できない。壁抜けかカート交換が要る。

なぜ一部面がクラッシュするか:ジャンプテーブル

ゲームがタイル物を読み込むと、種別をジャンプテーブルのindexとして使う。

; 標準タイル物のジャンプテーブル(種別0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; 種別0:?ブロック、隠し、flagpoleなど
  .word Obj_Platform      ; 種別1:特殊platform
  .word Obj_BrickRow      ; 種別2:レンガ列
  .word Obj_BlockRow      ; 種別3:ブロック列
  .word Obj_CoinRow       ; 種別4:コイン列
  .word Obj_BrickCol      ; 種別5:レンガ柱
  .word Obj_BlockCol      ; 種別6:ブロック柱
  .word Obj_Pipe          ; 種別7:土管
  .word Obj_8             ; 種別8
  .word Obj_9             ; 種別9
  .word Obj_10            ; 種別10
  .word Obj_11            ; 種別11

ジャンプテーブル:なぜ無効物種別がゲームをクラッシュさせるか

物種別が無効(12以上)だと、ゲームはこのテーブルにないポインタへ跳ぶ。4結果可能だ。

  1. 有効ポインタ → 物は普通に読み込まれる
  2. 別ジャンプテーブルへのポインタ(重なり)→ 違う物が現れる。例:種別12はY=13テーブルを指してL土管になる。
  3. 実行可能物へのポインタ → ランダムコード実行(クラッシュ濃厚)
  4. 明示 placeholder(NOP) → 物は何もしない(一部スプライトはこうで、動かず飛ぶ敵を出す)

グリッチ面ID $58:スプライトポインタが無効アドレスを指してゲームがクラッシュ

グリッチ面ID $50:雲トンネル、破損データ生成の面

グリッチ面ID $58(クラッシュするトンネル):スプライトポインタはmapper ROMなしNESに存在しないメモリ領域を指す。ゲームは同じKoopaを1フレーム5回位置(0,0)に読み込もうとして、PPU飽和でフリーズを起こす。

; なぜID $58がクラッシュするか:
; スプライトポインタ → 無効アドレス(標準NES空間外)
; → ゲームは不定バイトをスプライト種別として読む
; → 無効Koopa $00(handlerなし)が再帰呼び出し loop
; → 6502スタック溢れ → フリーズ

土管ワープの逆説

target_world BETWEEN 1 AND 8確認を思い出して。グリッチワールドで土管を見つけても、ゲームは目的地ワールドが1から8か確認する。グリッチワールドは8超の番号(36-1、255-1など)だからワープは失敗する。

Minus Worldに終わりがないのもそのためだ。スプライトにflagpoleがなくて、土管はどこにも通じない。

1列5物の技

1列あたり3物上限を超える edge case がある。待ち行列が詰まる(枠いっぱいと次物にnext screen flagなし)と、ゲームはnext screen flag付き物を見つけるまで現列を loop 「前処理」する。各前処理中にこうだ。

; 列前処理中:
; 1. 待ち行列内物は残り幅が
;    「偽前進」ごとに減る
; 2. 物が幅0に達したら待ち行列を出る
; 3. 空いた枠は新物で埋められる
;    同じ列に追加される

; 結果:同じ列で最大5物が処理されうる。
; 技術:screen境界をまたぐ物2つを置く
; (枠1と2)、前物よりX小さい dummy 物1つ(待ち行列詰まらせる)、
; 次画面X=0に物3つ(1つはnext screen flag付き)。

これを「queue skip」って呼んで、通常形式が許すより密な面を作る一部romhackerが使う。

版間の違い

ファミコンディスクシステム

SMB1のFDS版はメモリマップが違う。全ての面ポインタがずれるけどデータは同じだ。変わるのはグリッチワールドのindexが完全違うことだ。

FDS World 36 → Level ID $09 (5-3の水版)
  → flagpoleある!面を終えられる。
  → 次:$27(通常7-3)→ $44(地下4-4)
  → $44は終えられる → 斧動く → ゲーム終了!

FDSのMinus Worldはだからゲーム完了に通じうる
「bonus world」で、NES版と違う。

私のお気に入りFDS面:ID $5F。低いトンネルの3-3後半の地下版だ(自動スクロールなのが残念)。

The Lost Levels(日本版スーパーマリオ2)

Lost Levelsは多くを変える。

  1. タイルやスプライト同順:Frankenstein面なし(無効IDでもタイルとスプライトは同じ面を読み込む)
  2. 2つの分離high/lowテーブルじゃなく単一16ビットポインタテーブル1つ
  3. 4ディスクファイル:FDS用にROM分割した。
    • ファイル1:worlds 1-4
    • ファイル2:worlds 5-8
    • ファイル3:world 9と音エンジン
    • ファイル4:worlds A-D(完全違うポインタテーブル)
  4. 同じLevel IDは4面可能だ。読み込んだファイルによる
  5. Tennisグリッチなし:continue選択肢(ゲームオーバー後同じワールドから続ける)がウォームスタートを無用にする。ゲームはworld 9超で即リセットする
  6. 新物:毒キノコ、透明ブロック、透明ファイアフラワー、逆さ土管、風。でも既存一覧の真ん中に挿入 → SMB1との後方非互換
  7. world 4以降Piranha Plantはつねに赤、緑バネはworlds 2やBや3やCや7だけ

スーパーマリオコレクション(SNES)

同じ6502ルーチンの直接移植だ(SNESは互換モードでNESコード実行)。

  • ワープ zone 修正:Minus Worldなし(テキスト前に左土管に入れば正しいワールド)
  • クラッシュ:大半グリッチ面がクラッシュする(ID $6Aと9-1除く)
  • 城物追加:より固有描画
  • でも:4-2 wrong warpはまだ動く(直してない!)

4-2 wrong warp:物配置バグ

4-2には土管遷移物が2つある。ツル(warp zone)と土管(coin cash部屋)だ。最初の遷移物(ツルの方)はツルが画面に出るずっと前に置かれる。2つ目(土管の方)は面内遅すぎるところに置かれる。

; 4-2の遷移タイミング:
; 遷移物1(ツル → warp zone):ツルの3画面前に配置
; 遷移物2(土管 → coin cash):土管の1画面後に配置
;
; 普通は最初の物がマリオ土管到着前に無効化される。
; でもマリオが速い(かB+右ブロック近道を使う)と、ツルの
; 遷移は土管触ってもまだ有効だ!
; → ゲームはcoin cashじゃなくwarp zoneを読み込む。
;
; 物がツル直後で土管前に置かれてたら、バグは存在しない。

loop 面

loop(8-4、7-4)はどう動くのか。面はチェックポイントを持つ。画面番号とハードコードY位置付きだ。

; チェックポイント:{screen_number, vertical_position}
; マリオが正しい高さでこのチェックポイントを通る → 面続く
; そうじゃない → 4画面戻しワープ(64ブロック)
;
; 無限ループ作り:vertical_position = $F0
; (画面下の下)→ 検証不能だ。
;
; チェックポイントは単純(フラグ1つ)。world 7除く
; world 7は三つ組(3フラグ、1つ以上失敗が必要)
;
; 戻しワープは荒い:タイルデータ offset はハードコード値、
; スプライトデータ offset は0に戻る。いる敵は
; 即読み込み解除 → firebar消える。

形式を変える、コードじゃない

このアーキテクチャのいちばん魅惑的な教訓の1つは、SMB1開発者が6502レンダーコードに触れずとても表現豊かな面制を作れたことだ。面間変動は全部データから来る(ポインタ、物、スプライト、床模様)。コードからじゃない。

256グリッチワールドがあるのは、ポインタテーブルが128エントリ×4種別に寸法取りされてて、ゲームが読む値を検証しないからだ。ポインタがRAMに落ちると、ゲームはマリオレジスタをタイルとして解釈する。ポインタが音データに落ちると、ゲームは音楽を面設計として遊ぶ。ジャンプテーブルが overflow すると、ゲームはクラッシュまで何でも実行する。

More Super Mario Bros. Mechanics Explained、4つ目の動画

全部から学べること

  1. タイルやスプライト分離:2層の完全独立。違う保存順序が固有Frankenstein面を作る
  2. RLE圧縮と物制:面はビットマップじゃなく配置物一覧だ。床は床模様でね
  3. 3枠待ち行列:ハード(と面設計)の厳密上限だ
  4. ポインタ完全信用:ゲームはポインタとジャンプテーブルを信用する。遊べるグリッチかクラッシュかを出す
  5. 最大256バイト:6502 Yレジスタ上限。遠く行きすぎるとデータ繰り返す
  6. ウォームスタートやコールドスタート: Tennis → Mario への道を開いた「続ける」システムだ

いちばん美しいのは、全部40KBに収まる6502コードってことだ。コードはハードと直接話して、全メモリアクセスを完全信用する。ポインタが腐ってたらゲームはクラッシュする。クラッシュをグリッチワールドって呼ぶんだ。

覚えるべき3つ

  1. グリッチワールドは落ちの悪いポインタだ。ゲームは zone 種別4つにID 128個だが、固有面は34だけだ。ワールド番号が破損すると(Tennisか壁抜けで)、ゲームは別面用のポインタを読み込む。512可能組み合わせが予測不能結果を出す。

  2. Minus Worldはワープバグと破損の組み合わせだ。1-2の左土管は、テキスト前に起動すればworld 36 (0x24)を読み込む。このworldはLevel ID $01(2-2の水)を指す。flagpoleなし面だ。world 36用土管遷移がないから、面は無限 loop する。確認欠落が icon を作る。

  3. Tennis → MarioはOoT → Paper Marioの15年前だ。NESのRAMはコンデンサとSMB1のウォームスタートやコールドスタート制のおかげでカート交換を生き延びる。Tennisの歩数カウンタ(歩音鳴らしてRAMバイト増やす)はワールド番号アドレスにぴったり落ちる。トップスコア桁が0のままで、$A5バイト無事で、ゲームがウォームスタート検出することが要る。Tennisでしか動かなかった完全な偶然の一致だ。

Retro Game Mechanics Explainedの元動画は蟻の仕事だ。6502逆アセンブルへの詳細水準、全自動マップ、カート交換とウォームスタートの解説。シリーズ見てないなら見て。短いし各分が濃い。

面ソースコードはrgmechex.comで出てる。完全SMB1逆アセンブルは多くのリポジトリでオープンソースだ。40年前、日本人プログラマーは単体テストゼロとバグ追跡ゼロでこの面制を6502で書いた。今日もコード開けて最適化技術を学び続けてる。

Super Mario Bros.: 레벨 형식, 포인터, 256개 글리치 월드

128개 레벨과 4종 구역이 40KB ROM에 들어가는 방법, Minus World가 존재하는 이유, NES 테니스 경기가 글리치 월드를 불러오는 방법.

소개

Super Mario Bros.는 40킬로바이트 ROM이에요. 8개 월드와 32개 레벨, 적과 음악, 파워업, 전부 거기에 들어가요.

하지만 에뮬레이터를 열고 올바른 바이트를 만지작거리면 레벨 36-1을 불러올 수 있어요. 또는 255-1. 또는 모든 것이 Bowser 스프라이트와 nowhere로 가는 파이프로 된 월드에 떨어질 수 있어요.

이 글리치 월드들이 존재하는 것은 단순한 이유예요. SMB1 레벨 저장 시스템은 8비트 최적화의 경이이고, 게임이 건드리지 말아야 할 곳을 읽게 강제하면 매혹적인 결과가 나오거든요.

Retro Game Mechanics Explained가 4개 영상으로 다뤘어요. 그 시대를 가장 많이 판 게임의 6502 코드 속으로 한 번에 합치는 다이빙으로 엮을게요.

GLITCH OBJECTS: SMB1 숨은 구조 시리즈 제목

World 9-1: 테니스 카트 교환으로 닿는 첫 글리치 월드 타이틀 화면

웜 스타트: 테니스 RAM이 SMB1에서 살아남는 이유

레벨 저장 이야기를 하기 전에 SMB1이 어떻게 시작하는지 이해해야 해요. NES 테니스 카트 교환 글리치가 전적으로 게임의 웜 스타트와 콜드 스타트 감지 시스템에 기대기 때문이에요.

지켜지는 41바이트

SMB1이 콜드 스타트(첫 전원 켜기 또는 전원 껐다 켜기)를 감지하면 RAM 전체를 지워요. 웜 스타트(리셋 버튼, 전원 끊김 없음)를 감지하면 41바이트 메모리 구역을 지켜요.

; 웜 스타트 때 RAM에 지켜지는 41바이트
; 주소 $075F-$0787
;
; $075F : 시작 바이트 (world - 1)    [1바이트]
; $0760 : 월드 선택 플래그 (B 버튼) [1바이트]
; $0761-$0762 : 쓰지 않음                    [2바이트]
; $0763-$0768 : 타이머 (6자리, 3자리 표시) [6바이트]
; $0769-$076E : Luigi 코인                   [6바이트]
; $076F-$0774 : Mario 코인                   [6바이트]
; $0775-$077A : Luigi 점수                   [6바이트]
; $077B-$0780 : Mario 점수                   [6바이트]
; $0781-$0786 : 최고 점수 (6자리, 1자리 숨김) [6바이트]
; $0787 : 마법 바이트 $A5                 [1바이트]

이 41바이트는 기능 하나에 쓰여요. 게임 오버 뒤 같은 월드에서 계속하기를 허용하는 것. 6-3에서 죽으면 게임이 월드 6을 시작 바이트에 쓰고, 타이틀 화면에서 A + Start를 누르면 6-1부터 다시 시작해요.

웜 스타트 때 지켜지는 41바이트 RAM: TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, 마법 바이트 $A5

웜 스타트 이중 검사

콜드 스타트 대 웜 스타트: 리셋 감지 도식

SMB1이 부팅할 때 기준 하나가 아니라 둘을 확인해요.

CheckWarmStart:
  ; 1. 마법 바이트 $A5를 $0787에서 확인
  lda $0787
  cmp #$A5
  bne ColdStart        ; $A5가 아니면 콜드 스타트

  ; 2. 최고 점수 6자리 ($0781-$0786) 확인
  ;    각 자리가 0과 9 사이여야 해요
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; 자리 >= 10이면 콜드 스타트
  inx
  cpx #6
  bne CheckLoop

  ; 두 조건이 통과하면 웜 스타트
  ; RAM을 지우지 않고 시작 월드를 지켜요
  jmp WarmStartBoot

 $A5 바이트와 최고 점수 자리 검사: 웜 스타트의 심장

왜 이중 검사냐고요? $A5 바이트가 우연히 있을 수 있어서예요 (이 값을 남기는 다른 게임, 또는 RAM 칩 기본 휴지 상태). 최고 점수 자리가 유효한지(0-9) 확인해서 자료 일관성을 보장해요.

테니스가 유일하게 통하는 게임인 이유

SMB1을 처음 넣으면 (콜드 스타트) 게임이요.

  1. RAM 전체를 지워요 → 최고 점수 = 0, 월드 바이트 = 0
  2. $A5를 주소 $0787에 써요

다음에 콘솔을 끄지 않고 테니스로 바꿔요. 테니스는요.

  • 시작할 때 RAM을 청소하지 않아요 (그렇게 하는 NES 게임이 적어요)
  • 최고 점수 바이트에 쓰지 않아요 → 0으로 남아요 (유효)
  • $A5 바이트를 건드리지 않아요 → 그대로 있어요
  • 플레이어 발걸음 카운터에 주소 $075F를 써요
; 테니스 발걸음 증가:
; 코트에서 걸을 때마다 테니스가 $075F 바이트를 증가시켜요.
; 이 바이트는 SMB1의 "world number - 1"로 쓰는 것과 같아요.
;
; 0걸음  → 월드 0 → SMB1 = 월드 1
; 1-7걸음 → 월드 1-7 → 정상 월드
; 8+걸음 → 월드 8+ → 글리치 월드!
;
; 카운터는 음악이 멈출 때만 증가해요
; (음악 중에는 발걸음 소리가 나지 않아요).

SMB1을 도로 넣으면요.

  1. $A5 바이트가 여전히 있어요 (테니스가 건드리지 않았어요)
  2. 최고 점수 자리가 여전히 0이에요 (유효)
  3. 월드 바이트가 이제 8 이상이에요 (테니스 발걸음으로 증가)
  4. SMB1이 웜 스타트를 감지해요 → 망가진 월드 바이트를 지켜요
  5. A + Start 유지 → 월드 9-1, 월드 A-1, 월드 36-1 등.

Mario를 테니스 전에 부팅해야 하는 이유

미묘한 점: 먼저 SMB1을 부팅한 뒤 테니스, 다음 다시 SMB1 순서여야 해요. 테니스로 바로 시작하면 $A5 바이트가 절대 쓰이지 않아요 (테니스는 $A5를 쓰지 않아요). 웜 스타트 감지가 실패하고 RAM이 지워져요.

테니스 발걸음 카운터: 발걸음마다 월드 바이트가 증가해요

NES 테니스로 글리치 월드에 닿기: 카트 교환을 설명하는 영상

SMB1이 레벨을 40KB에 저장하는 방법

Nintendo R&D4는 겉보기에 단순한 문제를 풀어야 했어요. 가로로 스크롤되는 레벨을 타일과 적, 아이템과 함께 빡빡한 ROM 예산에 나타내기.

해법은 완전히 독립적인 두 자료 층 분리예요.

타일 배치 (레벨 지도)

각 레벨은 ROM 속 압축 타일 구조를 가리키는 포인터로 정해져요. 압축은 기초적이지만 genial해요. 제어 바이트 하나 다음 자료 1~3바이트.

타일 형식은 런(RLE 방식) 시스템을 써요.

; SMB1 타일 형식 (단순화)
; 각 "명령"은 제어 바이트예요
;
; $00-$7F : 타일 하나 놓고 1열 전진
; $80-$BF : 타일을 N번 반복 (N = 바이트 - $80 + 1)
; $C0-$FF : 특수 명령 (줄 끝, 점프, 팔레트 바꾸기)

예시: 벽돌 3개를 그리려면:
  $82 $01    ; 타일 $01 (벽돌)을 3번 반복

각 레벨은 16열짜리 줄 13개(13×16 = 208개 보이는 타일)를 담아요. 하지만 압축 형식 덕분에 훨씬 낮게 내려갈 수 있어요. 예를 들어 하늘과 빈 열은 거의 자리를 차지하지 않아요.

6502 렌더 루프:

; 타일 압축 해제 - 주 루프
; 입력: $XX의 tile_data 포인터
; 출력: PPU RAM의 레벨 타일맵

DecompressTile:
  lda (tile_ptr),y      ; 제어 바이트 읽기
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F : 단일 타일
  cmp #$C0
  bcc RunLength         ; $80-$BF : 런렝스
  jmp SpecialCommand    ; $C0-$FF : 특수 명령

SingleTile:
  sta PPU_DATA          ; 타일을 바로 쓰기
  jmp Next

RunLength:
  sec
  sbc #$7E              ; N = control - $7E
  tax
  lda (tile_ptr),y      ; 반복할 타일 읽기
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

스프라이트 배치 (적과 물건)

나란히 적과 물건(물음표 블록, 파이프, 굼바, 엉금엉금)은 완전히 분리된 구조에 저장돼요. 각 스폰은 2바이트로 정해져요.

; SMB1 스프라이트 형식
; 바이트 0: X 위치 (열 단위)
; 바이트 1: 스프라이트 종류 + Y 페이지 비트
; Y는 연속 속 인덱스에서 유도돼요

스프라이트 연속 예시:
  $01 $4B    ; 1열에 굼바
  $09 $4B    ; 9열에 굼바
  $10 $61    ; 16열에 물음표 블록 (코인 들어 있어요)
  $15 $54    ; 21열에 초록 엉금엉금
  $FF        ; 연속 끝

각 레벨은 서로 다른 스프라이트 페이지 최대 5개(사실상 16열 "화면" 5개)를 참조할 수 있지만, 실제로 대부분 레벨은 2~3개만 써요.

포인터 표

설계의 천재성은 포인터 표예요. 각 레벨은 ROM 주소 쌍으로 저장돼요.

// 월드맵 내부 구조 (단순화)
struct LevelPointer {
    uint16_t tile_ptr;   // 타일 자료 ROM 주소
    uint16_t sprite_ptr; // 스프라이트 자료 ROM 주소
};

// AreaType별 4개 분리 표:
// 0 = Water, 1 = Overworld, 2 = Underground, 3 = Castle
LevelPointer level_table[4][128];

표당 128개 항목. 구역 종류 4개. 512개 가능 조합이지만, 공식 게임이 쓰는 것은 일부뿐이에요. 나머지는 초기화되지 않은 RAM이거나 포인터로 해석되는 자료예요.

게임이 레벨을 불러올 때 이렇게 해요.

; 레벨 불러오기
; A = AreaType (0-3), X = LevelID (0-127)

LoadLevel:
  sta AREA_TYPE
  asl                  ; 16비트 표 오프셋으로 *2
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; 타일 포인터
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; 스프라이트 포인터
  jsr DecompressTiles

게임은 포인터를 완전히 신뢰해요. 표에서 주소를 읽고 그 주소에 있는 것을 압축 해제해요. 끝.

레벨 ID $06 (물): 9-1, 6-2의 수중 버전

레벨 ID 표: 128개 가능 항목, 34개 할당

타일과 스프라이트 포인터의 다른 순서: 프랑켄슈타인 레벨의 원인

34개 고유 레벨과 7비트 ID 시스템

NES RAM 칩 (MB8416A): 카트리지를 바꿔도 자료를 지키는 것이 바로 이거예요

SMB1에는 32개 레벨이 아니라 34개 고유 레벨이 있어요. 많은 레벨이 중복(5-3 = 1-3이지만 총알과 함께)이고 "하드 모드" 깃발로 표시돼요. 진짜 고유 레벨:

  • 물 (종류 0): 3개 레벨 (2-2, 7-2, 5-2와 6-2 보너스 구역)
  • 오버월드 (종류 1): 22개 레벨 (2개 구름 보너스 방 포함)
  • 지하 (종류 2): 3개 레벨 (지하 보너스 방 포함)
  • 성 (종류 3): 6개 레벨
  • 컷신 방 1개 (지하와 물 레벨 앞)
  • 4-2 워프존 1개

각 레벨은 7비트 ID가 있어요. 하위 5비트는 하위 그룹 번호, 상위 2비트는 구역 종류.

; 레벨 ID 7비트 인코딩
; 비트 6-5: 종류 (00=Water, 01=Overworld, 10=Underground, 11=Castle)
; 비트 4-0: 하위 그룹 번호
;
; Water ID      : $00-$02  (종류 00, 번호 0-2)
; Overworld ID  : $20-$35  (종류 01, 번호 0-21)
; Underground ID: $40-$42  (종류 10, 번호 0-2)
; Castle ID     : $60-$65  (종류 11, 번호 0-5)
;
; ID $25 = %0100101 → 종류 01 (Overworld), 번호 5 → 1-1
; ID $23 = %0100011 → 종류 01 (Overworld), 번호 3 → 6-2

128개 가능 ID($00-$7F)에 진짜 레벨이 할당된 것은 34개뿐이에요. 쓰지 않는 ID는 어디든 가리켜요.

포인터 표: 두 목록, 두 순서

타일과 스프라이트 포인터는 같은 순서로 저장되지 않아요. 코드는 두 개의 분리 16비트 목록(상위 바이트와 하위 바이트가 두 분리 표)을 써요.

스프라이트 포인터 순서:
  인덱스 0-5   : Castle (6개 레벨)
  인덱스 6-27  : Overworld (22개 레벨)
  인덱스 28-30 : Underground (3개 레벨)
  인덱스 31-33 : Water (3개 레벨)

타일 포인터 순서:
  인덱스 0-2   : Water (3개 레벨)
  인덱스 3-24  : Overworld (22개 레벨)
  인덱스 25-27 : Underground (3개 레벨)
  인덱스 28-33 : Castle (6개 레벨)

왜 순서가 다를까요? 기술적 이유 없어요. 개발 중에 자료가 그렇게 정리됐을 가능성이 커요. 하지만 매혹적인 결과가 있어요. 레벨 ID가 유효하지 않으면 타일과 스프라이트 포인터가 서로 다른 레벨을 불러와서 프랑켄슈타인 레벨을 만들어요.

두 목록 사이를 오가려고 게임은 작은 오프셋 표(목차처럼)를 써요.

; 종류별 오프셋 표 (Water, Overworld, Underground, Castle)
; 각 항목 = 해당 목록 시작 인덱스

SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; Water=31, Overworld=6, Underground=28, Castle=0

TileOffsetTable:
  .byte $00, $03, $19, $1C    ; Water=0, Overworld=3, Underground=25, Castle=28

레벨 6-2(ID $23, Overworld 번호 3)를 불러오려면요.

; 1. 종류 = 01 (Overworld) → 오프셋 표 인덱스 = 1
; 2. 스프라이트 오프셋 = SpriteOffsetTable[1] = 6
;    최종 인덱스 = 6 + 3 (레벨 번호) = 9 → 10번째 스프라이트 포인터
; 3. 타일 오프셋 = TileOffsetTable[1] = 3
;    최종 인덱스 = 3 + 3 = 6 → 7번째 타일 포인터
; 4. 결과: 타일 포인터 $A619 + 스프라이트 포인터 $9ED0 = 6-2 ✓

그럼 $43(존재하지 않는 Underground 번호 3) 같은 유효하지 않은 ID는 어떻게 될까요?

; ID $43, 종류 = 10 (Underground), 번호 = 3
; 스프라이트 오프셋 = SpriteOffsetTable[2] = $1C = 28
;   인덱스 = 28 + 3 = 31 → 32번째 스프라이트 포인터 = 5-2 물 보너스!
; 타일 오프셋 = TileOffsetTable[2] = $19 = 25
;   인덱스 = 25 + 3 = 28 → 29번째 타일 포인터 = 1-4 (Castle)!
;
; 결과: 1-4 타일에 5-2 물 구역 Blooper가 있는 지하 레벨
; 진짜 프랑켄슈타인.

레벨 ID $43: 프랑켄슈타인 레벨. 1-4 타일 + 5-2 물 스프라이트

글리치 레벨 포인터 탐험: 오프셋 표 설명

월드 인덱스 표: 월드 9 오버플로우가 글리치 레벨을 만들 때

월드 인덱스 표: 월드 9가 오버플로우하는 이유

각 월드(1-8)의 첫 레벨 인덱스를 주는 8바이트 ROM 표가 있어요. 바로 뒤에 게임 순서 36개 레벨 ID 표가 있어요.

; WorldIndexTable (8바이트)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   -> 월드 1은 레벨 0에서 시작
;   -> 월드 2는 레벨 5에서 시작
;   -> 월드 8은 레벨 33에서 시작

; LevelIDTable (36바이트)
  .byte $25, $28, $29, $26, $24, ... ; 36개 레벨 ID

월드 9를 불러오려고 하면 게임이 존재하지 않는 WorldIndexTable 9번째 바이트를 읽어요. LevelIDTable로 1바이트 오버플로우해서 값 $25를 읽고, $25를 LevelIDTable 인덱스로 쓰고 (37번째 항목). 다시 2바이트 오버플로우해서 SpriteOffsetTable을 읽고 값 6을 읽어요.

; 월드 9:
;   1. WorldIndexTable[8] (오버플로우) → LevelIDTable의 $25 읽기
;   2. LevelIDTable[37] (오버플로우) → SpriteOffsetTable 2번째 바이트 = 6 읽기
;   3. ID = 6 → 존재하지 않는 Water 레벨 번호 6
;   4. 타일 포인터 = water 포인터 번호 6 = 6-2 타일
;   5. 스프라이트 포인터 = 인덱스 31+6 = 37 > 33 → 유효하지 않은 포인터
;   6. 결과: 글리치된 스프라이트가 있는 물속 6-2
;      → 월드 9-1!

월드 G(16)는 오버플로우가 더 멀리 가서 1-2 앞 컷신 레벨인 레벨 ID $01에 떨어져요.

; 월드 G (16):
;   WorldIndexTable[15] → LevelIDTable의 $01 읽기
;   LevelIDTable[1] = $29 (1-2 컷신)
;   → 월드 G-1 = 1-2 진입 컷신

글리치 월드가 존재하는 이유

게임에는 "정품" 레벨 32개(8월드 × 4레벨)가 있어요. 하지만 포인터 표가 구역 종류별 128개 항목으로 되어 있어요. 레벨 32 너머 항목에는 ROM 그 주소에 있는 것이 들어 있어요. 가끔 다른 레벨, 가끔 소리 자료, 가끔 RAM, 가끔 아무거나.

레벨 ID $01 물 (Minus World): 타일 포인터 $AE45, 스프라이트 포인터 $A171

레벨 ID $01 + AreaType 0 = Minus World

가장 유명한 글리치 월드. AreaType 0(물)의 레벨 ID $01이 가리키는 것은요.

  • 타일 포인터: $AE45 → 2-2와 7-2 수중 구역
  • 스프라이트 포인터: $A171 → 2-2와 7-2 스프라이트

결과: 2-2처럼 보이는 물 레벨이지만, 깃대봉이 없어서 무한히 돌아요. 레벨이 영원히 돌아요.

레벨 36-1이에요 (또는 $-1 월드의 36-1).

SMB1 웜 스타트 검사: Minus World가 존재하게 하는 것이 바로 이거예요

; Minus World에 깃대봉이 없는 이유:
; 2-2와 7-2 스프라이트 ($A171)에 순서에 깃대봉이 없어요
; 게임이 스프라이트 $FD (깃대봉)를 찾지만 절대 찾지 못해요 → 무한 루프
;
; 게임이 타이머가 0이 될 때까지 레벨을 무한히 만들어내요

RAM을 가리키는 포인터들

타일 포인터나 스프라이트 포인터가 ROM이 아니라 RAM 주소($00-$7F)를 가리키면, 게임이 계속 바뀌는 RAM을 타일처럼 해석하려고 해요.

; 예시: 물의 레벨 ID $03
; 타일 포인터: $A46B (3-3 - 유효)
; 스프라이트 포인터: $009D (제로 페이지 RAM을 가리켜요!)
;
; 제로 페이지 RAM에는 게임 레지스터와
; Mario 위치, 카운터 상태가 들어 있어요...
; 게임이 이를 스프라이트 연속으로 압축 해제하고,
; 결과는 레지스터 값인 적들이 있는 레벨이에요.

제로 페이지가 바뀌면 (Mario가 움직이고, 타이머가 돌고 등) 레벨 "스프라이트"도 바뀌어요. 그래서 일부 글리치 월드의 적이 깜빡이고 계속 변하는 거예요.

물의 레벨 ID $03: 스프라이트 포인터 $009D가 RAM을 가리켜요. 못 하는 레벨

레벨 ID $36: 빈 레벨 (오버월드)

오버월드의 레벨 ID $36:

  • 타일 포인터: $AC35 (1-2)
  • 스프라이트 포인터: $A0D8 (1-2)

결과: 없음. 게임이 레벨을 불러오지만 RGMechEx 카탈로그에 "레벨 없음"으로 표시돼요. 타일은 유효할지 몰라도 스프라이트가 비었거나 동작하지 않는 레벨을 만드는 곳을 가리켜요.

레벨 ID $1D (성): 충돌 챔피언

성의 레벨 ID $1D:

  • 타일 포인터: $A210 (4-4)
  • 스프라이트 포인터: $7EA0 (RAM!)

RAM 속 스프라이트 포인터 = 정의되지 않은 스프라이트. 게임이 타일 첫 줄에 Spiny 공이나 총알 발사대를 표시하려고 해요. 바로 죽어요.

; 스프라이트 포인터가 RAM을 가리키면,
; 게임이 계속 바뀌는 바이트를
; "스폰" 지시처럼 압축 해제해요. 결과:
; - 존재하지 않는 물건 나타나기 (정의되지 않은 값)
; - NES 스프라이트가 유효하지 않은 타일을 표시하려고 할 때 PPU 충돌
; - 콘솔 완전 멈춤

카탈로그된 256개 글리치 월드

RGMechEx가 4종 구역과 128개 ID 각각에 대해 모든 레벨 지도를 만드는 스크립트를 썼어요.

월드 카운터는 8비트 (0-255). 월드 1-8이 정품이에요. 글리치 월드 248개 가능해요. 각 글리치 월드는 그 월드 첫 레벨에 해당하고, 레벨 ID는 WorldIndexTable 오버플로우 구조로 계산돼요.

글리치 월드 표: 망가진 월드 248개, 첫 레벨 68개 닿기 가능

128개 가능 ID 중 68개만이 월드 "첫 레벨"(글리치 월드 번호로 닿기 가능)이에요. 나머지 60개는 레벨 2 이상이거나 닿을 수 없어요.

종류 할 수 있는 고유 ID 죽는 ID 빈 ID
물 (0) ~20 ~60 ~48
오버월드 (1) ~30 ~55 ~43
지하 (2) ~15 ~65 ~48
성 (3) ~25 ~58 ~45

포인터가 같은 ROM 주소에 떨어져서 많은 ID가 같은 레벨로 이어져요. 예를 들어 레벨 ID $28 (오버월드). 타일 포인터 $A7CD (2-1). 서로 다른 글리치 월드 38개에 나타나요. 스프라이트 포인터 $9F51이 많은 ID가 재사용하는 패딩과 소리 자료 구역을 가리켜서예요.

레벨 ID $28 (오버월드) 지도: 보통 스프라이트가 있는 2-1 타일, 글리치 월드 38개

Super Mario Bros. 글리치 레벨 설명: 세 번째 영상

진짜 고유한 글리치 레벨 6개

닿을 수 있는 글리치 레벨 ID 19개 중 불러오자마자 죽지 않는 것은 6개뿐이에요.

월드 레벨 ID 설명
E-1 (224) $50 구덩이 위 물음표 블록 하나. Mario가 바로 죽어요.
W $57 Mario가 갇혀서 움직이지 못해요.
42 (133) $50 Mario가 멀리 가면 가두는 구름 터널.
62 (131, 240) $4D 언 성: Mario가 위에 태어나 떨어지지 못해요 → 갇혀요.
127 $4B 지하 터널이지만 너무 멀리 가면 죽어요.
137 $4B 컷신 자동 스크롤을 켜요. Mario가 영원히 막는 벽돌 블록 하나를 만나요.

레벨 ID $50 (구름 터널): 글리치 월드 42-1과 E-1 레벨 ID $4D (성): 월드 62-1, 태어나는 곳에 갇힌 Mario 레벨 ID $4B (터널): 월드 127-1, 너무 멀리 가면 죽어요

248개 글리치 월드 중 진짜 새로운 것을 만드는 것은 6개예요. 나머지는 종류가 잘못된 보통 레벨이나 검은 화면이에요.

레벨 형식 자세히

레벨 자료의 정확한 형식으로 들어가서 글리치 레벨이 왜 버티는지(또는 아닌지) 이해해요.

레벨 헤더: 2바이트, 속성 6개

각 레벨은 6개 속성을 조종하는 2바이트 헤더로 시작해요.

; 바이트 0: 타이머 + Y 시작 + 수식어
;   비트 7-6: 타이머 (00=그대로, 01=200, 10=300, 11=400)
;   비트 5-3: Mario Y 시작 (111/110 = 자동 걷기)
;   비트 2-0: 레벨 종류 수식어
;              000=기본, 001=파도, 010=벽돌 벽,
;              011=물 바닥, 100=밤, 101=눈,
;              110=눈 밤, 111=회색 밤

; 바이트 1: 플랫폼 + 배경 + 바닥 패턴
;   비트 7-6: 특수 플랫폼 (00=나무, 01=버섯,
;                                 10=총알, 11=구름)
;   비트 5-4: 배경 (00=없음, 01=구름,
;                           10=산, 11=울타리)
;   비트 3-0: 처음 바닥 패턴 (0-15)

종류 수식어가 시각 변형을 조종해요. 물 레벨 위 파도, 8-3 벽돌 배경, 4-3 밤 팔레트, 6-2 눈 등.

타일 물건: 2바이트, 다음 화면 깃발, 3슬롯 큐

헤더 다음에 타일 물건 목록이 와요. 물건마다 2바이트. 바이트 $FD가 목록 끝을 표시해요.

; 타일 물건 형식 (16비트):
; 바이트 0:
;   비트 7-4: X 위치 (열 0-15)
;   비트 3-0: Y 위치
;     Y=0-11  : 보통 Y 위치
;     Y=12    : 특수 물건 (구멍, 다리, 밧줄, 물음표 블록)
;     Y=13    : 화면 건너뛰기와 특수 물건 2
;     Y=14    : 수식어와 풍경, 바닥 바꾸기
;     Y=15    : 특수 물건 3 (성, 계단, 큰 파이프)

; 바이트 1:
;   비트  7   : 다음 화면 깃발
;   비트 6-4 : 물건 종류 (0-7)
;   비트 3-0 : 너비와 높이 / 하위 종류

"다음 화면" 비트가 서면 현재 작업 열이 1 증가해요. 처음 16열 너머에 물건을 둘 수 있어요. 물건은 순서대로 (왼쪽에서 오른쪽) 나열되어야 해요. 게임이 순서대로 불러오니까요.

; 불러오기 루틴은 열마다 두 단계를 거쳐요
; 단계 1: 이 열에서 시작하는 새 물건을 찾아서
;            대기열에 넣기
; 단계 2: 대기열의 각 물건을 타일을 그리며 처리하고,
;            이 열에서 끝나는 것들은 빼기

큐는 정확히 3개 슬롯이에요. 직접 결과: 같은 열에서 시작하는 물건이 3개를 넘을 수 없어요. 큐가 차면 4번째 물건은 무시되고 절대 불러와지지 않아요.

그래서 잘 만든 레벨은 물건을 너무 쌓지 않아요. 1-2 예시: 천장의 1up 블록과 옆 벽돌이 든 열은 3개 한도를 지키려고 두 개 분리 물건으로 나눠요.

특수 Y 위치: 12, 13, 14, 15

Y=12면 물건에 Y 위치가 없어요 (종류별로 하드코딩돼요).

; Y=12: Y 위치 없는 물건
;   종류 0: 구멍 (바닥 지우기)
;   종류 1: 움직이는 플랫폼 밧줄
;   종류 2-4: Y 고정 다리
;   종류 5: 물과 용암이 있는 구멍
;   종류 6-7: 물음표 블록 줄

Y=13이면 하위 그룹 둘. 바이트 1의 비트 6이 1이면요.

; Y=13, 비트6=1: 특수 물건
;   0 = L 파이프 (컷신), 1 = 깃대봉, 2-3 = 다리와 도끼, 망치 (성 끝)
;   4 = 화면 멈춤, 5 = 무작위 적, 6 = 레벨 반복, 7+ = 죽을 수 있어요

비트6=0이면 하위 5비트가 화면 건너뛰기(다음 화면 깃발을 하나씩 통하지 않고 화면 N으로 바로 점프)를 인코딩해요.

Y=14면요. 종류 바꾸기는 비트6=1, 배경 + 바닥 패턴 바꾸기는 비트6=0으로 같은 원리예요.

바닥 패턴: 16개 바닥 무늬

레벨 바닥은 개별 물건으로 만들지 않아요. SMB1은 다음 바뀔 때까지 모든 열에 적용되는 배경 무늬 바닥 패턴을 써요.

; 바닥 패턴 (4비트 = 16가지)
;   0 = 완전 빔
;   1 = 2타일 높이 바닥
;   2 = 1타일 높이 바닥
;   3 = 바닥 + 아래
;   4 = 바닥 + 아래 2
;   5 = 바닥 1/2타일
;   6 = 3/4 바닥
;   ... 15까지 = 완전 채움 (바닥 + 천장)

그래서 구멍이 물건인 거예요. 나머지를 위해 패턴을 바꾸지 않고 특정 열에 바닥 패턴을 덮어써요.

256바이트 한도와 반복

레벨 타일 자료 전체가 최대 256바이트에 들어가요. 6502 Y 레지스터를 인덱스로 쓰고 8비트예요. 게임이 바이트 $FD를 찾지 못하고 자료 끝에 이르면 처음으로 돌아가서 256바이트를 무한히 반복해요.

; Y 인덱스 = 8비트 → 타일 자료 최대 256바이트
; Y 오버플로우 (255 → 0)인데 $FD 없음 → 반복
; 스프라이트도 마찬가지지만 파이프 물건 (3바이트)이
; 불러올 때마다 인덱스 홀짝을 바꿔요.

일부 글리치 레벨은 이 반복을 악용해서 "무한히" 이어지는 레벨을 만들어요.

스프라이트 시스템: 2바이트 + 파이프 전이

스프라이트는 비슷한 형식을 따르지만 헤더가 없고 몇 가지 핵심 차이가 있어요. 바이트 $FF가 목록 끝을 표시해요.

; 스프라이트 형식 (2바이트):
; 바이트 0: X 위치 (열)
; 바이트 1:
;   비트 7: 다음 화면 깃발
;   비트 6-0: 스프라이트 종류
;       일부 종류: 굼바, 엉금엉금, Blooper,
;       총알, Lakitu, Spiny, 플랫폼,
;       워프존 명령, 두꺼비와 공주,
;       적 무리 스폰 명령

바이트 1 최하위 비트는 하드 레벨 깃발이에요. 1이면 스프라이트가 레벨 5-3 이상에만 나타나고. "하드 모드" 레벨이 만들어지는 방법이 바로 이거예요.

Y 위치 15 = 화면 건너뛰기 (타일과 같아요). Y 위치 14 = 파이프 전이 (3바이트):

; 스프라이트 Y=14: 파이프와 덩굴 전이 (3바이트!)
;   바이트 0: X 위치
;   바이트 1: 비트 6-0 = 목적 레벨 ID 7비트
;   바이트 2: 비트 4-0 = 목적 화면
;            비트 7-5 = 이 전이가 유효한 월드
;
; 왜 월드냐고요? 보너스 방은 월드 사이 재사용돼요.
; 예시: 1-1 보너스 방은 2-1과 7-1도 써요.
; 이 방에 전이가 3개, 월드별 하나씩 있어서 Mario가
; 올바른 곳에 다시 나타나게 해요.

스프라이트에는 큐 시스템이 없어요. 유일한 한도는 스폰 구역(바로 화면 오른쪽 밖)에 동시에 불러올 수 있는 스프라이트가 4개를 넘을 수 없다는 것. 그 너머는 무시돼요.

글리치 월드에 닿는 방법

주된 방법 둘.

고전 방법: 벽 뚫기

벽 뚫기(벽 통과)가 정상 레벨 밖으로 나가서 숨은 워프존까지 걷게 해요. RAM 속 월드 카운터를 조작해서 어떤 레벨 ID든 불러올 수 있어요.

기법:

  1. 월드 1-2: 숨은 끝 파이프로 가기
  2. 오른쪽 벽에 벽 뚫기
  3. 빈 곳을 걸어서 워프 구역까지
  4. 게임이 값을 월드로 해석해요

하지만 이 방법으로는 글리치 월드의 작은 부분에만 닿을 수 있어요.

극단 방법: NES 테니스 카트 교환

자세한 것은 위 "웜 스타트" 섹션을 보세요. 요약: 테니스 발걸음 카운터가 SMB1 시작 월드와 같은 RAM 바이트에 쓰고, 웜 스타트 감지가 이 값을 지켜요.

만지작러 구역: 모두 탐험하는 코드

에뮬레이터에서 직접 모든 글리치를 탐험하고 싶으면 레벨 ID에 바로 패치할 수 있어요.

; FCEUX와 Mesen용 패치:
; RAM 주소 $075F = 현재 레벨 ID
; RAM 주소 $0760 = 구역 종류 (0=Water, 1=Overworld, 2=Underground, 3=Castle)

; 예시: 오버월드로 레벨 57 (0x39) 불러오기
; 에뮬레이터에서 메모리 추적기를 열고 쓰기:
; $075F = 0x39
; $0760 = 0x01
; 다음 워프 파이프에 들어가거나 죽고 다시 시작
; → 게임이 오버월드로 레벨 ID $39를 불러와요

RGMechEx가 타일 포인터와 스프라이트 포인터, 레벨 시각 지도가 있는 128개 레벨 × 4종 전체 목록을 rgmechex.com에 올렸어요.

가장 wtf한 레벨들

레벨 ID $1F (물): 하나로 15개 글리치 월드

타일 포인터 $A302 (3-4)와 스프라이트 포인터 $02A0 조합이 서로 다른 글리치 월드 15개(D-1, J-1, Y-1, Z-1, 55-1, 73-1...)를 줘요. 설명: 스프라이트 포인터가 할 수 있는 스프라이트에 충분히 가까운 자료 구역을 가리켜서 할 수 있는 결과를 만들지만, 3-4 성 타일과 오버월드 스프라이트 조합이 터무니없는 렌더를 만들어요.

레벨 ID $28 (오버월드): 38개 글리치 월드 = 기록

절대 기록. 글리치 월드 항목 38개가 같은 레벨(2-1 타일 + $9F51 스프라이트)을 가리켜요. 왜요? 스프라이트 포인터 $9F51이 많은 ID가 재사용하는 패딩과 소리 자료 구역에 떨어져서예요.

레벨 ID $49 (지하): FDS 레벨

타일 포인터 $76AE + 스프라이트 포인터 $1C9D. 타일 포인터가 Famicom Disk System 버전용으로 예약된 ROM 구역을 가리켜요. 결과: 표준 카트리지에는 없는 타일이 있는 레벨. 레벨 52-1과 196-1이 나타나는 레벨이에요.

레벨 ID $00-$02: 진짜 보너스 레벨

이 ID들은 게임 정품 하위 레벨이 써요.

  • $00: 5-2와 6-2 수중 구역 (H-1, 39-1이 써요)
  • $01: 2-2와 7-2 물 (Minus World, 36-1)
  • $02: 8-4 하위 레벨 (136-1, 151-1, 215-1이 써요)

보너스로 정상 닿는 레벨과 글리치 월드의 차이는 워프존이 현재 월드를 확인한다는 거예요.

; 워프존 검사 (단순화)
; 게임이 목표 월드가 1과 8 사이인지 확인해요
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; < 1 → 거부
  cmp #9
  bcs InvalidWarp       ; > 8 → 거부
  ; 1과 8 사이 유효 월드만
  jmp DoWarp

번호가 8보다 크거나 0인 글리치 월드에는 보통 파이프로 닿을 수 없어요. 벽 뚫기나 카트 교환이 필요해요.

일부 레벨이 죽는 이유: 점프 표

게임이 타일 물건을 불러올 때 종류를 점프 표 인덱스로 써요.

; 표준 타일 물건 점프 표 (종류 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; 종류 0: 물음표 블록, 숨김, 깃대봉...
  .word Obj_Platform      ; 종류 1: 특수 플랫폼
  .word Obj_BrickRow      ; 종류 2: 벽돌 줄
  .word Obj_BlockRow      ; 종류 3: 블록 줄
  .word Obj_CoinRow       ; 종류 4: 코인 줄
  .word Obj_BrickCol      ; 종류 5: 벽돌 열
  .word Obj_BlockCol      ; 종류 6: 블록 열
  .word Obj_Pipe          ; 종류 7: 파이프
  .word Obj_8             ; 종류 8
  .word Obj_9             ; 종류 9
  .word Obj_10            ; 종류 10
  .word Obj_11            ; 종류 11

점프 표: 유효하지 않은 물건 종류가 게임을 죽이는 이유

물건 종류가 유효하지 않으면 (12 이상) 게임이 이 표에 없는 포인터로 점프해요. 가능한 결과 4가지:

  1. 유효 포인터 → 물건이 정상 불러와져요
  2. 다른 점프 표를 가리키는 포인터 (겹침) → 다른 물건이 나타나요. 예시: 종류 12가 Y=13 표를 가리켜서 L 파이프가 돼요.
  3. 실행 파일을 가리키는 포인터 → 무작위 코드 실행 (죽을 가능성)
  4. 명시 플레이스홀더 (NOP) → 물건이 아무것도 안 해요 (일부 스프라이트가 이렇게 움직이지 않고 날아다니는 적을 만들어요)

글리치 레벨 ID $58: 스프라이트 포인터가 유효하지 않은 주소를 가리켜요. 게임이 죽어요

글리치 레벨 ID $50: 망가진 자료가 만든 구름 터널

글리치 레벨 ID $58 (죽는 터널). 스프라이트 포인터가 매퍼 ROM 없는 NES에 존재하지 않는 메모리 영역을 가리켜요. 게임이 같은 Koopa를 프레임마다 5번 위치 (0,0)에 불러오려고 해서 PPU가 포화되고 멈춰요.

; ID $58이 죽는 이유:
; 스프라이트 포인터 → 유효하지 않은 주소 (표준 NES 공간 밖)
; → 게임이 불확정 바이트를 스프라이트 종류처럼 읽어요
; → 핸들러 없는 Koopa $00 (유효하지 않은 종류)이 재귀 호출에 빠져요
; → 6502 스택 오버플로우 → 멈춤

파이프 워프 역설

target_world BETWEEN 1 AND 8 검사를 기억하세요. 글리치 월드에서 파이프를 찾아도 게임이 목적 월드가 1과 8 사이인지 확인해요. 글리치 월드 번호는 8보다 커요 (36-1, 255-1...). 워프가 실패해요.

Minus World에 끝이 없는 이유도 바로 이거예요. 스프라이트에 깃대봉이 없고, 파이프가 nowhere로 이어져요.

한 열에 물건 5개 트릭

한 열 3개 물건 한도를 넘는 엣지 경우가 있어요. 큐가 막히면 (슬롯 참 + 다음 화면 깃발 없는 다음 물건) 게임이 다음 화면 깃발이 있는 물건을 찾을 때까지 현재 열을 루프로 "미리 처리"해요. 각 미리 처리 동안요.

; 열 미리 처리 동안:
; 1. 대기열 물건들은 남은 너비가
;    열 "가짜 전진"마다 줄어들어요
; 2. 물건 너비가 0이 되면 대기열을 나가요
; 3. 풀린 슬롯을 새 물건이 채울 수 있어요
;    같은 열에 추가돼요

; 결과: 같은 열에 최대 5개 물건을 처리할 수 있어요.
; 기법: 화면 경계를 가로지르는 물건 2개를 두기
; (슬롯 1과 2), 이전 X에 더미 물건 1개 (대기열 막아요),
; 다음 화면 X=0에 물건 3개 (다음 화면 깃발 있는 것 포함).

이른바 "queue skip"이라고 하고, 일부 롬해커가 형식이 보통 허용하는 것보다 빽빽한 레벨을 만들려고 써요.

버전 사이 차이

Famicom Disk System

SMB1 FDS 버전은 메모리 맵이 달라요. 모든 레벨 포인터가 이동하지만 자료는 같아요. 바뀌는 것: 글리치 월드 인덱스가 완전히 달라요.

FDS 월드 36 → 레벨 ID $09 (5-3의 물 버전)
  → 깃대봉이 있어요! 레벨을 마칠 수 있어요.
  → 다음: $27 (정상 7-3) → $44 (4-4 지하)
  → $44을 마칠 수 있어요 → 도끼가 동작해요 → 게임 끝!

그래서 FDS Minus World는 게임 완료로 이어질 수 있는
"보너스 월드"예요. NES 버전과 달라요.

제가 제일 좋아하는 FDS 레벨: ID $5F, 낮은 터널 속 3-3 후반 지하 버전 (자동 스크롤이라 아쉬워요).

The Lost Levels (일본 Super Mario Bros. 2)

Lost Levels가 많은 것을 바꿔요.

  1. 타일과 스프라이트 순서 같음: 프랑켄슈타인 레벨 없어요 (유효하지 않은 ID에도 타일과 스프라이트가 같은 레벨을 불러와요)
  2. 분리 상위와 하위 표 둘 대신 단일 16비트 포인터 표 하나
  3. 디스크 파일 4개: ROM이 FDS용으로 쪼개졌어요.
    • 파일 1: 월드 1-4
    • 파일 2: 월드 5-8
    • 파일 3: 월드 9 + 사운드 엔진
    • 파일 4: 월드 A-D (완전히 다른 포인터 표)
  4. 같은 레벨 ID = 4개 가능 레벨, 불러온 파일에 따라
  5. 글리치 테니스 없어요: 이어하기 옵션(게임 오버 뒤 같은 월드에서 계속)이 웜 스타트를 쓸모없게 하고, 게임이 월드 > 9이면 바로 리셋해요
  6. 새 물건: 독버섯, 투명 블록, 투명 블록 불꽃, 거꾸로 파이프, 바람. 하지만 기존 목록 중간에 끼워서 SMB1 하위 호환 안 돼요
  7. 월드 4 뒤에는 피라냐 식물이 항상 빨강, 초록 스프링보드는 월드 2와 B, 3, C, 7에만

Super Mario All-Stars (SNES)

같은 6502 루틴 직접 이식 (SNES가 호환 모드로 NES 코드를 실행해요).

  • 워프존 수정: Minus World 없어요 (텍스트 전에 왼쪽 파이프에 들어가면 올바른 월드로 가요)
  • 죽음: 대부분 글리치 레벨이 죽어요 (ID $6A와 9-1 제외)
  • 추가 성 물건: 더 고유하게 렌더돼요
  • 하지만: 4-2 wrong warp는 여전히 돼요 (패치 안 됐어요!)

4-2 wrong warp: 물건 배치 버그

4-2에는 파이프 전이 물건 둘이 있어요. 덩굴 (워프존)과 파이프 (코인 방). 첫 전이 물건(덩굴 것)이 덩굴이 화면에 나타나기 훨씬 전에 놓여 있어요. 두 번째(파이프 것)가 레벨에서 너무 늦게 놓여 있어요.

; 4-2 전이 타이밍:
; 전이 물건 1 (덩굴 → 워프존): 덩굴 3화면 전에 놓여 있어요
; 전이 물건 2 (파이프 → 코인 방): 파이프 1화면 뒤에 놓여 있어요
;
; 보통 첫 물건은 Mario가 파이프에 이르기 전에 비활성화돼요.
; 하지만 Mario가 빠르면 (또는 B+오른쪽 블록 지름길을 쓰면)
; 파이프에 닿을 때 덩굴 전이가 여전히 활성 상태예요!
; → 게임이 코인 방 대신 워프존을 불러와요.
;
; 물건이 덩굴 바로 뒤 파이프 앞에 놓였으면 버그가 없었을 거예요.

반복 레벨

반복(8-4, 7-4)은 어떻게 동작하나요? 레벨에 화면 번호와 하드코딩된 Y 위치가 있는 체크포인트가 있어요.

; 체크포인트: {screen_number, vertical_position}
; Mario가 올바른 높이로 이 체크포인트를 지나면 → 레벨 계속
; 아니면 → 4화면 뒤로 워프 (64블록)
;
; 무한 반복 만들려면: vertical_position = $F0
; (화면 아래) → 검증 불가.
;
; 체크포인트는 단순해요 (깃발 하나). 월드 7만
; 삼중항을 써요 (깃발 3개, 최소 1개 실패해야 해요)
;
; 뒤로 워프는 거칠어요. 타일 자료 오프셋이 하드코딩된 값으로,
; 스프라이트 자료 오프셋이 0으로 돌아가요. 있던 적들이
; 바로 내려가요 → 파이어바가 사라져요.

형식이 아니라 코드를 바꾸기

이 아키텍처에서 가장 매혹적인 교훈 중 하나는 SMB1 개발자들이 6502 렌더 코드를 절대 건드리지 않고 아주 표현력 있는 레벨 시스템을 만들었다는 거예요. 레벨 사이 모든 변화가 코드가 아니라 자료에서 와요 (포인터, 물건, 스프라이트, 바닥 패턴).

256개 글리치 월드가 존재하는 것은 포인터 표가 종류별 128개 항목으로 되어 있고, 게임이 읽는 값을 절대 검증하지 않기 때문이에요. 포인터가 RAM에 떨어지면 게임이 Mario 레지스터를 타일처럼 해석해요. 포인터가 소리 자료에 떨어지면 게임이 음악을 레벨 디자인처럼 연주해요. 점프 표가 오버플로우하면 게임이 죽을 때까지 아무거나 실행해요.

More Super Mario Bros. Mechanics Explained: 네 번째 영상

이 모든 것에서 배울 수 있는 것

  1. 타일과 스프라이트 분리: 두 층 완전 독립. 저장 순서가 달라서 고유 프랑켄슈타인 레벨을 만들어요
  2. RLE 압축 + 물건 시스템: 레벨은 비트맵이 아니라 놓인 물건 목록이고, 바닥은 바닥 패턴으로
  3. 3슬롯 큐: 하드웨어 (그리고 레벨 디자인)의 엄격 한도
  4. 포인터 완전 신뢰: 게임이 포인터와 점프 표를 믿어서, 할 수 있는 글리치 아니면 죽음이 나와요
  5. 최대 256바이트: 6502 Y 레지스터 한도. 너무 멀리 가면 자료가 반복돼요
  6. 웜 스타트와 콜드 스타트: "이어하기" 시스템이 테니스 카트 교환에서 Mario로 가는 문을 열었어요

가장 아름다운 것은요. 이 모든 것이 40KB에 들어가는 6502 코드예요. 코드가 하드웨어와 직접 대화하고 매 메모리 접근을 완전히 신뢰해요. 포인터가 썩으면 게임이 죽어요. 그리고 죽음을 글리치 월드라고 불러요.

기억할 3가지

  1. 글리치 월드는 잘못 떨어진 포인터예요: 게임에 종류별 128개 ID와 4종 구역이 있지만 고유 레벨은 34개뿐이에요. 월드 번호가 망가지면 (테니스나 벽 뚫기로) 게임이 다른 레벨용 포인터를 불러오고, 512개 가능 조합이 예측할 수 없는 결과를 만들어요.

  2. Minus World는 워프 버그와 망가짐의 결합이에요: 1-2 왼쪽 파이프가 텍스트 전에 켜지면 월드 36 (0x24)을 불러와요. 이 월드는 레벨 ID $01 (2-2 물)을 가리키고, 깃대봉이 없는 레벨이에요. 월드 36용 파이프 전이가 없어서 레벨이 무한히 돌아요. 검사 부재가 아이콘을 만들어요.

  3. 테니스에서 Mario로, OoT에서 Paper Mario보다 15년 앞서요: NES RAM이 콘덴서와 SMB1 웜 스타트와 콜드 스타트 시스템 덕분에 카트리지 교환을 살아남아요. 테니스 발걸음 카운터(발걸음 소리를 내며 RAM 바이트를 증가)는 월드 번호 주소에 딱 떨어져요. 최고 점수 자리가 0으로 남고, $A5 바이트가 온전하고, 게임이 웜 스타트를 감지해야 해요. 테니스에서만 통했던 완벽한 우연 연속이에요.

Retro Game Mechanics Explained 원본 영상들은 엄청난 세밀 작업이에요. 6502 디스어셈블 세부, 모든 레벨 자동 지도, 카트 교환과 웜 스타트 설명. 시리즈를 안 봤으면 보세요. 짧고 매 분이 밀도 있어요.

지도 소스 코드는 rgmechex.com에 있고, SMB1 완전 디스어셈블은 많은 저장소에 오픈 소스로 있어요. 40년 전 일본 프로그래머들이 단위 테스트 제로와 버그 트래커 제로로 6502에 이 레벨 시스템을 썼고, 오늘날 코드를 열면서 최적화 기법을 계속 배우고 있어요.

Super Mario Bros.: seviye formatı, işaretçiler ve 256 glitch dünyası

128 seviye × 4 bölge türü 40KB ROMa nasıl sığar, Minus World neden vardır ve bir NES Tenis maçı glitch dünyaları nasıl yükleyebilir.

Giriş

Super Mario Bros. 40 kilobayt ROMdur. Sekiz dünya, 32 seviye, düşmanlar, müzik, power-uplar, hepsi oraya sığar.

Ama emülatör açıp doğru baytları kurcalarsan 36-1. seviyeyi yükleyebilirsin. Ya da 255-1i. Ya da her şeyi Bowser spriteları ve hiçbir yere gitmeyen borulardan oluşan bir dünyada bulabilirsin.

Bu glitch dünyaları basit bir nedenle vardır: SMB1in seviye depolama sistemi 8-bit optimizasyon harikasıdır ve oyunu okumaması gereken yeri okumaya zorlayınca sonuçlar büyüleyicidir.

Retro Game Mechanics Explained bunun hakkında 4 videoluk bir seri yaptı: bunları oyunun döneminin en çok satanının 6502 koduna tek bir dalışta derleyeceğiz.

GLITCH OBJECTS: RGMechEx serisinin SMB1 gizli mekanikleri başlığı

World 9-1: Tennis kart takasıyla erişilen ilk glitch dünya

Warm start: Tennis RAMi SMB1de neden hayatta kalır

Seviye depolamadan bahsetmeden önce SMB1in nasıl başladığını anlamak gerekir. Çünkü NES Tennis glitchi tamamen oyunun warm start / cold start tespit sistemine dayanır.

Korunan 41 bayt

SMB1 cold start (ilk çalıştırma ya da güç açma/kapama) tespit edince tüm RAMi siler. Ama warm start (reset düğmesi, güç kesintisi yok) tespit edince 41 baytlık bir bellek bölgesini korur:

; Warm startte RAMde korunan 41 bayt
; Adresler $075F-$0787
;
; $075F : başlatma baytı (world - 1)    [1 bayt]
; $0760 : dünya seçim bayrağı (B düğmesi) [1 bayt]
; $0761-$0762 : kullanılmaz                    [2 bayt]
; $0763-$0768 : zamanlayıcı (6 hane, 3ü gösterilir) [6 bayt]
; $0769-$076E : Luigi jetonları                   [6 bayt]
; $076F-$0774 : Mario jetonları                   [6 bayt]
; $0775-$077A : Luigi skoru                   [6 bayt]
; $077B-$0780 : Mario skoru                   [6 bayt]
; $0781-$0786 : en yüksek skor (6 hane, 1i gizli) [6 bayt]
; $0787 : sihirli bayt $A5                 [1 bayt]

Bu 41 bayt tek bir özelliğe hizmet eder: oyuncunun game overdan sonra aynı dünyada devam etmesine izin vermek. 6-3te ölürsen oyun 6. dünyayı başlatma baytına yazar ve başlık ekranında A + Start basılı tutarsan 6-1den yeniden başlarsın.

Warm startte RAMde korunan 41 bayt: TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD ve sihirli bayt $A5

Çift warm start doğrulaması

Cold start ve warm start: reset tespit şeması

SMB1 başlarken tek kriter değil iki kriter kontrol eder:

CheckWarmStart:
  ; 1. $0787deki sihirli bayt $A5i doğrula
  lda $0787
  cmp #$A5
  bne ColdStart        ; $A5 değilse → cold start

  ; 2. En yüksek skorun 6 hanesini doğrula ($0781-$0786)
  ;    Her hane 0 ile 9 arasında olmalı
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; hane >= 10 → cold start
  inx
  cpx #6
  bne CheckLoop

  ; İki koşul da geçerse → warm start
  ; RAM silinmez, başlangıç dünyası korunur
  jmp WarmBootStart

En yüksek skorun $A5 baytı ve hanelerinin doğrulanması: warm startin kalbi

Neden çift doğrulama? Çünkü $A5 baytı tesadüfen mevcut olabilir (bu değeri bırakan başka bir oyun ya da RAM çipinin varsayılan bekleme durumu). En yüksek skor hanelerinin geçerli (0-9) olduğunu doğrulayarak verilerin tutarlı olduğundan emin olunur.

Tennis neden işe yarayan tek oyundur

SMB1 ilk takıldığında (cold start) oyun:

  1. Tüm RAMi siler → en yüksek skor = 0, dünya baytı = 0
  2. $0787 adresine $A5 yazar

Sonra konsolu kapatmadan Tennise geçilir. Tennis:

  • Başlangıçta RAMi temizlemez (az NES oyunu yapar)
  • En yüksek skor baytlarına yazmaz → 0 kalırlar (geçerli)
  • $A5 baytına dokunmaz → mevcut kalır
  • Oyuncunun adım sayacı için $075F adresini kullanır
; Tennis içindeki footstep artışı:
; Kortta oyuncunun her adımında Tennis $075Fdeki baytı artırır.
; Bu aynı bayt SMB1 tarafından "world number - 1" olarak kullanılır.
;
; 0 adım  → world 0 → SMB1 = world 1
; 1-7 adım → world 1-7 → normal dünyalar
; 8+ adım → world 8+ → glitch dünyaları!
;
; Sayaç yalnızca müzik durunca artar
; (adım sesleri müzik çalarken oynamaz).

SMB1 geri takıldığında:

  1. $A5 baytı hâlâ oradadır (Tennis dokunmadı)
  2. En yüksek skor haneleri hâlâ 0dır (geçerli)
  3. Dünya baytı artık 8+ değerindedir (Tennis adımlarıyla artırıldı)
  4. SMB1 warm start tespit eder → bozuk dünya baytını korur
  5. A + Start basılı tut → world 9-1, world A-1, world 36-1 vs.

Neden önce Mario sonra Tennis başlatılmalıdır

Bir incelik: önce SMB1, sonra Tennis, sonra yeniden SMB1 başlatılmalıdır. Doğrudan Tennis ile başlasaydın $A5 baytı hiç yazılmazdı (Tennis $A5 yazmaz), o yüzden warm start tespiti başarısız olur ve RAM silinirdi.

Tennis adım sayacı: her footstep dünya baytını artırır

Access Glitch Worlds via NES Tennis: kart takasını açıklayan video

SMB1 seviyelerini 40KB içinde nasıl saklar

Nintendo R&D4 görünüşte basit bir sorunu çözmek zorundaydı: karolar, düşmanlar, eşyalarla yatay kayan seviyeleri aşırı sıkı ROM bütçesinde temsil etmek.

Çözüm, tamamen bağımsız iki veri katmanına ayırmaktır:

Kare düzeni (seviyenin haritası)

Her seviye ROMda sıkıştırılmış kare yapısına işaretçiyle tanımlanır. Sıkıştırma ilkeldir ama harikadır: 1-3 veri baytının izlediği "kontrol" baytı.

Kare formatı dizi (RLE benzeri) sistemi kullanır:

; SMB1 kare formatı (basitleştirilmiş)
; Her "komut" bir kontrol baytıdır:
;
; $00-$7F : bir kare koy, 1 sütun ilerle
; $80-$BF : kareyi N kez tekrarla (N = bayt - $80 + 1)
; $C0-$FF : özel komut (satır sonu, atlama, palet değişimi)

Örnek: art arda 3 tuğla çizmek için:
  $82 $01    ; $01 karesini (tuğla) 3 kez tekrarla

Her seviye 16 sütunluk 13 satır kare içerir (13×16 = 208 görünür kare). Ama sıkıştırılmış format çok daha alta inmeye izin verir: örneğin gök ve boş sütunlar neredeyse yer tutmaz.

6502de render döngüsü:

; Kare açma - ana döngü
; Girdi: $XX içindeki tile_data işaretçisi
; Çıktı: PPU RAMinde seviye kare haritası

DecompressTile:
  lda (tile_ptr),y      ; kontrol baytını oku
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F : tek kare
  cmp #$C0
  bcc RunLength         ; $80-$BF : dizi uzunluğu
  jmp SpecialCommand    ; $C0-$FF : özel komut

SingleTile:
  sta PPU_DATA          ; kareyi doğrudan yaz
  jmp Next

RunLength:
  sec
  sbc #$7E              ; N = control - $7E
  tax
  lda (tile_ptr),y      ; tekrarlanacak kareyi oku
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

Sprite düzeni (düşmanlar ve nesneler)

Paralel olarak düşmanlar ve nesneler (? bloklar, borular, goombalar, koopalar) tamamen ayrı bir yapıda saklanır. Her doğma 2 baytla tanımlanır:

; SMB1 sprite formatı
; Bayt 0 : X konumu (sütunlarla)
; Bayt 1 : sprite türü + Y sayfa bitleri
; Y dizideki indeksten türetilir

Sprite dizisi:
  $01 $4B    ; 1. sütunda goomba
  $09 $4B    ; 9. sütunda goomba
  $10 $61    ; 16. sütunda ? blok (jeton içerir)
  $15 $54    ; 21. sütunda yeşil koopa
  $FF        ; dizi sonu

Her seviye 5 farklı sprite sayfasına başvurabilir (aslında 16 sütunluk 5 "ekran") ama pratikte çoğu seviye yalnızca 2-3 kullanır.

İşaretçi tablosu

Tasarımın dehası işaretçi tablosudur. Her seviye ROM adresi çifti olarak saklanır:

// World Map iç yapısı (basitleştirilmiş)
struct LevelPointer {
    uint16_t tile_ptr;   // Kare verilerinin ROM adresi
    uint16_t sprite_ptr; // Sprite verilerinin ROM adresi
};

// Bölge türü başına 4 ayrı tablo:
// 0 = Water, 1 = Overworld, 2 = Underground, 3 = Castle
LevelPointer level_table[4][128];

Tablo başına 128 girdi. 4 bölge türü. 512 olası kombinasyon ama resmi oyunda yalnızca bir kısmı kullanılır. Geri kalan başlatılmamış RAM ya da işaretçi olarak yorumlanan verilerdir.

Oyun bir seviye yüklerken şunu yapar:

; Seviye yükleme
; A = AreaType (0-3), X = LevelID (0-127)

LoadLevel:
  sta AREA_TYPE
  asl                  ; 16-bit tabloda ofset için *2
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; karelere işaretçi
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; spritelara işaretçi
  jsr DecompressTiles

Oyun işaretçilere tam güvenir: tablodaki adresi okur ve o adresteki şeyi açar, nokta.

Level ID $06 (Water): 9-1, 6-2nin su altı sürümü

Level ID tablosu: 128 olası girdi, 34 atanmış

Kare ve sprite işaretçilerinin farklı sırası: Frankenstein seviyelerin nedeni

34 benzersiz seviye ve 7-bit ID sistemi

NES RAM çipi (MB8416A): kartuş takasında verileri koruyan odur

SMB1de 32 seviye değil 34 benzersiz seviye vardır. Birçok seviye kopyadır (5-3 = 1-3 ama Bullet Billli), "hard mode" bayrağıyla işaretli. Gerçek benzersiz seviyeler:

  • Su (Tür 0): 3 seviye (2-2, 7-2, bonus bölge 5-2/6-2)
  • Overworld (Tür 1): 22 seviye (2 bonus bulut odası dahil)
  • Underground (Tür 2): 3 seviye (yer altı bonus odaları dahil)
  • Castle (Tür 3): 6 seviye
  • + 1 ara sahne odası (yer altı/su seviyelerinden önce)
  • + 1 warp zone 4-2

Her seviyenin 7 bitlik IDsi vardır. Düşük ağırlıklı 5 bit = alt grup numarası, yüksek ağırlıklı 2 bit = bölge türü:

; Level ID 7-bit kodlaması
; Bit 6-5 : Tür (00=Water, 01=Overworld, 10=Underground, 11=Castle)
; Bit 4-0 : Alt grup numarası
;
; Water IDler      : $00-$02  (türler 00, numaralar 0-2)
; Overworld IDler  : $20-$35  (türler 01, numaralar 0-21)
; Underground IDler: $40-$42  (türler 10, numaralar 0-2)
; Castle IDler     : $60-$65  (türler 11, numaralar 0-5)
;
; ID $25 = %0100101 → tür 01 (Overworld), numara 5 → 1-1
; ID $23 = %0100011 → tür 01 (Overworld), numara 3 → 6-2

128 olası ID ($00-$7F), yalnızca 34ü gerçek seviyelere atanmış. Kullanılmayan IDler her şeye işaret eder.

İşaretçi tabloları: iki liste, iki sıra

Kare ve sprite işaretçileri aynı sırada saklanmaz. Kod iki ayrı 16-bit liste kullanır (iki ayrı tabloda high byte / low byte):

Sprite işaretçilerinin sırası:
  İndeks 0-5   : Castle (6 seviye)
  İndeks 6-27  : Overworld (22 seviye)
  İndeks 28-30 : Underground (3 seviye)
  İndeks 31-33 : Water (3 seviye)

Kare işaretçilerinin sırası:
  İndeks 0-2   : Water (3 seviye)
  İndeks 3-24  : Overworld (22 seviye)
  İndeks 25-27 : Underground (3 seviye)
  İndeks 28-33 : Castle (6 seviye)

Neden farklı sıralar? Teknik neden yok: veriler geliştirme sırasında muhtemelen böyle organize edildi. Ama büyüleyici bir sonucu vardır: seviye IDsi geçersiz olunca kare ve sprite işaretçileri farklı seviyeleri yükler, Frankenstein seviyeler yaratır.

Bu iki liste arasında gezinmek için oyun küçük ofset tabloları kullanır (içindekiler gibi):

; Türe göre ofset tabloları (Water, Overworld, Underground, Castle)
; Her girdi = ilgili listedeki başlangıç indeksi

SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; Water=31, Overworld=6, Underground=28, Castle=0

TileOffsetTable:
  .byte $00, $03, $19, $1C    ; Water=0, Overworld=3, Underground=25, Castle=28

6-2 seviyesini yüklemek için (ID $23, Overworld numara 3):

; 1. Tür = 01 (Overworld) → ofset tablosunda indeks = 1
; 2. Sprite ofset = SpriteOffsetTable[1] = 6
;    Final indeks = 6 + 3 (seviye numarası) = 9 → 10. sprite işaretçisi
; 3. Tile ofset = TileOffsetTable[1] = 3
;    Final indeks = 3 + 3 = 6 → 7. kare işaretçisi
; 4. Sonuç: kare işaretçi $A619 + sprite işaretçi $9ED0 = 6-2 ✓

Peki $43 gibi geçersiz ID ile ne olur (var olmayan Underground numara 3)?

; ID $43, Tür = 10 (Underground), numara = 3
; Sprite ofset = SpriteOffsetTable[2] = $1C = 28
;   İndeks = 28 + 3 = 31 → 32. sprite işaretçisi = su bonus 5-2!
; Tile ofset = TileOffsetTable[2] = $19 = 25
;   İndeks = 25 + 3 = 28 → 29. kare işaretçisi = 1-4 (Castle)!
;
; Sonuç: 1-4 kareli yer altı seviyesi
; ve 5-2 su bölgesinin Blooperları. Gerçek Frankenstein.

Level ID $43: Frankenstein seviye, 1-4 kareleri + su spriteları 5-2

Exploring Glitch Level Pointers: ofset tabloları açıklandı

World index table: world 9 taşınca glitch seviye yaratır

World index table: world 9 neden taşar

Her dünyanın (1-8) ilk seviyesinin indeksini veren 8 baytlık bir ROM tablosu vardır. Ve hemen sonra oyun sırasındaki 36 Level IDnin tablosu.

; WorldIndexTable (8 bayt)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   -> Dünya 1 0. seviyede başlar
;   -> Dünya 2 5. seviyede başlar
;   -> Dünya 8 33. seviyede başlar

; LevelIDTable (36 bayt)
  .byte $25, $28, $29, $26, $24, ... ; 36 Level ID

World 9 yüklenmeye çalışılınca oyun WorldIndexTableın var olmayan 9. baytını okur. LevelIDTable içine 1 bayt taşar, $25 değerini okur, sonra $25i LevelIDTable içinde indeks olarak kullanır (37. girdi): bu da SpriteOffsetTable içine yeniden 2 bayt taşar ve 6 değerini okur.

; World 9 :
;   1. WorldIndexTable[8] (taşma) → LevelIDTable içinde $25 okur
;   2. LevelIDTable[37] (taşma) → SpriteOffsetTable 2. baytını okur = 6
;   3. ID = 6 → var olmayan Water seviye numara 6
;   4. Tile pointer = water işaretçi numara 6 = 6-2 kareleri
;   5. Sprite pointer = indeks 31+6 = 37 > 33 → geçersiz işaretçi
;   6. Sonuç: glitchli spritelı su altı 6-2
;      → world 9-1!

World G (16) için taşma daha da ileri gider ve cutscene Level ID $01 üzerine düşer:

; World G (16) :
;   WorldIndexTable[15] → LevelIDTable içinde $01 okur
;   LevelIDTable[1] = $29 (1-2 giriş ara sahnesi)
;   → world G-1 = 1-2 giriş ara sahnesi

Glitch dünyaları neden vardır

Oyunda 32 "meşru" seviye vardır (8 dünya × 4 seviye). Ama işaretçi tablosu bölge türü başına 128 girdi yapar. 32. seviye ötesindeki girdiler ROMda o adreslerde ne varsa içerir: bazen başka bir seviye, bazen ses verileri, bazen RAM, bazen her şey.

Level ID $01 Water (Minus World): kare işaretçi $AE45, sprite işaretçi $A171

Level ID $01 + AreaType 0 = Minus World

Glitch dünyalarının en ünlüsü. AreaType 0 (su) içinde Level ID $01 şuna işaret eder:

  • Tile pointer: $AE45 → 2-2/7-2 su altı bölgesi
  • Sprite pointer: $A171 → 2-2/7-2 spriteları

Sonuç: 2-2ye benzeyen bir su seviyesi ama bayrak direği olmadığı için sonsuz döngüye girer. Seviye sonsuza dek döner.

Bu 36-1 seviyesidir (ya da $-1 dünyasında 36-1).

SMB1 warm start kontrolü: Minus Worldün var olmasına izin veren odur

; Bayrak direği Minus Worldde neden eksik:
; 2-2/7-2 spriteları ($A171) dizilerinde bayrak direği
; yoktur. Oyun $FD spritini (bayrak direği) arar
; ama asla bulamaz → sonsuz döngü
;
; Oyun seviyeyi sonsuza dek üretmeye devam eder
; zamanlayıcı sıfıra ulaşana kadar.

RAMe işaret eden işaretçiler

Kare işaretçi ya da sprite işaretçi ROM yerine RAM adresine ($00-$7F) işaret edince oyun RAMin sürekli değişimlerini kare olarak yorumlamaya çalışır:

; Örnek: Water içinde Level ID $03
; Tile Pointer : $A46B (3-3 - geçerli)
; Sprite Pointer : $009D (sıfır sayfası RAMe işaret eder!)
;
; Sıfır sayfası RAM oyun yazmaçlarını içerir,
; Marionun konumu, sayaçların durumu...
; Oyun bunu sprite dizisi olarak açar
; ve sonuç yazmaç değerleri olan düşmanlı bir seviyedir.

Sıfır sayfası değişince (Mario hareket ettiği, zamanlayıcı döndüğü için...) seviyenin "spriteları" da değişir. Bazı glitch dünyalarının sürekli yanıp sönen ve dönüşen düşmanları olmasının sebebi budur.

Level ID $03 Water: sprite işaretçi $009D RAMe işaret eder, oynanamaz seviye

Level ID $36: boş seviye (Overworld)

Overworld içinde Level ID $36:

  • Tile pointer: $AC35 (1-2)
  • Sprite pointer: $A0D8 (1-2)

Sonuç: hiçbir şey. Oyun seviyeyi yükler ama RGMechEx kataloğunda "seviyesiz" işaretlidir. Kareler geçerli olabilir ama spritelar boş ya da işlevsiz seviye üreten bir yere işaret eder.

Level ID $1D (Castle): crash şampiyonu

Castle içinde Level ID $1D:

  • Tile pointer: $A210 (4-4)
  • Sprite pointer: $7EA0 (RAM!)

RAMde sprite işaretçi = tanımsız spritelar. Oyun ilk kare satırında Spiny topu ya da Bullet Bill atıcı göstermeye çalışır. Hemen çöker.

; Sprite işaretçi RAMe işaret edince
; oyun sürekli değişen baytları "doğma" talimatları
; gibi açar. Sonuç:
; - Var olmayan nesnelerin belirmesi (tanımsız değer)
; - NES spriti geçersiz kare göstermeye çalışınca PPU çökmesi
; - Konsolun tam donması

Kataloglanmış 256 glitch dünyası

RGMechEx tüm seviyelerin haritalarını üreten bir script yazdı, 4 bölge türü ve her biri 128 ID için.

Dünya sayacı 8 bittir (0-255). 1-8. dünyalar meşrudur. Geriye 248 potansiyel glitch dünyası kalır. Her glitch dünya bu dünyanın ilk seviyesine karşılık gelir ve Level IDsi WorldIndexTable taşma mekanizmasıyla hesaplanır.

Glitch dünyaları tablosu: 248 bozuk dünya, 68 erişilebilir ilk seviye

128 olası IDden yalnızca 68i bir dünyanın "ilk seviyesidir" (glitch dünya numarası üzerinden erişilebilir). Diğer 60ı 2+ seviyeler ya da erişilemezdir.

Tür Oynanabilir benzersiz ID Çöken ID Boş ID
Water (0) ~20 ~60 ~48
Overworld (1) ~30 ~55 ~43
Underground (2) ~15 ~65 ~48
Castle (3) ~25 ~58 ~45

İşaretçiler aynı ROM adreslerine düştüğü için birçok ID aynı seviyeye götürür. Örneğin Level ID $28 (Overworld): kare işaretçi $A7CD (2-1), 38 farklı glitch dünyada görünür çünkü sprite işaretçi $9F51 birçok IDce yeniden kullanılan dolgu/ses verisi bölgesine işaret eder.

Level ID $28 (Overworld) haritası: normal spritelı 2-1 kareleri, 38 glitch dünya

Super Mario Bros. Glitch Levels Explained: 3. video

Gerçekten benzersiz 6 glitch seviyesi

Erişilebilir 19 glitch seviye IDsi arasında yalnızca 6sı yüklenirken hemen çökmez:

Dünya Level ID Açıklama
E-1 (224) $50 Uçurum üstünde tek ? blok. Mario anında ölür.
W $57 Mario doğmada sıkışır, hareket edemez.
42 (133) $50 Mario yeterince ileri giderse onu tuzağa düşüren bulut tüneli.
62 (131, 240) $4D Donmuş kale: Mario üstte doğar, düşemez → sıkışır.
127 $4B Yer altı tüneli ama çok ileri gidilirse çöker.
137 $4B Ara sahnelerin otomatik kaydırmasını etkinleştirir. Mario onu sonsuza dek bloklayan tek tuğla blokla karşılaşır.

Level ID $50 (bulut tüneli): glitch dünya 42-1 ve E-1 Level ID $4D (kale): world 62-1, Mario doğmada sıkışır Level ID $4B (tünel): world 127-1, çok ileri gidilirse çöker

248 glitch dünyadan gerçekten yeni bir şey üreten altı glitch dünya. Geri kalan yanlış bölge türünde normal seviyeler ya da siyah ekranlardır.

Seviye formatı ayrıntılı

Tam seviye veri formatına odaklanılır, glitch seviyelerin neden ayakta durduğunu (ya da durmadığını) anlamak için.

Seviye başlığı: 2 bayt, 6 özellik

Her seviye 6 özelliği kontrol eden 2 baytlık başlıkla başlar:

; Bayt 0 : zamanlayıcı + Y başlangıç + değiştirici
;   Bit 7-6 : zamanlayıcı (00=değişmez, 01=200, 10=300, 11=400)
;   Bit 5-3 : Y başlangıç Mario (111/110 = otomatik yürüme)
;   Bit 2-0 : seviye türü değiştirici
;              000=varsayılan, 001=dalgalar, 010=tuğla duvar,
;              011=su dibi, 100=gece, 101=kar,
;              110=karlı gece, 111=gri gece

; Bayt 1 : platform + arka plan + zemin deseni
;   Bit 7-6 : özel platform (00=ağaç, 01=mantar,
;                                 10=Bullet Bill, 11=bulut)
;   Bit 5-4 : arka plan (00=yok, 01=bulutlar,
;                           10=dağlar, 11=çitler)
;   Bit 3-0 : başlangıç zemin deseni (0-15)

Tür değiştirici görsel varyasyonları kontrol eder: su seviyelerinin üstündeki dalgalar, 8-3ün tuğla fonu, 4-3ün gece paleti, 6-2nin karı vs.

Kare nesneleri: 2 bayt, Next Screen Bayrağı, 3 slot kuyruk

Başlıktan sonra kare nesne listesi gelir, her nesne 2 bayttır. $FD baytı listenin sonunu işaretler.

; Kare nesne formatı (16 bit):
; Bayt 0 :
;   Bit 7-4 : X konumu (sütun 0-15)
;   Bit 3-0 : Y konumu
;     Y=0-11  : normal Y konumu
;     Y=12    : özel nesneler (delikler, köprüler, ip, ? bloklar)
;     Y=13    : ekran atlama / özel nesneler 2
;     Y=14    : değiştirici/dekor/zemin değişimi
;     Y=15    : özel nesneler 3 (kale, merdivenler, büyük boru)

; Bayt 1 :
;   Bit  7   : NEXT SCREEN FLAG
;   Bit 6-4 : nesne türü (0-7)
;   Bit 3-0 : genişlik/yükseklik / alt tür

"next screen" biti konunca mevcut çalışma sütunu 1 artırılır. Bu ilk 16 sütunun ötesine nesne yerleştirmeye izin verir. Oyun onları sırayla yüklediği için nesneler sırayla listelenmelidir (soldan sağa):

; Yükleme rutininin sütun başına İKİ fazı vardır:
; Faz 1 : bu sütunda başlayan yeni nesneleri ara
;            ve kuyruğa ekle (queue)
; Faz 2 : kuyruktaki her nesneyi kareleri çizerek işle
;            ve bu sütunda bitenleri çıkar

Kuyruk tam 3 slot yapar. Doğrudan sonuç: aynı sütunda başlayan 3ten fazla nesne olamaz. Kuyruk doluysa 4. nesne yoksayılır ve asla yüklenmez.

İyi tasarlanmış seviyelerin bu yüzden çok nesne yığmaktan kaçınmasıdır. 1-2deki örnek: tavandaki 1up blok + yanındaki tuğlalar 3 limitine uymak için iki ayrı nesneye bölünmüştür.

Özel Y konumu: 12, 13, 14, 15

Y=12 olunca nesnenin Y konumu yoktur (türe göre sabit kodludur):

; Y=12 : Y konumsuz nesneler
;   Tür 0 : delik (zemini siler)
;   Tür 1 : hareketli platform ipi
;   Tür 2-4 : sabit Yli köprüler
;   Tür 5 : su/lavlı delik
;   Tür 6-7 : ? blok sıraları

Y=13 olunca iki alt grup. Bayt 1in 6. biti 1 ise:

; Y=13, bit6=1 : özel nesneler
;   0 = L-boru (ara sahne), 1 = bayrak direği, 2-3 = köprü/balta/çekiç (kale sonu)
;   4 = ekran durdurma, 5 = rastgele düşmanlar, 6 = seviye döngüsü, 7+ = olası crash

Bit6=0 ise düşük ağırlıklı 5 bit ekran atlamayı kodlar (tek tek next screen bayrağıyla geçmeden doğrudan N ekranına atlama).

Y=14 olunca: tür değiştiriciyi değiştirmek için bit6=1 ile aynı ilke, fon + zemin desenini değiştirmek için bit6=0.

Zemin desenleri: 16 zemin motifi

Seviyelerin zemini tekil nesnelerden yapılmaz. SMB1 bir sonraki değişime kadar tüm sütunlara uygulanan arka plan motifi zemin desenleri kullanır:

; Zemin desenleri (4 bit = 16 olasılık)
;   0 = tam boş
;   1 = 2 kare yüksek zemin
;   2 = 1 kare yüksek zemin
;   3 = zemin + alt
;   4 = zemin + alt 2
;   5 = zemin 1/2 kare
;   6 = 3/4 zemin
;   ... 15e kadar = tam dolu (zemin + tavan)

Deliklerin nesne olmasının sebebi budur: geri kalan için deseni değiştirmeden belirli bir sütunda zemin desenini ezerler.

256 bayt limiti ve tekrar

Bir seviyenin tüm kare verileri maksimum 256 bayta sığar. 6502nin Y yazmacı indeks olarak kullanılır ve 8 bittir. Oyun $FD baytını bulmadan verilerin sonuna gelirse başa döner ve 256 baytı sonsuza dek tekrarlar:

; İndeks Y = 8 bit → maksimum 256 bayt kare verisi
; Y taşarsa (255 → 0) $FD ile karşılaşmadan → tekrar
; Spritelar için aynısı ama boru nesneleri (3 bayt)
; her yüklemede indeks paritesini kaydırır.

Bazı glitch seviyeler "süresiz" süren seviyeler üretmek için bu tekrardan yararlanır.

Sprite sistemi: 2 bayt + boru geçişleri

Spritelar benzer formatı izler ama başlıksız ve birkaç kilit farkla. $FF baytı listenin sonunu işaretler.

; Sprite formatı (2 bayt):
; Bayt 0 : X konumu (sütun)
; Bayt 1 :
;   Bit 7 : NEXT SCREEN FLAG
;   Bit 6-0 : sprite türü
;       Bazı türler: goomba, koopa, Blooper,
;       Bullet Bill, Lakitu, Spiny, platformlar,
;       warp zone komutu, toad/prenses,
;       düşman grubu doğurma komutları

Bayt 1in düşük ağırlıklı biti hard level bayrağıdır: 1 ise sprite yalnızca ≥ 5-3 seviyelerinde belirir. "hard mode" seviyeleri böyle yaratılır.

Y konumu 15 = ekran atlama (karelerle aynı). Y konumu 14 = boru geçişi (3 bayt):

; Sprite Y=14 : boru/sarmaşık geçişi (3 bayt!)
;   Bayt 0 : X konumu
;   Bayt 1 : bit 6-0 = 7-bit Level ID (hedef)
;   Bayt 2 : bit 4-0 = hedef ekran
;            bit 7-5 = bu geçişin geçerli olduğu dünya
;
; Neden dünya? Bonus odalar dünyalar arasında yeniden kullanılır.
; Örnek: 1-1 bonus odası 2-1 ve 7-1 tarafından da kullanılır.
; Bu odanın 3 geçişi vardır, dünya başına bir tane, böylece Mario
; doğru yerde yeniden belirir.

Spriteların kuyruk sistemi yoktur. Tek limit doğma bölgesinde (hemen ekran dışı sağda) aynı anda yüklenebilecek 4ten fazla sprite olamamasıdır. Ötesi yoksayılır.

Glitch dünyalarına nasıl erişilir

İki ana yöntem vardır.

Klasik yöntem: duvar klibi

Duvar klibi (duvarlardan geçme) normal seviyeden çıkıp gizli warp bölgesine kadar yürümeye izin verir. RAM üzerinden dünya sayacını manipüle ederek herhangi bir Level ID yüklenebilir.

Teknik:

  1. World 1-2: gizli bitiş borusuna git
  2. Sağ duvarda duvar klibi yap
  3. Boşlukta warp bölgesine kadar yürü
  4. Oyun değerleri dünya olarak yorumlar

Ama bu yöntem glitch dünyalarının yalnızca küçük kısmına erişim verir.

Ekstrem yöntem: NES Tennis kart takası

Tam detay için yukarıdaki "Warm start" bölümüne bak. Özet: Tennis adım sayacı SMB1 başlangıç dünyasıyla aynı RAM baytına yazar ve warm start tespiti bu değeri korur.

Meraklı köşesi: her şeyi keşfetme kodu

Emülatörde tüm glitchi kendin keşfetmek istersen Level IDyi doğrudan yamayabilirsin:

; FCEUX / Mesen için yama:
; RAM adresi $075F = mevcut Level ID
; RAM adresi $0760 = Bölge Türü (0=Water, 1=Overworld, 2=Underground, 3=Castle)

; Örnek: Overworldde Seviye 57 (0x39) yükle
; Emülatörde bellek izleyiciyi aç ve yaz:
; $075F = 0x39
; $0760 = 0x01
; Sonra warp borusuna gir ya da öl ve yeniden başla
; → Oyun ID $39 seviyesini Overworldde yükler

RGMechEx otomatik üretilmiş haritalı 128 seviye × 4 türün tam listesini rgmechex.com üzerinde yayınladı. Her girdi kare işaretçiyi, sprite işaretçiyi ve seviyenin görsel haritasını gösterir.

En saçma seviyeler

Level ID $1F (Water): tekte 15 glitch dünya

Kare işaretçi $A302 (3-4) ile sprite işaretçi $02A0 birleşimi 15 farklı glitch dünya verir (D-1, J-1, Y-1, Z-1, 55-1, 73-1...). Açıklama: sprite işaretçi oynanabilir sonuçlar üretecek kadar geçerli spritelara yakın veriler içeren ROM bölgesine işaret eder ama 3-4 kale kareleriyle overworld spritelarının kombinasyonu absürt bir render yaratır.

Level ID $28 (Overworld): 38 glitch dünya = rekor

Mutlak rekor. 38 glitch dünya girdisi aynı seviyeye işaret eder (2-1 kareleri + $9F51 spriteları). Neden? Çünkü sprite işaretçi $9F51 birçok IDce yeniden kullanılan dolgu/ses verisi bölgesine düşer.

Level ID $49 (Underground): FDS seviyesi

Kare işaretçi $76AE + sprite işaretçi $1C9D. Kare işaretçi Famicom Disk System sürümüne ayrılmış ROM bölgesine işaret eder. Sonuç: standart kartuşta var olmayan kareli bir seviye. 52-1 ve 196-1 seviyelerini gösteren seviye budur.

Level ID $00-$02: gerçek bonus seviyeler

Bu IDler oyunun meşru alt seviyelerince kullanılır:

  • $00: 5-2/6-2 su altı bölgesi (H-1, 39-1 kullanır)
  • $01: 2-2/7-2 suyu (Minus World, 36-1)
  • $02: 8-4 alt seviyesi (136-1, 151-1, 215-1)

Normal erişilen "bonus" seviye ile glitch dünya arasındaki fark warp zoneların mevcut dünyayı doğrulamasıdır:

; Warp zone doğrulaması (basitleştirilmiş)
; Oyun hedef dünyanın 1 ile 8 arasında olduğunu doğrular
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; < 1 → reddedildi
  cmp #9
  bcs InvalidWarp       ; > 8 → reddedildi
  ; yalnızca 1 ile 8 arası geçerli dünya
  jmp DoWarp

Numarası > 8 ya da 0 olan glitch dünyalarına normal borularla ulaşılamaz. Duvar klibi ya da kart takası gerekir.

Bazı seviyeler neden çöker: jump tablelar

Oyun bir kare nesnesi yüklerken türünü jump table içinde indeks olarak kullanır:

; Standart kare nesnelerinin jump tableı (türler 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; tür 0 : ? blok, gizli, bayrak direği...
  .word Obj_Platform      ; tür 1 : özel platform
  .word Obj_BrickRow      ; tür 2 : tuğla sırası
  .word Obj_BlockRow      ; tür 3 : blok sırası
  .word Obj_CoinRow       ; tür 4 : jeton sırası
  .word Obj_BrickCol      ; tür 5 : tuğla sütunu
  .word Obj_BlockCol      ; tür 6 : blok sütunu
  .word Obj_Pipe          ; tür 7 : boru
  .word Obj_8             ; tür 8
  .word Obj_9             ; tür 9
  .word Obj_10            ; tür 10
  .word Obj_11            ; tür 11

Jump tablelar: geçersiz nesne türü oyunu neden çökertir

Bir nesnenin türü geçersizse (≥12) oyun bu tabloda var olmayan bir işaretçiye atlar. 4 olası sonuç:

  1. Geçerli işaretçi → nesne normal yüklenir
  2. Başka jump tablea işaretçi (örtüşme) → farklı bir nesne belirir. Örnek: tür 12 Y=13 tablosuna işaret eder, bu da L-boru verir.
  3. Çalıştırılabilir koda işaretçi → rastgele kod çalıştırma (olası crash)
  4. Açık yer tutucu (NOP) → nesne hiçbir şey yapmaz (bazı spritelar böyledir, hareket etmeden yerinde uçan düşmanlar üretir)

Glitch seviye ID $58: sprite işaretçi geçersiz adrese işaret eder, oyun çöker

Glitch seviye ID $50: bulut tüneli, bozuk verilerle üretilmiş seviye

Glitch seviye ID $58 (çöken tünel): sprite işaretçi NES standart haritasında var olmayan bellek bölgesine işaret eder. Oyun aynı Koopayı kare başına 5 kez (0,0) konumunda yüklemeye çalışır, bu PPUyu doyurur ve donmaya neden olur.

; ID $58 neden çöker:
; Sprite işaretçi → geçersiz adres (standart NES alanı dışı)
; → Oyun belirsiz baytları sprite türleri olarak okur
; → Geçersiz $00 Koopa (handlersız tür) özyinelemeli çağrıda döngüye girer
; → 6502 yığın taşması → donma

Boru warp paradoksu

target_world BETWEEN 1 AND 8 kontrolünü hatırla. Glitch dünyada bir boru bile bulsan oyun hedef dünyanın 1 ile 8 arasında olduğunu doğrular. Glitch dünyaların numaraları > 8dir (36-1, 255-1...), o yüzden warp başarısız olur.

Minus Worldün sonu olmamasının sebebi de budur: bayrak direği spritelarda yoktur ve borular hiçbir yere gitmez.

Tek sütunda 5 nesne numarası

Sütun başına 3 nesne limitini aşmaya izin veren uç durum vardır. Kuyruk bloklanınca (slotlar dolu + sonraki nesnede next screen bayrağı eksik) oyun mevcut sütunu next screen bayraklı nesne bulana kadar döngüde "ön işler". Her ön işleme sırasında:

; Sütunun ön işlenmesi sırasında:
; 1. Kuyruktaki nesnelerin kalan genişliği
;    her "sahte sütun ilerlemede" azaltılır
; 2. Bir nesne genişlik=0a ulaşırsa kuyruktan çıkar
; 3. Serbest slot aynı sütunda eklenen yeni nesneyle
;    doldurulabilir

; Sonuç: aynı sütunda 5e kadar nesne işlenebilir.
; Teknik: ekran sınırını geçen 2 nesne yerleştir
; (slot 1 ve 2), öncekinden X < olan 1 kukla nesne (kuyruğu bloklar),
; sonra sonraki ekranın X=0ına 3 nesne (biri next screen bayraklı).

Buna "queue skip" denir ve bazı romhackerlar formatın normalde izin verdiğinden daha yoğun seviyeler yaratmak için kullanır.

Sürümler arası farklar

Famicom Disk System

SMB1in FDS sürümünün farklı bellek haritası vardır. Tüm seviye işaretçileri kaymıştır ama veriler aynıdır. Değişen şey: glitch dünya indeksleri tamamen farklıdır:

FDS World 36 → Level ID $09 (5-3 su sürümü)
  → Bayrak direği mevcut! Seviye bitirilebilir.
  → Sonra: $27 (normal 7-3) → $44 (4-4 underground)
  → $44 bitirilebilir → balta çalışır → oyun sonu!
  
  Yani FDS Minus World NES sürümünün tersine oyunun
  tamamlanmasına götürebilen "bonus dünya"dır.

Favori FDS seviyem: ID $5F, alçak tünelde 3-3 ikinci yarısının yer altı sürümü (otomatik kaydırıcı olması yazık).

The Lost Levels (Japon Super Mario Bros. 2)

Lost Levels birçok şeyi değiştirir:

  1. Aynı kare/sprite sırası: artık Frankenstein seviye yok (geçersiz IDde bile kareler ve spritelar aynı seviyeyi yükler)
  2. İki ayrı high/low tablo yerine tek 16-bit işaretçi tablosu
  3. 4 disk dosyası: ROM FDS için bölündü:
    • Dosya 1: world 1-4
    • Dosya 2: world 5-8
    • Dosya 3: world 9 + ses motoru
    • Dosya 4: world A-D (tamamen farklı işaretçi tablosu)
  4. Aynı Level ID = yüklenen dosyaya göre 4 olası seviye
  5. Artık Tennis glitchi yok: continue seçeneği (game overdan sonra aynı dünyada devam) warm starti gereksiz kılar ve oyun world > 9 ise hemen sıfırlar
  6. Yeni nesneler: zehirli mantar, görünmez blok, görünmez ateş çiçeği blok, ters borular, rüzgâr: ama mevcut listelerin ortasına eklendi → SMB1 ile geriye uyumsuzluk
  7. World 4 sonrası hep kırmızı Piranha Plant, yalnızca world 2/B/3/C/7de yeşil yay tahtaları

Super Mario All-Stars (SNES)

Aynı 6502 rutinlerle doğrudan port (SNES uyumlu modda NES kodunu çalıştırır):

  • Warp zone düzeltildi: artık Minus World yok (metinden önce sol boruya girmek doğru dünyaya götürür)
  • Çökme: çoğu glitch seviye çöker (ID $6A ve 9-1 hariç)
  • Eklenen kale nesneleri: daha benzersiz renderlar
  • Ama: 4-2 wrong warp hâlâ çalışır (yamalanmadı!)

4-2 wrong warp: nesne yerleştirme hatası

4-2de iki boru geçiş nesnesi vardır: sarmaşık (warp zone) ve boru (jeton odası). İlk geçiş nesnesi (sarmaşığınki) sarmaşık ekranda belirmeden çok önce yerleştirilir. İkincisi (borununki) seviyede çok geç yerleştirilir.

; 4-2de geçişlerin zamanlaması:
; Geçiş nesnesi 1 (sarmaşık → warp zone) : sarmaşıktan 3 ekran önce yerleştirilir
; Geçiş nesnesi 2 (boru → jeton odası) : borudan 1 ekran sonra yerleştirilir
;
; Normalde Mario boruya ulaşmadan önce ilk nesne devre dışı kalır.
; Ama Mario hızlı giderse (ya da B+sağ blok kısayolunu kullanırsa) sarmaşık
; geçişi boruya dokunduğunda hâlâ aktiftir!
; → Oyun jeton odası yerine warp zone yükler.
;
; Nesne sarmaşığın hemen sonrasına ama borudan önce
; yerleştirilseydi hata var olmazdı.

Döngülü seviyeler

Döngüler (8-4, 7-4) nasıl çalışır? Seviyenin ekran numaraları ve sabit Y konumlarıyla kontrol noktaları vardır:

; Kontrol noktası: {screen_number, vertical_position}
; Mario bu kontrol noktasından doğru yükseklikte geçerse → seviye devam eder
; Yoksa → 4 ekran geri warp (64 blok)
;
; Sonsuz döngü için: vertical_position = $F0
; (ekranın altının altında) → doğrulamak imkânsız.
;
; Kontrol noktaları teklidir (tek bayrak) world 7 hariç
; o üçlü kullanır (3 bayrak, en az 1inde başarısız olmak gerekir)
;
; Geri warp serttir: kare veri ofseti sabit değere ayarlanır,
; sprite veri ofseti 0a sıfırlanır. Mevcut düşmanlar
; anında boşaltılır → firebarlar kaybolur.

Formatı değiştirmek, kodu değil

Bu mimarinin en büyüleyici derslerinden biri SMB1 geliştiricilerinin 6502 render koduna hiç dokunmadan çok etkileyici bir seviye sistemi yaratmayı başarmasıdır. Seviyeler arası tüm varyasyon verilerden (işaretçiler, nesneler, spritelar, zemin desenleri) gelir, koddan değil.

256 glitch dünya vardır çünkü işaretçi tabloları 128 girdi × 4 tür boyutludur ve oyun okuduğu değerleri asla doğrulamaz. İşaretçi RAMe düşünce oyun Marionun yazmaçlarını kare olarak yorumlar. İşaretçi ses verilerine düşünce oyun müziği seviye tasarımı olarak oynar. Ve jump tablelar taşınca oyun crash olana kadar her şeyi çalıştırır.

More Super Mario Bros. Mechanics Explained: 4. video

Bütün bunlardan ne öğrenilebilir

  1. Kare/sprite ayrımı: iki katmanın tam bağımsızlığı, benzersiz Frankenstein seviyeler yaratan farklı depolama sıralarıyla
  2. RLE sıkıştırma + nesne sistemi: seviyeler bitmap değil yerleştirilmiş nesne listeleridir, zemin için zemin desenleriyle
  3. 3 slot kuyruk: donanımın (ve seviye tasarımının) katı limiti
  4. İşaretçilere tam güven: oyun işaretçilere ve jump tablelara güvenir, bu ya oynanabilir glitchler ya crash üretir
  5. Maksimum 256 bayt: 6502 Y yazmacının limiti, çok ileri gidilirse verilerin tekrar etmesine neden olur
  6. Warm start / cold start: Tennis → Mario kart takasına kapı açan "devam" sistemi

En güzeli: bütün bunlar 40KB içine sığan 6502 kodudur. Kod doğrudan donanımla konuşur ve her bellek erişimine tam güvenir. İşaretçi bozuksa oyun çöker. Ve crashlere glitch dünyaları deriz.

Akılda tutulacak 3 şey

  1. Glitch dünyaları kötü düşen işaretçilerdir: Oyun 128 ID × 4 bölge türüne sahiptir ama yalnızca 34 benzersiz seviye. Dünya numarası bozulunca (Tennis ya da duvar klibiyle) oyun başka bir seviye için tasarlanmış işaretçi yükler ve 512 olası kombinasyon öngörülemez sonuçlar üretir.

  2. Minus World warp hatasıyla bozulmanın birleşimidir: 1-2deki sol boru metin belirmeden etkinleştirilirse world 36 (0x24) yükler. Bu dünya Level ID $01e işaret eder (2-2 suyu), bayrak direksiz bir seviye. Ve world 36 için boru geçişi olmadığı için seviye sonsuz döngüye girer. Doğrulama yokluğu ikonu yaratır.

  3. Tennis → Mario, OoT → Paper Mario tan 15 yıl önce: NES RAMi kapasitörler ve SMB1in warm start / cold start sistemi sayesinde kartuş takasında hayatta kalır. Tennis adım sayacı (adım seslerini oynatarak RAM baytını artıran) tam dünya numarası adresine düşer. En yüksek skor hanelerinin 0 kalması, $A5 baytının bozulmaması gerekir ve oyunun warm start tespit etmesi gerekir: yalnızca Tennis ile işe yaramış mükemmel bir tesadüfler yarışması.

Retro Game Mechanics Explained orijinal videoları inanılmaz bir karınca işidir: 6502 sökümünde detay seviyesi, tüm seviyelerin otomatik haritaları, kart takası ve warm start açıklamaları. Seriyi izlemediysen izle, kısadır ve her dakika yoğundur.

Haritaların kaynak kodu rgmechex.com adresinde mevcuttur ve tam SMB1 sökümü birçok depoda açık kaynaktır. 40 yıl önce Japon programcılar bu seviye sistemini sıfır birim testi ve sıfır hata izleyiciyle 6502de yazdı ve bugün kodlarını açarak optimizasyon teknikleri öğrenmeye devam ediyoruz.

Super Mario Bros.: il formato di livello, i puntatori e i 256 glitch world

Come 128 livelli × 4 tipi di zona stanno in 40KB di ROM, perché il Minus World esiste, e come un match di Tennis NES può caricare glitch world.

Introduzione

Super Mario Bros., sono 40 kilobyte di ROM. Otto mondi, 32 livelli, nemici, musica, power-up, tutto sta lì dentro.

Ma se apri un emulatore e traffichi i byte giusti, puoi caricare il livello 36-1. O il 255-1. O atterrare in un mondo dove tutto è fatto di sprite di Bowser e tubi che non portano da nessuna parte.

Questi glitch world esistono per una ragione semplice: il sistema di storage dei livelli di SMB1 è una meraviglia di ottimizzazione 8-bit, e quando forziamo il gioco a leggere dove non dovrebbe, dà risultati affascinanti.

Retro Game Mechanics Explained ha fatto una serie di 4 video là sopra: li compiliamo in un'unica immersione nel codice 6502 del gioco più venduto della sua epoca.

GLITCH OBJECTS -- le titre de la série RGMechEx sur les mécaniques cachées de SMB1

World 9-1 -- l'écran titre du premier glitch world accessible via le cart swap Tennis

Il warm start: perché la RAM di Tennis sopravvive in SMB1

Prima di parlare di storage di livelli, bisogna capire come SMB1 si avvia. Perché il glitch del cart swap NES Tennis poggia interamente sul sistema di rilevamento warm start / cold start del gioco.

I 41 byte preservati

Quando SMB1 rileva un cold start (prima accensione o power off/on), cancella tutta la RAM. Ma quando rileva un warm start (reset pulsante, niente interruzione di alimentazione), preserva una zona memoria di 41 byte:

; I 41 byte preservati in RAM durante un warm start
; Indirizzi $075F-$0787
;
; $075F : byte di avvio (world - 1)    [1 byte]
; $0760 : flag di selezione mondo (pulsante B) [1 byte]
; $0761-$0762 : inutilizzato                    [2 byte]
; $0763-$0768 : timer (6 cifre, 3 mostrate) [6 byte]
; $0769-$076E : monete Luigi                   [6 byte]
; $076F-$0774 : monete Mario                   [6 byte]
; $0775-$077A : punteggio Luigi                   [6 byte]
; $077B-$0780 : punteggio Mario                   [6 byte]
; $0781-$0786 : top score (6 cifre, 1 nascosta) [6 byte]
; $0787 : il byte magico $A5                 [1 byte]

Questi 41 byte servono a una sola funzionalità: permettere al giocatore di continuare allo stesso mondo dopo un game over. Se muori in 6-3, il gioco scrive il mondo 6 nel byte di avvio, e alla title screen, se tieni A + Start, ricominci in 6-1.

Les 41 bytes préservés en RAM lors d'un warm start -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, et le byte magique $A5

La doppia verifica del warm start

Cold start vs warm start -- le diagramme de détection du reset

Quando SMB1 fa boot, non verifica un solo criterio ma due:

; verifica warm start: byte magico + cifre top score valide
CheckWarmStart:
  ; 1. Verifica il byte magico $A5 a $0787
  lda $0787
  cmp #$A5
  bne ColdStart        ; non $A5 → cold start

  ; 2. Verifica le 6 cifre del top score ($0781-$0786)
  ;    Ogni cifra deve stare tra 0 e 9
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; cifra >= 10 → cold start
  inx
  cpx #6
  bne CheckLoop

  ; Se entrambe le condizioni passano → warm start
  ; La RAM non è cancellata, il mondo di partenza è preservato
  jmp WarmStartBoot

La vérification du byte $A5 et des digits du top score -- le coeur du warm start

Perché una doppia verifica? Perché il byte $A5 potrebbe esserci per caso (un altro gioco che lascia questo valore, o lo stato di riposo di default del chip RAM). Verificando che le cifre del top score siano valide (0-9), ci assicuriamo che i dati siano coerenti.

Perché Tennis è l'unico gioco che funziona

Quando inseriamo SMB1 per la prima volta (cold start), il gioco:

  1. Cancella tutta la RAM → top score = 0, world byte = 0
  2. Scrive $A5 all'indirizzo $0787

Poi, passiamo su Tennis senza spegnere la console. Tennis:

  • Non pulisce la RAM all'avvio (pochi giochi NES lo fanno)
  • Non scrive sui byte del top score → restano a 0 (validi)
  • Non tocca il byte $A5 → resta presente
  • Usa l'indirizzo $075F per il contatore di passi del giocatore
; Il footstep increment in Tennis:
; A ogni passo del giocatore sul campo, Tennis incrementa il byte a $075F.
; Questo stesso byte è usato da SMB1 come "world number - 1".
;
; 0 passi  → world 0 → SMB1 = world 1
; 1-7 passi → world 1-7 → mondi normali
; 8+ passi → world 8+ → glitch world!
;
; Il contatore si incrementa solo quando la musica si ferma
; (i footstep sound non suonano durante la musica).

Quando rimettiamo SMB1:

  1. Il byte $A5 è sempre lì (Tennis non l'ha toccato)
  2. Le cifre del top score sono sempre 0 (valide)
  3. Il world byte vale ormai 8+ (incrementato dai passi di Tennis)
  4. SMB1 rileva un warm start → preserva il world byte corrotto
  5. Tenere A + Start → world 9-1, world A-1, world 36-1, ecc.

Perché bisogna avviare Mario prima di Tennis

Una sottigliezza: bisogna prima avviare SMB1, poi Tennis, poi di nuovo SMB1. Se iniziassi direttamente da Tennis, il byte $A5 non sarebbe mai scritto (Tennis non scrive $A5), quindi il rilevamento warm start fallirebbe e la RAM sarebbe cancellata.

Le compteur de pas de Tennis : chaque footstep incrémente le world byte

Access Glitch Worlds via NES Tennis -- la vidéo qui explique le cart swap

Come SMB1 salva i suoi livelli in 40KB

Nintendo R&D4 ha dovuto risolvere un problema semplice in apparenza: rappresentare livelli che scorrono orizzontalmente con tile, nemici, item, il tutto in un budget ROM ultra-stretto.

La soluzione, è una separazione in due strati di dati completamente indipendenti:

Il tile layout (la mappa del livello)

Ogni livello è definito da un puntatore verso una struttura di tile compressa in ROM. La compressione è rudimentale ma geniale: un byte "controllo" seguito da 1-3 byte di dati.

Il formato tile usa un sistema di run (tipo RLE):

; Formato tile SMB1 (semplificato)
; Ogni "comando" è un byte controllo:
;
; $00-$7F : mette una tile, avanza di 1 colonna
; $80-$BF : mette una tile ripetuta N volte (N = byte - $80 + 1)
; $C0-$FF : comando speciale (fine riga, salto, cambio palette)

Esempio: per disegnare 3 mattoni consecutivi:
  $82 $01    ; ripete la tile $01 (brick) 3 volte

Ogni livello contiene 13 righe di 16 colonne di tile (13×16 = 208 tile visibili). Ma il formato compresso permette di scendere molto più in basso: per esempio, il cielo e le colonne vuote non prendono quasi spazio.

Il ciclo di rendering in 6502:

; Decompressione tile - ciclo principale
; Ingresso: puntatore tile_data in $XX
; Uscita: tilemap livello nella RAM PPU

DecompressTile:
  lda (tile_ptr),y      ; leggi byte controllo
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F : tile unica
  cmp #$C0
  bcc RunLength         ; $80-$BF : run-length
  jmp SpecialCommand    ; $C0-$FF : comando speciale

SingleTile:
  sta PPU_DATA          ; scrivi la tile direttamente
  jmp Next

RunLength:
  sec
  sbc #$7E              ; N = control - $7E
  tax
  lda (tile_ptr),y      ; leggi la tile da ripetere
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

Lo sprite layout (nemici e oggetti)

In parallelo, nemici e oggetti (blocchi ?, tubi, goomba, koopa) sono salvati in una struttura completamente separata. Ogni spawn è definito da 2 byte:

; Formato sprite SMB1
; Byte 0: posizione X (in colonne)
; Byte 1: tipo di sprite + bit di pagina Y
; Y è derivato dall'indice nella sequenza

Una sequenza di sprite:
  $01 $4B    ; goomba alla colonna 1
  $09 $4B    ; goomba alla colonna 9
  $10 $61    ; blocco ? alla colonna 16 (contiene moneta)
  $15 $54    ; koopa verde alla colonna 21
  $FF        ; fine sequenza

Ogni livello può referenziare fino a 5 pagine di sprite diverse (infine, 5 "screen" di 16 colonne), ma in pratica quasi tutti i livelli ne usano solo 2-3.

La tabella dei puntatori

Il genio del design, è la tabella di puntatori. Ogni livello è salvato come una coppia di indirizzi ROM:

// Struttura interna (semplificata) della World Map
// coppia di puntatori ROM: tile + sprite
struct LevelPointer {
    uint16_t tile_ptr;   // Indirizzo ROM dei dati tile
    uint16_t sprite_ptr; // Indirizzo ROM dei dati sprite
};

// 4 tabelle separate, una per AreaType:
// 0 = Water, 1 = Overworld, 2 = Underground, 3 = Castle
LevelPointer level_table[4][128];

128 voci per tabella. 4 tipi di zona. 512 combinazioni possibili, ma solo una frazione è usata dal gioco ufficiale. Il resto, è RAM non inizializzata o dati interpretati come puntatori.

Quando il gioco carica un livello, fa questo:

; Caricamento di un livello
; A = AreaType (0-3), X = LevelID (0-127)

LoadLevel:
  sta AREA_TYPE
  asl                  ; *2 per offset in tabella 16-bit
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; puntatore verso le tile
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; puntatore verso gli sprite
  jsr DecompressTiles

Il gioco accorda una fiducia totale ai puntatori: legge l'indirizzo nella tabella e decomprime ciò che si trova a quell'indirizzo, punto finale.

Level ID $06 (Water) -- 9-1, la version sous-marine de 6-2

La table des Level IDs : 128 entrées possibles, 34 assignées

L'ordre différent des pointeurs tiles et sprites -- la cause des Frankenstein levels

I 34 livelli unici e il sistema di ID a 7 bit

Le chip RAM de la NES (MB8416A) -- c'est lui qui conserve les données quand on swap les cartouches

SMB1 non ha 32 livelli, ma 34 livelli unici. Molti livelli sono doppioni (5-3 = 1-3 ma con Bullet Bill) segnati da un flag "hard mode". I veri livelli unici:

  • Acqua (Tipo 0): 3 livelli (2-2, 7-2, zona bonus 5-2/6-2)
  • Overworld (Tipo 1): 22 livelli (di cui le 2 stanze nuvole bonus)
  • Underground (Tipo 2): 3 livelli (di cui le stanze bonus sotterranee)
  • Castle (Tipo 3): 6 livelli
  • + 1 stanza cutscene (prima dei livelli sotterranei/acqua)
  • + 1 warp zone di 4-2

Ogni livello ha un ID su 7 bit. I 5 bit di peso debole = numero nel sottogruppo, i 2 bit di peso forte = tipo di zona:

; Codifica a 7 bit del Level ID
; Bit 6-5: Tipo (00=Water, 01=Overworld, 10=Underground, 11=Castle)
; Bit 4-0: Numero nel sottogruppo
;
; Water IDs      : $00-$02  (tipi 00, numeri 0-2)
; Overworld IDs  : $20-$35  (tipi 01, numeri 0-21)
; Underground IDs: $40-$42  (tipi 10, numeri 0-2)
; Castle IDs     : $60-$65  (tipi 11, numeri 0-5)
;
; ID $25 = %0100101 → tipo 01 (Overworld), numero 5 → 1-1
; ID $23 = %0100011 → tipo 01 (Overworld), numero 3 → 6-2

128 ID possibili ($00-$7F), solo 34 assegnati a veri livelli. Gli ID inutilizzati puntano a qualsiasi cosa.

Le tabelle di puntatori: due liste, due ordini

I puntatori tile e sprite non sono salvati nello stesso ordine. Il codice usa due liste 16-bit separate (high byte / low byte in due tabelle distinte):

Ordine dei puntatori sprite:
  Indice 0-5   : Castle (6 livelli)
  Indice 6-27  : Overworld (22 livelli)
  Indice 28-30 : Underground (3 livelli)
  Indice 31-33 : Water (3 livelli)

Ordine dei puntatori tile:
  Indice 0-2   : Water (3 livelli)
  Indice 3-24  : Overworld (22 livelli)
  Indice 25-27 : Underground (3 livelli)
  Indice 28-33 : Castle (6 livelli)

Perché ordini diversi? Nessuna ragione tecnica: è probabilmente così che i dati sono stati organizzati durante lo sviluppo. Ma crea una conseguenza affascinante: quando un ID di livello è invalido, i puntatori tile e sprite caricano livelli diversi, creando Frankenstein level.

Per navigare tra queste due liste, il gioco usa piccole tabelle di offset (come un indice):

; Tabelle di offset per tipo (Water, Overworld, Underground, Castle)
; Ogni voce = indice di inizio nella lista corrispondente

SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; Water=31, Overworld=6, Underground=28, Castle=0

TileOffsetTable:
  .byte $00, $03, $19, $1C    ; Water=0, Overworld=3, Underground=25, Castle=28

Per caricare il livello 6-2 (ID $23, Overworld numero 3):

; 1. Tipo = 01 (Overworld) → indice in tabella offset = 1
; 2. Sprite offset = SpriteOffsetTable[1] = 6
;    Indice finale = 6 + 3 (numero di livello) = 9 → 10° puntatore sprite
; 3. Tile offset = TileOffsetTable[1] = 3
;    Indice finale = 3 + 3 = 6 → 7° puntatore tile
; 4. Risultato: puntatore tile $A619 + puntatore sprite $9ED0 = 6-2 ok

Ora, cosa succede con un ID invalido come $43 (Underground numero 3, che non esiste)?

; ID $43, Tipo = 10 (Underground), numero = 3
; Sprite offset = SpriteOffsetTable[2] = $1C = 28
;   Indice = 28 + 3 = 31 → 32° puntatore sprite = acqua bonus 5-2!
; Tile offset = TileOffsetTable[2] = $19 = 25
;   Indice = 25 + 3 = 28 → 29° puntatore tile = 1-4 (Castle)!
;
; Risultato: un livello sotterraneo con le tile di 1-4
; e i Blooper della zona acqua di 5-2. Un vero Frankenstein.

Level ID $43 -- Frankenstein level : tiles 1-4 + sprites eau 5-2

Exploring Glitch Level Pointers -- les tables d'offset expliquées

Le world index table -- quand l'overflow de world 9 crée un glitch level

La world index table: perché world 9 va in overflow

C'è una tabella ROM di 8 byte che dà l'indice del primo livello di ogni mondo (1-8). E subito dopo, la tabella dei 36 Level ID di tutti i livelli in ordine di gioco.

; WorldIndexTable (8 byte)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   -> Mondo 1 inizia al livello 0
;   -> Mondo 2 inizia al livello 5
;   -> Mondo 8 inizia al livello 33

; LevelIDTable (36 byte)
  .byte $25, $28, $29, $26, $24, ... ; i 36 Level ID

Quando proviamo a caricare world 9, il gioco legge il 9° byte di WorldIndexTable... che non esiste. Va in overflow di 1 byte in LevelIDTable, legge il valore $25, poi usa $25 come indice in LevelIDTable (37ª voce): che va di nuovo in overflow di 2 byte in SpriteOffsetTable, e legge il valore 6.

; World 9:
;   1. WorldIndexTable[8] (overflow) → legge $25 in LevelIDTable
;   2. LevelIDTable[37] (overflow) → legge il 2° byte di SpriteOffsetTable = 6
;   3. ID = 6 → Water level number 6 (che non esiste)
;   4. Tile pointer = puntatore water numero 6 = tile di 6-2
;   5. Sprite pointer = indice 31+6 = 37 > 33 → puntatore invalido
;   6. Risultato: 6-2 sott'acqua con sprite glitchati
;      → world 9-1!

Per world G (16), l'overflow va ancora più lontano e cade sul Level ID $01, che è il livello cutscene che precede 1-2:

; World G (16):
;   WorldIndexTable[15] → legge $01 in LevelIDTable
;   LevelIDTable[1] = $29 (cutscene 1-2)
;   → world G-1 = la cutscene di ingresso di 1-2

Perché i glitch world esistono

Il gioco ha 32 livelli "legittimi" (8 mondi × 4 livelli). Ma la tabella di puntatori fa 128 voci per tipo di zona. Le voci oltre il livello 32 contengono ciò che si trova in ROM a quegli indirizzi: a volte un altro livello, a volte dati sonori, a volte RAM, a volte qualsiasi cosa.

Level ID $01 Water (Minus World) -- tile pointer $AE45, sprite pointer $A171

Level ID $01 + AreaType 0 = Minus World

Il più famoso dei glitch world. Il Level ID $01 in AreaType 0 (acqua) punta verso:

  • Tile pointer: $AE45 → la zona sottomarina di 2-2/7-2
  • Sprite pointer: $A171 → gli sprite di 2-2/7-2

Il risultato: un livello di acqua che assomiglia a 2-2, ma che va in loop all'infinito perché il flagpole non esiste. Il livello gira in loop per sempre.

È il livello 36-1 (o 36-1 nel mondo $-1).

Le warm start check de SMB1 -- c'est lui qui permet au Minus World d'exister

; Perché manca il flagpole nel Minus World:
; Gli sprite di 2-2/7-2 ($A171) non hanno flagpole
; nella loro sequenza. Il gioco cerca lo sprite $FD (flagpole)
; ma non lo trova mai → loop infinito
;
; Il gioco continua a generare il livello all'infinito
; finché il timer arriva a zero.

I puntatori che puntano verso la RAM

Quando il tile pointer o lo sprite pointer punta verso un indirizzo in RAM ($00-$7F) invece che in ROM, il gioco prova a interpretare i cambiamenti costanti della RAM come tile:

; Esempio: Level ID $03 in Water
; Tile Pointer: $A46B (3-3 - valido)
; Sprite Pointer: $009D (punta verso la RAM pagina zero!)
;
; La RAM pagina zero contiene i registri del gioco,
; la posizione di Mario, lo stato dei contatori...
; Il gioco decomprime questo come una sequenza di sprite,
; e il risultato è un livello con nemici
; che sono in realtà valori di registri.

Quando la pagina zero cambia (perché Mario si muove, il timer gira, ecc.), gli "sprite" del livello cambiano anche. Ecco perché certi glitch world hanno nemici che lampeggiano e si trasformano di continuo.

Level ID $03 Water -- sprite pointer $009D pointe vers la RAM, niveau injouable

Level ID $36: il livello vuoto (Overworld)

Level ID $36 in Overworld:

  • Tile pointer: $AC35 (1-2)
  • Sprite pointer: $A0D8 (1-2)

Risultato: niente. Il gioco carica il livello ma è segnato "senza livello" nel catalogo di RGMechEx. Le tile sono forse valide ma gli sprite puntano verso un posto che produce un livello vuoto o non funzionante.

Level ID $1D (Castle): il campione dei crash

Level ID $1D in Castle:

  • Tile pointer: $A210 (4-4)
  • Sprite pointer: $7EA0 (RAM!)

Sprite pointer in RAM = sprite indefiniti. Il gioco prova a mostrare una palla Spiny o un cannone Bullet Bill nella prima riga di tile. Crash subito.

; Quando lo sprite pointer punta verso la RAM,
; il gioco decomprime byte che cambiano sempre
; come istruzioni "spawn". Il risultato:
; - Apparizione di oggetti inesistenti (valore undefined)
; - Crash PPU quando lo sprite NES prova a mostrare una tile invalida
; - Freeze completo della console

I 256 glitch world catalogati

RGMechEx ha scritto uno script che genera le mappe di tutti i livelli, per i 4 tipi di zona, e i 128 ID ciascuno.

Il contatore di mondo è su 8 bit (0-255). I mondi 1-8 sono legittimi. Restano 248 glitch world potenziali. Ogni glitch world corrisponde al primo livello di questo mondo, e il suo Level ID è calcolato col meccanismo di overflow della WorldIndexTable.

Table des glitch worlds -- 248 mondes corrompus, 68 premiers niveaux accessibles

Sui 128 ID possibili, solo 68 sono "first level" di un mondo (accessibili via il glitch world number). Gli altri 60 sono livelli 2+ o inaccessibili.

Tipo ID unici giocabili ID che crashano ID vuoti
Water (0) circa 20 circa 60 circa 48
Overworld (1) circa 30 circa 55 circa 43
Underground (2) circa 15 circa 65 circa 48
Castle (3) circa 25 circa 58 circa 45

Molti ID portano allo stesso livello per via dei puntatori che cadono sugli stessi indirizzi ROM. Il Level ID $28 (Overworld) per esempio: tile pointer $A7CD (2-1): appare in 38 glitch world diversi, perché il suo sprite pointer $9F51 punta verso una zona della ROM usata come padding/dati sonori riusata da un sacco di ID.

Carte du niveau ID $28 (Overworld) -- 2-1 tiles avec des sprites normaux, 38 glitch worlds

Super Mario Bros. Glitch Levels Explained -- la 3ème vidéo

I 6 glitch level davvero unici

Tra i 19 ID di glitch level accessibili, solo 6 non crashano subito al caricamento:

World Level ID Descrizione
E-1 (224) $50 Un solo blocco ? sopra un baratro. Mario muore istantaneamente.
W $57 Mario spawna bloccato, incapace di muoversi.
42 (133) $50 Tunnel di nuvole che intrappola Mario se va abbastanza lontano.
62 (131, 240) $4D Castello gelato: Mario spawna in alto, non può cadere → bloccato.
127 $4B Tunnel sotterraneo, ma crasha se si va troppo lontano.
137 $4B Attiva lo scorrimento automatico delle cutscene. Mario incontra un unico brick block che lo blocca per sempre.

Level ID $50 (cloud tunnel) -- le glitch world 42-1 et E-1 Level ID $4D (castle) -- world 62-1, Mario bloqué au spawn Level ID $4B (tunnel) -- world 127-1, crashe si on va trop loin

Sei glitch world su 248 che producono qualcosa di davvero nuovo. Il resto, sono livelli normali col tipo di zona sbagliato, o schermi neri.

Il formato dei livelli in dettaglio

Focus sul formato esatto dei dati di livello, per capire perché i glitch level stanno in piedi (o no).

L'header livello: 2 byte, 6 proprietà

Ogni livello inizia con un header di 2 byte che controlla 6 proprietà:

; Byte 0: timer + Y start + modifier
;   Bit 7-6: timer (00=invariato, 01=200, 10=300, 11=400)
;   Bit 5-3: Y start Mario (111/110 = autowalk)
;   Bit 2-0: level type modifier
;              000=default, 001=waves, 010=brick wall,
;              011=water bottom, 100=night, 101=snow,
;              110=snow night, 111=gray night

; Byte 1: platform + background + floor pattern
;   Bit 7-6: special platform (00=tree, 01=mushroom,
;                                 10=Bullet Bill, 11=cloud)
;   Bit 5-4: background (00=none, 01=clouds,
;                           10=montains, 11=fences)
;   Bit 3-0: floor pattern iniziale (0-15)

Il type modifier controlla variazioni visive: le onde in alto nei livelli di acqua, lo sfondo mattone di 8-3, la palette notte di 4-3, la neve di 6-2, ecc.

Gli oggetti tile: 2 byte, Next Screen Flag, coda 3 slot

Dopo l'header viene una lista di oggetti tile, ogni oggetto fa 2 byte. Il byte $FD segna la fine della lista.

; Formato oggetto tile (16 bit):
; Byte 0:
;   Bit 7-4: posizione X (colonna 0-15)
;   Bit 3-0: posizione Y
;     Y=0-11  : posizione Y normale
;     Y=12    : oggetti speciali (buchi, ponti, rope, blocchi ?)
;     Y=13    : screen skip / oggetti speciali 2
;     Y=14    : cambio di modifier/scenery/floor
;     Y=15    : oggetti speciali 3 (castello, scale, grosso tubo)

; Byte 1:
;   Bit  7   : NEXT SCREEN FLAG
;   Bit 6-4 : tipo di oggetto (0-7)
;   Bit 3-0 : larghezza/altezza / sottotipo

Quando il bit "next screen" è messo, la colonna di lavoro corrente è incrementata di 1. Permette di piazzare oggetti oltre le prime 16 colonne. Gli oggetti devono essere elencati in ordine (sinistra a destra) perché il gioco li carica in sequenza:

; La routine di caricamento ha DUE fasi per colonna:
; Fase 1: cerca i nuovi oggetti che iniziano su questa colonna
;            e li aggiunge alla coda (queue)
; Fase 2: elabora ogni oggetto nella coda disegnando le tile,
;            e rimuove quelli che finiscono su questa colonna

La coda fa esattamente 3 slot. Conseguenza diretta: non possiamo avere più di 3 oggetti che iniziano sulla stessa colonna. Se la coda è piena, il 4° oggetto è ignorato e non sarà mai caricato.

Ecco perché i livelli ben progettati evitano di impilare troppi oggetti. Esempio in 1-2: la colonna col blocco 1up nel soffitto + i mattoni accanto sono divisi in due oggetti distinti per rispettare il limite di 3.

Posizione Y speciale: 12, 13, 14, 15

Quando Y=12, l'oggetto non ha posizione Y (è hardcodata per tipo):

; Y=12: oggetti senza posizione Y
;   Tipo 0: buco (rimuove il suolo)
;   Tipo 1: rope di piattaforma mobile
;   Tipi 2-4: ponti a Y fissa
;   Tipo 5: buco con acqua/lava
;   Tipi 6-7: file di blocchi ?

Quando Y=13, due sottogruppi. Se il bit 6 del byte 1 è a 1:

; Y=13, bit6=1: oggetti speciali
;   0 = L-pipe (cutscene), 1 = flagpole, 2-3 = ponte/ascia/hamer (fine castello)
;   4 = stop screen, 5 = random enemies, 6 = loop level, 7+ = possibile crash

Se bit6=0, i 5 bit di peso debole codificano uno screen skip (saltare direttamente a uno schermo N, senza passare dal next screen flag uno a uno).

Quando Y=14: stesso principio con bit6=1 per cambiare il type modifier, bit6=0 per cambiare sfondo + floor pattern.

I floor pattern: 16 motivi di suolo

Il suolo dei livelli non è fatto di oggetti individuali. SMB1 usa floor pattern, un motivo di sfondo che si applica a tutte le colonne fino al prossimo cambiamento:

; Floor pattern (4 bit = 16 possibilità)
;   0 = vuoto totale
;   1 = suolo 2 tile alto
;   2 = suolo 1 tile alto
;   3 = suolo + bottom
;   4 = suolo + bottom 2
;   5 = suolo 1/2 tile
;   6 = 3/4 suolo
;   ... fino a 15 = riempito totale (suolo + soffitto)

Ecco perché i buchi sono oggetti: fanno override del floor pattern su una colonna specifica, senza dover cambiare il pattern per tutto il resto.

Il limite dei 256 byte e il repeat

Tutti i dati tile di un livello stanno in 256 byte massimo. Il registro Y del 6502 è usato come indice, e fa 8 bit. Se il gioco arriva alla fine dei dati senza trovare il byte $FD, ricomincia dall'inizio e ripete i 256 byte all'infinito:

; Indice Y = 8 bit → max 256 byte di dati tile
; Se Y va in overflow (255 → 0) senza incontrare $FD → repeat
; Stesso per gli sprite, ma gli oggetti pipe (3 byte)
; spostano la parità dell'indice a ogni caricamento.

Certi glitch level sfruttano questo repeat per generare livelli che durano "all'infinito".

Il sistema di sprite: 2 byte + transizioni pipe

Gli sprite seguono un formato simile, ma senza header e con qualche differenza chiave. Il byte $FF segna la fine della lista.

; Formato sprite (2 byte):
; Byte 0: posizione X (colonna)
; Byte 1:
;   Bit 7: NEXT SCREEN FLAG
;   Bit 6-0: tipo di sprite
;       Alcuni tipi includono: goomba, koopa, Blooper,
;       Bullet Bill, Lakitu, Spiny, piattaforme,
;       comando warp zone, toad/principessa,
;       comandi di spawn di gruppi di nemici

Il bit di peso debole del byte 1 è l'hard level flag: se messo a 1, lo sprite appare solo nei livelli ≥ 5-3. È così che i livelli "hard mode" sono creati.

Posizione Y 15 = screen skip (identico alle tile). Posizione Y 14 = transizione pipe (3 byte):

; Sprite Y=14: transizione pipe/vine (3 byte!)
;   Byte 0: posizione X
;   Byte 1: bit 6-0 = Level ID 7-bit (destinazione)
;   Byte 2: bit 4-0 = screen di destinazione
;            bit 7-5 = world dove questa transizione è valida
;
; Perché un world? Le bonus room sono riusate tra mondi.
; Esempio: la stanza bonus di 1-1 è usata anche da 2-1 e 7-1.
; Questa stanza ha 3 transizioni, una per mondo, perché Mario
; riappaia al posto giusto.

Gli sprite non hanno sistema di coda. L'unico limite è che non ci possono essere più di 4 sprite caricati insieme nella zona di spawn (appena fuori schermo a destra). Oltre, gli sprite sono ignorati.

Come accedere ai glitch world

Ci sono due metodi principali.

Il metodo classico: il wall clip

Il wall clip (passaggio attraverso i muri) permette di uscire dal livello normale e camminare fino alla warzone nascosta. Manipolando il contatore di mondo via la RAM, possiamo caricare qualsiasi Level ID.

La tecnica:

  1. World 1-2: andare nel tubo di fine nascosto
  2. Fare il wall clip sul muro di destra
  3. Camminare nel vuoto fino alla zona warp
  4. Il gioco interpreta i valori come mondi

Ma questo metodo dà accesso solo a una piccola parte dei glitch world.

Il metodo estremo: NES Tennis cart swap

Vedi la sezione "Il warm start" più sopra per il dettaglio completo. In riassunto: il contatore di passi di Tennis scrive sullo stesso byte RAM del mondo di partenza di SMB1, e il rilevamento warm start preserva questo valore.

L'angolo degli smanettoni: il codice per esplorare tutto

Se vuoi esplorare tutti i glitch da solo in un emulatore, puoi patchare il Level ID direttamente:

; Patch per FCEUX / Mesen:
; Indirizzo RAM $075F = Level ID attuale
; Indirizzo RAM $0760 = Area Type (0=Water, 1=Overworld, 2=Underground, 3=Castle)

; Esempio: caricare il Level 57 (0x39) in Overworld
; Nell'emulatore, apri il tracciatore memoria e scrivi:
; $075F = 0x39
; $0760 = 0x01
; Poi entra in un tubo di warp o muori e ricomincia
; → Il gioco carica il livello ID $39 in Overworld

RGMechEx ha pubblicato la lista completa dei 128 livelli × 4 tipi con mappe generate automaticamente su rgmechex.com. Ogni voce mostra il tile pointer, lo sprite pointer, e una mappa visiva del livello.

I livelli più assurdi

Level ID $1F (Water): 15 glitch world in uno

Il tile pointer $A302 (3-4) combinato allo sprite pointer $02A0 dà 15 glitch world diversi (D-1, J-1, Y-1, Z-1, 55-1, 73-1...). Spiegazione: lo sprite pointer punta verso una zona della ROM che contiene dati abbastanza vicini a sprite validi da produrre risultati giocabili, ma la combinazione delle tile di castello 3-4 con sprite di overworld crea un rendering assurdo.

Level ID $28 (Overworld): 38 glitch world = record

Il record assoluto. 38 voci di glitch world puntano verso lo stesso livello (tile 2-1 + sprite $9F51). Perché? Perché lo sprite pointer $9F51 cade in una zona della ROM usata come padding/dati sonori riusata da un sacco di ID.

Level ID $49 (Underground): il livello FDS

Tile pointer $76AE + sprite pointer $1C9D. Il tile pointer punta verso la zona della ROM riservata alla versione Famicom Disk System. Risultato: un livello con tile che non esistono nella cartuccia standard. È il livello che fa apparire il livello 52-1 e 196-1.

Level ID $00-$02: i veri livelli bonus

Questi ID sono usati da sotto-livelli legittimi del gioco:

  • $00: zona sottomarina di 5-2/6-2 (usata da H-1, 39-1)
  • $01: l'acqua di 2-2/7-2 (il Minus World, 36-1)
  • $02: sotto-livello di 8-4 (136-1, 151-1, 215-1)

La differenza tra un livello "bonus" accessibile normalmente e un glitch world, è che le warp zone verificano il mondo attuale:

; Verifica warp zone (semplificato)
; Il gioco verifica che il mondo target sia tra 1 e 8
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; < 1 → rifiutato
  cmp #9
  bcs InvalidWarp       ; > 8 → rifiutato
  ; world valido solo tra 1 e 8
  jmp DoWarp

I glitch world con numeri > 8 o 0 non possono essere raggiunti con tubi normali. Serve il wall clip o il cart swap.

Perché certi livelli crashano: le jump table

Quando il gioco carica un oggetto tile, usa il suo tipo come indice in una jump table:

; Jump table degli oggetti tile standard (tipi 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; tipo 0: blocco ?, hidden, flagpole...
  .word Obj_Platform      ; tipo 1: piattaforma speciale
  .word Obj_BrickRow      ; tipo 2: fila di mattoni
  .word Obj_BlockRow      ; tipo 3: fila di blocchi
  .word Obj_CoinRow       ; tipo 4: fila di monete
  .word Obj_BrickCol      ; tipo 5: colonna di mattoni
  .word Obj_BlockCol      ; tipo 6: colonna di blocchi
  .word Obj_Pipe          ; tipo 7: tubo
  .word Obj_8             ; tipo 8
  .word Obj_9             ; tipo 9
  .word Obj_10            ; tipo 10
  .word Obj_11            ; tipo 11

Les jump tables : pourquoi un type d'objet invalide fait crasher le jeu

Se un oggetto ha un tipo invalido (≥12), il gioco salta a un puntatore che non esiste in questa tabella. 4 esiti possibili:

  1. Puntatore valido → l'oggetto si carica normalmente
  2. Puntatore verso un'altra jump table (sovrapposizione) → appare un oggetto diverso. Esempio: tipo 12 punta verso la tabella Y=13, il che dà un L-pipe.
  3. Puntatore verso eseguibile → esecuzione di codice casuale (probabile crash)
  4. Placeholder esplicito (NOP) → l'oggetto non fa niente (certi sprite sono così, producendo nemici che volano sul posto senza muoversi)

Glitch level ID $58 : le sprite pointer pointe vers une adresse invalide, le jeu crashe

Glitch level ID $50 : le cloud tunnel, un niveau généré par des données corrompues

Il glitch level ID $58 (il tunnel che crasha): il suo sprite pointer punta verso una regione memoria che non esiste su NES senza mapper ROM. Il gioco prova a caricare lo stesso Koopa 5 volte per frame alla posizione (0,0), il che satura la PPU e provoca un freeze.

; Perché ID $58 crasha:
; Sprite pointer → indirizzo invalido (fuori spazio NES standard)
; → Il gioco legge byte indeterminati come tipi di sprite
; → Il Koopa $00 (tipo invalido senza handler) va in loop in chiamata ricorsiva
; → Stack overflow 6502 → freeze

Il paradosso pipe warp

Ricorda il check target_world BETWEEN 1 AND 8. Anche se trovi un tubo in un glitch world, il gioco verifica che il mondo di destinazione sia tra 1 e 8. I glitch world hanno numeri > 8 (36-1, 255-1...), quindi la warp fallisce.

È anche per questo che il Minus World non ha fine: il flagpole non è presente negli sprite, e i tubi non portano da nessuna parte.

Il trick dei 5 oggetti in una colonna

Esiste un edge case che permette di superare il limite di 3 oggetti per colonna. Quando la coda si blocca (slot pieni + oggetto seguente con next screen flag mancante), il gioco "pre-elabora" la colonna corrente in loop finché trova un oggetto con next screen flag. Durante ogni pre-elaborazione:

; Durante la pre-elaborazione di colonna:
; 1. Gli oggetti nella coda vedono la loro larghezza restante
;    decrementata a ogni "falsa avanzata" di colonna
; 2. Se un oggetto arriva a larghezza=0, esce dalla coda
; 3. Uno slot liberato può essere riempito da un nuovo oggetto
;    aggiunto nella stessa colonna

; Risultato: fino a 5 oggetti possono essere elaborati sulla stessa colonna.
; Tecnica: piazzare 2 oggetti che attraversano lo screen boundary
; (slot 1 e 2), 1 oggetto dummy in X < precedente (blocca la coda),
; poi 3 oggetti a X=0 dello schermo seguente (di cui uno con next screen flag).

È ciò che chiamiamo un "queue skip" ed è usato da certi romhacker per creare livelli più densi di ciò che il formato permette normalmente.

Le differenze tra versioni

Famicom Disk System

La versione FDS di SMB1 ha una memory map diversa. Tutti i puntatori di livello sono spostati, ma i dati sono gli stessi. Ciò che cambia: gli indici dei glitch world sono completamente diversi:

FDS World 36 → Level ID $09 (versione acqua di 5-3)
  → Il flagpole è presente! Si può finire il livello.
  → Poi: $27 (7-3 normale) → $44 (4-4 underground)
  → $44 è finibile → l'ascia funziona → fine del gioco!

Il Minus World FDS è quindi un "bonus world" che può portare
al completamento del gioco, al contrario della versione NES.

Il mio livello FDS preferito: ID $5F, una versione sotterranea della seconda metà di 3-3 in tunnel basso (peccato sia un autoscroller).

The Lost Levels (Super Mario Bros. 2 giapponese)

Lost Levels cambia molte cose:

  1. Ordine identico tile/sprite: niente più Frankenstein level (tile e sprite caricano lo stesso livello anche con un ID invalido)
  2. Una sola tabella di puntatori 16-bit invece di due tabelle separate high/low
  3. 4 file disco: la ROM è stata divisa per l'FDS:
    • File 1: world 1-4
    • File 2: world 5-8
    • File 3: world 9 + sound engine
    • File 4: world A-D (tabella di puntatori completamente diversa)
  4. Stesso Level ID = 4 livelli possibili secondo il file caricato
  5. Niente più glitch Tennis: l'opzione continue (continue allo stesso mondo dopo game over) rende il warm start inutile, e il gioco fa reset subito se world > 9
  6. Nuovi oggetti: fungo veleno, blocco invisibile, blocco invisibile fire flower, tubi rovesciati, vento: ma inseriti in mezzo alle liste esistenti → incompatibilità backward con SMB1
  7. Piranha Plant sempre rosse dopo world 4, springboard verdi solo in world 2/B/3/C/7

Super Mario All-Stars (SNES)

Port diretto con le stesse routine 6502 (il SNES esegue il codice NES in modalità compatibile):

  • Warp zone fissata: niente più Minus World (entrare nel tubo sinistro prima del testo porta al mondo giusto)
  • Crash: quasi tutti i glitch level crashano (tranne ID $6A e 9-1)
  • Oggetti castello aggiunti: resi più unici
  • Ma: il 4-2 wrong warp funziona ancora (non patchato!)

Il 4-2 wrong warp: un bug di piazzamento oggetto

In 4-2, ci sono due oggetti di transizione pipe: la vite (warp zone) e il tubo (stanza monete). Il primo oggetto di transizione (quello della vite) è piazzato ben prima che la vite appaia sullo schermo. Il secondo (il tubo) è piazzato troppo tardi nel livello.

; Timing delle transizioni in 4-2:
; Oggetto transizione 1 (vite → warp zone): piazzato 3 schermi prima della vite
; Oggetto transizione 2 (tubo → stanza monete): piazzato 1 schermo dopo il tubo
;
; Normalmente il primo oggetto è disattivato prima che Mario
; raggiunga il tubo. Ma se Mario va veloce (o usa
; la scorciatoia del blocco B+right), la transizione della vite
; è ancora attiva quando tocca il tubo!
; → Il gioco carica la warp zone invece della stanza monete.
;
; Se l'oggetto fosse stato piazzato subito dopo la vite ma prima
; del tubo, il bug non esisterebbe.

I livelli a loop

Come funzionano i loop (8-4, 7-4)? Il livello ha checkpoint con numeri di schermo e posizioni Y hardcodate:

; Checkpoint: {numero_schermo, posizione_verticale}
; Se Mario passa questo checkpoint alla giusta altezza → il livello continua
; Altrimenti → warp back di 4 schermi (64 blocchi)
;
; Per fare un loop infinito: posizione_verticale = $F0
; (sotto il fondo dello schermo) → impossibile da validare.
;
; I checkpoint sono semplici (un solo flag) tranne per world 7
; che usa triplette (3 flag, bisogna fallirne almeno 1)
;
; Il warp back è rude: offset dei dati tile regolato a un valore
; hardcodato, offset dei dati sprite rimesso a 0. I nemici presenti
; sono scaricati istantaneamente → le firebar spariscono.

Cambiare il formato, non il codice

Una delle lezioni più affascinanti di questa architettura, è che gli sviluppatori di SMB1 sono riusciti a creare un sistema di livelli molto espressivo senza mai toccare il codice di rendering 6502. Tutta la variazione tra i livelli viene dai dati (puntatori, oggetti, sprite, floor pattern), non dal codice.

I 256 glitch world esistono perché le tabelle di puntatori sono dimensionate per 128 voci × 4 tipi, e il gioco non valida mai i valori che legge. Quando un puntatore cade in RAM, il gioco interpreta i registri di Mario come tile. Quando un puntatore cade nei dati sonori, il gioco suona musica sotto forma di level design. E quando le jump table vanno in overflow, il gioco esegue qualsiasi cosa fino al crash.

More Super Mario Bros. Mechanics Explained -- la 4ème vidéo

Cosa possiamo imparare da tutto questo

  1. Separazione tile/sprite: indipendenza totale dei due strati, con ordini di storage diversi che creano Frankenstein level unici
  2. Compressione RLE + sistema di oggetti: i livelli non sono bitmap ma liste di oggetti piazzati, con floor pattern per il suolo
  3. Coda 3 slot: limite stretto dell'hardware (e del design di livelli)
  4. Fiducia totale ai puntatori: il gioco si fida di puntatori e jump table, il che produce o glitch giocabili, o crash
  5. 256 byte max: il limite del registro Y 6502, che fa sì che i dati si ripetano se si va troppo oltre
  6. Warm start / cold start: un sistema di "continua" che ha aperto la porta al cart swap Tennis → Mario

Il più bello: tutto questo, è codice 6502 che sta in 40KB. Il codice dialoga direttamente con l'hardware e accorda una fiducia totale a ogni accesso memoria. Se il puntatore è marcio, il gioco crasha. E i crash, li chiamiamo glitch world.

Le 3 cose da trattenere

  1. I glitch world sono puntatori caduti male: il gioco ha 128 ID × 4 tipi di zona, ma solo 34 livelli unici. Quando il world number è corrotto (da Tennis o wall clip), il gioco carica un puntatore pensato per un altro livello, e le 512 combinazioni possibili producono risultati imprevedibili.

  2. Il Minus World è un bug di warp combinato a corruzione: il tubo sinistro in 1-2, se attivato prima che il testo appaia, carica world 36 (0x24). Questo world punta verso Level ID $01 (acqua di 2-2), un livello senza flagpole. E dato che non c'è transizione pipe per world 36, il livello va in loop all'infinito. L'assenza di verifica crea l'icona.

  3. Tennis → Mario, 15 anni prima di OoT → Paper Mario: la RAM del NES sopravvive a uno swap di cartuccia grazie ai condensatori e al sistema di warm start / cold start di SMB1. Il contatore di passi di Tennis (che incrementa un byte RAM suonando il suono dei passi) cade esattamente sull'indirizzo del world number. Serve che le cifre del top score restino a 0, che il byte $A5 sia intatto, e che il gioco rilevi un warm start: un concorso di circostanze perfetto funzionato solo con Tennis.

I video originali di Retro Game Mechanics Explained sono un lavoro da formiche pazzesco: il livello di dettaglio sulla disassemblata 6502, le mappe automatiche di tutti i livelli, le spiegazioni del cart swap e del warm start. Se non hai visto la serie, guardala, è corta e ogni minuto è denso.

Il codice sorgente delle mappe è disponibile su rgmechex.com, e la disassemblata completa di SMB1 è open source su un sacco di repo. 40 anni fa, programmatori giapponesi hanno scritto questo sistema di livelli in 6502 con zero test unitari e zero bug tracker, e continuiamo a imparare tecniche di ottimizzazione aprendo il loro codice oggi.

Super Mario Bros.: das Levelformat, die Pointer und die 256 Glitch Worlds

Wie 128 Level × 4 Zonentypen in 40KB ROM passen, warum das Minus World existiert, und wie ein NES-Tennis-Match Glitch Worlds laden kann.

Einleitung

Super Mario Bros. sind 40 Kilobyte ROM. Acht Welten, 32 Level, Gegner, Musik, Power-Ups, alles passt da hinein.

Aber wenn du einen Emulator öffnest und an den richtigen Bytes fummelst, kannst du Level 36-1 laden. Oder 255-1. Oder in einer Welt landen, wo alles aus Bowser-Sprites und Röhren besteht, die nirgendwohin führen.

Diese Glitch Worlds existieren aus einem simplen Grund: SMB1s Levelspeichersystem ist ein Wunderwerk 8-bit-Optimierung, und wenn man das Spiel zwingt zu lesen, wo es nicht soll, gibt das faszinierende Ergebnisse.

Retro Game Mechanics Explained hat eine 4-Video-Serie darüber gemacht, wir kompilieren sie zu einem einzigen Tauchgang in den 6502-Code des meistverkauften Spiels seiner Zeit.

GLITCH OBJECTS, der Serientitel von RGMechEx über SMB1s versteckte Mechaniken

World 9-1, der Titelscreen der ersten via Tennis-Cart-Swap erreichbaren Glitch World

Der Warm Start: warum Tennis-RAM in SMB1 überlebt

Bevor wir über Levelspeicherung reden, muss man verstehen, wie SMB1 startet. Denn der NES-Tennis-Cart-Swap-Glitch beruht vollständig auf SMB1s Warm-Start-/Cold-Start-Erkennungssystem.

Die 41 erhaltenen Bytes

Wenn SMB1 einen Cold Start erkennt (erstes Einschalten oder Power off/on), löscht es den ganzen RAM. Wenn es einen Warm Start erkennt (Reset-Button, keine Stromunterbrechung), erhält es eine 41-Byte-Speicherzone:

; Die 41 erhaltenen RAM-Bytes bei Warm Start
; Adressen $075F-$0787
;
; $075F : Startbyte (world - 1)    [1 byte]
; $0760 : Weltwahl-Flag (B-Button) [1 byte]
; $0761-$0762 : ungenutzt                    [2 bytes]
; $0763-$0768 : Timer (6 Stellen, 3 angezeigt) [6 bytes]
; $0769-$076E : Luigi-Münzen                   [6 bytes]
; $076F-$0774 : Mario-Münzen                   [6 bytes]
; $0775-$077A : Luigi-Score                   [6 bytes]
; $077B-$0780 : Mario-Score                   [6 bytes]
; $0781-$0786 : Top Score (6 Stellen, 1 versteckt) [6 bytes]
; $0787 : das magische Byte $A5                 [1 byte]

Diese 41 Bytes dienen einer einzigen Funktion: Dem Spieler erlauben, nach einem Game Over in derselben Welt weiterzumachen. Wenn du in 6-3 stirbst, schreibt das Spiel Welt 6 ins Startbyte, und am Titelscreen startest du mit gehaltenem A + Start in 6-1 neu.

Die 41 erhaltenen RAM-Bytes bei Warm Start, TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD und das magische $A5-Byte

Die doppelte Warm-Start-Prüfung

Cold Start vs Warm Start, das Reset-Erkennungsdiagramm

Wenn SMB1 bootet, prüft es nicht ein einziges Kriterium, sondern zwei:

CheckWarmStart:
  ; 1. Magisches Byte $A5 bei $0787 prüfen
  lda $0787
  cmp #$A5
  bne ColdStart        ; kein $A5 → Cold Start

  ; 2. Die 6 Stellen des Top Score ($0781-$0786) prüfen
  ;    Jede Stelle muss zwischen 0 und 9 liegen
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; Stelle >= 10 → Cold Start
  inx
  cpx #6
  bne CheckLoop

  ; Wenn beide Bedingungen passen → Warm Start
  ; RAM wird nicht gelöscht, Startwelt wird erhalten
  jmp WarmStartBoot

Die $A5-Byte- und Top-Score-Stellen-Prüfung, das Herz des Warm Starts

Warum eine doppelte Prüfung? Weil das $A5-Byte zufällig vorhanden sein könnte (ein anderes Spiel, das diesen Wert hinterlässt, oder der Default-Ruhezustand des RAM-Chips). Indem man prüft, dass die Top-Score-Stellen gültig sind (0-9), stellt man sicher, dass die Daten kohärent sind.

Warum Tennis das einzige Spiel ist, das funktioniert

Wenn man SMB1 zum ersten Mal einlegt (Cold Start), macht das Spiel:

  1. Löscht den ganzen RAM → Top Score = 0, World-Byte = 0
  2. Schreibt $A5 an Adresse $0787

Danach swapt man auf Tennis, ohne die Konsole auszuschalten. Tennis:

  • Räumt den RAM beim Start nicht auf (wenige NES-Spiele tun das)
  • Schreibt nicht auf die Top-Score-Bytes → sie bleiben 0 (gültig)
  • Fasst das $A5-Byte nicht an → es bleibt vorhanden
  • Nutzt Adresse $075F für den Spielerschrittzähler
; Das Footstep-Increment in Tennis :
; Bei jedem Spielerschritt auf dem Court inkrementiert Tennis das Byte bei $075F.
; Dasselbe Byte nutzt SMB1 als "World Number - 1".
;
; 0 Schritte  → Welt 0 → SMB1 = Welt 1
; 1-7 Schritte → Welt 1-7 → normale Welten
; 8+ Schritte → Welt 8+ → Glitch Worlds !
;
; Der Zähler inkrementiert nur wenn die Musik stoppt
; (die Footstep-Sounds spielen nicht während der Musik).

Wenn man SMB1 zurücklegt:

  1. Das $A5-Byte ist immer noch da (Tennis hat es nicht angefasst)
  2. Die Top-Score-Stellen sind immer noch 0 (gültig)
  3. Das World-Byte ist jetzt 8+ (von Tennis-Schritten inkrementiert)
  4. SMB1 erkennt einen Warm Start → erhält das korrupte World-Byte
  5. A + Start halten → Welt 9-1, Welt A-1, Welt 36-1 usw.

Warum man Mario vor Tennis booten muss

Eine Subtilität: Man muss zuerst SMB1 booten, dann Tennis, dann wieder SMB1. Würde man direkt mit Tennis anfangen, würde das $A5-Byte nie geschrieben (Tennis schreibt kein $A5), also würde die Warm-Start-Erkennung scheitern und der RAM gelöscht.

Der Tennis-Schrittzähler: Jeder Footstep inkrementiert das World-Byte

Access Glitch Worlds via NES Tennis, das Video, das den Cart Swap erklärt

Wie SMB1 seine Level in 40KB speichert

Nintendo R&D4 musste ein scheinbar simples Problem lösen: Horizontal scrollende Level mit Tiles, Gegnern, Items darstellen, alles in ultra-knappem ROM-Budget.

Die Lösung ist eine Trennung in zwei komplett unabhängige Datenschichten:

Das Tile-Layout (die Levelkarte)

Jedes Level ist definiert durch einen Pointer auf eine in ROM komprimierte Tilestruktur. Die Kompression ist rudimentär aber genial: Ein "Kontroll"-Byte gefolgt von 1-3 Datenbytes.

Das Tileformat nutzt ein Run-System (RLE-artig):

; SMB1-Tileformat (vereinfacht)
; Jeder "Befehl" ist ein Kontrollbyte :
;
; $00-$7F : setzt eine Tile, rückt 1 Spalte vor
; $80-$BF : setzt eine N-mal wiederholte Tile (N = Byte - $80 + 1)
; $C0-$FF : Spezialbefehl (Zeilenende, Sprung, Palettenwechsel)

Beispiel : um 3 aufeinanderfolgende Bricks zu zeichnen :
  $82 $01    ; wiederholt Tile $01 (Brick) 3-mal

Jedes Level enthält 13 Zeilen zu 16 Tile-Spalten (13×16 = 208 sichtbare Tiles). Aber das komprimierte Format erlaubt, viel tiefer zu gehen, zum Beispiel nehmen Himmel und leere Spalten fast keinen Platz ein.

Die 6502-Render-Schleife:

; Tile-Dekompression, Hauptschleife
; Input : tile_data-Pointer in $XX
; Output : Level-Tilemap im PPU-RAM

DecompressTile:
  lda (tile_ptr),y      ; Kontrollbyte lesen
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F : einzelne Tile
  cmp #$C0
  bcc RunLength         ; $80-$BF : Run-Length
  jmp SpecialCommand    ; $C0-$FF : Spezialbefehl

SingleTile:
  sta PPU_DATA          ; Tile direkt schreiben
  jmp Next

RunLength:
  sec
  sbc #$7E              ; N = Control - $7E
  tax
  lda (tile_ptr),y      ; zu wiederholende Tile lesen
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

Das Sprite-Layout (Gegner und Objekte)

Parallel dazu werden Gegner und Objekte (?-Blöcke, Röhren, Goombas, Koopas) in einer komplett separaten Struktur gespeichert. Jeder Spawn ist definiert durch 2 Bytes:

; SMB1-Spriteformat
; Byte 0 : X-Position (in Spalten)
; Byte 1 : Spritetyp + Y-Seitenbits
; Y wird vom Index in der Sequenz abgeleitet

Eine Sprite-Sequenz :
  $01 $4B    ; Goomba in Spalte 1
  $09 $4B    ; Goomba in Spalte 9
  $10 $61    ; ?-Block in Spalte 16 (enthält Münze)
  $15 $54    ; grüner Koopa in Spalte 21
  $FF        ; Sequenzende

Jedes Level kann bis zu 5 verschiedene Sprite-Seiten referenzieren (genau, 5 "Screens" zu 16 Spalten), aber in der Praxis nutzen die meisten Level nur 2-3.

Die Pointer-Tabelle

Die Genialität des Designs ist die Pointer-Tabelle. Jedes Level wird als Paar von ROM-Adressen gespeichert:

// Interne Struktur (vereinfacht) der World Map
struct LevelPointer {
    uint16_t tile_ptr;   // ROM-Adresse der Tile-Daten
    uint16_t sprite_ptr; // ROM-Adresse der Sprite-Daten
};

// 4 separate Tabellen, eine pro AreaType :
// 0 = Water, 1 = Overworld, 2 = Underground, 3 = Castle
LevelPointer level_table[4][128];

128 Einträge pro Tabelle. 4 Zonentypen. 512 mögliche Kombinationen, aber nur ein Bruchteil wird vom offiziellen Spiel genutzt. Der Rest ist nicht initialisierter RAM oder Daten, die als Pointer interpretiert werden.

Wenn das Spiel ein Level lädt, macht es das:

; Laden eines Levels
; A = AreaType (0-3), X = LevelID (0-127)

LoadLevel:
  sta AREA_TYPE
  asl                  ; *2 für Offset in 16-bit Tabelle
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; Pointer auf Tiles
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; Pointer auf Sprites
  jsr DecompressTiles

Das Spiel vertraut den Pointern total: Es liest die Adresse in der Tabelle und dekomprimiert, was an dieser Adresse steht, Punkt.

Level ID $06 (Water), 9-1, die Unterwasserversion von 6-2

Die Level-ID-Tabelle: 128 mögliche Einträge, 34 zugewiesen

Die unterschiedliche Reihenfolge von Tile- und Sprite-Pointern, die Ursache der Frankenstein-Levels

Die 34 einzigartigen Level und das 7-bit-ID-System

Der NES-RAM-Chip (MB8416A), er bewahrt Daten beim Cartridge-Swap

SMB1 hat nicht 32 Level, sondern 34 einzigartige Level. Viele Level sind Duplikate (5-3 = 1-3 aber mit Bullet Bills), markiert mit "Hard Mode"-Flag. Die echten einzigartigen Level:

  • Wasser (Typ 0): 3 Level (2-2, 7-2, Bonuszone 5-2/6-2)
  • Overworld (Typ 1): 22 Level (inkl. der 2 Wolken-Bonusräume)
  • Underground (Typ 2): 3 Level (inkl. der unterirdischen Bonusräume)
  • Castle (Typ 3): 6 Level
  • + 1 Cutscene-Raum (vor Untergrund/Wasser-Levels)
  • + 1 Warp Zone von 4-2

Jedes Level hat eine ID auf 7 Bit. Die 5 niederwertigen Bits = Nummer in der Untergruppe, die 2 hochwertigen Bits = Zonentyp:

; 7-bit Kodierung der Level-ID
; Bits 6-5 : Typ (00=Water, 01=Overworld, 10=Underground, 11=Castle)
; Bits 4-0 : Nummer in der Untergruppe
;
; Water IDs      : $00-$02  (Typen 00, Nummern 0-2)
; Overworld IDs  : $20-$35  (Typen 01, Nummern 0-21)
; Underground IDs: $40-$42  (Typen 10, Nummern 0-2)
; Castle IDs     : $60-$65  (Typen 11, Nummern 0-5)
;
; ID $25 = %0100101 → Typ 01 (Overworld), Nummer 5 → 1-1
; ID $23 = %0100011 → Typ 01 (Overworld), Nummer 3 → 6-2

128 mögliche IDs ($00-$7F), nur 34 echten Levels zugewiesen. Ungenutzte IDs zeigen auf irgendwas.

Die Pointer-Tabellen: zwei Listen, zwei Reihenfolgen

Tile- und Sprite-Pointer sind nicht in derselben Reihenfolge gespeichert. Der Code nutzt zwei separate 16-bit-Listen (High Byte / Low Byte in zwei unterschiedlichen Tabellen):

Sprite-Pointer-Reihenfolge :
  Index 0-5   : Castle (6 Level)
  Index 6-27  : Overworld (22 Level)
  Index 28-30 : Underground (3 Level)
  Index 31-33 : Water (3 Level)

Tile-Pointer-Reihenfolge :
  Index 0-2   : Water (3 Level)
  Index 3-24  : Overworld (22 Level)
  Index 25-27 : Underground (3 Level)
  Index 28-33 : Castle (6 Level)

Warum unterschiedliche Reihenfolgen? Kein technischer Grund, wahrscheinlich wurden die Daten so während der Entwicklung organisiert. Aber es erzeugt eine faszinierende Konsequenz: Wenn eine Level-ID ungültig ist, laden Tile- und Sprite-Pointer verschiedene Level und erzeugen Frankenstein-Levels.

Um zwischen diesen zwei Listen zu navigieren, nutzt das Spiel kleine Offset-Tabellen (wie ein Inhaltsverzeichnis):

; Offset-Tabellen pro Typ (Water, Overworld, Underground, Castle)
; Jeder Eintrag = Startindex in der entsprechenden Liste

SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; Water=31, Overworld=6, Underground=28, Castle=0

TileOffsetTable:
  .byte $00, $03, $19, $1C    ; Water=0, Overworld=3, Underground=25, Castle=28

Um Level 6-2 zu laden (ID $23, Overworld Nummer 3):

; 1. Typ = 01 (Overworld) → Index in Offset-Tabelle = 1
; 2. Sprite-Offset = SpriteOffsetTable[1] = 6
;    Finalindex = 6 + 3 (Levelnummer) = 9 → 10. Sprite-Pointer
; 3. Tile-Offset = TileOffsetTable[1] = 3
;    Finalindex = 3 + 3 = 6 → 7. Tile-Pointer
; 4. Ergebnis : Tile-Pointer $A619 + Sprite-Pointer $9ED0 = 6-2 ✓

Was passiert jetzt mit einer ungültigen ID wie $43 (Underground Nummer 3, existiert nicht)?

; ID $43, Typ = 10 (Underground), Nummer = 3
; Sprite-Offset = SpriteOffsetTable[2] = $1C = 28
;   Index = 28 + 3 = 31 → 32. Sprite-Pointer = Wasserbonus 5-2 !
; Tile-Offset = TileOffsetTable[2] = $19 = 25
;   Index = 25 + 3 = 28 → 29. Tile-Pointer = 1-4 (Castle) !
;
; Ergebnis : ein Untergrundlevel mit 1-4-Tiles
; und Bloopern aus der 5-2-Wasserzone. Ein echter Frankenstein.

Level ID $43, Frankenstein-Level: 1-4-Tiles + 5-2-Wassersprites

Exploring Glitch Level Pointers, die Offset-Tabellen erklärt

Die World-Index-Table, wenn Welt-9-Overflow ein Glitch-Level erzeugt

Die World-Index-Table: warum Welt 9 overflowt

Es gibt eine 8-Byte-ROM-Tabelle, die den Index des ersten Levels jeder Welt (1-8) gibt. Und direkt danach die Tabelle der 36 Level-IDs aller Level in Spielreihenfolge.

; WorldIndexTable (8 bytes)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   -> Welt 1 beginnt bei Level 0
;   -> Welt 2 beginnt bei Level 5
;   -> Welt 8 beginnt bei Level 33

; LevelIDTable (36 bytes)
  .byte $25, $28, $29, $26, $24, ... ; die 36 Level-IDs

Wenn man versucht, Welt 9 zu laden, liest das Spiel das 9. Byte von WorldIndexTable... das nicht existiert. Es overflowt um 1 Byte in LevelIDTable, liest den Wert $25 und nutzt $25 dann als Index in LevelIDTable (37. Eintrag), was erneut um 2 Bytes in SpriteOffsetTable overflowt und den Wert 6 liest.

; Welt 9 :
;   1. WorldIndexTable[8] (Overflow) → liest $25 in LevelIDTable
;   2. LevelIDTable[37] (Overflow) → liest 2. Byte von SpriteOffsetTable = 6
;   3. ID = 6 → Wasserlevel Nummer 6 (existiert nicht)
;   4. Tile-Pointer = Wasser-Pointer Nummer 6 = 6-2-Tiles
;   5. Sprite-Pointer = Index 31+6 = 37 > 33 → ungültiger Pointer
;   6. Ergebnis : 6-2 unter Wasser mit geglitchten Sprites
;      → Welt 9-1 !

Für Welt G (16) geht der Overflow noch weiter und fällt auf Level-ID $01, das Cutscene-Level vor 1-2:

; Welt G (16) :
;   WorldIndexTable[15] → liest $01 in LevelIDTable
;   LevelIDTable[1] = $29 (Cutscene 1-2)
;   → Welt G-1 = die 1-2-Eingangscutscene

Warum Glitch Worlds existieren

Das Spiel hat 32 "legitime" Level (8 Welten × 4 Level). Aber die Pointer-Tabelle macht 128 Einträge pro Zonentyp. Einträge jenseits von Level 32 enthalten, was in ROM an diesen Adressen steht, manchmal ein anderes Level, manchmal Sounddaten, manchmal RAM, manchmal irgendwas.

Level ID $01 Water (Minus World), Tile-Pointer $AE45, Sprite-Pointer $A171

Level ID $01 + AreaType 0 = Minus World

Die berühmteste der Glitch Worlds. Level-ID $01 in AreaType 0 (Wasser) zeigt auf:

  • Tile-Pointer: $AE45 → die 2-2/7-2-Unterwasserzone
  • Sprite-Pointer: $A171 → die 2-2/7-2-Sprites

Das Ergebnis: Ein Wasserlevel, das wie 2-2 aussieht, aber endlos loopt, weil der Flagpole nicht existiert. Das Level läuft ewig im Kreis.

Das ist Level 36-1 (oder 36-1 in Welt $-1).

Der SMB1-Warm-Start-Check, er ermöglicht erst das Minus World

; Warum der Flagpole im Minus World fehlt :
; Die 2-2/7-2-Sprites ($A171) haben keinen Flagpole
; in ihrer Sequenz. Das Spiel sucht Sprite $FD (Flagpole)
; findet es aber nie → Endlosschleife
;
; Das Spiel generiert das Level endlos weiter
; bis der Timer null erreicht.

Pointer, die auf RAM zeigen

Wenn Tile- oder Sprite-Pointer auf eine RAM-Adresse ($00-$7F) statt ROM zeigen, versucht das Spiel, die ständigen RAM-Änderungen als Tiles zu interpretieren:

; Beispiel : Level ID $03 in Water
; Tile-Pointer : $A46B (3-3, gültig)
; Sprite-Pointer : $009D (zeigt auf Zero-Page-RAM !)
;
; Zero-Page-RAM enthält Spielregister,
; Marios Position, Zählerzustände...
; Das Spiel dekomprimiert das als Sprite-Sequenz,
; und das Ergebnis ist ein Level mit Gegnern,
; die in Wahrheit Registerwerte sind.

Wenn sich Zero-Page ändert (weil sich Mario bewegt, der Timer läuft usw.), ändern sich auch die "Sprites" des Levels. Darum haben manche Glitch Worlds blinkende sich ständig transformierende Gegner.

Level ID $03 Water, Sprite-Pointer $009D zeigt auf RAM, unspielbares Level

Level ID $36: das leere Level (Overworld)

Level-ID $36 in Overworld:

  • Tile-Pointer: $AC35 (1-2)
  • Sprite-Pointer: $A0D8 (1-2)

Ergebnis: Nichts. Das Spiel lädt das Level, aber es ist im RGMechEx-Katalog als "ohne Level" markiert. Die Tiles sind vielleicht gültig, aber die Sprites zeigen auf eine Stelle, die ein leeres oder nicht funktionierendes Level produziert.

Level ID $1D (Castle): der Crash-Champion

Level-ID $1D in Castle:

  • Tile-Pointer: $A210 (4-4)
  • Sprite-Pointer: $7EA0 (RAM !)

Sprite-Pointer in RAM = undefinierte Sprites. Das Spiel versucht, einen Spiny Ball oder Bullet-Bill-Blaster in der ersten Tile-Zeile anzuzeigen. Das crasht sofort.

; Wenn der Sprite-Pointer auf RAM zeigt,
; dekomprimiert das Spiel sich ständig ändernde Bytes
; als "Spawn"-Instruktionen. Das Ergebnis :
; - Erscheinen nichtexistenter Objekte (undefinierter Wert)
; - PPU-Crash wenn das NES-Sprite ein ungültiges Tile anzeigen will
; - Kompletter Konsolen-Freeze

Die 256 katalogisierten Glitch Worlds

RGMechEx schrieb ein Skript, das die Maps aller Level generiert, für die 4 Zonentypen und je 128 IDs.

Der Weltzähler ist 8-bit (0-255). Welten 1-8 sind legitim. Es bleiben 248 potenzielle Glitch Worlds. Jede Glitch World entspricht dem ersten Level dieser Welt, und ihre Level-ID wird vom WorldIndexTable-Overflow-Mechanismus berechnet.

Glitch-Worlds-Tabelle, 248 korrupte Welten, 68 erreichbare erste Level

Von 128 möglichen IDs sind nur 68 "First Level" einer Welt (via Glitch-World-Number erreichbar). Die 60 anderen sind Level 2+ oder unerreichbar.

Typ Spielbare einzigartige IDs Crashende IDs Leere IDs
Water (0) ~20 ~60 ~48
Overworld (1) ~30 ~55 ~43
Underground (2) ~15 ~65 ~48
Castle (3) ~25 ~58 ~45

Viele IDs führen zum selben Level, weil Pointer auf dieselben ROM-Adressen fallen. Level-ID $28 (Overworld) zum Beispiel, Tile-Pointer $A7CD (2-1), erscheint in 38 verschiedenen Glitch Worlds, weil sein Sprite-Pointer $9F51 auf eine ROM-Zone zeigt, die als Padding/Sounddaten von vielen IDs wiederverwendet wird.

Karte von Level-ID $28 (Overworld), 2-1-Tiles mit normalen Sprites, 38 Glitch Worlds

Super Mario Bros. Glitch Levels Explained, das 3. Video

Die 6 wirklich einzigartigen Glitch Levels

Unter den 19 erreichbaren Glitch-Level-IDs crashen nur 6 nicht sofort beim Laden:

Welt Level-ID Beschreibung
E-1 (224) $50 Ein einzelner ?-Block über einem Abgrund. Mario stirbt instantan.
W $57 Mario spawnt blockiert, unfähig sich zu bewegen.
42 (133) $50 Wolkentunnel, der Mario fängt, wenn er weit genug geht.
62 (131, 240) $4D Gefrorenes Schloss: Mario spawnt oben, kann nicht fallen → blockiert.
127 $4B Untergrundtunnel, crasht aber wenn man zu weit geht.
137 $4B Aktiviert Cutscene-Autoscroll. Mario trifft einen einzigen Brick-Block, der ihn für immer blockiert.

Level ID $50 (Cloud Tunnel), Glitch World 42-1 und E-1 Level ID $4D (Castle), Welt 62-1, Mario am Spawn blockiert Level ID $4B (Tunnel), Welt 127-1, crasht wenn man zu weit geht

Sechs Glitch Worlds von 248, die etwas wirklich Neues produzieren. Der Rest sind normale Level mit falschem Zonentyp oder schwarze Screens.

Das Levelformat im Detail

Fokus auf das exakte Leveldatenformat, um zu verstehen, warum Glitch Levels stehen (oder nicht).

Der Level-Header: 2 Bytes, 6 Eigenschaften

Jedes Level beginnt mit einem 2-Byte-Header, der 6 Eigenschaften steuert:

; Byte 0 : Timer + Y-Start + Modifier
;   Bits 7-6 : Timer (00=unverändert, 01=200, 10=300, 11=400)
;   Bits 5-3 : Y-Start Mario (111/110 = Autowalk)
;   Bits 2-0 : Leveltyp-Modifier
;              000=default, 001=Wellen, 010=Brickwall,
;              011=Wasserboden, 100=Nacht, 101=Schnee,
;              110=Schneenacht, 111=graue Nacht

; Byte 1 : Plattform + Hintergrund + Bodenmuster
;   Bits 7-6 : Spezialplattform (00=Baum, 01=Pilz,
;                                 10=Bullet Bill, 11=Wolke)
;   Bits 5-4 : Hintergrund (00=keiner, 01=Wolken,
;                           10=Berge, 11=Zäune)
;   Bits 3-0 : initiales Bodenmuster (0-15)

Der Typ-Modifier steuert visuelle Variationen: Die Wellen oben in Wasserlevels, der Brickhintergrund von 8-3, die Nachtpalette von 4-3, der Schnee von 6-2 usw.

Die Tile-Objekte: 2 Bytes, Next-Screen-Flag, 3-Slot-Queue

Nach dem Header kommt eine Liste von Tile-Objekten, jedes Objekt 2 Bytes groß. Byte $FD markiert das Listenende.

; Tile-Objektformat (16 Bit) :
; Byte 0 :
;   Bits 7-4 : X-Position (Spalte 0-15)
;   Bits 3-0 : Y-Position
;     Y=0-11  : normale Y-Position
;     Y=12    : Spezialobjekte (Löcher, Brücken, Seil, ?-Blöcke)
;     Y=13    : Screen Skip / Spezialobjekte 2
;     Y=14    : Modifier/Scenery/Floor-Wechsel
;     Y=15    : Spezialobjekte 3 (Schloss, Treppen, große Röhre)

; Byte 1 :
;   Bit  7   : NEXT-SCREEN-FLAG
;   Bits 6-4 : Objekttyp (0-7)
;   Bits 3-0 : Breite/Höhe / Subtyp

Wenn das "Next Screen"-Bit gesetzt ist, wird die aktuelle Arbeitsspalte um 1 inkrementiert. Das erlaubt, Objekte jenseits der ersten 16 Spalten zu platzieren. Objekte müssen der Reihe nach gelistet sein (links nach rechts), weil das Spiel sie sequenziell lädt:

; Die Laderoutine hat ZWEI Phasen pro Spalte :
; Phase 1 : neue Objekte suchen, die auf dieser Spalte starten
;            und in die Warteschlange (Queue) legen
; Phase 2 : jedes Objekt in der Queue verarbeiten, indem Tiles gezeichnet werden,
;            und die entfernen, die auf dieser Spalte enden

Die Queue macht exakt 3 Slots. Direkte Konsequenz: Man kann nicht mehr als 3 Objekte haben, die auf derselben Spalte starten. Wenn die Queue voll ist, wird das 4. Objekt ignoriert und nie geladen.

Darum vermeiden gut designte Level, zu viele Objekte zu stapeln. Beispiel in 1-2: Die Spalte mit dem 1up-Block in der Decke + den Bricks daneben ist in zwei separate Objekte gesplittet, um das 3er-Limit einzuhalten.

Spezielle Y-Position: 12, 13, 14, 15

Wenn Y=12, hat das Objekt keine Y-Position (sie ist per Typ hardcoded):

; Y=12 : Objekte ohne Y-Position
;   Typ 0 : Loch (entfernt Boden)
;   Typ 1 : Seil mobiler Plattform
;   Typen 2-4 : Brücken auf fixem Y
;   Typ 5 : Loch mit Wasser/Lava
;   Typen 6-7 : Reihen von ?-Blöcken

Wenn Y=13, zwei Untergruppen. Wenn Bit 6 von Byte 1 auf 1 steht:

; Y=13, Bit6=1 : Spezialobjekte
;   0 = L-Pipe (Cutscene), 1 = Flagpole, 2-3 = Brücke/Axt/Hammer (Schlossende)
;   4 = Stop Screen, 5 = Zufallsgegner, 6 = Loop Level, 7+ = möglicher Crash

Wenn Bit6=0, kodieren die 5 niederwertigen Bits einen Screen Skip (direkt zu Screen N springen, ohne Next-Screen-Flag eins nach dem anderen).

Wenn Y=14: Gleiches Prinzip mit Bit6=1 für Typ-Modifier-Wechsel, Bit6=0 für Hintergrund- + Bodenmuster-Wechsel.

Die Bodenmuster: 16 Bodenmotive

Der Levelboden besteht nicht aus Einzelobjekten. SMB1 nutzt Floor Patterns, ein Hintergrundmotiv, das auf alle Spalten bis zum nächsten Wechsel angewendet wird:

; Floor Patterns (4 Bit = 16 Möglichkeiten)
;   0 = total leer
;   1 = 2 Tiles hoher Boden
;   2 = 1 Tile hoher Boden
;   3 = Boden + Unterteil
;   4 = Boden + Unterteil 2
;   5 = 1/2-Tile-Boden
;   6 = 3/4-Boden
;   ... bis 15 = total gefüllt (Boden + Decke)

Darum sind Löcher Objekte: Sie overriden das Floor Pattern auf einer spezifischen Spalte, ohne das Pattern für den Rest ändern zu müssen.

Das 256-Byte-Limit und Repeat

Alle Tile-Daten eines Levels passen in maximal 256 Bytes. Das 6502-Y-Register wird als Index genutzt, und es ist 8-bit. Wenn das Spiel ans Datenende kommt, ohne Byte $FD zu finden, loopt es zum Anfang zurück und wiederholt die 256 Bytes endlos:

; Index Y = 8 Bit → max 256 Bytes Tile-Daten
; Wenn Y overflowt (255 → 0) ohne $FD zu treffen → Repeat
; Dasselbe für Sprites, aber Pipe-Objekte (3 Bytes)
; verschieben die Indexparität bei jedem Laden.

Manche Glitch Levels nutzen dieses Repeat, um "unendlich" dauernde Level zu generieren.

Das Spritesystem: 2 Bytes + Pipe-Transitions

Sprites folgen einem ähnlichen Format, aber ohne Header und mit einigen Schlüsselunterschieden. Byte $FF markiert das Listenende.

; Spriteformat (2 Bytes) :
; Byte 0 : X-Position (Spalte)
; Byte 1 :
;   Bit 7 : NEXT-SCREEN-FLAG
;   Bits 6-0 : Spritetyp
;       Manche Typen inklusive : Goomba, Koopa, Blooper,
;       Bullet Bill, Lakitu, Spiny, Plattformen,
;       Warp-Zone-Befehl, Toad/Prinzessin,
;       Gruppengegnerspawn-Befehle

Das niederwertige Bit von Byte 1 ist das Hard-Level-Flag: Wenn auf 1 gesetzt, erscheint das Sprite nur in Levels ≥ 5-3. So werden "Hard Mode"-Level erstellt.

Y-Position 15 = Screen Skip (identisch zu Tiles). Y-Position 14 = Pipe Transition (3 Bytes):

; Sprite Y=14 : Pipe/Vine-Transition (3 Bytes !)
;   Byte 0 : X-Position
;   Byte 1 : Bits 6-0 = 7-bit Level-ID (Ziel)
;   Byte 2 : Bits 4-0 = Zielscreen
;            Bits 7-5 = Welt wo diese Transition gültig ist
;
; Warum eine Welt ? Bonusräume werden zwischen Welten wiederverwendet.
; Beispiel : Der 1-1-Bonusraum wird auch von 2-1 und 7-1 genutzt.
; Dieser Raum hat 3 Transitionen, eine pro Welt, damit Mario
; am richtigen Ort wiedererscheint.

Sprites haben kein Queue-System. Das einzige Limit ist, dass nicht mehr als 4 Sprites gleichzeitig in der Spawnzone (knapp rechts außerhalb des Screens) geladen sein können. Darüber werden Sprites ignoriert.

Wie man an Glitch Worlds kommt

Es gibt zwei Hauptmethoden.

Die klassische Methode: der Wall Clip

Der Wall Clip (durch Wände gehen) erlaubt, das normale Level zu verlassen und bis zur versteckten Warpzone zu laufen. Indem man den Weltzähler via RAM manipuliert, kann man jede Level-ID laden.

Die Technik:

  1. Welt 1-2: In die versteckte Endröhre gehen
  2. Den Wall Clip an der rechten Wand machen
  3. In der Leere bis zur Warpzone laufen
  4. Das Spiel interpretiert die Werte als Welten

Aber diese Methode gibt nur Zugang zu einem kleinen Teil der Glitch Worlds.

Die extreme Methode: NES-Tennis-Cart-Swap

Siehe Abschnitt "Der Warm Start" oben fürs komplette Detail. Zusammengefasst: Tennis-Schrittzähler schreibt auf dasselbe RAM-Byte wie SMB1s Startwelt, und die Warm-Start-Erkennung erhält diesen Wert.

Die Bastlerecke: der Code um alles zu erkunden

Wenn du alle Glitches selbst in einem Emulator erkunden willst, kannst du die Level-ID direkt patchen:

; Patch für FCEUX / Mesen :
; RAM-Adresse $075F = aktuelle Level-ID
; RAM-Adresse $0760 = Area Type (0=Water, 1=Overworld, 2=Underground, 3=Castle)

; Beispiel : Level 57 (0x39) in Overworld laden
; Im Emulator Memory Tracer öffnen und schreiben :
; $075F = 0x39
; $0760 = 0x01
; Dann in eine Warp-Röhre gehen oder sterben und neu starten
; → Das Spiel lädt Level-ID $39 in Overworld

RGMechEx hat die komplette Liste der 128 Level × 4 Typen mit automatisch generierten Maps auf rgmechex.com publiziert. Jeder Eintrag zeigt Tile-Pointer, Sprite-Pointer und eine visuelle Levelkarte.

Die wtf-igsten Level

Level ID $1F (Water): 15 Glitch Worlds in einem

Tile-Pointer $A302 (3-4) kombiniert mit Sprite-Pointer $02A0 gibt 15 verschiedene Glitch Worlds (D-1, J-1, Y-1, Z-1, 55-1, 73-1...). Erklärung: Der Sprite-Pointer zeigt auf eine ROM-Zone, die Daten enthält, die nah genug an gültigen Sprites sind, um spielbare Ergebnisse zu produzieren, aber die Kombination aus 3-4-Castle-Tiles mit Overworld-Sprites erzeugt absurdes Rendering.

Level ID $28 (Overworld): 38 Glitch Worlds = Rekord

Der absolute Rekord. 38 Glitch-World-Einträge zeigen auf dasselbe Level (2-1-Tiles + $9F51-Sprites). Warum? Weil Sprite-Pointer $9F51 in eine ROM-Zone fällt, die als Padding/Sounddaten von vielen IDs wiederverwendet wird.

Level ID $49 (Underground): das FDS-Level

Tile-Pointer $76AE + Sprite-Pointer $1C9D. Der Tile-Pointer zeigt auf die für die Famicom-Disk-System-Version reservierte ROM-Zone. Ergebnis: Ein Level mit Tiles, die in der Standard-Cartridge nicht existieren. Das Level lässt Level 52-1 und 196-1 erscheinen.

Level ID $00-$02: die echten Bonuslevel

Diese IDs werden von legitimen Spiel-Unterlevels genutzt:

  • $00: 5-2/6-2-Unterwasserzone (genutzt von H-1, 39-1)
  • $01: 2-2/7-2-Wasser (das Minus World, 36-1)
  • $02: 8-4-Unterlevel (136-1, 151-1, 215-1)

Der Unterschied zwischen einem normal erreichbaren "Bonus"-Level und einer Glitch World ist, dass Warpzonen die aktuelle Welt prüfen:

; Warp-Zone-Prüfung (vereinfacht)
; Das Spiel prüft dass die Zielwelt zwischen 1 und 8 liegt
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; < 1 → abgelehnt
  cmp #9
  bcs InvalidWarp       ; > 8 → abgelehnt
  ; gültige Welt nur zwischen 1 und 8
  jmp DoWarp

Glitch Worlds mit Nummern > 8 oder 0 können nicht via normale Röhren erreicht werden. Man braucht Wall Clip oder Cart Swap.

Warum manche Level crashen: die Jump Tables

Wenn das Spiel ein Tile-Objekt lädt, nutzt es seinen Typ als Index in einer Jump Table:

; Jump Table der Standard-Tile-Objekte (Typen 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; Typ 0 : ?-Block, Hidden, Flagpole...
  .word Obj_Platform      ; Typ 1 : Spezialplattform
  .word Obj_BrickRow      ; Typ 2 : Brickreihe
  .word Obj_BlockRow      ; Typ 3 : Blockreihe
  .word Obj_CoinRow       ; Typ 4 : Münzreihe
  .word Obj_BrickCol      ; Typ 5 : Bricksäule
  .word Obj_BlockCol      ; Typ 6 : Blocksäule
  .word Obj_Pipe          ; Typ 7 : Röhre
  .word Obj_8             ; Typ 8
  .word Obj_9             ; Typ 9
  .word Obj_10            ; Typ 10
  .word Obj_11            ; Typ 11

Die Jump Tables: warum ein ungültiger Objekttyp das Spiel crasht

Wenn ein Objekt einen ungültigen Typ hat (≥12), springt das Spiel auf einen Pointer, der in dieser Tabelle nicht existiert. 4 mögliche Outcomes:

  1. Gültiger Pointer → das Objekt lädt normal
  2. Pointer auf eine andere Jump Table (Überlappung) → ein anderes Objekt erscheint. Beispiel: Typ 12 zeigt auf die Y=13-Tabelle, was eine L-Pipe gibt.
  3. Pointer auf Executable → Ausführung zufälligen Codes (wahrscheinlicher Crash)
  4. Expliziter Placeholder (NOP) → das Objekt tut nichts (manche Sprites sind so und produzieren Gegner, die reglos in der Luft fliegen)

Glitch-Level-ID $58: der Sprite-Pointer zeigt auf eine ungültige Adresse, das Spiel crasht

Glitch-Level-ID $50: der Cloud Tunnel, ein aus korrupten Daten generiertes Level

Glitch-Level-ID $58 (der crashende Tunnel): Sein Sprite-Pointer zeigt auf eine Speicherregion, die auf NES ohne ROM-Mapper nicht existiert. Das Spiel versucht, denselben Koopa 5-mal pro Frame an Position (0,0) zu laden, was die PPU sättigt und einen Freeze verursacht.

; Warum ID $58 crasht :
; Sprite-Pointer → ungültige Adresse (außerhalb NES-Standardraum)
; → Das Spiel liest unbestimmte Bytes als Spritetypen
; → Koopa $00 (ungültiger Typ ohne Handler) loopt in rekursivem Call
; → 6502-Stack-Overflow → Freeze

Das Pipe-Warp-Paradox

Erinnere dich an den Check target_world BETWEEN 1 AND 8. Selbst wenn du in einer Glitch World eine Röhre findest, prüft das Spiel, dass die Zielwelt zwischen 1 und 8 liegt. Glitch Worlds haben Nummern > 8 (36-1, 255-1...), also schlägt der Warp fehl.

Darum hat das Minus World auch kein Ende: Der Flagpole ist nicht in den Sprites, und die Röhren führen nirgendwohin.

Der Trick mit 5 Objekten in einer Spalte

Es gibt einen Edge Case, der das 3-Objekte-pro-Spalte-Limit umgeht. Wenn die Queue blockiert (Slots voll + folgendes Objekt ohne Next-Screen-Flag), "präprozessiert" das Spiel die aktuelle Spalte in Schleife, bis es ein Objekt mit Next-Screen-Flag findet. Bei jedem Präprozessieren:

; Während Spalten-Präprozessieren :
; 1. Objekte in der Queue sehen ihre Restbreite
;    bei jedem "falschen Spaltenvorrücken" dekrementiert
; 2. Wenn ein Objekt Breite=0 erreicht, verlässt es die Queue
; 3. Ein freier Slot kann von einem neuen Objekt gefüllt werden
;    das in derselben Spalte hinzugefügt wird

; Ergebnis : bis zu 5 Objekte können auf derselben Spalte verarbeitet werden.
; Technik : 2 Objekte platzieren, die die Screen-Boundary queren
; (Slots 1 und 2), 1 Dummy-Objekt bei X < vorherigem (blockiert Queue),
; dann 3 Objekte bei X=0 des Folgescreens (eines mit Next-Screen-Flag).

Das nennt man "Queue Skip" und manche Romhacker nutzen es, um dichtere Level zu erstellen als das Format normal erlaubt.

Die Versionsunterschiede

Famicom Disk System

Die FDS-Version von SMB1 hat eine andere Memory Map. Alle Level-Pointer sind verschoben, aber die Daten sind dieselben. Was sich ändert: Die Glitch-World-Indizes sind komplett anders:

FDS-Welt 36 → Level-ID $09 (Wasserversion von 5-3)
  → Der Flagpole ist vorhanden ! Man kann das Level beenden.
  → Danach : $27 (normales 7-3) → $44 (4-4 Underground)
  → $44 ist beendbar → die Axt funktioniert → Spielende !

Das FDS-Minus-World ist also eine "Bonuswelt", die zur
Spielkomplettierung führen kann, anders als die NES-Version.

Mein Lieblings-FDS-Level: ID $5F, eine Untergrundversion der zweiten 3-3-Hälfte als niedriger Tunnel (schade, dass es ein Autoscroller ist).

The Lost Levels (japanisches Super Mario Bros. 2)

Lost Levels ändert viel:

  1. Identische Tile/Sprite-Reihenfolge: Keine Frankenstein-Levels mehr (Tiles und Sprites laden dasselbe Level selbst mit ungültiger ID)
  2. Eine einzige 16-bit Pointer-Tabelle statt zwei separater High/Low-Tabellen
  3. 4 Disk-Dateien: Die ROM wurde für FDS gesplittet:
    • Datei 1: Welten 1-4
    • Datei 2: Welten 5-8
    • Datei 3: Welt 9 + Sound-Engine
    • Datei 4: Welten A-D (komplett andere Pointer-Tabelle)
  4. Gleiche Level-ID = 4 mögliche Level je nach geladener Datei
  5. Kein Tennis-Glitch mehr: Die Continue-Option (Weitermachen in derselben Welt nach Game Over) macht Warm Start unnütz, und das Spiel resettet sofort, wenn Welt > 9
  6. Neue Objekte: Giftpilz, unsichtbarer Block, unsichtbarer Feuerblumenblock, Upside-Down-Pipes, Wind, aber mitten in existierende Listen eingefügt → Backward-Inkompatibilität mit SMB1
  7. Piranha-Pflanzen nach Welt 4 immer rot, grüne Springboards nur in Welten 2/B/3/C/7

Super Mario All-Stars (SNES)

Direkter Port mit denselben 6502-Routinen (SNES führt NES-Code im Kompatibilitätsmodus aus):

  • Warpzone gefixt: Kein Minus World mehr (in die linke Röhre vor dem Text gehen führt zur richtigen Welt)
  • Absturz: Die meisten Glitch Levels crashen (außer ID $6A und 9-1)
  • Schlossobjekte hinzugefügt: Einzigartigere Render
  • Aber: Der 4-2 Wrong Warp funktioniert noch (nicht gepatcht !)

Der 4-2 Wrong Warp: ein Objektplatzierungsbug

In 4-2 gibt es zwei Pipe-Transition-Objekte: Die Ranke (Warpzone) und die Röhre (Coin-Cash-Room). Das erste Transition-Objekt (das der Ranke) ist weit vor dem Rankenerscheinen auf dem Screen platziert. Das zweite (die Röhre) ist zu spät im Level platziert.

; Transition-Timing in 4-2 :
; Transition-Objekt 1 (Ranke → Warpzone) : 3 Screens vor der Ranke platziert
; Transition-Objekt 2 (Röhre → Coin Cash) : 1 Screen nach der Röhre platziert
;
; Normalerweise ist das erste Objekt deaktiviert bevor Mario
; die Röhre erreicht. Aber wenn Mario schnell ist (oder
; den B+Right-Block-Shortcut nutzt), ist die Rankentransition
; noch aktiv wenn er die Röhre berührt !
; → Das Spiel lädt die Warpzone statt des Coin Cash.
;
; Wäre das Objekt direkt nach der Ranke aber vor
; der Röhre platziert, existierte der Bug nicht.

Die Loop-Level

Wie funktionieren Loops (8-4, 7-4)? Das Level hat Checkpoints mit hardcodierten Screennummern und Y-Positionen:

; Checkpoint : {screen_number, vertical_position}
; Wenn Mario diesen Checkpoint auf richtiger Höhe passiert → Level geht weiter
; Sonst → Warp back um 4 Screens (64 Blöcke)
;
; Für Endlosschleife : vertical_position = $F0
; (unterhalb des Screenbodens) → unmöglich zu validieren.
;
; Checkpoints sind simpel (ein einziges Flag) außer für Welt 7
; die Triplets nutzt (3 Flags, man muss mindestens 1 failen)
;
; Der Warp back ist ruppig : Tile-Data-Offset auf hardcoded Wert gesetzt,
; Sprite-Data-Offset auf 0 zurückgesetzt. Vorhandene Gegner
; werden instant entladen → Firebars verschwinden.

Format ändern, nicht Code

Eine der faszinierendsten Lehren dieser Architektur ist, dass SMB1s Entwickler ein sehr expressives Levelsystem schufen, ohne je den 6502-Render-Code anzufassen. Alle Variation zwischen Levels kommt aus Daten (Pointer, Objekte, Sprites, Floor Patterns), nicht aus Code.

Die 256 Glitch Worlds existieren, weil die Pointer-Tabellen für 128 Einträge × 4 Typen dimensioniert sind und das Spiel nie validiert, was es liest. Wenn ein Pointer in RAM fällt, interpretiert das Spiel Marios Register als Tiles. Wenn ein Pointer in Sounddaten fällt, spielt das Spiel Musik als Leveldesign. Und wenn Jump Tables overflown, führt das Spiel irgendwas bis zum Crash aus.

More Super Mario Bros. Mechanics Explained, das 4. Video

Was man daraus lernen kann

  1. Tile/Sprite-Trennung: Totale Unabhängigkeit beider Schichten, mit unterschiedlichen Speicherreihenfolgen, die einzigartige Frankenstein-Levels erzeugen
  2. RLE-Kompression + Objektsystem: Level sind keine Bitmaps, sondern platzierte Objektlisten, mit Floor Patterns für den Boden
  3. 3-Slot-Queue: Striktes Hardware- (und Leveldesign-) Limit
  4. Totales Pointer-Vertrauen: Das Spiel vertraut Pointern und Jump Tables, was entweder spielbare Glitches oder Crashes produziert
  5. Max 256 Bytes: Das 6502-Y-Register-Limit, das Daten bei zu weitem Gehen wiederholen lässt
  6. Warm Start / Cold Start: Ein "Continue"-System, das die Tür zum Tennis → Mario-Cart-Swap öffnete

Das Schönste: Das alles ist 6502-Code, der in 40KB passt. Der Code redet direkt mit der Hardware und schenkt jedem Speicherzugriff totales Vertrauen. Wenn der Pointer faul ist, crasht das Spiel. Und Crashes nennen wir Glitch Worlds.

Die 3 Dinge zum Mitnehmen

  1. Glitch Worlds sind schlecht fallende Pointer, das Spiel hat 128 IDs × 4 Zonentypen, aber nur 34 einzigartige Level. Wenn die World-Number korrupt ist (durch Tennis oder Wall Clip), lädt das Spiel einen Pointer, der für ein anderes Level designed war, und die 512 möglichen Kombinationen produzieren unvorhersehbare Ergebnisse.

  2. Das Minus World ist ein Warp-Bug kombiniert mit Korruption, die linke Röhre in 1-2 lädt, wenn vor Texterscheinen aktiviert, Welt 36 (0x24). Diese Welt zeigt auf Level-ID $01 (2-2-Wasser), ein Level ohne Flagpole. Und da es keine Pipe-Transition für Welt 36 gibt, loopt das Level endlos. Das Fehlen von Prüfung erschafft die Ikone.

  3. Tennis → Mario, 15 Jahre vor OoT → Paper Mario, NES-RAM überlebt einen Cartridge-Swap dank Kondensatoren und SMB1s Warm-Start-/Cold-Start-System. Tennis-Schrittzähler (der ein RAM-Byte beim Abspielen der Schritt-Sounds inkrementiert) fällt genau auf die World-Number-Adresse. Die Top-Score-Stellen müssen 0 bleiben, das $A5-Byte intakt, und das Spiel muss einen Warm Start erkennen, ein perfekter Umständewettbewerb, der nur mit Tennis funktionierte.

Die Originalvideos von Retro Game Mechanics Explained sind eine wahnsinnige Ameisenarbeit, der Detailgrad zum 6502-Disassembly, die automatischen Maps aller Level, die Erklärungen von Cart Swap und Warm Start. Wenn du die Serie nicht gesehen hast, schau sie, sie ist kurz und jede Minute ist dicht.

Der Map-Quellcode liegt auf rgmechex.com, und das komplette SMB1-Disassembly ist in vielen Repos Open Source. Vor 40 Jahren schrieben japanische Programmierer dieses Levelsystem in 6502 mit null Unit-Tests und null Bug-Tracker, und wir lernen heute noch Optimierungstechniken, indem wir ihren Code öffnen.

Super Mario Bros. : формат уровней, указатели и 256 глитч-миров

Как 128 уровней × 4 типа зон умещаются в 40KB ROM, почему существует Minus World и как матч NES-тенниса может загружать глитч-миры.

Введение

Super Mario Bros. это 40 килобайт ROM. Восемь миров, 32 уровня, враги, музыка, пауэрапы, всё умещается туда.

Но если открыть эмулятор и поковырять нужные байты, можно загрузить уровень 36-1. Или 255-1. Или приземлиться в мире, где всё сделано из спрайтов Боузера и труб, ведущих в никуда.

Эти глитч-миры существуют по простой причине : система хранения уровней SMB1 это чудо 8-битной оптимизации, и когда заставляешь игру читать там, где не надо, получаются захватывающие результаты.

Retro Game Mechanics Explained сделал серию из 4 видео об этом -- скомпилируем их в одно погружение в код 6502 самой продаваемой игры своей эпохи.

GLITCH OBJECTS -- название серии RGMechEx про скрытые механики SMB1

Мир 9-1 -- титульный экран первого глитч-мира, доступного через cart swap Tennis

Тёплый старт : почему RAM Tennis выживает в SMB1

Прежде чем говорить о хранении уровней, надо понять, как стартует SMB1. Потому что глитч cart swap NES Tennis целиком опирается на систему детекта warm start / cold start игры.

41 сохраняемый байт

Когда SMB1 детектит cold start (первое включение или выкл/вкл питания), он стирает всю RAM. Но когда детектит warm start (кнопка reset, без отключения питания), он сохраняет область памяти в 41 байт :

; 41 байтов, сохраняемых в RAM при warm start
; Адреса $075F-$0787
;
; $075F : стартовый байт (world - 1)    [1 байт]
; $0760 : флаг выбора мира (кнопка B) [1 байт]
; $0761-$0762 : неиспользуемо                    [2 байта]
; $0763-$0768 : таймер (6 цифр, 3 показываются) [6 байтов]
; $0769-$076E : монеты Луиджи                   [6 байтов]
; $076F-$0774 : монеты Марио                   [6 байтов]
; $0775-$077A : счёт Луиджи                   [6 байтов]
; $077B-$0780 : счёт Марио                   [6 байтов]
; $0781-$0786 : топ-счёт (6 цифр, 1 скрыта) [6 байтов]
; $0787 : магический байт $A5                 [1 байт]

Эти 41 байта служат одной функции : позволить игроку продолжить с того же мира после game over. Если умираешь в 6-3, игра пишет мир 6 в стартовый байт, и на титульном экране, если держать A + Start, перезапускаешься в 6-1.

41 байт, сохраняемых в RAM при warm start -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD и магический байт $A5

Двойная проверка warm start

Cold start против warm start -- диаграмма детекта reset

Когда SMB1 бутится, он проверяет не один критерий, а два :

CheckWarmStart:
  ; 1. Проверить магический байт $A5 по $0787
  lda $0787
  cmp #$A5
  bne ColdStart        ; нет $A5 → cold start

  ; 2. Проверить 6 цифр топ-счёта ($0781-$0786)
  ;    Каждая цифра должна быть между 0 и 9
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; цифра >= 10 → cold start
  inx
  cpx #6
  bne CheckLoop

  ; Если оба условия проходят → warm start
  ; RAM не стирается, стартовый мир сохраняется
  jmp WarmStartBoot

Проверка байта $A5 и цифр топ-счёта -- сердце warm start

Зачем двойная проверка? Потому что байт $A5 мог оказаться там случайно (другая игра, оставившая это значение, или состояние покоя чипа RAM по умолчанию). Проверяя, что цифры топ-счёта валидны (0-9), убеждаемся, что данные связны.

Почему работает только Tennis

Когда вставляешь SMB1 в первый раз (cold start), игра :

  1. Стирает всю RAM → топ-счёт = 0, world-байт = 0
  2. Пишет $A5 по адресу $0787

Затем свапаешь на Tennis, не выключая консоль. Tennis :

  • Не чистит RAM при старте (мало какие игры NES это делают)
  • Не пишет в байты топ-счёта → они остаются 0 (валидны)
  • Не трогает байт $A5 → он остаётся на месте
  • Использует адрес $075F для счётчика шагов игрока
; Инкремент шагов в Tennis :
; При каждом шаге игрока на корте Tennis инкрементирует байт по $075F.
; Этот же байт используется SMB1 как "world number - 1".
;
; 0 шагов  → мир 0 → SMB1 = мир 1
; 1-7 шагов → миры 1-7 → нормальные миры
; 8+ шагов → мир 8+ → глитч-миры !
;
; Счётчик инкрементируется, только когда останавливается музыка
; (звуки шагов не играют во время музыки).

Когда возвращаешь SMB1 :

  1. Байт $A5 всё ещё там (Tennis его не трогал)
  2. Цифры топ-счёта всё ещё 0 (валидны)
  3. World-байт теперь 8+ (наинкрементирован шагами Tennis)
  4. SMB1 детектит warm start → сохраняет повреждённый world-байт
  5. Держать A + Start → мир 9-1, мир A-1, мир 36-1 и т.д.

Почему надо бутить Mario до Tennis

Тонкость : надо сначала забутить SMB1, потом Tennis, потом снова SMB1. Если бы начал сразу с Tennis, байт $A5 никогда не был бы записан (Tennis не пишет $A5), так что детект warm start провалился бы и RAM стёрлась бы.

Счётчик шагов Tennis : каждый шаг инкрементирует world-байт

Access Glitch Worlds via NES Tennis -- видео, объясняющее cart swap

Как SMB1 хранит свои уровни в 40KB

Nintendo R&D4 должна была решить на вид простую проблему : представить горизонтально скроллящиеся уровни с тайлами, врагами, предметами, всё в ультратесном бюджете ROM.

Решение это разделение на два слоя данных полностью независимых :

Tile layout (карта уровня)

Каждый уровень определяется указателем на структуру тайлов, сжатую в ROM. Сжатие рудиментарно, но гениально : "контрольный" байт, за которым 1-3 байта данных.

Формат тайлов использует систему ранов (RLE-like) :

; Формат тайлов SMB1 (упрощённо)
; Каждая "команда" это контрольный байт :
;
; $00-$7F : положить тайл, продвинуться на 1 колонку
; $80-$BF : положить тайл, повторённый N раз (N = байт - $80 + 1)
; $C0-$FF : спецкоманда (конец строки, прыжок, смена палитры)

Пример : чтобы нарисовать 3 кирпича подряд :
  $82 $01    ; повторить тайл $01 (brick) 3 раза

Каждый уровень содержит 13 строк по 16 колонок тайлов (13×16 = 208 видимых тайлов). Но сжатый формат позволяет спускаться куда ниже -- например, небо и пустые колонки почти не занимают места.

Цикл рендера на 6502 :

; Декомпрессия тайлов - главный loop
; Вход : указатель tile_data в $XX
; Выход : тайлмап уровня в RAM PPU

DecompressTile:
  lda (tile_ptr),y      ; читать контрольный байт
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F : одиночный тайл
  cmp #$C0
  bcc RunLength         ; $80-$BF : run-length
  jmp SpecialCommand    ; $C0-$FF : спецкоманда

SingleTile:
  sta PPU_DATA          ; писать тайл напрямую
  jmp Next

RunLength:
  sec
  sbc #$7E              ; N = control - $7E
  tax
  lda (tile_ptr),y      ; читать тайл для повтора
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

Sprite layout (враги и объекты)

Параллельно враги и объекты (блоки ?, трубы, гумбы, купа) хранятся в совершенно отдельной структуре. Каждый спавн определяется 2 байтами :

; Формат спрайтов SMB1
; Байт 0 : позиция X (в колонках)
; Байт 1 : тип спрайта + биты страницы Y
; Y выводится из индекса в последовательности

Последовательность спрайтов :
  $01 $4B    ; гумба на колонке 1
  $09 $4B    ; гумба на колонке 9
  $10 $61    ; блок ? на колонке 16 (содержит монету)
  $15 $54    ; зелёная купа на колонке 21
  $FF        ; конец последовательности

Каждый уровень может ссылаться до 5 страниц спрайтов (точнее, 5 "скринов" по 16 колонок), но на практике большинство уровней использует только 2-3.

Таблица указателей

Гениальность дизайна это таблица указателей. Каждый уровень хранится как пара адресов ROM :

// Внутренняя структура (упрощённо) World Map
struct LevelPointer {
    uint16_t tile_ptr;   // Адрес ROM данных тайлов
    uint16_t sprite_ptr; // Адрес ROM данных спрайтов
};

// 4 отдельные таблицы, по одной на AreaType :
// 0 = Water, 1 = Overworld, 2 = Underground, 3 = Castle
LevelPointer level_table[4][128];

128 входов на таблицу. 4 типа зон. 512 возможных комбинаций, но лишь доля используется официальной игрой. Остальное это неинициализированная RAM или данные, интерпретируемые как указатели.

Когда игра грузит уровень, она делает это :

; Загрузка уровня
; A = AreaType (0-3), X = LevelID (0-127)

LoadLevel:
  sta AREA_TYPE
  asl                  ; *2 для офсета в 16-битной таблице
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; указатель на тайлы
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; указатель на спрайты
  jsr DecompressTiles

Игра оказывает указателям полное доверие : читает адрес в таблице и декомпрессирует то, что по этому адресу, точка.

Level ID $06 (Water) -- 9-1, подводная версия 6-2

Таблица Level ID : 128 возможных входов, 34 назначено

Разный порядок указателей tiles и sprites -- причина Frankenstein levels

34 уникальных уровня и система 7-битных ID

Чип RAM NES (MB8416A) -- именно он сохраняет данные, когда свапаешь картриджи

В SMB1 не 32 уровня, а 34 уникальных уровня. Многие уровни дубликаты (5-3 = 1-3, но с Bullet Bills), помеченные флагом "hard mode". Настоящие уникальные уровни :

  • Вода (Type 0) : 3 уровня (2-2, 7-2, бонусная зона 5-2/6-2)
  • Overworld (Type 1) : 22 уровня (включая 2 бонусные облачные комнаты)
  • Underground (Type 2) : 3 уровня (включая подземные бонусные комнаты)
  • Castle (Type 3) : 6 уровней
  • + 1 cutscene-комната (перед подземными/водными уровнями)
  • + 1 warp-зона из 4-2

У каждого уровня ID на 7 битах. 5 младших битов = номер в подгруппе, 2 старших бита = тип зоны :

; 7-битное кодирование Level ID
; Биты 6-5 : Тип (00=Water, 01=Overworld, 10=Underground, 11=Castle)
; Биты 4-0 : Номер в подгруппе
;
; Water IDs      : $00-$02  (типы 00, номера 0-2)
; Overworld IDs  : $20-$35  (типы 01, номера 0-21)
; Underground IDs: $40-$42  (типы 10, номера 0-2)
; Castle IDs     : $60-$65  (типы 11, номера 0-5)
;
; ID $25 = %0100101 → тип 01 (Overworld), номер 5 → 1-1
; ID $23 = %0100011 → тип 01 (Overworld), номер 3 → 6-2

128 возможных ID ($00-$7F), только 34 назначены настоящим уровням. Неиспользуемые ID указывают на что попало.

Таблицы указателей : два списка, два порядка

Указатели tiles и sprites хранятся не в одном порядке. Код использует два отдельных 16-битных списка (старший байт / младший байт в двух разных таблицах) :

Порядок указателей sprites :
  Индекс 0-5   : Castle (6 уровней)
  Индекс 6-27  : Overworld (22 уровня)
  Индекс 28-30 : Underground (3 уровня)
  Индекс 31-33 : Water (3 уровня)

Порядок указателей tiles :
  Индекс 0-2   : Water (3 уровня)
  Индекс 3-24  : Overworld (22 уровня)
  Индекс 25-27 : Underground (3 уровня)
  Индекс 28-33 : Castle (6 уровней)

Почему разные порядки? Никакой технической причины -- вероятно, данные так организовались при разработке. Но это создаёт захватывающее следствие : когда ID уровня невалиден, указатели tiles и sprites грузят разные уровни, создавая Frankenstein levels.

Чтобы перемещаться между этими списками, игра использует маленькие таблицы офсетов (как оглавление) :

; Таблицы офсетов по типу (Water, Overworld, Underground, Castle)
; Каждый вход = стартовый индекс в соответствующем списке

SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; Water=31, Overworld=6, Underground=28, Castle=0

TileOffsetTable:
  .byte $00, $03, $19, $1C    ; Water=0, Overworld=3, Underground=25, Castle=28

Чтобы загрузить уровень 6-2 (ID $23, Overworld номер 3) :

; 1. Тип = 01 (Overworld) → индекс в таблице офсетов = 1
; 2. Sprite offset = SpriteOffsetTable[1] = 6
;    Финальный индекс = 6 + 3 (номер уровня) = 9 → 10-й указатель sprites
; 3. Tile offset = TileOffsetTable[1] = 3
;    Финальный индекс = 3 + 3 = 6 → 7-й указатель tiles
; 4. Результат : указатель tiles $A619 + указатель sprites $9ED0 = 6-2 ✓

А что происходит с невалидным ID вроде $43 (Underground номер 3, не существующий)?

; ID $43, Тип = 10 (Underground), номер = 3
; Sprite offset = SpriteOffsetTable[2] = $1C = 28
;   Индекс = 28 + 3 = 31 → 32-й указатель sprites = водный бонус 5-2 !
; Tile offset = TileOffsetTable[2] = $19 = 25
;   Индекс = 25 + 3 = 28 → 29-й указатель tiles = 1-4 (Castle) !
;
; Результат : подземный уровень с тайлами 1-4
; и блуперами из водной зоны 5-2. Настоящий Франкенштейн.

Level ID $43 -- Frankenstein level : тайлы 1-4 + спрайты воды 5-2

Exploring Glitch Level Pointers -- таблицы офсетов объяснены

Таблица world index -- когда оверфлоу мира 9 создаёт глитч-левел

World index table : почему мир 9 оверфлоуит

Есть ROM-таблица на 8 байтов, дающая индекс первого уровня каждого мира (1-8). А сразу после таблица 36 Level ID всех уровней в порядке игры.

; WorldIndexTable (8 байтов)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   -> Мир 1 начинается с уровня 0
;   -> Мир 2 начинается с уровня 5
;   -> Мир 8 начинается с уровня 33

; LevelIDTable (36 байтов)
  .byte $25, $28, $29, $26, $24, ... ; 36 Level ID

Когда пытаешься загрузить мир 9, игра читает 9-й байт WorldIndexTable... которого нет. Она оверфлоуит на 1 байт в LevelIDTable, читает значение $25, затем использует $25 как индекс в LevelIDTable (37-й вход) -- что снова оверфлоуит на 2 байта в SpriteOffsetTable и читает значение 6.

; Мир 9 :
;   1. WorldIndexTable[8] (overflow) → читает $25 в LevelIDTable
;   2. LevelIDTable[37] (overflow) → читает 2-й байт SpriteOffsetTable = 6
;   3. ID = 6 → Water level number 6 (не существует)
;   4. Tile pointer = указатель water номер 6 = тайлы 6-2
;   5. Sprite pointer = индекс 31+6 = 37 > 33 → невалидный указатель
;   6. Результат : 6-2 под водой с глюкнутыми спрайтами
;      → мир 9-1 !

Для мира G (16) оверфлоу идёт ещё дальше и падает на Level ID $01, уровень cutscene перед 1-2 :

; Мир G (16) :
;   WorldIndexTable[15] → читает $01 в LevelIDTable
;   LevelIDTable[1] = $29 (cutscene 1-2)
;   → мир G-1 = входная cutscene 1-2

Почему существуют глитч-миры

В игре 32 "легитимных" уровня (8 миров × 4 уровня). Но таблица указателей на 128 входов на тип зоны. Входы за 32-м содержат то, что находится в ROM по этим адресам -- порой другой уровень, порой звуковые данные, порой RAM, порой что попало.

Level ID $01 Water (Minus World) -- tile pointer $AE45, sprite pointer $A171

Level ID $01 + AreaType 0 = Minus World

Самый знаменитый из глитч-миров. Level ID $01 в AreaType 0 (вода) указывает на :

  • Tile pointer : $AE45 → подводная зона 2-2/7-2
  • Sprite pointer : $A171 → спрайты 2-2/7-2

Итог : водный уровень, похожий на 2-2, но зацикливающийся бесконечно, потому что flagpole не существует. Уровень крутится в петле вечно.

Это уровень 36-1 (или 36-1 в мире $-1).

Проверка warm start в SMB1 -- именно она даёт существовать Minus World

; Почему во Minus World нет flagpole :
; У спрайтов 2-2/7-2 ($A171) нет flagpole
; в последовательности. Игра ищет спрайт $FD (flagpole)
; но никогда не находит → бесконечная петля
;
; Игра продолжает генерировать уровень бесконечно
; пока таймер не дойдёт до нуля.

Указатели, указывающие в RAM

Когда tile pointer или sprite pointer указывает на адрес в RAM ($00-$7F), а не в ROM, игра пытается интерпретировать постоянные изменения RAM как тайлы :

; Пример : Level ID $03 в Water
; Tile Pointer : $A46B (3-3 - валиден)
; Sprite Pointer : $009D (указывает в нулевую страницу RAM !)
;
; Нулевая страница RAM содержит регистры игры,
; позицию Марио, состояние счётчиков...
; Игра декомпрессирует это как последовательность спрайтов,
; и результат это уровень с врагами,
; которые на деле значения регистров.

Когда нулевая страница меняется (потому что Марио двигается, таймер тикает и т.д.), "спрайты" уровня тоже меняются. Поэтому у некоторых глитч-миров враги мигают и постоянно трансформируются.

Level ID $03 Water -- sprite pointer $009D указывает в RAM, уровень неиграбелен

Level ID $36 : пустой уровень (Overworld)

Level ID $36 в Overworld :

  • Tile pointer : $AC35 (1-2)
  • Sprite pointer : $A0D8 (1-2)

Итог : ничего. Игра грузит уровень, но он помечен "без уровня" в каталоге RGMechEx. Тайлы, может, валидны, но спрайты указывают туда, где получается пустой или нерабочий уровень.

Level ID $1D (Castle) : чемпион крашей

Level ID $1D в Castle :

  • Tile pointer : $A210 (4-4)
  • Sprite pointer : $7EA0 (RAM !)

Sprite pointer в RAM = undefined sprites. Игра пытается показать Spiny ball или Bullet Bill blaster в первой строке тайлов. Крашится сразу.

; Когда sprite pointer указывает в RAM,
; игра декомпрессирует байты, меняющиеся всё время,
; как инструкции "спавн". Результат :
; - Появление несуществующих объектов (значение undefined)
; - Краш PPU, когда спрайт NES пытается показать невалидный тайл
; - Полный фриз консоли

256 каталогизированных глитч-миров

RGMechEx написал скрипт, генерирующий карты всех уровней для 4 типов зон и 128 ID каждого.

Счётчик мира на 8 битах (0-255). Миры 1-8 легитимны. Остаётся 248 потенциальных глитч-миров. Каждый глитч-мир соответствует первому уровню этого мира, а его Level ID вычисляется механизмом оверфлоу WorldIndexTable.

Таблица глитч-миров -- 248 повреждённых миров, 68 доступных первых уровней

Из 128 возможных ID только 68 это "первые уровни" мира (доступные через номер глитч-мира). Остальные 60 это уровни 2+ или недоступные.

Тип Уникальных играбельных ID Крашащихся ID Пустых ID
Water (0) ~20 ~60 ~48
Overworld (1) ~30 ~55 ~43
Underground (2) ~15 ~65 ~48
Castle (3) ~25 ~58 ~45

Многие ID ведут на один уровень из-за указателей, падающих на те же адреса ROM. Level ID $28 (Overworld) например -- tile pointer $A7CD (2-1) -- появляется в 38 разных глитч-мирах, потому что его sprite pointer $9F51 указывает в зону ROM, используемую как padding/звуковые данные, переиспользуемые кучей ID.

Карта уровня ID $28 (Overworld) -- тайлы 2-1 с нормальными спрайтами, 38 глитч-миров

Super Mario Bros. Glitch Levels Explained -- 3-е видео

6 реально уникальных глитч-уровней

Среди 19 доступных ID глитч-уровней только 6 не крашатся сразу при загрузке :

Мир Level ID Описание
E-1 (224) $50 Один ? блок над пропастью. Марио умирает мгновенно.
W $57 Марио спавнится заблокированным, не может двигаться.
42 (133) $50 Облачный туннель, ловящий Марио, если зайти достаточно далеко.
62 (131, 240) $4D Замороженный замок : Марио спавнится наверху, не может упасть → заблокирован.
127 $4B Подземный туннель, но крашится, если зайти слишком далеко.
137 $4B Включает автопрокрутку катсцен. Марио встречает единственный brick block, блокирующий его навсегда.

Level ID $50 (cloud tunnel) -- глитч-мир 42-1 и E-1 Level ID $4D (castle) -- мир 62-1, Марио заблокирован на спавне Level ID $4B (tunnel) -- мир 127-1, крашится, если зайти слишком далеко

Шесть глитч-миров из 248, дающих что-то реально новое. Остальное это нормальные уровни с неправильным типом зоны или чёрные экраны.

Формат уровней в деталях

Фокус на точном формате данных уровней, чтобы понять, почему глитч-уровни стоят (или нет).

Хедер уровня : 2 байта, 6 свойств

Каждый уровень начинается с хедера в 2 байта, контролирующего 6 свойств :

; Байт 0 : таймер + Y старта + модификатор
;   Биты 7-6 : таймер (00=без изменений, 01=200, 10=300, 11=400)
;   Биты 5-3 : Y старта Марио (111/110 = автопроход)
;   Биты 2-0 : модификатор типа уровня
;              000=default, 001=waves, 010=brick wall,
;              011=water bottom, 100=night, 101=snow,
;              110=snow night, 111=gray night

; Байт 1 : платформа + фон + паттерн пола
;   Биты 7-6 : спецплатформа (00=tree, 01=mushroom,
;                                 10=Bullet Bill, 11=cloud)
;   Биты 5-4 : фон (00=none, 01=clouds,
;                           10=montains, 11=fences)
;   Биты 3-0 : начальный паттерн пола (0-15)

Модификатор типа контролирует визуальные вариации : волны наверху водных уровней, кирпичный фон 8-3, ночная палитра 4-3, снег 6-2 и т.д.

Объекты тайлов : 2 байта, Next Screen Flag, очередь 3 слота

После хедера идёт список объектов тайлов, каждый объект 2 байта. Байт $FD маркирует конец списка.

; Формат объекта тайла (16 бит) :
; Байт 0 :
;   Биты 7-4 : X позиция (колонка 0-15)
;   Биты 3-0 : Y позиция
;     Y=0-11  : нормальная Y позиция
;     Y=12    : спецобъекты (дыры, мосты, верёвка, ? блоки)
;     Y=13    : screen skip / спецобъекты 2
;     Y=14    : смена модификатора/декораций/пола
;     Y=15    : спецобъекты 3 (замок, лестницы, большая труба)

; Байт 1 :
;   Бит  7   : ФЛАГ СЛЕДУЮЩЕГО ЭКРАНА
;   Биты 6-4 : тип объекта (0-7)
;   Биты 3-0 : ширина/высота / подтип

Когда бит "next screen" выставлен, текущая рабочая колонка инкрементируется на 1. Это позволяет ставить объекты за пределами первых 16 колонок. Объекты должны перечисляться по порядку (слева направо), потому что игра грузит их последовательно :

; У загрузочной рутины ДВЕ фазы на колонку :
; Фаза 1 : искать новые объекты, начинающиеся на этой колонке
;            и добавлять в очередь (queue)
; Фаза 2 : обработать каждый объект в очереди, отрисовывая тайлы,
;            и убрать те, что заканчиваются на этой колонке

Очередь ровно на 3 слота. Прямое следствие : нельзя иметь больше 3 объектов, начинающихся на одной колонке. Если очередь полна, 4-й объект игнорируется и никогда не загрузится.

Поэтому хорошо спроектированные уровни избегают складывать слишком много объектов. Пример в 1-2 : колонка с 1up-блоком в потолке + кирпичи рядом разбиты на два отдельных объекта, чтобы уважить лимит 3.

Спецпозиции Y : 12, 13, 14, 15

Когда Y=12, у объекта нет Y-позиции (она захардкожена по типу) :

; Y=12 : объекты без Y позиции
;   Тип 0 : дыра (убирает пол)
;   Тип 1 : верёвка движущейся платформы
;   Типы 2-4 : мосты на фиксированном Y
;   Тип 5 : дыра с водой/лавой
;   Типы 6-7 : ряды ? блоков

Когда Y=13, две подгруппы. Если бит 6 байта 1 равен 1 :

; Y=13, бит6=1 : спецобъекты
;   0 = L-pipe (cutscene), 1 = flagpole, 2-3 = мост/топор/молот (конец замка)
;   4 = стоп экрана, 5 = случайные враги, 6 = петля уровня, 7+ = возможный краш

Если бит6=0, 5 младших битов кодируют screen skip (прыгнуть прямо на экран N, не проходя next screen flag по одному).

Когда Y=14 : тот же принцип с бит6=1 для смены модификатора типа, бит6=0 для смены фона + паттерна пола.

Floor patterns : 16 мотивов пола

Пол уровней сделан не отдельными объектами. SMB1 использует floor patterns, фоновый мотив, применяющийся ко всем колонкам до следующей смены :

; Floor patterns (4 бита = 16 возможностей)
;   0 = полная пустота
;   1 = пол 2 тайла высотой
;   2 = пол 1 тайл высотой
;   3 = пол + низ
;   4 = пол + низ 2
;   5 = пол 1/2 тайла
;   6 = 3/4 пола
;   ... до 15 = полное заполнение (пол + потолок)

Поэтому дыры это объекты : они оверрайдят floor pattern на конкретной колонке, не меняя паттерн для всего остального.

Лимит 256 байтов и repeat

Все tile-данные уровня умещаются максимум в 256 байтов. Y-регистр 6502 используется как индекс, а он 8-битный. Если игра доходит до конца данных, не найдя байт $FD, она зацикливается в начало и повторяет 256 байтов бесконечно :

; Индекс Y = 8 бит → макс 256 байтов tile-данных
; Если Y оверфлоуит (255 → 0), не встретив $FD → повтор
; То же для спрайтов, но pipe-объекты (3 байта)
; сдвигают чётность индекса при каждой загрузке.

Некоторые глитч-уровни эксплуатируют этот повтор, чтобы генерировать уровни, длящиеся "бесконечно".

Система спрайтов : 2 байта + pipe-переходы

Спрайты следуют похожему формату, но без хедера и с ключевыми отличиями. Байт $FF маркирует конец списка.

; Формат спрайта (2 байта) :
; Байт 0 : позиция X (колонка)
; Байт 1 :
;   Бит 7 : ФЛАГ СЛЕДУЮЩЕГО ЭКРАНА
;   Биты 6-0 : тип спрайта
;       Некоторые типы включают : goomba, koopa, Blooper,
;       Bullet Bill, Lakitu, Spiny, платформы,
;       команда warp zone, toad/принцесса,
;       команды спавна групп врагов

Младший бит байта 1 это флаг сложного уровня : если выставлен, спрайт появляется только на уровнях ≥ 5-3. Так создаются уровни "hard mode".

Y-позиция 15 = screen skip (идентично тайлам). Y-позиция 14 = pipe-переход (3 байта) :

; Спрайт Y=14 : pipe/vine переход (3 байта !)
;   Байт 0 : позиция X
;   Байт 1 : биты 6-0 = Level ID 7-бит (назначение)
;   Байт 2 : биты 4-0 = экран назначения
;            биты 7-5 = мир, где этот переход валиден
;
; Почему мир? Бонусные комнаты переиспользуются между мирами.
; Пример : бонусная комната 1-1 также используется 2-1 и 7-1.
; У этой комнаты 3 перехода, по одному на мир, чтобы Марио
; появлялся в правильном месте.

У спрайтов нет queue-системы. Единственный лимит в том, что не может быть больше 4 спрайтов, загруженных одновременно в зоне спавна (чуть за экраном справа). Дальше спрайты игнорируются.

Как попасть в глитч-миры

Есть два главных метода.

Классический метод : wall clip

Wall clip (проход сквозь стены) позволяет выйти из нормального уровня и дойти пешком до скрытой warzone. Манипулируя счётчиком мира через RAM, можно загрузить любой Level ID.

Техника :

  1. Мир 1-2 : идти в скрытую конечную трубу
  2. Сделать wall clip на правой стене
  3. Идти в пустоте до warp-зоны
  4. Игра интерпретирует значения как миры

Но этот метод даёт доступ лишь к малой части глитч-миров.

Экстремальный метод : NES Tennis cart swap

См. секцию "Тёплый старт" выше для полных деталей. Вкратце : счётчик шагов Tennis пишет в тот же байт RAM, что стартовый мир SMB1, а детект warm start сохраняет это значение.

Уголок ковырятелей : код, чтобы всё исследовать

Если хочешь сам исследовать все глитчи в эмуляторе, можно патчить Level ID напрямую :

; Патч для FCEUX / Mesen :
; Адрес RAM $075F = текущий Level ID
; Адрес RAM $0760 = Area Type (0=Water, 1=Overworld, 2=Underground, 3=Castle)

; Пример : загрузить Уровень 57 (0x39) в Overworld
; В эмуляторе открыть трассировщик памяти и записать :
; $075F = 0x39
; $0760 = 0x01
; Затем войти в warp-трубу или умереть и перезапуститься
; → Игра грузит уровень ID $39 в Overworld

RGMechEx опубликовал полный список 128 уровней × 4 типа со сгенерированными картами на rgmechex.com. Каждый вход показывает tile pointer, sprite pointer и визуальную карту уровня.

Самые wtf уровни

Level ID $1F (Water) : 15 глитч-миров в одном

Tile pointer $A302 (3-4) в комбинации со sprite pointer $02A0 даёт 15 разных глитч-миров (D-1, J-1, Y-1, Z-1, 55-1, 73-1...). Объяснение : sprite pointer указывает в зону ROM, содержащую данные достаточно близкие к валидным спрайтам, чтобы давать играбельные результаты, но комбинация тайлов замка 3-4 со спрайтами overworld создаёт абсурдный рендер.

Level ID $28 (Overworld) : 38 глитч-миров = рекорд

Абсолютный рекорд. 38 входов глитч-миров указывают на один уровень (тайлы 2-1 + спрайты $9F51). Почему? Потому что sprite pointer $9F51 падает в зону ROM, используемую как padding/звуковые данные, переиспользуемые кучей ID.

Level ID $49 (Underground) : уровень FDS

Tile pointer $76AE + sprite pointer $1C9D. Tile pointer указывает в зону ROM, зарезервированную за версией Famicom Disk System. Итог : уровень с тайлами, не существующими в стандартном картридже. Это уровень, показывающий уровень 52-1 и 196-1.

Level ID $00-$02 : настоящие бонусные уровни

Эти ID используются легитимными подуровнями игры :

  • $00 : подводная зона 5-2/6-2 (используется H-1, 39-1)
  • $01 : вода 2-2/7-2 (Minus World, 36-1)
  • $02 : подуровень 8-4 (136-1, 151-1, 215-1)

Разница между "бонусным" уровнем, доступным нормально, и глитч-миром в том, что warp-зоны проверяют текущий мир :

; Проверка warp zone (упрощённо)
; Игра проверяет, что целевой мир между 1 и 8
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; < 1 → отказ
  cmp #9
  bcs InvalidWarp       ; > 8 → отказ
  ; мир валиден только между 1 и 8
  jmp DoWarp

Глитч-миры с номерами > 8 или 0 не могут достигаться нормальными трубами. Нужен wall clip или cart swap.

Почему некоторые уровни крашатся : jump tables

Когда игра грузит объект тайла, она использует его тип как индекс в jump table :

; Jump table стандартных объектов тайлов (типы 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; тип 0 : блок ?, hidden, flagpole...
  .word Obj_Platform      ; тип 1 : спецплатформа
  .word Obj_BrickRow      ; тип 2 : ряд кирпичей
  .word Obj_BlockRow      ; тип 3 : ряд блоков
  .word Obj_CoinRow       ; тип 4 : ряд монет
  .word Obj_BrickCol      ; тип 5 : колонка кирпичей
  .word Obj_BlockCol      ; тип 6 : колонка блоков
  .word Obj_Pipe          ; тип 7 : труба
  .word Obj_8             ; тип 8
  .word Obj_9             ; тип 9
  .word Obj_10            ; тип 10
  .word Obj_11            ; тип 11

Jump tables : почему невалидный тип объекта крашит игру

Если у объекта невалидный тип (≥12), игра прыгает на указатель, не существующий в этой таблице. 4 возможных исхода :

  1. Валидный указатель → объект грузится нормально
  2. Указатель на другую jump table (перекрытие) → появляется другой объект. Пример : тип 12 указывает на таблицу Y=13, что даёт L-pipe.
  3. Указатель на исполняемое → выполнение случайного кода (вероятный краш)
  4. Явный плейсхолдер (NOP) → объект ничего не делает (некоторые спрайты такие, давая врагов, висящих на месте без движения)

Глитч-левел ID $58 : sprite pointer указывает на невалидный адрес, игра крашится

Глитч-левел ID $50 : cloud tunnel, уровень, сгенерированный повреждёнными данными

Глитч-левел ID $58 (крашающийся туннель) : его sprite pointer указывает на область памяти, не существующую на NES без ROM-мэппера. Игра пытается грузить одного и того же Koopa 5 раз за кадр в позицию (0,0), что насыщает PPU и вызывает фриз.

; Почему крашится ID $58 :
; Sprite pointer → невалидный адрес (вне стандартного пространства NES)
; → Игра читает неопределённые байты как типы спрайтов
; → Koopa $00 (невалидный тип без хендлера) петлит в рекурсивном вызове
; → Переполнение стека 6502 → фриз

Парадокс pipe warp

Помнишь проверку target_world BETWEEN 1 AND 8. Даже если найдёшь трубу в глитч-мире, игра проверяет, что мир назначения между 1 и 8. У глитч-миров номера > 8 (36-1, 255-1...), так что warp проваливается.

Поэтому у Minus World нет конца : flagpole отсутствует в спрайтах, а трубы ведут в никуда.

Трюк 5 объектов в колонке

Есть edge case, позволяющий обойти лимит 3 объектов на колонку. Когда очередь блокируется (слоты полны + у следующего объекта нет флага next screen), игра "предобрабатывает" текущую колонку в цикле, пока не найдёт объект с флагом next screen. Во время каждой предобработки :

; Во время предобработки колонки :
; 1. Объекты в очереди видят свою оставшуюся ширину
;    декрементируемой при каждой "ложной подвижке" колонки
; 2. Если объект достигает ширины=0, он покидает очередь
; 3. Освобождённый слот может заняться новым объектом,
;    добавленным в той же колонке

; Результат : до 5 объектов могут обработаться на одной колонке.
; Техника : поставить 2 объекта, пересекающих границу скрина
; (слоты 1 и 2), 1 dummy-объект на X < предыдущего (блокирует очередь),
; затем 3 объекта на X=0 следующего экрана (один с флагом next screen).

Это называется "queue skip", и используется некоторыми ромхакерами, чтобы создавать уровни плотнее, чем формат нормально позволяет.

Различия между версиями

Famicom Disk System

У версии FDS SMB1 другая memory map. Все указатели уровней сдвинуты, но данные те же. Что меняется : индексы глитч-миров совершенно другие :

Мир FDS 36 → Level ID $09 (водная версия 5-3)
  → Flagpole присутствует ! Уровень можно закончить.
  → Затем : $27 (нормальный 7-3) → $44 (4-4 underground)
  → $44 финишируем → топор работает → конец игры !

Minus World FDS, стало быть, "бонусный мир", способный вести
к прохождению игры, в отличие от версии NES.

Мой любимый уровень FDS : ID $5F, подземная версия второй половины 3-3 в низком туннеле (жаль, что это autoscroller).

The Lost Levels (японский Super Mario Bros. 2)

Lost Levels много меняет :

  1. Одинаковый порядок tiles/sprites : больше никаких Frankenstein levels (tiles и sprites грузят один уровень даже с невалидным ID)
  2. Одна 16-битная таблица указателей вместо двух раздельных таблиц high/low
  3. 4 файла диска : ROM сплитована под FDS :
    • Файл 1 : миры 1-4
    • Файл 2 : миры 5-8
    • Файл 3 : мир 9 + звуковой движок
    • Файл 4 : миры A-D (совершенно другая таблица указателей)
  4. Тот же Level ID = 4 возможных уровня в зависимости от загруженного файла
  5. Больше никакого глитча Tennis : опция continue (продолжить с того же мира после game over) делает warm start бесполезным, а игра ресетится сразу, если мир > 9
  6. Новые объекты : ядовитый гриб, невидимый блок, невидимый блок fire flower, перевёрнутые трубы, ветер -- но вставлены в середину существующих списков → обратная несовместимость с SMB1
  7. Piranha Plants всегда красные после мира 4, зелёные springboards только в мирах 2/B/3/C/7

Super Mario All-Stars (SNES)

Прямой порт с теми же рутинами 6502 (SNES исполняет код NES в совместимом режиме) :

  • Warp zone пофикшена : больше никакого Minus World (вход в левую трубу до появления текста ведёт в правильный мир)
  • Падучесть : большинство глитч-уровней крашатся (кроме ID $6A и 9-1)
  • Добавлены замковые объекты : сделаны более уникальными
  • Но : wrong warp 4-2 всё ещё работает (не запатчен !)

Wrong warp 4-2 : баг размещения объекта

В 4-2 есть два объекта pipe-перехода : лоза (warp zone) и труба (coin cash room). Первый объект перехода (лоза) размещён задолго до появления лозы на экране. Второй (труба) размещён слишком поздно в уровне.

; Тайминг переходов в 4-2 :
; Объект перехода 1 (лоза → warp zone) : размещён за 3 экрана до лозы
; Объект перехода 2 (труба → coin cash) : размещён через 1 экран после трубы
;
; Нормально первый объект деактивируется до того, как Марио
; достигает трубы. Но если Марио идёт быстро (или использует
; шорткат блока B+right), переход лозы
; всё ещё активен, когда он касается трубы !
; → Игра грузит warp zone вместо coin cash.
;
; Если бы объект разместили сразу после лозы, но до
; трубы, бага бы не существовало.

Петлевые уровни

Как работают петли (8-4, 7-4)? У уровня есть чекпоинты с номерами экранов и захардкоженными Y-позициями :

; Чекпоинт : {screen_number, vertical_position}
; Если Марио проходит чекпоинт на правильной высоте → уровень продолжается
; Иначе → warp back на 4 экрана (64 блока)
;
; Чтобы сделать бесконечную петлю : vertical_position = $F0
; (ниже низа экрана) → провалидировать невозможно.
;
; Чекпоинты простые (один флаг), кроме мира 7,
; где используются триплеты (3 флага, надо провалить хотя бы 1)
;
; Warp back грубый : офсет tile-данных выставлен на захардкоженное
; значение, офсет sprite-данных сброшен в 0. Присутствующие враги
; выгружаются мгновенно → firebars исчезают.

Менять формат, а не код

Один из самых захватывающих уроков этой архитектуры в том, что разработчики SMB1 сумели создать очень выразительную систему уровней, ни разу не тронув код рендера 6502. Вся вариация между уровнями идёт из данных (указатели, объекты, спрайты, floor patterns), а не из кода.

256 глитч-миров существуют потому, что таблицы указателей размерены под 128 входов × 4 типа, а игра никогда не валидирует читаемые значения. Когда указатель падает в RAM, игра интерпретирует регистры Марио как тайлы. Когда указатель падает в звуковые данные, игра играет музыку в виде левел-дизайна. А когда jump tables оверфлоуят, игра исполняет что попало до краша.

More Super Mario Bros. Mechanics Explained -- 4-е видео

Чему тут можно научиться

  1. Разделение tiles/sprites : полная независимость двух слоёв, с разными порядками хранения, создающими уникальные Frankenstein levels
  2. RLE-сжатие + система объектов : уровни не битмапы, а списки размещённых объектов, с floor patterns для пола
  3. Очередь 3 слота : строгий лимит железа (и дизайна уровней)
  4. Полное доверие указателям : игра доверяет указателям и jump tables, что даёт либо играбельные глитчи, либо краши
  5. 256 байтов макс : лимит Y-регистра 6502, из-за которого данные повторяются, если зайти слишком далеко
  6. Warm start / cold start : система "продолжить", открывшая дверь cart swap Tennis → Mario

Самое красивое : всё это код 6502, умещающийся в 40KB. Код говорит напрямую с железом и оказывает полное доверие каждому доступу к памяти. Если указатель гнилой, игра крашится. А краши мы называем глитч-мирами.

3 вещи запомнить

  1. Глитч-миры это указатели, упавшие неудачно -- У игры 128 ID × 4 типа зон, но только 34 уникальных уровня. Когда номер мира повреждён (через Tennis или wall clip), игра грузит указатель, задуманный для другого уровня, и 512 возможных комбинаций дают непредсказуемые результаты.

  2. Minus World это баг warp в сочетании с порчей -- Левая труба в 1-2, если активировать до появления текста, грузит мир 36 (0x24). Этот мир указывает на Level ID $01 (вода 2-2), уровень без flagpole. А раз нет pipe-перехода для мира 36, уровень петлит бесконечно. Отсутствие проверки создаёт иконку.

  3. Tennis → Mario, за 15 лет до OoT → Paper Mario -- RAM NES переживает свап картриджа благодаря конденсаторам и системе warm start / cold start SMB1. Счётчик шагов Tennis (инкрементирующий байт RAM при проигрывании звука шагов) ровно падает на адрес номера мира. Нужно, чтобы цифры топ-счёта оставались 0, байт $A5 был цел, а игра детектила warm start -- идеальное стечение обстоятельств, сработавшее только с Tennis.

Оригинальные видео Retro Game Mechanics Explained это просто муравьиная работа -- уровень деталей по дизасму 6502, автоматическим картам всех уровней, объяснениям cart swap и warm start. Если не видел серию, глянь, она короткая, и каждая минута плотная.

Исходник карт доступен на rgmechex.com, а полный дизасм SMB1 открыт на куче репозиториев. 40 лет назад японские программисты написали эту систему уровней на 6502 с нулём юнит-тестов и нулём баг-трекера, а мы продолжаем учить техники оптимизации, открывая их код сегодня.

Super Mario Bros.: el formato de nivel, los punteros y los 256 glitch worlds

Cómo 128 niveles × 4 tipos de zona caben en 40KB de ROM, por qué existe el Minus World, y cómo un match de Tenis NES puede cargar glitch worlds.

Introducción

Super Mario Bros. son 40 kilobytes de ROM. Ocho mundos, 32 niveles, enemigos, música, power-ups, todo cabe ahí.

Pero si abres un emulador y trasteas los bytes buenos, puedes cargar el nivel 36-1. O el 255-1. O aterrizar en un mundo donde todo está hecho de sprites de Bowser y tuberías que no llevan a ninguna parte.

Esos glitch worlds existen por una razón simple: el sistema de almacenamiento de niveles de SMB1 es una maravilla de optimización 8-bit, y cuando fuerzas el juego a leer donde no debe, da resultados fascinantes.

Retro Game Mechanics Explained hizo una serie de 4 vídeos sobre eso: vamos a compilarla en una sola inmersión en el código 6502 del juego más vendido de su época.

GLITCH OBJECTS: el título de la serie RGMechEx sobre las mecánicas ocultas de SMB1

World 9-1: la pantalla título del primer glitch world accesible vía el cart swap Tennis

El warm start: por qué la RAM de Tennis sobrevive en SMB1

Antes de hablar de almacenamiento de niveles, hay que entender cómo arranca SMB1. Porque el glitch del cart swap NES Tennis se apoya por entero en el sistema de detección warm start / cold start del juego.

Los 41 bytes preservados

Cuando SMB1 detecta un cold start (primer encendido o apagado/encendido), borra toda la RAM. Pero cuando detecta un warm start (botón reset, sin corte de alimentación), preserva una zona de memoria de 41 bytes:

; Los 41 bytes preservados en RAM en un warm start
; Direcciones $075F-$0787
;
; $075F : byte de arranque (world - 1)    [1 byte]
; $0760 : flag de selección de mundo (botón B) [1 byte]
; $0761-$0762 : sin usar                    [2 bytes]
; $0763-$0768 : timer (6 dígitos, 3 mostrados) [6 bytes]
; $0769-$076E : monedas Luigi                   [6 bytes]
; $076F-$0774 : monedas Mario                   [6 bytes]
; $0775-$077A : puntuación Luigi                   [6 bytes]
; $077B-$0780 : puntuación Mario                   [6 bytes]
; $0781-$0786 : top score (6 dígitos, 1 oculto) [6 bytes]
; $0787 : el byte mágico $A5                 [1 byte]

Esos 41 bytes sirven a una sola función: permitir al jugador continuar en el mismo mundo tras un game over. Si mueres en 6-3, el juego escribe el mundo 6 en el byte de arranque, y en la pantalla título, si mantienes A + Start, recomienzas en 6-1.

Los 41 bytes preservados en RAM en un warm start: TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, y el byte mágico $A5

La doble comprobación del warm start

Cold start vs warm start: el diagrama de detección del reset

Cuando SMB1 arranca, no comprueba un solo criterio sino dos:

CheckWarmStart:
  ; 1. Comprobar el byte mágico $A5 en $0787
  lda $0787
  cmp #$A5
  bne ColdStart        ; no $A5 → cold start

  ; 2. Comprobar los 6 dígitos del top score ($0781-$0786)
  ;    Cada dígito tiene que estar entre 0 y 9
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; dígito >= 10 → cold start
  inx
  cpx #6
  bne CheckLoop

  ; Si ambas condiciones pasan → warm start
  ; La RAM no se borra, el mundo de arranque se preserva
  jmp WarmStartBoot

La comprobación del byte $A5 y los dígitos del top score: el corazón del warm start

¿Por qué una doble comprobación? Porque el byte $A5 podría estar presente por azar (otro juego que deja ese valor, o el estado de reposo por defecto del chip RAM). Comprobando que los dígitos del top score son válidos (0-9), nos aseguramos de que los datos son coherentes.

Por qué Tennis es el único juego que funciona

Cuando insertas SMB1 por primera vez (cold start), el juego:

  1. Borra toda la RAM → top score = 0, world byte = 0
  2. Escribe $A5 en la dirección $0787

Después, cambias a Tennis sin apagar la consola. Tennis:

  • No limpia la RAM al arrancar (pocos juegos NES lo hacen)
  • No escribe sobre los bytes del top score → quedan a 0 (válidos)
  • No toca el byte $A5 → sigue presente
  • Usa la dirección $075F para el contador de pasos del jugador
; El incremento de pasos en Tennis:
; A cada paso del jugador en la pista, Tennis incrementa el byte en $075F.
; Ese mismo byte lo usa SMB1 como "número de mundo - 1".
;
; 0 pasos  → world 0 → SMB1 = world 1
; 1-7 pasos → world 1-7 → mundos normales
; 8+ pasos → world 8+ → ¡glitch worlds!
;
; El contador solo se incrementa cuando la música se para
; (los sonidos de pasos no suenan durante la música).

Cuando vuelves a meter SMB1:

  1. El byte $A5 sigue ahí (Tennis no lo tocó)
  2. Los dígitos del top score siguen a 0 (válidos)
  3. El world byte ahora vale 8+ (incrementado por los pasos de Tennis)
  4. SMB1 detecta un warm start → preserva el world byte corrupto
  5. Mantener A + Start → world 9-1, world A-1, world 36-1, etc.

Por qué hay que arrancar Mario antes que Tennis

Una sutileza: hay que arrancar primero SMB1, luego Tennis, luego SMB1 otra vez. Si empezaras directamente por Tennis, el byte $A5 nunca se escribiría (Tennis no escribe $A5), así que la detección warm start fallaría y la RAM se borraría.

El contador de pasos de Tennis: cada footstep incrementa el world byte

Access Glitch Worlds via NES Tennis: el vídeo que explica el cart swap

Cómo SMB1 guarda sus niveles en 40KB

Nintendo R&D4 tuvo que resolver un problema simple en apariencia: representar niveles que hacen scroll horizontal con tiles, enemigos, items, todo en un presupuesto ROM ultrarrecortado.

La solución es una separación en dos capas de datos por completo independientes:

El tile layout (el mapa del nivel)

Cada nivel se define con un puntero hacia una estructura de tiles comprimida en ROM. La compresión es rudimentaria pero genial: un byte "control" seguido de 1-3 bytes de datos.

El formato tile usa un sistema de runs (tipo RLE):

; Formato tile SMB1 (simplificado)
; Cada "comando" es un byte control:
;
; $00-$7F : pone un tile, avanza 1 columna
; $80-$BF : pone un tile repetido N veces (N = byte - $80 + 1)
; $C0-$FF : comando especial (fin de línea, salto, cambio de paleta)

Ejemplo : para dibujar 3 ladrillos consecutivos :
  $82 $01    ; repite el tile $01 (brick) 3 veces

Cada nivel contiene 13 líneas de 16 columnas de tiles (13×16 = 208 tiles visibles). Pero el formato comprimido permite bajar mucho más: por ejemplo, el cielo y las columnas vacías casi no ocupan sitio.

El bucle de render en 6502:

; Descompresión tile - loop principal
; Entrada : puntero tile_data en $XX
; Salida : tilemap nivel en la RAM PPU

DecompressTile:
  lda (tile_ptr),y      ; leer byte control
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F : tile único
  cmp #$C0
  bcc RunLength         ; $80-$BF : run-length
  jmp SpecialCommand    ; $C0-$FF : comando especial

SingleTile:
  sta PPU_DATA          ; escribir el tile directamente
  jmp Next

RunLength:
  sec
  sbc #$7E              ; N = control - $7E
  tax
  lda (tile_ptr),y      ; leer el tile a repetir
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

El sprite layout (los enemigos y objetos)

En paralelo, los enemigos y objetos (bloques ?, tuberías, goombas, koopas) se guardan en una estructura por completo separada. Cada spawn se define con 2 bytes:

; Formato sprite SMB1
; Byte 0 : posición X (en columnas)
; Byte 1 : tipo de sprite + bits de página Y
; Y se deriva del índice en la secuencia

Una secuencia de sprites :
  $01 $4B    ; goomba en la columna 1
  $09 $4B    ; goomba en la columna 9
  $10 $61    ; bloque ? en la columna 16 (contiene moneda)
  $15 $54    ; koopa verde en la columna 21
  $FF        ; fin de secuencia

Cada nivel puede referenciar hasta 5 páginas de sprites distintas (en fin, 5 "screens" de 16 columnas), pero en la práctica la mayoría de los niveles solo usa 2-3.

La tabla de punteros

La genialidad del diseño es la tabla de punteros. Cada nivel se guarda como un par de direcciones ROM:

// Estructura interna (simplificada) del World Map
struct LevelPointer {
    uint16_t tile_ptr;   // Dirección ROM de los datos tiles
    uint16_t sprite_ptr; // Dirección ROM de los datos sprites
};

// 4 tablas separadas, una por AreaType :
// 0 = Water, 1 = Overworld, 2 = Underground, 3 = Castle
LevelPointer level_table[4][128];

128 entradas por tabla. 4 tipos de zona. 512 combinaciones posibles, pero solo una fracción la usa el juego oficial. El resto es RAM sin inicializar o datos que se interpretan como punteros.

Cuando el juego carga un nivel, hace eso:

; Carga de un nivel
; A = AreaType (0-3), X = LevelID (0-127)

LoadLevel:
  sta AREA_TYPE
  asl                  ; *2 para offset en tabla 16-bit
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; puntero hacia los tiles
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; puntero hacia los sprites
  jsr DecompressTiles

El juego concede una confianza total a los punteros: lee la dirección en la tabla y descomprime lo que hay en esa dirección, punto final.

Level ID $06 (Water): 9-1, la versión submarina de 6-2

La tabla de los Level IDs: 128 entradas posibles, 34 asignadas

El orden distinto de los punteros tiles y sprites: la causa de los Frankenstein levels

Los 34 niveles únicos y el sistema de ID 7-bit

El chip RAM de la NES (MB8416A): él conserva los datos cuando cambiamos los cartuchos

SMB1 no tiene 32 niveles, sino 34 niveles únicos. Muchos niveles son duplicados (5-3 = 1-3 pero con Bullet Bills) marcados con una bandera "hard mode". Los verdaderos niveles únicos:

  • Agua (Tipo 0): 3 niveles (2-2, 7-2, zona bonus 5-2/6-2)
  • Overworld (Tipo 1): 22 niveles (incluidas las 2 salas nubes bonus)
  • Underground (Tipo 2): 3 niveles (incluidas las salas bonus subterráneas)
  • Castle (Tipo 3): 6 niveles
  • + 1 sala cutscene (antes de los niveles subterráneos/agua)
  • + 1 warp zone de 4-2

Cada nivel tiene un ID en 7 bits. Los 5 bits de peso bajo = número en el subgrupo, los 2 bits de peso alto = tipo de zona:

; Codificación 7-bit del Level ID
; Bits 6-5 : Tipo (00=Water, 01=Overworld, 10=Underground, 11=Castle)
; Bits 4-0 : Número en el subgrupo
;
; Water IDs      : $00-$02  (tipos 00, números 0-2)
; Overworld IDs  : $20-$35  (tipos 01, números 0-21)
; Underground IDs: $40-$42  (tipos 10, números 0-2)
; Castle IDs     : $60-$65  (tipos 11, números 0-5)
;
; ID $25 = %0100101 → tipo 01 (Overworld), número 5 → 1-1
; ID $23 = %0100011 → tipo 01 (Overworld), número 3 → 6-2

128 IDs posibles ($00-$7F), solo 34 asignados a verdaderos niveles. Los IDs sin usar apuntan a cualquier cosa.

Las tablas de punteros: dos listas, dos órdenes

Los punteros tiles y sprites no se guardan en el mismo orden. El código usa dos listas 16-bit separadas (high byte / low byte en dos tablas distintas):

Orden de los punteros sprites :
  Índice 0-5   : Castle (6 niveles)
  Índice 6-27  : Overworld (22 niveles)
  Índice 28-30 : Underground (3 niveles)
  Índice 31-33 : Water (3 niveles)

Orden de los punteros tiles :
  Índice 0-2   : Water (3 niveles)
  Índice 3-24  : Overworld (22 niveles)
  Índice 25-27 : Underground (3 niveles)
  Índice 28-33 : Castle (6 niveles)

¿Por qué órdenes distintos? Ninguna razón técnica: probablemente así se organizaron los datos durante el desarrollo. Pero crea una consecuencia fascinante: cuando un ID de nivel es inválido, los punteros tiles y sprites cargan niveles distintos, creando Frankenstein levels.

Para navegar entre esas dos listas, el juego usa pequeñas tablas de offset (como un índice):

; Tablas de offset por tipo (Water, Overworld, Underground, Castle)
; Cada entrada = índice de inicio en la lista correspondiente

SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; Water=31, Overworld=6, Underground=28, Castle=0

TileOffsetTable:
  .byte $00, $03, $19, $1C    ; Water=0, Overworld=3, Underground=25, Castle=28

Para cargar el nivel 6-2 (ID $23, Overworld número 3):

; 1. Tipo = 01 (Overworld) → índice en tabla de offset = 1
; 2. Sprite offset = SpriteOffsetTable[1] = 6
;    Índice final = 6 + 3 (número de nivel) = 9 → 10º puntero sprites
; 3. Tile offset = TileOffsetTable[1] = 3
;    Índice final = 3 + 3 = 6 → 7º puntero tiles
; 4. Resultado : puntero tiles $A619 + puntero sprites $9ED0 = 6-2 ✓

Ahora, ¿qué pasa con un ID inválido como $43 (Underground número 3, que no existe)?

; ID $43, Tipo = 10 (Underground), número = 3
; Sprite offset = SpriteOffsetTable[2] = $1C = 28
;   Índice = 28 + 3 = 31 → 32º puntero sprites = ¡agua bonus 5-2!
; Tile offset = TileOffsetTable[2] = $19 = 25
;   Índice = 25 + 3 = 28 → 29º puntero tiles = ¡1-4 (Castle)!
;
; Resultado : un nivel subterráneo con los tiles de 1-4
; y los Bloopers de la zona agua de 5-2. Un verdadero Frankenstein.

Level ID $43: Frankenstein level : tiles 1-4 + sprites agua 5-2

Exploring Glitch Level Pointers: las tablas de offset explicadas

La world index table: cuando el overflow de world 9 crea un glitch level

La world index table: por qué world 9 hace overflow

Hay una tabla ROM de 8 bytes que da el índice del primer nivel de cada mundo (1-8). Y justo después, la tabla de los 36 Level IDs de todos los niveles en orden de juego.

; WorldIndexTable (8 bytes)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   -> Mundo 1 empieza en el nivel 0
;   -> Mundo 2 empieza en el nivel 5
;   -> Mundo 8 empieza en el nivel 33

; LevelIDTable (36 bytes)
  .byte $25, $28, $29, $26, $24, ... ; los 36 Level IDs

Cuando intentamos cargar world 9, el juego lee el 9º byte de WorldIndexTable... que no existe. Hace overflow de 1 byte en LevelIDTable, lee el valor $25, y luego usa $25 como índice en LevelIDTable (37ª entrada): lo que hace overflow de nuevo de 2 bytes en SpriteOffsetTable, y lee el valor 6.

; World 9 :
;   1. WorldIndexTable[8] (overflow) → lee $25 en LevelIDTable
;   2. LevelIDTable[37] (overflow) → lee el 2º byte de SpriteOffsetTable = 6
;   3. ID = 6 → Water level number 6 (que no existe)
;   4. Tile pointer = puntero water número 6 = tiles de 6-2
;   5. Sprite pointer = índice 31+6 = 37 > 33 → puntero inválido
;   6. Resultado : 6-2 bajo el agua con sprites glitcheados
;      → ¡world 9-1!

Para world G (16), el overflow va aún más lejos y cae en el Level ID $01, que es el nivel cutscene que precede 1-2:

; World G (16) :
;   WorldIndexTable[15] → lee $01 en LevelIDTable
;   LevelIDTable[1] = $29 (cutscene 1-2)
;   → world G-1 = la cutscene de entrada de 1-2

Por qué existen los glitch worlds

El juego tiene 32 niveles "legítimos" (8 mundos × 4 niveles). Pero la tabla de punteros hace 128 entradas por tipo de zona. Las entradas más allá del nivel 32 contienen lo que haya en ROM en esas direcciones: a veces otro nivel, a veces datos sonoros, a veces RAM, a veces cualquier cosa.

Level ID $01 Water (Minus World): tile pointer $AE45, sprite pointer $A171

Level ID $01 + AreaType 0 = Minus World

El más famoso de los glitch worlds. El Level ID $01 en AreaType 0 (agua) apunta hacia:

  • Tile pointer: $AE45 → la zona submarina de 2-2/7-2
  • Sprite pointer: $A171 → los sprites de 2-2/7-2

El resultado: un nivel de agua que se parece a 2-2, pero que entra en bucle infinito porque el flagpole no existe. El nivel gira en bucle para siempre.

Es el nivel 36-1 (o 36-1 en el mundo $-1).

El warm start check de SMB1: él permite que el Minus World exista

; Por qué falta el flagpole en el Minus World :
; Los sprites de 2-2/7-2 ($A171) no tienen flagpole
; en su secuencia. El juego busca el sprite $FD (flagpole)
; pero nunca lo encuentra → bucle infinito
;
; El juego sigue generando el nivel al infinito
; hasta que el timer llega a cero.

Los punteros que apuntan a la RAM

Cuando el tile pointer o el sprite pointer apunta a una dirección en RAM ($00-$7F) en vez de en ROM, el juego intenta interpretar los cambios constantes de la RAM como tiles:

; Ejemplo : Level ID $03 en Water
; Tile Pointer : $A46B (3-3 - válido)
; Sprite Pointer : $009D (¡apunta hacia la RAM página cero!)

; La RAM página cero contiene los registros del juego,
; la posición de Mario, el estado de los contadores...
; El juego descomprime eso como una secuencia de sprites,
; y el resultado es un nivel con enemigos
; que en realidad son valores de registros.

Cuando la página cero cambia (porque Mario se mueve, el timer corre, etc.), los "sprites" del nivel también cambian. Por eso algunos glitch worlds tienen enemigos que parpadean y se transforman constantemente.

Level ID $03 Water: sprite pointer $009D apunta hacia la RAM, nivel injugable

Level ID $36: el nivel vacío (Overworld)

Level ID $36 en Overworld:

  • Tile pointer: $AC35 (1-2)
  • Sprite pointer: $A0D8 (1-2)

Resultado: nada. El juego carga el nivel pero está marcado "sin nivel" en el catálogo de RGMechEx. Los tiles quizá sean válidos pero los sprites apuntan hacia un sitio que produce un nivel vacío o no funcional.

Level ID $1D (Castle): el campeón de los crashes

Level ID $1D en Castle:

  • Tile pointer: $A210 (4-4)
  • Sprite pointer: $7EA0 (¡RAM!)

Sprite pointer en RAM = undefined sprites. El juego intenta mostrar un Spiny ball o un Bullet Bill blaster en la primera línea de tiles. Crashea de inmediato.

; Cuando el sprite pointer apunta hacia la RAM,
; el juego descomprime bytes que cambian todo el tiempo
; como instrucciones "spawn". El resultado :
; - Aparición de objetos inexistentes (valor undefined)
; - Crash PPU cuando el sprite NES intenta mostrar un tile inválido
; - Freeze completo de la consola

Los 256 glitch worlds catalogados

RGMechEx escribió un script que genera los mapas de todos los niveles, para los 4 tipos de zona, y los 128 IDs cada uno.

El contador de mundo está en 8 bits (0-255). Los mundos 1-8 son legítimos. Quedan 248 glitch worlds potenciales. Cada glitch world corresponde al primer nivel de ese mundo, y su Level ID se calcula con el mecanismo de overflow de la WorldIndexTable.

Tabla de los glitch worlds: 248 mundos corruptos, 68 primeros niveles accesibles

De los 128 IDs posibles, solo 68 son "first level" de un mundo (accesibles vía el glitch world number). Los otros 60 son niveles 2+ o inaccesibles.

Tipo IDs únicos jugables IDs que crashean IDs vacíos
Water (0) ~20 ~60 ~48
Overworld (1) ~30 ~55 ~43
Underground (2) ~15 ~65 ~48
Castle (3) ~25 ~58 ~45

Muchos IDs llevan al mismo nivel por los punteros que caen en las mismas direcciones ROM. El Level ID $28 (Overworld) por ejemplo: tile pointer $A7CD (2-1), aparece en 38 glitch worlds distintos, porque su sprite pointer $9F51 apunta hacia una zona de la ROM que se usa como padding/datos sonoros reutilizado por montones de IDs.

Mapa del nivel ID $28 (Overworld): tiles 2-1 con sprites normales, 38 glitch worlds

Super Mario Bros. Glitch Levels Explained: el 3er vídeo

Los 6 glitch levels realmente únicos

Entre los 19 IDs de glitch level accesibles, solo 6 no crashean de inmediato al cargar:

World Level ID Descripción
E-1 (224) $50 Un solo bloque ? sobre un abismo. Mario muere al instante.
W $57 Mario aparece bloqueado, incapaz de moverse.
42 (133) $50 Túnel de nubes que atrapa Mario si va bastante lejos.
62 (131, 240) $4D Castillo congelado: Mario aparece arriba, no puede caer → bloqueado.
127 $4B Túnel subterráneo, pero crashea si vas demasiado lejos.
137 $4B Activa el scroll automático de las cutscenes. Mario se encuentra un único brick block que lo bloquea para siempre.

Level ID $50 (cloud tunnel): el glitch world 42-1 y E-1 Level ID $4D (castle): world 62-1, Mario bloqueado en el spawn Level ID $4B (tunnel): world 127-1, crashea si vas demasiado lejos

Seis glitch worlds de 248 que producen algo realmente nuevo. El resto son niveles normales con el tipo de zona equivocado, o pantallas negras.

El formato de los niveles en detalle

Vamos al formato exacto de los datos de nivel, para entender por qué los glitch levels se sostienen (o no).

El header nivel: 2 bytes, 6 propiedades

Cada nivel empieza con un header de 2 bytes que controla 6 propiedades:

; Byte 0 : timer + Y start + modifier
;   Bits 7-6 : timer (00=sin cambios, 01=200, 10=300, 11=400)
;   Bits 5-3 : Y start Mario (111/110 = autowalk)
;   Bits 2-0 : level type modifier
;              000=default, 001=waves, 010=brick wall,
;              011=water bottom, 100=night, 101=snow,
;              110=snow night, 111=gray night

; Byte 1 : platform + background + floor pattern
;   Bits 7-6 : special platform (00=tree, 01=mushroom,
;                                 10=Bullet Bill, 11=cloud)
;   Bits 5-4 : background (00=none, 01=clouds,
;                           10=montains, 11=fences)
;   Bits 3-0 : floor pattern inicial (0-15)

El type modifier controla variaciones visuales: las olas arriba de los niveles de agua, el fondo ladrillo de 8-3, la paleta noche de 4-3, la nieve de 6-2, etc.

Los objetos tiles: 2 bytes, Next Screen Flag, cola 3 slots

Tras el header viene una lista de objetos tile, cada objeto hace 2 bytes. El byte $FD marca el fin de la lista.

; Formato objeto tile (16 bits) :
; Byte 0 :
;   Bits 7-4 : posición X (columna 0-15)
;   Bits 3-0 : posición Y
;     Y=0-11  : posición Y normal
;     Y=12    : objetos especiales (agujeros, puentes, rope, ? blocks)
;     Y=13    : screen skip / objetos especiales 2
;     Y=14    : cambio de modifier/scenery/floor
;     Y=15    : objetos especiales 3 (castillo, escaleras, tubería grande)

; Byte 1 :
;   Bit  7   : NEXT SCREEN FLAG
;   Bits 6-4 : tipo de objeto (0-7)
;   Bits 3-0 : anchura/altura / subtipo

Cuando el bit "next screen" está puesto, la columna de trabajo actual se incrementa 1. Eso permite colocar objetos más allá de las 16 primeras columnas. Los objetos tienen que listarse en orden (izquierda a derecha) porque el juego los carga secuencialmente:

; La rutina de carga tiene DOS fases por columna :
; Fase 1 : buscar los nuevos objetos que empiezan en esa columna
;            y añadirlos a la cola de espera (queue)
; Fase 2 : procesar cada objeto en la cola dibujando los tiles,
;            y quitar los que terminan en esa columna

La cola hace exactamente 3 slots. Consecuencia directa: no puede haber más de 3 objetos que empiecen en la misma columna. Si la cola está llena, el 4º objeto se ignora y jamás se cargará.

Por eso los niveles bien diseñados evitan apilar demasiados objetos. Ejemplo en 1-2: la columna con el bloque 1up en el techo + los ladrillos de al lado se parte en dos objetos distintos para respetar el límite de 3.

Posición Y especial: 12, 13, 14, 15

Cuando Y=12, el objeto no tiene posición Y (está hardcodeada por tipo):

; Y=12 : objetos sin posición Y
;   Tipo 0 : agujero (quita el suelo)
;   Tipo 1 : rope de plataforma móvil
;   Tipos 2-4 : puentes en Y fija
;   Tipo 5 : agujero con agua/lava
;   Tipos 6-7 : filas de ? blocks

Cuando Y=13, dos subgrupos. Si el bit 6 del byte 1 está a 1:

; Y=13, bit6=1 : objetos especiales
;   0 = tubería L (cutscene), 1 = flagpole, 2-3 = puente/hacha/jammer (fin castillo)
;   4 = stop screen, 5 = random enemies, 6 = loop level, 7+ = posible crash

Si bit6=0, los 5 bits de peso bajo codifican un screen skip (saltar directo a una pantalla N, sin pasar por el next screen flag uno por uno).

Cuando Y=14: mismo principio con bit6=1 para cambiar el type modifier, bit6=0 para cambiar el fondo + floor pattern.

Los floor patterns: 16 motivos de suelo

El suelo de los niveles no está hecho de objetos individuales. SMB1 usa floor patterns, un motivo de fondo que se aplica a todas las columnas hasta el próximo cambio:

; Floor patterns (4 bits = 16 posibilidades)
;   0 = vacío total
;   1 = suelo 2 tiles alto
;   2 = suelo 1 tile alto
;   3 = suelo + bottom
;   4 = suelo + bottom 2
;   5 = suelo 1/2 tile
;   6 = 3/4 suelo
;   ... hasta 15 = relleno total (suelo + techo)

Por eso los agujeros son objetos: hacen override del floor pattern en una columna específica, sin tener que cambiar el patrón para todo lo demás.

El límite de 256 bytes y el repeat

Todos los datos tile de un nivel caben en 256 bytes máximo. El registro Y del 6502 se usa como índice, y hace 8 bits. Si el juego llega al fin de los datos sin encontrar el byte $FD, vuelve al principio y repite los 256 bytes al infinito:

; Índice Y = 8 bits → máx 256 bytes de datos tiles
; Si Y hace overflow (255 → 0) sin encontrar $FD → repeat
; Igual para los sprites, pero los objetos pipe (3 bytes)
; desplazan la paridad del índice en cada carga.

Algunos glitch levels explotan ese repeat para generar niveles que duran "indefinidamente".

El sistema de sprites: 2 bytes + pipe transitions

Los sprites siguen un formato similar, pero sin header y con algunas diferencias clave. El byte $FF marca el fin de la lista.

; Formato sprite (2 bytes) :
; Byte 0 : posición X (columna)
; Byte 1 :
;   Bit 7 : NEXT SCREEN FLAG
;   Bits 6-0 : tipo de sprite
;       Algunos tipos incluyen : goomba, koopa, Blooper,
;       Bullet Bill, Lakitu, Spiny, plataformas,
;       comando warp zone, toad/princesa,
;       comandos de spawn de grupos de enemigos

El bit de peso bajo del byte 1 es el hard level flag: si está a 1, el sprite solo aparece en los niveles ≥ 5-3. Así se crean los niveles "hard mode".

Posición Y 15 = screen skip (idéntico a los tiles). Posición Y 14 = pipe transition (3 bytes):

; Sprite Y=14 : pipe/vine transition (¡3 bytes!)
;   Byte 0 : posición X
;   Byte 1 : bits 6-0 = Level ID 7-bit (destino)
;   Byte 2 : bits 4-0 = pantalla de destino
;            bits 7-5 = mundo donde esa transición es válida
;
; ¿Por qué un mundo? Las bonus rooms se reutilizan entre mundos.
; Ejemplo : la sala bonus de 1-1 también la usan 2-1 y 7-1.
; Esa sala tiene 3 transiciones, una por mundo, para que Mario
; reaparezca en el buen sitio.

Los sprites no tienen queue system. El único límite es que no puede haber más de 4 sprites cargados simultáneamente en la zona de spawn (justo fuera de pantalla a la derecha). Más allá, los sprites se ignoran.

Cómo acceder a los glitch worlds

Hay dos métodos principales.

El método clásico: el wall clip

El wall clip (paso a través de los muros) permite salir del nivel normal y caminar hasta la warzone oculta. Manipulando el contador de mundo vía la RAM, se puede cargar cualquier Level ID.

La técnica:

  1. World 1-2: ir a la tubería de fin oculta
  2. Hacer el wall clip en el muro derecho
  3. Caminar en el vacío hasta la zona warp
  4. El juego interpreta los valores como mundos

Pero ese método solo da acceso a una pequeña parte de los glitch worlds.

El método extremo: NES Tennis cart swap

Ver la sección "El warm start" arriba para el detalle completo. En resumen: el contador de pasos de Tennis escribe sobre el mismo byte RAM que el mundo de arranque de SMB1, y la detección warm start preserva ese valor.

El rincón de los trasteadores: el código para explorarlo todo

Si quieres explorar todos los glitch tú mismo en un emulador, puedes parchear el Level ID directamente:

; Patch para FCEUX / Mesen :
; Dirección RAM $075F = Level ID actual
; Dirección RAM $0760 = Area Type (0=Water, 1=Overworld, 2=Underground, 3=Castle)

; Ejemplo : cargar el Level 57 (0x39) en Overworld
; En el emulador, abrir el trazador de memoria y escribir :
; $075F = 0x39
; $0760 = 0x01
; Luego entrar en una tubería de warp o morir y recomenzar
; → El juego carga el nivel ID $39 en Overworld

RGMechEx publicó la lista completa de los 128 niveles × 4 tipos con mapas generados automáticamente en rgmechex.com. Cada entrada muestra el tile pointer, el sprite pointer, y un mapa visual del nivel.

Los niveles más wtf

Level ID $1F (Water): 15 glitch worlds en uno

El tile pointer $A302 (3-4) combinado con el sprite pointer $02A0 da 15 glitch worlds distintos (D-1, J-1, Y-1, Z-1, 55-1, 73-1...). Explicación: el sprite pointer apunta hacia una zona de la ROM con datos bastante cercanos a sprites válidos para producir resultados jugables, pero la combinación de los tiles de castillo 3-4 con sprites de overworld crea un render absurdo.

Level ID $28 (Overworld): 38 glitch worlds = récord

El récord absoluto. 38 entradas de glitch world apuntan hacia el mismo nivel (tiles 2-1 + sprites $9F51). ¿Por qué? Porque el sprite pointer $9F51 cae en una zona de la ROM que se usa como padding/datos sonoros reutilizado por montones de IDs.

Level ID $49 (Underground): el nivel FDS

Tile pointer $76AE + sprite pointer $1C9D. El tile pointer apunta hacia la zona de la ROM reservada a la versión Famicom Disk System. Resultado: un nivel con tiles que no existen en el cartucho estándar. Es el nivel que hace aparecer el nivel 52-1 y 196-1.

Level ID $00-$02: los verdaderos niveles bonus

Esos IDs los usan subniveles legítimos del juego:

  • $00: zona submarina de 5-2/6-2 (usado por H-1, 39-1)
  • $01: el agua de 2-2/7-2 (el Minus World, 36-1)
  • $02: subnivel de 8-4 (136-1, 151-1, 215-1)

La diferencia entre un nivel "bonus" accesible normalmente y un glitch world es que las warp zones comprueban el mundo actual:

; Comprobación warp zone (simplificado)
; El juego comprueba que el mundo objetivo esté entre 1 y 8
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; < 1 → rechazado
  cmp #9
  bcs InvalidWarp       ; > 8 → rechazado
  ; mundo válido entre 1 y 8 solo
  jmp DoWarp

Los glitch worlds con números > 8 o 0 no pueden alcanzarse con tuberías normales. Hace falta el wall clip o el cart swap.

Por qué algunos niveles crashean: las jump tables

Cuando el juego carga un objeto tile, usa su tipo como índice en una jump table:

; Jump table de los objetos tiles estándar (tipos 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; tipo 0 : bloque ?, hidden, flagpole...
  .word Obj_Platform      ; tipo 1 : plataforma especial
  .word Obj_BrickRow      ; tipo 2 : fila de ladrillos
  .word Obj_BlockRow      ; tipo 3 : fila de bloques
  .word Obj_CoinRow       ; tipo 4 : fila de monedas
  .word Obj_BrickCol      ; tipo 5 : columna de ladrillos
  .word Obj_BlockCol      ; tipo 6 : columna de bloques
  .word Obj_Pipe          ; tipo 7 : tubería
  .word Obj_8             ; tipo 8
  .word Obj_9             ; tipo 9
  .word Obj_10            ; tipo 10
  .word Obj_11            ; tipo 11

Las jump tables: por qué un tipo de objeto inválido hace crashear el juego

Si un objeto tiene un tipo inválido (≥12), el juego salta a un puntero que no existe en esa tabla. 4 outcomes posibles:

  1. Puntero válido → el objeto se carga normalmente
  2. Puntero hacia otra jump table (solapamiento) → aparece un objeto distinto. Ejemplo: tipo 12 apunta hacia la tabla Y=13, lo que da una tubería L.
  3. Puntero hacia ejecutable → ejecución de código aleatorio (crash probable)
  4. Placeholder explícito (NOP) → el objeto no hace nada (algunos sprites son así, produciendo enemigos que vuelan en el sitio sin moverse)

Glitch level ID $58: el sprite pointer apunta hacia una dirección inválida, el juego crashea

Glitch level ID $50: el cloud tunnel, un nivel generado por datos corruptos

La glitch level ID $58 (el túnel que crashea): su sprite pointer apunta hacia una región de memoria que no existe en NES sin mapper ROM. El juego intenta cargar el mismo Koopa 5 veces por frame en la posición (0,0), lo que satura la PPU y provoca un freeze.

; Por qué ID $58 crashea :
; Sprite pointer → dirección inválida (fuera del espacio NES estándar)
; → El juego lee bytes indeterminados como tipos de sprite
; → El Koopa $00 (tipo inválido sin handler) entra en llamada recursiva
; → Stack overflow 6502 → freeze

La paradoja pipe warp

Recuerda el check target_world BETWEEN 1 AND 8. Aunque encuentres una tubería en un glitch world, el juego comprueba que el mundo de destino esté entre 1 y 8. Los glitch worlds tienen números > 8 (36-1, 255-1...), así que el warp falla.

También por eso el Minus World no tiene fin: el flagpole no está presente en los sprites, y las tuberías no llevan a ninguna parte.

El truco de los 5 objetos en una columna

Existe un edge case que permite saltarse el límite de 3 objetos por columna. Cuando la cola se bloquea (slots llenos + objeto siguiente sin next screen flag), el juego "preprocesa" la columna actual en bucle hasta encontrar un objeto con next screen flag. Durante cada preprocesamiento:

; Durante el preprocesamiento de columna :
; 1. Los objetos en la cola ven su anchura restante
;    decrementada en cada "falso avance" de columna
; 2. Si un objeto llega a anchura=0, sale de la cola
; 3. Un slot liberado puede rellenarse con un objeto nuevo
;    añadido en la misma columna

; Resultado : hasta 5 objetos pueden procesarse en la misma columna.
; Técnica : colocar 2 objetos que atraviesen la screen boundary
; (slots 1 y 2), 1 objeto dummy en X < previo (bloquea la cola),
; luego 3 objetos en X=0 de la pantalla siguiente (uno con next screen flag).

Eso se llama un "queue skip" y lo usan algunos romhackers para crear niveles más densos de lo que el formato permite normalmente.

Las diferencias entre versiones

Famicom Disk System

La versión FDS de SMB1 tiene una memory map distinta. Todos los punteros de nivel están desplazados, pero los datos son los mismos. Lo que cambia: los índices de los glitch worlds son por completo distintos:

FDS World 36 → Level ID $09 (agua versión de 5-3)
  → ¡El flagpole está presente! Se puede terminar el nivel.
  → Después: $27 (7-3 normal) → $44 (4-4 underground)
  → $44 es terminable → el hacha funciona → ¡fin del juego!

El Minus World FDS es por tanto un "bonus world" que puede llevar
a completar el juego, al contrario que la versión NES.

Mi nivel FDS favorito: ID $5F, una versión subterránea de la segunda mitad de 3-3 en túnel bajo (lástima que sea un autoscroller).

The Lost Levels (Super Mario Bros. 2 japonés)

Lost Levels cambia muchas cosas:

  1. Orden idéntico tiles/sprites: se acabaron los Frankenstein levels (tiles y sprites cargan el mismo nivel incluso con un ID inválido)
  2. Una sola tabla de punteros 16-bit en vez de dos tablas separadas high/low
  3. 4 archivos disco: la ROM se partió para el FDS:
    • Archivo 1: worlds 1-4
    • Archivo 2: worlds 5-8
    • Archivo 3: world 9 + sound engine
    • Archivo 4: worlds A-D (tabla de punteros por completo distinta)
  4. Mismo Level ID = 4 niveles posibles según el archivo cargado
  5. Se acabó el glitch Tennis: la opción continue (continue en el mismo mundo tras game over) vuelve el warm start inútil, y el juego resetea de inmediato si world > 9
  6. Objetos nuevos: champiñón veneno, bloque invisible, bloque invisible fire flower, upside down pipes, viento, pero insertados en medio de las listas existentes → incompatibilidad backward con SMB1
  7. Piranha Plants siempre rojas tras world 4, springboards verdes solo en worlds 2/B/3/C/7

Super Mario All-Stars (SNES)

Porteo directo con las mismas rutinas 6502 (el SNES ejecuta el código NES en modo compatible):

  • Warp zone arreglada: se acabó el Minus World (entrar en la tubería izquierda antes del texto lleva al buen mundo)
  • Crasheo: la mayoría de los glitch levels crashean (salvo ID $6A y 9-1)
  • Objetos castillo añadidos: renders más únicos
  • Pero: el 4-2 wrong warp aún funciona (¡no parcheado!)

El 4-2 wrong warp: un bug de colocación de objeto

En 4-2, hay dos objetos de transición pipe: la enredadera (warp zone) y la tubería (sala monedas). El primer objeto de transición (el de la enredadera) está colocado mucho antes de que la enredadera aparezca en pantalla. El segundo (la tubería) está colocado demasiado tarde en el nivel.

; Timing de las transiciones en 4-2 :
; Objeto transición 1 (enredadera → warp zone) : colocado 3 pantallas antes de la enredadera
; Objeto transición 2 (tubería → sala monedas) : colocado 1 pantalla tras la tubería
;
; Normalmente el primer objeto se desactiva antes de que Mario
; alcance la tubería. Pero si Mario va rápido (o usa
; el atajo del bloque B+right), ¡la transición de la enredadera
; sigue activa cuando toca la tubería!
; → El juego carga la warp zone en vez de la sala monedas.
;
; Si el objeto se hubiera colocado justo tras la enredadera pero antes
; de la tubería, el bug no existiría.

Los niveles con bucle

¿Cómo funcionan los loop (8-4, 7-4)? El nivel tiene checkpoints con números de pantalla y posiciones Y hardcodeados:

; Checkpoint : {número_pantalla, posición_vertical}
; Si Mario pasa ese checkpoint a la buena altura → el nivel sigue
; Si no → warp back de 4 pantallas (64 bloques)
;
; Para hacer un bucle infinito : posición_vertical = $F0
; (bajo el fondo de la pantalla) → imposible de validar.
;
; Los checkpoints son simples (una sola bandera) salvo para world 7
; que usa tripletes (3 banderas, hay que fallar al menos 1)
;
; El warp back es rudo : offset de tile data puesto a un valor
; hardcodeado, offset de sprite data puesto a 0. Los enemigos presentes
; se descargan al instante → las firebars desaparecen.

Cambiar el formato, no el código

Una de las lecciones más fascinantes de esa arquitectura es que los desarrolladores de SMB1 lograron crear un sistema de nivel muy expresivo sin tocar jamás el código de render 6502. Toda la variación entre los niveles viene de los datos (punteros, objetos, sprites, floor patterns), no del código.

Los 256 glitch worlds existen porque las tablas de punteros están dimensionadas para 128 entradas × 4 tipos, y el juego nunca valida los valores que lee. Cuando un puntero cae en RAM, el juego interpreta los registros de Mario como tiles. Cuando un puntero cae en los datos sonoros, el juego toca música en forma de level design. Y cuando las jump tables hacen overflow, el juego ejecuta cualquier cosa hasta el crash.

More Super Mario Bros. Mechanics Explained: el 4º vídeo

Lo que podemos aprender de todo eso

  1. Separación tiles/sprites: independencia total de ambas capas, con órdenes de almacenamiento distintos que crean Frankenstein levels únicos
  2. Compresión RLE + sistema de objetos: los niveles no son bitmaps sino listas de objetos colocados, con floor patterns para el suelo
  3. Cola 3 slots: límite estricto del hardware (y del diseño de nivel)
  4. Confianza total a los punteros: el juego confía en los punteros y las jump tables, lo que produce o bien glitchs jugables, o bien crashes
  5. 256 bytes máx: el límite del registro Y 6502, que hace que los datos se repitan si vamos demasiado lejos
  6. Warm start / cold start: un sistema de "continuar" que abrió la puerta al cart swap Tennis → Mario

Lo más bello: todo eso es código 6502 que cabe en 40KB. El código dialoga directo con el hardware y concede una confianza total a cada acceso memoria. Si el puntero está podrido, el juego crashea. Y los crashes los llamamos glitch worlds.

Las 3 cosas a recordar

  1. Los glitch worlds son punteros que caen mal: El juego tiene 128 IDs × 4 tipos de zona, pero solo 34 niveles únicos. Cuando el world number se corrompe (por Tennis o wall clip), el juego carga un puntero diseñado para otro nivel, y las 512 combinaciones posibles producen resultados imprevisibles.

  2. El Minus World es un bug de warp combinado con corrupción: La tubería izquierda en 1-2, si se activa antes de que el texto aparezca, carga world 36 (0x24). Ese world apunta hacia Level ID $01 (agua de 2-2), un nivel sin flagpole. Y como no hay transición pipe para world 36, el nivel entra en bucle infinito. La ausencia de comprobación crea el icono.

  3. Tennis → Mario, 15 años antes que OoT → Paper Mario: La RAM de la NES sobrevive a un cambio de cartucho gracias a los condensadores y al sistema de warm start / cold start de SMB1. El contador de pasos de Tennis (que incrementa un byte RAM al tocar el sonido de los pasos) cae justo en la dirección del world number. Hace falta que los dígitos del top score queden a 0, que el byte $A5 quede intacto, y que el juego detecte un warm start: un concurso de circunstancias perfecto que solo funcionó con Tennis.

Los vídeos originales de Retro Game Mechanics Explained son un puto trabajo de hormiga: el nivel de detalle sobre el desensamblado 6502, los mapas automáticos de todos los niveles, las explicaciones del cart swap y del warm start. Si no viste la serie, mírala, es corta y cada minuto es denso.

El código fuente de los mapas está disponible en rgmechex.com, y el desensamblado completo de SMB1 es open source en montones de repos. Hace 40 años, programadores japoneses escribieron ese sistema de nivel en 6502 con cero test unitario y cero bug tracker, y seguimos aprendiendo técnicas de optimización abriendo su código hoy.

Super Mario Bros.: o formato de nível, os ponteiros e as 256 glitch worlds

Como 128 níveis × 4 tipos de zona cabem em 40KB de ROM, por que o Minus World existe, e como uma partida de Tennis NES pode carregar glitch worlds.

Introdução

Super Mario Bros. são 40 kilobytes de ROM. Oito mundos, 32 níveis, inimigos, música, power-ups, tudo cabe ali.

Mas se você abrir um emulador e fuçar nos bytes certos, pode carregar o nível 36-1. Ou o 255-1. Ou cair num mundo onde tudo é feito de sprites do Bowser e de canos que não levam a lugar nenhum.

Essas glitch worlds existem por uma razão simples: o sistema de armazenamento de níveis do SMB1 é uma obra-prima de otimização de 8-bit, e quando você força o jogo a ler onde não deveria, os resultados são fascinantes.

Retro Game Mechanics Explained fez uma série de 4 vídeos sobre isso -- vamos compilar tudo em uma única imersão no código 6502 do jogo mais vendido da sua época.

GLITCH OBJECTS -- o título da série do RGMechEx sobre as mecânicas ocultas do SMB1

World 9-1 -- a tela título da primeira glitch world acessível via o cart swap do Tennis

O warm start: por que a RAM do Tennis sobrevive no SMB1

Antes de falar sobre armazenamento de níveis, é preciso entender como o SMB1 inicia. Porque o glitch do cart swap do Tennis NES depende inteiramente do sistema de detecção warm start / cold start do jogo.

Os 41 bytes preservados

Quando o SMB1 detecta um cold start (primeira vez ligado ou power off/on), ele apaga toda a RAM. Mas quando detecta um warm start (reset pelo botão, sem corte de energia), ele preserva uma área de memória de 41 bytes:

; Les 41 bytes préservés en RAM lors d'un warm start
; Adresses $075F-$0787
;
; $075F : byte de démarrage (world - 1)    [1 byte]
; $0760 : flag de sélection de monde (B button) [1 byte]
; $0761-$0762 : inutilisé                    [2 bytes]
; $0763-$0768 : timer (6 digits, 3 affichés) [6 bytes]
; $0769-$076E : coins Luigi                   [6 bytes]
; $076F-$0774 : coins Mario                   [6 bytes]
; $0775-$077A : score Luigi                   [6 bytes]
; $077B-$0780 : score Mario                   [6 bytes]
; $0781-$0786 : top score (6 digits, 1 caché) [6 bytes]
; $0787 : le byte magique $A5                 [1 byte]

Esses 41 bytes servem para uma única funcionalidade: permitir que o jogador continue no mesmo mundo após um game over. Se você morrer em 6-3, o jogo grava o mundo 6 no byte de início, e na tela título, se você segurar A + Start, recomeça em 6-1.

Os 41 bytes preservados na RAM durante um warm start -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, e o byte mágico $A5

A dupla verificação do warm start

Cold start vs warm start -- o diagrama de detecção do reset

Quando o SMB1 inicia, ele não verifica um único critério mas dois:

CheckWarmStart:
  ; 1. Vérifier le byte magique $A5 à $0787
  lda $0787
  cmp #$A5
  bne ColdStart        ; pas $A5 → cold start

  ; 2. Vérifier les 6 digits du top score ($0781-$0786)
  ;    Chaque digit doit être entre 0 et 9
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; digit >= 10 → cold start
  inx
  cpx #6
  bne CheckLoop

  ; Si les deux conditions passent → warm start
  ; La RAM n'est pas effacée, le monde de départ est préservé
  jmp WarmStartBoot

A verificação do byte $A5 e dos dígitos do top score -- o coração do warm start

Por que uma dupla verificação? Porque o byte $A5 poderia estar presente por acaso (outro jogo que deixou esse valor, ou o estado de repouso padrão do chip RAM). Ao verificar que os dígitos do top score são válidos (0-9), garante-se que os dados são coerentes.

Por que o Tennis é o único jogo que funciona

Quando inserimos o SMB1 pela primeira vez (cold start), o jogo:

  1. Apaga toda a RAM → top score = 0, world byte = 0
  2. Grava $A5 no endereço $0787

Depois, fazemos o swap para o Tennis sem desligar o console. O Tennis:

  • Não limpa a RAM ao iniciar (poucos jogos de NES fazem isso)
  • Não grava nos bytes do top score → eles ficam em 0 (válidos)
  • Não toca no byte $A5 → ele continua presente
  • Usa o endereço $075F para o contador de passos do jogador
; Le footstep increment dans Tennis :
; À chaque pas du joueur sur le court, Tennis incrémente le byte à $075F.
; Ce même byte est utilisé par SMB1 comme "world number - 1".
;
; 0 pas  → world 0 → SMB1 = world 1
; 1-7 pas → world 1-7 → worlds normaux
; 8+ pas → world 8+ → glitch worlds !
;
; Le compteur ne s'incrémente que quand la musique s'arrête
; (les footstep sounds ne jouent pas pendant la musique).

Quando volamos ao SMB1:

  1. O byte $A5 ainda está lá (o Tennis não o tocou)
  2. Os dígitos do top score ainda são 0 (válidos)
  3. O world byte agora vale 8+ (incrementado pelos passos do Tennis)
  4. O SMB1 detecta um warm start → preserva o world byte corrompido
  5. Manter A + Start → world 9-1, world A-1, world 36-1, etc.

Por que é necessário iniciar o Mario antes do Tennis

Uma sutileza: é preciso primeiro iniciar o SMB1, depois o Tennis, e depois o SMB1 novamente. Se você começasse direto pelo Tennis, o byte $A5 nunca seria gravado (o Tennis não grava $A5), portanto a detecção de warm start falharia e a RAM seria apagada.

O contador de passos do Tennis: cada footstep incrementa o world byte

Acessando Glitch Worlds via NES Tennis -- o vídeo que explica o cart swap

Como o SMB1 armazena seus níveis em 40KB

A Nintendo R&D4 teve que resolver um problema aparentemente simples: representar níveis que rolam horizontalmente com tiles, inimigos, itens, tudo em um orçamento de ROM ultra apertado.

A solução é uma separação em duas camadas de dados completamente independentes:

O tile layout (o mapa do nível)

Cada nível é definido por um ponteiro para uma estrutura de tiles comprimida na ROM. A compressão é rudimentar, mas brilhante: um byte "controle" seguido de 1-3 bytes de dados.

O formato de tile usa um sistema de runs (tipo RLE):

; Format tile SMB1 (simplifié)
; Chaque "commande" est un byte contrôle :
;
; $00-$7F : pose une tile, avance d'1 colonne
; $80-$BF : pose une tile répétée N fois (N = byte - $80 + 1)
; $C0-$FF : commande spéciale (fin de ligne, saut, changement de palette)

Exemple : pour dessiner 3 briques consécutives :
  $82 $01    ; répète la tile $01 (brick) 3 fois

Cada nível contém 13 linhas de 16 colunas de tiles (13×16 = 208 tiles visíveis). Mas o formato comprimido permite ir muito mais baixo -- por exemplo, o céu e as colunas vazias quase não ocupam espaço.

O loop de renderização em 6502:

; Décompression tile - loop principale
; Entrée : pointeur tile_data en $XX
; Sortie : tilemap niveau dans la RAM PPU

DecompressTile:
  lda (tile_ptr),y      ; lire byte contrôle
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F : tile unique
  cmp #$C0
  bcc RunLength         ; $80-$BF : run-length
  jmp SpecialCommand    ; $C0-$FF : commande spéciale

SingleTile:
  sta PPU_DATA          ; écrire la tile directement
  jmp Next

RunLength:
  sec
  sbc #$7E              ; N = control - $7E
  tax
  lda (tile_ptr),y      ; lire la tile à répéter
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

O sprite layout (inimigos e objetos)

Paralelamente, inimigos e objetos (blocos ?, canos, goombas, koopas) são armazenados em uma estrutura completamente separada. Cada spawn é definido por 2 bytes:

; Format sprite SMB1
; Byte 0 : position X (en colonnes)
; Byte 1 : type de sprite + bits de page Y
; Y est dérivé de l'index dans la séquence

Une séquence de sprites :
  $01 $4B    ; goomba à la colonne 1
  $09 $4B    ; goomba à la colonne 9
  $10 $61    ; bloc ? à la colonne 16 (contient pièce)
  $15 $54    ; koopa verte à la colonne 21
  $FF        ; fin de séquence

Cada nível pode referenciar até 5 páginas de sprites diferentes (na verdade, 5 "telas" de 16 colunas), mas na prática a maioria dos níveis usa apenas 2-3.

A tabela de ponteiros

O gênio do design é a tabela de ponteiros. Cada nível é armazenado como um par de endereços ROM:

// Structure interne (simplifiée) du World Map
struct LevelPointer {
    uint16_t tile_ptr;   // Adresse ROM des données tiles
    uint16_t sprite_ptr; // Adresse ROM des données sprites
};

// 4 tables séparées, une par AreaType :
// 0 = Water, 1 = Overworld, 2 = Underground, 3 = Castle
LevelPointer level_table[4][128];

128 entradas por tabela. 4 tipos de zona. 512 combinações possíveis, mas apenas uma fração é usada pelo jogo oficial. O resto é RAM não inicializada ou dados que são interpretados como ponteiros.

Quando o jogo carrega um nível, ele faz isso:

; Chargement d'un niveau
; A = AreaType (0-3), X = LevelID (0-127)

LoadLevel:
  sta AREA_TYPE
  asl                  ; *2 pour offset dans table 16-bit
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; pointeur vers les tiles
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; pointeur vers les sprites
  jsr DecompressTiles

Nenhuma validação. Nenhuma verificação de que o ponteiro é válido. O jogo lê o endereço na tabela e descomprime o que estiver nesse endereço, ponto final.

Level ID $06 (Water) -- 9-1, a versão subaquática de 6-2

A tabela de Level IDs: 128 entradas possíveis, 34 atribuídas

A ordem diferente dos ponteiros tiles e sprites -- a causa dos Frankenstein levels

Os 34 níveis únicos e o sistema de ID de 7 bits

O chip RAM da NES (MB8416A) -- é ele que preserva os dados quando fazemos swap das cartuchos

O SMB1 não tem 32 níveis, mas 34 níveis únicos. Muitos níveis são duplicatas (5-3 = 1-3 mas com Bullet Bills) marcados por um flag de "hard mode". Os verdadeiros níveis únicos:

  • Água (Tipo 0): 3 níveis (2-2, 7-2, zona bônus 5-2/6-2)
  • Overworld (Tipo 1): 22 níveis (incluindo as 2 salas de nuvens bônus)
  • Underground (Tipo 2): 3 níveis (incluindo as salas bônus subterrâneas)
  • Castle (Tipo 3): 6 níveis
  • + 1 sala de cutscene (antes dos níveis subterrâneos/água)
  • + 1 warp zone de 4-2

Cada nível tem um ID de 7 bits. Os 5 bits de menor peso = número no subgrupo, os 2 bits de maior peso = tipo de zona:

; Encodage 7-bit du Level ID
; Bits 6-5 : Type (00=Water, 01=Overworld, 10=Underground, 11=Castle)
; Bits 4-0 : Numéro dans le sous-groupe
;
; Water IDs      : $00-$02  (types 00, numéros 0-2)
; Overworld IDs  : $20-$35  (types 01, numéros 0-21)
; Underground IDs: $40-$42  (types 10, numéros 0-2)
; Castle IDs     : $60-$65  (types 11, numéros 0-5)
;
; ID $25 = %0100101 → type 01 (Overworld), numéro 5 → 1-1
; ID $23 = %0100011 → type 01 (Overworld), numéro 3 → 6-2

128 IDs possíveis ($00-$7F), apenas 34 atribuídos a níveis reais. Os IDs não utilizados apontam para qualquer coisa.

As tabelas de ponteiros: duas listas, duas ordens

Os ponteiros tiles e sprites não são armazenados na mesma ordem. O código usa duas listas separadas de 16-bit (high byte / low byte em duas tabelas distintas):

Ordem dos ponteiros sprites:
  Index 0-5   : Castle (6 níveis)
  Index 6-27  : Overworld (22 níveis)
  Index 28-30 : Underground (3 níveis)
  Index 31-33 : Water (3 níveis)

Ordem dos ponteiros tiles:
  Index 0-2   : Water (3 níveis)
  Index 3-24  : Overworld (22 níveis)
  Index 25-27 : Underground (3 níveis)
  Index 28-33 : Castle (6 níveis)

Por que ordens diferentes? Nenhuma razão técnica -- provavelmente é só como os dados foram organizados durante o desenvolvimento. Mas isso cria uma consequência fascinante: quando um ID de nível é inválido, os ponteiros tiles e sprites carregam níveis diferentes, criando Frankenstein levels.

Para navegar entre essas duas listas, o jogo usa pequenas tabelas de offset (como uma tabela de conteúdo):

; Tables d'offset par type (Water, Overworld, Underground, Castle)
; Chaque entrée = index de début dans la liste correspondante

SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; Water=31, Overworld=6, Underground=28, Castle=0

TileOffsetTable:
  .byte $00, $03, $19, $1C    ; Water=0, Overworld=3, Underground=25, Castle=28

Para carregar o nível 6-2 (ID $23, Overworld número 3):

; 1. Type = 01 (Overworld) → index dans table d'offset = 1
; 2. Sprite offset = SpriteOffsetTable[1] = 6
;    Index final = 6 + 3 (numéro de niveau) = 9 → 10ème pointeur sprites
; 3. Tile offset = TileOffsetTable[1] = 3
;    Index final = 3 + 3 = 6 → 7ème pointeur tiles
; 4. Résultat : pointeur tiles $A619 + pointeur sprites $9ED0 = 6-2 ✓

Agora, o que acontece com um ID inválido como $43 (Underground número 3, que não existe)?

; ID $43, Type = 10 (Underground), numéro = 3
; Sprite offset = SpriteOffsetTable[2] = $1C = 28
;   Index = 28 + 3 = 31 → 32ème pointeur sprites = eau bonus 5-2 !
; Tile offset = TileOffsetTable[2] = $19 = 25
;   Index = 25 + 3 = 28 → 29ème pointeur tiles = 1-4 (Castle) !
;
; Résultat : un niveau souterrain avec les tiles de 1-4
; et les Bloopers de la zone eau de 5-2. Un vrai Frankenstein.

Level ID $43 -- Frankenstein level: tiles 1-4 + sprites água 5-2

Explorando Glitch Level Pointers -- as tabelas de offset explicadas

A world index table -- quando o overflow de world 9 cria um glitch level

A world index table: por que world 9 faz overflow

Existe uma tabela ROM de 8 bytes que dá o índice do primeiro nível de cada mundo (1-8). E logo em seguida, a tabela dos 36 Level IDs de todos os níveis na ordem de jogo.

; WorldIndexTable (8 bytes)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   -> Monde 1 commence au niveau 0
;   -> Monde 2 commence au niveau 5
;   -> Monde 8 commence au niveau 33

; LevelIDTable (36 bytes)
  .byte $25, $28, $29, $26, $24, ... ; les 36 Level IDs

Quando tentamos carregar world 9, o jogo lê o 9º byte da WorldIndexTable... que não existe. Ele faz overflow de 1 byte na LevelIDTable, lê o valor $25, depois usa $25 como índice na LevelIDTable (37ª entrada) -- o que faz overflow novamente de 2 bytes na SpriteOffsetTable, e lê o valor 6.

; World 9 :
;   1. WorldIndexTable[8] (overflow) → lit $25 dans LevelIDTable
;   2. LevelIDTable[37] (overflow) → lit le 2ème byte de SpriteOffsetTable = 6
;   3. ID = 6 → Water level number 6 (qui n'existe pas)
;   4. Tile pointer = pointeur water numéro 6 = tiles de 6-2
;   5. Sprite pointer = index 31+6 = 37 > 33 → pointeur invalide
;   6. Résultat : 6-2 sous l'eau avec des sprites glitchés
;      → world 9-1 !

Para world G (16), o overflow vai ainda mais longe e cai no Level ID $01, que é o nível cutscene que antecede 1-2:

; World G (16) :
;   WorldIndexTable[15] → lit $01 dans LevelIDTable
;   LevelIDTable[1] = $29 (cutscene 1-2)
;   → world G-1 = la cutscene d'entrée de 1-2

Por que as glitch worlds existem

O jogo tem 32 níveis "legítimos" (8 mundos × 4 níveis). Mas a tabela de ponteiros tem 128 entradas por tipo de zona. As entradas além do nível 32 contêm o que estiver na ROM nesses endereços -- às vezes outro nível, às vezes dados de som, às vezes RAM, às vezes qualquer coisa.

Level ID $01 Water (Minus World) -- tile pointer $AE45, sprite pointer $A171

Level ID $01 + AreaType 0 = Minus World

A mais famosa das glitch worlds. O Level ID $01 no AreaType 0 (água) aponta para:

  • Tile pointer: $AE45 → a zona subaquática de 2-2/7-2
  • Sprite pointer: $A171 → os sprites de 2-2/7-2

O resultado: um nível aquático que se parece com 2-2, mas que entra em loop infinito porque a flagpole não existe. Sem fim de nível, sem saída.

É o nível 36-1 (ou 36-1 no mundo $-1).

O warm start check do SMB1 -- é ele que permite ao Minus World existir

; Pourquoi le flagpole manque dans le Minus World :
; Les sprites de 2-2/7-2 ($A171) n'ont pas de flagpole
; dans leur séquence. Le jeu cherche le sprite $FD (flagpole)
; mais ne le trouve jamais → boucle infinie
;
; Le jeu continue de générer le niveau à l'infini
; jusqu'à ce que le timer atteigne zéro.

Os ponteiros que apontam para a RAM

Quando o tile pointer ou o sprite pointer aponta para um endereço na RAM ($00-$7F) em vez de na ROM, o jogo tenta interpretar as mudanças constantes da RAM como tiles:

; Exemple : Level ID $03 en Water
; Tile Pointer : $A46B (3-3 - valide)
; Sprite Pointer : $009D (pointe vers la RAM page zéro !)
;
; La RAM page zéro contient les registres du jeu,
; la position de Mario, l'état des compteurs...
; Le jeu décompresse ça comme une séquence de sprites,
; et le résultat c'est un niveau avec des ennemis
; qui sont en fait des valeurs de registres.

Quando a página zero muda (porque o Mario se move, o timer gira, etc.), os "sprites" do nível também mudam. É por isso que certas glitch worlds têm inimigos que piscam e se transformam constantemente.

Level ID $03 Water -- sprite pointer $009D aponta para a RAM, nível injogável

Level ID $36: o nível vazio (Overworld)

Level ID $36 no Overworld:

  • Tile pointer: $AC35 (1-2)
  • Sprite pointer: $A0D8 (1-2)

Resultado: nada. O jogo carrega o nível mas ele é marcado "sem nível" no catálogo do RGMechEx. Os tiles talvez sejam válidos mas os sprites apontam para um lugar que produz um nível vazio ou não funcional.

Level ID $1D (Castle): o campeão dos crashes

Level ID $1D no Castle:

  • Tile pointer: $A210 (4-4)
  • Sprite pointer: $7EA0 (RAM!)

Sprite pointer na RAM = sprites indefinidos. O jogo tenta exibir uma Spiny ball ou um Bullet Bill blaster na primeira linha de tiles. Isso causa crash imediatamente.

; Quand le sprite pointer pointe vers la RAM,
; le jeu décompresse des bytes qui changent tout le temps
; comme des instructions "spawn". Le résultat :
; - Apparition d'objets inexistants (valeur undefined)
; - Crash PPU quand le sprite NES essaie d'afficher un tile invalide
; - Freeze complet de la console

As 256 glitch worlds catalogadas

O RGMechEx escreveu um script que gera os mapas de todos os níveis, para os 4 tipos de zona, e os 128 IDs cada.

O contador de mundo tem 8 bits (0-255). Os mundos 1-8 são legítimos. Restam 248 glitch worlds potenciais. Cada glitch world corresponde ao primeiro nível desse mundo, e sua Level ID é calculada pelo mecanismo de overflow da WorldIndexTable.

Tabela de glitch worlds -- 248 mundos corrompidos, 68 primeiros níveis acessíveis

Dos 128 IDs possíveis, apenas 68 são o "primeiro nível" de um mundo (acessíveis via o número da glitch world). Os 60 restantes são níveis 2+ ou inacessíveis.

Tipo IDs únicos jogáveis IDs que crasham IDs vazios
Water (0) ~20 ~60 ~48
Overworld (1) ~30 ~55 ~43
Underground (2) ~15 ~65 ~48
Castle (3) ~25 ~58 ~45

Muitos IDs levam ao mesmo nível por causa dos ponteiros que caem nos mesmos endereços ROM. O Level ID $28 (Overworld), por exemplo -- tile pointer $A7CD (2-1) -- aparece em 38 glitch worlds diferentes, porque seu sprite pointer $9F51 aponta para uma zona da ROM que é usada como padding/dados sonoros reutilizados por vários IDs.

Mapa do nível ID $28 (Overworld) -- tiles 2-1 com sprites normais, 38 glitch worlds

Super Mario Bros. Glitch Levels Explained -- o 3º vídeo

Os 6 glitch levels realmente únicos

Dentre os 19 IDs de glitch level acessíveis, apenas 6 não crasham imediatamente no carregamento:

World Level ID Descrição
E-1 (224) $50 Um único bloco ? sobre um abismo. O Mario morre instantaneamente.
W $57 O Mario nasce preso, incapaz de se mover.
42 (133) $50 Túnel de nuvens que prende o Mario se ele for longe o suficiente.
62 (131, 240) $4D Castelo congelado: o Mario nasce lá em cima, não pode cair → preso.
127 $4B Túnel subterrâneo, mas crasha se for longe demais.
137 $4B Ativa o scroll automático das cutscenes. O Mario encontra um único bloco brick que o bloqueia para sempre.

Level ID $50 (túnel de nuvens) -- a glitch world 42-1 e E-1 Level ID $4D (castelo) -- world 62-1, Mario preso no spawn Level ID $4B (túnel) -- world 127-1, crasha se for longe demais

Seis glitch worlds de 248 que produzem algo realmente novo. O resto são níveis normais com o tipo de zona errado, ou telas pretas.

O formato dos níveis em detalhes

Mergulhando no formato exato dos dados de nível, para entender por que os glitch levels se sustentam (ou não).

O cabeçalho do nível: 2 bytes, 6 propriedades

Cada nível começa com um cabeçalho de 2 bytes que controla 6 propriedades:

; Byte 0 : timer + Y start + modifier
;   Bits 7-6 : timer (00=inchangé, 01=200, 10=300, 11=400)
;   Bits 5-3 : Y start Mario (111/110 = autowalk)
;   Bits 2-0 : level type modifier
;              000=default, 001=waves, 010=brick wall,
;              011=water bottom, 100=night, 101=snow,
;              110=snow night, 111=gray night

; Byte 1 : platform + background + floor pattern
;   Bits 7-6 : special platform (00=tree, 01=mushroom,
;                                 10=Bullet Bill, 11=cloud)
;   Bits 5-4 : background (00=none, 01=clouds,
;                           10=montains, 11=fences)
;   Bits 3-0 : floor pattern initial (0-15)

O tipo modifier controla variações visuais: as ondas no topo dos níveis aquáticos, o fundo de tijolos de 8-3, a paleta noturna de 4-3, a neve de 6-2, etc.

Os objetos tile: 2 bytes, flag de next screen, fila de 3 slots

Depois do cabeçalho vem uma lista de objetos tile, cada objeto tem 2 bytes. O byte $FD marca o fim da lista.

; Format objet tile (16 bits) :
; Byte 0 :
;   Bits 7-4 : X position (colonne 0-15)
;   Bits 3-0 : Y position
;     Y=0-11  : position Y normale
;     Y=12    : objets spéciaux (trous, ponts, rope, ? blocks)
;     Y=13    : screen skip / objets spéciaux 2
;     Y=14    : changement de modifier/scenery/floor
;     Y=15    : objets spéciaux 3 (château, escaliers, gros tuyau)

; Byte 1 :
;   Bit  7   : NEXT SCREEN FLAG
;   Bits 6-4 : type d'objet (0-7)
;   Bits 3-0 : largeur/hauteur / sous-type

Quando o bit "next screen" é setado, a coluna de trabalho atual é incrementada de 1. Isso permite colocar objetos além das 16 primeiras colunas. Os objetos devem ser listados em ordem (da esquerda para a direita) porque o jogo os carrega sequencialmente:

; La routine de chargement a DEUX phases par colonne :
; Phase 1 : chercher les nouveaux objets qui commencent sur cette colonne
;            et les ajouter à la file d'attente (queue)
; Phase 2 : traiter chaque objet dans la queue en dessinant les tiles,
;            et retirer ceux qui finissent sur cette colonne

A fila tem exatamente 3 slots. Consequência direta: não é possível ter mais de 3 objetos começando na mesma coluna. Se a fila estiver cheia, o 4º objeto é ignorado e nunca será carregado.

É por isso que níveis bem projetados evitam empilar muitos objetos. Exemplo em 1-2: a coluna com o bloco 1up no teto + os tijolos ao lado são divididos em dois objetos distintos para respeitar o limite de 3.

Y position especial: 12, 13, 14, 15

Quando Y=12, o objeto não tem posição Y (ela é fixa por tipo):

; Y=12 : objets sans Y position
;   Type 0 : trou (supprime le sol)
;   Type 1 : rope de plateforme mobile
;   Types 2-4 : ponts à Y fixe
;   Type 5 : trou avec eau/lave
;   Types 6-7 : rangées de ? blocks

Quando Y=13, dois subgrupos. Se o bit 6 do byte 1 estiver em 1:

; Y=13, bit6=1 : objets spéciaux
;   0 = L-pipe (cutscene), 1 = flagpole, 2-3 = pont/axe/hamer (fin château)
;   4 = stop screen, 5 = random enemies, 6 = loop level, 7+ = crash possible

Se bit6=0, os 5 bits de menor peso codificam um screen skip (pular diretamente para uma tela N, sem passar pelo next screen flag um por um).

Quando Y=14: mesmo princípio com bit6=1 para mudar o tipo modifier, bit6=0 para mudar o fundo + padrão de chão.

Os floor patterns: 16 padrões de chão

O chão dos níveis não é feito de objetos individuais. O SMB1 usa floor patterns, um padrão de fundo que se aplica a todas as colunas até a próxima mudança:

; Floor patterns (4 bits = 16 possibilités)
;   0 = vide total
;   1 = sol 2 tiles haut
;   2 = sol 1 tile haut
;   3 = sol + bottom
;   4 = sol + bottom 2
;   5 = sol 1/2 tile
;   6 = 3/4 sol
;   ... jusqu'à 15 = rempli total (sol + plafond)

É por isso que os buracos são objetos: eles sobrescrevem o floor pattern em uma coluna específica, sem precisar mudar o padrão para todo o resto.

O limite de 256 bytes e o repeat

Todos os dados tile de um nível cabem em no máximo 256 bytes. O registrador Y do 6502 é usado como índice, e ele tem 8 bits. Se o jogo chegar ao fim dos dados sem encontrar o byte $FD, ele volta ao começo e repete os 256 bytes infinitamente:

; Index Y = 8 bits → max 256 bytes de données tiles
; Si Y overflow (255 → 0) sans rencontrer $FD → repeat
; Même chose pour les sprites, mais les objets pipe (3 bytes)
; décalent la parité de l'index à chaque chargement.

Alguns glitch levels exploram esse repeat para gerar níveis que duram "indefinidamente".

O sistema de sprites: 2 bytes + transições de cano

Os sprites seguem um formato similar, mas sem header e com algumas diferenças-chave. O byte $FF marca o fim da lista.

; Format sprite (2 bytes) :
; Byte 0 : position X (colonne)
; Byte 1 :
;   Bit 7 : NEXT SCREEN FLAG
;   Bits 6-0 : type de sprite
;       Certains types incluent : goomba, koopa, Blooper,
;       Bullet Bill, Lakitu, Spiny, plateformes,
;       commande warp zone, toad/princesse,
;       commandes de spawn de groupes d'ennemis

O bit de menor peso do byte 1 é o hard level flag: se estiver em 1, o sprite só aparece nos níveis ≥ 5-3. É assim que os níveis "hard mode" são criados.

Y position 15 = screen skip (idêntico aos tiles). Y position 14 = transição de cano (3 bytes):

; Sprite Y=14 : pipe/vine transition (3 bytes !)
;   Byte 0 : position X
;   Byte 1 : bits 6-0 = Level ID 7-bit (destination)
;   Byte 2 : bits 4-0 = screen de destination
;            bits 7-5 = world où cette transition est valide
;
; Pourquoi un world ? Les bonus rooms sont réutilisées entre mondes.
; Exemple : la salle bonus de 1-1 est aussi utilisée par 2-1 et 7-1.
; Cette salle a 3 transitions, une par monde, pour que Mario
; réapparaisse au bon endroit.

Os sprites não têm um sistema de fila. A única limitação é que não pode haver mais de 4 sprites carregados simultaneamente na zona de spawn (logo fora da tela à direita). Acima disso, os sprites são ignorados.

Como acessar as glitch worlds

Existem dois métodos principais.

O método clássico: o wall clip

O wall clip (passar através das paredes) permite sair do nível normal e caminhar até a warp zone escondida. Manipulando o contador de mundo via RAM, é possível carregar qualquer Level ID.

A técnica:

  1. World 1-2: ir para o cano escondido no final
  2. Fazer o wall clip na parede da direita
  3. Caminhar no vazio até a zona warp
  4. O jogo interpreta os valores como mundos

Mas esse método só dá acesso a uma pequena parte das glitch worlds.

O método extremo: cart swap do NES Tennis

Ver a seção "O warm start" acima para o detalhe completo. Resumindo: o contador de passos do Tennis grava no mesmo byte RAM do mundo inicial do SMB1, e a detecção de warm start preserva esse valor.

O cantinho dos curiosos: o código para explorar tudo

Se você quiser explorar todas as glitch worlds você mesmo em um emulador, pode aplicar o patch no Level ID diretamente:

; Patch pour FCEUX / Mesen :
; Adresse RAM $075F = Level ID actuel
; Adresse RAM $0760 = Area Type (0=Water, 1=Overworld, 2=Underground, 3=Castle)

; Exemple : charger le Level 57 (0x39) en Overworld
; Dans l'émulateur, ouvrir le traceur mémoire et écrire :
; $075F = 0x39
; $0760 = 0x01
; Puis entrer dans un tuyau de warp ou mourir et recommencer
; → Le jeu charge le niveau ID $39 en Overworld

O RGMechEx publicou a lista completa dos 128 níveis × 4 tipos com mapas gerados automaticamente no rgmechex.com. Cada entrada mostra o tile pointer, o sprite pointer e um mapa visual do nível.

Os níveis mais wtf

Level ID $1F (Water): 15 glitch worlds em uma

O tile pointer $A302 (3-4) combinado com o sprite pointer $02A0 dá 15 glitch worlds diferentes (D-1, J-1, Y-1, Z-1, 55-1, 73-1...). Explicação: o sprite pointer aponta para uma zona da ROM que contém dados suficientemente próximos de sprites válidos para produzir resultados jogáveis, mas a combinação dos tiles de castelo 3-4 com sprites de overworld cria uma renderização absurda.

Level ID $28 (Overworld): 38 glitch worlds = recorde

O recorde absoluto. 38 entradas de glitch world apontam para o mesmo nível (tiles 2-1 + sprites $9F51). Por quê? Porque o sprite pointer $9F51 cai em uma zona da ROM que é usada como padding/dados sonoros reutilizados por vários IDs.

Level ID $49 (Underground): o nível FDS

Tile pointer $76AE + sprite pointer $1C9D. O tile pointer aponta para a zona da ROM reservada à versão do Famicom Disk System. Resultado: um nível com tiles que não existem na cartucho padrão. É o nível que faz aparecer o nível 52-1 e 196-1.

Level ID $00-$02: os verdadeiros níveis bônus

Esses IDs são usados por subníveis legítimos do jogo:

  • $00: zona subaquática de 5-2/6-2 (usado por H-1, 39-1)
  • $01: a água de 2-2/7-2 (o Minus World, 36-1)
  • $02: subnível de 8-4 (136-1, 151-1, 215-1)

A diferença entre um nível "bônus" acessível normalmente e uma glitch world é que as warp zones verificam o mundo atual:

; Vérification warp zone (simplifié)
; Le jeu vérifie que le monde cible est entre 1 et 8
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; < 1 → refusé
  cmp #9
  bcs InvalidWarp       ; > 8 → refusé
  ; world valide entre 1 et 8 uniquement
  jmp DoWarp

As glitch worlds com números > 8 ou 0 não podem ser alcançadas por canos normais. É preciso o wall clip ou o cart swap.

Por que certos níveis crasham: as jump tables

Quando o jogo carrega um objeto tile, ele usa seu tipo como índice em uma jump table:

; Jump table des objets tiles standards (types 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; type 0 : bloc ?, hidden, flagpole...
  .word Obj_Platform      ; type 1 : plateforme spéciale
  .word Obj_BrickRow      ; type 2 : rangée de briques
  .word Obj_BlockRow      ; type 3 : rangée de blocks
  .word Obj_CoinRow       ; type 4 : rangée de pièces
  .word Obj_BrickCol      ; type 5 : colonne de briques
  .word Obj_BlockCol      ; type 6 : colonne de blocks
  .word Obj_Pipe          ; type 7 : tuyau
  .word Obj_8             ; type 8
  .word Obj_9             ; type 9
  .word Obj_10            ; type 10
  .word Obj_11            ; type 11

As jump tables: por que um tipo de objeto inválido faz o jogo crashar

Se um objeto tiver um tipo inválido (≥12), o jogo pula para um ponteiro que não existe nessa tabela. 4 resultados possíveis:

  1. Ponteiro válido → o objeto carrega normalmente
  2. Ponteiro para outra jump table (sobreposição) → um objeto diferente aparece. Exemplo: tipo 12 aponta para a tabela Y=13, o que dá um L-pipe.
  3. Ponteiro para código executável → execução de código aleatório (crash provável)
  4. Placeholder explícito (NOP) → o objeto não faz nada (alguns sprites são assim, produzindo inimigos que ficam voando no lugar sem se mover)

Glitch level ID $58: o sprite pointer aponta para um endereço inválido, o jogo crasha

Glitch level ID $50: o cloud tunnel, um nível gerado por dados corrompidos

A glitch level ID $58 (o túnel que crasha): seu sprite pointer aponta para uma região de memória que não existe na NES sem mapper de ROM. O jogo tenta carregar o mesmo Koopa 5 vezes por frame na posição (0,0), o que satura a PPU e causa um freeze.

; Pourquoi ID $58 crashe :
; Sprite pointer → adresse invalide (hors espace NES standard)
; → Le jeu lit des bytes indéterminés comme des types de sprite
; → Le Koopa $00 (type invalide sans handler) boucle en appel récursif
; → Stack overflow 6502 → freeze

O paradoxo do pipe warp

Lembre-se do check target_world BETWEEN 1 AND 8. Mesmo que você encontre um cano em uma glitch world, o jogo verifica que o mundo de destino está entre 1 e 8. As glitch worlds têm números > 8 (36-1, 255-1...), então a warp falha.

É também por isso que o Minus World não tem fim: a flagpole não está presente nos sprites, e os canos não levam a lugar nenhum.

O truque dos 5 objetos em uma coluna

Existe um edge case que permite ultrapassar o limite de 3 objetos por coluna. Quando a fila trava (slots cheios + objeto seguinte com next screen flag faltando), o jogo "pré-processa" a coluna atual em loop até encontrar um objeto com next screen flag. Durante cada pré-processamento:

; Pendant le prétraitement de colonne :
; 1. Les objets dans la queue voient leur largeur restante
;    décrémentée à chaque "fausse avancée" de colonne
; 2. Si un objet atteint largeur=0, il quitte la queue
; 3. Un slot libéré peut être rempli par un nouvel objet
;    ajouté dans la même colonne

; Résultat : jusqu'à 5 objets peuvent être traités sur la même colonne.
; Technique : placer 2 objets qui traversent la screen boundary
; (slots 1 et 2), 1 objet dummy en X < précédent (bloque la queue),
; puis 3 objets à X=0 de l'écran suivant (dont un avec next screen flag).

Isso é o que chamamos de "queue skip" e é usado por certos romhackers para criar níveis mais densos do que o formato normalmente permite.

As diferenças entre versões

Famicom Disk System

A versão FDS do SMB1 tem um memory map diferente. Todos os ponteiros de nível são deslocados, mas os dados são os mesmos. O que muda: os índices das glitch worlds são completamente diferentes:

FDS World 36 → Level ID $09 (eau version de 5-3)
  → Le flagpole est présent ! On peut finir le niveau.
  → Ensuite : $27 (7-3 normal) → $44 (4-4 underground)
  → $44 est finissable → la hache fonctionne → fin du jeu !
  
Le Minus World FDS est donc un "bonus world" qui peut mener
à la complétion du jeu, contrairement à la version NES.

Meu nível FDS favorito: ID $5F, uma versão subterrânea da segunda metade de 3-3 em túnel baixo (pena que seja um autoscroller).

The Lost Levels (Super Mario Bros. 2 japonês)

Lost Levels muda muitas coisas:

  1. Ordem idêntica tiles/sprites: sem mais Frankenstein levels (tiles e sprites carregam o mesmo nível mesmo com um ID inválido)
  2. Uma única tabela de ponteiros 16-bit em vez de duas tabelas separadas high/low
  3. 4 arquivos de disco: a ROM foi dividida para o FDS:
    • Arquivo 1: mundos 1-4
    • Arquivo 2: mundos 5-8
    • Arquivo 3: mundo 9 + motor de som
    • Arquivo 4: mundos A-D (tabela de ponteiros completamente diferente)
  4. Mesmo Level ID = 4 níveis possíveis dependendo do arquivo carregado
  5. Sem glitch Tennis: a opção continue (continuar no mesmo mundo após game over) torna o warm start desnecessário, e o jogo reseta imediatamente se world > 9
  6. Novos objetos: cogumelo venenoso, bloco invisível, bloco invisível fire flower, canos de cabeça para baixo, vento -- mas inseridos no meio das listas existentes → incompatibilidade retroativa com SMB1
  7. Piranha Plants sempre vermelhas após world 4, springboards verdes apenas nos mundos 2/B/3/C/7

Super Mario All-Stars (SNES)

Port direto com as mesmas rotinas 6502 (o SNES executa o código NES em modo compatível):

  • Warp zone corrigida: sem mais Minus World (entrar no cano esquerdo antes do texto leva ao mundo correto)
  • Travamento: a maioria dos glitch levels crasha (exceto ID $6A e 9-1)
  • Objetos de castelo adicionados: renderizações mais únicas
  • Mas: o 4-2 wrong warp ainda funciona (não foi corrigido!)

O 4-2 wrong warp: um bug de posicionamento de objeto

Em 4-2, existem dois objetos de transição de cano: a videira (warp zone) e o cano (sala de moedas). O primeiro objeto de transição (o da videira) é posicionado muito antes que a videira apareça na tela. O segundo (o cano) é posicionado tarde demais no nível.

; Timing des transitions dans 4-2 :
; Objet transition 1 (vigne → warp zone) : placé 3 écrans avant la vigne
; Objet transition 2 (tuyau → coin cash) : placé 1 écran après le tuyau
;
; Normalement le premier objet est désactivé avant que Mario
; n'atteigne le tuyau. Mais si Mario va vite (ou utilise
; le raccourci du bloc B+right), la transition de la vigne
; est toujours active quand il touche le tuyau !
; → Le jeu charge la warp zone au lieu du coin cash.
;
; Si l'objet avait été placé juste après la vigne mais avant
; le tuyau, le bug n'existerait pas.

Os níveis em loop

Como funcionam os loops (8-4, 7-4)? O nível tem checkpoints com números de tela e posições Y fixos:

; Checkpoint : {screen_number, vertical_position}
; Si Mario passe ce checkpoint à la bonne hauteur → niveau continue
; Sinon → warp back de 4 écrans (64 blocks)
;
; Pour faire une boucle infinie : vertical_position = $F0
; (en dessous du bas de l'écran) → impossible de valider.
;
; Les checkpoints sont simples (un seul flag) sauf pour world 7
; qui utilise des triplets (3 flags, il faut en échouer au moins 1)
;
; Le warp back est rude : offset de tile data réglé à une valeur
; hardcodée, offset de sprite data remis à 0. Les ennemis présents
; sont déchargés instantanément → les firebars disparaissent.

Mudar o formato, não a código

Uma das lições mais fascinantes dessa arquitetura é que os desenvolvedores do SMB1 conseguiram criar um sistema de nível muito expressivo sem nunca tocar no código de renderização 6502. Toda a variação entre os níveis vem dos dados (ponteiros, objetos, sprites, floor patterns), não do código.

As 256 glitch worlds existem porque as tabelas de ponteiros são dimensionadas para 128 entradas × 4 tipos, e o jogo nunca valida os valores que lê. Quando um ponteiro cai na RAM, o jogo interpreta os registradores do Mario como tiles. Quando um ponteiro cai nos dados sonoros, o jogo toca música em forma de level design. E quando as jump tables fazem overflow, o jogo executa qualquer coisa até o crash.

More Super Mario Bros. Mechanics Explained -- o 4º vídeo

O que podemos aprender com tudo isso

  1. Separação tiles/sprites: independência total das duas camadas, com ordens de armazenamento diferentes que criam Frankenstein levels únicos
  2. Compressão RLE + sistema de objetos: os níveis não são bitmaps mas listas de objetos posicionados, com floor patterns para o chão
  3. Fila de 3 slots: limitação estrita do hardware (e do design de nível)
  4. Sem validação: o jogo confia nos ponteiros e nas jump tables, o que produz ou glitches jogáveis ou crashes
  5. 256 bytes no máximo: o limite do registrador Y do 6502, que faz os dados se repetirem se você for longe demais
  6. Warm start / cold start: um sistema de "continuar" que abriu a porta para o cart swap Tennis → Mario

O mais belo de tudo: é código 6502 que cabe em 40KB. Sem camada de abstração, sem validação de acesso a memória, sem gerenciador de exceções. Se o ponteiro estiver zoado, o jogo crasha. E os crashes, chamamos de glitch worlds.

Os 3 pontos para lembrar

  1. As glitch worlds são ponteiros que caem errado -- O jogo tem 128 IDs × 4 tipos de zona, mas apenas 34 níveis únicos. Quando o world number é corrompido (pelo Tennis ou wall clip), o jogo carrega um ponteiro feito para outro nível, e as 512 combinações possíveis produzem resultados imprevisíveis.

  2. O Minus World é um bug de warp combinado com corrupção -- O cano esquerdo em 1-2, se ativado antes que o texto apareça, carrega world 36 (0x24). Esse world aponta para Level ID $01 (água de 2-2), um nível sem flagpole. E como não há transição de cano para world 36, o nível entra em loop infinito. A ausência de verificação cria o ícone.

  3. Tennis → Mario, 15 anos antes de OoT → Paper Mario -- A RAM da NES sobrevive a um swap de cartucho graças aos capacitores e ao sistema de warm start / cold start do SMB1. O contador de passos do Tennis (que incrementa um byte RAM ao tocar o som dos passos) cai exato no endereço do world number. É preciso que os dígitos do top score fiquem em 0, que o byte $A5 esteja intacto, e que o jogo detecte um warm start -- uma conjugação de circunstâncias perfeita que só funcionou com o Tennis.

Os vídeos originais do Retro Game Mechanics Explained são um trabalho formidável -- o nível de detalhe na desmontagem 6502, os mapas automáticos de todos os níveis, as explicações do cart swap e do warm start. Se você não viu a série, assista, ela é curta e cada minuto é denso.

O código-fonte dos mapas está disponível no rgmechex.com, e a desmontagem completa do SMB1 é open source em vários repositórios. Há 40 anos, programadores japoneses escreveram esse sistema de nível em 6502 com zero testes unitários e zero rastreador de bugs, e continuamos aprendendo coisas ao abrir o código deles hoje.

Super Mario Bros. : format level, pointer, dan 256 glitch world

Bagaimana 128 level × 4 tipe zona muat dalam 40KB ROM, kenapa Minus World ada, dan bagaimana match Tenis NES bisa memuat glitch world.

Pendahuluan

Super Mario Bros. itu 40 kilobyte ROM. Delapan dunia, 32 level, musuh, musik, power-up, semuanya muat di sana.

Tapi kalau kamu membuka emulator dan mengutak-atik byte yang benar, kamu bisa memuat level 36-1. Atau 255-1. Atau mendarat di dunia di mana semuanya terbuat dari sprite Bowser dan pipa yang tidak menuju ke mana-mana.

Glitch world ini ada karena alasan sederhana : sistem penyimpanan level SMB1 adalah keajaiban optimasi 8-bit, dan ketika kita memaksa permainan membaca di tempat yang tidak seharusnya, hasilnya menarik.

Retro Game Mechanics Explained membuat seri 4 video soal ini -- kita akan mengompilasinya menjadi satu penyelaman ke kode 6502 permainan terlaris di zamannya.

GLITCH OBJECTS -- judul seri RGMechEx tentang mekanik tersembunyi SMB1

World 9-1 -- layar judul glitch world pertama yang bisa diakses lewat cart swap Tennis

Warm start : kenapa RAM Tenis bertahan di SMB1

Sebelum bicara penyimpanan level, harus paham cara SMB1 menyala. Karena glitch cart swap NES Tennis sepenuhnya bersandar pada sistem deteksi warm start / cold start permainan.

41 byte yang dipertahankan

Ketika SMB1 mendeteksi cold start (pertama dinyalakan atau power off/on), ia menghapus seluruh RAM. Tapi ketika mendeteksi warm start (tombol reset, tanpa pemutusan daya), ia mempertahankan zona memori 41 byte :

; 41 byte yang dipertahankan di RAM saat warm start
; Alamat $075F-$0787
;
; $075F : byte startup (world - 1)    [1 byte]
; $0760 : flag pemilihan dunia (tombol B) [1 byte]
; $0761-$0762 : tak dipakai                    [2 byte]
; $0763-$0768 : timer (6 digit, 3 ditampilkan) [6 byte]
; $0769-$076E : koin Luigi                   [6 byte]
; $076F-$0774 : koin Mario                   [6 byte]
; $0775-$077A : skor Luigi                   [6 byte]
; $077B-$0780 : skor Mario                   [6 byte]
; $0781-$0786 : skor tertinggi (6 digit, 1 tersembunyi) [6 byte]
; $0787 : byte ajaib $A5                 [1 byte]

41 byte ini berfungsi untuk satu fitur : memungkinkan pemain melanjutkan di dunia yang sama setelah game over. Kalau kamu mati di 6-3, permainan menulis dunia 6 ke byte startup, dan di title screen, kalau kamu menahan A + Start, kamu mulai ulang di 6-1.

41 byte yang dipertahankan di RAM saat warm start -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, dan byte ajaib $A5

Verifikasi ganda warm start

Cold start vs warm start -- diagram deteksi reset

Ketika SMB1 boot, ia tidak memeriksa satu kriteria tapi dua :

CheckWarmStart:
  ; 1. Memeriksa byte ajaib $A5 di $0787
  lda $0787
  cmp #$A5
  bne ColdStart        ; bukan $A5 → cold start

  ; 2. Memeriksa 6 digit skor tertinggi ($0781-$0786)
  ;    Setiap digit harus antara 0 dan 9
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; digit >= 10 → cold start
  inx
  cpx #6
  bne CheckLoop

  ; Kalau kedua kondisi lolos → warm start
  ; RAM tidak dihapus, dunia awal dipertahankan
  jmp WarmStartBoot

Verifikasi byte $A5 dan digit skor tertinggi -- jantung warm start

Kenapa verifikasi ganda? Karena byte $A5 bisa hadir secara kebetulan (permainan lain yang meninggalkan nilai ini, atau keadaan istirahat bawaan chip RAM). Dengan memeriksa bahwa digit skor tertinggi valid (0-9), kita memastikan datanya koheren.

Kenapa Tenis satu-satunya permainan yang berhasil

Ketika kita memasukkan SMB1 pertama kali (cold start), permainan :

  1. Menghapus seluruh RAM → skor tertinggi = 0, byte world = 0
  2. Menulis $A5 ke alamat $0787

Lalu, kita menukar ke Tennis tanpa mematikan konsol. Tennis :

  • Tidak membersihkan RAM saat startup (sedikit permainan NES yang melakukannya)
  • Tidak menulis ke byte skor tertinggi → mereka tetap 0 (valid)
  • Tidak menyentuh byte $A5 → ia tetap ada
  • Memakai alamat $075F untuk penghitung langkah pemain
; Footstep increment di Tennis :
; Di setiap langkah pemain di lapangan, Tennis menaikkan byte di $075F.
; Byte yang sama dipakai SMB1 sebagai "nomor world - 1".
;
; 0 langkah  → world 0 → SMB1 = world 1
; 1-7 langkah → world 1-7 → dunia normal
; 8+ langkah → world 8+ → glitch world !
;
; Penghitung hanya naik ketika musik berhenti
; (suara footstep tidak berbunyi selama musik).

Ketika kita memasang SMB1 lagi :

  1. Byte $A5 masih ada (Tennis tidak menyentuhnya)
  2. Digit skor tertinggi masih 0 (valid)
  3. Byte world sekarang bernilai 8+ (dinaikkan oleh langkah Tennis)
  4. SMB1 mendeteksi warm start → mempertahankan byte world yang rusak
  5. Menahan A + Start → world 9-1, world A-1, world 36-1, dll.

Kenapa harus memboot Mario sebelum Tenis

Satu kehalusan : harus dulu memboot SMB1, lalu Tennis, lalu SMB1 lagi. Kalau kamu mulai langsung dengan Tennis, byte $A5 tidak pernah ditulis (Tennis tidak menulis $A5), jadi deteksi warm start gagal dan RAM dihapus.

Penghitung langkah Tenis : setiap footstep menaikkan world byte

Mengakses Glitch World lewat NES Tennis -- video yang menjelaskan cart swap

Cara SMB1 menyimpan levelnya dalam 40KB

Nintendo R&D4 harus memecahkan masalah yang tampak sederhana : merepresentasikan level yang scroll horizontal dengan tile, musuh, item, semuanya dalam anggaran ROM yang sangat ketat.

Solusinya adalah pemisahan menjadi dua lapisan data yang sepenuhnya independen :

Tile layout (peta level)

Setiap level didefinisikan oleh pointer ke struktur tile terkompresi di ROM. Kompresinya rudimenter tapi jenius : byte "kontrol" diikuti 1-3 byte data.

Format tile memakai sistem run (mirip RLE) :

; Format tile SMB1 (disederhanakan)
; Setiap "perintah" adalah byte kontrol :
;
; $00-$7F : menaruh satu tile, maju 1 kolom
; $80-$BF : menaruh tile berulang N kali (N = byte - $80 + 1)
; $C0-$FF : perintah khusus (akhir baris, lompatan, ganti palet)

; Contoh : untuk menggambar 3 bata berurutan :
;   $82 $01    ; mengulangi tile $01 (brick) 3 kali

Setiap level berisi 13 baris 16 kolom tile (13×16 = 208 tile terlihat). Tapi format terkompresi memungkinkan turun jauh lebih rendah -- misalnya, langit dan kolom kosong nyaris tidak memakan tempat.

Loop render dalam 6502 :

; Dekompresi tile - loop utama
; Masukan : pointer tile_data di $XX
; Keluaran : tilemap level di RAM PPU

DecompressTile:
  lda (tile_ptr),y      ; membaca byte kontrol
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F : tile tunggal
  cmp #$C0
  bcc RunLength         ; $80-$BF : run-length
  jmp SpecialCommand    ; $C0-$FF : perintah khusus

SingleTile:
  sta PPU_DATA          ; menulis tile langsung
  jmp Next

RunLength:
  sec
  sbc #$7E              ; N = control - $7E
  tax
  lda (tile_ptr),y      ; membaca tile yang diulang
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

Sprite layout (musuh dan objek)

Paralel dengan itu, musuh dan objek (blok ?, pipa, goomba, koopa) disimpan dalam struktur yang sama sekali terpisah. Setiap spawn didefinisikan oleh 2 byte :

; Format sprite SMB1
; Byte 0 : posisi X (dalam kolom)
; Byte 1 : tipe sprite + bit halaman Y
; Y diturunkan dari indeks dalam urutan

; Urutan sprite :
;   $01 $4B    ; goomba di kolom 1
;   $09 $4B    ; goomba di kolom 9
;   $10 $61    ; blok ? di kolom 16 (berisi koin)
;   $15 $54    ; koopa hijau di kolom 21
;   $FF        ; akhir urutan

Setiap level bisa mereferensikan hingga 5 halaman sprite berbeda (tepatnya, 5 "layar" 16 kolom), tapi dalam praktik sebagian besar level hanya memakai 2-3.

Tabel pointer

Kejeniusan desainnya adalah tabel pointer. Setiap level disimpan sebagai pasangan alamat ROM :

// Struktur internal (disederhanakan) World Map
struct LevelPointer {
    uint16_t tile_ptr;   // Alamat ROM data tile
    uint16_t sprite_ptr; // Alamat ROM data sprite
};

// 4 tabel terpisah, satu per AreaType :
// 0 = Water, 1 = Overworld, 2 = Underground, 3 = Castle
LevelPointer level_table[4][128];

128 entri per tabel. 4 tipe zona. 512 kombinasi yang mungkin, tapi hanya sebagian kecil yang dipakai permainan resmi. Sisanya adalah RAM yang tidak diinisialisasi atau data yang diinterpretasikan sebagai pointer.

Ketika permainan memuat level, ia melakukan ini :

; Pemuatan level
; A = AreaType (0-3), X = LevelID (0-127)

LoadLevel:
  sta AREA_TYPE
  asl                  ; *2 untuk offset dalam tabel 16-bit
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; pointer ke tile
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; pointer ke sprite
  jsr DecompressTiles

Permainan memberikan kepercayaan total pada pointer : ia membaca alamat di tabel dan mendekompresi apa yang ada di alamat itu, titik.

Level ID $06 (Water) -- 9-1, versi bawah air 6-2

Tabel Level ID : 128 entri mungkin, 34 ditetapkan

Urutan berbeda pointer tile dan sprite -- penyebab level Frankenstein

34 level unik dan sistem ID 7-bit

Chip RAM NES (MB8416A) -- dialah yang menyimpan data ketika kartrid ditukar

SMB1 tidak punya 32 level, melainkan 34 level unik. Banyak level adalah duplikat (5-3 = 1-3 tapi dengan Bullet Bill) yang ditandai bendera "hard mode". Level unik yang sebenarnya :

  • Air (Tipe 0) : 3 level (2-2, 7-2, zona bonus 5-2/6-2)
  • Overworld (Tipe 1) : 22 level (termasuk 2 ruang awan bonus)
  • Underground (Tipe 2) : 3 level (termasuk ruang bonus bawah tanah)
  • Castle (Tipe 3) : 6 level
  • + 1 ruang cutscene (sebelum level bawah tanah/air)
  • + 1 warp zone 4-2

Setiap level punya ID 7 bit. 5 bit bobot rendah = nomor dalam subgrup, 2 bit bobot tinggi = tipe zona :

; Enkode 7-bit Level ID
; Bit 6-5 : Tipe (00=Water, 01=Overworld, 10=Underground, 11=Castle)
; Bit 4-0 : Nomor dalam subgrup
;
; Water IDs      : $00-$02  (tipe 00, nomor 0-2)
; Overworld IDs  : $20-$35  (tipe 01, nomor 0-21)
; Underground IDs: $40-$42  (tipe 10, nomor 0-2)
; Castle IDs     : $60-$65  (tipe 11, nomor 0-5)
;
; ID $25 = %0100101 → tipe 01 (Overworld), nomor 5 → 1-1
; ID $23 = %0100011 → tipe 01 (Overworld), nomor 3 → 6-2

128 ID yang mungkin ($00-$7F), hanya 34 yang ditetapkan ke level sungguhan. ID tak terpakai menunjuk ke apa saja.

Tabel pointer : dua daftar, dua urutan

Pointer tile dan sprite tidak disimpan dalam urutan yang sama. Kode memakai dua daftar 16-bit terpisah (high byte / low byte dalam dua tabel berbeda) :

Urutan pointer sprite :
  Indeks 0-5   : Castle (6 level)
  Indeks 6-27  : Overworld (22 level)
  Indeks 28-30 : Underground (3 level)
  Indeks 31-33 : Water (3 level)

Urutan pointer tile :
  Indeks 0-2   : Water (3 level)
  Indeks 3-24  : Overworld (22 level)
  Indeks 25-27 : Underground (3 level)
  Indeks 28-33 : Castle (6 level)

Kenapa urutannya berbeda? Tidak ada alasan teknis -- mungkin begitulah data disusun selama pengembangan. Tapi itu menciptakan akibat menarik : ketika ID level tidak valid, pointer tile dan sprite memuat level yang berbeda, menciptakan level Frankenstein.

Untuk bernavigasi antara dua daftar ini, permainan memakai tabel offset kecil (seperti daftar isi) :

; Tabel offset per tipe (Water, Overworld, Underground, Castle)
; Setiap entri = indeks awal dalam daftar yang cocok

SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; Water=31, Overworld=6, Underground=28, Castle=0

TileOffsetTable:
  .byte $00, $03, $19, $1C    ; Water=0, Overworld=3, Underground=25, Castle=28

Untuk memuat level 6-2 (ID $23, Overworld nomor 3) :

; 1. Tipe = 01 (Overworld) → indeks dalam tabel offset = 1
; 2. Sprite offset = SpriteOffsetTable[1] = 6
;    Indeks akhir = 6 + 3 (nomor level) = 9 → pointer sprite ke-10
; 3. Tile offset = TileOffsetTable[1] = 3
;    Indeks akhir = 3 + 3 = 6 → pointer tile ke-7
; 4. Hasil : pointer tile $A619 + pointer sprite $9ED0 = 6-2 ✓

Sekarang, apa yang terjadi dengan ID tidak valid seperti $43 (Underground nomor 3, yang tidak ada) ?

; ID $43, Tipe = 10 (Underground), nomor = 3
; Sprite offset = SpriteOffsetTable[2] = $1C = 28
;   Indeks = 28 + 3 = 31 → pointer sprite ke-32 = air bonus 5-2 !
; Tile offset = TileOffsetTable[2] = $19 = 25
;   Indeks = 25 + 3 = 28 → pointer tile ke-29 = 1-4 (Castle) !
;
; Hasil : level bawah tanah dengan tile 1-4
; dan Blooper zona air 5-2. Frankenstein sungguhan.

Level ID $43 -- level Frankenstein : tile 1-4 + sprite air 5-2

Menjelajahi Pointer Level Glitch -- tabel offset dijelaskan

Tabel indeks world -- ketika overflow world 9 menciptakan glitch level

Tabel indeks world : kenapa world 9 overflow

Ada tabel ROM 8 byte yang memberi indeks level pertama tiap dunia (1-8). Dan tepat sesudahnya, tabel 36 Level ID dari semua level dalam urutan permainan.

; WorldIndexTable (8 byte)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   -> Dunia 1 dimulai di level 0
;   -> Dunia 2 dimulai di level 5
;   -> Dunia 8 dimulai di level 33

; LevelIDTable (36 byte)
  .byte $25, $28, $29, $26, $24, ... ; ke-36 Level ID

Ketika kita mencoba memuat world 9, permainan membaca byte ke-9 WorldIndexTable... yang tidak ada. Ia overflow 1 byte ke LevelIDTable, membaca nilai $25, lalu memakai $25 sebagai indeks ke LevelIDTable (entri ke-37) -- yang overflow lagi 2 byte ke SpriteOffsetTable, dan membaca nilai 6.

; World 9 :
;   1. WorldIndexTable[8] (overflow) → membaca $25 di LevelIDTable
;   2. LevelIDTable[37] (overflow) → membaca byte ke-2 SpriteOffsetTable = 6
;   3. ID = 6 → Water level nomor 6 (yang tidak ada)
;   4. Tile pointer = pointer water nomor 6 = tile 6-2
;   5. Sprite pointer = indeks 31+6 = 37 > 33 → pointer tidak valid
;   6. Hasil : 6-2 di bawah air dengan sprite glitch
;      → world 9-1 !

Untuk world G (16), overflow melaju lebih jauh dan jatuh pada Level ID $01, yaitu level cutscene yang mendahului 1-2 :

; World G (16) :
;   WorldIndexTable[15] → membaca $01 di LevelIDTable
;   LevelIDTable[1] = $29 (cutscene 1-2)
;   → world G-1 = cutscene masuk 1-2

Kenapa glitch world ada

Permainan punya 32 level "sah" (8 dunia × 4 level). Tapi tabel pointer berukuran 128 entri per tipe zona. Entri setelah level 32 berisi apa yang ada di ROM pada alamat itu -- kadang level lain, kadang data suara, kadang RAM, kadang apa saja.

Level ID $01 Water (Minus World) -- tile pointer $AE45, sprite pointer $A171

Level ID $01 + AreaType 0 = Minus World

Glitch world paling terkenal. Level ID $01 dalam AreaType 0 (air) menunjuk ke :

  • Tile pointer : $AE45 → zona bawah air 2-2/7-2
  • Sprite pointer : $A171 → sprite 2-2/7-2

Hasilnya : level air yang menyerupai 2-2, tapi berputar tanpa batas karena flagpole tidak ada. Level berputar selamanya.

Ini level 36-1 (atau 36-1 di dunia $-1).

Pemeriksaan warm start SMB1 -- dialah yang memungkinkan Minus World ada

; Kenapa flagpole hilang di Minus World :
; Sprite 2-2/7-2 ($A171) tidak punya flagpole
; dalam urutannya. Permainan mencari sprite $FD (flagpole)
; tapi tidak pernah menemukannya → putaran tak berujung
;
; Permainan terus membangkitkan level sampai tak terbatas
; hingga timer mencapai nol.

Pointer yang menunjuk ke RAM

Ketika tile pointer atau sprite pointer menunjuk ke alamat di RAM ($00-$7F) alih-alih ROM, permainan mencoba menginterpretasikan perubahan konstan RAM sebagai tile :

; Contoh : Level ID $03 di Water
; Tile Pointer : $A46B (3-3 - valid)
; Sprite Pointer : $009D (menunjuk ke RAM page nol!)
;
; RAM page nol berisi register permainan,
; posisi Mario, keadaan penghitung...
; Permainan mendekompresi itu sebagai urutan sprite,
; dan hasilnya adalah level dengan musuh
; yang sebenarnya adalah nilai register.

Ketika page nol berubah (karena Mario bergerak, timer berjalan, dll.), "sprite" level ikut berubah. Makanya beberapa glitch world punya musuh yang berkedip dan terus berubah.

Level ID $03 Water -- sprite pointer $009D menunjuk ke RAM, level tak bisa dimainkan

Level ID $36 : level kosong (Overworld)

Level ID $36 di Overworld :

  • Tile pointer : $AC35 (1-2)
  • Sprite pointer : $A0D8 (1-2)

Hasil : tidak ada. Permainan memuat level tapi ditandai "tanpa level" dalam katalog RGMechEx. Tile mungkin valid tapi sprite menunjuk ke tempat yang menghasilkan level kosong atau tidak berfungsi.

Level ID $1D (Castle) : juara crash

Level ID $1D di Castle :

  • Tile pointer : $A210 (4-4)
  • Sprite pointer : $7EA0 (RAM !)

Sprite pointer di RAM = sprite tak terdefinisi. Permainan mencoba menampilkan bola Spiny atau Bullet Bill blaster di baris pertama tile. Itu langsung crash.

; Ketika sprite pointer menunjuk ke RAM,
; permainan mendekompresi byte yang terus berubah
; sebagai instruksi "spawn". Hasilnya :
; - Kemunculan objek yang tidak ada (nilai tak terdefinisi)
; - Crash PPU ketika sprite NES mencoba menampilkan tile tidak valid
; - Freeze total konsol

256 glitch world yang dikatalogkan

RGMechEx menulis skrip yang membangkitkan peta semua level, untuk 4 tipe zona, dan 128 ID masing-masing.

Penghitung dunia 8 bit (0-255). Dunia 1-8 sah. Tersisa 248 glitch world potensial. Setiap glitch world cocok dengan level pertama dunia itu, dan Level ID-nya dihitung lewat mekanisme overflow WorldIndexTable.

Tabel glitch world -- 248 dunia rusak, 68 level pertama bisa diakses

Dari 128 ID yang mungkin, hanya 68 yang merupakan "first level" sebuah dunia (bisa diakses lewat nomor glitch world). 60 lainnya adalah level 2+ atau tidak bisa diakses.

Tipe ID unik yang bisa dimainkan ID yang crash ID kosong
Water (0) ~20 ~60 ~48
Overworld (1) ~30 ~55 ~43
Underground (2) ~15 ~65 ~48
Castle (3) ~25 ~58 ~45

Banyak ID berujung pada level yang sama karena pointer jatuh pada alamat ROM yang sama. Level ID $28 (Overworld) misalnya -- tile pointer $A7CD (2-1) -- muncul di 38 glitch world berbeda, karena sprite pointer $9F51 menunjuk ke zona ROM yang dipakai sebagai padding/data suara yang dipakai ulang banyak ID.

Peta level ID $28 (Overworld) -- tile 2-1 dengan sprite normal, 38 glitch world

Glitch Level Super Mario Bros. Dijelaskan -- video ke-3

6 glitch level yang benar-benar unik

Di antara 19 ID glitch level yang bisa diakses, hanya 6 yang tidak langsung crash saat dimuat :

World Level ID Deskripsi
E-1 (224) $50 Satu blok ? di atas jurang. Mario langsung mati.
W $57 Mario spawn terkunci, tidak bisa bergerak.
42 (133) $50 Terowongan awan yang menjebak Mario kalau ia pergi cukup jauh.
62 (131, 240) $4D Kastil beku : Mario spawn di atas, tidak bisa jatuh → terkunci.
127 $4B Terowongan bawah tanah, tapi crash kalau pergi terlalu jauh.
137 $4B Mengaktifkan autoscroll cutscene. Mario bertemu satu blok bata yang memblokirnya selamanya.

Level ID $50 (terowongan awan) -- glitch world 42-1 dan E-1 Level ID $4D (castle) -- world 62-1, Mario terkunci di spawn Level ID $4B (terowongan) -- world 127-1, crash kalau pergi terlalu jauh

Enam glitch world dari 248 yang menghasilkan sesuatu yang benar-benar baru. Sisanya adalah level normal dengan tipe zona yang salah, atau layar hitam.

Format level secara detail

Fokus pada format persis data level, untuk memahami kenapa glitch level bertahan (atau tidak).

Header level : 2 byte, 6 properti

Setiap level dimulai dengan header 2 byte yang mengendalikan 6 properti :

; Byte 0 : timer + Y start + modifier
;   Bit 7-6 : timer (00=tak berubah, 01=200, 10=300, 11=400)
;   Bit 5-3 : Y start Mario (111/110 = autowalk)
;   Bit 2-0 : modifier tipe level
;              000=default, 001=gelombang, 010=dinding bata,
;              011=dasar air, 100=malam, 101=salju,
;              110=malam salju, 111=malam abu-abu

; Byte 1 : platform + latar + pola lantai
;   Bit 7-6 : platform khusus (00=pohon, 01=jamur,
;                                 10=Bullet Bill, 11=awan)
;   Bit 5-4 : latar (00=tidak ada, 01=awan,
;                           10=gunung, 11=pagar)
;   Bit 3-0 : pola lantai awal (0-15)

Modifier tipe mengendalikan variasi visual : gelombang di atas level air, latar bata 8-3, palet malam 4-3, salju 6-2, dll.

Objek tile : 2 byte, Next Screen Flag, antrean 3 slot

Setelah header ada daftar objek tile, tiap objek 2 byte. Byte $FD menandai akhir daftar.

; Format objek tile (16 bit) :
; Byte 0 :
;   Bit 7-4 : posisi X (kolom 0-15)
;   Bit 3-0 : posisi Y
;     Y=0-11  : posisi Y normal
;     Y=12    : objek khusus (lubang, jembatan, tali, blok ?)
;     Y=13    : skip layar / objek khusus 2
;     Y=14    : perubahan modifier/scenery/lantai
;     Y=15    : objek khusus 3 (kastil, tangga, pipa besar)

; Byte 1 :
;   Bit  7   : BENDERA LAYAR BERIKUTNYA
;   Bit 6-4 : tipe objek (0-7)
;   Bit 3-0 : lebar/tinggi / subtipe

Ketika bit "layar berikutnya" disetel, kolom kerja saat ini dinaikkan 1. Itu memungkinkan menaruh objek setelah 16 kolom pertama. Objek harus didaftar berurutan (kiri ke kanan) karena permainan memuatnya sekuensial :

; Rutin pemuatan punya DUA fase per kolom :
; Fase 1 : mencari objek baru yang dimulai di kolom ini
;            dan menambahkannya ke antrean (queue)
; Fase 2 : memproses tiap objek dalam antrean dengan menggambar tile,
;            dan menghapus yang berakhir di kolom ini

Antreannya tepat 3 slot. Akibat langsung : tidak bisa punya lebih dari 3 objek yang dimulai di kolom yang sama. Kalau antrean penuh, objek ke-4 diabaikan dan tidak akan pernah dimuat.

Makanya level yang dirancang baik menghindari menumpuk terlalu banyak objek. Contoh di 1-2 : kolom dengan blok 1up di langit-langit + bata di sebelahnya dipecah jadi dua objek berbeda untuk menghormati batas 3.

Posisi Y khusus : 12, 13, 14, 15

Ketika Y=12, objek tidak punya posisi Y (ia di hardcode per tipe) :

; Y=12 : objek tanpa posisi Y
;   Tipe 0 : lubang (menghapus tanah)
;   Tipe 1 : tali platform bergerak
;   Tipe 2-4 : jembatan pada Y tetap
;   Tipe 5 : lubang dengan air/lava
;   Tipe 6-7 : barisan blok ?

Ketika Y=13, dua subgrup. Kalau bit 6 byte 1 bernilai 1 :

; Y=13, bit6=1 : objek khusus
;   0 = pipa L (cutscene), 1 = flagpole, 2-3 = jembatan/kapak/palu (akhir kastil)
;   4 = hentikan layar, 5 = musuh acak, 6 = loop level, 7+ = mungkin crash

Kalau bit6=0, 5 bit bobot rendah mengenkode skip layar (melompat langsung ke layar N, tanpa lewat bendera layar berikutnya satu per satu).

Ketika Y=14 : prinsip yang sama dengan bit6=1 untuk mengubah modifier tipe, bit6=0 untuk mengubah latar + pola lantai.

Pola lantai : 16 motif tanah

Tanah level tidak terbuat dari objek individual. SMB1 memakai pola lantai, motif latar yang berlaku untuk semua kolom hingga perubahan berikutnya :

; Pola lantai (4 bit = 16 kemungkinan)
;   0 = kosong total
;   1 = tanah 2 tile tinggi
;   2 = tanah 1 tile tinggi
;   3 = tanah + bawah
;   4 = tanah + bawah 2
;   5 = tanah 1/2 tile
;   6 = 3/4 tanah
;   ... hingga 15 = terisi total (tanah + langit-langit)

Makanya lubang adalah objek : mereka meng override pola lantai pada kolom spesifik, tanpa harus mengubah pola untuk sisanya.

Batas 256 byte dan pengulangan

Semua data tile level muat dalam maksimum 256 byte. Register Y 6502 dipakai sebagai indeks, dan ia 8 bit. Kalau permainan tiba di akhir data tanpa menemukan byte $FD, ia berputar ke awal dan mengulangi 256 byte sampai tak terbatas :

; Indeks Y = 8 bit → maks 256 byte data tile
; Kalau Y overflow (255 → 0) tanpa bertemu $FD → pengulangan
; Sama untuk sprite, tapi objek pipa (3 byte)
; menggeser paritas indeks di setiap pemuatan.

Beberapa glitch level mengeksploitasi pengulangan ini untuk membangkitkan level yang bertahan "tanpa batas".

Sistem sprite : 2 byte + transisi pipa

Sprite mengikuti format mirip, tapi tanpa header dan dengan beberapa perbedaan kunci. Byte $FF menandai akhir daftar.

; Format sprite (2 byte) :
; Byte 0 : posisi X (kolom)
; Byte 1 :
;   Bit 7 : BENDERA LAYAR BERIKUTNYA
;   Bit 6-0 : tipe sprite
;       Beberapa tipe termasuk : goomba, koopa, Blooper,
;       Bullet Bill, Lakitu, Spiny, platform,
;       perintah warp zone, toad/putri,
;       perintah spawn grup musuh

Bit bobot rendah byte 1 adalah bendera level sulit : kalau disetel 1, sprite hanya muncul di level ≥ 5-3. Begitulah level "hard mode" dibuat.

Posisi Y 15 = skip layar (identik dengan tile). Posisi Y 14 = transisi pipa (3 byte) :

; Sprite Y=14 : transisi pipa/vine (3 byte !)
;   Byte 0 : posisi X
;   Byte 1 : bit 6-0 = Level ID 7-bit (tujuan)
;   Byte 2 : bit 4-0 = layar tujuan
;            bit 7-5 = dunia tempat transisi valid
;
; Kenapa dunia? Ruang bonus dipakai ulang antar dunia.
; Contoh : ruang bonus 1-1 juga dipakai 2-1 dan 7-1.
; Ruang ini punya 3 transisi, satu per dunia, supaya Mario
; muncul kembali di tempat yang benar.

Sprite tidak punya sistem antrean. Satu-satunya batas adalah tidak boleh ada lebih dari 4 sprite yang dimuat bersamaan di zona spawn (tepat di luar layar ke kanan). Selebihnya, sprite diabaikan.

Cara mengakses glitch world

Ada dua metode utama.

Metode klasik : wall clip

Wall clip (melewati tembok) memungkinkan keluar dari level normal dan berjalan hingga warzone tersembunyi. Dengan memanipulasi penghitung dunia lewat RAM, kita bisa memuat Level ID apa pun.

Tekniknya :

  1. World 1-2 : masuk ke pipa akhir tersembunyi
  2. Melakukan wall clip di dinding kanan
  3. Berjalan di kekosongan hingga zona warp
  4. Permainan menginterpretasikan nilai sebagai dunia

Tapi metode ini hanya memberi akses ke sebagian kecil glitch world.

Metode ekstrem : cart swap NES Tennis

Lihat bagian "Warm start" di atas untuk detail lengkap. Singkatnya : penghitung langkah Tennis menulis ke byte RAM yang sama dengan dunia awal SMB1, dan deteksi warm start mempertahankan nilai itu.

Sudut pengoprek : kode untuk menjelajahi semuanya

Kalau kamu ingin menjelajahi semua glitch sendiri di emulator, kamu bisa mem patch Level ID langsung :

; Patch untuk FCEUX / Mesen :
; Alamat RAM $075F = Level ID saat ini
; Alamat RAM $0760 = Tipe Area (0=Water, 1=Overworld, 2=Underground, 3=Castle)

; Contoh : memuat Level 57 (0x39) di Overworld
; Di emulator, buka pelacak memori dan tulis :
; $075F = 0x39
; $0760 = 0x01
; Lalu masuki pipa warp atau mati dan mulai ulang
; → Permainan memuat level ID $39 di Overworld

RGMechEx menerbitkan daftar lengkap 128 level × 4 tipe dengan peta yang dibangkitkan otomatis di rgmechex.com. Setiap entri menunjukkan tile pointer, sprite pointer, dan peta visual level.

Level paling gila

Level ID $1F (Water) : 15 glitch world dalam satu

Tile pointer $A302 (3-4) dikombinasikan dengan sprite pointer $02A0 memberikan 15 glitch world berbeda (D-1, J-1, Y-1, Z-1, 55-1, 73-1...). Penjelasan : sprite pointer menunjuk ke zona ROM yang berisi data yang cukup dekat dengan sprite valid untuk menghasilkan hasil yang bisa dimainkan, tapi kombinasi tile kastil 3-4 dengan sprite overworld menciptakan render absurd.

Level ID $28 (Overworld) : 38 glitch world = rekor

Rekor mutlak. 38 entri glitch world menunjuk ke level yang sama (tile 2-1 + sprite $9F51). Kenapa? Karena sprite pointer $9F51 jatuh di zona ROM yang dipakai sebagai padding/data suara yang dipakai ulang banyak ID.

Level ID $49 (Underground) : level FDS

Tile pointer $76AE + sprite pointer $1C9D. Tile pointer menunjuk ke zona ROM yang disediakan untuk versi Famicom Disk System. Hasilnya : level dengan tile yang tidak ada di kartrid standar. Inilah level yang memunculkan level 52-1 dan 196-1.

Level ID $00-$02 : level bonus sungguhan

ID ini dipakai oleh sub-level sah permainan :

  • $00 : zona bawah air 5-2/6-2 (dipakai H-1, 39-1)
  • $01 : air 2-2/7-2 (Minus World, 36-1)
  • $02 : sub-level 8-4 (136-1, 151-1, 215-1)

Perbedaan antara level "bonus" yang bisa diakses normal dan glitch world adalah warp zone memeriksa dunia saat ini :

; Pemeriksaan warp zone (disederhanakan)
; Permainan memeriksa bahwa dunia target antara 1 dan 8
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; < 1 → ditolak
  cmp #9
  bcs InvalidWarp       ; > 8 → ditolak
  ; dunia valid hanya antara 1 dan 8
  jmp DoWarp

Glitch world dengan nomor > 8 atau 0 tidak bisa dicapai lewat pipa normal. Perlu wall clip atau cart swap.

Kenapa beberapa level crash : jump table

Ketika permainan memuat objek tile, ia memakai tipenya sebagai indeks ke jump table :

; Jump table objek tile standar (tipe 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; tipe 0 : blok ?, tersembunyi, flagpole...
  .word Obj_Platform      ; tipe 1 : platform khusus
  .word Obj_BrickRow      ; tipe 2 : barisan bata
  .word Obj_BlockRow      ; tipe 3 : barisan blok
  .word Obj_CoinRow       ; tipe 4 : barisan koin
  .word Obj_BrickCol      ; tipe 5 : kolom bata
  .word Obj_BlockCol      ; tipe 6 : kolom blok
  .word Obj_Pipe          ; tipe 7 : pipa
  .word Obj_8             ; tipe 8
  .word Obj_9             ; tipe 9
  .word Obj_10            ; tipe 10
  .word Obj_11            ; tipe 11

Jump table : kenapa tipe objek tidak valid membuat permainan crash

Kalau objek punya tipe tidak valid (≥12), permainan melompat ke pointer yang tidak ada di tabel ini. 4 hasil yang mungkin :

  1. Pointer valid → objek dimuat normal
  2. Pointer ke jump table lain (tumpang tindih) → objek berbeda muncul. Contoh : tipe 12 menunjuk ke tabel Y=13, yang memberikan pipa L.
  3. Pointer ke executable → eksekusi kode acak (mungkin crash)
  4. Placeholder eksplisit (NOP) → objek tidak melakukan apa-apa (beberapa sprite seperti itu, menghasilkan musuh yang melayang di tempat tanpa bergerak)

Glitch level ID $58 : sprite pointer menunjuk ke alamat tidak valid, permainan crash

Glitch level ID $50 : terowongan awan, level yang dibangkitkan dari data rusak

Glitch level ID $58 (terowongan yang crash) : sprite pointernya menunjuk ke wilayah memori yang tidak ada di NES tanpa mapper ROM. Permainan mencoba memuat Koopa yang sama 5 kali per frame di posisi (0,0), yang menjenuhkan PPU dan menyebabkan freeze.

; Kenapa ID $58 crash :
; Sprite pointer → alamat tidak valid (di luar ruang NES standar)
; → Permainan membaca byte tak tentu sebagai tipe sprite
; → Koopa $00 (tipe tidak valid tanpa handler) berputar dalam panggilan rekursif
; → Stack overflow 6502 → freeze

Paradoks warp pipa

Ingat cek target_world BETWEEN 1 AND 8. Bahkan kalau kamu menemukan pipa di glitch world, permainan memeriksa bahwa dunia tujuan antara 1 dan 8. Glitch world bernomor > 8 (36-1, 255-1...), jadi warp gagal.

Itu juga kenapa Minus World tidak punya akhir : flagpole tidak ada di sprite, dan pipa tidak menuju ke mana-mana.

Trik 5 objek dalam satu kolom

Ada edge case yang memungkinkan melampaui batas 3 objek per kolom. Ketika antrean macet (slot penuh + objek berikutnya tanpa bendera layar berikutnya), permainan "memproses awal" kolom saat ini berputar hingga menemukan objek dengan bendera layar berikutnya. Selama setiap pra-pemrosesan :

; Selama pra-pemrosesan kolom :
; 1. Objek dalam antrean melihat lebar sisanya
;    berkurang di setiap "kemajuan kolom palsu"
; 2. Kalau objek mencapai lebar=0, ia keluar dari antrean
; 3. Slot yang bebas bisa diisi objek baru
;    yang ditambahkan di kolom yang sama

; Hasil : hingga 5 objek bisa diproses di kolom yang sama.
; Teknik : menaruh 2 objek yang melintasi batas layar
; (slot 1 dan 2), 1 objek dummy di X < sebelumnya (memblokir antrean),
; lalu 3 objek di X=0 layar berikutnya (salah satunya dengan bendera layar berikutnya).

Inilah yang disebut "queue skip" dan dipakai beberapa romhacker untuk membuat level lebih padat daripada yang diizinkan format normal.

Perbedaan antar versi

Famicom Disk System

Versi FDS SMB1 punya peta memori berbeda. Semua pointer level bergeser, tapi datanya sama. Yang berubah : indeks glitch world sama sekali berbeda :

FDS World 36 → Level ID $09 (versi air 5-3)
  → Flagpole ada! Kita bisa menyelesaikan level.
  → Lalu : $27 (7-3 normal) → $44 (4-4 underground)
  → $44 bisa diselesaikan → kapak berfungsi → akhir permainan!

Jadi Minus World FDS adalah "dunia bonus" yang bisa berujung
pada penyelesaian permainan, berlawanan dengan versi NES.

Level FDS favorit saya : ID $5F, versi bawah tanah dari paruh kedua 3-3 dalam terowongan rendah (sayang itu autoscroller).

The Lost Levels (Super Mario Bros. 2 Jepang)

Lost Levels mengubah banyak hal :

  1. Urutan identik tile/sprite : tidak ada lagi level Frankenstein (tile dan sprite memuat level yang sama bahkan dengan ID tidak valid)
  2. Satu tabel pointer 16-bit alih-alih dua tabel terpisah high/low
  3. 4 berkas disk : ROM dipecah untuk FDS :
    • Berkas 1 : world 1-4
    • Berkas 2 : world 5-8
    • Berkas 3 : world 9 + mesin suara
    • Berkas 4 : world A-D (tabel pointer sama sekali berbeda)
  4. Level ID yang sama = 4 level yang mungkin menurut berkas yang dimuat
  5. Tidak ada lagi glitch Tenis : opsi continue (melanjutkan di dunia yang sama setelah game over) membuat warm start tidak berguna, dan permainan langsung reset kalau world > 9
  6. Objek baru : jamur beracun, blok tak terlihat, blok bunga api tak terlihat, pipa terbalik, angin -- tapi disisipkan di tengah daftar yang sudah ada → ketidakcocokan backward dengan SMB1
  7. Piranha Plant selalu merah setelah world 4, springboard hijau hanya di world 2/B/3/C/7

Super Mario All-Stars (SNES)

Porting langsung dengan rutin 6502 yang sama (SNES mengeksekusi kode NES dalam mode kompatibel) :

  • Warp zone diperbaiki : tidak ada lagi Minus World (memasuki pipa kiri sebelum teks berujung ke dunia yang benar)
  • Crash : sebagian besar glitch level crash (kecuali ID $6A dan 9-1)
  • Objek kastil ditambahkan : dibuat lebih unik
  • Tapi : wrong warp 4-2 masih berfungsi (tidak di patch!)

Wrong warp 4-2 : bug penempatan objek

Di 4-2, ada dua objek transisi pipa : vine (warp zone) dan pipa (ruang koin). Objek transisi pertama (milik vine) ditempatkan jauh sebelum vine muncul di layar. Yang kedua (pipa) ditempatkan terlalu telat di level.

; Pewaktuan transisi di 4-2 :
; Objek transisi 1 (vine → warp zone) : ditempatkan 3 layar sebelum vine
; Objek transisi 2 (pipa → ruang koin) : ditempatkan 1 layar setelah pipa
;
; Normalnya objek pertama dinonaktifkan sebelum Mario
; mencapai pipa. Tapi kalau Mario cepat (atau memakai
; pintasan blok B+kanan), transisi vine
; masih aktif ketika ia menyentuh pipa!
; → Permainan memuat warp zone alih-alih ruang koin.
;
; Kalau objek ditempatkan tepat setelah vine tapi sebelum
; pipa, bug tidak akan ada.

Level berputar

Bagaimana loop bekerja (8-4, 7-4)? Level punya checkpoint dengan nomor layar dan posisi Y yang di hardcode :

; Checkpoint : {nomor_layar, posisi_vertikal}
; Kalau Mario melewati checkpoint ini pada ketinggian yang benar → level lanjut
; Kalau tidak → warp balik 4 layar (64 blok)
;
; Untuk membuat putaran tak berujung : posisi_vertikal = $F0
; (di bawah bawah layar) → mustahil divalidasi.
;
; Checkpoint sederhana (satu bendera) kecuali world 7
; yang memakai triplet (3 bendera, harus gagal minimal 1)
;
; Warp balik kasar : offset data tile disetel ke nilai
; hardcode, offset data sprite disetel ulang ke 0. Musuh yang ada
; dibongkar seketika → firebar menghilang.

Mengubah format, bukan kode

Salah satu pelajaran paling menarik dari arsitektur ini adalah pengembang SMB1 berhasil membuat sistem level yang sangat ekspresif tanpa pernah menyentuh kode render 6502. Seluruh variasi antar level berasal dari data (pointer, objek, sprite, pola lantai), bukan kode.

256 glitch world ada karena tabel pointer berukuran untuk 128 entri × 4 tipe, dan permainan tidak pernah memvalidasi nilai yang dibacanya. Ketika pointer jatuh di RAM, permainan menginterpretasikan register Mario sebagai tile. Ketika pointer jatuh di data suara, permainan memainkan musik dalam bentuk desain level. Dan ketika jump table overflow, permainan mengeksekusi apa saja hingga crash.

Mekanik Super Mario Bros. Lainnya Dijelaskan -- video ke-4

Apa yang bisa dipelajari dari semua ini

  1. Pemisahan tile/sprite : independensi total kedua lapisan, dengan urutan penyimpanan berbeda yang menciptakan level Frankenstein unik
  2. Kompresi RLE + sistem objek : level bukan bitmap melainkan daftar objek yang ditempatkan, dengan pola lantai untuk tanah
  3. Antrean 3 slot : batas ketat perangkat keras (dan desain level)
  4. Kepercayaan total pada pointer : permainan memercayai pointer dan jump table, yang menghasilkan entah glitch yang bisa dimainkan, atau crash
  5. 256 byte maks : batas register Y 6502, yang membuat data berulang kalau pergi terlalu jauh
  6. Warm start / cold start : sistem "lanjutkan" yang membuka pintu ke cart swap Tennis → Mario

Yang terindah : semua itu adalah kode 6502 yang muat dalam 40KB. Kode berdialog langsung dengan perangkat keras dan memberikan kepercayaan total pada setiap akses memori. Kalau pointernya busuk, permainan crash. Dan crash itu kita sebut glitch world.

3 hal yang perlu diingat

  1. Glitch world adalah pointer yang jatuh buruk -- Permainan punya 128 ID × 4 tipe zona, tapi hanya 34 level unik. Ketika nomor world rusak (oleh Tennis atau wall clip), permainan memuat pointer yang dirancang untuk level lain, dan 512 kombinasi yang mungkin menghasilkan hasil yang tidak bisa diprediksi.

  2. Minus World adalah bug warp yang digabung korupsi -- Pipa kiri di 1-2, kalau diaktifkan sebelum teks muncul, memuat world 36 (0x24). World ini menunjuk ke Level ID $01 (air 2-2), level tanpa flagpole. Dan karena tidak ada transisi pipa untuk world 36, level berputar tanpa batas. Ketiadaan verifikasi menciptakan ikonnya.

  3. Tenis → Mario, 15 tahun sebelum OoT → Paper Mario -- RAM NES bertahan dari penukaran kartrid berkat kapasitor dan sistem warm start / cold start SMB1. Penghitung langkah Tennis (yang menaikkan byte RAM saat memainkan suara langkah) jatuh tepat pada alamat nomor world. Digit skor tertinggi harus tetap 0, byte $A5 harus utuh, dan permainan harus mendeteksi warm start -- perlombaan keadaan sempurna yang hanya berfungsi dengan Tennis.

Video asli Retro Game Mechanics Explained adalah pekerjaan semut yang gila -- tingkat detail pada disassembly 6502, peta otomatis semua level, penjelasan cart swap dan warm start. Kalau kamu belum melihat serinya, tonton, ia pendek dan setiap menitnya padat.

Kode sumber peta tersedia di rgmechex.com, dan disassembly lengkap SMB1 open source di banyak repo. 40 tahun lalu, programmer Jepang menulis sistem level ini dalam 6502 dengan nol unit test dan nol bug tracker, dan kita terus mempelajari teknik optimasi dengan membuka kode mereka hari ini.

Super Mario Bros. : स्तर प्रारूप, pointer और 256 glitch world

128 स्तर × 4 क्षेत्र प्रकार 40KB ROM में कैसे समाते हैं, Minus World क्यों मौजूद है, और NES Tennis मैच glitch world कैसे लोड कर सकता है।

परिचय

Super Mario Bros., यह 40 kilobyte ROM है। आठ world, 32 स्तर, शत्रु, संगीत, power-up, सब इसमें समाता है।

लेकिन अगर तुम emulator खोलते हो और सही byte छेड़ते हो, तो तुम स्तर 36-1 लोड कर सकते हो। या 255-1। या ऐसे world में उतर सकते हो जहाँ सब Bowser sprite और कहीं न जाने वाले पाइपों से बना है।

ये glitch world साधारण कारण से मौजूद हैं : SMB1 की स्तर भंडारण प्रणाली 8-bit अनुकूलन का चमत्कार है, और जब हम खेल को वहाँ पढ़ने को मजबूर करते हैं जहाँ नहीं चाहिए, तो यह आकर्षक परिणाम देता है।

Retro Game Mechanics Explained ने इस पर 4 वीडियो की शृंखला बनाई, हम उन्हें अपने समय के सबसे बिके खेल के 6502 कोड में एक ही गोते में संकलित करेंगे।

GLITCH OBJECTS , SMB1 छिपी यांत्रिकी पर RGMechEx शृंखला का शीर्षक

World 9-1 , cart swap Tennis से पहुँचने योग्य पहले glitch world का शीर्षक स्क्रीन

warm start : Tennis की RAM SMB1 में क्यों बचती है

स्तर भंडारण की बात से पहले, समझना होगा कि SMB1 शुरू कैसे होता है। क्योंकि NES Tennis cart swap glitch पूरी तरह खेल के warm start / cold start पहचान तंत्र पर टिका है।

41 संरक्षित byte

जब SMB1 cold start पहचानता है (पहली बार चालू या power off/on), तो वह पूरी RAM मिटाता है। लेकिन जब वह warm start पहचानता है (बटन reset, बिजली कटौती नहीं), तो वह 41 byte स्मृति क्षेत्र संरक्षित करता है :

; warm start पर RAM में संरक्षित 41 byte
; पते $075F-$0787
;
; $075F : शुरुआत byte (world - 1)    [1 byte]
; $0760 : world चयन flag (B button) [1 byte]
; $0761-$0762 : अप्रयुक्त                    [2 byte]
; $0763-$0768 : timer (6 अंक, 3 दिखाए गए) [6 byte]
; $0769-$076E : Luigi सिक्के                   [6 byte]
; $076F-$0774 : Mario सिक्के                   [6 byte]
; $0775-$077A : Luigi स्कोर                   [6 byte]
; $077B-$0780 : Mario स्कोर                   [6 byte]
; $0781-$0786 : top score (6 अंक, 1 छिपा) [6 byte]
; $0787 : जादुई byte $A5                 [1 byte]

ये 41 byte एक ही सुविधा की सेवा करते हैं : खिलाड़ी को game over बाद उसी world में जारी रखने देना। अगर तुम 6-3 में मरते हो, तो खेल शुरुआत byte में world 6 लिखता है, और title screen पर, अगर तुम A + Start दबाए रखते हो, तो तुम 6-1 में फिर से शुरू करते हो।

warm start पर RAM में संरक्षित 41 byte , TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, और जादुई byte $A5

warm start की दोहरी जाँच

Cold start बनाम warm start , reset पहचान चित्र

जब SMB1 boot होता है, तो वह एक ही मानदंड नहीं बल्कि दो जाँचता है :

CheckWarmStart:
  ; 1. $0787 पर जादुई byte $A5 जाँचना
  lda $0787
  cmp #$A5
  bne ColdStart        ; $A5 नहीं → cold start

  ; 2. top score के 6 अंक जाँचना ($0781-$0786)
  ;    हर अंक 0 और 9 के बीच होना चाहिए
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; अंक >= 10 → cold start
  inx
  cpx #6
  bne CheckLoop

  ; अगर दोनों शर्तें गुजरती हैं → warm start
  ; RAM मिटी नहीं है, शुरुआत world संरक्षित है
  jmp WarmStartBoot

$A5 byte और top score अंकों की जाँच , warm start का हृदय

दोहरी जाँच क्यों ? क्योंकि $A5 byte संयोग से मौजूद हो सकता है (दूसरा खेल जो यह मान छोड़ता है, या RAM chip की डिफ़ॉल्ट विश्राम स्थिति)। top score अंक वैध (0-9) हैं यह जाँचकर, हम सुनिश्चित करते हैं कि डेटा सुसंगत है।

Tennis एकमात्र खेल क्यों है जो काम करता है

जब हम पहली बार SMB1 डालते हैं (cold start), तो खेल :

  1. पूरी RAM मिटाता है → top score = 0, world byte = 0
  2. $0787 पते पर $A5 लिखता है

फिर, हम console बंद किए बिना Tennis पर swap करते हैं। Tennis :

  • शुरू में RAM साफ नहीं करता (कम NES खेल करते हैं)
  • top score byte पर नहीं लिखता → वे 0 रहते हैं (वैध)
  • $A5 byte को नहीं छूता → वह मौजूद रहता है
  • $075F पते को खिलाड़ी कदम counter के लिए इस्तेमाल करता है
; Tennis में footstep increment :
; court पर खिलाड़ी के हर कदम पर, Tennis $075F पर byte बढ़ाता है।
; वही byte SMB1 "world number - 1" के रूप में इस्तेमाल करता है।
;
; 0 कदम  → world 0 → SMB1 = world 1
; 1-7 कदम → world 1-7 → सामान्य world
; 8+ कदम → world 8+ → glitch world !
;
; counter तभी बढ़ता है जब संगीत रुकता है
; (footstep ध्वनियाँ संगीत के दौरान नहीं बजतीं)।

जब हम SMB1 वापस रखते हैं :

  1. $A5 byte अभी भी वहाँ है (Tennis ने उसे नहीं छुआ)
  2. top score अंक अभी भी 0 हैं (वैध)
  3. world byte अब 8+ है (Tennis कदमों से बढ़ाया गया)
  4. SMB1 warm start पहचानता है → भ्रष्ट world byte संरक्षित करता है
  5. A + Start दबाए रखना → world 9-1, world A-1, world 36-1, आदि।

Mario को Tennis से पहले boot क्यों करना होगा

सूक्ष्मता : पहले SMB1 boot करना होगा, फिर Tennis, फिर फिर से SMB1। अगर तुम सीधे Tennis से शुरू करते, तो $A5 byte कभी लिखा नहीं जाता (Tennis $A5 नहीं लिखता), इसलिए warm start पहचान विफल होती और RAM मिट जाती।

Tennis कदम counter : हर footstep world byte बढ़ाता है

Access Glitch Worlds via NES Tennis , cart swap समझाने वाला वीडियो

SMB1 40KB में अपने स्तर कैसे संग्रहित करता है

Nintendo R&D4 को दिखने में सरल समस्या सुलझानी थी : tile, शत्रु, item के साथ क्षैतिज scroll करने वाले स्तरों को दर्शाना, सब अति तंग ROM बजट में।

समाधान दो पूरी तरह स्वतंत्र डेटा परतों में अलगाव है :

tile layout (स्तर नक्शा)

हर स्तर ROM में संपीड़ित tile संरचना की ओर pointer से परिभाषित है। संपीड़न प्राथमिक लेकिन शानदार है : 1-3 डेटा byte के बाद "नियंत्रण" byte।

tile प्रारूप run (RLE जैसा) तंत्र इस्तेमाल करता है :

; SMB1 tile प्रारूप (सरलीकृत)
; हर "कमांड" नियंत्रण byte है :
;
; $00-$7F : tile रखता है, 1 कॉलम आगे बढ़ता है
; $80-$BF : N बार दोहराई tile रखता है (N = byte - $80 + 1)
; $C0-$FF : विशेष कमांड (पंक्ति अंत, छलांग, palette बदलाव)

उदाहरण : लगातार 3 ईंटें बनाने के लिए :
  $82 $01    ; tile $01 (brick) 3 बार दोहराता है

हर स्तर में tile की 16 कॉलम की 13 पंक्तियाँ होती हैं (13×16 = 208 दिखते tile)। लेकिन संपीड़ित प्रारूप काफी नीचे जाने देता है, उदाहरण के लिए, आसमान और खाली कॉलम लगभग जगह नहीं लेते।

6502 में रेंडर लूप :

; Tile विसंपीड़न - मुख्य loop
; प्रवेश : $XX में tile_data pointer
; निकास : PPU RAM में स्तर tilemap

DecompressTile:
  lda (tile_ptr),y      ; नियंत्रण byte पढ़ना
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F : एकल tile
  cmp #$C0
  bcc RunLength         ; $80-$BF : run-length
  jmp SpecialCommand    ; $C0-$FF : विशेष कमांड

SingleTile:
  sta PPU_DATA          ; tile सीधे लिखना
  jmp Next

RunLength:
  sec
  sbc #$7E              ; N = control - $7E
  tax
  lda (tile_ptr),y      ; दोहराने वाली tile पढ़ना
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

sprite layout (शत्रु और वस्तुएँ)

समानांतर में, शत्रु और वस्तुएँ (?, ब्लॉक, पाइप, goomba, koopa) पूरी तरह अलग संरचना में संग्रहित हैं। हर spawn 2 byte से परिभाषित है :

; SMB1 sprite प्रारूप
; Byte 0 : X स्थिति (कॉलम में)
; Byte 1 : sprite प्रकार + पेज Y बिट
; Y अनुक्रम में सूचकांक से व्युत्पन्न है

sprite अनुक्रम :
  $01 $4B    ; कॉलम 1 पर goomba
  $09 $4B    ; कॉलम 9 पर goomba
  $10 $61    ; कॉलम 16 पर ? ब्लॉक (सिक्का रखता है)
  $15 $54    ; कॉलम 21 पर हरा koopa
  $FF        ; अनुक्रम अंत

हर स्तर sprite के 5 पेज तक संदर्भ दे सकता है (आखिर, 16 कॉलम के 5 "screen"), लेकिन व्यवहार में ज्यादातर स्तर सिर्फ 2-3 इस्तेमाल करते हैं।

pointer तालिका

डिजाइन की प्रतिभा pointer तालिका है। हर स्तर ROM पतों की जोड़ी के रूप में संग्रहित है :

// World Map की आंतरिक संरचना (सरलीकृत)
struct LevelPointer {
    uint16_t tile_ptr;   // tile डेटा का ROM पता
    uint16_t sprite_ptr; // sprite डेटा का ROM पता
};

// 4 अलग तालिकाएँ, प्रति AreaType एक :
// 0 = Water, 1 = Overworld, 2 = Underground, 3 = Castle
LevelPointer level_table[4][128];

प्रति तालिका 128 प्रविष्टियाँ। 4 क्षेत्र प्रकार। 512 संभावित संयोजन, लेकिन सिर्फ अंश आधिकारिक खेल इस्तेमाल करता है। बाकी, अनारंभ RAM है या डेटा है जिसकी pointer के रूप में व्याख्या की जाती है।

जब खेल स्तर लोड करता है, तो वह यह करता है :

; स्तर लोड करना
; A = AreaType (0-3), X = LevelID (0-127)

LoadLevel:
  sta AREA_TYPE
  asl                  ; 16-बिट तालिका में offset के लिए *2
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; tile की ओर pointer
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; sprite की ओर pointer
  jsr DecompressTiles

खेल pointer पर पूर्ण भरोसा देता है : वह तालिका में पता पढ़ता है और उस पते पर जो है उसे विसंपीड़ित करता है, बस।

Level ID $06 (Water) , 9-1, 6-2 का पानी के नीचे संस्करण

Level ID तालिका : 128 संभावित प्रविष्टियाँ, 34 निर्दिष्ट

tile और sprite pointer का अलग क्रम , Frankenstein level का कारण

34 अनोखे स्तर और 7-बिट ID तंत्र

NES RAM chip (MB8416A) , वही है जो cartouche swap करने पर डेटा रखता है

SMB1 में 32 स्तर नहीं, बल्कि 34 अनोखे स्तर हैं। बहुत से स्तर दोहरे हैं (5-3 = 1-3 लेकिन Bullet Bill के साथ) "hard mode" ध्वज से चिह्नित। असली अनोखे स्तर :

  • पानी (प्रकार 0) : 3 स्तर (2-2, 7-2, बोनस क्षेत्र 5-2/6-2)
  • Overworld (प्रकार 1) : 22 स्तर (2 बादल बोनस कमरे सहित)
  • Underground (प्रकार 2) : 3 स्तर (भूमिगत बोनस कमरे सहित)
  • Castle (प्रकार 3) : 6 स्तर
  • + 1 cutscene कमरा (भूमिगत/पानी स्तरों से पहले)
  • + 4-2 का 1 warp क्षेत्र

हर स्तर की 7 बिट ID है। 5 कम वजन बिट = उपसमूह में संख्या, 2 उच्च वजन बिट = क्षेत्र प्रकार :

; Level ID का 7-बिट एन्कोडिंग
; बिट 6-5 : प्रकार (00=Water, 01=Overworld, 10=Underground, 11=Castle)
; बिट 4-0 : उपसमूह में संख्या
;
; Water ID      : $00-$02  (प्रकार 00, संख्या 0-2)
; Overworld ID  : $20-$35  (प्रकार 01, संख्या 0-21)
; Underground ID: $40-$42  (प्रकार 10, संख्या 0-2)
; Castle ID     : $60-$65  (प्रकार 11, संख्या 0-5)
;
; ID $25 = %0100101 → प्रकार 01 (Overworld), संख्या 5 → 1-1
; ID $23 = %0100011 → प्रकार 01 (Overworld), संख्या 3 → 6-2

128 संभावित ID ($00-$7F), सिर्फ 34 असली स्तरों को निर्दिष्ट। अप्रयुक्त ID कुछ भी इंगित करते हैं।

pointer तालिकाएँ : दो सूचियाँ, दो क्रम

tile और sprite pointer उसी क्रम में संग्रहित नहीं हैं। कोड दो अलग 16-बिट सूचियाँ इस्तेमाल करता है (दो अलग तालिकाओं में high byte / low byte) :

sprite pointer क्रम :
  सूचकांक 0-5   : Castle (6 स्तर)
  सूचकांक 6-27  : Overworld (22 स्तर)
  सूचकांक 28-30 : Underground (3 स्तर)
  सूचकांक 31-33 : Water (3 स्तर)

tile pointer क्रम :
  सूचकांक 0-2   : Water (3 स्तर)
  सूचकांक 3-24  : Overworld (22 स्तर)
  सूचकांक 25-27 : Underground (3 स्तर)
  सूचकांक 28-33 : Castle (6 स्तर)

अलग क्रम क्यों ? कोई तकनीकी कारण नहीं, शायद विकास के दौरान डेटा ऐसे व्यवस्थित हुआ था। लेकिन यह आकर्षक परिणाम बनाता है : जब स्तर ID अमान्य होती है, तो tile और sprite pointer अलग स्तर लोड करते हैं, Frankenstein level बनाते हुए।

इन दो सूचियों के बीच navigate करने के लिए, खेल छोटी offset तालिकाएँ इस्तेमाल करता है (सामग्री तालिका जैसी) :

; प्रति प्रकार offset तालिकाएँ (Water, Overworld, Underground, Castle)
; हर प्रविष्टि = संबंधित सूची में शुरुआत सूचकांक

SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; Water=31, Overworld=6, Underground=28, Castle=0

TileOffsetTable:
  .byte $00, $03, $19, $1C    ; Water=0, Overworld=3, Underground=25, Castle=28

स्तर 6-2 (ID $23, Overworld संख्या 3) लोड करने के लिए :

; 1. प्रकार = 01 (Overworld) → offset तालिका में सूचकांक = 1
; 2. Sprite offset = SpriteOffsetTable[1] = 6
;    अंतिम सूचकांक = 6 + 3 (स्तर संख्या) = 9 → 10वाँ sprite pointer
; 3. Tile offset = TileOffsetTable[1] = 3
;    अंतिम सूचकांक = 3 + 3 = 6 → 7वाँ tile pointer
; 4. परिणाम : tile pointer $A619 + sprite pointer $9ED0 = 6-2 ✓

अब, $43 (Underground संख्या 3, जो मौजूद नहीं है) जैसी अमान्य ID के साथ क्या होता है ?

; ID $43, प्रकार = 10 (Underground), संख्या = 3
; Sprite offset = SpriteOffsetTable[2] = $1C = 28
;   सूचकांक = 28 + 3 = 31 → 32वाँ sprite pointer = पानी बोनस 5-2 !
; Tile offset = TileOffsetTable[2] = $19 = 25
;   सूचकांक = 25 + 3 = 28 → 29वाँ tile pointer = 1-4 (Castle) !
;
; परिणाम : 1-4 tile वाला भूमिगत स्तर
; और 5-2 पानी क्षेत्र के Blooper। असली Frankenstein।

Level ID $43 , Frankenstein level : tile 1-4 + sprite पानी 5-2

Exploring Glitch Level Pointers , offset तालिकाएँ समझाई गईं

world index table , जब world 9 overflow glitch level बनाता है

world index table : world 9 overflow क्यों करता है

8 byte वाली ROM तालिका है जो हर world (1-8) के पहले स्तर का सूचकांक देती है। और ठीक बाद, खेल क्रम में सभी स्तरों के 36 Level ID की तालिका।

; WorldIndexTable (8 byte)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   -> World 1 स्तर 0 पर शुरू होता है
;   -> World 2 स्तर 5 पर शुरू होता है
;   -> World 8 स्तर 33 पर शुरू होता है

; LevelIDTable (36 byte)
  .byte $25, $28, $29, $26, $24, ... ; 36 Level ID

जब world 9 लोड करने की कोशिश की जाती है, तो खेल WorldIndexTable का 9वाँ byte पढ़ता है... जो मौजूद नहीं है। वह LevelIDTable में 1 byte overflow करता है, मान $25 पढ़ता है, फिर LevelIDTable (37वीं प्रविष्टि) में सूचकांक के रूप में $25 इस्तेमाल करता है, जो SpriteOffsetTable में फिर से 2 byte overflow करता है, और मान 6 पढ़ता है।

; World 9 :
;   1. WorldIndexTable[8] (overflow) → LevelIDTable में $25 पढ़ता है
;   2. LevelIDTable[37] (overflow) → SpriteOffsetTable का दूसरा byte पढ़ता है = 6
;   3. ID = 6 → Water level number 6 (जो मौजूद नहीं है)
;   4. Tile pointer = water pointer संख्या 6 = 6-2 tile
;   5. Sprite pointer = सूचकांक 31+6 = 37 > 33 → अमान्य pointer
;   6. परिणाम : glitch sprite के साथ पानी के नीचे 6-2
;      → world 9-1 !

world G (16) के लिए, overflow और आगे जाता है और Level ID $01 पर गिरता है, जो 1-2 से पहले cutscene स्तर है :

; World G (16) :
;   WorldIndexTable[15] → LevelIDTable में $01 पढ़ता है
;   LevelIDTable[1] = $29 (cutscene 1-2)
;   → world G-1 = 1-2 प्रवेश cutscene

glitch world क्यों मौजूद हैं

खेल में 32 "वैध" स्तर हैं (8 world × 4 स्तर)। लेकिन pointer तालिका प्रति क्षेत्र प्रकार 128 प्रविष्टियाँ करती है। स्तर 32 से परे प्रविष्टियों में वह है जो उन पतों पर ROM में है, कभी दूसरा स्तर, कभी ध्वनि डेटा, कभी RAM, कभी कुछ भी।

Level ID $01 Water (Minus World) , tile pointer $AE45, sprite pointer $A171

Level ID $01 + AreaType 0 = Minus World

सबसे प्रसिद्ध glitch world। AreaType 0 (पानी) में Level ID $01 इंगित करता है :

  • Tile pointer : $AE45 → 2-2/7-2 पानी के नीचे क्षेत्र
  • Sprite pointer : $A171 → 2-2/7-2 sprite

परिणाम : पानी स्तर जो 2-2 जैसा है, लेकिन अनंत लूप करता है क्योंकि flagpole मौजूद नहीं है। स्तर हमेशा के लिए लूप करता है।

यह स्तर 36-1 है (या world $-1 में 36-1)।

SMB1 warm start check , वही है जो Minus World को मौजूद करने देता है

; Minus World में flagpole क्यों छूटा है :
; 2-2/7-2 sprite ($A171) में उनके अनुक्रम में flagpole नहीं है
; खेल sprite $FD (flagpole) खोजता है
; लेकिन कभी नहीं पाता → अनंत लूप
;
; खेल स्तर अनंत बनाता रहता है
; जब तक timer शून्य न पहुँचे।

pointer जो RAM की ओर इंगित करते हैं

जब tile pointer या sprite pointer ROM के बजाय RAM पते ($00-$7F) की ओर इंगित करता है, तो खेल RAM के लगातार बदलावों को tile की तरह व्याख्या करने की कोशिश करता है :

; उदाहरण : Water में Level ID $03
; Tile Pointer : $A46B (3-3 - वैध)
; Sprite Pointer : $009D (शून्य RAM पेज की ओर इंगित करता है !)
;
; शून्य RAM पेज में खेल register हैं,
; Mario स्थिति, counter स्थिति...
; खेल इसे sprite अनुक्रम की तरह विसंपीड़ित करता है,
; और परिणाम ऐसा स्तर है जिसमें शत्रु हैं
; जो असल में register मान हैं।

जब शून्य पेज बदलता है (क्योंकि Mario हिलता है, timer चलता है, आदि), तो स्तर के "sprite" भी बदलते हैं। इसीलिए कुछ glitch world में शत्रु हैं जो झपकते और लगातार बदलते हैं।

Level ID $03 Water , sprite pointer $009D RAM की ओर इंगित करता है, खेलने अयोग्य स्तर

Level ID $36 : खाली स्तर (Overworld)

Overworld में Level ID $36 :

  • Tile pointer : $AC35 (1-2)
  • Sprite pointer : $A0D8 (1-2)

परिणाम : कुछ नहीं। खेल स्तर लोड करता है लेकिन वह RGMechEx सूची में "बिना स्तर" चिह्नित है। tile शायद वैध हैं लेकिन sprite ऐसी जगह इंगित करते हैं जो खाली या निष्क्रिय स्तर बनाती है।

Level ID $1D (Castle) : crash का चैंपियन

Castle में Level ID $1D :

  • Tile pointer : $A210 (4-4)
  • Sprite pointer : $7EA0 (RAM !)

RAM में Sprite pointer = अपरिभाषित sprite। खेल tile की पहली पंक्ति में Spiny ball या Bullet Bill blaster दिखाने की कोशिश करता है। यह तुरंत crash करता है।

; जब sprite pointer RAM की ओर इंगित करता है,
; खेल हर समय बदलने वाले byte को
; "spawn" निर्देशों की तरह विसंपीड़ित करता है
; परिणाम :
; - अनस्तित्व वस्तुओं का प्रकट होना (अपरिभाषित मान)
; - PPU Crash जब NES sprite अमान्य tile दिखाने की कोशिश करता है
; - console का पूरा Freeze

सूचीबद्ध 256 glitch world

RGMechEx ने स्क्रिप्ट लिखी जो सभी स्तरों के नक्शे बनाती है, 4 क्षेत्र प्रकारों के लिए, और हर एक के 128 ID।

world counter 8 बिट (0-255) पर है। world 1-8 वैध हैं। 248 संभावित glitch world बचे हैं। हर glitch world इस world के पहले स्तर से मेल खाता है, और उसकी Level ID WorldIndexTable overflow तंत्र से गणना की जाती है।

glitch world तालिका , 248 भ्रष्ट world, 68 पहले स्तर पहुँच योग्य

128 संभावित ID में से, सिर्फ 68 इस world के "first level" हैं (glitch world number से पहुँच योग्य)। अन्य 60 स्तर 2+ या अपहुँच योग्य हैं।

प्रकार खेलने योग्य अनोखे ID crash करने वाले ID खाली ID
Water (0) ~20 ~60 ~48
Overworld (1) ~30 ~55 ~43
Underground (2) ~15 ~65 ~48
Castle (3) ~25 ~58 ~45

बहुत से ID उसी स्तर की ओर ले जाते हैं क्योंकि pointer उसी ROM पतों पर गिरते हैं। Level ID $28 (Overworld) उदाहरण के लिए, tile pointer $A7CD (2-1), 38 अलग glitch world में प्रकट होता है, क्योंकि उसका sprite pointer $9F51 ROM क्षेत्र में गिरता है जो बहुत से ID से padding/ध्वनि डेटा के रूप में इस्तेमाल होता है।

Level ID $28 (Overworld) स्तर नक्शा , सामान्य sprite के साथ 2-1 tile, 38 glitch world

Super Mario Bros. Glitch Levels Explained , तीसरा वीडियो

6 सच में अनोखे glitch level

19 पहुँच योग्य glitch level ID में से, सिर्फ 6 लोड पर तुरंत crash नहीं करते :

World Level ID विवरण
E-1 (224) $50 खाई ऊपर एकल ? block। Mario तुरंत मरता है।
W $57 Mario spawn अवरुद्ध, हिलने में असमर्थ।
42 (133) $50 बादल सुरंग जो Mario फँसाती है अगर वह काफी दूर जाता है।
62 (131, 240) $4D जमा महल : Mario ऊपर spawn करता है, गिर नहीं सकता → अवरुद्ध।
127 $4B भूमिगत सुरंग, लेकिन बहुत दूर जाने पर crash करता है।
137 $4B cutscene का स्वचालित scroll सक्रिय करता है। Mario एकल brick block से मिलता है जो उसे हमेशा रोकता है।

Level ID $50 (cloud tunnel) , glitch world 42-1 और E-1 Level ID $4D (castle) , world 62-1, Mario spawn अवरुद्ध Level ID $4B (tunnel) , world 127-1, बहुत दूर जाने पर crash करता है

248 में से छह glitch world जो सच में नया कुछ बनाते हैं। बाकी, गलत क्षेत्र प्रकार वाले सामान्य स्तर हैं, या काली स्क्रीन हैं।

विस्तार में स्तर प्रारूप

स्तर डेटा के सटीक प्रारूप पर ध्यान, यह समझने के लिए कि glitch level क्यों टिकते हैं (या नहीं)।

स्तर header : 2 byte, 6 गुण

हर स्तर 2 byte header से शुरू होता है जो 6 गुण नियंत्रित करता है :

; Byte 0 : timer + Y start + modifier
;   बिट 7-6 : timer (00=अपरिवर्तित, 01=200, 10=300, 11=400)
;   बिट 5-3 : Y start Mario (111/110 = autowalk)
;   बिट 2-0 : level type modifier
;              000=default, 001=waves, 010=brick wall,
;              011=water bottom, 100=night, 101=snow,
;              110=snow night, 111=gray night

; Byte 1 : platform + background + floor pattern
;   बिट 7-6 : special platform (00=tree, 01=mushroom,
;                                 10=Bullet Bill, 11=cloud)
;   बिट 5-4 : background (00=none, 01=clouds,
;                           10=montains, 11=fences)
;   बिट 3-0 : प्रारंभिक floor pattern (0-15)

प्रकार modifier दृश्य भिन्नताएँ नियंत्रित करता है : पानी स्तरों के ऊपर लहरें, 8-3 की ईंट पृष्ठभूमि, 4-3 की रात palette, 6-2 की बर्फ, आदि।

tile वस्तुएँ : 2 byte, Next Screen Flag, 3 slot कतार

header बाद tile वस्तुओं की सूची आती है, हर वस्तु 2 byte करती है। byte $FD सूची अंत चिह्नित करता है।

; tile वस्तु प्रारूप (16 बिट) :
; Byte 0 :
;   बिट 7-4 : X स्थिति (कॉलम 0-15)
;   बिट 3-0 : Y स्थिति
;     Y=0-11  : सामान्य Y स्थिति
;     Y=12    : विशेष वस्तुएँ (गड्ढे, पुल, rope, ? block)
;     Y=13    : screen skip / विशेष वस्तुएँ 2
;     Y=14    : modifier/scenery/floor बदलाव
;     Y=15    : विशेष वस्तुएँ 3 (महल, सीढ़ियाँ, बड़ा पाइप)

; Byte 1 :
;   बिट  7   : NEXT SCREEN FLAG
;   बिट 6-4 : वस्तु प्रकार (0-7)
;   बिट 3-0 : चौड़ाई/ऊँचाई / उप प्रकार

जब "next screen" बिट सेट होता है, तो वर्तमान कार्य कॉलम 1 बढ़ता है। यह पहले 16 कॉलम से परे वस्तुएँ रखने देता है। वस्तुएँ क्रम में सूचीबद्ध होनी चाहिए (बाएँ से दाएँ) क्योंकि खेल उन्हें क्रमिक लोड करता है :

; लोड दिनचर्या में प्रति कॉलम दो चरण हैं :
; चरण 1 : इस कॉलम पर शुरू होने वाली नई वस्तुएँ खोजना
;            और उन्हें कतार (queue) में जोड़ना
; चरण 2 : कतार में हर वस्तु tile बनाकर प्रोसेस करना,
;            और जो इस कॉलम पर खत्म होती हैं उन्हें हटाना

कतार ठीक 3 slot करती है। सीधा परिणाम : उसी कॉलम पर शुरू होने वाली 3 से ज्यादा वस्तुएँ नहीं हो सकतीं। अगर कतार भरी है, तो चौथी वस्तु नजरअंदाज की जाती है और कभी लोड नहीं होगी।

इसीलिए अच्छी तरह डिजाइन स्तर बहुत सी वस्तुएँ ढेर करने से बचते हैं। 1-2 में उदाहरण : छत में 1up block + बगल की ईंटें 3 की सीमा सम्मान देने के लिए दो अलग वस्तुओं में बाँटी गई हैं।

विशेष Y स्थिति : 12, 13, 14, 15

जब Y=12, तो वस्तु की Y स्थिति नहीं होती (वह प्रकार से hardcoded है) :

; Y=12 : Y स्थिति रहित वस्तुएँ
;   प्रकार 0 : गड्ढा (जमीन हटाता है)
;   प्रकार 1 : चल platform rope
;   प्रकार 2-4 : स्थिर Y वाले पुल
;   प्रकार 5 : पानी/लावा वाला गड्ढा
;   प्रकार 6-7 : ? block पंक्तियाँ

जब Y=13, तो दो उपसमूह। अगर byte 1 का बिट 6 1 है :

; Y=13, bit6=1 : विशेष वस्तुएँ
;   0 = L-pipe (cutscene), 1 = flagpole, 2-3 = पुल/धुरी/hamer (महल अंत)
;   4 = stop screen, 5 = random enemies, 6 = loop level, 7+ = संभावित crash

अगर bit6=0, तो 5 कम वजन बिट screen skip एन्कोड करते हैं (एक एक करके next screen flag से गुजरे बिना सीधे N स्क्रीन पर छलांग)।

जब Y=14 : bit6=1 के साथ प्रकार modifier बदलने के लिए वही सिद्धांत, पृष्ठभूमि + floor pattern बदलने के लिए bit6=0।

floor pattern : जमीन के 16 रूप

स्तरों की जमीन अलग वस्तुओं से नहीं बनी है। SMB1 floor pattern इस्तेमाल करता है, पृष्ठभूमि रूप जो अगले बदलाव तक सभी कॉलम पर लागू होता है :

; Floor pattern (4 बिट = 16 संभावनाएँ)
;   0 = पूर्ण खाली
;   1 = 2 tile ऊँची जमीन
;   2 = 1 tile ऊँची जमीन
;   3 = जमीन + bottom
;   4 = जमीन + bottom 2
;   5 = जमीन 1/2 tile
;   6 = 3/4 जमीन
;   ... 15 तक = पूर्ण भरा (जमीन + छत)

इसीलिए गड्ढे वस्तुएँ हैं : वे बाकी सब के लिए pattern बदले बिना विशिष्ट कॉलम पर floor pattern override करते हैं।

256 byte सीमा और repeat

स्तर का सारा tile डेटा अधिकतम 256 byte में समाता है। 6502 का Y register सूचकांक के रूप में इस्तेमाल होता है, और वह 8 बिट करता है। अगर खेल $FD byte मिले बिना डेटा अंत पहुँचता है, तो वह शुरुआत में लूप करता है और 256 byte अनंत दोहराता है :

; सूचकांक Y = 8 बिट → अधिकतम 256 byte tile डेटा
; अगर Y overflow (255 → 0) बिना $FD मिले → repeat
; sprite के लिए भी वही, लेकिन pipe वस्तुएँ (3 byte)
; हर लोड पर सूचकांक समता बदलती हैं।

कुछ glitch level इस repeat का शोषण करते हैं ताकि "अनंत" चलने वाले स्तर बनाएँ।

sprite तंत्र : 2 byte + pipe transition

sprite समान प्रारूप चलते हैं, लेकिन header के बिना और कुछ मुख्य अंतरों के साथ। byte $FF सूची अंत चिह्नित करता है।

; sprite प्रारूप (2 byte) :
; Byte 0 : X स्थिति (कॉलम)
; Byte 1 :
;   बिट 7 : NEXT SCREEN FLAG
;   बिट 6-0 : sprite प्रकार
;       कुछ प्रकार शामिल हैं : goomba, koopa, Blooper,
;       Bullet Bill, Lakitu, Spiny, platform,
;       warp क्षेत्र कमांड, toad/राजकुमारी,
;       शत्रु समूह spawn कमांड

byte 1 का कम वजन बिट hard level flag है : अगर 1 पर सेट है, तो sprite सिर्फ ≥ 5-3 स्तरों में प्रकट होता है। "hard mode" स्तर ऐसे बनाए जाते हैं।

Y स्थिति 15 = screen skip (tile जैसा)। Y स्थिति 14 = pipe transition (3 byte) :

; Sprite Y=14 : pipe/vine transition (3 byte !)
;   Byte 0 : X स्थिति
;   Byte 1 : बिट 6-0 = Level ID 7-बिट (गंतव्य)
;   Byte 2 : बिट 4-0 = गंतव्य screen
;            बिट 7-5 = world जहाँ यह transition वैध है
;
; world क्यों ? बोनस कमरे world के बीच फिर से इस्तेमाल होते हैं।
; उदाहरण : 1-1 बोनस कक्ष 2-1 और 7-1 भी इस्तेमाल करते हैं।
; इस कक्ष में 3 transition हैं, प्रति world एक, ताकि Mario
; सही जगह फिर से प्रकट हो।

sprite में queue तंत्र नहीं है। एकमात्र सीमा है कि spawn क्षेत्र (स्क्रीन दाएँ ठीक बाहर) में एक साथ लोड 4 से ज्यादा sprite नहीं हो सकते। उसके परे, sprite नजरअंदाज किए जाते हैं।

glitch world तक कैसे पहुँचें

दो मुख्य विधियाँ हैं।

क्लासिक विधि : wall clip

wall clip (दीवारों से गुजरना) सामान्य स्तर से निकलकर छिपे warzone तक चलने देता है। RAM से world counter हेरफेर करके, कोई भी Level ID लोड किया जा सकता है।

तकनीक :

  1. World 1-2 : छिपे अंत पाइप में जाना
  2. दाएँ दीवार पर wall clip करना
  3. warp क्षेत्र तक शून्य में चलना
  4. खेल मानों को world की तरह व्याख्या करता है

लेकिन यह विधि glitch world के छोटे हिस्से तक ही पहुँच देती है।

चरम विधि : NES Tennis cart swap

पूरा विवरण के लिए ऊपर "warm start" खंड देखो। संक्षेप में : Tennis कदम counter SMB1 शुरुआत world वाले RAM byte पर लिखता है, और warm start पहचान यह मान संरक्षित करती है।

छेड़छाड़ कोना : सब खोजने का कोड

अगर तुम emulator में सारे glitch खुद खोजना चाहते हो, तो तुम Level ID सीधे patch कर सकते हो :

; FCEUX / Mesen के लिए Patch :
; RAM पता $075F = वर्तमान Level ID
; RAM पता $0760 = Area Type (0=Water, 1=Overworld, 2=Underground, 3=Castle)

; उदाहरण : Overworld में Level 57 (0x39) लोड करना
; emulator में, स्मृति tracer खोलो और लिखो :
; $075F = 0x39
; $0760 = 0x01
; फिर warp पाइप में प्रवेश करो या मरो और फिर से शुरू करो
; → खेल Overworld में स्तर ID $39 लोड करता है

RGMechEx ने tile pointer, sprite pointer, और स्तर का दृश्य नक्शा दिखाने वाली हर प्रविष्टि के साथ 128 स्तर × 4 प्रकार की पूरी सूची rgmechex.com पर प्रकाशित की।

सबसे wtf स्तर

Level ID $1F (Water) : एक में 15 glitch world

tile pointer $A302 (3-4) sprite pointer $02A0 से संयुक्त 15 अलग glitch world देता है (D-1, J-1, Y-1, Z-1, 55-1, 73-1...)। व्याख्या : sprite pointer ROM क्षेत्र की ओर इंगित करता है जिसमें खेलने योग्य परिणाम बनाने के लिए वैध sprite के पर्याप्त करीब डेटा है, लेकिन overworld sprite के साथ महल 3-4 tile का संयोजन बेतुका रेंडर बनाता है।

Level ID $28 (Overworld) : 38 glitch world = record

पूर्ण record। 38 glitch world प्रविष्टियाँ उसी स्तर की ओर इंगित करती हैं (2-1 tile + $9F51 sprite)। क्यों ? क्योंकि sprite pointer $9F51 ROM क्षेत्र में गिरता है जो बहुत से ID से padding/ध्वनि डेटा के रूप में इस्तेमाल होता है।

Level ID $49 (Underground) : FDS स्तर

Tile pointer $76AE + sprite pointer $1C9D। tile pointer Famicom Disk System संस्करण के लिए आरक्षित ROM क्षेत्र की ओर इंगित करता है। परिणाम : ऐसे tile वाला स्तर जो मानक कार्ट्रिज में मौजूद नहीं हैं। यह वह स्तर है जो स्तर 52-1 और 196-1 प्रकट करता है।

Level ID $00-$02 : असली बोनस स्तर

ये ID खेल के वैध उपस्तरों इस्तेमाल करते हैं :

  • $00 : 5-2/6-2 पानी के नीचे क्षेत्र (H-1, 39-1 इस्तेमाल करते हैं)
  • $01 : 2-2/7-2 पानी (Minus World, 36-1)
  • $02 : 8-4 उपस्तर (136-1, 151-1, 215-1)

सामान्यतः पहुँच योग्य "बोनस" स्तर और glitch world के बीच अंतर यह है कि warp क्षेत्र वर्तमान world जाँचते हैं :

; warp क्षेत्र जाँच (सरलीकृत)
; खेल जाँचता है कि लक्ष्य world 1 और 8 के बीच है
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; < 1 → अस्वीकृत
  cmp #9
  bcs InvalidWarp       ; > 8 → अस्वीकृत
  ; सिर्फ 1 और 8 के बीच वैध world
  jmp DoWarp

8 से ज्यादा या 0 संख्या वाले glitch world सामान्य पाइपों से नहीं पहुँचे जा सकते। wall clip या cart swap चाहिए।

कुछ स्तर crash क्यों करते हैं : jump table

जब खेल tile वस्तु लोड करता है, तो वह उसके प्रकार को jump table में सूचकांक के रूप में इस्तेमाल करता है :

; मानक tile वस्तुओं की Jump table (प्रकार 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; प्रकार 0 : ? ब्लॉक, hidden, flagpole...
  .word Obj_Platform      ; प्रकार 1 : विशेष platform
  .word Obj_BrickRow      ; प्रकार 2 : ईंट पंक्ति
  .word Obj_BlockRow      ; प्रकार 3 : block पंक्ति
  .word Obj_CoinRow       ; प्रकार 4 : सिक्का पंक्ति
  .word Obj_BrickCol      ; प्रकार 5 : ईंट कॉलम
  .word Obj_BlockCol      ; प्रकार 6 : block कॉलम
  .word Obj_Pipe          ; प्रकार 7 : पाइप
  .word Obj_8             ; प्रकार 8
  .word Obj_9             ; प्रकार 9
  .word Obj_10            ; प्रकार 10
  .word Obj_11            ; प्रकार 11

jump table : अमान्य वस्तु प्रकार खेल crash क्यों कराता है

अगर वस्तु का प्रकार अमान्य है (≥12), तो खेल इस तालिका में मौजूद न pointer पर छलांग लगाता है। 4 संभावित परिणाम :

  1. वैध Pointer → वस्तु सामान्य लोड होती है
  2. दूसरी jump table की ओर Pointer (अतिव्यापन) → अलग वस्तु प्रकट होती है। उदाहरण : प्रकार 12 Y=13 तालिका की ओर इंगित करता है, जो L-pipe देता है।
  3. निष्पादन योग्य की ओर Pointer → बेतरतीब कोड निष्पादन (संभावित crash)
  4. स्पष्ट Placeholder (NOP) → वस्तु कुछ नहीं करती (कुछ sprite ऐसे हैं, जगह पर उड़ने वाले शत्रु बनाते हैं बिना हिले)

Glitch level ID $58 : sprite pointer अमान्य पते की ओर इंगित करता है, खेल crash करता है

Glitch level ID $50 : cloud tunnel, भ्रष्ट डेटा से उत्पन्न स्तर

glitch level ID $58 (crash करने वाली सुरंग) : उसका sprite pointer स्मृति क्षेत्र की ओर इंगित करता है जो ROM mapper रहित NES पर मौजूद नहीं है। खेल स्थिति (0,0) पर प्रति frame 5 बार वही Koopa लोड करने की कोशिश करता है, जो PPU संतृप्त करता है और freeze कराता है।

; ID $58 crash क्यों करता है :
; Sprite pointer → अमान्य पता (मानक NES स्थान से बाहर)
; → खेल अनिर्धारित byte को sprite प्रकारों की तरह पढ़ता है
; → Koopa $00 (बिना handler अमान्य प्रकार) पुनरावर्ती कॉल में लूप करता है
; → 6502 Stack overflow → freeze

pipe warp विरोधाभास

target_world BETWEEN 1 AND 8 जाँच याद रखो। भले ही तुम्हें glitch world में पाइप मिले, खेल जाँचता है कि गंतव्य world 1 और 8 के बीच है। glitch world की संख्याएँ 8 से ज्यादा हैं (36-1, 255-1...), इसलिए warp विफल होती है।

इसीलिए Minus World का अंत नहीं है : sprite में flagpole मौजूद नहीं है, और पाइप कहीं नहीं ले जाते।

एक कॉलम में 5 वस्तुओं की चाल

edge case मौजूद है जो प्रति कॉलम 3 वस्तुओं की सीमा पार करने देता है। जब कतार अवरुद्ध होती है (slot भरे + अगली वस्तु में next screen flag छूटा), तो खेल next screen flag वाली वस्तु मिलने तक लूप में वर्तमान कॉलम "पूर्व प्रोसेस" करता है। हर पूर्व प्रोसेसिंग के दौरान :

; कॉलम पूर्व प्रोसेसिंग के दौरान :
; 1. कतार में वस्तुएँ अपनी बची चौड़ाई देखती हैं
;    कॉलम की हर "नकली बढ़त" पर घटाई जाती है
; 2. अगर वस्तु चौड़ाई=0 पहुँचती है, तो वह कतार छोड़ती है
; 3. मुक्त slot नई वस्तु से भरा जा सकता है
;    उसी कॉलम में जोड़ी गई

; परिणाम : उसी कॉलम पर 5 तक वस्तुएँ प्रोसेस हो सकती हैं।
; तकनीक : screen सीमा पार करने वाली 2 वस्तुएँ रखना
; (slot 1 और 2), X < पिछला वाली 1 dummy वस्तु (कतार अवरुद्ध करती है),
; फिर अगले screen के X=0 पर 3 वस्तुएँ (एक next screen flag वाली)।

इसे "queue skip" कहते हैं और कुछ romhacker इसका इस्तेमाल प्रारूप सामान्यतः देने से घने स्तर बनाने के लिए करते हैं।

संस्करणों के बीच अंतर

Famicom Disk System

SMB1 का FDS संस्करण अलग memory map रखता है। सभी स्तर pointer बदले हुए हैं, लेकिन डेटा वही है। जो बदलता है : glitch world सूचकांक पूरी तरह अलग हैं :

FDS World 36 → Level ID $09 (5-3 का पानी संस्करण)
  → flagpole मौजूद है ! स्तर खत्म किया जा सकता है।
  → फिर : $27 (सामान्य 7-3) → $44 (4-4 underground)
  → $44 खत्म करने योग्य है → कुल्हाड़ी काम करती है → खेल अंत !

FDS Minus World इसलिए "बोनस world" है जो
खेल पूर्णता तक ले जा सकता है, NES संस्करण के विपरीत।

मेरा पसंदीदा FDS स्तर : ID $5F, निचली tunnel में 3-3 दूसरे भाग का भूमिगत संस्करण (अफसोस कि यह autoscroller है)।

The Lost Levels (जापानी Super Mario Bros. 2)

Lost Levels बहुत कुछ बदलता है :

  1. समान tile/sprite क्रम : और Frankenstein level नहीं (tile और sprite अमान्य ID से भी वही स्तर लोड करते हैं)
  2. high/low अलग दो तालिकाओं के बजाय एकल 16-बिट pointer तालिका
  3. FDS के लिए split 4 डिस्क फाइल :
    • फाइल 1 : world 1-4
    • फाइल 2 : world 5-8
    • फाइल 3 : world 9 + sound engine
    • फाइल 4 : world A-D (पूरी तरह अलग pointer तालिका)
  4. वही Level ID = 4 संभावित स्तर लोड फाइल अनुसार
  5. और Tennis glitch नहीं : continue विकल्प (game over बाद उसी world में जारी) warm start बेकार बनाता है, और खेल world > 9 होने पर तुरंत reset करता है
  6. नई वस्तुएँ : जहरीला मशरूम, अदृश्य block, अदृश्य block fire flower, उल्टे पाइप, हवा, लेकिन मौजूदा सूचियों के बीच डाली गईं → SMB1 के साथ backward असंगति
  7. world 4 बाद Piranha Plant हमेशा लाल, हरे springboard सिर्फ world 2/B/3/C/7 में

Super Mario All-Stars (SNES)

उसी 6502 दिनचर्या वाला सीधा portage (SNES संगत मोड में NES कोड निष्पादित करता है) :

  • Warp क्षेत्र ठीक किया गया : और Minus World नहीं (पाठ प्रकट होने से पहले बाएँ पाइप में प्रवेश सही world ले जाता है)
  • Plantage : ज्यादातर glitch level crash करते हैं (ID $6A और 9-1 सिवाय)
  • जोड़े महल वस्तुएँ : ज्यादा अनोखे रेंडर
  • लेकिन : 4-2 wrong warp अभी भी काम करता है (patch नहीं !)

4-2 wrong warp : वस्तु placement bug

4-2 में, दो pipe transition वस्तुएँ हैं : बेल (warp क्षेत्र) और पाइप (coin cash कमरा)। पहली transition वस्तु (बेल वाली) बेल स्क्रीन पर प्रकट होने से काफी पहले रखी गई है। दूसरी (पाइप वाली) स्तर में बहुत देर रखी गई है।

; 4-2 में transition समय :
; transition वस्तु 1 (बेल → warp क्षेत्र) : बेल से 3 स्क्रीन पहले रखी गई
; transition वस्तु 2 (पाइप → coin cash) : पाइप के 1 स्क्रीन बाद रखी गई
;
; सामान्यतः पहली वस्तु Mario
; पाइप पहुँचने से पहले निष्क्रिय होती है। लेकिन अगर Mario तेज जाता है (या
; ब्लॉक B+right shortcut इस्तेमाल करता है), तो बेल transition
; अभी भी सक्रिय है जब वह पाइप छूता है !
; → खेल coin cash के बजाय warp क्षेत्र लोड करता है।
;
; अगर वस्तु बेल ठीक बाद लेकिन पाइप से पहले
; रखी गई होती, तो bug मौजूद नहीं होता।

लूप स्तर

loop (8-4, 7-4) कैसे काम करते हैं ? स्तर में hardcoded स्क्रीन संख्या और Y स्थितियों वाले checkpoint हैं :

; Checkpoint : {screen_number, vertical_position}
; अगर Mario सही ऊँचाई पर यह checkpoint पार करता है → स्तर जारी रहता है
; वरना → 4 स्क्रीन पीछे warp (64 block)
;
; अनंत लूप बनाने के लिए : vertical_position = $F0
; (स्क्रीन नीचे के नीचे) → मान्य करना असंभव।
;
; checkpoint सरल हैं (एकल flag) world 7 सिवाय
; जो triplet इस्तेमाल करता है (3 flag, कम से कम 1 विफल होना होगा)
;
; warp back कठोर है : tile data offset hardcoded मान पर सेट,
; sprite data offset 0 पर वापस। मौजूद शत्रु
; तुरंत unload होते हैं → firebar गायब हो जाते हैं।

प्रारूप बदलना, कोड नहीं

इस आर्किटेक्चर के सबसे आकर्षक सबकों में से एक यह है कि SMB1 developer 6502 रेंडर कोड कभी छुए बिना बहुत अभिव्यंजक स्तर तंत्र बनाने में सफल रहे। स्तरों के बीच सारी भिन्नता डेटा (pointer, वस्तुएँ, sprite, floor pattern) से आती है, कोड से नहीं।

256 glitch world मौजूद हैं क्योंकि pointer तालिकाएँ 128 प्रविष्टियों × 4 प्रकार के लिए आकार की हैं, और खेल कभी उन मानों को मान्य नहीं करता जिन्हें वह पढ़ता है। जब pointer RAM में गिरता है, तो खेल Mario register को tile की तरह व्याख्या करता है। जब pointer ध्वनि डेटा में गिरता है, तो खेल level design के रूप में संगीत बजाता है। और जब jump table overflow करती हैं, तो खेल crash तक कुछ भी निष्पादित करता है।

More Super Mario Bros. Mechanics Explained , चौथा वीडियो

इन सबसे क्या सीख सकते हैं

  1. tile/sprite अलगाव : दो परतों की पूर्ण स्वतंत्रता, अलग भंडारण क्रमों के साथ जो अनोखे Frankenstein level बनाते हैं
  2. संपीड़न RLE + वस्तु तंत्र : स्तर bitmap नहीं बल्कि रखी वस्तुओं की सूचियाँ हैं, जमीन के लिए floor pattern के साथ
  3. 3 slot कतार : hardware (और स्तर डिजाइन) की कड़ी सीमा
  4. pointer पर पूर्ण भरोसा : खेल pointer और jump table पर भरोसा करता है, जो या तो खेलने योग्य glitch या crash बनाता है
  5. अधिकतम 256 byte : 6502 Y register की सीमा, जो बहुत दूर जाने पर डेटा दोहराती है
  6. Warm start / cold start : "जारी" तंत्र जिसने cart swap Tennis → Mario का द्वार खोला

सबसे सुंदर : यह सब, 40KB में समाने वाला 6502 कोड है। कोड सीधे hardware से संवाद करता है और हर स्मृति पहुँच पर पूर्ण भरोसा देता है। अगर pointer सड़ा है, तो खेल crash करता है। और crash को हम glitch world कहते हैं।

याद रखने वाली 3 चीजें

  1. glitch world खराब गिरने वाले pointer हैं , खेल में 128 ID × 4 क्षेत्र प्रकार हैं, लेकिन सिर्फ 34 अनोखे स्तर। जब world number भ्रष्ट होता है (Tennis या wall clip से), तो खेल दूसरे स्तर के लिए डिजाइन pointer लोड करता है, और 512 संभावित संयोजन अप्रत्याशित परिणाम बनाते हैं।

  2. Minus World warp bug और भ्रष्टाचार का संयुक्त है , 1-2 में बायाँ पाइप, अगर पाठ प्रकट होने से पहले सक्रिय होता है, तो world 36 (0x24) लोड करता है। यह world Level ID $01 (2-2 पानी) की ओर इंगित करता है, flagpole रहित स्तर। और चूँकि world 36 के लिए pipe transition नहीं है, स्तर अनंत लूप करता है। जाँच अनुपस्थिति icon बनाती है।

  3. Tennis → Mario, OoT → Paper Mario से 15 साल पहले , NES RAM condenser और SMB1 warm start / cold start तंत्र से cartridge swap बचती है। Tennis कदम counter (जो कदम ध्वनि बजाकर RAM byte बढ़ाता है) ठीक world number पते पर गिरता है। top score अंक 0 रहने होंगे, $A5 byte अक्षुण्ण होना होगा, और खेल को warm start पहचानना होगा, परिस्थितियों का पूर्ण संयोग जो सिर्फ Tennis से काम करता था।

Retro Game Mechanics Explained के मूल वीडियो चींटी काम हैं, 6502 disassembly, सभी स्तरों के स्वचालित नक्शे, cart swap और warm start व्याख्याओं पर विवरण स्तर। अगर तुमने शृंखला नहीं देखी, तो देखो, यह छोटी है और हर मिनट घना है।

नक्शों का स्रोत कोड rgmechex.com पर उपलब्ध है, और SMB1 का पूरा disassembly बहुत से repo पर ओपन सोर्स है। 40 साल पहले, जापानी programmer ने शून्य इकाई परीक्षण और शून्य bug tracker के साथ 6502 में यह स्तर तंत्र लिखा था, और हम आज उनका कोड खोलकर अनुकूलन तकनीकें सीखना जारी रखते हैं।

Super Mario Bros. : صيغة المستوى والمؤشرات و256 glitch worlds

كيف تتسع 128 مستوى × 4 أنواع منطقة في 40KB من ROM، ولماذا يوجد Minus World، وكيف يمكن لمباراة Tennis على NES تحميل glitch worlds.

مقدمة

Super Mario Bros. هي 40 كيلوبايت من ROM. ثمانية عوالم و32 مستوى وأعداء وموسيقى وpower-ups، كل شيء يتسع هناك.

لكن إذا فتحت محاكيا وعبثت بالبايتات الصحيحة، تستطيع تحميل مستوى 36-1. أو 255-1. أو الهبوط في عالم كل شيء فيه مصنوع من sprites لـBowser وأنابيب لا تؤدي لأي مكان.

توجد glitch worlds هذه لسبب بسيط : نظام تخزين مستويات SMB1 هو أعجوبة تحسين 8-bit، وعندما نُجبِر اللعبة على القراءة حيث لا يجب، يعطي ذلك نتائج ساحرة.

صنعت Retro Game Mechanics Explained سلسلة من 4 فيديوهات عن ذلك، سنجمعها في غوصة واحدة في كود 6502 للعبة الأكثر مبيعا في عصرها.

GLITCH OBJECTS -- le titre de la série RGMechEx sur les mécaniques cachées de SMB1

World 9-1 -- l'écran titre du premier glitch world accessible via le cart swap Tennis

البدء الدافئ : لماذا تنجو RAM الخاصة بـTennis في SMB1

قبل الكلام عن تخزين المستويات، يجب فهم كيف تقلع SMB1. لأن glitch تبديل الخرطوشة في NES Tennis يرتكز كليا على نظام كشف البدء الدافئ / البدء البارد في اللعبة.

البايتات الـ41 المحفوظة

عندما تكشف SMB1 بدءا باردا (أول تشغيل أو إطفاء/تشغيل)، تمسح كل RAM. لكن عندما تكشف بدءا دافئا (زر reset، دون قطع طاقة)، تحفظ منطقة ذاكرة من 41 بايت :

; البايتات الـ41 المحفوظة في RAM أثناء بدء دافئ
; العناوين $075F-$0787
;
; $075F : بايت البدء (world - 1)    [1 byte]
; $0760 : علم اختيار العالم (زر B) [1 byte]
; $0761-$0762 : غير مستخدم                    [2 bytes]
; $0763-$0768 : مؤقت (6 أرقام، 3 معروضة) [6 bytes]
; $0769-$076E : عملات Luigi                   [6 bytes]
; $076F-$0774 : عملات Mario                   [6 bytes]
; $0775-$077A : نتيجة Luigi                   [6 bytes]
; $077B-$0780 : نتيجة Mario                   [6 bytes]
; $0781-$0786 : أعلى نتيجة (6 أرقام، 1 مخفي) [6 bytes]
; $0787 : البايت السحري $A5                 [1 byte]

تخدم هذه البايتات الـ41 وظيفة واحدة : السماح للاعب بـالمواصلة في نفس العالم بعد game over. إذا مت في 6-3، تكتب اللعبة العالم 6 في بايت البدء، وفي شاشة العنوان، إذا أبقيت A + Start، تعيد البدء في 6-1.

Les 41 bytes préservés en RAM lors d'un warm start -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, et le byte magique $A5

الفحص المزدوج للبدء الدافئ

Cold start vs warm start -- le diagramme de détection du reset

عندما تقلع SMB1، لا تتحقق من معيار واحد بل اثنين :

CheckWarmStart:
  ; 1. فحص البايت السحري $A5 في $0787
  lda $0787
  cmp #$A5
  bne ColdStart        ; ليس $A5 ← بدء بارد

  ; 2. فحص أرقام أعلى نتيجة الستة ($0781-$0786)
  ;    كل رقم يجب أن يكون بين 0 و9
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; رقم >= 10 ← بدء بارد
  inx
  cpx #6
  bne CheckLoop

  ; إذا نجح الشرطان ← بدء دافئ
  ; لا تُمسَح RAM، ويُحفَظ عالم البدء
  jmp WarmStartBoot

La vérification du byte $A5 et des digits du top score -- le coeur du warm start

لماذا فحص مزدوج ؟ لأن بايت $A5 قد يكون موجودا بالمصادفة (لعبة أخرى تترك هذه القيمة، أو حالة السكون الافتراضية لشريحة RAM). وبفحص أن أرقام أعلى نتيجة صالحة (0-9)، نتأكد أن البيانات متسقة.

لماذا Tennis هي اللعبة الوحيدة التي تنجح

عندما نُدخِل SMB1 أول مرة (بدء بارد)، تقوم اللعبة بـ :

  1. مسح كل RAM ← أعلى نتيجة = 0 وبايت العالم = 0
  2. كتابة $A5 في العنوان $0787

ثم نبدّل نحو Tennis دون إطفاء المنصة. تقوم Tennis بـ :

  • لا تنظف RAM عند الإقلاع (قليل من ألعاب NES يفعل ذلك)
  • لا تكتب على بايتات أعلى نتيجة ← تبقى 0 (صالحة)
  • لا تمس البايت $A5 ← يبقى موجودا
  • تستخدم العنوان $075F لعداد خطوات اللاعب
; زيادة الخطوات في Tennis :
; مع كل خطوة للاعب على الملعب، تزيد Tennis البايت في $075F.
; هذا البايت نفسه تستخدمه SMB1 كـ"رقم العالم - 1".
;
; 0 خطوة  ← العالم 0 ← SMB1 = العالم 1
; 1-7 خطوات ← العالم 1-7 ← عوالم عادية
; 8+ خطوات ← العالم 8+ ← glitch worlds !
;
; لا يزيد العداد إلا عندما تتوقف الموسيقى
; (أصوات الخطوات لا تعمل أثناء الموسيقى).

وعندما نعيد SMB1 :

  1. بايت $A5 ما زال هناك (لم تمسه Tennis)
  2. أرقام أعلى نتيجة ما زالت 0 (صالحة)
  3. بايت العالم صار الآن 8+ (زادته خطوات Tennis)
  4. تكشف SMB1 بدءا دافئا ← تحفظ بايت العالم الفاسد
  5. الإبقاء على A + Start ← العالم 9-1 والعالم A-1 والعالم 36-1، إلخ.

لماذا يجب إقلاع Mario قبل Tennis

دقة : يجب أولا إقلاع SMB1، ثم Tennis، ثم SMB1 مجددا. وإذا بدأت مباشرة بـTennis، فلن يُكتَب بايت $A5 أبدا (لا تكتبه Tennis)، فيفشل كشف البدء الدافئ وتُمسَح RAM.

Le compteur de pas de Tennis : chaque footstep incrémente le world byte

Access Glitch Worlds via NES Tennis -- la vidéo qui explique le cart swap

كيف تخزن SMB1 مستوياتها في 40KB

وجب على Nintendo R&D4 حل مشكلة بسيطة ظاهريا : تمثيل مستويات تتحرك أفقيا مع tiles وأعداء وعناصر، الكل في ميزانية ROM ضيقة للغاية.

والحل هو فصل إلى طبقتي بيانات مستقلتين تماما :

مخطط tiles (خريطة المستوى)

يُعرَّف كل مستوى بمؤشر نحو بنية tiles مضغوطة في ROM. الضغط بدائي لكن عبقري : بايت "تحكم" متبوع بـ1-3 بايتات بيانات.

يستخدم صيغة tile نظام runs (شبيه RLE) :

; صيغة tile في SMB1 (مبسطة)
; كل "أمر" هو بايت تحكم :
;
; $00-$7F : وضع tile، وتقدم عمودا واحدا
; $80-$BF : وضع tile مكررة N مرات (N = بايت - $80 + 1)
; $C0-$FF : أمر خاص (نهاية سطر وقفز وتغيير لوحة)
;
; مثال : لرسم 3 طوبات متتالية :
;   $82 $01    ; كرر tile رقم $01 (طوبة) 3 مرات

يحوي كل مستوى 13 سطرا من 16 عمودا من tiles (13×16 = 208 tiles مرئية). لكن الصيغة المضغوطة تسمح بالنزول أدنى بكثير، فمثلا السماء والأعمدة الفارغة لا تأخذ حيزا تقريبا.

حلقة العرض في 6502 :

; فك ضغط tile، الحلقة الرئيسية
; مدخل : مؤشر tile_data في $XX
; مخرج : خريطة tiles المستوى في RAM الخاصة بـPPU
;
DecompressTile:
  lda (tile_ptr),y      ; قراءة بايت تحكم
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F : tile مفردة
  cmp #$C0
  bcc RunLength         ; $80-$BF : طول تشغيل
  jmp SpecialCommand    ; $C0-$FF : أمر خاص
;
SingleTile:
  sta PPU_DATA          ; كتابة tile مباشرة
  jmp Next
;
RunLength:
  sec
  sbc #$7E              ; N = تحكم - $7E
  tax
  lda (tile_ptr),y      ; قراءة tile للتكرار
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

مخطط sprites (الأعداء والعناصر)

بالتوازي، تُخزَّن الأعداء والعناصر (كتل ؟ وأنابيب وgoombas وkoopas) في بنية منفصلة تماما. كل ظهور يُعرَّف ببايتين :

; صيغة sprite في SMB1
; بايت 0 : موضع X (بالأعمدة)
; بايت 1 : نوع sprite + بتات صفحة Y
; يُشتَق Y من الفهرس في التسلسل
;
; تسلسل sprites :
;   $01 $4B    ; goomba في العمود 1
;   $09 $4B    ; goomba في العمود 9
;   $10 $61    ; كتلة ؟ في العمود 16 (تحوي قطعة)
;   $15 $54    ; koopa خضراء في العمود 21
;   $FF        ; نهاية التسلسل

يستطيع كل مستوى الإشارة حتى 5 صفحات sprites مختلفة (في الواقع، 5 "شاشات" من 16 عمودا)، لكن عمليا لا تستخدم معظم المستويات إلا 2-3.

جدول المؤشرات

عبقرية التصميم هي جدول المؤشرات. يُخزَّن كل مستوى كـزوج من عناوين ROM :

// البنية الداخلية (مبسطة) لخريطة العالم
struct LevelPointer {
    uint16_t tile_ptr;   // عنوان ROM لبيانات tiles
    uint16_t sprite_ptr; // عنوان ROM لبيانات sprites
};

// 4 جداول منفصلة، واحد لكل AreaType :
// 0 = ماء و1 = خارجي و2 = تحت أرضي و3 = قلعة
LevelPointer level_table[4][128];

128 مدخلا لكل جدول. 4 أنواع منطقة. 512 تركيبة ممكنة، لكن جزءا فقط يستخدمه اللعب الرسمي. الباقي RAM غير مُهيَّأة أو بيانات تُفسَّر كمؤشرات.

عندما يحمّل اللعب مستوى، يفعل ذلك :

; تحميل مستوى
; A = النوع (0-3) وX = المعرف (0-127)
;
LoadLevel:
  sta AREA_TYPE
  asl                  ; ×2 للإزاحة في جدول 16-bit
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; مؤشر نحو tiles
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; مؤشر نحو sprites
  jsr DecompressTiles

يمنح اللعب ثقة كاملة للمؤشرات : يقرأ العنوان في الجدول ويفك ضغط ما يوجد في هذا العنوان، نقطة نهائية.

Level ID $06 (Water) -- 9-1, la version sous-marine de 6-2

La table des Level IDs : 128 entrées possibles, 34 assignées

L'ordre différent des pointeurs tiles et sprites -- la cause des Frankenstein levels

المستويات الـ34 الفريدة ونظام المعرف 7-bit

Le chip RAM de la NES (MB8416A) -- c'est lui qui conserve les données quand on swap les cartouches

لا تملك SMB1 32 مستوى، بل 34 مستوى فريدا. الكثير من المستويات مكررات (5-3 = 1-3 لكن مع Bullet Bills) موسومة بعلم "وضع صعب". المستويات الحقيقية الفريدة :

  • ماء (نوع 0) : 3 مستويات (2-2 و7-2 ومنطقة مكافأة 5-2/6-2)
  • خارجي (نوع 1) : 22 مستوى (منها غرفتا سحب المكافأة)
  • تحت أرضي (نوع 2) : 3 مستويات (منها غرف المكافأة تحت الأرضية)
  • قلعة (نوع 3) : 6 مستويات
  • + غرفة مشهد واحدة (قبل المستويات تحت الأرضية/المائية)
  • + منطقة warp واحدة في 4-2

لكل مستوى معرف على 7 بت. البتات الخمسة الضعيفة = الرقم في المجموعة الفرعية، والبتان القويان = نوع المنطقة :

; ترميز 7-bit لمعرف المستوى
; بتان 6-5 : النوع (00=ماء و01=خارجي و10=تحت أرضي و11=قلعة)
; بتات 4-0 : الرقم في المجموعة الفرعية
;
; معرفات ماء      : $00-$02  (أنواع 00 وأرقام 0-2)
; معرفات خارجية  : $20-$35  (أنواع 01 وأرقام 0-21)
; معرفات تحت أرضية: $40-$42  (أنواع 10 وأرقام 0-2)
; معرفات قلعة     : $60-$65  (أنواع 11 وأرقام 0-5)
;
; معرف $25 = %0100101 ← نوع 01 (خارجي) ورقم 5 ← 1-1
; معرف $23 = %0100011 ← نوع 01 (خارجي) ورقم 3 ← 6-2

128 معرفا ممكنا ($00-$7F)، و34 فقط مُسنَدة لمستويات حقيقية. تشير المعرفات غير المستخدمة لأي شيء.

جداول المؤشرات : قائمتان وترتيبان

لا تُخزَّن مؤشرات tiles وsprites بنفس الترتيب. يستخدم الكود قائمتين 16-bit منفصلتين (بايت عالٍ / بايت منخفض في جدولين متميزين) :

ترتيب مؤشرات sprites :
  فهرس 0-5   : قلعة (6 مستويات)
  فهرس 6-27  : خارجي (22 مستوى)
  فهرس 28-30 : تحت أرضي (3 مستويات)
  فهرس 31-33 : ماء (3 مستويات)
;
ترتيب مؤشرات tiles :
  فهرس 0-2   : ماء (3 مستويات)
  فهرس 3-24  : خارجي (22 مستوى)
  فهرس 25-27 : تحت أرضي (3 مستويات)
  فهرس 28-33 : قلعة (6 مستويات)

لماذا ترتيبان مختلفان ؟ لا سبب تقني، فهكذا نُظِّمت البيانات غالبا أثناء التطوير. لكن ذلك يخلق عاقبة ساحرة : عندما يكون معرف مستوى غير صالح، تحمّل مؤشرات tiles وsprites مستويين مختلفين، خالقة مستويات Frankenstein.

للتنقل بين هاتين القائمتين، يستخدم اللعب جداول إزاحة صغيرة (كفهرس) :

; جداول الإزاحة حسب النوع (ماء وخارجي وتحت أرضي وقلعة)
; كل مدخل = فهرس البداية في القائمة الموافقة
;
SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; ماء=31 وخارجي=6 وتحت أرضي=28 وقلعة=0
;
TileOffsetTable:
  .byte $00, $03, $19, $1C    ; ماء=0 وخارجي=3 وتحت أرضي=25 وقلعة=28

لتحميل مستوى 6-2 (معرف $23 وخارجي رقم 3) :

; 1. النوع = 01 (خارجي) ← الفهرس في جدول الإزاحة = 1
; 2. إزاحة Sprite = SpriteOffsetTable[1] = 6
;    الفهرس النهائي = 6 + 3 (رقم المستوى) = 9 ← مؤشر sprites العاشر
; 3. إزاحة Tile = TileOffsetTable[1] = 3
;    الفهرس النهائي = 3 + 3 = 6 ← مؤشر tiles السابع
; 4. النتيجة : مؤشر tiles رقم $A619 + مؤشر sprites رقم $9ED0 = 6-2 ✓

الآن، ماذا يحدث مع معرف غير صالح مثل $43 (تحت أرضي رقم 3، غير موجود) ؟

; معرف $43 والنوع = 10 (تحت أرضي) والرقم = 3
; إزاحة Sprite = SpriteOffsetTable[2] = $1C = 28
;   الفهرس = 28 + 3 = 31 ← مؤشر sprites الـ32 = ماء مكافأة 5-2 !
; إزاحة Tile = TileOffsetTable[2] = $19 = 25
;   الفهرس = 25 + 3 = 28 ← مؤشر tiles الـ29 = 1-4 (قلعة) !
;
; النتيجة : مستوى تحت أرضي مع tiles الخاصة بـ1-4
; وBloopers منطقة ماء 5-2. Frankenstein حقيقي.

Level ID $43 -- Frankenstein level : tiles 1-4 + sprites eau 5-2

Exploring Glitch Level Pointers -- les tables d'offset expliquées

Le world index table -- quand l'overflow de world 9 crée un glitch level

جدول فهرس العالم : لماذا يفيض العالم 9

هناك جدول ROM من 8 بايتات يعطي فهرس أول مستوى لكل عالم (1-8). وبعده مباشرة، جدول 36 معرف مستوى لكل المستويات بترتيب اللعب.

; جدول WorldIndexTable (8 بايتات)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   ← العالم 1 يبدأ عند المستوى 0
;   ← العالم 2 يبدأ عند المستوى 5
;   ← العالم 8 يبدأ عند المستوى 33
;
; جدول LevelIDTable (36 بايتا)
  .byte $25, $28, $29, $26, $24, ... ; معرفات المستويات الـ36

وعند محاولة تحميل العالم 9، يقرأ اللعب البايت التاسع من WorldIndexTable... غير الموجود. فيفيض ببايت واحد في LevelIDTable، ويقرأ القيمة $25، ثم يستخدم $25 كفهرس في LevelIDTable (المدخل 37)، مما يفيض مجددا ببايتين في SpriteOffsetTable، ويقرأ القيمة 6.

; العالم 9 :
;   1. WorldIndexTable[8] (فيض) ← يقرأ $25 في LevelIDTable
;   2. LevelIDTable[37] (فيض) ← يقرأ البايت الثاني من SpriteOffsetTable = 6
;   3. المعرف = 6 ← مستوى ماء رقم 6 (غير موجود)
;   4. مؤشر Tile = مؤشر ماء رقم 6 = tiles الخاصة بـ6-2
;   5. مؤشر Sprite = فهرس 31+6 = 37 > 33 ← مؤشر غير صالح
;   6. النتيجة : 6-2 تحت الماء مع sprites مُفسَدة
;      ← العالم 9-1 !

بالنسبة للعالم G (16)، يذهب الفيض أبعد ويقع على معرف المستوى $01، وهو مستوى المشهد الذي يسبق 1-2 :

; العالم G (16) :
;   WorldIndexTable[15] ← يقرأ $01 في LevelIDTable
;   LevelIDTable[1] = $29 (مشهد 1-2)
;   ← العالم G-1 = مشهد دخول 1-2

لماذا توجد glitch worlds

يملك اللعب 32 مستوى "شرعيا" (8 عوالم × 4 مستويات). لكن جدول المؤشرات يبلغ 128 مدخلا لكل نوع منطقة. تحوي المداخل بعد المستوى 32 ما يوجد في ROM في هذه العناوين، أحيانا مستوى آخر، وأحيانا بيانات صوتية، وأحيانا RAM، وأحيانا أي شيء.

Level ID $01 Water (Minus World) -- tile pointer $AE45, sprite pointer $A171

معرف المستوى $01 + النوع 0 = Minus World

أشهر glitch worlds. معرف المستوى $01 في النوع 0 (ماء) يشير نحو :

  • مؤشر Tile : $AE45 ← المنطقة تحت المائية في 2-2/7-2
  • مؤشر Sprite : $A171 ← sprites الخاصة بـ2-2/7-2

النتيجة : مستوى ماء يشبه 2-2، لكنه يدور للأبد لأن flagpole غير موجود. يدور المستوى في حلقة للأبد.

إنه مستوى 36-1 (أو 36-1 في العالم $-1).

Le warm start check de SMB1 -- c'est lui qui permet au Minus World d'exister

; لماذا ينقص flagpole في Minus World :
; لا تحوي sprites الخاصة بـ2-2/7-2 ($A171) flagpole
; في تسلسلها. يبحث اللعب عن sprite رقم $FD (flagpole)
; لكنه لا يجده أبدا ← حلقة لانهائية
;
; يواصل اللعب توليد المستوى للأبد
; حتى يبلغ المؤقت صفرا.

المؤشرات التي تشير نحو RAM

عندما يشير مؤشر tile أو مؤشر sprite نحو عنوان في RAM ($00-$7F) بدلا من ROM، يحاول اللعب تفسير تغيرات RAM المستمرة كـtiles :

; مثال : معرف المستوى $03 في ماء
; مؤشر Tile : $A46B (3-3، صالح)
; مؤشر Sprite : $009D (يشير نحو RAM الصفحة صفر !)
;
; تحوي RAM الصفحة صفر سجلات اللعب،
; وموضع Mario وحالة العدادات...
; يفك اللعب ضغط ذلك كتسلسل sprites،
; والنتيجة مستوى مع أعداء
; هم في الواقع قيم سجلات.

وعندما تتغير الصفحة صفر (لأن Mario يتحرك أو المؤقت يدور، إلخ)، تتغير "sprites" المستوى أيضا. لهذا تملك بعض glitch worlds أعداء تومض وتتحول باستمرار.

Level ID $03 Water -- sprite pointer $009D pointe vers la RAM, niveau injouable

معرف المستوى $36 : المستوى الفارغ (خارجي)

معرف المستوى $36 في خارجي :

  • مؤشر Tile : $AC35 (1-2)
  • مؤشر Sprite : $A0D8 (1-2)

النتيجة : لا شيء. يحمّل اللعب المستوى لكنه موسوم "دون مستوى" في فهرس RGMechEx. قد تكون tiles صالحة لكن sprites تشير نحو مكان ينتج مستوى فارغا أو غير عامل.

معرف المستوى $1D (قلعة) : بطل الانهيارات

معرف المستوى $1D في قلعة :

  • مؤشر Tile : $A210 (4-4)
  • مؤشر Sprite : $7EA0 (RAM !)

مؤشر Sprite في RAM = sprites غير معرفة. يحاول اللعب عرض كرة Spiny أو قاذف Bullet Bill في أول سطر tiles. ينهار فورا.

; عندما يشير مؤشر sprite نحو RAM،
; يفك اللعب ضغط بايتات تتغير طوال الوقت
; كتعليمات "ظهور". النتيجة :
; - ظهور عناصر غير موجودة (قيمة غير معرفة)
; - انهيار PPU عندما يحاول sprite الخاص بـNES عرض tile غير صالح
; - تجمد كامل للمنصة

glitch worlds الـ256 المفهرسة

كتب RGMechEx سكربت يولد خرائط كل المستويات، للأنواع الأربعة للمنطقة، والمعرفات الـ128 لكل واحد.

عداد العالم على 8 بت (0-255). العوالم 1-8 شرعية. يبقى 248 glitch worlds محتملة. يطابق كل glitch world أول مستوى لهذا العالم، وتُحسَب معرف مستواه بآلية فيض WorldIndexTable.

Table des glitch worlds -- 248 mondes corrompus, 68 premiers niveaux accessibles

من المعرفات الـ128 الممكنة، 68 فقط هي "مستوى أول" لعالم (قابلة للوصول عبر رقم glitch world). الستون الأخرى مستويات 2+ أو غير قابلة للوصول.

النوع معرفات فريدة قابلة للعب معرفات منهارة معرفات فارغة
ماء (0) ~20 ~60 ~48
خارجي (1) ~30 ~55 ~43
تحت أرضي (2) ~15 ~65 ~48
قلعة (3) ~25 ~58 ~45

تؤدي الكثير من المعرفات لنفس المستوى لأن المؤشرات تقع على نفس عناوين ROM. معرف المستوى $28 (خارجي) مثلا، مؤشر tiles رقم $A7CD (2-1)، يظهر في 38 glitch worlds مختلفة، لأن مؤشر sprite رقم $9F51 يشير نحو منطقة ROM مستخدمة كحشو/بيانات صوتية يُعاد استخدامها من الكثير من المعرفات.

Carte du niveau ID $28 (Overworld) -- 2-1 tiles avec des sprites normaux, 38 glitch worlds

Super Mario Bros. Glitch Levels Explained -- la 3ème vidéo

مستويات glitch الستة الفريدة فعلا

من معرفات glitch level الـ19 القابلة للوصول، 6 فقط لا تنهار فورا عند التحميل :

العالم معرف المستوى الوصف
E-1 (224) $50 كتلة ؟ واحدة فوق هاوية. يموت Mario فورا.
W $57 يظهر Mario عالقا، عاجزا عن الحركة.
42 (133) $50 نفق سحب يحبس Mario إذا ذهب بعيدا بما يكفي.
62 (131 و240) $4D قلعة متجمدة : يظهر Mario في الأعلى، لا يستطيع السقوط ← عالق.
127 $4B نفق تحت أرضي، لكنه ينهار إذا ذهب بعيدا.
137 $4B يفعّل التمرير الآلي للمشاهد. يلاقي Mario كتلة طوب وحيدة تحجبه للأبد.

Level ID $50 (cloud tunnel) -- le glitch world 42-1 et E-1 Level ID $4D (castle) -- world 62-1, Mario bloqué au spawn Level ID $4B (tunnel) -- world 127-1, crashe si on va trop loin

ستة glitch worlds من 248 تنتج شيئا جديدا فعلا. الباقي مستويات عادية بنوع منطقة خطأ، أو شاشات سوداء.

صيغة المستويات بالتفصيل

نحو الصيغة الدقيقة لبيانات المستوى، لفهم لماذا تصمد glitch levels (أم لا).

ترويسة المستوى : بايتان و6 خصائص

يبدأ كل مستوى بترويسة بايتين تتحكم في 6 خصائص :

; بايت 0 : مؤقت + بداية Y + معدِّل
;   بتان 7-6 : مؤقت (00=دون تغيير و01=200 و10=300 و11=400)
;   بتات 5-3 : بداية Y الخاصة بـMario (111/110 = مشي آلي)
;   بتات 2-0 : معدِّل نوع المستوى
;              000=افتراضي و001=موجات و010=جدار طوب،
;              011=قاع ماء و100=ليل و101=ثلج،
;              110=ثلج ليل و111=ليل رمادي
;
; بايت 1 : منصة + خلفية + نمط أرضية
;   بتان 7-6 : منصة خاصة (00=شجرة و01=فطر،
;                                 10=Bullet Bill و11=سحابة)
;   بتان 5-4 : خلفية (00=لا شيء و01=سحب،
;                           10=جبال و11=أسوار)
;   بتات 3-0 : نمط أرضية أولي (0-15)

يتحكم معدِّل النوع في تغيرات بصرية : الموجات أعلى مستويات الماء، وخلفية الطوب في 8-3، ولوحة الليل في 4-3، وثلج 6-2، إلخ.

عناصر tiles : بايتان وعلم الشاشة التالية وطابور 3 خانات

بعد الترويسة تأتي قائمة عناصر tile، كل عنصر بايتان. يسم البايت $FD نهاية القائمة.

; صيغة عنصر tile (16 بت) :
; بايت 0 :
;   بتات 7-4 : موضع X (عمود 0-15)
;   بتات 3-0 : موضع Y
;     Y=0-11  : موضع Y عادي
;     Y=12    : عناصر خاصة (ثقوب وجسور وحبال وكتل ؟)
;     Y=13    : تخطي شاشة / عناصر خاصة 2
;     Y=14    : تغيير معدِّل/مشهد/أرضية
;     Y=15    : عناصر خاصة 3 (قلعة وسلالم وأنبوب كبير)
;
; بايت 1 :
;   بت  7   : علم الشاشة التالية
;   بتات 6-4 : نوع العنصر (0-7)
;   بتات 3-0 : عرض/ارتفاع / نوع فرعي

وعند ضبط بت "الشاشة التالية"، يُزاد عمود العمل الحالي 1. وهذا يسمح بوضع عناصر بعد الأعمدة الـ16 الأولى. يجب سرد العناصر بالترتيب (يسارا ليمينا) لأن اللعب يحمّلها تسلسليا :

; روتين التحميل له مرحلتان لكل عمود :
; مرحلة 1 : البحث عن العناصر الجديدة التي تبدأ على هذا العمود
;            وإضافتها لطابور الانتظار
; مرحلة 2 : معالجة كل عنصر في الطابور برسم tiles،
;            وإزالة ما ينتهي على هذا العمود

يبلغ الطابور بالضبط 3 خانات. عاقبة مباشرة : لا يمكن وجود أكثر من 3 عناصر تبدأ على نفس العمود. وإذا كان الطابور ممتلئا، يُتجاهَل العنصر الرابع ولن يُحمَّل أبدا.

لهذا تتجنب المستويات المصممة جيدا تكديس عناصر كثيرة. مثال في 1-2 : العمود مع كتلة 1up في السقف + الطوب بجانبه مُقسَّم إلى عنصرين متميزين لاحترام حد 3.

موضع Y الخاص : 12 و13 و14 و15

عندما Y=12، لا موضع Y للعنصر (مُرمَّز صلبا حسب النوع) :

; Y=12 : عناصر دون موضع Y
;   نوع 0 : ثقب (يزيل الأرض)
;   نوع 1 : حبل منصة متحركة
;   أنواع 2-4 : جسور عند Y ثابت
;   نوع 5 : ثقب مع ماء/حمم
;   أنواع 6-7 : صفوف كتل ؟

عندما Y=13، مجموعتان فرعيتان. إذا كان بت 6 من بايت 1 عند 1 :

; Y=13 وبت6=1 : عناصر خاصة
;   0 = أنبوب L (مشهد) و1 = عمود علم و2-3 = جسر/فأس/مطرقة (نهاية قلعة)
;   4 = إيقاف شاشة و5 = أعداء عشوائيون و6 = مستوى حلقي و7+ = انهيار محتمل

وإذا كان بت6=0، ترمّز البتات الخمسة الضعيفة تخطي شاشة (قفز مباشرة لشاشة N، دون المرور بعلم الشاشة التالية واحدا بواحد).

وعندما Y=14 : نفس المبدأ مع بت6=1 لتغيير معدِّل النوع، وبت6=0 لتغيير الخلفية + نمط الأرضية.

أنماط الأرضية : 16 نمط أرض

أرضية المستويات ليست عناصر فردية. تستخدم SMB1 أنماط أرضية، نمط خلفية يُطبَّق على كل الأعمدة حتى التغيير التالي :

; أنماط أرضية (4 بت = 16 احتمالا)
;   0 = فارغ كامل
;   1 = أرض بارتفاع بلاطتين
;   2 = أرض بارتفاع بلاطة
;   3 = أرض + قاع
;   4 = أرض + قاع 2
;   5 = أرض نصف بلاطة
;   6 = 3/4 أرض
;   ... حتى 15 = ممتلئ كامل (أرض + سقف)

لهذا الثقوب عناصر : إنها تتجاوز نمط الأرضية على عمود بعينه، دون تغيير النمط لبقية الباقي.

حد 256 بايت والتكرار

تتسع كل بيانات tile لمستوى في 256 بايت حدا. يُستخدَم سجل Y في 6502 كفهرس، وهو 8 بت. وإذا بلغ اللعب نهاية البيانات دون إيجاد بايت $FD، يعيد الحلقة للبداية ويكرر 256 بايت للأبد :

; فهرس Y = 8 بت ← حد 256 بايت لبيانات tiles
; إذا فاض Y (255 ← 0) دون لقاء $FD ← تكرار
; نفس الشيء لـsprites، لكن عناصر الأنبوب (3 بايتات)
; تزيح تكافؤ الفهرس مع كل تحميل.

تستغل بعض glitch levels هذا التكرار لتوليد مستويات تدوم "للأبد".

نظام sprites : بايتان + انتقالات أنبوب

تتبع sprites صيغة مشابهة، لكن دون ترويسة ومع بعض الفروق الأساسية. يسم البايت $FF نهاية القائمة.

; صيغة sprite (بايتان) :
; بايت 0 : موضع X (عمود)
; بايت 1 :
;   بت 7 : علم الشاشة التالية
;   بتات 6-0 : نوع sprite
;       بعض الأنواع تتضمن : goomba وkoopa وBlooper،
;       Bullet Bill وLakitu وSpiny ومنصات،
;       أمر منطقة warp وtoad/أميرة،
;       أوامر ظهور مجموعات أعداء

البت الضعيف من بايت 1 هو علم المستوى الصعب : إذا ضُبط عند 1، لا يظهر sprite إلا في المستويات ≥ 5-3. وهكذا تُنشأ مستويات "الوضع الصعب".

موضع Y رقم 15 = تخطي شاشة (مطابق لـtiles). موضع Y رقم 14 = انتقال أنبوب (3 بايتات) :

; Sprite عند Y=14 : انتقال أنبوب/كرمة (3 بايتات !)
;   بايت 0 : موضع X
;   بايت 1 : بتات 6-0 = معرف المستوى 7-bit (وجهة)
;   بايت 2 : بتات 4-0 = شاشة الوجهة
;            بتات 7-5 = العالم الذي يصح فيه هذا الانتقال
;
; لماذا عالم ؟ تُعاد استخدام غرف المكافأة بين العوالم.
; مثال : غرفة مكافأة 1-1 تُستخدَم أيضا في 2-1 و7-1.
; لهذه الغرفة 3 انتقالات، واحد لكل عالم، لكي يظهر Mario
; في المكان الصحيح.

لا تملك sprites نظام طابور. الحد الوحيد أنه لا يمكن وجود أكثر من 4 sprites مُحمَّلة في آن واحد في منطقة الظهور (خارج الشاشة يمينا بقليل). وبعد ذلك، تُتجاهَل sprites.

كيف تصل لglitch worlds

هناك طريقتان رئيسيتان.

الطريقة التقليدية : اختراق الجدار

يسمح اختراق الجدار (المرور عبر الجدران) بالخروج من المستوى العادي والمشي حتى منطقة warp المخفية. بالتلاعب بعداد العالم عبر RAM، يمكن تحميل أي معرف مستوى.

التقنية :

  1. العالم 1-2 : الذهاب في أنبوب النهاية المخفي
  2. تنفيذ اختراق الجدار على الجدار الأيمن
  3. المشي في الفراغ حتى منطقة warp
  4. يفسّر اللعب القيم كعوالم

لكن هذه الطريقة لا تعطي الوصول إلا لجزء صغير من glitch worlds.

الطريقة القصوى : تبديل خرطوشة NES Tennis

انظر قسم "البدء الدافئ" أعلاه للتفصيل الكامل. باختصار : يكتب عداد خطوات Tennis على نفس بايت RAM كعالم بداية SMB1، وكشف البدء الدافئ يحفظ هذه القيمة.

ركن العابثين : الكود لاستكشاف الكل

إذا أردت استكشاف glitch كلها بنفسك في محاكٍ، تستطيع ترقيع معرف المستوى مباشرة :

; ترقيع لـFCEUX / Mesen :
; عنوان RAM رقم $075F = معرف المستوى الحالي
; عنوان RAM رقم $0760 = نوع المنطقة (0=ماء و1=خارجي و2=تحت أرضي و3=قلعة)
;
; مثال : تحميل المستوى 57 (0x39) في خارجي
; في المحاكي، افتح متتبع الذاكرة واكتب :
; $075F = 0x39
; $0760 = 0x01
; ثم ادخل أنبوب warp أو مُت وأعد البدء
; ← يحمّل اللعب مستوى المعرف $39 في خارجي

نشرت RGMechEx القائمة الكاملة للمستويات الـ128 × الأنواع الأربعة مع خرائط مولدة آليا على rgmechex.com. يعرض كل مدخل مؤشر tile ومؤشر sprite وخريطة بصرية للمستوى.

المستويات الأكثر جنونا

معرف المستوى $1F (ماء) : 15 glitch worlds في واحد

مؤشر tile رقم $A302 (3-4) مدمجا مع مؤشر sprite رقم $02A0 يعطي 15 glitch worlds مختلفة (D-1 وJ-1 وY-1 وZ-1 و55-1 و73-1...). التفسير : يشير مؤشر sprite نحو منطقة ROM تحوي بيانات قريبة بما يكفي من sprites صالحة لنتائج قابلة للعب، لكن تركيبة tiles قلعة 3-4 مع sprites خارجية تخلق عرضا عبثيا.

معرف المستوى $28 (خارجي) : 38 glitch worlds = رقم قياسي

الرقم المطلق. تشير 38 مدخلا من glitch world نحو نفس المستوى (tiles الخاصة بـ2-1 + sprites رقم $9F51). لماذا ؟ لأن مؤشر sprite رقم $9F51 يقع في منطقة ROM مستخدمة كحشو/بيانات صوتية يُعاد استخدامها من الكثير من المعرفات.

معرف المستوى $49 (تحت أرضي) : مستوى FDS

مؤشر tile رقم $76AE + مؤشر sprite رقم $1C9D. يشير مؤشر tile نحو منطقة ROM محفوظة لنسخة Famicom Disk System. النتيجة : مستوى مع tiles غير موجودة في الخرطوشة القياسية. إنه المستوى الذي يُظهر المستوى 52-1 و196-1.

معرفات المستوى $00-$02 : مستويات المكافأة الحقيقية

تستخدم هذه المعرفات مستويات فرعية شرعية من اللعب :

  • $00 : منطقة تحت مائية في 5-2/6-2 (يستخدمها H-1 و39-1)
  • $01 : ماء 2-2/7-2 (Minus World و36-1)
  • $02 : مستوى فرعي في 8-4 (136-1 و151-1 و215-1)

الفرق بين مستوى "مكافأة" قابل للوصول عادة وglitch world أن مناطق warp تتحقق من العالم الحالي :

; فحص منطقة warp (مبسط)
; يتحقق اللعب أن العالم الهدف بين 1 و8
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; < 1 ← مرفوض
  cmp #9
  bcs InvalidWarp       ; > 8 ← مرفوض
  ; عالم صالح بين 1 و8 فقط
  jmp DoWarp

لا يمكن بلوغ glitch worlds ذات أرقام > 8 أو 0 بأنابيب عادية. يلزم اختراق الجدار أو تبديل الخرطوشة.

لماذا تنهار بعض المستويات : جداول القفز

عندما يحمّل اللعب عنصر tile، يستخدم نوعه كفهرس في جدول قفز :

; جدول قفز عناصر tiles القياسية (أنواع 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; نوع 0 : كتلة ؟ ومخفي وعمود علم...
  .word Obj_Platform      ; نوع 1 : منصة خاصة
  .word Obj_BrickRow      ; نوع 2 : صف طوب
  .word Obj_BlockRow      ; نوع 3 : صف كتل
  .word Obj_CoinRow       ; نوع 4 : صف قطع
  .word Obj_BrickCol      ; نوع 5 : عمود طوب
  .word Obj_BlockCol      ; نوع 6 : عمود كتل
  .word Obj_Pipe          ; نوع 7 : أنبوب
  .word Obj_8             ; نوع 8
  .word Obj_9             ; نوع 9
  .word Obj_10            ; نوع 10
  .word Obj_11            ; نوع 11

Les jump tables : pourquoi un type d'objet invalide fait crasher le jeu

إذا كان لعنصر نوع غير صالح (≥12)، يقفز اللعب لمؤشر غير موجود في هذا الجدول. 4 نتائج ممكنة :

  1. مؤشر صالح ← يُحمَّل العنصر عاديا
  2. مؤشر نحو جدول قفز آخر (تداخل) ← يظهر عنصر مختلف. مثال : النوع 12 يشير نحو جدول Y=13، مما يعطي أنبوب L.
  3. مؤشر نحو قابل للتنفيذ ← تنفيذ كود عشوائي (انهيار محتمل)
  4. عنصر نائب صريح (NOP) ← لا يفعل العنصر شيئا (بعض sprites هكذا، منتجة أعداء تطير في مكانها دون حركة)

Glitch level ID $58 : le sprite pointer pointe vers une adresse invalide, le jeu crashe

Glitch level ID $50 : le cloud tunnel, un niveau généré par des données corrompues

مستوى glitch ذو معرف $58 (النفق المنهار) : يشير مؤشر sprite نحو منطقة ذاكرة غير موجودة على NES دون mapper خاص بـROM. يحاول اللعب تحميل نفس Koopa 5 مرات لكل إطار في الموضع (0,0)، مما يشبع PPU ويسبب تجمدا.

; لماذا ينهار المعرف $58 :
; مؤشر Sprite ← عنوان غير صالح (خارج فضاء NES القياسي)
; ← يقرأ اللعب بايتات غير محددة كأنواع sprite
; ← Koopa رقم $00 (نوع غير صالح دون معالج) يدور في استدعاء تكراري
; ← فيض كومة 6502 ← تجمد

مفارقة warp الأنبوب

تذكّر فحص target_world BETWEEN 1 AND 8. حتى لو وجدت أنبوبا في glitch world، يتحقق اللعب أن عالم الوجهة بين 1 و8. لدى glitch worlds أرقام > 8 (36-1 و255-1...)، فيفشل warp.

ولهذا لا نهاية لـMinus World : flagpole غير حاضر في sprites، والأنابيب لا تؤدي لأي مكان.

حيلة العناصر الخمسة في عمود

هناك حالة حدية تسمح بتجاوز حد 3 عناصر لكل عمود. عندما يعلق الطابور (خانات ممتلئة + عنصر تالٍ دون علم شاشة تالية)، "يُعالِج" اللعب مسبقا العمود الحالي في حلقة حتى إيجاد عنصر مع علم شاشة تالية. وأثناء كل معالجة مسبقة :

; أثناء المعالجة المسبقة لعمود :
; 1. ترى العناصر في الطابور عرضها المتبقي
;    منقوصا مع كل "تقدم زائف" لعمود
; 2. وإذا بلغ عنصر عرضا=0، يغادر الطابور
; 3. يمكن ملء خانة محررة بعنصر جديد
;    مضاف في نفس العمود
;
; النتيجة : يمكن معالجة حتى 5 عناصر على نفس العمود.
; التقنية : وضع عنصرين يعبران حد الشاشة
; (خانتان 1 و2) وعنصر dummy عند X < السابق (يعلق الطابور)،
; ثم 3 عناصر عند X=0 للشاشة التالية (منها واحد مع علم شاشة تالية).

وهذا ما يسمى "تخطي طابور" ويستخدمه بعض صانعي romhack لإنشاء مستويات أكثف مما تسمح به الصيغة عادة.

الفروق بين النسخ

Famicom Disk System

تملك نسخة FDS من SMB1 خريطة ذاكرة مختلفة. كل مؤشرات المستوى مزاحة، لكن البيانات نفسها. ما يتغير : فهارس glitch worlds مختلفة تماما :

عالم FDS رقم 36 ← معرف المستوى $09 (نسخة ماء من 5-3)
  ← flagpole موجود ! يمكن إنهاء المستوى.
  ← ثم : $27 (7-3 عادي) ← $44 (4-4 تحت أرضي)
  ← $44 قابل للإنهاء ← الفأس يعمل ← نهاية اللعب !
;
إذن Minus World في FDS هو "عالم مكافأة" يمكنه أن يقود
لإكمال اللعب، خلافا لنسخة NES.

مستوى FDS المفضل لدي : معرف $5F، نسخة تحت أرضية من النصف الثاني لـ3-3 في نفق منخفض (للأسف إنه تمرير آلي).

The Lost Levels (Super Mario Bros. 2 اليابانية)

تغيّر Lost Levels أشياء كثيرة :

  1. ترتيب tiles/sprites متماثل : لا مستويات Frankenstein بعد (تحمّل tiles وsprites نفس المستوى حتى مع معرف غير صالح)
  2. جدول مؤشرات 16-bit واحد بدلا من جدولين منفصلين عالٍ/منخفض
  3. 4 ملفات قرص : قُسِّمت ROM لـFDS :
    • ملف 1 : عوالم 1-4
    • ملف 2 : عوالم 5-8
    • ملف 3 : عالم 9 + محرك صوت
    • ملف 4 : عوالم A-D (جدول مؤشرات مختلف تماما)
  4. نفس معرف المستوى = 4 مستويات ممكنة حسب الملف المُحمَّل
  5. لا glitch خاص بـTennis بعد : خيار المواصلة (مواصلة في نفس العالم بعد game over) يجعل البدء الدافئ عديم الفائدة، واللعب يُصفَّر فورا إذا كان العالم > 9
  6. عناصر جديدة : فطر سام وكتلة غير مرئية وكتلة غير مرئية بزهرة نار وأنابيب مقلوبة ورياح، لكن مُدرَجة وسط القوائم الموجودة ← عدم توافق خلفي مع SMB1
  7. نباتات Piranha حمراء دائما بعد العالم 4، ومنصات قفز خضراء فقط في عوالم 2/B/3/C/7

Super Mario All-Stars (SNES)

نقل مباشر مع نفس روتينات 6502 (ينفذ SNES كود NES في وضع متوافق) :

  • منطقة warp مُصلَّحة : لا Minus World بعد (الدخول في الأنبوب الأيسر قبل النص يقود للعالم الصحيح)
  • انهيار : تنهار معظم glitch levels (باستثناء معرف $6A و9-1)
  • عناصر قلعة مضافة : عروض أكثر فرادة
  • لكن : 4-2 wrong warp ما زال يعمل (لم يُصحَّح !)

4-2 wrong warp : bug موضع عنصر

في 4-2، هناك عنصرا انتقال أنبوب : الكرمة (منطقة warp) والأنبوب (غرفة نقود). وُضِع عنصر الانتقال الأول (الخاص بالكرمة) قبل ظهور الكرمة على الشاشة بكثير. ووُضِع الثاني (الخاص بالأنبوب) متأخرا أكثر من اللازم في المستوى.

; توقيت الانتقالات في 4-2 :
; عنصر انتقال 1 (كرمة ← منطقة warp) : موضوع 3 شاشات قبل الكرمة
; عنصر انتقال 2 (أنبوب ← غرفة نقود) : موضوع شاشة بعد الأنبوب
;
; عادة يُعطَّل العنصر الأول قبل بلوغ Mario
; الأنبوب. لكن إذا ذهب Mario سريعا (أو استخدم
; اختصار الكتلة B+يمين)، يكون انتقال الكرمة
; ما زال نشطا عندما يلمس الأنبوب !
; ← يحمّل اللعب منطقة warp بدلا من غرفة النقود.
;
; وإذا وُضِع العنصر بعد الكرمة مباشرة وقبل
; الأنبوب، لما وُجد bug.

مستويات الحلقة

كيف تعمل loop (8-4 و7-4) ؟ يملك المستوى نقاط فحص مع أرقام شاشة ومواضع Y مُرمَّزة صلبا :

; نقطة فحص : {رقم_شاشة وموضع_عمودي}
; إذا اجتاز Mario نقطة الفحص هذه بالارتفاع الصحيح ← يستمر المستوى
; وإلا ← warp خلفي 4 شاشات (64 كتلة)
;
; لصنع حلقة لانهائية : موضع_عمودي = $F0
; (تحت أسفل الشاشة) ← مستحيل التحقق.
;
; نقاط الفحص بسيطة (علم واحد) باستثناء العالم 7
; الذي يستخدم ثلاثيات (3 أعلام، يجب إفشال واحد على الأقل)
;
; warp الخلفي قاسٍ : إزاحة بيانات tile مضبوطة على قيمة
; مُرمَّزة صلبا، وإزاحة بيانات sprite تُصفَّر. الأعداء الحاضرون
; يُفرَّغون فورا ← تختفي firebars.

تغيير الصيغة، لا الكود

من أكثر دروس هذه البنية سحرا أن مطوري SMB1 نجحوا في إنشاء نظام مستوى معبّر جدا دون لمس كود العرض 6502 أبدا. كل التباين بين المستويات يأتي من البيانات (مؤشرات وعناصر وsprites وأنماط أرضية)، لا من الكود.

توجد glitch worlds الـ256 لأن جداول المؤشرات مُبعَّدة لـ128 مدخلا × 4 أنواع، واللعب لا يتحقق أبدا من القيم التي يقرأها. عندما يقع مؤشر في RAM، يفسّر اللعب سجلات Mario كـtiles. وعندما يقع مؤشر في البيانات الصوتية، يعزف اللعب موسيقى على شكل تصميم مستوى. وعندما تفيض جداول القفز، ينفذ اللعب أي شيء حتى الانهيار.

More Super Mario Bros. Mechanics Explained -- la 4ème vidéo

ما يمكن تعلمه من كل ذلك

  1. فصل tiles/sprites : استقلال كامل للطبقتين، مع ترتيبي تخزين مختلفين يخلقان مستويات Frankenstein فريدة
  2. ضغط RLE + نظام عناصر : المستويات ليست صور نقطية بل قوائم عناصر موضوعة، مع أنماط أرضية للأرض
  3. طابور 3 خانات : حد صارم للعتاد (ولتصميم المستوى)
  4. ثقة كاملة بالمؤشرات : يثق اللعب بالمؤشرات وجداول القفز، مما ينتج إما glitchs قابلة للعب أو انهيارات
  5. 256 بايت حدا : حد سجل Y في 6502، الذي يجعل البيانات تتكرر إذا ذهبنا بعيدا
  6. بدء دافئ / بدء بارد : نظام "مواصلة" فتح الباب لتبديل الخرطوشة Tennis ← Mario

الأجمل : كل ذلك كود 6502 يتسع في 40KB. يحاور الكود العتاد مباشرة ويمنح ثقة كاملة لكل وصول ذاكرة. وإذا كان المؤشر فاسدا، ينهار اللعب. ونسمي الانهيارات glitch worlds.

الأشياء الثلاثة للتذكر

  1. glitch worlds هي مؤشرات تقع سيئا، يملك اللعب 128 معرفا × 4 أنواع منطقة، لكن 34 مستوى فريدا فقط. وعندما يكون رقم العالم فاسدا (بواسطة Tennis أو اختراق الجدار)، يحمّل اللعب مؤشرا مصمما لمستوى آخر، وتنتج التركيبات الـ512 الممكنة نتائج غير متوقعة.

  2. Minus World هو bug في warp مدمج بفساد، الأنبوب الأيسر في 1-2، إذا فُعِّل قبل ظهور النص، يحمّل العالم 36 (0x24). يشير هذا العالم نحو معرف المستوى $01 (ماء 2-2)، وهو مستوى دون flagpole. وبما أنه لا انتقال أنبوب للعالم 36، يدور المستوى للأبد. يخلق غياب الفحص الأيقونة.

  3. Tennis ← Mario، قبل 15 عاما من OoT ← Paper Mario، تنجو RAM الخاصة بـNES من تبديل خرطوشة بفضل المكثفات ونظام البدء الدافئ / البدء البارد في SMB1. يقع عداد خطوات Tennis (الذي يزيد بايت RAM بعزف صوت الخطوات) بالضبط على عنوان رقم العالم. يجب أن تبقى أرقام أعلى نتيجة عند 0، وأن يبقى بايت $A5 سليما، وأن يكشف اللعب بدءا دافئا، مسابقة ظروف كاملة لم تنجح إلا مع Tennis.

فيديوهات Retro Game Mechanics Explained الأصلية عمل نمل مذهل، مستوى التفصيل عن تفكيك 6502 وخرائط آلية لكل المستويات وشروح تبديل الخرطوشة والبدء الدافئ. إذا لم تشاهد السلسلة، شاهدها، إنها قصيرة وكل دقيقة كثيفة.

الكود المصدري للخرائط متاح على rgmechex.com، والتفكيك الكامل لـSMB1 مفتوح المصدر على الكثير من المستودعات. قبل 40 عاما، كتب مبرمجون يابانيون نظام المستوى هذا في 6502 دون اختبار وحدة ودون متتبع bugs، وما زلنا نتعلم تقنيات تحسين بفتح كودهم اليوم.

Super Mario Bros. : định dạng màn, con trỏ và 256 glitch world

Cách 128 màn × 4 kiểu vùng vừa trong 40KB ROM, vì sao Minus World tồn tại, và cách trận Tennis NES tải được glitch world.

Giới thiệu

Super Mario Bros. là 40 kilobyte ROM. Tám thế giới, 32 màn, kẻ thù, nhạc, power-up, tất cả vừa trong đó.

Nhưng nếu bạn mở trình giả lập và chọc đúng byte, bạn có thể tải màn 36-1. Hay 255-1. Hay đáp xuống thế giới mọi thứ làm bằng sprite Bowser và ống dẫn tới hư vô.

Các glitch world này tồn tại vì lý do đơn giản : hệ thống lưu màn của SMB1 là kỳ quan tối ưu 8-bit, và khi ta ép game đọc nơi không nên, nó cho kết quả cuốn hút.

Retro Game Mechanics Explained đã làm series 4 video về chuyện này -- ta sẽ gom chúng thành một lần lặn vào mã 6502 của game bán chạy nhất thời của nó.

GLITCH OBJECTS -- tiêu đề series RGMechEx về cơ chế ẩn của SMB1

World 9-1 -- màn hình tiêu đề của glitch world đầu tiên truy cập được qua cart swap Tennis

Warm start : vì sao RAM của Tennis sống sót trong SMB1

Trước khi nói lưu màn, phải hiểu SMB1 khởi động thế nào. Vì glitch cart swap NES Tennis hoàn toàn dựa trên hệ thống phát hiện warm start / cold start của game.

41 byte được giữ

Khi SMB1 phát hiện cold start (bật nguồn lần đầu hay tắt/bật nguồn), nó xóa sạch RAM. Nhưng khi nó phát hiện warm start (nút reset, không cắt nguồn), nó giữ vùng bộ nhớ 41 byte :

; 41 byte được giữ trong RAM lúc warm start
; Địa chỉ $075F-$0787
;
; $075F : byte khởi động (world - 1)    [1 byte]
; $0760 : cờ chọn thế giới (nút B) [1 byte]
; $0761-$0762 : không dùng                    [2 byte]
; $0763-$0768 : timer (6 chữ số, 3 hiện) [6 byte]
; $0769-$076E : xu Luigi                   [6 byte]
; $076F-$0774 : xu Mario                   [6 byte]
; $0775-$077A : điểm Luigi                   [6 byte]
; $077B-$0780 : điểm Mario                   [6 byte]
; $0781-$0786 : điểm cao nhất (6 chữ số, 1 ẩn) [6 byte]
; $0787 : byte kỳ diệu $A5                 [1 byte]

41 byte này phục vụ một tính năng duy nhất : cho phép người chơi tiếp tục ở cùng thế giới sau game over. Nếu bạn chết ở 6-3, game ghi thế giới 6 vào byte khởi động, và ở màn hình tiêu đề, nếu bạn giữ A + Start, bạn bắt đầu lại ở 6-1.

41 byte được giữ trong RAM lúc warm start -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, và byte kỳ diệu $A5

Kiểm tra kép warm start

Cold start đấu warm start -- sơ đồ phát hiện reset

Khi SMB1 boot, nó không kiểm tra một tiêu chí mà hai :

CheckWarmStart:
  ; 1. Kiểm tra byte kỳ diệu $A5 ở $0787
  lda $0787
  cmp #$A5
  bne ColdStart        ; không phải $A5 → cold start

  ; 2. Kiểm tra 6 chữ số của điểm cao nhất ($0781-$0786)
  ;    Mỗi chữ số phải từ 0 tới 9
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; chữ số >= 10 → cold start
  inx
  cpx #6
  bne CheckLoop

  ; Nếu cả hai điều kiện qua → warm start
  ; RAM không bị xóa, thế giới khởi đầu được giữ
  jmp WarmStartBoot

Kiểm tra byte $A5 và các chữ số điểm cao nhất -- lõi của warm start

Vì sao kiểm tra kép? Vì byte $A5 có thể tình cờ hiện diện (game khác để lại giá trị này, hay trạng thái nghỉ mặc định của chip RAM). Bằng cách kiểm tra các chữ số điểm cao nhất hợp lệ (0-9), ta đảm bảo dữ liệu nhất quán.

Vì sao chỉ Tennis mới chạy

Khi ta lắp SMB1 lần đầu (cold start), game :

  1. Xóa sạch RAM → điểm cao nhất = 0, byte world = 0
  2. Ghi $A5 vào địa chỉ $0787

Rồi, ta swap sang Tennis mà không tắt máy. Tennis :

  • Không dọn RAM lúc khởi động (ít game NES nào làm)
  • Không ghi lên các byte điểm cao nhất → chúng vẫn 0 (hợp lệ)
  • Không đụng byte $A5 → nó vẫn ở đó
  • Dùng địa chỉ $075F cho bộ đếm bước chân của người chơi
; Tăng footstep trong Tennis :
; Mỗi bước của người chơi trên sân, Tennis tăng byte ở $075F.
; Byte này cũng được SMB1 dùng làm "world number - 1".
;
; 0 bước  → world 0 → SMB1 = world 1
; 1-7 bước → world 1-7 → world thường
; 8+ bước → world 8+ → glitch worlds !
;
; Bộ đếm chỉ tăng khi nhạc dừng
; (tiếng bước chân không phát khi nhạc chạy).

Khi ta lắp lại SMB1 :

  1. Byte $A5 vẫn đó (Tennis không đụng tới)
  2. Các chữ số điểm cao nhất vẫn 0 (hợp lệ)
  3. Byte world giờ thành 8+ (tăng bởi bước chân Tennis)
  4. SMB1 phát hiện warm start → giữ byte world hỏng
  5. Giữ A + Start → world 9-1, world A-1, world 36-1, v.v.

Vì sao phải boot Mario trước Tennis

Một tinh tế : phải boot SMB1 trước, rồi Tennis, rồi lại SMB1. Nếu bạn khởi đầu trực tiếp bằng Tennis, byte $A5 sẽ không bao giờ ghi (Tennis không ghi $A5), nên phát hiện warm start thất bại và RAM bị xóa.

Bộ đếm bước chân của Tennis : mỗi footstep tăng byte world

Truy cập Glitch World qua NES Tennis -- video giải thích cart swap

Cách SMB1 lưu màn trong 40KB

Nintendo R&D4 phải giải bài toán trông đơn giản : biểu diễn màn cuộn ngang với tile, kẻ thù, vật phẩm, tất cả trong ngân sách ROM siêu chật.

Giải pháp là tách thành hai lớp dữ liệu hoàn toàn độc lập :

Bố cục tile (bản đồ màn)

Mỗi màn định nghĩa bằng con trỏ tới cấu trúc tile nén trong ROM. Nén thô sơ mà thiên tài : byte "điều khiển" theo sau 1-3 byte dữ liệu.

Định dạng tile dùng hệ run (kiểu RLE) :

; Định dạng tile SMB1 (rút gọn)
; Mỗi "lệnh" là byte điều khiển :
;
; $00-$7F : đặt một tile, tiến 1 cột
; $80-$BF : đặt tile lặp N lần (N = byte - $80 + 1)
; $C0-$FF : lệnh đặc biệt (kết dòng, nhảy, đổi palette)

Ví dụ : để vẽ 3 viên gạch liên tiếp :
  $82 $01    ; lặp tile $01 (gạch) 3 lần

Mỗi màn chứa 13 hàng 16 cột tile (13×16 = 208 tile thấy được). Nhưng định dạng nén cho phép xuống thấp hơn nhiều -- ví dụ, trời và cột trống gần như không tốn chỗ.

Vòng lặp render bằng 6502 :

; Giải nén tile - vòng lặp chính
; Đầu vào : con trỏ tile_data ở $XX
; Đầu ra : bản đồ màn trong RAM PPU

DecompressTile:
  lda (tile_ptr),y      ; đọc byte điều khiển
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F : tile đơn
  cmp #$C0
  bcc RunLength         ; $80-$BF : run-length
  jmp SpecialCommand    ; $C0-$FF : lệnh đặc biệt

SingleTile:
  sta PPU_DATA          ; ghi tile trực tiếp
  jmp Next

RunLength:
  sec
  sbc #$7E              ; N = control - $7E
  tax
  lda (tile_ptr),y      ; đọc tile cần lặp
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

Bố cục sprite (kẻ thù và vật thể)

Song song, kẻ thù và vật thể (khối ?, ống, goomba, koopa) lưu trong cấu trúc hoàn toàn riêng. Mỗi spawn định nghĩa bằng 2 byte :

; Định dạng sprite SMB1
; Byte 0 : vị trí X (theo cột)
; Byte 1 : kiểu sprite + bit trang Y
; Y suy từ index trong chuỗi

Một chuỗi sprite :
  $01 $4B    ; goomba ở cột 1
  $09 $4B    ; goomba ở cột 9
  $10 $61    ; khối ? ở cột 16 (chứa xu)
  $15 $54    ; koopa xanh ở cột 21
  $FF        ; kết chuỗi

Mỗi màn có thể tham chiếu tới 5 trang sprite khác nhau (đúng hơn, 5 "màn hình" 16 cột), nhưng thực tế hầu hết màn chỉ dùng 2-3.

Bảng con trỏ

Thiên tài thiết kế là bảng con trỏ. Mỗi màn lưu dưới dạng cặp địa chỉ ROM :

// Cấu trúc nội bộ (rút gọn) của World Map
struct LevelPointer {
    uint16_t tile_ptr;   // Địa chỉ ROM của dữ liệu tile
    uint16_t sprite_ptr; // Địa chỉ ROM của dữ liệu sprite
};

// 4 bảng riêng, một cho mỗi AreaType :
// 0 = Water, 1 = Overworld, 2 = Underground, 3 = Castle
LevelPointer level_table[4][128];

128 mục cho mỗi bảng. 4 kiểu vùng. 512 tổ hợp khả dĩ, nhưng chỉ một phần được game chính thức dùng. Phần còn lại là RAM chưa khởi tạo hay dữ liệu bị diễn giải thành con trỏ.

Khi game tải màn, nó làm vầy :

; Tải một màn
; A = AreaType (0-3), X = LevelID (0-127)

LoadLevel:
  sta AREA_TYPE
  asl                  ; *2 cho offset trong bảng 16-bit
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; con trỏ tới tile
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; con trỏ tới sprite
  jsr DecompressTiles

Game đặt niềm tin tuyệt đối vào con trỏ : nó đọc địa chỉ trong bảng và giải nén thứ ở địa chỉ đó, hết.

Level ID $06 (Water) -- 9-1, bản dưới nước của 6-2

Bảng Level ID : 128 mục khả dĩ, 34 gán

Thứ tự khác nhau của con trỏ tile và sprite -- nguyên nhân màn Frankenstein

34 màn duy nhất và hệ ID 7-bit

Chip RAM của NES (MB8416A) -- chính nó giữ dữ liệu khi ta swap băng

SMB1 không có 32 màn, mà 34 màn duy nhất. Nhiều màn là trùng (5-3 = 1-3 nhưng có Bullet Bill) đánh dấu bằng cờ "hard mode". Các màn thật :

  • Nước (Kiểu 0) : 3 màn (2-2, 7-2, vùng thưởng 5-2/6-2)
  • Overworld (Kiểu 1) : 22 màn (kể cả 2 phòng mây thưởng)
  • Underground (Kiểu 2) : 3 màn (kể cả phòng thưởng ngầm)
  • Castle (Kiểu 3) : 6 màn
  • + 1 phòng cắt cảnh (trước màn ngầm/nước)
  • + 1 warp zone của 4-2

Mỗi màn có ID trên 7 bit. 5 bit thấp = số trong nhóm con, 2 bit cao = kiểu vùng :

; Mã hóa Level ID 7-bit
; Bit 6-5 : Kiểu (00=Water, 01=Overworld, 10=Underground, 11=Castle)
; Bit 4-0 : Số trong nhóm con
;
; Water IDs      : $00-$02  (kiểu 00, số 0-2)
; Overworld IDs  : $20-$35  (kiểu 01, số 0-21)
; Underground IDs: $40-$42  (kiểu 10, số 0-2)
; Castle IDs     : $60-$65  (kiểu 11, số 0-5)
;
; ID $25 = %0100101 → kiểu 01 (Overworld), số 5 → 1-1
; ID $23 = %0100011 → kiểu 01 (Overworld), số 3 → 6-2

128 ID khả dĩ ($00-$7F), chỉ 34 gán cho màn thật. Các ID không dùng trỏ tới bất cứ gì.

Bảng con trỏ : hai danh sách, hai thứ tự

Các con trỏ tile và sprite không lưu cùng thứ tự. Mã dùng hai danh sách 16-bit riêng (byte cao / byte thấp trong hai bảng khác) :

Thứ tự con trỏ sprite :
  Index 0-5   : Castle (6 màn)
  Index 6-27  : Overworld (22 màn)
  Index 28-30 : Underground (3 màn)
  Index 31-33 : Water (3 màn)

Thứ tự con trỏ tile :
  Index 0-2   : Water (3 màn)
  Index 3-24  : Overworld (22 màn)
  Index 25-27 : Underground (3 màn)
  Index 28-33 : Castle (6 màn)

Vì sao thứ tự khác nhau? Không lý do kỹ thuật nào -- có lẽ dữ liệu tổ chức vậy lúc phát triển. Nhưng việc này tạo hậu quả cuốn hút : khi ID màn không hợp lệ, con trỏ tile và sprite tải các màn khác nhau, tạo màn Frankenstein.

Để đi giữa hai danh sách này, game dùng các bảng offset nhỏ (như mục lục) :

; Bảng offset theo kiểu (Water, Overworld, Underground, Castle)
; Mỗi mục = index bắt đầu trong danh sách tương ứng

SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; Water=31, Overworld=6, Underground=28, Castle=0

TileOffsetTable:
  .byte $00, $03, $19, $1C    ; Water=0, Overworld=3, Underground=25, Castle=28

Để tải màn 6-2 (ID $23, Overworld số 3) :

; 1. Kiểu = 01 (Overworld) → index trong bảng offset = 1
; 2. Sprite offset = SpriteOffsetTable[1] = 6
;    Index cuối = 6 + 3 (số màn) = 9 → con trỏ sprite thứ 10
; 3. Tile offset = TileOffsetTable[1] = 3
;    Index cuối = 3 + 3 = 6 → con trỏ tile thứ 7
; 4. Kết quả : con trỏ tile $A619 + con trỏ sprite $9ED0 = 6-2 ✓

Giờ, chuyện gì với ID không hợp lệ như $43 (Underground số 3, không tồn tại)?

; ID $43, Kiểu = 10 (Underground), số = 3
; Sprite offset = SpriteOffsetTable[2] = $1C = 28
;   Index = 28 + 3 = 31 → con trỏ sprite thứ 32 = nước thưởng 5-2 !
; Tile offset = TileOffsetTable[2] = $19 = 25
;   Index = 25 + 3 = 28 → con trỏ tile thứ 29 = 1-4 (Castle) !
;
; Kết quả : màn ngầm với tile của 1-4
; và Blooper của vùng nước 5-2. Frankenstein thật.

Level ID $43 -- màn Frankenstein : tile của 1-4 + sprite nước 5-2

Khám phá Con trỏ Glitch Level -- bảng offset giải thích

Bảng index thế giới -- khi tràn world 9 tạo glitch level

Bảng index thế giới : vì sao world 9 tràn

Có bảng ROM 8 byte cho index của màn đầu mỗi thế giới (1-8). Và ngay sau, bảng 36 Level ID của mọi màn theo thứ tự game.

; WorldIndexTable (8 byte)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   -> Thế giới 1 bắt đầu ở màn 0
;   -> Thế giới 2 bắt đầu ở màn 5
;   -> Thế giới 8 bắt đầu ở màn 33

; LevelIDTable (36 byte)
  .byte $25, $28, $29, $26, $24, ... ; 36 Level ID

Khi ta cố tải world 9, game đọc byte thứ 9 của WorldIndexTable... không tồn tại. Nó tràn 1 byte vào LevelIDTable, đọc giá trị $25, rồi dùng $25 làm index trong LevelIDTable (mục thứ 37) -- tràn tiếp 2 byte vào SpriteOffsetTable, và đọc giá trị 6.

; World 9 :
;   1. WorldIndexTable[8] (tràn) → đọc $25 trong LevelIDTable
;   2. LevelIDTable[37] (tràn) → đọc byte thứ 2 của SpriteOffsetTable = 6
;   3. ID = 6 → Water level number 6 (không tồn tại)
;   4. Tile pointer = con trỏ water số 6 = tile của 6-2
;   5. Sprite pointer = index 31+6 = 37 > 33 → con trỏ không hợp lệ
;   6. Kết quả : 6-2 dưới nước với sprite glitch
;      → world 9-1 !

Với world G (16), tràn còn xa hơn và rơi vào Level ID $01, tức màn cắt cảnh đứng trước 1-2 :

; World G (16) :
;   WorldIndexTable[15] → đọc $01 trong LevelIDTable
;   LevelIDTable[1] = $29 (cắt cảnh 1-2)
;   → world G-1 = cắt cảnh vào 1-2

Vì sao glitch world tồn tại

Game có 32 màn "hợp lệ" (8 thế giới × 4 màn). Nhưng bảng con trỏ làm 128 mục cho mỗi kiểu vùng. Các mục vượt màn 32 chứa thứ ở trong ROM tại các địa chỉ đó -- đôi khi màn khác, đôi khi dữ liệu âm thanh, đôi khi RAM, đôi khi bất cứ gì.

Level ID $01 Water (Minus World) -- tile pointer $AE45, sprite pointer $A171

Level ID $01 + AreaType 0 = Minus World

Nổi tiếng nhất trong các glitch world. Level ID $01 ở AreaType 0 (nước) trỏ tới :

  • Tile pointer : $AE45 → vùng dưới nước của 2-2/7-2
  • Sprite pointer : $A171 → sprite của 2-2/7-2

Kết quả : màn nước trông như 2-2, nhưng lặp vô hạn vì cột cờ không tồn tại. Màn quay vòng mãi mãi.

Đó là màn 36-1 (hay 36-1 trong thế giới $-1).

Kiểm tra warm start của SMB1 -- chính nó cho phép Minus World tồn tại

; Vì sao cột cờ thiếu trong Minus World :
; Sprite của 2-2/7-2 ($A171) không có cột cờ
; trong chuỗi. Game tìm sprite $FD (cột cờ)
; nhưng không bao giờ thấy → vòng lặp vô hạn
;
; Game tiếp tục sinh màn vô hạn
; tới khi timer về không.

Các con trỏ trỏ vào RAM

Khi tile pointer hay sprite pointer trỏ vào địa chỉ RAM ($00-$7F) thay vì ROM, game cố diễn giải các thay đổi liên tục của RAM thành tile :

; Ví dụ : Level ID $03 ở Water
; Tile Pointer : $A46B (3-3 - hợp lệ)
; Sprite Pointer : $009D (trỏ vào RAM trang 0 !)
;
; RAM trang 0 chứa thanh ghi của game,
; vị trí của Mario, trạng thái bộ đếm...
; Game giải nén nó thành chuỗi sprite,
; và kết quả là màn với kẻ thù
; thực chất là giá trị thanh ghi.

Khi trang 0 đổi (vì Mario động, timer chạy, v.v.), "sprite" của màn cũng đổi. Đó là lý do vài glitch world có kẻ thù nhấp nháy và biến hình liên tục.

Level ID $03 Water -- sprite pointer $009D trỏ vào RAM, màn không chơi được

Level ID $36 : màn trống (Overworld)

Level ID $36 ở Overworld :

  • Tile pointer : $AC35 (1-2)
  • Sprite pointer : $A0D8 (1-2)

Kết quả : không gì. Game tải màn nhưng nó bị đánh dấu "không màn" trong danh mục của RGMechEx. Các tile có thể hợp lệ nhưng sprite trỏ tới nơi cho ra màn trống hay không chạy được.

Level ID $1D (Castle) : quán quân crash

Level ID $1D ở Castle :

  • Tile pointer : $A210 (4-4)
  • Sprite pointer : $7EA0 (RAM !)

Sprite pointer trong RAM = sprite không xác định. Game cố hiện Spiny ball hay Bullet Bill blaster trong dòng tile đầu. Crash ngay.

; Khi sprite pointer trỏ vào RAM,
; game giải nén các byte đổi liên tục
; thành lệnh "spawn". Kết quả :
; - Xuất hiện vật thể không tồn tại (giá trị không xác định)
; - Crash PPU khi sprite NES cố hiện tile không hợp lệ
; - Treo toàn bộ console

256 glitch world lập danh mục

RGMechEx đã viết script sinh map của mọi màn, cho cả 4 kiểu vùng, và 128 ID mỗi cái.

Bộ đếm thế giới trên 8 bit (0-255). Các thế giới 1-8 hợp lệ. Còn lại 248 glitch world tiềm năng. Mỗi glitch world khớp màn đầu của thế giới đó, và Level ID của nó tính bằng cơ chế tràn của WorldIndexTable.

Bảng glitch world -- 248 thế giới hỏng, 68 màn đầu truy cập được

Trên 128 ID khả dĩ, chỉ 68 là "màn đầu" của một thế giới (truy cập được qua số glitch world). 60 còn lại là màn 2+ hay không truy cập được.

Kiểu ID duy nhất chơi được ID gây crash ID trống
Water (0) ~20 ~60 ~48
Overworld (1) ~30 ~55 ~43
Underground (2) ~15 ~65 ~48
Castle (3) ~25 ~58 ~45

Nhiều ID dẫn tới cùng màn vì các con trỏ rơi lên cùng địa chỉ ROM. Level ID $28 (Overworld) ví dụ -- tile pointer $A7CD (2-1) -- xuất hiện trong 38 glitch world khác nhau, vì sprite pointer $9F51 của nó trỏ vào vùng ROM dùng làm đệm/dữ liệu âm thanh tái dùng bởi nhiều ID.

Bản đồ màn ID $28 (Overworld) -- tile 2-1 với sprite thường, 38 glitch world

Super Mario Bros. Glitch Levels giải thích -- video thứ 3

6 glitch level thực sự độc đáo

Trong 19 ID glitch level truy cập được, chỉ 6 không crash ngay lúc tải :

World Level ID Mô tả
E-1 (224) $50 Một khối ? duy nhất trên vực. Mario chết ngay.
W $57 Mario spawn kẹt, không động được.
42 (133) $50 Đường hầm mây bẫy Mario nếu nó đi đủ xa.
62 (131, 240) $4D Lâu đài đông cứng : Mario spawn trên cao, không rơi được → kẹt.
127 $4B Đường hầm ngầm, nhưng crash nếu đi quá xa.
137 $4B Kích hoạt tự cuộn của cắt cảnh. Mario gặp đúng một khối gạch chặn nó mãi mãi.

Level ID $50 (đường hầm mây) -- glitch world 42-1 và E-1 Level ID $4D (lâu đài) -- world 62-1, Mario kẹt lúc spawn Level ID $4B (đường hầm) -- world 127-1, crash nếu đi quá xa

Sáu glitch world trên 248 cho ra thứ thực sự mới. Phần còn lại là màn thường với sai kiểu vùng, hay màn hình đen.

Định dạng màn chi tiết

Tới định dạng chính xác của dữ liệu màn, để hiểu vì sao glitch level đứng được (hay không).

Header màn : 2 byte, 6 thuộc tính

Mỗi màn bắt đầu bằng header 2 byte điều khiển 6 thuộc tính :

; Byte 0 : timer + Y start + modifier
;   Bit 7-6 : timer (00=không đổi, 01=200, 10=300, 11=400)
;   Bit 5-3 : Y start Mario (111/110 = tự đi)
;   Bit 2-0 : bộ sửa kiểu màn
;              000=mặc định, 001=sóng, 010=tường gạch,
;              011=đáy nước, 100=đêm, 101=tuyết,
;              110=đêm tuyết, 111=đêm xám

; Byte 1 : platform + background + floor pattern
;   Bit 7-6 : platform đặc biệt (00=cây, 01=nấm,
;                                 10=Bullet Bill, 11=mây)
;   Bit 5-4 : background (00=không, 01=mây,
;                           10=núi, 11=hàng rào)
;   Bit 3-0 : floor pattern ban đầu (0-15)

Bộ sửa kiểu điều khiển biến thể hình ảnh : sóng trên đỉnh màn nước, nền gạch của 8-3, palette đêm của 4-3, tuyết của 6-2, v.v.

Vật thể tile : 2 byte, Next Screen Flag, hàng đợi 3 slot

Sau header là danh sách vật thể tile, mỗi vật thể 2 byte. Byte $FD đánh dấu kết danh sách.

; Định dạng vật thể tile (16 bit) :
; Byte 0 :
;   Bit 7-4 : vị trí X (cột 0-15)
;   Bit 3-0 : vị trí Y
;     Y=0-11  : vị trí Y thường
;     Y=12    : vật thể đặc biệt (hố, cầu, dây, khối ?)
;     Y=13    : bỏ qua màn hình / vật thể đặc biệt 2
;     Y=14    : đổi modifier/scenery/floor
;     Y=15    : vật thể đặc biệt 3 (lâu đài, cầu thang, ống lớn)

; Byte 1 :
;   Bit  7   : NEXT SCREEN FLAG
;   Bit 6-4 : kiểu vật thể (0-7)
;   Bit 3-0 : chiều rộng/chiều cao / kiểu con

Khi bit "next screen" đặt, cột làm việc hiện tại tăng 1. Việc này cho phép đặt vật thể vượt 16 cột đầu. Các vật thể phải liệt kê theo thứ tự (trái sang phải) vì game tải chúng tuần tự :

; Quy trình tải có HAI pha cho mỗi cột :
; Pha 1 : tìm vật thể mới bắt đầu trên cột này
;            và thêm chúng vào hàng đợi (queue)
; Pha 2 : xử lý mỗi vật thể trong hàng đợi bằng cách vẽ tile,
;            và gỡ thứ kết thúc trên cột này

Hàng đợi đúng 3 slot. Hệ quả trực tiếp : không thể có hơn 3 vật thể bắt đầu trên cùng cột. Nếu hàng đợi đầy, vật thể thứ 4 bị lờ và không bao giờ tải.

Đó là lý do các màn thiết kế tốt tránh chồng quá nhiều vật thể. Ví dụ trong 1-2 : cột có khối 1up trong trần + gạch bên cạnh được tách thành hai vật thể riêng để tôn trọng giới hạn 3.

Vị trí Y đặc biệt : 12, 13, 14, 15

Khi Y=12, vật thể không có vị trí Y (nó ghi cứng theo kiểu) :

; Y=12 : vật thể không vị trí Y
;   Kiểu 0 : hố (xóa mặt đất)
;   Kiểu 1 : dây của platform di động
;   Kiểu 2-4 : cầu ở Y cố định
;   Kiểu 5 : hố với nước/dung nham
;   Kiểu 6-7 : hàng khối ?

Khi Y=13, hai nhóm con. Nếu bit 6 của byte 1 ở 1 :

; Y=13, bit6=1 : vật thể đặc biệt
;   0 = ống L (cắt cảnh), 1 = cột cờ, 2-3 = cầu/rìu/búa (kết lâu đài)
;   4 = dừng màn hình, 5 = kẻ thù ngẫu nhiên, 6 = vòng lặp màn, 7+ = có thể crash

Nếu bit6=0, 5 bit thấp mã hóa bỏ qua màn hình (nhảy thẳng tới màn N, không cần next screen flag từng cái một).

Khi Y=14 : cùng nguyên tắc với bit6=1 để đổi bộ sửa kiểu, bit6=0 để đổi nền + floor pattern.

Floor pattern : 16 mẫu nền

Mặt đất của màn không làm bằng vật thể riêng lẻ. SMB1 dùng floor pattern, mẫu nền áp cho mọi cột tới lần đổi tiếp :

; Floor pattern (4 bit = 16 khả năng)
;   0 = trống hoàn toàn
;   1 = mặt đất dày 2 tile
;   2 = mặt đất dày 1 tile
;   3 = mặt đất + đáy
;   4 = mặt đất + đáy 2
;   5 = mặt đất 1/2 tile
;   6 = 3/4 mặt đất
;   ... tới 15 = lấp đầy (mặt đất + trần)

Đó là lý do hố là vật thể : chúng ghi đè floor pattern trên cột cụ thể, mà không phải đổi pattern cho phần còn lại.

Giới hạn 256 byte và lặp lại

Mọi dữ liệu tile của một màn vừa trong tối đa 256 byte. Thanh ghi Y của 6502 dùng làm index, và nó 8 bit. Nếu game tới cuối dữ liệu mà không gặp byte $FD, nó quay lại đầu và lặp 256 byte vô hạn :

; Index Y = 8 bit → tối đa 256 byte dữ liệu tile
; Nếu Y tràn (255 → 0) mà không gặp $FD → lặp lại
; Tương tự với sprite, nhưng vật thể ống (3 byte)
; lệch tính chẵn lẻ của index mỗi lần tải.

Vài glitch level khai thác lặp này để sinh màn kéo dài "vô hạn".

Hệ sprite : 2 byte + chuyển ống

Sprite theo định dạng tương tự, nhưng không header và vài khác biệt chính. Byte $FF kết danh sách.

; Định dạng sprite (2 byte) :
; Byte 0 : vị trí X (cột)
; Byte 1 :
;   Bit 7 : NEXT SCREEN FLAG
;   Bit 6-0 : kiểu sprite
;       Vài kiểu gồm : goomba, koopa, Blooper,
;       Bullet Bill, Lakitu, Spiny, platform,
;       lệnh warp zone, toad/công chúa,
;       lệnh spawn nhóm kẻ thù

Bit thấp của byte 1 là cờ hard level : nếu ở 1, sprite chỉ xuất hiện trong màn ≥ 5-3. Đó là cách tạo màn "hard mode".

Vị trí Y 15 = bỏ qua màn hình (giống tile). Vị trí Y 14 = chuyển ống (3 byte) :

; Sprite Y=14 : chuyển ống/dây (3 byte !)
;   Byte 0 : vị trí X
;   Byte 1 : bit 6-0 = Level ID 7-bit (đích)
;   Byte 2 : bit 4-0 = màn đích
;            bit 7-5 = world nơi chuyển này hợp lệ
;
; Vì sao world? Phòng thưởng tái dùng giữa các thế giới.
; Ví dụ : phòng thưởng của 1-1 cũng dùng cho 2-1 và 7-1.
; Phòng này có 3 chuyển, một cho mỗi thế giới, để Mario
; xuất hiện lại đúng chỗ.

Sprite không có hệ queue. Giới hạn duy nhất là không thể có hơn 4 sprite tải đồng thời trong vùng spawn (ngay ngoài màn hình bên phải). Vượt quá, sprite bị lờ.

Cách truy cập glitch world

Có hai phương pháp chính.

Phương pháp cổ điển : wall clip

Wall clip (đi xuyên tường) cho phép ra khỏi màn thường và đi bộ tới warzone ẩn. Bằng cách thao túng bộ đếm thế giới qua RAM, ta có thể tải bất kỳ Level ID nào.

Kỹ thuật :

  1. World 1-2 : đi vào ống kết ẩn
  2. Làm wall clip trên tường phải
  3. Đi trong khoảng không tới vùng warp
  4. Game diễn giải các giá trị thành thế giới

Nhưng phương pháp này chỉ cho một phần nhỏ glitch world.

Phương pháp cực đoan : cart swap NES Tennis

Xem mục "Warm start" ở trên để biết chi tiết đầy đủ. Tóm lại : bộ đếm bước chân của Tennis ghi lên cùng byte RAM với thế giới khởi đầu của SMB1, và phát hiện warm start giữ giá trị này.

Góc vọc thủ : mã để khám phá hết

Nếu bạn muốn khám phá mọi glitch tự mình trong trình giả lập, bạn có thể vá Level ID trực tiếp :

; Vá cho FCEUX / Mesen :
; Địa chỉ RAM $075F = Level ID hiện tại
; Địa chỉ RAM $0760 = Area Type (0=Water, 1=Overworld, 2=Underground, 3=Castle)

; Ví dụ : tải Level 57 (0x39) ở Overworld
; Trong trình giả lập, mở trình theo dõi bộ nhớ và ghi :
; $075F = 0x39
; $0760 = 0x01
; Rồi vào ống warp hay chết và bắt đầu lại
; → Game tải màn ID $39 ở Overworld

RGMechEx đã xuất bản danh sách đầy đủ 128 màn × 4 kiểu với map sinh tự động trên rgmechex.com. Mỗi mục hiện tile pointer, sprite pointer, và bản đồ trực quan của màn.

Các màn dị nhất

Level ID $1F (Water) : 15 glitch world trong một

Tile pointer $A302 (3-4) kết hợp sprite pointer $02A0 cho ra 15 glitch world khác nhau (D-1, J-1, Y-1, Z-1, 55-1, 73-1...). Giải thích : sprite pointer trỏ vào vùng ROM chứa dữ liệu đủ gần sprite hợp lệ để cho kết quả chơi được, nhưng kết hợp tile lâu đài 3-4 với sprite overworld tạo render phi lý.

Level ID $28 (Overworld) : 38 glitch world = kỷ lục

Kỷ lục tuyệt đối. 38 mục glitch world trỏ về cùng màn (tile 2-1 + sprite $9F51). Vì sao? Vì sprite pointer $9F51 rơi vào vùng ROM dùng làm đệm/dữ liệu âm thanh tái dùng bởi nhiều ID.

Level ID $49 (Underground) : màn FDS

Tile pointer $76AE + sprite pointer $1C9D. Tile pointer trỏ vào vùng ROM dành cho bản Famicom Disk System. Kết quả : màn với tile không tồn tại trong băng chuẩn. Đó là màn tạo ra màn 52-1 và 196-1.

Level ID $00-$02 : các màn thưởng thật

Các ID này được các màn con hợp lệ của game dùng :

  • $00 : vùng dưới nước của 5-2/6-2 (dùng bởi H-1, 39-1)
  • $01 : nước của 2-2/7-2 (Minus World, 36-1)
  • $02 : màn con của 8-4 (136-1, 151-1, 215-1)

Khác biệt giữa màn "thưởng" truy cập bình thường và glitch world là warp zone kiểm tra thế giới hiện tại :

; Kiểm tra warp zone (rút gọn)
; Game kiểm tra thế giới đích từ 1 tới 8
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; < 1 → từ chối
  cmp #9
  bcs InvalidWarp       ; > 8 → từ chối
  ; world hợp lệ chỉ từ 1 tới 8
  jmp DoWarp

Các glitch world với số > 8 hay 0 không thể tới bằng ống thường. Phải wall clip hay cart swap.

Vì sao vài màn crash : jump table

Khi game tải vật thể tile, nó dùng kiểu của nó làm index trong jump table :

; Jump table của vật thể tile chuẩn (kiểu 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; kiểu 0 : khối ?, ẩn, cột cờ...
  .word Obj_Platform      ; kiểu 1 : platform đặc biệt
  .word Obj_BrickRow      ; kiểu 2 : hàng gạch
  .word Obj_BlockRow      ; kiểu 3 : hàng block
  .word Obj_CoinRow       ; kiểu 4 : hàng xu
  .word Obj_BrickCol      ; kiểu 5 : cột gạch
  .word Obj_BlockCol      ; kiểu 6 : cột block
  .word Obj_Pipe          ; kiểu 7 : ống
  .word Obj_8             ; kiểu 8
  .word Obj_9             ; kiểu 9
  .word Obj_10            ; kiểu 10
  .word Obj_11            ; kiểu 11

Jump table : vì sao kiểu vật thể không hợp lệ làm game crash

Nếu vật thể có kiểu không hợp lệ (≥12), game nhảy tới con trỏ không tồn tại trong bảng này. 4 kết quả khả dĩ :

  1. Con trỏ hợp lệ → vật thể tải bình thường
  2. Con trỏ tới jump table khác (chồng lấn) → vật thể khác xuất hiện. Ví dụ : kiểu 12 trỏ tới bảng Y=13, cho ra ống L.
  3. Con trỏ tới mã thực thi → thực thi mã ngẫu nhiên (có thể crash)
  4. Chỗ giữ rõ (NOP) → vật thể không làm gì (vài sprite như vậy, tạo kẻ thù bay tại chỗ không động)

Glitch level ID $58 : sprite pointer trỏ tới địa chỉ không hợp lệ, game crash

Glitch level ID $50 : đường hầm mây, màn sinh bởi dữ liệu hỏng

Glitch level ID $58 (đường hầm crash) : sprite pointer của nó trỏ tới vùng bộ nhớ không tồn tại trên NES không mapper ROM. Game cố tải cùng Koopa 5 lần mỗi frame ở vị trí (0,0), làm bão hòa PPU và gây treo.

; Vì sao ID $58 crash :
; Sprite pointer → địa chỉ không hợp lệ (ngoài không gian NES chuẩn)
; → Game đọc các byte không xác định thành kiểu sprite
; → Koopa $00 (kiểu không hợp lệ không handler) lặp gọi đệ quy
; → Tràn ngăn xếp 6502 → treo

Nghịch lý pipe warp

Nhớ kiểm tra target_world BETWEEN 1 AND 8. Ngay cả khi bạn thấy ống trong glitch world, game kiểm tra thế giới đích từ 1 tới 8. Các glitch world có số > 8 (36-1, 255-1...), nên warp thất bại.

Đó cũng là lý do Minus World không có kết : cột cờ không hiện trong sprite, và các ống không dẫn đâu.

Mẹo 5 vật thể trong một cột

Có trường hợp biên cho phép vượt giới hạn 3 vật thể cho mỗi cột. Khi hàng đợi kẹt (slot đầy + vật thể tiếp thiếu next screen flag), game "tiền xử lý" cột hiện tại vòng lặp tới khi thấy vật thể có next screen flag. Trong mỗi lần tiền xử lý :

; Trong lúc tiền xử lý cột :
; 1. Các vật thể trong hàng đợi thấy chiều rộng còn lại
;    giảm mỗi "bước tiến giả" của cột
; 2. Nếu vật thể chạm chiều rộng=0, nó rời hàng đợi
; 3. Slot trống có thể lấp bằng vật thể mới
;    thêm trong cùng cột

; Kết quả : tới 5 vật thể xử lý trên cùng cột.
; Kỹ thuật : đặt 2 vật thể vượt biên màn hình
; (slot 1 và 2), 1 vật thể mồi ở X < trước (chặn hàng đợi),
; rồi 3 vật thể ở X=0 của màn tiếp (gồm một có next screen flag).

Việc này gọi là "queue skip" và vài romhacker dùng nó để tạo màn đặc hơn định dạng thường cho phép.

Khác biệt giữa các phiên bản

Famicom Disk System

Bản FDS của SMB1 có bản đồ bộ nhớ khác. Mọi con trỏ màn dịch chuyển, nhưng dữ liệu giống. Điều đổi : index của các glitch world hoàn toàn khác :

FDS World 36 → Level ID $09 (bản nước của 5-3)
  → Cột cờ hiện diện ! Có thể qua màn.
  → Sau đó : $27 (7-3 thường) → $44 (4-4 ngầm)
  → $44 qua được → rìu hoạt động → kết game !

Minus World FDS vì vậy là "world thưởng" có thể dẫn
tới hoàn tất game, ngược với bản NES.

Màn FDS tôi thích nhất : ID $5F, bản ngầm của nửa sau 3-3 ở đường hầm thấp (tiếc là tự cuộn).

The Lost Levels (Super Mario Bros. 2 Nhật)

Lost Levels đổi nhiều thứ :

  1. Thứ tự tile/sprite giống hệt : hết màn Frankenstein (tile và sprite tải cùng màn ngay cả với ID không hợp lệ)
  2. Một bảng con trỏ 16-bit duy nhất thay vì hai bảng riêng cao/thấp
  3. 4 tệp đĩa : ROM chẻ cho FDS :
    • Tệp 1 : world 1-4
    • Tệp 2 : world 5-8
    • Tệp 3 : world 9 + engine âm thanh
    • Tệp 4 : world A-D (bảng con trỏ hoàn toàn khác)
  4. Cùng Level ID = 4 màn khả dĩ tùy tệp tải
  5. Hết glitch Tennis : tùy chọn tiếp tục (tiếp tục ở cùng thế giới sau game over) khiến warm start vô ích, và game reset ngay nếu world > 9
  6. Vật thể mới : nấm độc, block vô hình, block vô hình fire flower, ống lộn ngược, gió -- nhưng chèn giữa danh sách hiện có → không tương thích ngược với SMB1
  7. Piranha Plant luôn đỏ sau world 4, lò xo xanh chỉ ở world 2/B/3/C/7

Super Mario All-Stars (SNES)

Port trực tiếp với cùng routine 6502 (SNES chạy mã NES ở chế độ tương thích) :

  • Warp zone sửa : hết Minus World (vào ống trái trước khi chữ hiện dẫn tới đúng thế giới)
  • Sập : hầu hết glitch level crash (trừ ID $6A và 9-1)
  • Vật thể lâu đài thêm : render độc đáo hơn
  • Nhưng : 4-2 wrong warp vẫn chạy (chưa vá !)

4-2 wrong warp : bug đặt vật thể

Trong 4-2, có hai vật thể chuyển ống : dây leo (warp zone) và ống (phòng xu). Vật thể chuyển đầu (của dây leo) đặt trước xa khi dây leo xuất hiện trên màn hình. Cái thứ hai (ống) đặt quá muộn trong màn.

; Thời điểm chuyển trong 4-2 :
; Vật thể chuyển 1 (dây leo → warp zone) : đặt 3 màn hình trước dây leo
; Vật thể chuyển 2 (ống → phòng xu) : đặt 1 màn hình sau ống
;
; Bình thường vật thể đầu bị tắt trước khi Mario
; chạm ống. Nhưng nếu Mario nhanh (hay dùng
; lối tắt của khối B+right), chuyển của dây leo
; vẫn hoạt động khi nó chạm ống !
; → Game tải warp zone thay vì phòng xu.
;
; Nếu vật thể đặt ngay sau dây leo nhưng trước
; ống, bug đã không tồn tại.

Các màn vòng lặp

Vòng lặp chạy thế nào (8-4, 7-4)? Màn có checkpoint với số màn hình và vị trí Y ghi cứng :

; Checkpoint : {số_màn_hình, vị_trí_dọc}
; Nếu Mario qua checkpoint này ở đúng chiều cao → màn tiếp tục
; Ngược lại → warp lùi 4 màn hình (64 block)
;
; Để tạo vòng lặp vô hạn : vị_trí_dọc = $F0
; (dưới đáy màn hình) → không thể đạt.
;
; Checkpoint đơn giản (một cờ) trừ world 7
; dùng bộ ba (3 cờ, phải hỏng ít nhất 1)
;
; Warp lùi gắt : offset dữ liệu tile đặt thành giá trị
; ghi cứng, offset dữ liệu sprite về 0. Kẻ thù hiện diện
; bị dỡ ngay → firebar biến mất.

Đổi định dạng, không đổi mã

Một trong những bài học cuốn hút nhất của kiến trúc này là nhà phát triển SMB1 đã tạo hệ màn rất biểu cảm mà không bao giờ chạm mã render 6502. Mọi biến thiên giữa các màn tới từ dữ liệu (con trỏ, vật thể, sprite, floor pattern), không phải mã.

256 glitch world tồn tại vì bảng con trỏ định cỡ 128 mục × 4 kiểu, và game không bao giờ kiểm tra giá trị nó đọc. Khi con trỏ rơi vào RAM, game diễn giải thanh ghi của Mario thành tile. Khi con trỏ rơi vào dữ liệu âm thanh, game chơi nhạc dưới dạng thiết kế màn. Và khi jump table tràn, game thực thi bất cứ gì tới khi crash.

More Super Mario Bros. Mechanics Explained -- video thứ 4

Rút ra được gì từ tất cả

  1. Tách tile/sprite : độc lập hoàn toàn của hai lớp, với thứ tự lưu khác nhau tạo màn Frankenstein độc đáo
  2. Nén RLE + hệ vật thể : màn không phải bitmap mà là danh sách vật thể đặt, với floor pattern cho mặt đất
  3. Hàng đợi 3 slot : giới hạn chặt của phần cứng (và thiết kế màn)
  4. Tin tuyệt đối vào con trỏ : game tin con trỏ và jump table, tạo glitch chơi được hay crash
  5. Tối đa 256 byte : giới hạn của thanh ghi Y 6502, khiến dữ liệu lặp nếu đi quá xa
  6. Warm start / cold start : hệ "tiếp tục" mở ra cart swap Tennis → Mario

Đẹp nhất : tất cả là mã 6502 gói trong 40KB. Mã đối thoại trực tiếp với phần cứng và đặt niềm tin tuyệt đối vào mỗi lần truy cập bộ nhớ. Nếu con trỏ hỏng, game crash. Và các crash, ta gọi là glitch world.

3 điều cần giữ

  1. Glitch world là con trỏ rơi sai -- Game có 128 ID × 4 kiểu vùng, nhưng chỉ 34 màn duy nhất. Khi số world hỏng (bởi Tennis hay wall clip), game tải con trỏ thiết kế cho màn khác, và 512 tổ hợp khả dĩ cho kết quả không đoán được.

  2. Minus World là bug warp cộng hỏng -- Ống trái trong 1-2, nếu kích hoạt trước khi chữ hiện, tải world 36 (0x24). World này trỏ tới Level ID $01 (nước của 2-2), màn không cột cờ. Và vì không có chuyển ống nào cho world 36, màn lặp vô hạn. Thiếu kiểm tra tạo biểu tượng.

  3. Tennis → Mario, 15 năm trước OoT → Paper Mario -- RAM của NES sống sót qua swap băng nhờ tụ điện và hệ warm start / cold start của SMB1. Bộ đếm bước chân của Tennis (tăng byte RAM khi phát tiếng bước) rơi đúng địa chỉ của số world. Phải giữ các chữ số điểm cao nhất ở 0, byte $A5 nguyên vẹn, và game phát hiện warm start -- hội tụ hoàn hảo chỉ chạy được với Tennis.

Các video gốc của Retro Game Mechanics Explained đúng là công trình kiến -- chi tiết về dịch ngược 6502, bản đồ tự động của mọi màn, giải thích cart swap và warm start. Nếu bạn chưa xem series, hãy xem, nó ngắn và mỗi phút đều đặc.

Super Mario Bros.: รูปแบบของเลเวล, พอยน์เตอร์ และ 256 glitch worlds

วิธีที่ 128 เลเวล × 4 ประเภทพื้นที่บรรจุอยู่ใน ROM 40KB ทำไม Minus World ถึงมีอยู่ และวิธีที่การแข่งขันเทนนิส NES สามารถโหลด glitch worlds

บทนำ

Super Mario Bros. คือ ROM ขนาด 40 กิโลไบต์ แปดโลก, 32 เลเวล, ศัตรู, เพลง, power-ups ทุกอย่างบรรจุอยู่ในนั้น

แต่ถ้าคุณเปิดเครื่องเลียนแบบแล้วแก้ไข byte ที่ถูกต้อง คุณสามารถโหลดเลเวล 36-1 ได้ หรือ 255-1 หรือลงจอดในโลกที่ทุกอย่างทำจาก sprite ของ Bowser และท่อที่นำไปสู่ที่ไหนไม่ได้

glitch worlds เหล่านี้มีอยู่ด้วยเหตุผลง่ายๆ: ระบบการจัดเก็บเลเวลของ SMB1 คือผลงานชิ้นเอกของการเพิ่มประสิทธิภาพ 8 บิต และเมื่อเราบังคับให้เกมอ่านในที่ที่ไม่ควรจะอ่าน มันจะให้ผลลัพธ์ที่น่าทึ่ง

Retro Game Mechanics Explained ได้ทำซีรีส์วิดีโอ 4 ตอนเกี่ยวกับเรื่องนี้ -- เราจะรวมมันเป็นการเดินทางเดียวในโค้ด 6502 ของเกมที่ขายดีที่สุดในยุคของมัน

GLITCH OBJECTS -- ชื่อเรื่องของซีรีส์ RGMechEx เกี่ยวกับกลไกที่ซ่อนอยู่ของ SMB1

World 9-1 -- หน้าจอชื่อของ glitch world แรกที่เข้าถึงได้ผ่าน cart swap Tennis

Warm start: ทำไม RAM ของ Tennis ถึงอยู่รอดใน SMB1

ก่อนจะพูดถึงการจัดเก็บเลเวล เราต้องเข้าใจว่า SMB1 เริ่มต้นอย่างไร เพราะ glitch ของ cart swap NES Tennis นั้นขึ้นอยู่กับ ระบบการตรวจจับ warm start / cold start ของเกมโดยสิ้นเชิง

41 byte ที่ถูกเก็บรักษา

เมื่อ SMB1 ตรวจจับ cold start (การเปิดเครื่องครั้งแรกหรือปิด/เปิดเครื่อง) มันจะล้าง RAM ทั้งหมด แต่เมื่อมันตรวจจับ warm start (รีเซ็ตปุ่ม ไม่มีการตัดไฟ) มันจะเก็บรักษาพื้นที่หน่วยความจำ 41 byte:

; Les 41 bytes préservés en RAM lors d'un warm start
; Adresses $075F-$0787
;
; $075F : byte de démarrage (world - 1)    [1 byte]
; $0760 : flag de sélection de monde (B button) [1 byte]
; $0761-$0762 : inutilisé                    [2 bytes]
; $0763-$0768 : timer (6 digits, 3 affichés) [6 bytes]
; $0769-$076E : coins Luigi                   [6 bytes]
; $076F-$0774 : coins Mario                   [6 bytes]
; $0775-$077A : score Luigi                   [6 bytes]
; $077B-$0780 : score Mario                   [6 bytes]
; $0781-$0786 : top score (6 digits, 1 caché) [6 bytes]
; $0787 : le byte magique $A5                 [1 byte]

41 byte เหล่านี้มีไว้เพื่อฟังก์ชันเดียว: อนุญาตให้ผู้เล่น ดำเนินการต่อในโลกเดียวกันหลังจาก game over ถ้าคุณตายใน 6-3 เกมจะเขียนโลก 6 ลงใน byte เริ่มต้น และที่หน้าจอชื่อ ถ้าคุณกด A + Start คุณจะเริ่มใหม่ใน 6-1

41 byte ที่ถูกเก็บรักษาใน RAM เมื่อ warm start -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD และ byte วิเศษ $A5

การตรวจสอบสองครั้งของ warm start

Cold start vs warm start -- แผนภาพการตรวจจับรีเซ็ต

เมื่อ SMB1 บูต มันไม่ได้ตรวจสอบเกณฑ์เดียวแต่ สอง:

CheckWarmStart:
  ; 1. Vérifier le byte magique $A5 à $0787
  lda $0787
  cmp #$A5
  bne ColdStart        ; pas $A5 → cold start

  ; 2. Vérifier les 6 digits du top score ($0781-$0786)
  ;    Chaque digit doit être entre 0 et 9
  ldx #0
CheckLoop:
  lda $0781,x
  cmp #$0A
  bcs ColdStart        ; digit >= 10 → cold start
  inx
  cpx #6
  bne CheckLoop

  ; Si les deux conditions passent → warm start
  ; La RAM n'est pas effacée, le monde de départ est préservé
  jmp WarmStartBoot

การตรวจสอบ byte $A5 และหลักตัวเลขของ top score -- หัวใจของ warm start

ทำไมต้องตรวจสอบสองครั้ง? เพราะ byte $A5 อาจปรากฏโดยบังเอิญ (เกมอื่นที่ทิ้งค่านี้ไว้ หรือสถานะพักเริ่มต้นของชิป RAM) เมื่อตรวจสอบว่าหลักตัวเลขของ top score ถูกต้อง (0-9) เราจะมั่นใจได้ว่าข้อมูลมีความสอดคล้องกัน

ทำไม Tennis ถึงเป็นเกมเดียวที่ใช้งานได้

เมื่อเราใส่ SMB1 เป็นครั้งแรก (cold start) เกม:

  1. ล้าง RAM ทั้งหมด → top score = 0, world byte = 0
  2. เขียน $A5 ที่อยู่ $0787

จากนั้นเราสลับไป Tennis โดยไม่ปิดเครื่องเล่น Tennis:

  • ไม่ล้าง RAM เมื่อเริ่มต้น (เกม NES ไม่กี่เกมทำแบบนี้)
  • ไม่เขียนลงใน byte ของ top score → ยังคงเป็น 0 (ถูกต้อง)
  • ไม่แตะ byte $A5 → ยังคงมีอยู่
  • ใช้ที่อยู่ $075F สำหรับตัวนับก้าวของผู้เล่น
; Le footstep increment dans Tennis :
; À chaque pas du joueur sur le court, Tennis incrémente le byte à $075F.
; Ce même byte est utilisé par SMB1 comme "world number - 1".
;
; 0 pas  → world 0 → SMB1 = world 1
; 1-7 pas → world 1-7 → worlds normaux
; 8+ pas → world 8+ → glitch worlds !
;
; Le compteur ne s'incrémente que quand la musique s'arrête
; (les footstep sounds ne jouent pas pendant la musique).

เมื่อเรากลับ SMB1:

  1. byte $A5 ยังอยู่ที่นั่น (Tennis ไม่ได้แตะมัน)
  2. หลักตัวเลขของ top score ยังเป็น 0 (ถูกต้อง)
  3. world byte มีค่า 8+ แล้ว (เพิ่มขึ้นจากก้าวของ Tennis)
  4. SMB1 ตรวจจับ warm start → เก็บรักษา world byte ที่เสียหาย
  5. กด A + Start → world 9-1, world A-1, world 36-1 ฯลฯ

ทำไมต้องบูต Mario ก่อน Tennis

ความละเอียดอ่อน: เราต้องบูต SMB1 ก่อน แล้ว Tennis แล้ว SMB1 อีกครั้ง ถ้าคุณเริ่ม Tennis โดยตรง byte $A5 จะไม่ถูกเขียนเลย (Tennis ไม่เขียน $A5) ดังนั้นการตรวจจับ warm start จะล้มเหลวและ RAM จะถูกล้าง

ตัวนับก้าวของ Tennis: ทุกก้าวจะเพิ่ม world byte

เข้าถึง Glitch Worlds ผ่าน NES Tennis -- วิดีโอที่อธิบาย cart swap

วิธีที่ SMB1 เก็บเลเวลใน 40KB

Nintendo R&D4 ต้องแก้ปัญหาที่ดูเหมือนง่าย: แสดงเลเวลที่เลื่อนในแนวนอนด้วย tiles, ศัตรู, items ทั้งหมดอยู่ในงบ ROM ที่จำกัดมาก

วิธีแก้คือการแยกออกเป็นสองชั้นข้อมูล ที่เป็นอิสระต่อกันโดยสมบูรณ์:

Tile layout (แผนที่ของเลเวล)

ทุกเลเวลถูกกำหนดโดยพอยน์เตอร์ไปยังโครงสร้าง tiles ที่บีบอัดใน ROM การบีบอัดนั้นหยาบแต่ชาญฉลาด: byte "ควบคุม" ตามด้วย 1-3 byte ของข้อมูล

รูปแบบ tile ใช้ระบบ runs (คล้าย RLE):

; Format tile SMB1 (simplifié)
; Chaque "commande" est un byte contrôle :
;
; $00-$7F : pose une tile, avance d'1 colonne
; $80-$BF : pose une tile répétée N fois (N = byte - $80 + 1)
; $C0-$FF : commande spéciale (fin de ligne, saut, changement de palette)

Exemple : pour dessiner 3 briques consécutives :
  $82 $01    ; répète la tile $01 (brick) 3 fois

แต่ละเลเวลมี 13 แถวของ 16 คอลัมน์ tiles (13×16 = 208 tiles ที่มองเห็นได้) แต่รูปแบบที่บีบอัดช่วยให้ลดลงได้มากกว่านั้น -- ตัวอย่างเช่น ท้องฟ้าและคอลัมน์ว่างเปล่าแทบไม่ใช้พื้นที่เลย

ลูปเรนเดอร์ใน 6502:

; Décompression tile - loop principale
; Entrée : pointeur tile_data en $XX
; Sortie : tilemap niveau dans la RAM PPU

DecompressTile:
  lda (tile_ptr),y      ; lire byte contrôle
  iny
  cmp #$80
  bcc SingleTile        ; $00-$7F : tile unique
  cmp #$C0
  bcc RunLength         ; $80-$BF : run-length
  jmp SpecialCommand    ; $C0-$FF : commande spéciale

SingleTile:
  sta PPU_DATA          ; écrire la tile directement
  jmp Next

RunLength:
  sec
  sbc #$7E              ; N = control - $7E
  tax
  lda (tile_ptr),y      ; lire la tile à répéter
  iny
: sta PPU_DATA
  dex
  bne :-
  jmp Next

Sprite layout (ศัตรูและวัตถุ)

พร้อมกันนั้น ศัตรูและวัตถุ (บล็อก ?, ท่อ, goombas, koopas) ถูกเก็บในโครงสร้างที่แยกจากกันโดยสมบูรณ์ การ spawn แต่ละครั้งถูกกำหนดโดย 2 byte:

; Format sprite SMB1
; Byte 0 : position X (en colonnes)
; Byte 1 : type de sprite + bits de page Y
; Y est dérivé de l'index dans la séquence

Une séquence de sprites :
  $01 $4B    ; goomba à la colonne 1
  $09 $4B    ; goomba à la colonne 9
  $10 $61    ; bloc ? à la colonne 16 (contient pièce)
  $15 $54    ; koopa verte à la colonne 21
  $FF        ; fin de séquence

แต่ละเลเวลสามารถอ้างอิงถึง 5 หน้า sprites ที่แตกต่างกันได้สูงสุด (หมายถึง 5 "หน้าจอ" ของ 16 คอลัมน์) แต่ในทางปฏิบัติส่วนใหญ่ใช้เพียง 2-3 หน้าเท่านั้น

ตารางพอยน์เตอร์

อัจฉริยะของการออกแบบคือตารางพอยน์เตอร์ แต่ละเลเวลถูกเก็บเป็น คู่ ของที่อยู่ ROM:

// Structure interne (simplifiée) du World Map
struct LevelPointer {
    uint16_t tile_ptr;   // Adresse ROM des données tiles
    uint16_t sprite_ptr; // Adresse ROM des données sprites
};

// 4 tables séparées, une par AreaType :
// 0 = Water, 1 = Overworld, 2 = Underground, 3 = Castle
LevelPointer level_table[4][128];

128 รายการต่อตาราง 4 ประเภทพื้นที่ 512 ชุดค่าผสมที่เป็นไปได้ แต่เฉพาะส่วนน้อยเท่านั้นที่ใช้โดยเกมอย่างเป็นทางการ ที่เหลือคือ RAM ที่ไม่ได้เริ่มต้นหรือข้อมูลที่ถูกตีความเป็นพอยน์เตอร์

เมื่อเกมโหลดเลเวล มันทำแบบนี้:

; Chargement d'un niveau
; A = AreaType (0-3), X = LevelID (0-127)

LoadLevel:
  sta AREA_TYPE
  asl                  ; *2 pour offset dans table 16-bit
  tax
  lda LevelTable_TilePtr, x
  sta TILE_PTR
  lda LevelTable_TilePtr+1, x
  sta TILE_PTR+1       ; pointeur vers les tiles
  lda LevelTable_SpritePtr, x
  sta SPRITE_PTR
  lda LevelTable_SpritePtr+1, x
  sta SPRITE_PTR+1     ; pointeur vers les sprites
  jsr DecompressTiles

ไม่มีการตรวจสอบ ไม่มีการยืนยันว่าพอยน์เตอร์ถูกต้อง เกมอ่านที่อยู่ในตารางแล้วบีบอัดสิ่งที่อยู่ที่ที่อยู่นั้น จุดจบ

Level ID $06 (Water) -- 9-1, เวอร์ชันใต้น้ำของ 6-2

ตาราง Level IDs: 128 รายการที่เป็นไปได้ 34 รายการที่กำหนด

ลำดับที่แตกต่างของพอยน์เตอร์ tiles และ sprites -- สาเหตุของ Frankenstein levels

34 เลเวลที่ไม่ซ้ำกันและระบบ ID 7 บิต

ชิป RAM ของ NES (MB8416A) -- มันคือสิ่งที่เก็บรักษาข้อมูลเมื่อเราสลับตลับ

SMB1 ไม่มี 32 เลเวล แต่มี 34 เลเวลที่ไม่ซ้ำกัน เลเวลจำนวนมากเป็นสำเนา (5-3 = 1-3 แต่มี Bullet Bills) ที่ทำเครื่องหมายด้วยธง "hard mode" เลเวลที่ไม่ซ้ำกันจริงๆ:

  • น้ำ (Type 0): 3 เลเวล (2-2, 7-2, โซนโบนัส 5-2/6-2)
  • Overworld (Type 1): 22 เลเวล (รวมห้องเมฆโบนัส 2 ห้อง)
  • Underground (Type 2): 3 เลเวล (รวมห้องโบนัสใต้ดิน)
  • Castle (Type 3): 6 เลเวล
  • + 1 ห้อง cutscene (ก่อนเลเวลใต้ดิน/น้ำ)
  • + 1 warp zone ของ 4-2

แต่ละเลเวลมี ID 7 บิต 5 บิตต่ำสุด = หมายเลขในกลุ่มย่อย 2 บิตสูงสุด = ประเภทพื้นที่:

; Encodage 7-bit du Level ID
; Bits 6-5 : Type (00=Water, 01=Overworld, 10=Underground, 11=Castle)
; Bits 4-0 : Numéro dans le sous-groupe
;
; Water IDs      : $00-$02  (types 00, numéros 0-2)
; Overworld IDs  : $20-$35  (types 01, numéros 0-21)
; Underground IDs: $40-$42  (types 10, numéros 0-2)
; Castle IDs     : $60-$65  (types 11, numéros 0-5)
;
; ID $25 = %0100101 → type 01 (Overworld), numéro 5 → 1-1
; ID $23 = %0100011 → type 01 (Overworld), numéro 3 → 6-2

128 IDs ที่เป็นไปได้ ($00-$7F) มีเพียง 34 เท่านั้นที่กำหนดให้กับเลเวลจริง IDs ที่ไม่ได้ใช้ชี้ไปยังอะไรก็ได้

ตารางพอยน์เตอร์: สองรายการ สองลำดับ

พอยน์เตอร์ tiles และ sprites ไม่ได้ถูกเก็บในลำดับเดียวกัน โค้ดใช้รายการ 16 บิตสองรายการที่แยกจากกัน (high byte / low byte ในสองตารางที่แตกต่างกัน):

Ordre des pointeurs sprites :
  Index 0-5   : Castle (6 niveaux)
  Index 6-27  : Overworld (22 niveaux)
  Index 28-30 : Underground (3 niveaux)
  Index 31-33 : Water (3 niveaux)

Ordre des pointeurs tiles :
  Index 0-2   : Water (3 niveaux)
  Index 3-24  : Overworld (22 niveaux)
  Index 25-27 : Underground (3 niveaux)
  Index 28-33 : Castle (6 niveaux)

ทำไมลำดับจึงต่างกัน? ไม่มีเหตุผลทางเทคนิค -- มันน่าจะเป็นแบบที่ข้อมูลถูกจัดเรียงระหว่างการพัฒนา แต่มันสร้างผลลัพธ์ที่น่าทึ่ง: เมื่อ ID เลเวลไม่ถูกต้อง พอยน์เตอร์ tiles และ sprites จะโหลดเลเวล ที่แตกต่างกัน สร้าง Frankenstein levels

เพื่อนำทางระหว่างสองรายการนี้ เกมใช้ ตาราง offset เล็กๆ (เหมือนสารบัญ):

; Tables d'offset par type (Water, Overworld, Underground, Castle)
; Chaque entrée = index de début dans la liste correspondante

SpriteOffsetTable:
  .byte $1F, $06, $1C, $00    ; Water=31, Overworld=6, Underground=28, Castle=0

TileOffsetTable:
  .byte $00, $03, $19, $1C    ; Water=0, Overworld=3, Underground=25, Castle=28

เพื่อโหลดเลเวล 6-2 (ID $23, Overworld หมายเลข 3):

; 1. Type = 01 (Overworld) → index dans table d'offset = 1
; 2. Sprite offset = SpriteOffsetTable[1] = 6
;    Index final = 6 + 3 (numéro de niveau) = 9 → 10ème pointeur sprites
; 3. Tile offset = TileOffsetTable[1] = 3
;    Index final = 3 + 3 = 6 → 7ème pointeur tiles
; 4. Résultat : pointeur tiles $A619 + pointeur sprites $9ED0 = 6-2 ✓

ตอนนี้เกิดอะไรขึ้นกับ ID ที่ไม่ถูกต้องเช่น $43 (Underground หมายเลข 3 ที่ไม่มีอยู่)?

; ID $43, Type = 10 (Underground), numéro = 3
; Sprite offset = SpriteOffsetTable[2] = $1C = 28
;   Index = 28 + 3 = 31 → 32ème pointeur sprites = eau bonus 5-2 !
; Tile offset = TileOffsetTable[2] = $19 = 25
;   Index = 25 + 3 = 28 → 29ème pointeur tiles = 1-4 (Castle) !
;
; Résultat : un niveau souterrain avec les tiles de 1-4
; et les Bloopers de la zone eau de 5-2. Un vrai Frankenstein.

Level ID $43 -- Frankenstein level: tiles 1-4 + sprites น้ำ 5-2

Exploring Glitch Level Pointers -- ตาราง offset ที่อธิบาย

ตาราง world index -- เมื่อ overflow ของ world 9 สร้าง glitch level

ตาราง world index: ทำไม world 9 ถึง overflow

มีตาราง ROM ขนาด 8 byte ที่ให้ index ของเลเวลแรกของแต่ละโลก (1-8) และทันทีหลังจากนั้นคือตาราง Level IDs 36 ตัวของทุกเลเวลในลำดับการเล่น

; WorldIndexTable (8 bytes)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   -> Monde 1 commence au niveau 0
;   -> Monde 2 commence au niveau 5
;   -> Monde 8 commence au niveau 33

; LevelIDTable (36 bytes)
  .byte $25, $28, $29, $26, $24, ... ; les 36 Level IDs

เมื่อเราพยายามโหลด world 9 เกมจะอ่าน byte ที่ 9 ของ WorldIndexTable... ที่ไม่มีอยู่ มัน overflow 1 byte ลงใน LevelIDTable อ่านค่า $25 แล้วใช้ $25 เป็น index ใน LevelIDTable (รายการที่ 37) -- ซึ่ง overflow อีก 2 byte ลงใน SpriteOffsetTable และอ่านค่า 6

; World 9 :
;   1. WorldIndexTable[8] (overflow) → lit $25 dans LevelIDTable
;   2. LevelIDTable[37] (overflow) → lit le 2ème byte de SpriteOffsetTable = 6
;   3. ID = 6 → Water level number 6 (qui n'existe pas)
;   4. Tile pointer = pointeur water numéro 6 = tiles de 6-2
;   5. Sprite pointer = index 31+6 = 37 > 33 → pointeur invalide
;   6. Résultat : 6-2 sous l'eau avec des sprites glitchés
;      → world 9-1 !

สำหรับ world G (16) overflow ไกลยิ่งขึ้นและตกไปที่ Level ID $01 ซึ่งเป็นเลเวล cutscene ก่อน 1-2:

; World G (16) :
;   WorldIndexTable[15] → lit $01 dans LevelIDTable
;   LevelIDTable[1] = $29 (cutscene 1-2)
;   → world G-1 = la cutscene d'entrée de 1-2

ทำไม glitch worlds ถึงมีอยู่

เกมมี 32 เลเวล "ถูกกฎหมาย" (8 โลก × 4 เลเวล) แต่ตารางพอยน์เตอร์มี 128 รายการต่อประเภทพื้นที่ รายการที่อยู่เลยเลเวล 32 มีสิ่งที่อยู่ใน ROM ที่ที่อยู่เหล่านั้น -- บางครั้งเป็นเลเวลอื่น บางครั้งเป็นข้อมูลเสียง บางครั้งเป็น RAM บางครั้งเป็นอะไรก็ได้

Level ID $01 Water (Minus World) -- tile pointer $AE45, sprite pointer $A171

Level ID $01 + AreaType 0 = Minus World

ที่มีชื่อเสียงที่สุดของ glitch worlds Level ID $01 ใน AreaType 0 (น้ำ) ชี้ไปที่:

  • Tile pointer: $AE45 → โซนใต้น้ำของ 2-2/7-2
  • Sprite pointer: $A171 → sprites ของ 2-2/7-2

ผลลัพธ์: เลเวลน้ำที่ดูเหมือน 2-2 แต่วนลูปไม่รู้จบเพราะ flagpole ไม่มีอยู่ ไม่มีจุดสิ้นสุดเลเวล ไม่มีทางออก

นี่คือเลเวล 36-1 (หรือ 36-1 ในโลก $-1)

การตรวจสอบ warm start ของ SMB1 -- มันคือสิ่งที่ทำให้ Minus World ดำรงอยู่

; Pourquoi le flagpole manque dans le Minus World :
; Les sprites de 2-2/7-2 ($A171) n'ont pas de flagpole
; dans leur séquence. Le jeu cherche le sprite $FD (flagpole)
; mais ne le trouve jamais → boucle infinie
;
; Le jeu continue de générer le niveau à l'infini
; jusqu'à ce que le timer atteigne zéro.

พอยน์เตอร์ที่ชี้ไปยัง RAM

เมื่อ tile pointer หรือ sprite pointer ชี้ไปยังที่อยู่ใน RAM ($00-$7F) แทนที่จะเป็น ROM เกมพยายามตีความการเปลี่ยนแปลงของ RAM อย่างต่อเนื่องเป็น tiles:

; Exemple : Level ID $03 en Water
; Tile Pointer : $A46B (3-3 - valide)
; Sprite Pointer : $009D (pointe vers la RAM page zéro !)
;
; La RAM page zéro contient les registres du jeu,
; la position de Mario, l'état des compteurs...
; Le jeu décompresse ça comme une séquence de sprites,
; et le résultat c'est un niveau avec des ennemis
; qui sont en fait des valeurs de registres.

เมื่อหน้าแรกเปลี่ยน (เพราะ Mario เคลื่อนที่ ตัวนับหมุน ฯลฯ) "sprites" ของเลเวลก็เปลี่ยนด้วย นี่คือเหตุผลที่ glitch worlds บางตัวมีศัตรูที่กะพริบและเปลี่ยนแปลงตลอดเวลา

Level ID $03 Water -- sprite pointer $009D ชี้ไปยัง RAM, เลเวลเล่นไม่ได้

Level ID $36: เลเวลว่างเปล่า (Overworld)

Level ID $36 ใน Overworld:

  • Tile pointer: $AC35 (1-2)
  • Sprite pointer: $A0D8 (1-2)

ผลลัพธ์: ไม่มีอะไร เกมโหลดเลเวลแต่มันถูกทำเครื่องหมายว่า "ไม่มีเลเวล" ในแคตตาล็อกของ RGMechEx tiles อาจถูกต้องแต่ sprites ชี้ไปยังตำแหน่งที่สร้างเลเวลว่างเปล่าหรือใช้งานไม่ได้

Level ID $1D (Castle): แชมป์แห่งการ crash

Level ID $1D ใน Castle:

  • Tile pointer: $A210 (4-4)
  • Sprite pointer: $7EA0 (RAM!)

Sprite pointer ใน RAM = undefined sprites เกมพยายามแสดง Spiny ball หรือ Bullet Bill blaster ในแถว tiles แรก มัน crash ทันที

; Quand le sprite pointer pointe vers la RAM,
; le jeu décompresse des bytes qui changent tout le temps
; comme des instructions "spawn". Le résultat :
; - Apparition d'objets inexistants (valeur undefined)
; - Crash PPU quand le sprite NES essaie d'afficher un tile invalide
; - Freeze complet de la console

256 glitch worlds ที่จัดทำขึ้น

RGMechEx เขียนสคริปต์ที่สร้างแผนที่ของ ทุกเลเวล สำหรับ 4 ประเภทพื้นที่ และ 128 IDs แต่ละตัว

ตัวนับโลกมี 8 บิต (0-255) โลก 1-8 ถูกกฎหมาย ยังเหลือ 248 glitch worlds ที่เป็นไปได้ glitch world แต่ละตัวสอดคล้องกับเลเวลแรกของโลกนั้น และ Level ID ของมันถูกคำนวณโดยกลไก overflow ของ WorldIndexTable

ตาราง glitch worlds -- 248 โลกที่เสียหาย 68 เลเวลแรกที่เข้าถึงได้

ใน 128 IDs ที่เป็นไปได้ มีเพียง 68 ตัวเท่านั้นที่เป็น "first level" ของโลก (เข้าถึงได้ผ่านหมายเลข glitch world) อีก 60 ตัวเป็นเลเวล 2+ หรือเข้าถึงไม่ได้

ประเภท IDs ที่เล่นได้ไม่ซ้ำกัน IDs ที่ crash IDs ว่างเปล่า
Water (0) ~20 ~60 ~48
Overworld (1) ~30 ~55 ~43
Underground (2) ~15 ~65 ~48
Castle (3) ~25 ~58 ~45

IDs จำนวนมากนำไปสู่เลเวลเดียวกันเนื่องจากพอยน์เตอร์ที่ตกไปที่ที่อยู่ ROM เดียวกัน Level ID $28 (Overworld) ตัวอย่างเช่น -- tile pointer $A7CD (2-1) -- ปรากฏใน 38 glitch worlds ที่แตกต่างกัน เพราะ sprite pointer $9F51 ชี้ไปยังโซน ROM ที่ใช้เป็น padding/ข้อมูลเสียงที่ใช้ซ้ำโดย IDs จำนวนมาก

แผนที่เลเวล ID $28 (Overworld) -- 2-1 tiles กับ sprites ปกติ, 38 glitch worlds

Super Mario Bros. Glitch Levels Explained -- วิดีโอที่ 3

6 glitch levels ที่ไม่ซ้ำกันจริงๆ

ใน 19 IDs ของ glitch level ที่เข้าถึงได้ มีเพียง 6 ตัวเท่านั้นที่ไม่ crash ทันที เมื่อโหลด:

World Level ID คำอธิบาย
E-1 (224) $50 บล็อก ? เดียวเหนือเหวลึก Mario ตายทันที
W $57 Mario ถูกขังเมื่อ spawn ไม่สามารถเคลื่อนที่ได้
42 (133) $50 อุโมงค์เมฆที่ขัง Mario ถ้าเขาไปไกลพอ
62 (131, 240) $4D ปราสาทแช่แข็ง: Mario spawn ด้านบน ไม่สามารถตกลงมาได้ → ถูกขัง
127 $4B อุโมงค์ใต้ดิน แต่ crash ถ้าไปไกลเกินไป
137 $4B เปิดใช้งานการเลื่อนอัตโนมัติของ cutscenes Mario พบบล็อก brick เดียวที่ขังเขาตลอดกาล

Level ID $50 (อุโมงค์เมฆ) -- glitch world 42-1 และ E-1 Level ID $4D (ปราสาท) -- world 62-1, Mario ถูกขังที่ spawn Level ID $4B (อุโมงค์) -- world 127-1, crash ถ้าไปไกลเกินไป

หก glitch worlds จาก 248 ตัวที่สร้างสิ่งที่ใหม่จริงๆ ที่เหลือเป็นเลเวลปกติแต่มีประเภทพื้นที่ผิด หรือหน้าจอว่างเปล่า

รูปแบบเลเวลโดยละเอียด

มุ่งไปที่รูปแบบข้อมูลเลเวลที่แน่นอน เพื่อเข้าใจว่าทำไม glitch levels ถึงยืนหยัดได้ (หรือไม่)

Header เลเวล: 2 byte, 6 คุณสมบัติ

ทุกเลเวลเริ่มต้นด้วย header ขนาด 2 byte ที่ควบคุม 6 คุณสมบัติ:

; Byte 0 : timer + Y start + modifier
;   Bits 7-6 : timer (00=inchangé, 01=200, 10=300, 11=400)
;   Bits 5-3 : Y start Mario (111/110 = autowalk)
;   Bits 2-0 : level type modifier
;              000=default, 001=waves, 010=brick wall,
;              011=water bottom, 100=night, 101=snow,
;              110=snow night, 111=gray night

; Byte 1 : platform + background + floor pattern
;   Bits 7-6 : special platform (00=tree, 01=mushroom,
;                                 10=Bullet Bill, 11=cloud)
;   Bits 5-4 : background (00=none, 01=clouds,
;                           10=montains, 11=fences)
;   Bits 3-0 : floor pattern initial (0-15)

ตัวดัดแปลง type ควบคุมการเปลี่ยนแปลงภาพ: คลื่นด้านบนเลเวลน้ำ, พื้นหลังอิฐของ 8-3, พาเลทกลางคืนของ 4-3, หิมะของ 6-2 ฯลฯ

วัตถุ tiles: 2 byte, Next Screen Flag, คิว 3 slots

หลัง header มาคือรายการ วัตถุ tiles แต่ละวัตถุมี 2 byte byte $FD ทำเครื่องหมายจุดสิ้นสุดของรายการ

; Format objet tile (16 bits) :
; Byte 0 :
;   Bits 7-4 : X position (colonne 0-15)
;   Bits 3-0 : Y position
;     Y=0-11  : position Y normale
;     Y=12    : objets spéciaux (trous, ponts, rope, ? blocks)
;     Y=13    : screen skip / objets spéciaux 2
;     Y=14    : changement de modifier/scenery/floor
;     Y=15    : objets spéciaux 3 (château, escaliers, gros tuyau)

; Byte 1 :
;   Bit  7   : NEXT SCREEN FLAG
;   Bits 6-4 : type d'objet (0-7)
;   Bits 3-0 : largeur/hauteur / sous-type

เมื่อ bit "next screen" ถูกตั้ง คอลัมน์ปัจจุบันจะเพิ่มขึ้น 1 ช่วยให้วางวัตถุเลย 16 คอลัมน์แรกได้ วัตถุต้องเรียง ตามลำดับ (ซ้ายไปขวา) เพราะเกมโหลดมันตามลำดับ:

; La routine de chargement a DEUX phases par colonne :
; Phase 1 : chercher les nouveaux objets qui commencent sur cette colonne
;            et les ajouter à la file d'attente (queue)
; Phase 2 : traiter chaque objet dans la queue en dessinant les tiles,
;            et retirer ceux qui finissent sur cette colonne

คิวมีเพียง 3 slots เท่านั้น ผลลัพธ์โดยตรง: เราไม่สามารถมีวัตถุมากกว่า 3 ชิ้นที่เริ่มต้นในคอลัมน์เดียวกัน ถ้าคิวเต็ม วัตถุที่ 4 จะถูกละเว้นและจะไม่ถูกโหลดเลย

นี่คือเหตุผลที่เลเวลที่ออกแบบดีหลีกเลี่ยงการวางวัตถุมากเกินไป ตัวอย่างใน 1-2: คอลัมน์ที่มีบล็อก 1up ในเพดาน + อิฐข้างๆ ถูกแยกเป็นสองวัตถุที่แตกต่างกันเพื่อเคารพขีดจำกัด 3

Y position พิเศษ: 12, 13, 14, 15

เมื่อ Y=12 วัตถุไม่มีตำแหน่ง Y (มันถูก hardcode ตามประเภท):

; Y=12 : objets sans Y position
;   Type 0 : trou (supprime le sol)
;   Type 1 : rope de plateforme mobile
;   Types 2-4 : ponts à Y fixe
;   Type 5 : trou avec eau/lave
;   Types 6-7 : rangées de ? blocks

เมื่อ Y=13 มีสองกลุ่มย่อย ถ้า bit 6 ของ byte 1 เป็น 1:

; Y=13, bit6=1 : objets spéciaux
;   0 = L-pipe (cutscene), 1 = flagpole, 2-3 = pont/axe/hamer (fin château)
;   4 = stop screen, 5 = random enemies, 6 = loop level, 7+ = crash possible

ถ้า bit6=0 5 บิตต่ำสุดจะเข้ารหัส screen skip (ข้ามไปยังหน้าจอ N โดยตรง โดยไม่ต้องผ่าน next screen flag ทีละตัว)

เมื่อ Y=14: หลักการเดียวกันกับ bit6=1 เพื่อเปลี่ยน type modifier, bit6=0 เพื่อเปลี่ยนพื้นหลัง + floor pattern

Floor patterns: 16 ลวดลายพื้น

พื้นของเลเวลไม่ได้ทำจากวัตถุแต่ละชิ้น SMB1 ใช้ floor patterns ลวดลายพื้นหลังที่ใช้กับทุกคอลัมน์จนถึงการเปลี่ยนแปลงถัดไป:

; Floor patterns (4 bits = 16 possibilités)
;   0 = vide total
;   1 = sol 2 tiles haut
;   2 = sol 1 tile haut
;   3 = sol + bottom
;   4 = sol + bottom 2
;   5 = sol 1/2 tile
;   6 = 3/4 sol
;   ... jusqu'à 15 = rempli total (sol + plafond)

นี่คือเหตุผลที่รูเป็นวัตถุ: มัน override floor pattern ในคอลัมน์เฉพาะ โดยไม่ต้องเปลี่ยน pattern สำหรับที่เหลือ

ขีดจำกัด 256 byte และ repeat

ข้อมูล tiles ทั้งหมดของเลเวลบรรจุใน สูงสุด 256 byte Y register ของ 6502 ถูกใช้เป็น index และมันมี 8 บิต ถ้าเกมไปถึงจุดสิ้นสุดของข้อมูลโดยไม่พบ byte $FD มันจะวนกลับไปเริ่มต้น และทำซ้ำ 256 byte ไม่รู้จบ:

; Index Y = 8 bits → max 256 bytes de données tiles
; Si Y overflow (255 → 0) sans rencontrer $FD → repeat
; Même chose pour les sprites, mais les objets pipe (3 bytes)
; décalent la parité de l'index à chaque chargement.

glitch levels บางตัวใช้ repeat นี้เพื่อสร้างเลเวลที่อยู่ "ตลอดกาล"

ระบบ sprites: 2 byte + pipe transitions

Sprites ใช้รูปแบบที่คล้ายกัน แต่ไม่มี header และมีความแตกต่างที่สำคัญบางประการ byte $FF ทำเครื่องหมายจุดสิ้นสุดของรายการ

; Format sprite (2 bytes) :
; Byte 0 : position X (colonne)
; Byte 1 :
;   Bit 7 : NEXT SCREEN FLAG
;   Bits 6-0 : type de sprite
;       Certains types incluent : goomba, koopa, Blooper,
;       Bullet Bill, Lakitu, Spiny, plateformes,
;       commande warp zone, toad/princesse,
;       commandes de spawn de groupes d'ennemis

บิตต่ำสุดของ byte 1 คือ hard level flag: ถ้าตั้งเป็น 1 sprite จะปรากฏเฉพาะในเลเวล ≥ 5-3 เท่านั้น นี่คือวิธีที่เลเวล "hard mode" ถูกสร้างขึ้น

Y position 15 = screen skip (เหมือน tiles) Y position 14 = pipe transition (3 byte):

; Sprite Y=14 : pipe/vine transition (3 bytes !)
;   Byte 0 : position X
;   Byte 1 : bits 6-0 = Level ID 7-bit (destination)
;   Byte 2 : bits 4-0 = screen de destination
;            bits 7-5 = world où cette transition est valide
;
; Pourquoi un world ? Les bonus rooms sont réutilisées entre mondes.
; Exemple : la salle bonus de 1-1 est aussi utilisée par 2-1 et 7-1.
; Cette salle a 3 transitions, une par monde, pour que Mario
; réapparaisse au bon endroit.

Sprites ไม่มีระบบคิว ขีดจำกัดเดียวคือไม่สามารถมี sprites โหลดพร้อมกันมากกว่า 4 ชิ้นในโซน spawn (แค่นอกหน้าจอทางขวา) เกินกว่านั้น sprites จะถูกละเว้น

วิธีเข้าถึง glitch worlds

มีสองวิธีหลัก

วิธีคลาสสิก: wall clip

Wall clip (ผ่านกำแพง) ช่วยให้ออกจากเลเวลปกติและเดินไปยัง warzone ที่ซ่อนอยู่ ด้วยการจัดการตัวนับโลกผ่าน RAM เราสามารถโหลด Level ID ใดก็ได้

เทคนิค:

  1. World 1-2: ไปท่อที่ซ่อนจุดสิ้นสุด
  2. ทำ wall clip บนกำแพงขวา
  3. เดินในที่ว่างจนถึงโซน warp
  4. เกมตีความค่าเป็นโลก

แต่วิธีนี้ให้เข้าถึงเพียงส่วนเล็กๆ ของ glitch worlds เท่านั้น

วิธีสุดขีด: NES Tennis cart swap

ดูส่วน "Warm start" ด้านบนสำหรับรายละเอียดที่สมบูรณ์ โดยสรุป: ตัวนับก้าวของ Tennis เขียนลงใน byte RAM เดียวกันกับโลกเริ่มต้นของ SMB1 และการตรวจจับ warm start เก็บรักษาค่านี้

มุมนักปั้น: โค้ดสำหรับสำรวจทุกอย่าง

ถ้าคุณต้องการสำรวจ glitch ทั้งหมดด้วยตัวเองในเครื่องเลียนแบบ คุณสามารถ patch Level ID โดยตรง:

; Patch pour FCEUX / Mesen :
; Adresse RAM $075F = Level ID actuel
; Adresse RAM $0760 = Area Type (0=Water, 1=Overworld, 2=Underground, 3=Castle)

; Exemple : charger le Level 57 (0x39) en Overworld
; Dans l'émulateur, ouvrir le traceur mémoire et écrire :
; $075F = 0x39
; $0760 = 0x01
; Puis entrer dans un tuyau de warp ou mourir et recommencer
; → Le jeu charge le niveau ID $39 en Overworld

RGMechEx เผยแพร่รายการที่สมบูรณ์ของ 128 เลเวล × 4 ประเภทพร้อมแผนที่ที่สร้างอัตโนมัติบน rgmechex.com แต่ละรายการแสดง tile pointer, sprite pointer และแผนที่ภาพของเลเวล

เลเวลที่วุ่นวายที่สุด

Level ID $1F (Water): 15 glitch worlds ในตัวเดียว

Tile pointer $A302 (3-4) รวมกับ sprite pointer $02A0 ให้ 15 glitch worlds ที่แตกต่างกัน (D-1, J-1, Y-1, Z-1, 55-1, 73-1...) คำอธิบาย: sprite pointer ชี้ไปยังโซน ROM ที่มีข้อมูลใกล้เคียงกับ sprites ที่ถูกต้องเพียงพอที่จะสร้างผลลัพธ์ที่เล่นได้ แต่การผสมผสาน tiles ปราสาท 3-4 กับ sprites overworld สร้างผลลัพธ์ที่ไร้สาระ

Level ID $28 (Overworld): 38 glitch worlds = บันทึก

บันทึกสัมบูรณ์ 38 รายการ glitch world ชี้ไปยังเลเวลเดียวกัน (2-1 tiles + $9F51 sprites) ทำไม? เพราะ sprite pointer $9F51 ตกไปในโซน ROM ที่ใช้เป็น padding/ข้อมูลเสียงที่ใช้ซ้ำโดย IDs จำนวนมาก

Level ID $49 (Underground): เลเวล FDS

Tile pointer $76AE + sprite pointer $1C9D tile pointer ชี้ไปยังโซน ROM ที่สงวนไว้สำหรับเวอร์ชัน Famicom Disk System ผลลัพธ์: เลเวลที่มี tiles ที่ไม่มีอยู่ในตลับมาตรฐาน นี่คือเลเวลที่ทำให้เลเวล 52-1 และ 196-1 ปรากฏ

Level ID $00-$02: เลเวลโบนัสจริงๆ

IDs เหล่านี้ถูกใช้โดย sub-levels ที่ถูกกฎหมายของเกม:

  • $00: โซนใต้น้ำของ 5-2/6-2 (ใช้โดย H-1, 39-1)
  • $01: น้ำของ 2-2/7-2 (Minus World, 36-1)
  • $02: sub-level ของ 8-4 (136-1, 151-1, 215-1)

ความแตกต่างระหว่างเลเวล "โบนัส" ที่เข้าถึงได้ตามปกติ กับ glitch world คือ warp zones ตรวจสอบโลกปัจจุบัน:

; Vérification warp zone (simplifié)
; Le jeu vérifie que le monde cible est entre 1 et 8
CheckWarp:
  lda TARGET_WORLD
  cmp #1
  bcc InvalidWarp       ; < 1 → refusé
  cmp #9
  bcs InvalidWarp       ; > 8 → refusé
  ; world valide entre 1 et 8 uniquement
  jmp DoWarp

Glitch worlds ที่มีหมายเลข > 8 หรือ 0 ไม่สามารถเข้าถึงได้ผ่านท่อปกติ ต้องใช้ wall clip หรือ cart swap

ทำไมเลเวลบางตัวถึง crash: jump tables

เมื่อเกมโหลดวัตถุ tile มันใช้ประเภทเป็น index ใน jump table:

; Jump table des objets tiles standards (types 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; type 0 : bloc ?, hidden, flagpole...
  .word Obj_Platform      ; type 1 : plateforme spéciale
  .word Obj_BrickRow      ; type 2 : rangée de briques
  .word Obj_BlockRow      ; type 3 : rangée de blocks
  .word Obj_CoinRow       ; type 4 : rangée de pièces
  .word Obj_BrickCol      ; type 5 : colonne de briques
  .word Obj_BlockCol      ; type 6 : colonne de blocks
  .word Obj_Pipe          ; type 7 : tuyau
  .word Obj_8             ; type 8
  .word Obj_9             ; type 9
  .word Obj_10            ; type 10
  .word Obj_11            ; type 11

Jump tables: ทำไมประเภทวัตถุที่ไม่ถูกต้องถึงทำให้เกม crash

ถ้าวัตถุมีประเภทที่ไม่ถูกต้อง (≥12) เกมจะกระโดดไปยังพอยน์เตอร์ที่ไม่มีอยู่ในตารางนี้ 4 ผลลัพธ์ที่เป็นไปได้:

  1. พอยน์เตอร์ที่ถูกต้อง → วัตถุโหลดปกติ
  2. พอยน์เตอร์ไปยัง jump table อื่น (ทับซ้อน) → วัตถุที่แตกต่างกันปรากฏ ตัวอย่าง: ประเภท 12 ชี้ไปยังตาราง Y=13 ซึ่งให้ L-pipe
  3. พอยน์เตอร์ไปยัง executable → เรียกใช้โค้ดแบบสุ่ม (crash ที่เป็นไปได้)
  4. Placeholder ที่ชัดเจน (NOP) → วัตถุไม่ทำอะไร (sprites บางตัวเป็นแบบนี้ สร้างศัตรูที่บินอยู่กับที่โดยไม่เคลื่อนที่)

Glitch level ID $58: sprite pointer ชี้ไปยังที่อยู่ที่ไม่ถูกต้อง เกม crash

Glitch level ID $50: อุโมงค์เมฆ, เลเวลที่สร้างจากข้อมูลที่เสียหาย

Glitch level ID $58 (อุโมงค์ที่ crash): sprite pointer ชี้ไปยังโซนหน่วยความจำที่ ไม่มีอยู่ใน NES โดยไม่มี mapper ROM เกมพยายามโหลด Koopa 5 ครั้งต่อเฟรมที่ตำแหน่ง (0,0) ซึ่งทำให้ PPU อิ่มตัวและเกิด freeze

; Pourquoi ID $58 crashe :
; Sprite pointer → adresse invalide (hors espace NES standard)
; → Le jeu lit des bytes indéterminés comme des types de sprite
; → Le Koopa $00 (type invalide sans handler) boucle en appel récursif
; → Stack overflow 6502 → freeze

ปริศนา pipe warp

จำการตรวจสอบ target_world BETWEEN 1 AND 8 ได้ไหม? แม้ว่าคุณจะพบท่อใน glitch world เกมจะตรวจสอบว่าโลกที่อยู่ระหว่าง 1 และ 8 Glitch worlds มีหมายเลข > 8 (36-1, 255-1...) ดังนั้น warp จึงล้มเหลว

นี่คือเหตุผลที่ Minus World ไม่มีจุดสิ้นสุด: flagpole ไม่มีอยู่ใน sprites และท่อไม่ได้นำไปที่ไหนเลย

เทคนิค 5 วัตถุในคอลัมน์เดียว

มี edge case ที่อนุญาตให้เกินขีดจำกัด 3 วัตถุต่อคอลัมน์ เมื่อคิวติดขัด (slots เต็ม + วัตถุถัดไปที่ไม่มี next screen flag) เกมจะ " prétraite" คอลัมน์ปัจจุบันในลูปจนกว่าจะพบวัตถุที่มี next screen flag ในแต่ละ prétraitement:

; Pendant le prétraitement de colonne :
; 1. Les objets dans la queue voient leur largeur restante
;    décrémentée à chaque "fausse avancée" de colonne
; 2. Si un objet atteint largeur=0, il quitte la queue
; 3. Un slot libéré peut être rempli par un nouvel objet
;    ajouté dans la même colonne

; Résultat : jusqu'à 5 objets peuvent être traités sur la même colonne.
; Technique : placer 2 objets qui traversent la screen boundary
; (slots 1 et 2), 1 objet dummy en X < précédent (bloque la queue),
; puis 3 objets à X=0 de l'écran suivant (dont un avec next screen flag).

นี่คือสิ่งที่เรียกว่า "queue skip" และมันถูกใช้โดย romhackers บางคนเพื่อสร้างเลเวลที่หนาแน่นกว่าที่รูปแบบปกติอนุญาต

ความแตกต่างระหว่างเวอร์ชัน

Famicom Disk System

เวอร์ชัน FDS ของ SMB1 มี memory map ที่แตกต่างกัน พอยน์เตอร์เลเวลทั้งหมดถูกเลื่อน แต่ข้อมูลเหมือนกัน สิ่งที่เปลี่ยน: ดัชนีของ glitch worlds แตกต่างกันโดยสิ้นเชิง:

FDS World 36 → Level ID $09 (eau version de 5-3)
  → Le flagpole est présent ! On peut finir le niveau.
  → Ensuite : $27 (7-3 normal) → $44 (4-4 underground)
  → $44 est finissable → la hache fonctionne → fin du jeu !
  
Le Minus World FDS est donc un "bonus world" qui peut mener
à la complétion du jeu, contrairement à la version NES.

เลเวล FDS ที่ฉันชอบ: ID $5F เวอร์ชันใต้ดินของครึ่งหลังของ 3-3 ในอุโมงค์ต่ำ (น่าเสียดายที่มันเป็น autoscroller)

The Lost Levels (Super Mario Bros. 2 ญี่ปุ่น)

Lost Levels เปลี่ยนแปลงหลายสิ่ง:

  1. ลำดับ tiles/sprites ที่เหมือนกัน: ไม่มี Frankenstein levels อีกต่อไป (tiles และ sprites โหลดเลเวลเดียวกันแม้กับ ID ที่ไม่ถูกต้อง)
  2. ตารางพอยน์เตอร์ 16 บิตเดียว แทนที่จะเป็นสองตารางแยก high/low
  3. 4 ไฟล์ดิสก์: ROM ถูกแบ่งสำหรับ FDS:
    • ไฟล์ 1: worlds 1-4
    • ไฟล์ 2: worlds 5-8
    • ไฟล์ 3: world 9 + sound engine
    • ไฟล์ 4: worlds A-D (ตารางพอยน์เตอร์ที่แตกต่างกันโดยสิ้นเชิง)
  4. Level ID เดียวกัน = 4 เลเวลที่เป็นไปได้ ตามไฟล์ที่โหลด
  5. ไม่มี glitch Tennis: ตัวเลือก continue (ดำเนินการต่อในโลกเดียวกันหลัง game over) ทำให้ warm start ไม่จำเป็น และเกม รีเซ็ตทันที ถ้า world > 9
  6. วัตถุใหม่: เห็ดพิษ, บล็อกที่มองไม่เห็น, บล็อกที่มองไม่เห็น fire flower, ท่อคว่ำ, ลม -- แต่แทรกอยู่กลางรายการที่มีอยู่ → ไม่เข้ากันย้อนหลัง กับ SMB1
  7. Piranha Plants แดงเสมอ หลัง world 4, สปริงบอร์ดเขียว เฉพาะใน worlds 2/B/3/C/7 เท่านั้น

Super Mario All-Stars (SNES)

พอร์ตโดยตรงพร้อมรูทีน 6502 เดียวกัน (SNES รันโค้ด NES ในโหมดที่เข้ากันได้):

  • Warp zone แก้ไข: ไม่มี Minus World อีกต่อไป (เข้าท่อซ้ายก่อนข้อความนำไปสู่โลกที่ถูกต้อง)
  • Planting: glitch levels ส่วนใหญ่ crash (ยกเว้น ID $6A และ 9-1)
  • วัตถุปราสาทเพิ่ม: ทำให้ไม่ซ้ำกันมากขึ้น
  • แต่: 4-2 wrong warp ยังใช้งานได้ (ไม่ได้แก้ไข!)

4-2 wrong warp: บั๊กการวางวัตถุ

ใน 4-2 มีวัตถุ pipe transition สองชิ้น: เถาวัลย์ (warp zone) และท่อ (coin cash room) วัตถุ transition ตัวแรก (ของเถาวัลย์) ถูกวาง ก่อนเถาวัลย์ปรากฏบนหน้าจอ ตัวที่สอง (ท่อ) ถูกวาง ช้าเกินไปในเลเวล

; Timing des transitions dans 4-2 :
; Objet transition 1 (vigne → warp zone) : placé 3 écrans avant la vigne
; Objet transition 2 (tuyau → coin cash) : placé 1 écran après le tuyau
;
; Normalement le premier objet est désactivé avant que Mario
; n'atteigne le tuyau. Mais si Mario va vite (ou utilise
; le raccourci du bloc B+right), la transition de la vigne
; est toujours active quand il touche le tuyau !
; → Le jeu charge la warp zone au lieu du coin cash.
;
; Si l'objet avait été placé juste après la vigne mais avant
; le tuyau, le bug n'existerait pas.

เลเวลแบบลูป

Loop (8-4, 7-4) ทำงานอย่างไร? เลเวลมี checkpoints พร้อมหมายเลขหน้าจอและตำแหน่ง Y ที่ hardcode:

; Checkpoint : {screen_number, vertical_position}
; Si Mario passe ce checkpoint à la bonne hauteur → niveau continue
; Sinon → warp back de 4 écrans (64 blocks)
;
; Pour faire une boucle infinie : vertical_position = $F0
; (en dessous du bas de l'écran) → impossible de valider.
;
; Les checkpoints sont simples (un seul flag) sauf pour world 7
; qui utilise des triplets (3 flags, il faut en échouer au moins 1)
;
; Le warp back est rude : offset de tile data réglé à une valeur
; hardcodée, offset de sprite data remis à 0. Les ennemis présents
; sont déchargés instantanément → les firebars disparaissent.

เปลี่ยนรูปแบบ ไม่ใช่โค้ด

บทเรียนที่น่าทึ่งที่สุดอย่างหนึ่งของสถาปัตยกรรมนี้คือ นักพัฒนา SMB1 ประสบความสำเร็จในการสร้างระบบเลเวลที่แสดงออกได้มาก โดยไม่เคยแตะโค้ดเรนเดอร์ 6502 การเปลี่ยนแปลงทั้งหมดระหว่างเลเวลมาจาก ข้อมูล (พอยน์เตอร์, วัตถุ, sprites, floor patterns) ไม่ใช่โค้ด

248 glitch worlds มีอยู่เพราะ ตารางพอยน์เตอร์มีขนาด 128 รายการ × 4 ประเภท และเกมไม่เคยตรวจสอบค่าที่อ่าน เมื่อพอยน์เตอร์ตกไปใน RAM เกมตีความ register ของ Mario เป็น tiles เมื่อพอยน์เตอร์ตกไปในข้อมูลเสียง เกมเล่นเพลงในรูปแบบ level design และเมื่อ jump tables overflow เกมรันอะไรก็ได้จนกว่าจะ crash

More Super Mario Bros. Mechanics Explained -- วิดีโอที่ 4

สิ่งที่เราเรียนรู้จากทั้งหมดนี้

  1. การแยก tiles/sprites: อิสระทั้งสองชั้นอย่างสมบูรณ์ ด้วยลำดับการจัดเก็บที่แตกต่างกันซึ่งสร้าง Frankenstein levels ที่ไม่ซ้ำกัน
  2. การบีบอัด RLE + ระบบวัตถุ: เลเวลไม่ใช่ bitmap แต่เป็นรายการวัตถุที่วาง พร้อม floor patterns สำหรับพื้น
  3. คิว 3 slots: ขีดจำกัดที่เข้มงวดของฮาร์ดแวร์ (และการออกแบบเลเวล)
  4. ไม่มีการตรวจสอบ: เกมไว้วางใจพอยน์เตอร์และ jump tables ซึ่งสร้าง glitch ที่เล่นได้ หรือ crash
  5. สูงสุด 256 byte: ขีดจำกัดของ Y register 6502 ซึ่งทำให้ข้อมูลทำซ้ำถ้าไปไกลเกินไป
  6. Warm start / cold start: ระบบ "ดำเนินการต่อ" ที่เปิดประตูสู่ cart swap Tennis → Mario

สิ่งที่สวยงามที่สุด: ทั้งหมดนี้คือโค้ด 6502 ที่บรรจุใน 40KB ไม่มีชั้นการจัด.abstract ไม่มีการตรวจสอบหน่วยความจำ ไม่มีตัวจัดการข้อผิดพลาด ถ้าพอยน์เตอร์เสีย เกม crash และ crash เรียกว่า glitch worlds

3 สิ่งที่ต้องจำ

  1. Glitch worlds คือพอยน์เตอร์ที่ตกไม่ดี -- เกมมี 128 IDs × 4 ประเภทพื้นที่ แต่มีเพียง 34 เลเวลที่ไม่ซ้ำกัน เมื่อหมายเลขโลกเสียหาย (จาก Tennis หรือ wall clip) เกมจะโหลดพอยน์เตอร์ที่ออกแบบสำหรับเลเวลอื่น และ 512 ชุดค่าผสมที่เป็นไปได้สร้างผลลัพธ์ที่ไม่คาดคิด

  2. Minus World คือบั๊ก warp รวมกับความเสียหาย -- ท่อซ้ายใน 1-2 ถ้าเปิดใช้งานก่อนข้อความปรากฏ จะโหลด world 36 (0x24) โลกนี้ชี้ไปยัง Level ID $01 (น้ำ 2-2) เลเวลที่ไม่มี flagpole และเพราะไม่มี pipe transition สำหรับ world 36 เลเวลจึงวนลูปไม่รู้จบ ความไม่มีการตรวจสอบสร้างไอคอน

  3. Tennis → Mario, 15 ปีก่อน OoT → Paper Mario -- RAM ของ NES รอดจากการสลับตลับ nhờตัวเก็บประจุและระบบ warm start / cold start ของ SMB1 ตัวนับก้าวของ Tennis (ที่เพิ่ม byte RAM ขณะเล่นเสียงก้าว) ตกไปที่ที่อยู่ของหมายเลขโลกพอดี หลักตัวเลขของ top score ต้องเป็น 0, byte $A5 ต้องสมบูรณ์ และเกมต้องตรวจจับ warm start -- การรวมกันของสถานการณ์ที่สมบูรณ์แบบซึ่งใช้งานได้เฉพาะกับ Tennis เท่านั้น

วิดีโอต้นฉบับของ Retro Game Mechanics Explained คืองานฝีมือระดับชั้นยอด -- ระดับรายละเอียดในการถอดรหัส 6502, แผนที่อัตโนมัติของทุกเลเวล, คำอธิบาย cart swap และ warm start ถ้าคุณยังไม่ได้ดูซีรีส์นี้ ดูมันสั้นและทุกนาทีมีคุณค่า

ซอร์สโค้ดของแผนที่มีอยู่บน rgmechex.com และการถอดรหัส SMB1 ที่สมบูรณ์เป็น open source ใน repos หลายแห่ง 40 ปีที่แล้ว โปรแกรมเมอร์ญี่ปุ่นเขียนระบบเลเวล 6502 นี้โดยไม่มี unit test และไม่มี bug tracker และเรายังเรียนรู้สิ่งใหม่ๆ อยู่ทุกวันเมื่อเปิดดูโค้ดของพวกเขา

Related Articles