GitHub avatar

Fox's Blog

Super Mario Bros.: the level format, pointers, and 256 glitch worlds

How 128 levels x 4 area types fit in 40KB of ROM, why the Minus World exists, and how a NES Tennis cartridge swap can load glitch worlds.

Introduction

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

But if you open an emulator and tweak the right bytes, you can load level 36-1. Or 255-1. Or land in a world made entirely of Bowser sprites and pipes that lead nowhere.

These 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 did a 4-part video series on this -- we're going to compile it all into one deep dive into the 6502 code of the best-selling game of its era.

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

World 9-1 -- the title screen of the first glitch world accessible via the Tennis cart swap

The warm start: why Tennis's RAM survives in SMB1

Before we talk about level storage, we need to understand how SMB1 boots. Because the NES Tennis cart swap glitch relies 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 the RAM. But when it detects a warm start (reset button, no power cycle), it preserves a 41-byte memory region:

; 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]

These 41 bytes serve a single purpose: allowing the player to continue on the same world after a game over. If you die in 6-3, the game writes world 6 into the start byte, and at the title screen, if you hold A + Start, you restart in 6-1.

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

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. 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

The $A5 byte check and top score digits check -- the heart of the warm start

Why a double check? Because the $A5 byte could be present by chance (another game leaving that value behind, or the default idle state of the RAM chip). By verifying that the top score digits are valid (0-9), it ensures the data is coherent.

Why Tennis is the only game that works

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

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

Then you swap to Tennis without turning off the console. Tennis:

  • Doesn't clean RAM on boot (few NES games do)
  • Doesn't write to the top score bytes → they stay at 0 (valid)
  • Doesn't touch the $A5 byte → it stays intact
  • Uses address $075F for the player's step counter
; 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).

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's footsteps)
  4. SMB1 detects a warm start → preserves the corrupted world byte
  5. Hold A + Start → world 9-1, world A-1, world 36-1, etc.

Why you have to boot Mario before Tennis

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

Tennis's step counter: each footstep increments the world byte

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

How SMB1 stores its levels in 40KB

Nintendo R&D4 had to solve a deceptively simple problem: represent levels that scroll horizontally with tiles, enemies, items, all within an ultra-tight ROM budget.

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

The tile layout (the level map)

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

The tile format uses a run-length system:

; 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

Each level contains 13 rows of 16 tile columns (13x16 = 208 visible tiles). But the compressed format allows for much smaller data -- for example, the sky and empty columns take up almost no space.

The 6502 rendering loop:

; 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

The sprite layout (enemies and objects)

In parallel, enemies and objects (question blocks, pipes, goombas, koopas) are stored in a completely separate structure. Each spawn is defined by 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

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

The pointer table

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

// 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 entries per table. 4 area types. 512 possible combinations, but only a fraction is used by the official game. The rest is uninitialized RAM or data being interpreted as pointers.

When the game loads a level, it does this:

; 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

No validation. No check that the pointer is valid. The game reads the address from the table and decompresses whatever is at that address, end of story.

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

The Level ID table: 128 possible entries, 34 assigned

The different ordering of tile and sprite pointers -- the cause of Frankenstein levels

The 34 unique levels and the 7-bit ID system

The NES RAM chip (MB8416A) -- it's the one that preserves data when you swap cartridges

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

  • Water (Type 0): 3 levels (2-2, 7-2, bonus area 5-2/6-2)
  • Overworld (Type 1): 22 levels (including the 2 cloud bonus rooms)
  • Underground (Type 2): 3 levels (including 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 within the subgroup, the 2 high bits = area type:

; 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 possible IDs ($00-$7F), only 34 assigned to real levels. Unused IDs point to whatever happens to be there.

The pointer tables: two lists, two orderings

The 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):

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)

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

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

; 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

To load level 6-2 (ID $23, Overworld number 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 ✓

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

; 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 + water sprites from 5-2

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 that gives the index of the first level in each world (1-8). And right after it, the table of 36 Level IDs for all levels in gameplay order.

; 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

When trying to load world 9, the game reads the 9th byte of WorldIndexTable... which doesn't exist. It overflows by 1 byte into LevelIDTable, reads the value $25, then uses $25 as an index into LevelIDTable (37th entry) -- which overflows by 2 bytes into SpriteOffsetTable, and reads the value 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 !

For world G (16), the overflow goes even further and lands on Level ID $01, which is the cutscene level that precedes 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

Why the glitch worlds exist

The game has 32 "legitimate" levels (8 worlds x 4 levels). But the pointer table has 128 entries per area type. Entries beyond level 32 contain whatever is at those ROM addresses -- sometimes another level, sometimes sound data, sometimes RAM, sometimes anything at all.

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

Level ID $01 + AreaType 0 = Minus World

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

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

The result: a water level that looks like 2-2, but loops forever because the flagpole doesn't exist. No level end, no exit.

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

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

; 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.

Pointers that point to RAM

When the tile pointer or sprite pointer points to a RAM address ($00-$7F) instead of ROM, the game tries to interpret the constantly changing RAM values as 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.

When the zero page changes (because Mario moves, the timer ticks, etc.), the level's "sprites" change too. That's why some glitch worlds have enemies that blink and constantly transform.

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. The tiles might be valid but the sprites point to a location that produces 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!)

Sprite pointer in RAM = undefined sprites. The game tries to display a Spiny ball or Bullet Bill blaster in the first row of tiles. It crashes immediately.

; 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

The 256 cataloged glitch worlds

RGMechEx wrote a script that generates maps of every level, for all 4 area types, and all 128 IDs each.

The world counter is 8-bit (0-255). Worlds 1-8 are legitimate. That leaves 248 potential glitch worlds. Each glitch world corresponds to the first level of that world, and its Level ID is calculated by the WorldIndexTable overflow mechanism.

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

Out of the 128 possible IDs, only 68 are "first levels" of a world (accessible via the glitch world number). The other 60 are level 2+ or inaccessible.

Type Playable unique IDs IDs that crash 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 the 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 sprite pointer $9F51 points to a region of ROM that's used as padding/sound data reused by many IDs.

Map of level ID $28 (Overworld) -- 2-1 tiles with normal sprites, 38 glitch worlds

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

The 6 truly unique glitch levels

Out of the 19 accessible glitch level IDs, only 6 don't crash immediately on load:

World Level ID Description
E-1 (224) $50 A single ? block over a chasm. Mario dies instantly.
W $57 Mario spawns stuck, unable to move.
42 (133) $50 Cloud tunnel that traps Mario if he goes far enough.
62 (131, 240) $4D Frozen castle: Mario spawns at the top, can't fall → stuck.
127 $4B Underground tunnel, but crashes if you go too far.
137 $4B Activates cutscene auto-scrolling. Mario meets a single brick block that blocks him forever.

Level ID $50 (cloud tunnel) -- glitch worlds 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 that produce something truly new. The rest are normal levels with the wrong area type, or black screens.

The level format in detail

Let's dive into the exact level data format, to understand why glitch levels hold up (or don't).

The level header: 2 bytes, 6 properties

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

; 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)

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

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

After the header comes a list of tile objects, each object is 2 bytes. The byte $FD marks the end of the list.

; 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

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

; 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

The queue holds 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 loaded.

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

Special Y positions: 12, 13, 14, 15

When Y=12, the object has no Y position (it's hardcoded by 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

When Y=13, two subgroups. If bit 6 of byte 1 is set:

; 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

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

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

Floor patterns: 16 ground motifs

The ground in levels isn't made of individual objects. SMB1 uses floor patterns, a background motif that applies to all columns until the next change:

; 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)

That's why pits are objects: they override the floor pattern on a specific column without having to change the pattern for everything else.

The 256-byte limit and the repeat

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

; 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.

Some glitch levels exploit this repeat to generate levels that last "indefinitely".

The sprite system: 2 bytes + pipe transitions

Sprites follow a similar format, but without a header and with a few key differences. The byte $FF marks the end of the list.

; 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

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

Y position 15 = screen skip (same as 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.

Sprites don't have a queue system. The only limit is that no more than 4 sprites can be loaded simultaneously in the spawn area (just off-screen to the right). Beyond that, sprites are ignored.

How to access the glitch worlds

There are two main methods.

The classic method: the wall clip

The wall clip (passing through walls) lets you get out of the normal level and walk to the hidden warp 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. Perform the wall clip on the right wall
  3. Walk through the void to the warp zone
  4. The game interprets the values as worlds

But this method only gives access to a small portion of the glitch worlds.

The extreme method: NES Tennis cart swap

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

The tinkerer's corner: code to explore everything

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

; 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 published the complete list of 128 levels x 4 types with auto-generated maps on rgmechex.com. Each entry shows the tile pointer, the sprite pointer, and a visual map of the level.

The most wtf 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 points to a region of ROM that contains data close enough to valid sprites to produce playable results, but combining the 3-4 castle tiles with overworld sprites creates an absurd rendering.

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

The absolute record. 38 glitch world entries point to the same level (2-1 tiles + $9F51 sprites). Why? Because the sprite pointer $9F51 falls into a ROM region 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 points to the ROM region reserved for the Famicom Disk System version. Result: a level with tiles that don't exist in the standard cartridge. It's the level that produces world 52-1 and 196-1.

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

These IDs are used by legitimate sub-levels in the game:

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

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

; 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 with numbers > 8 or 0 can't be reached through normal pipes. You need the wall clip or the cart swap.

Why some levels crash: the jump tables

When the game loads a tile object, it uses its type as an index into a 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

The 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 this 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, which produces an L-pipe.
  3. Pointer to executable code → execution of random code (probable crash)
  4. Explicit placeholder (NOP) → the object does nothing (some sprites are like this, producing enemies that hover in place without moving)

Glitch level ID $58: the sprite pointer points to an invalid address, the game crashes

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

Glitch level ID $58 (the crashing tunnel): its sprite pointer points to a memory region that doesn't exist on a NES without mapper ROM. The game tries to load the same Koopa 5 times per frame at position (0,0), which saturates the PPU and causes a 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

The pipe warp paradox

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

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

The 5-objects-in-one-column trick

There's an edge case that lets you bypass the 3-objects-per-column limit. When the queue gets stuck (slots full + next object missing the next screen flag), the game "preprocesses" the current column in a loop until it finds an object with the next screen flag. During each preprocessing pass:

; 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).

This is called a "queue skip" and it's used by some ROM hackers to create levels denser than the format normally allows.

The differences between versions

Famicom Disk System

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

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.

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

The Lost Levels (Super Mario Bros. 2 Japanese)

Lost Levels changes a lot of things:

  1. Same tiles/sprites ordering: 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 was 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 which file is loaded
  5. No more Tennis glitch: the continue option (continue on the same world after game over) makes the warm start unnecessary, and the game resets immediately if world > 9
  6. New objects: poison mushroom, invisible block, invisible fire flower, upside-down pipes, wind -- but inserted in the middle of existing lists → backward incompatibility with SMB1
  7. Piranha Plants always red after world 4, springboards green 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 a compatible mode):

  • Warp zone fixed: no more Minus World (entering the left pipe before the text leads to the correct world)
  • Crashing: most glitch levels crash (except ID $6A and 9-1)
  • Castle objects added: more unique renderings
  • But: the 4-2 wrong warp still works (not 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) is placed well before the vine appears on screen. The second (the pipe) is placed too late in the level.

; 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.

The loop levels

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

; 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.

Changing the format, not the code

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

The 256 glitch worlds exist because the pointer tables are sized for 128 entries x 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 in the form of level design. And when the jump tables overflow, the game executes anything until it crashes.

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

What we can learn from all this

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

The best part: all of this is 6502 code that fits in 40KB. No abstraction layer, no memory access validation, no exception handler. If the pointer is garbage, the game crashes. And the crashes, we call them glitch worlds.

The 3 key takeaways

  1. Glitch worlds are pointers gone wrong -- The game has 128 IDs x 4 area types, but only 34 unique levels. When the world number is corrupted (by Tennis or wall clip), the game loads a pointer designed for a different level, and the 512 possible combinations 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). This world points to Level ID $01 (water from 2-2), a level with no flagpole. And since there's no pipe transition for world 36, the level loops forever. The lack of validation creates the icon.

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

The original videos by Retro Game Mechanics Explained are an absolute labor of love -- the level of detail on the 6502 disassembly, the auto-generated maps of every level, the cart swap and warm start explanations. If you haven't watched the series, check it out, it's short and every minute is dense.

The map source code is available on rgmechex.com, and the complete SMB1 disassembly is open source across many repos. 40 years ago, Japanese programmers wrote this level system in 6502 with zero unit tests and zero bug trackers, and we're still learning stuff 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

Pas de validation. Pas de check que le pointeur est valide. Le jeu 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. Pas de fin de niveau, pas de sortie.

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. Pas de validation : 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. Pas de couche d'abstraction, pas de validation d'accès mémoire, pas de gestionnaire d'exceptions. 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 trucs en ouvrant leur code aujourd'hui.

Super Mario Bros.:关卡格式、指针与256个故障世界

128个关卡 × 4种区域类型如何装进40KB ROM、Minus World为何存在,以及一场NES Tennis卡带交换如何能加载故障世界。

引言

Super Mario Bros.,只有40KB ROM。八个世界,32个关卡,敌人、音乐、道具,全都在里面。

但如果你打开模拟器,修改正确的字节,你就能加载第36-1关。或者255-1关。甚至进入一个满屏都是Bowser精灵和通往虚空的水管的故障世界。

这些glitch world存在的原因很简单:SMB1的关卡存储系统是8位优化的杰作,而当你强制游戏在不该读取的地方读取数据时,就会产生令人着迷的结果。

Retro Game Mechanics Explained制作了一个4集系列视频----我们将把它们整合为一次对这个时代最畅销游戏6502代码的深入探索。

GLITCH OBJECTS -- RGMechEx关于SMB1隐藏机制的系列标题

World 9-1 -- 通过卡带交换Tennis可访问的第一个故障世界的标题画面

Warm Start:为什么Tennis的RAM能在SMB1中存活

在讨论关卡存储之前,需要先了解SMB1如何启动。因为NES Tennis卡带交换的glitch完全依赖于游戏的warm start / cold start检测系统。

被保留的41字节

当SMB1检测到cold start(首次通电或关机再开机)时,它会清除所有RAM。但当它检测到warm start(按重置键,没有断电)时,它会保留一个41字节的内存区域:

; Warm start时RAM中保留的41字节
; 地址 $075F-$0787
;
; $075F : 启动字节 (world - 1)    [1 byte]
; $0760 : 世界选择标志 (B button) [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重新开始。

Warm start时RAM中保留的41字节 -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, 和魔法字节 $A5

Warm start的双重验证

Cold start vs warm start -- 重置检测流程图

当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),可以确保数据是一致的。

为什么只有Tennis能用

当我们首次插入SMB1(cold start)时,游戏会:

  1. 清除所有RAM → 最高分 = 0,世界字节 = 0
  2. 在地址$0787写入$A5

然后我们不关机直接换上Tennis。Tennis:

  • 启动时不清理RAM(很少有NES游戏这么做)
  • 不写入最高分的字节 → 它们保持为0(有效)
  • 不修改$A5字节 → 它保持不变
  • 使用地址$075F 作为玩家的脚步计数器
; Tennis中的脚步递增机制:
; 每当玩家在球场上走一步,Tennis会递增$075F处的字节。
; SMB1使用同一个字节作为 "世界编号 - 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检测到warm start → 保留损坏的世界字节
  5. 按住A + Start → world 9-1, world A-1, world 36-1, 等等

为什么必须先启动Mario再启动Tennis

一个微妙之处:必须先启动SMB1,再启动Tennis,然后再启动SMB1。如果你直接从Tennis开始,$A5字节永远不会被写入(Tennis不会写入$A5),因此warm start检测会失败,RAM会被清除。

Tennis的脚步计数器:每个footstep递增世界字节

通过NES Tennis访问故障世界 -- 解释卡带交换的视频

SMB1如何在40KB中存储关卡

Nintendo R&D4必须解决一个表面上很简单的问题:用有限的ROM预算表示水平滚动的关卡,包含图块、敌人、道具。

解决方案是将数据分为完全独立的两层:

Tile layout(关卡地图)

每个关卡由一个指向ROM中压缩图块结构的指针定义。压缩方式简单但精妙:一个"控制"字节后跟1-3个数据字节。

Tile格式使用游程编码(RLE-like)系统:

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

示例:绘制3个连续的砖块:
  $82 $01    ; 重复tile $01 (brick) 3次

每个关卡包含13行 × 16列的图块(13×16 = 208个可见图块)。但压缩格式可以大幅减少数据量----例如天空和空白列几乎不占空间。

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 : 游程编码
  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字节定义:

; SMB1的sprite格式
; Byte 0 : X位置(列)
; Byte 1 : sprite类型 + Y页面位
; Y由序列中的索引推导

一个sprite序列:
  $01 $4B    ; goomba在第1列
  $09 $4B    ; goomba在第9列
  $10 $61    ; ?方块在第16列(含金币)
  $15 $54    ; 绿色koopa在第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 = 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       ; 指向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,6-2的水下版本

Level ID表:128个可能的条目,34个已分配

Tile和sprite指针的不同顺序 -- Frankenstein关卡的成因

34个独立关卡和7位ID系统

NES的RAM芯片 (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个过场房间(地下/水下关卡之前)
  • + 1个4-2的warp zone

每个关卡有一个7位的ID。低5位=子组内的编号,高2位=区域类型:

; Level ID的7位编码
; Bit 6-5 : 类型 (00=Water, 01=Overworld, 10=Underground, 11=Castle)
; Bit 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指向任何地方。

指针表:两个列表,两种顺序

Tile和sprite指针的存储顺序不同。代码使用两个独立的16位列表(高位字节/低位字节分别在两个不同的表中):

Sprite指针的顺序:
  索引 0-5   : Castle (6个关卡)
  索引 6-27  : Overworld (22个关卡)
  索引 28-30 : Underground (3个关卡)
  索引 31-33 : Water (3个关卡)

Tile指针的顺序:
  索引 0-2   : Water (3个关卡)
  索引 3-24  : Overworld (22个关卡)
  索引 25-27 : Underground (3个关卡)
  索引 28-33 : Castle (6个关卡)

为什么顺序不同?没有技术原因----可能只是开发过程中数据的组织方式。但这产生了一个令人着迷的后果:当关卡ID无效时,tile和sprite指针会加载不同的关卡,创造出Frankenstein关卡。

为了在这两个列表之间导航,游戏使用小型偏移表(就像目录一样):

; 按类型划分的偏移表 (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偏移 = SpriteOffsetTable[1] = 6
;    最终索引 = 6 + 3 (关卡编号) = 9 → 第10个sprite指针
; 3. Tile偏移 = TileOffsetTable[1] = 3
;    最终索引 = 3 + 3 = 6 → 第7个tile指针
; 4. 结果:tile指针 $A619 + sprite指针 $9ED0 = 6-2 ✓

那么当ID无效时,比如$43(Underground编号3,实际上不存在)会怎样?

; ID $43, 类型 = 10 (Underground), 编号 = 3
; Sprite偏移 = SpriteOffsetTable[2] = $1C = 28
;   索引 = 28 + 3 = 31 → 第32个sprite指针 = 水下奖励5-2!
; Tile偏移 = TileOffsetTable[2] = $19 = 25
;   索引 = 25 + 3 = 28 → 第29个tile指针 = 1-4 (Castle)!
;
; 结果:一个地下关卡使用了1-4的tiles
; 和5-2水下区域的Bloopers。一个真正的Frankenstein。

Level ID $43 -- Frankenstein关卡:tiles 1-4 + sprites水下5-2

探索故障关卡指针 -- 偏移表详解

World index表 -- 当world 9溢出时创建的故障关卡

World index表:为什么world 9会溢出

有一个8字节的ROM表,给出每个世界(1-8)第一个关卡的索引。紧随其后的是所有36个关卡的Level ID表,按游戏顺序排列。

; 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个字节……但它不存在。它溢出1个字节到LevelIDTable,读取值$25,然后将$25作为LevelIDTable中的索引(第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的tiles
;   5. Sprite指针 = 索引 31+6 = 37 > 33 → 无效指针
;   6. 结果:水下的6-2加上glitch sprites
;      → world 9-1!

对于world G(16),溢出更远,落入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个关卡)。但指针表每个区域类型有128个条目。第32个关卡之后的条目包含ROM中这些地址处的数据----有时是另一个关卡,有时是音效数据,有时是RAM,有时是任何东西。

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

Level ID $01 + AreaType 0 = Minus World

最著名的故障世界。Level ID $01在AreaType 0(水下)指向:

  • Tile指针:$AE45 → 2-2/7-2的水下区域
  • Sprite指针:$A171 → 2-2/7-2的sprites

结果:一个看起来像2-2的水下关卡,但因为flagpole不存在而无限循环。没有关卡结束,没有出口。

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

SMB1的warm start检查 -- 它使Minus World得以存在

; Minus World中flagpole缺失的原因:
; 2-2/7-2的sprites ($A171) 中没有flagpole。
; 游戏搜索sprite $FD (flagpole) 但永远找不到 → 无限循环
;
; 游戏继续无限生成关卡
; 直到计时器归零。

指向RAM的指针

当tile指针或sprite指针指向RAM地址($00-$7F)而非ROM时,游戏会尝试将RAM的持续变化解释为tiles:

; 示例:Level ID $03 在 Water
; Tile指针:$A46B (3-3 - 有效)
; Sprite指针:$009D (指向零页RAM!)
;
; 零页RAM包含游戏的寄存器、
; Mario的位置、计数器状态...
; 游戏将其解压缩为sprite序列,
; 结果是一个敌人实际上是
; 寄存器值的关卡。

当零页变化时(因为Mario移动、计时器运行等),关卡的"sprites"也会变化。这就是为什么某些故障世界的敌人会不断闪烁和变形。

Level ID $03 Water -- sprite指针 $009D 指向RAM,关卡不可玩

Level ID $36:空关卡(Overworld)

Level ID $36在Overworld:

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

结果:什么都没有。游戏加载了关卡,但在RGMechEx的目录中标记为"无关卡"。tiles可能是有效的,但sprites指向一个产生空关卡或不可用关卡的位置。

Level ID $1D (Castle):崩溃之王

Level ID $1D在Castle:

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

Sprite指针在RAM中 = 未定义的sprites。游戏尝试在第一行tiles中显示Spiny ball或Bullet Bill blaster。这会立即崩溃。

; 当sprite指针指向RAM时,
; 游戏解压缩不断变化的字节
; 作为"生成"指令。结果:
; - 出现不存在的物体(未定义的值)
; - 当NES sprite尝试显示无效tile时PPU崩溃
; - 控制台完全死机

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指针 $A7CD (2-1)----出现在38个不同的故障世界中,因为其sprite指针 $9F51指向一个被用作填充/音效数据的ROM区域,许多ID共用这段数据。

关卡 ID $28 (Overworld) 的地图 -- 2-1 tiles 加上正常sprites,38个故障世界

Super Mario Bros. Glitch Levels Explained -- 第三个视频

真正独特的6个故障关卡

在19个可访问的glitch level ID中,只有6个不会在加载时立即崩溃:

World Level ID 描述
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 (云端隧道) -- 故障世界 42-1 和 E-1 Level ID $4D (城堡) -- world 62-1,Mario出生时被卡住 Level ID $4B (隧道) -- world 127-1,走得太远会崩溃

248个故障世界中只有6个产生了真正新的东西。其余的是使用了错误区域类型的正常关卡,或者是黑屏。

关卡格式详解

深入了解关卡数据的精确格式,理解故障关卡为何能运作(或不能)。

关卡头部:2字节,6个属性

每个关卡以一个2字节的头部开始,控制6个属性:

; Byte 0 : 计时器 + Y起始 + 修改器
;   Bit 7-6 : 计时器 (00=不变, 01=200, 10=300, 11=400)
;   Bit 5-3 : Mario的Y起始 (111/110 = 自动行走)
;   Bit 2-0 : 关卡类型修改器
;              000=默认, 001=波浪, 010=砖墙,
;              011=水底, 100=夜晚, 101=雪,
;              110=雪夜, 111=灰色夜晚

; Byte 1 : 平台 + 背景 + 地面图案
;   Bit 7-6 : 特殊平台 (00=树, 01=蘑菇,
;                         10=Bullet Bill, 11=云)
;   Bit 5-4 : 背景 (00=无, 01=云,
;                   10=山, 11=栅栏)
;   Bit 3-0 : 初始地面图案 (0-15)

类型修改器控制视觉变化:水关卡顶部的波浪、8-3的砖块背景、4-3的夜间调色板、6-2的雪景等等。

Tile物体:2字节,Next Screen Flag,3槽队列

头部之后是tile物体列表,每个物体2字节。字节$FD标记列表结束。

; Tile物体格式 (16位) :
; Byte 0 :
;   Bit 7-4 : X位置 (列 0-15)
;   Bit 3-0 : Y位置
;     Y=0-11  : 正常Y位置
;     Y=12    : 特殊物体(洞、桥、绳子、?方块)
;     Y=13    : 屏幕跳转 / 特殊物体2
;     Y=14    : 修改器/场景/地面切换
;     Y=15    : 特殊物体3(城堡、楼梯、大水管)

; Byte 1 :
;   Bit  7   : NEXT SCREEN FLAG
;   Bit 6-4 : 物体类型 (0-7)
;   Bit 3-0 : 宽度/高度 / 子类型

当"next screen"位置位时,当前工作列增加1。这允许在前16列之外放置物体。物体必须按顺序列出(从左到右),因为游戏是顺序加载的:

; 加载例程每列有两阶段:
; 阶段1:查找从此列开始的新物体
;         并将它们添加到队列中
; 阶段2:处理队列中的每个物体(绘制tiles),
;         并移除在此列结束的物体

队列正好有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时,有两个子组。如果byte 1的bit 6为1:

; Y=13, bit6=1 : 特殊物体
;   0 = L-pipe (过场), 1 = flagpole, 2-3 = 桥/斧头/锤子(城堡结尾)
;   4 = 停止屏幕, 5 = 随机敌人, 6 = 循环关卡, 7+ = 可能崩溃

如果bit6=0,低5位编码一个屏幕跳转(直接跳到第N个屏幕,不用一个个通过next screen flag)。

当Y=14时:相同原理,bit6=1更改类型修改器,bit6=0更改背景+地面图案。

地面图案:16种地面模式

关卡的地面不是由单个物体组成的。SMB1使用地面图案,一个应用于所有列的背景模式,直到下一次更改:

; 地面图案 (4位 = 16种可能)
;   0 = 完全空白
;   1 = 2个tile高的地面
;   2 = 1个tile高的地面
;   3 = 地面 + 底部
;   4 = 地面 + 底部2
;   5 = 1/2 tile高的地面
;   6 = 3/4 地面
;   ... 直到 15 = 完全填充(地面 + 天花板)

这就是为什么洞是物体:它们在特定列覆盖地面图案,而不必为其余部分更改图案。

256字节限制和repeat

一个关卡的所有tile数据最多占256字节。6502的Y寄存器用作索引,它是8位的。如果游戏到达数据末尾时未找到$FD字节,它会回到开头并无限重复256字节:

; 索引Y = 8位 → 最多256字节的tile数据
; 如果Y溢出 (255 → 0) 且未遇到 $FD → 重复
; sprites也是如此,但水管物体 (3字节)
; 会在每次加载时改变索引的奇偶性。

某些故障关卡利用这个repeat来生成"无限持续"的关卡。

Sprite系统:2字节 + 水管过渡

Sprites使用类似的格式,但没有头部,且有一些关键差异。字节$FF标记列表结束。

; Sprite格式 (2字节) :
; Byte 0 : X位置 (列)
; Byte 1 :
;   Bit 7 : NEXT SCREEN FLAG
;   Bit 6-0 : sprite类型
;       某些类型包括:goomba, koopa, Blooper,
;       Bullet Bill, Lakitu, Spiny, 平台,
;       warp zone命令, toad/公主,
;       敌人群生成命令

Byte 1的最低位是hard level flag:如果置1,该sprite只出现在≥ 5-3的关卡中。这就是"hard mode"关卡的创建方式。

Y位置15 = 屏幕跳转(与tiles相同)。Y位置14 = 水管过渡(3字节):

; Sprite Y=14 : 水管/藤蔓过渡 (3字节!)
;   Byte 0 : X位置
;   Byte 1 : bit 6-0 = 7位Level ID (目的地)
;   Byte 2 : bit 4-0 = 目标屏幕
;            bit 7-5 = 该过渡有效的世界
;
; 为什么要指定世界?奖励房间在不同世界之间复用。
; 例如:1-1的奖励房间也被2-1和7-1使用。
; 这个房间有3个过渡,每个世界一个,
; 让Mario在正确的位置重新出现。

Sprites没有队列系统。唯一的限制是在spawn区域(屏幕右侧外)中不能同时加载超过4个sprites。超过的sprites会被忽略。

如何访问故障世界

有两种主要方法。

经典方法:wall clip

Wall clip(穿墙)允许你离开正常关卡,走到隐藏的warp zone。通过RAM操纵世界计数器,你可以加载任何Level ID。

技术要点:

  1. World 1-2:进入隐藏的结尾水管
  2. 在右侧墙壁做wall clip
  3. 走到空白区域直到warp zone
  4. 游戏将值解释为世界

但这种方法只能访问一小部分故障世界。

极端方法:NES Tennis卡带交换

参见上方"Warm Start"部分了解完整细节。简而言之:Tennis的脚步计数器写入与SMB1启动世界相同的RAM字节,warm start检测保留了这个值。

修改者专区:全面探索的代码

如果你想在模拟器中自行探索所有故障关卡,可以直接修改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
; 然后进入warp水管或死亡后重新开始
; → 游戏在Overworld加载关卡ID $39

RGMechEx在 rgmechex.com 上发布了完整的128关卡 × 4类型列表及自动生成的地图。每个条目显示tile指针、sprite指针和关卡的可视化地图。

最离谱的关卡

Level ID $1F (Water):15个故障世界合而为一

Tile指针 $A302 (3-4) 加上sprite指针 $02A0 产生15个不同的故障世界(D-1, J-1, Y-1, Z-1, 55-1, 73-1...)。解释:sprite指针指向一个ROM区域,其中的数据足够接近有效sprites,能产生可玩的结果,但城堡3-4的tiles与overworld sprites的组合创造了荒谬的渲染。

Level ID $28 (Overworld):38个故障世界 = 纪录

绝对纪录。38个故障世界条目指向同一个关卡(2-1 tiles + $9F51 sprites)。为什么?因为sprite指针 $9F51落在一个被用作填充/音效数据的ROM区域,许多ID共用这段数据。

Level ID $49 (Underground):FDS关卡

Tile指针 $76AE + sprite指针 $1C9D。Tile指针指向为Famicom Disk System版本保留的ROM区域。结果:一个包含标准卡带中不存在的tiles的关卡。这就是产生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 zone会验证当前世界:

; 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或卡带交换。

为什么某些关卡崩溃:Jump Table

当游戏加载tile物体时,它使用物体类型作为跳转表中的索引:

; 标准tile物体跳转表 (类型 0-11)
JumpTable_TileObjects:
  .word Obj_Special       ; 类型 0 : ?方块, 隐藏方块, 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 Table:为什么无效的物体类型会导致游戏崩溃

如果物体有一个无效类型(≥12),游戏会跳转到一个在此表中不存在的指针。4种可能的结果:

  1. 有效指针 → 物体正常加载
  2. 指向另一个跳转表的指针(重叠) → 出现不同的物体。例如:类型12指向Y=13的表,这会产生一个L-pipe。
  3. 指向可执行代码的指针 → 执行随机代码(可能崩溃)
  4. 显式占位符(NOP) → 物体不做任何事(某些sprites就是这样,在原地飘动而不移动的敌人)

故障关卡 ID $58:sprite指针指向无效地址,游戏崩溃

故障关卡 ID $50:云端隧道,由损坏数据生成的关卡

故障关卡ID $58(会崩溃的隧道):其sprite指针指向一个在无mapper ROM的NES上不存在的内存区域。游戏尝试以每帧5次的频率在位置(0,0)加载同一个Koopa,导致PPU饱和并死机。

; ID $58 崩溃的原因:
; Sprite指针 → 无效地址(超出NES标准空间)
; → 游戏将不确定的字节读取为sprite类型
; → $00 Koopa(无处理程序的无效类型)进行递归调用循环
; → 6502栈溢出 → 死机

水管warp悖论

还记得检查 target_world BETWEEN 1 AND 8 吗?即使你在故障世界中找到一个水管,游戏也会验证目标世界在1到8之间。故障世界的编号> 8(36-1, 255-1...),因此warp会失败。

Minus World没有终点也是因为这个:flagpole不在sprites中,水管也不通向任何地方。

每列5个物体的技巧

存在一个边缘情况,可以突破每列3个物体的限制。当队列阻塞(槽位已满 + 下一个物体缺少next screen flag)时,游戏会在当前列"预处理"循环直到找到带有next screen flag的物体。在每次预处理期间:

; 在列预处理期间:
; 1. 队列中的物体在每次"假进列"时
;    宽度递减
; 2. 如果物体达到宽度=0,它会离开队列
; 3. 释放的槽位可以被添加到同一列的
;    新物体填充

; 结果:同一列上最多可以处理5个物体。
; 技巧:放置2个跨越屏幕边界的物体
; (槽位1和2),1个X < 前一个的虚拟物体(阻塞队列),
; 然后3个X=0的下一个屏幕的物体(其中一个是next screen flag)。

这被称为"队列跳转",被某些romhackers用来创建比格式正常允许的更密集的关卡。

不同版本之间的差异

Famicom Disk System

SMB1的FDS版本有不同的内存映射。所有关卡指针都偏移了,但数据是相同的。变化之处:故障世界的索引完全不同:

FDS World 36 → Level ID $09 (5-3的水下版本)
  → flagpole存在!可以完成关卡。
  → 接着:$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. Tile/sprite相同顺序:不再有Frankenstein关卡(即使ID无效,tiles和sprites也加载相同的关卡)
  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 glitch:continue选项(game over后继续到相同世界)使warm start变得不必要,如果world > 9游戏会立即重置
  6. 新物体:毒蘑菇、隐形方块、隐形火力花方块、倒置水管、风----但插入在现有列表中间 → 与SMB1不兼容
  7. Piranha Plants在world 4后总是红色的,springboards只在world 2/B/3/C/7是绿色的

Super Mario All-Stars (SNES)

直接移植,使用相同的6502例程(SNES以兼容模式执行NES代码):

  • Warp zone修复:不再有Minus World(在文本前进入左侧水管到达正确世界)
  • 崩溃:大多数故障关卡崩溃(ID $6A和9-1除外)
  • 城堡物体添加:渲染更加独特
  • 但是:4-2错误warp仍然有效(未修复!)

4-2错误warp:一个物体放置bug

在4-2中,有两个水管过渡物体:藤蔓(warp zone)和水管(金币房间)。第一个过渡物体(藤蔓的)放置在藤蔓出现在屏幕上之前很远的位置。第二个(水管的)放置在关卡中太晚的位置。

; 4-2中的过渡时序:
; 过渡物体1 (藤蔓 → warp zone):在藤蔓之前3个屏幕放置
; 过渡物体2 (水管 → 金币房间):在水管之后1个屏幕放置
;
; 通常第一个物体会在Mario到达水管前被禁用。
; 但如果Mario速度很快(或使用B+右方块的快捷方式),
; 藤蔓的过渡在他碰到水管时仍然有效!
; → 游戏加载warp zone而非金币房间。
;
; 如果物体放在藤蔓之后但水管之前,
; 这个bug就不会存在。

循环关卡

Loop关卡(8-4, 7-4)如何工作?关卡有检查点,包含硬编码的屏幕编号和Y位置:

; 检查点:{screen_number, vertical_position}
; 如果Mario以正确的高度通过此检查点 → 关卡继续
; 否则 → 回退4个屏幕 (64 blocks)
;
; 要实现无限循环:vertical_position = $F0
; (在屏幕底部下方) → 无法通过。
;
; 检查点很简单(只有一个flag),除了world 7
; 它使用三元组(3个flag,至少要失败1个)
;
; 回退很粗暴:tile data偏移设置为硬编码值,
; sprite data偏移重置为0。存在的敌人
# 立即卸载 → firebars消失。

改变格式,不改变代码

这个架构最令人着迷的教训之一是,SMB1的开发者在不修改6502渲染代码的情况下,创造了一个非常有表现力的关卡系统。所有关卡之间的变化都来自数据(指针、物体、sprites、地面图案),而非代码。

256个故障世界存在是因为指针表为128个条目 × 4种类型做了空间分配,且游戏从不验证读取的值。当指针落在RAM中时,游戏将Mario的寄存器解释为tiles。当指针落在音效数据中时,游戏将音乐当作关卡设计来播放。当jump table溢出时,游戏执行任何东西直到崩溃。

More Super Mario Bros. Mechanics Explained -- 第四个视频

从中可以学到什么

  1. Tile/Sprite分离:两层完全独立,不同的存储顺序创造了独特的Frankenstein关卡
  2. RLE压缩 + 物体系统:关卡不是位图而是放置的物体列表,使用地面图案处理地面
  3. 3槽队列:硬件(和关卡设计)的严格限制
  4. 无验证:游戏信任指针和jump table,这产生了可玩的glitch或崩溃
  5. 最大256字节:6502 Y寄存器的限制,使得数据在走得太远时会重复
  6. Warm start / cold start:一个"继续"系统为Tennis卡带交换 → Mario打开了大门

最精彩的是:这一切都是40KB中的6502代码。没有抽象层,没有内存访问验证,没有异常处理器。如果指针是垃圾,游戏就崩溃。而这些崩溃,我们称之为故障世界。

3个要点

  1. 故障世界是落在错误位置的指针 -- 游戏有128个ID × 4种区域类型,但只有34个独立关卡。当世界编号被损坏(通过Tennis或wall clip)时,游戏加载一个为其他关卡设计的指针,512种可能组合产生不可预测的结果。

  2. Minus World是warp bug加上损坏 -- 1-2中的左侧水管,如果在文本出现前激活,会加载world 36 (0x24)。这个world指向Level ID $01(2-2的水下),一个没有flagpole的关卡。由于world 36没有水管过渡,关卡无限循环。缺少验证创造了这个标志性的glitch。

  3. Tennis → Mario,比OoT → Paper Mario早15年 -- NES的RAM在卡带交换中存活,得益于电容器和SMB1的warm start / cold start系统。Tennis的脚步计数器(通过播放脚步音效递增RAM字节)恰好落在世界编号的地址上。需要最高分数字保持为0、$A5字节完好、且游戏检测到warm start----一个只与Tennis一起成功的完美巧合。

Retro Game Mechanics Explained 的原始视频是令人叹为观止的细致工作----6502反汇编的详细程度、所有关卡的自动地图、卡带交换和warm start的解释。如果你还没看过这个系列,去看,很短,每一分钟都是干货。

地图的源代码在 rgmechex.com 上,SMB1的完整反汇编是开源的,在多个仓库中。40年前,日本程序员用6502编写了这个关卡系统,没有单元测试,没有bug追踪器,而我们今天打开他们的代码仍然能学到东西。

Super Mario Bros.:レベルフォーマット、ポインタ、そして256のグリッチワールド

128レベル × 4タイプのエリアがどうやって40KBのROMに収まるのか、Minus Worldがなぜ存在するのか、そしてNESテニスのカートリッジ交換でグリッチワールドをロードできる仕組み。

はじめに

Super Mario Bros.はROM 40キロバイト。8つのワールド、32のレベル、敵、音楽、パワーアップ、全部これに収まっている。

でもエミュレーターを開いて適当なバイトをいじれば、レベル36-1をロードできる。255-1もだ。Bowserのスプライトとどこにも繋がらないパイプだけで構成されたワールドに着地することもできる。

これらのグリッチワールドが存在するのは単純な理由によるものだ。SMB1のレベル保存システムは8ビットの最適化の傑作であり、ゲームに本来読むべきでない場所を読ませると、興味深い結果が生まれる。

Retro Game Mechanics Explainedがこれについて4本の動画シリーズを制作した -- 当時最も売れたゲームの6502コードを1本の深掘りにまとめよう。

GLITCH OBJECTS -- SMB1の隠しメカニズムに関するRGMechExシリーズのタイトルカード

World 9-1 -- カートリッジ交換テニスでアクセスできる最初のグリッチワールドのタイトル画面

ワームスタート:なぜテニスのRAMがSMB1で生き残るのか

レベルの保存について話す前に、SMB1がどのように起動するかを理解する必要がある。NESテニスのカートリッジ交換グリッチは、ゲームのワームスタート/コールドスタート検出システムに完全に依存しているからだ。

保持される41バイト

SMB1がコールドスタート(初回の電源投入または電源オフ→オン)を検出すると、すべてのRAMを消去する。しかしワームスタート(リセットボタン、電源切断なし)を検出すると、41バイトのメモリ領域を保持する:

; 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バイトは単一の機能のために使われる:ゲームオーバー後に同じワールドを続けること。6-3で死ぬと、ゲームはワールド6を起動バイトに書き込み、タイトル画面でA + Startを長押しすると6-1から再開できる。

ワームスタート時にRAMに保持される41バイト -- TOP SCORE、MARIO SCORE、TIMER、WORLD SELECT、CONTINUE WORLD、そしてマジックバイト$A5

ワームスタートの二重チェック

コールドスタート vs ワームスタート -- リセット検出のフローチャート

SMB1は起動時に1つの条件だけでなく2つを確認する:

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

バイト$A5とトップスコアの桁のチェック -- ワームスタートの中核

なぜ二重チェックなのか? バイト$A5が偶然存在する可能性があるからだ(別のゲームがこの値を残した、あるいはRAMチップのデフォルトの待機状態など)。トップスコアの桁が有効(0-9)であることを確認することで、データが整合していることを保証している。

なぜテニスだけが機能するのか

SMB1を初めて挿入すると(コールドスタート)、ゲームは:

  1. すべてのRAMを消去 → トップスコア = 0、ワールドバイト = 0
  2. アドレス$0787に$A5を書き込む

その後、コンソールを消さずにテニスに切り替える。テニスは:

  • 起動時にRAMをクリーンアップしない(NESのほとんどのゲームは这样做らない)
  • トップスコアのバイトに書き込まない → 0のまま(有効)
  • バイト$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. バイト$A5はまだthere(テニスが触っていない)
  2. トップスコアの桁はまだ0(有効)
  3. ワールドバイトは現在8以上(テニスの歩数でインクリメントされた)
  4. SMB1がワームスタートを検出 → 破損したワールドバイトを保持
  5. A + Startを長押し → world 9-1、world A-1、world 36-1など

なぜ先にMarioを起動してからテニスに切り替える必要があるのか

細かい点がある:まずSMB1を起動し、次にテニス、そして再度SMB1を起動する必要がある。最初からテニスを始めた場合、バイト$A5は書き込まれない(テニスは$A5を書き込まない)。そのためワームスタート検出に失敗し、RAMが消去されてしまう。

テニスの歩数カウンター:各footstepがワールドバイトをインクリメントする

NESテニスでグリッチワールドにアクセスする -- カートリッジ交換を説明する動画

SMB1が40KBにレベルを保存する方法

Nintendo R&D4は一見単純な問題を解決する必要があった:水平スクロールするタイル、敵、アイテムを備えたレベルを、ROMの超strictな予算内で表現することだ。

解決策は、完全に独立した2つのデータレイヤーに分離することだった:

タイルレイヤー(レベルの地図)

各レベルはROM内の圧縮されたタイル構造へのポインタで定義される。圧縮は原始的だが天才的だ:「コントロール」バイトの後に1-3バイトのデータが続く。

タイルフォーマットはラン(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

各レベルは16列 × 13行のタイル(13×16 = 208タイルの可視領域)を含む。しかし圧縮フォーマットにより、もっと小さくすることができる -- 例えば空や空の列はほとんどスペースを取らない。

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

スプライトレイヤー(敵とオブジェクト)

並行して、敵とオブジェクト(?ブロック、パイプ、クリボー、ノコノコ)は完全に別の構造に保存される。各スポーンは2バイトで定義される:

; 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ページの異なるスプライトを参照できる(つまり16列の5「スクリーン」)。しかし実際にはほとんどのレベルは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 IDテーブル:128の可能なエントリ、34が割り当てられている

タイルポインタとスプライトポインタの異なる順序 -- Frankensteinレベルの原因

34のユニークレベルと7ビットIDシステム

NESのRAMチップ(MB8416A)-- カートリッジ交換時にデータを保持するのはこのチップ

SMB1には32レベルではなく、34のユニークレベルがある。多くのレベルは重複(5-3 = 1-3だがBullet Billあり)で、"ハードモード"フラグでマークされている。本物のユニークレベルは:

  • 水(タイプ0):3レベル(2-2、7-2、ボーナスエリア5-2/6-2)
  • オーバーワールド(タイプ1):22レベル(2つの雲ボーナスルームを含む)
  • アンダーグラウンド(タイプ2):3レベル(地下ボーナスルームを含む)
  • キャッスル(タイプ3):6レベル
  • + 1カットシーンルーム(地下/水中のレベルの前)
  • + 14-2のワープゾーン

各レベルは7ビットのIDを持つ。下位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の可能なID($00-$7F)、34のみが実在のレベルに割り当てられている。未使用のIDはどこを指してもおかしくない。

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

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

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が無効な場合、タイルポインタとスプライトポインタが異なるレベルをロードし、Frankensteinレベルが生まれる。

この2つのリスト間をナビゲートするために、ゲームは小さなオフセットテーブル(目次のようなもの)を使う:

; 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、オーバーワールド番号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 ✓

では、$43のような無効なID(アンダーグラウンド番号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レベル:タイル1-4 + スプライト水中5-2

グリッチレベルポインタの探索 -- オフセットテーブルの説明

ワールドインデックステーブル -- World 9のオーバーフローでグリッチレベルが生成される

ワールドインデックステーブル:なぜWorld 9がオーバーフローするのか

各ワールド(1-8)の最初のレベルのインデックスを与える8バイトのROMテーブルがある。その直後、ゲーム順ですべての36Level IDのテーブルがある。

; 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をロードしようとすると、ゲームはWorldIndexTableの9バイト目を読む...がそれは存在しない。LevelIDTableに1バイトオーバーフローし、値$25を読み、その$25をLevelIDTableのインデックスとして使用する(37番目のエントリ)-- これによりSpriteOffsetTableに2バイトオーバーフローし、値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)では、オーバーフローはさらに遠く、Level ID $01に到達する。これは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

グリッチワールドが存在する理由

ゲームには32の「正規」レベル(8ワールド × 4レベル)がある。しかしポインタテーブルはエリアタイプごとに128エントリある。32以降のエントリには、そのROMアドレスにあるデータが入っている -- 時には別のレベル、有时はサウンドデータ、有时はRAM、有时はただのゴミ。

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

Level ID $01 + AreaType 0 = Minus World

最も有名なグリッチワールド。Level ID $01のAreaType 0(水)は以下を指す:

  • タイルポインタ:$AE45 → 2-2/7-2の水中エリア
  • スプライトポインタ:$A171 → 2-2/7-2のスプライト

結果:2-2に似た水中レベルだが、flagpoleが存在しないので無限にループする。レベルの終わりなし、出口なし。

これはレベル36-1(またはワールド$-1の36-1)だ。

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を指すポインタ

タイルポインタやスプライトポインタがROMではなくRAMのアドレス($00-$7F)を指すと、ゲームはRAMのconstantな変化をタイルとして解釈しようとする:

; 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が動いたり、タイマーが回ったり)、レベルの「スプライト」も変わる。だから一部のグリッチワールドでは敵が点滅し、constantに変化するのだ。

Level ID $03 Water -- スプライトポインタ$009DがRAMを指す、プレイ不可能なレベル

Level ID $36:空のレベル(Overworld)

Level ID $36のOverworld:

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

結果:何もなし。ゲームはレベルをロードするが、RGMechExのカタログでは「レベルなし」とマークされている。タイルは有効かもしれないが、スプライトが空のレベルまたは機能しないレベルを生成する場所を指している。

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

Level ID $1DのCastle:

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

スプライトポインタがRAM = 未定義スプライト。ゲームはタイルの最初の行にSpiny ballやBullet Bill blasterを表示しようとする。即座にクラッシュ。

; 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のグリッチワールド

RGMechExはすべてのレベルのマップを生成するスクリプトを作成した。4つのエリアタイプすべて、それぞれ128IDだ。

ワールドカウンターは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)-- タイルポインタ$A7CD(2-1)-- は38の異なるグリッチワールドに出現する。スプライトポインタ$9F51がROMのサウンドデータ/パディング領域を指し、多くのIDが再利用しているからだ。

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

Super Mario Bros.グリッチレベル解説 -- 3本目の動画

本当にユニークな6つのグリッチレベル

アクセス可能な19のグリッチレベルIDのうち、ロード時に即座にクラッシュしないのは6のみ:

ワールド Level ID 説明
E-1(224) $50 穴の上に?ブロックが1つ。Marioは即死。
W $57 Marioがスポーンして動けない。
42(133) $50 雲のトンネル。十分進むとMarioが閉じ込められる。
62(131、240) $4D 凍った城:Marioが上にスポーンし、落ちられない → ロック。
127 $4B 地下トンネル。だが進みすぎるとクラッシュ。
137 $4B カットシーンの自動スクロールを有効化。Marioが唯一のbrick blockに永久にブロックされる。

Level ID $50(雲トンネル)-- グリッチワールド42-1とE-1 Level ID $4D(城)-- ワールド62-1、スポーンでロックされたMario Level ID $4B(トンネル)-- ワールド127-1、進みすぎるとクラッシュ

248のグリッチワールドのうち、本当に新しいものを生み出すのは6のみ。残りは間違ったエリアタイプの通常レベルか、黒い画面だ。

レベルフォーマットの詳細

グリッチレベルが成立する(しない)理由を理解するため、レベルデータの正確なフォーマットに飛び込む。

レベルヘッダー:2バイト、6プロパティ

各レベルは2バイトのヘッダーで始まり、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)

タイプモディファイアは視覚的なバリエーションを制御する:水中レベルの波、8-3のbrick背景、4-3のナイトパレット、6-2の雪など。

タイルオブジェクト:2バイト、Next Screen Flag、3スロットのキュー

ヘッダーの後にはタイルオブジェクトのリストが続く。各オブジェクトは2バイト。バイト$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

「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スロット。直接的な結果:同じ列で始まるオブジェクトは3つ以上持てない。キューが満杯の場合、4番目のオブジェクトは無視され、ロードされない。

だから設計の良いレベルはオブジェクトの詰め込みを避ける。1-2の例:天井の1upブロックがある列 + 横のbrickは、3の制限を守ために2つの別オブジェクトに分割されている。

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

Y=12の場合、オブジェクトにはY位置がない(タイプでハードコードされている):

; 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の場合、2つのサブグループがある。バイト1のビット6が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

ビット6=0の場合、下位5ビットがスクリーンスキップ(next screen flagを1つずつ使わずに、スクリーンNに直接ジャンプ)をエンコードする。

Y=14の場合:同じ原理でビット6=1はタイプモディファイアの変更、ビット6=0は背景 + フロアパターンの変更。

フロアパターン:16種類の床パターン

レベルの床は個別のオブジェクトで構成されていない。SMB1はフロアパターンを使用する。次の変更まで全列に適用される背景パターンだ:

; 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)

だから穴はオブジェクトだ:特定の列でフロアパターンをオーバーライドし、残りのパターンを変更せずに済む。

256バイトの制限とリピート

すべてのタイルデータは最大256バイトに収まる。6502のYレジスタがインデックスとして使われ、8ビットだ。$FDバイトを見つけずにデータの末尾に達した場合、最初に戻って256バイトを無限に繰り返す:

; 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.

一部のグリッチレベルはこのリピートを利用して、「無限」に続くレベルを生成する。

スプライトシステム:2バイト + パイプトランジション

スプライトは同様のフォーマットに従うが、ヘッダーがなく、いくつかの重要な違いがある。バイト$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

バイト1の最下位ビットはハードレベルフラグ:1に設定されると、スプライトは5-3以上のレベルにのみ出現する。これで「ハードモード」レベルが作られる。

Y位置15 = スクリーンスキップ(タイルと同じ)。Y位置14 = パイプトランジション(3バイト):

; 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.

スプライトにはキューシステムがない。唯一の制限は、スポーンゾーン(画面右のすぐ外)に同時にロードできるスプライトが4つ以下であること。それ以上はスプライトが無視される。

グリッチワールドへのアクセス方法

主に2つの方法がある。

クラシックな方法:壁抜け

壁抜け(壁を通り抜ける)により、通常のレベルから脱出し、隠されたワープゾーンまで歩くことができる。RAM経由でワールドカウンターを操作することで、任意のLevel IDをロードできる。

テクニック:

  1. World 1-2:隠された終点パイプに入る
  2. 右の壁で壁抜けを行う
  3. ワープゾーンまで虚空を歩く
  4. ゲームが値をワールドとして解釈する

しかし、この方法はグリッチワールドの一部にしかアクセスできない。

極端な方法:NESテニスカートリッジ交換

詳細は上の「ワームスタート」セクションを参照。要するに:テニスの歩数カウンターがSMB1のワールドバイトと同じRAMバイトに書き込み、ワームスタート検出がこの値を保持する。

ハッカー向け:すべてを探索するコード

エミュレーターですべてのグリッチを自分で探索したい場合、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に公開している。各エントリにはタイルポインタ、スプライトポインタ、レベルの視覚マップが表示される。

最もクレイジーなレベル

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

タイルポインタ$A302(3-4)にスプライトポインタ$02A0を組み合わせると、15の異なるグリッチワールド(D-1、J-1、Y-1、Z-1、55-1、73-1...)が生成される。説明:スプライトポインタがROMのサウンドデータ/パディング領域を指し、有効なスプライトに近いデータを含むためプレイ可能な結果を生む。しかし3-4のキャッスルタイルとオーバーワールドスプライトの組み合わせは、意味のないレンダリングを生む。

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

絶対記録。38のグリッチワールドエントリが同じレベル(2-1タイル + $9F51スプライト)を指す。なぜ? スプライトポインタ$9F51がROMのサウンドデータ/パディング領域を指し、多くのIDが再利用しているからだ。

Level ID $49(Underground):FDSレベル

タイルポインタ$76AE + スプライトポインタ$1C9D。タイルポインタがFamicom Disk System用に予約されたROM領域を指す。結果:標準カートリッジには存在しないタイルを持つレベル。これがレベル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)

通常アクセス可能な「ボーナス」レベルとグリッチワールドの違いは、ワープゾーンが現在のワールドをチェックすることだ:

; 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

番号が8超または0のグリッチワールドは通常のパイプでは到達できない。壁抜けまたはカートリッジ交換が必要だ。

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

ゲームがタイルオブジェクトをロードする際、タイプをジャンプテーブルのインデックスとして使う:

; 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

ジャンプテーブル:なぜ無効なオブジェクトタイプでゲームがクラッシュするのか

オブジェクトが無効なタイプ(≥12)を持つ場合、ゲームはこのテーブルに存在しないポインタにジャンプする。4つの結果が可能:

  1. 有効なポインタ → オブジェクトが通常通りロードされる
  2. 別のジャンプテーブルへのポインタ(オーバーラップ) → 異なるオブジェクトが出現。例:タイプ12がY=13テーブルを指し、L-pipeになる
  3. 実行可能コードへのポインタ → ランダムコードの実行(クラッシュの可能性大)
  4. 明示的なプレースホルダー(NOP) → オブジェクトが何もしない(一部のスプライトはこのように、動かない空中浮遊の敵を生成する)

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

グリッチレベルID $50:雲トンネル、破損データから生成されたレベル

グリッチレベルID $58(クラッシュするトンネル):スプライトポインタがROMマッパーなしのNESには存在しないメモリ領域を指す。ゲームは(0,0)位置に同じKoopaを1フレームに5回ロードしようとして、PPUを飽和させ、フリーズを引き起こす。

; 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

パイプワープのパラドックス

target_world BETWEEN 1 AND 8のチェックを覚えているか? グリッチワールドでパイプを見つけたとしても、ゲームは目的地のワールドが1-8の間であることを確認する。グリッチワールドは番号が8超(36-1、255-1...)なので、ワープは失敗する。

Minus Worldに終わりがないのもそのためだ:flagpoleがスプライトに存在せず、パイプはどこにも繋がらない。

1列に5つのオブジェクトのトリック

3オブジェクト/列の制限を上書きするエッジケースが存在する。キューがブロックされた場合(スロット満杯 + 次のオブジェクトにnext screen flagなし)、ゲームはnext screen flagを持つオブジェクトが見つかるまで現在の列をループで「プレプロセス」する。各プレプロセスで:

; 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).

これは「キュースキップ」と呼ばれ、一部のROMハッカーがフォーマットが通常許すより密度の高いレベルを作成するために使用している。

バージョン間の違い

Famicom Disk System

SMB1のFDSバージョンはメモリマップが異なる。すべてのレベルポインタがシフトされているが、データは同じ。違い:グリッチワールドのインデックスが完全に異なる:

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の後半の地下バージョン( autoscrollingなのが残念だが)。

The Lost Levels(Super Mario Bros. 日本版)

Lost Levelsは多くのことを変える:

  1. タイル/スプライトの同じ順序:Frankensteinレベルなし(無効なIDでもタイルとスプライトが同じレベルをロードする)
  2. 1つの16ビットポインタテーブル(high/lowの2テーブルではなく)
  3. 4つのディスクファイル:ROMがFDS用に分割された:
    • ファイル1:ワールド1-4
    • ファイル2:ワールド5-8
    • ファイル3:ワールド9 + サウンドエンジン
    • ファイル4:ワールドA-D(完全に異なるポインタテーブル)
  4. 同じLevel 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には2つのパイプトランジションオブジェクトがある:蔦(ワープゾーン)とパイプ(コインキャッシュルーム)。最初のトランジションオブジェクト(蔦のもの)は、蔦が画面に出現するはるか前に配置されている。2番目(パイプ)はレベル内で遅すぎる位置に配置されている。

; 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.

ループレベル

ループ(8-4、7-4)はどう機能するか? レベルにはハードコードされたスクリーン番号とY位置を持つチェックポイントがある:

; 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.

フォーマットを変え、コードは変えない

このアーキテクチャの最も興味深い教訓の1つは、SMB1の開発者が6502レンダリングコードに触れることなく、非常に表現力豊かなレベルシステムを構築したことだ。レベル間のすべてのバリエーションはデータ(ポインタ、オブジェクト、スプライト、フロアパターン)から来ており、コードではない。

256のグリッチワールドが存在するのは、ポインタテーブルが128エントリ × 4タイプに Sizedされており、ゲームが読み取る値を決してバリデーションしないからだ。ポインタがRAMに落ちると、ゲームはMarioのレジスタをタイルとして解釈する。ポインタがサウンドデータに落ちると、ゲームは音楽をレベルデザインとして再生する。そしてジャンプテーブルがオーバーフローすると、ゲームはクラッシュまで何でも実行する。

More Super Mario Bros. Mechanics Explained -- 4本目の動画

ここから学べること

  1. タイル/スプライトの分離:2レイヤーの完全な独立、異なる保存順序がユニークなFrankensteinレベルを生む
  2. RLE圧縮 + オブジェクトシステム:レベルはビットマップではなく配置されたオブジェクトのリスト、床にはフロアパターン
  3. 3スロットキュー:ハードウェア(とレベルデザイン)の厳格な制限
  4. バリデーションなし:ゲームはポインタとジャンプテーブルを信頼し、プレイ可能なグリッチまたはクラッシュを生む
  5. 最大256バイト:6502のYレジスタの制限により、進みすぎるとデータが繰り返される
  6. ワームスタート/コールドスタート:「続ける」システムがテニスカートリッジ交換 → Marioへの扉を開いた

最も美しいのは、これらすべてが40KBに収まる6502コードだ。抽象レイヤーなし、メモリアクセスバリデーションなし、例外ハンドラなし。ポインタが壊れていたら、ゲームはクラッシュする。そしてクラッシュは、グリッチワールドと呼ばれる。

覚えるべき3つのこと

  1. グリッチワールドは場違いに落ちたポインタだ -- ゲームには128ID × 4エリアタイプがあるが、ユニークレベルは34のみ。ワールド番号が(テニスや壁抜けによって)破損すると、別のレベル用に設計されたポインタがロードされ、512の組み合わせが予測不可能な結果を生む。

  2. Minus Worldはワープバグと破損の組み合わせだ -- 1-2の左パイプがテキスト表示前にアクティブ化されると、ワールド36(0x24)がロードされる。このワールドはLevel ID $01(2-2の水)を指し、flagpoleのないレベルだ。そしてワールド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.: 레벨 포맷, 포인터, 그리고 256개의 글리치 월드

128개 레벨 × 4가지 영역 타입이 40KB ROM에 어떻게 들어가는지, Minus World가 왜 존재하는지, 그리고 NES 테니스 경기가 글리치 월드를 로드할 수 있는 이유.

소개

Super Mario Bros.는 40KB ROM로 이루어져 있습니다. 8개 월드, 32개 레벨, 적, 음악, 파워업이 모두 거기 들어 있습니다.

하지만 에뮬레이터를 열고 올바른 바이트를 조작하면 레벨 36-1을 로드할 수 있습니다. 또는 255-1도 가능합니다. 또는 모든 것이 Bowser 스프라이트와 아무 데도 연결되지 않는 파이프로 이루어진 월드에 착륙할 수도 있습니다.

이 글리치 월드들이 존재하는 이유는 간단합니다: SMB1의 레벨 저장 시스템은 8비트 최적화의 걸작이고, 게임이 읽으면 안 되는 곳에서 읽도록 강제하면 매혹적인 결과가 나옵니다.

Retro Game Mechanics Explained가 이 주제에 대해 4편의 영상을 만들었습니다 -- 가장 많이 팔린 그 시대의 게임의 6502 코드를 하나의 깊이 있는 탐험으로 정리하겠습니다.

GLITCH OBJECTS -- SMB1의 숨겨진 메커니즘에 대한 RGMechEx 시리즈 타이틀

World 9-1 -- 테니스 카트 스왑을 통해 접근 가능한 첫 번째 글리치 월드의 타이틀 화면

웜 스타트: 왜 테니스의 RAM이 SMB1에서 살아남는가

레벨 저장에 대해 이야기하기 전에, SMB1이 어떻게 시작되는지 이해해야 합니다. NES 테니스 카트 스왑 글리치는 게임의 웜 스타트 / 콜드 스타트 감지 시스템에 전적으로 의존하기 때문입니다.

보존되는 41바이트

SMB1이 콜드 스타트 (처음 전원 켜기 또는 전원 껐다가 켜기)를 감지하면 RAM 전체를 지웁니다. 하지만 웜 스타트 (리셋 버튼, 전원 차단 없음)를 감지하면 41바이트 메모리 영역을 보존합니다:

; 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바이트는 하나의 기능을 위해 사용됩니다: 플레이어가 게임 오버 후 같은 월드에서 계속할 수 있게 하는 것. 6-3에서 죽으면 게임은 월드 6을 시작 바이트에 기록하고, 타이틀 화면에서 A + Start를 누르면 6-1에서 다시 시작합니다.

웜 스타트 시 RAM에서 보존되는 41바이트 -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, 그리고 마법의 바이트 $A5

웜 스타트의 이중 검증

콜드 스타트 vs 웜 스타트 -- 리셋 감지 다이어그램

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

바이트 $A5와 탑 스코어 자릿수 확인 -- 웜 스타트의 핵심

왜 이중 검증일까요? 바이트 $A5가 우연히 존재할 수 있기 때문입니다 (다른 게임이 이 값을 남기거나, RAM 칩의 기본 유휴 상태). 탑 스코어의 자릿수가 유효한지 (0-9) 확인함으로써 데이터가 일관된다는 것을 보장합니다.

왜 테니스가 유일하게 작동하는 게임인가

처음으로 SMB1을 삽입하면 (콜드 스타트), 게임은:

  1. RAM 전체를 지움 → 탑 스코어 = 0, 월드 바이트 = 0
  2. $0787에 $A5를 기록

그런 다음 콘솔을 끄지 않고 테니스로 전환합니다. 테니스:

  • 시작 시 RAM을 정리하지 않음 (NES 게임 중 거의 그렇게 하지 않음)
  • 탑 스코어 바이트에 기록하지 않음 → 0으로 유지 (유효)
  • 바이트 $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. 바이트 $A5가 아직 거기 있음 (테니스가 건드리지 않았음)
  2. 탑 스코어 자릿수가 여전히 0 (유효)
  3. 월드 바이트가 이제 8+ (테니스 발걸음으로 증가)
  4. SMB1이 웜 스타트를 감지 → 손상된 월드 바이트를 보존
  5. A + Start 유지 → world 9-1, world A-1, world 36-1 등

왜 테니스 전에 마리오를 부팅해야 하는가

미묘한 점이 있습니다: SMB1을 먼저 부팅한 다음, 테니스, 그리고 다시 SMB1을 부팅해야 합니다. 테니스부터 시작하면 바이트 $A5가 기록되지 않습니다 (테니스는 $A5를 기록하지 않으므로), 그래서 웜 스타트 감지가 실패하고 RAM이 지워집니다.

테니스의 발걸음 카운터: 매 발걸음마다 월드 바이트를 증가시킴

NES 테니스를 통한 글리치 월드 접근 -- 카트 스왑을 설명하는 영상

SMB1이 40KB에 레벨을 저장하는 방법

Nintendo R&D4는 겉보기에 단순한 문제를 해결해야 했습니다: 타일로 수평 스크롤하는 레벨을 표현하고, 적과 아이템을 포함하며, 모든 것을 초 Restricted ROM 예산 안에 넣기.

해결책은 완전히 독립된 두 개의 데이터 레이어로 분리하는 것입니다:

타일 레이아웃 (레벨 맵)

각 레벨은 ROM에서 압축된 타일 구조를 가리키는 포인터로 정의됩니다. 압축은 원시적이지만 영리합니다: "컨트롤" 바이트 뒤에 1-3바이트의 데이터.

타일 포맷은 런 (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

각 레벨은 16열 × 13행의 타일 (13×16 = 208개 보이는 타일)을 포함합니다. 하지만 압축된 포맷은 훨씬 적은 공간으로 줄일 수 있습니다 -- 예를 들어, 하늘과 빈 열은 거의 공간을 차지하지 않습니다.

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

스프라이트 레이아웃 (적과 오브젝트)

별도로, 적과 오브젝트 (블록 ?, 파이프, goomba, koopa)는 완전히 별도의 구조에 저장됩니다. 각 스폰은 2바이트로 정의됩니다:

; 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개의 서로 다른 스프라이트 페이지를 참조할 수 있습니다 (즉, 16열짜리 5개 "스크린"). 하지만 실제로 대부분의 레벨은 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 ID 테이블: 128개 가능한 항목, 34개 할당됨

타일 포인터와 스프라이트 포인터의 서로 다른 순서 -- 프랑켄슈타인 레벨의 원인

34개 고유 레벨과 7비트 ID 시스템

NES의 RAM 칩 (MB8416A) -- 카트리지 스왑 시 데이터를 보존하는 칩

SMB1에는 32개 레벨이 아니라 34개 고유 레벨이 있습니다. 많은 레벨은 "하드 모드" 플래그로 표시된 중복입니다 (5-3 = 1-3이지만 Bullet Bill 포함). 진짜 고유 레벨:

  • 물 (타입 0): 3개 레벨 (2-2, 7-2, 보너스 영역 5-2/6-2)
  • 오버월드 (타입 1): 22개 레벨 (구름 보너스 2개 방 포함)
  • 지하 (타입 2): 3개 레벨 (지하 보너스 방 포함)
  • 성 (타입 3): 6개 레벨
  • + 1개 컷씬 방 (지하/수중 레벨 앞)
  • + 4-2의 워프 존

각 레벨은 7비트 ID를 가지고 있습니다. 하위 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개 가능한 ID ($00-$7F), 실제 레벨에 할당된 것은 34개만. 사용되지 않은 ID는 아무 데나 가리킵니다.

포인터 테이블: 두 개의 목록, 두 개의 순서

타일 포인터와 스프라이트 포인터는 같은 순서로 저장되지 않습니다. 코드는 두 개의 별도 16비트 목록을 사용합니다 (하이 바이트 / 로우 바이트가 두 개의 별도 테이블에):

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가 유효하지 않으면 타일 포인터와 스프라이트 포인터가 서로 다른 레벨을 로드하여 프랑켄슈타인 레벨을 만듭니다.

이 두 목록 사이를 탐색하기 위해, 게임은 작은 오프셋 테이블 (목차와 같은)을 사용합니다:

; 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, 오버월드 번호 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 ✓

이제, $43 (지하 번호 3, 존재하지 않음) 같은 유효하지 않은 ID로는 어떻게 될까요?

; 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 -- 프랑켄슈타인 레벨: 타일 1-4 + 스프라이트 물 5-2

글리치 레벨 포인터 탐험 -- 오프셋 테이블 설명

월드 인덱스 테이블 -- world 9 오버플로우가 글리치 레벨을 만드는 이유

월드 인덱스 테이블: 왜 world 9가 오버플로우되는가

각 월드의 첫 번째 레벨 인덱스를 제공하는 8바이트 ROM 테이블이 있습니다. 그리고 바로 뒤에, 게임 순서대로 모든 36개 레벨의 Level ID 테이블이 있습니다.

; 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를 로드하려고 하면, 게임은 WorldIndexTable의 9번째 바이트를 읽습니다... 하지만 존재하지 않습니다. 1바이트가 LevelIDTable로 오버플로우하여 $25 값을 읽고, $25를 LevelIDTable의 인덱스로 사용합니다 (37번째 항목) -- 이는 SpriteOffsetTable로 다시 2바이트 오버플로우하여 값 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)의 경우, 오버플로우가 더 멀어져 Level ID $01에 떨어지는데, 이것이 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

왜 글리치 월드가 존재하는가

게임에는 32개의 "합법적인" 레벨 (8월드 × 4레벨)이 있습니다. 하지만 포인터 테이블은 영역 타입당 128개 항목을 가지고 있습니다. 32번째 레벨 이후의 항목에는 해당 주소의 ROM 내용이 들어 있습니다 -- 때로는 다른 레벨, 때로는 사운드 데이터, 때로는 RAM, 때로는 아무거나.

Level ID $01 Water (Minus World) -- 타일 포인터 $AE45, 스프라이트 포인터 $A171

Level ID $01 + AreaType 0 = Minus World

가장 유명한 글리치 월드. Level ID $01은 AreaType 0 (물)에서 다음을 가리킵니다:

  • 타일 포인터: $AE45 → 2-2/7-2의 수중 영역
  • 스프라이트 포인터: $A171 → 2-2/7-2의 스프라이트

결과: 2-2와 비슷한 수중 레벨이지만 깃발 기둥이 존재하지 않아 무한히 반복됩니다. 레벨 끝 없음, 출구 없음.

이것이 레벨 36-1 (또는 월드 $-1의 36-1)입니다.

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을 가리키는 포인터

타일 포인터나 스프라이트 포인터가 ROM이 아닌 RAM 주소 ($00-$7F)를 가리킬 때, 게임은 RAM의 지속적인 변화를 타일로 해석하려 합니다:

; 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.

제로 페이지가 변경되면 (마리오가 움직이거나 타이머가 돌아가는 등), 레벨의 "스프라이트"도 바뀝니다. 이것이 일부 글리치 월드에서 적이 깜빡이고 지속적으로 변하는 이유입니다.

Level ID $03 Water -- 스프라이트 포인터 $009D이 RAM을 가리킴, 플레이 불가능한 레벨

Level ID $36: 빈 레벨 (Overworld)

Level ID $36은 Overworld:

  • 타일 포인터: $AC35 (1-2)
  • 스프라이트 포인터: $A0D8 (1-2)

결과: 없음. 게임은 레벨을 로드하지만 RGMechEx 카탈로그에서 "레벨 없음"으로 표시됩니다. 타일은 유효할 수 있지만 스프라이트가 빈 레벨이나 작동하지 않는 레벨을 만드는 곳을 가리킵니다.

Level ID $1D (성): 크래시 챔피언

Level ID $1D은 성:

  • 타일 포인터: $A210 (4-4)
  • 스프라이트 포인터: $7EA0 (RAM!)

스프라이트 포인터가 RAM에 있으면 미정의 스프라이트가 됩니다. 게임은 첫 번째 타일 행에 Spiny ball이나 Bullet Bill 발사기를 표시하려 합니다. 즉시 크래시됩니다.

; 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개 글리치 월드 카탈로그

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가 같은 주소에 떨어지는 포인터 때문에 같은 레벨로 이어집니다. 예를 들어 Level ID $28 (Overworld) -- 타일 포인터 $A7CD (2-1) -- 은 38개 서로 다른 글리치 월드에 나타납니다. 스프라이트 포인터 $9F51이 많은 ID에 의해 재사용되는 패딩/사운드 데이터 영역의 ROM을 가리키기 때문입니다.

Level ID $28 (Overworld) 레벨 맵 -- 2-1 타일과 일반 스프라이트, 38개 글리치 월드

Super Mario Bros. Glitch Levels Explained -- 세 번째 영상

정말 고유한 6개 글리치 레벨

19개 접근 가능한 글리치 레벨 ID 중 6개만이 로드 시 즉시 크래시되지 않습니다:

월드 Level ID 설명
E-1 (224) $50 절벽 위에 ? 블록 하나. 마리오가 즉시 죽음.
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개 글리치 월드만 정말 새로운 것을 만듭니다. 나머지는 잘못된 영역 타입을 가진 일반 레벨이거나 검은 화면입니다.

레벨 포맷 상세 분석

글리치 레벨이 왜 성립하는지 (또는 그렇지 않은지) 이해하기 위해 정확한 레벨 데이터 포맷으로 넘어가겠습니다.

레벨 헤더: 2바이트, 6가지 속성

각 레벨은 6가지 속성을 제어하는 2바이트 헤더로 시작합니다:

; 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)

타입 수정자는 시각적 변형을 제어합니다: 수중 레벨 상단의 파도, 8-3의 벽돌 배경, 4-3의 야간 팔레트, 6-2의 눈 등.

타일 오브젝트: 2바이트, Next Screen Flag, 3슬롯 큐

헤더 뒤에 타일 오브젝트 목록이 오며, 각 오브젝트는 2바이트입니다. 바이트 $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

"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슬롯입니다. 직접적인 결과: 같은 열에서 시작하는 오브젝트가 3개를 초과할 수 없습니다. 큐가 가득 차면 네 번째 오브젝트는 무시되고 로드되지 않습니다.

이것이 잘 설계된 레벨이 너무 많은 오브젝트를 쌓지 않는 이유입니다. 1-2의 예: 천장의 1up 블록이 있는 열과 옆의 벽돌은 3개 제한을 준수하기 위해 두 개의 별도 오브젝트로 분할됩니다.

특수 Y 위치: 12, 13, 14, 15

Y=12일 때, 오브젝트는 Y 위치가 없습니다 (타입별로 하드코딩됨):

; 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일 때, 두 개의 서브그룹. 바이트 1의 비트 6이 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

비트6=0이면, 하위 5비트가 스크린 스킵 (하나씩 next screen flag를 거치지 않고 N 스크린으로 직접 건너뛰기)를 인코딩합니다.

Y=14일 때: 비트6=1이면 타입 수정자를 변경하고, 비트6=0이면 배경 + 바닥 패턴을 변경하는 같은 원리입니다.

바닥 패턴: 16개 바닥 모양

레벨의 바닥은 개별 오브젝트로 만들어지지 않습니다. SMB1은 바닥 패턴을 사용하며, 다음 변경까지 모든 열에 적용되는 배경 모양입니다:

; 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)

구멍이 오브젝트인 이유: 특정 열에서 바닥 패턴을 오버라이드하여 나머지 패턴을 변경하지 않고도 구멍을 만들 수 있기 때문입니다.

256바이트 제한과 반복

각 레벨의 모든 타일 데이터는 최대 256바이트에 들어갑니다. 6502의 Y 레지스터는 인덱스로 사용되며 8비트입니다. 게임이 $FD 바이트를 찾지 못한 채 데이터 끝에 도달하면 처음으로 돌아가 256바이트를 무한히 반복합니다:

; 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.

일부 글리치 레벨은 이 반복을 활용하여 "무한히" 지속되는 레벨을 생성합니다.

스프라이트 시스템: 2바이트 + 파이프 전환

스프라이트는 유사한 포맷을 따르지만 헤더가 없고 몇 가지 핵심 차이가 있습니다. 바이트 $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

바이트 1의 최하위 비트는 하드 레벨 플래그입니다: 1로 설정하면 해당 스프라이트는 5-3 이상의 레벨에만 나타납니다. 이것이 "하드 모드" 레벨이 만들어지는 방식입니다.

Y 위치 15 = 스크린 스킵 (타일과 동일). Y 위치 14 = 파이프 전환 (3바이트):

; 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.

스프라이트에는 큐 시스템이 없습니다. 유일한 제한은 스폰 영역 (오른쪽 화면 밖)에서 동시에 로드된 스프라이트가 4개를 초과할 수 없다는 것입니다. 그 이상은 스프라이트가 무시됩니다.

글리치 월드에 접근하는 방법

두 가지 주요 방법이 있습니다.

클래식 방법: 월 클립

월 클립 (벽 통과)은 일반 레벨 밖으로 나와 숨겨진 워프 존까지 걸어갈 수 있게 합니다. RAM을 통해 월드 카운터를 조작하면 어떤 Level ID든 로드할 수 있습니다.

기술:

  1. World 1-2: 숨겨진 끝 파이프로 이동
  2. 오른쪽 벽에서 월 클립 수행
  3. 빈 공간을 걸어 워프 영역까지 이동
  4. 게임이 값을 월드로 해석

하지만 이 방법은 글리치 월드의 일부에만 접근할 수 있습니다.

극단적 방법: NES 테니스 카트 스왑

전체 세부 사항은 위의 "웜 스타트" 섹션을 참조하세요. 요약: 테니스의 발걸음 카운터가 SMB1의 시작 월드와 같은 RAM 바이트에 기록하고, 웜 스타트 감지가 이 값을 보존합니다.

해커들을 위한 코너: 모두 탐험하기 위한 코드

에뮬레이터에서 모든 글리치를 직접 탐험하고 싶다면, 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에 게시했습니다. 각 항목은 타일 포인터, 스프라이트 포인터, 레벨의 시각적 맵을 보여줍니다.

가장 황당한 레벨들

Level ID $1F (Water): 15개 글리치 월드가 하나에

타일 포인터 $A302 (3-4)와 스프라이트 포인터 $02A0의 조합으로 15개 서로 다른 글리치 월드 (D-1, J-1, Y-1, Z-1, 55-1, 73-1...)가 나옵니다. 설명: 스프라이트 포인터가 유효한 스프라이트에 충분히 가까운 데이터가 포함된 ROM 영역을 가리켜 플레이 가능한 결과를 만들지만, 3-4의 성 타일과 오버월드 스프라이트의 조합이 엉뚱한 렌더링을 만듭니다.

Level ID $28 (Overworld): 38개 글리치 월드 = 기록

절대 기록. 38개 글리치 월드 항목이 같은 레벨 (2-1 타일 + $9F51 스프라이트)을 가리킵니다. 왜? 스프라이트 포인터 $9F51이 많은 ID에 의해 재사용되는 패딩/사운드 데이터 영역의 ROM에 떨어지기 때문입니다.

Level ID $49 (Underground): FDS 레벨

타일 포인터 $76AE + 스프라이트 포인터 $1C9D. 타일 포인터가 패미컴 디스크 시스템 버전에 예약된 ROM 영역을 가리킵니다. 결과: 표준 카트리지에 존재하지 않는 타일을 가진 레벨. 이것이 레벨 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)

일반적으로 접근 가능한 "보너스" 레벨과 글리치 월드의 차이점은 워프 존이 현재 월드를 확인한다는 것입니다:

; 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

번호가 8 초과 또는 0인 글리치 월드는 일반 파이프로 도달할 수 없습니다. 월 클립이나 카트 스왑이 필요합니다.

왜 일부 레벨이 크래시되는가: 점프 테이블

게임이 타일 오브젝트를 로드할 때, 타입을 점프 테이블의 인덱스로 사용합니다:

; 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

점프 테이블: 왜 유효하지 않은 오브젝트 타입이 게임을 크래시시키는지

오브젝트가 유효하지 않은 타입 (≥12)을 가지면, 게임은 이 테이블에 존재하지 않는 포인터로 점프합니다. 4가지 가능한 결과:

  1. 유효한 포인터 → 오브젝트가 일반적으로 로드됨
  2. 다른 점프 테이블로의 포인터 (겹침) → 다른 오브젝트가 나타남. 예: 타입 12가 Y=13 테이블을 가리켜 L-pipe가 됨
  3. 실행 가능한 코드로의 포인터 → 무작위 코드 실행 (크래시 가능성 높음)
  4. 명시적 NOP → 오브젝트가 아무것도 하지 않음 (일부 스프라이트가 이렇게 하여 적이 제자리에서 날아다니지만 움직이지 않음)

글리치 레벨 ID $58: 스프라이트 포인터가 유효하지 않은 주소를 가리킴, 게임 크래시

글리치 레벨 ID $50: 구름 터널, 손상된 데이터로 생성된 레벨

글리치 레벨 ID $58 (크래시되는 터널): 스프라이트 포인터가 ROM 매핑 NES에 존재하지 않는 메모리 영역을 가리킵니다. 게임은 (0,0) 위치에 같은 Koopa를 프레임당 5번 로드하려 하여 PPU를 포화시키고 프리즈를 일으킵니다.

; 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

파이프 워프의 역설

target_world BETWEEN 1 AND 8 체크를 기억하세요. 글리치 월드에서 파이프를 찾아도 게임은 대상 월드가 1과 8 사이인지 확인합니다. 글리치 월드는 번호가 8 초과 (36-1, 255-1...)이므로 워프가 실패합니다.

Minus World에 끝이 없는 이유도 같습니다: 깃발 기둥이 스프라이트에 없고, 파이프가 아무 데도 연결되지 않습니다.

한 열에 5개 오브젝트 트릭

한 열에 3개 오브젝트 제한을 우회할 수 있는 엣지 케이스가 있습니다. 큐가 막히면 (슬롯 가득 + 다음 next screen flag 없는 오브젝트), 게임은 next screen flag가 있는 오브젝트를 찾을 때까지 현재 열을 "사전 처리"로 반복합니다. 각 사전 처리에서:

; 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).

이것이 "큐 스킵"이라고 불리며 일부 롬해커가 포맷이 허용하는 것보다 더 조밀한 레벨을 만드는 데 사용됩니다.

버전 간 차이점

패미컴 디스크 시스템

SMB1의 FDS 버전은 메모리 맵이 다릅니다. 모든 레벨 포인터가 이동되지만 데이터는 동일합니다. 달라지는 점: 글리치 월드의 인덱스가 완전히 다릅니다:

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 두 번째 절반의 지하 버전 (자동 스크롤러인 것이 안타까움).

The Lost Levels (Super Mario Bros. 2 일본 버전)

Lost Levels는 많은 것을 변경합니다:

  1. 타일/스프라이트 동일 순서: 더 이상 프랑켄슈타인 레벨 없음 (유효하지 않은 ID에서도 타일과 스프라이트가 같은 레벨을 로드)
  2. 단일 16비트 포인터 테이블 대신 두 개의 별도 하이/로우 테이블
  3. 4개 디스크 파일: FDS용으로 ROM이 분할됨:
    • 파일 1: worlds 1-4
    • 파일 2: worlds 5-8
    • 파일 3: world 9 + 사운드 엔진
    • 파일 4: worlds A-D (완전히 다른 포인터 테이블)
  4. 같은 Level ID = 4가지 가능한 레벨 로드된 파일에 따라
  5. 테니스 글리치 없음: 컨티뉴 옵션 (게임 오버 후 같은 월드에서 계속)이 웜 스타트를 불필요하게 만들고, world > 9이면 게임이 즉시 리셋됨
  6. 새 오브젝트: 독 버섯, 보이지 않는 블록, 보이지 않는 파이어 플라워 블록, 거꾸로 된 파이프, 바람 -- 기존 목록 중간에 삽입됨 → SMB1과의 하위 호환성 없음
  7. Piranha Plants world 4 이후 항상 빨강, 스프링보드는 worlds 2/B/3/C/7에서만 초록

Super Mario All-Stars (SNES)

동일한 6502 루틴을 사용하는 직접 포팅 (SNES는 호환 모드에서 NES 코드를 실행):

  • 워프 존 수정: Minus World 없음 (텍스트 왼쪽 파이프에 입력하면 올바른 월드로 이동)
  • 플랜트: 대부분의 글리치 레벨 크래시 (ID $6A과 9-1 제외)
  • 성 오브젝트 추가: 더 고유하게 렌더링
  • 하지만: 4-2 잘못된 워프가 여전히 작동 (패치되지 않음!)

4-2 잘못된 워프: 오브젝트 배치 버그

4-2에는 두 개의 파이프 전환 오브젝트가 있습니다: 덩굴 (워프 존)과 파이프 (코인 캐시 방). 첫 번째 전환 오브젝트 (덩굴의 것)는 덩굴이 화면에 나타나기 전에 훨씬 먼저 배치됩니다. 두 번째 (파이프)는 레벨에서 너무 늦게 배치됩니다.

; 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.

반복 레벨

반복 (8-4, 7-4)은 어떻게 작동하나요? 레벨은 하드코딩된 스크린 번호와 Y 위치를 가진 체크포인트를 가지고 있습니다:

; 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 렌더링 코드를 건드리지 않고도 매우 표현적인 레벨 시스템을 만들었다는 것입니다. 레벨 간의 모든 변형은 코드가 아닌 데이터 (포인터, 오브젝트, 스프라이트, 바닥 패턴)에서 나옵니다.

248개 글리치 월드가 존재하는 이유는 포인터 테이블이 128개 항목 × 4가지 타입으로 설계되었고, 게임이 읽는 값을 결코 검증하지 않기 때문입니다. 포인터가 RAM에 떨어지면, 게임은 마리오의 레지스터를 타일로 해석합니다. 포인터가 사운드 데이터에 떨어지면, 게임은 음악을 레벨 디자인으로 재생합니다. 그리고 점프 테이블이 오버플로우하면, 게임은 크래시까지 아무거나 실행합니다.

More Super Mario Bros. Mechanics Explained -- 네 번째 영상

이것에서 배울 수 있는 것

  1. 타일/스프라이트 분리: 두 레이어의 완전한 독립성, 서로 다른 저장 순서가 고유한 프랑켄슈타인 레벨을 만듦
  2. RLE 압축 + 오브젝트 시스템: 레벨은 비트맵이 아니라 배치된 오브젝트 목록이며, 바닥용 바닥 패턴이 있음
  3. 3슬롯 큐: 하드웨어 (및 레벨 디자인)의 엄격한 제한
  4. 검증 없음: 게임이 포인터와 점프 테이블을 신뢰하여, 플레이 가능한 글리치 또는 크래시를 만듦
  5. 최대 256바이트: 6502 Y 레지스터의 제한으로 너무 멀리 가면 데이터가 반복됨
  6. 웜 스타트 / 콜드 스타트: "계속" 시스템이 테니스 카트 스왑 → 마리오의 문을 열어줌

가장 아름다운 점: 이것 모두는 40KB에 들어가는 6502 코드입니다. 추상화 계층 없음, 메모리 접근 검증 없음, 예외 처리기 없음. 포인터가 나쁘면 게임이 크래시됩니다. 그리고 크래시를 우리는 글리치 월드라고 부릅니다.

기억해야 할 3가지

  1. 글리치 월드는 잘못 떨어지는 포인터입니다 -- 게임에는 128개 ID × 4가지 영역 타입이 있지만 고유 레벨은 34개뿐입니다. 월드 번호가 손상되면 (테니스나 월 클립), 게임은 다른 레벨을 위해 설계된 포인터를 로드하고, 512가지 가능한 조합이 예측할 수 없는 결과를 만듭니다.

  2. Minus World는 워프 버그와 손상의 조합입니다 -- 1-2의 왼쪽 파이프가 텍스트가 나타나기 전에 활성화되면 world 36 (0x24)을 로드합니다. 이 월드는 Level ID $01 (2-2의 수중), 깃발 기둥이 없는 레벨을 가리킵니다. 그리고 world 36에 파이프 전환가 없기 때문에 레벨은 무한히 반복됩니다. 검증의 부재가 이 아이콘을 만듭니다.

  3. 테니스 → 마리오, OoT → Paper Mario보다 15년 앞서 -- NES의 RAM은 커패시터와 SMB1의 웜 스타트 / 콜드 스타트 시스템 덕분에 카트리지 스왑에서 살아남습니다. 테니스의 발걸음 카운터 (플레이할 때 발걸음 소리를 재생하며 RAM 바이트를 증가시키는)가 월드 번호의 주소에 정확히 떨어집니다. 탑 스코어의 자릿수가 0으로 유지되어야 하고, 바이트 $A5가 온전해야 하며, 게임이 웜 스타트를 감지해야 합니다 -- 테니스에서만 작동한 완벽한 상황의 우연입니다.

Retro Game Mechanics Explained의 원래 영상은 정말 대단한 노력입니다 -- 6502 디스어셈블리, 모든 레벨의 자동 생성 맵, 카트 스왑과 웜 스타트에 대한 설명의 수준이. 시리즈를 보지 않았다면, 짧지만 매 분밀도 밀도 있는 영상을 꼭 보세요.

맵의 소스 코드는 rgmechex.com에서 사용 가능하고, SMB1의 전체 디스어셈블리는 많은 리포지토리에서 오픈 소스입니다. 40년 전, 일본 프로그래머들이 단위 테스트도 버그 트래커도 없이 6502로 이 레벨 시스템을 작성했고, 오늘날에도 그들의 코드를 열어보면 여전히 새로운 것을 배울 수 있습니다.

Super Mario Bros.: Level formatı, göstergeçler ve 256 glitch world

128 level × 4 alan türü 40KB ROM'a nasıl sığıyor, Minus World neden var ve bir NES Tennis maçı nasıl glitch world'leri yükleyebiliyor.

Giriş

Super Mario Bros., 40 kilobyte ROM. Sekiz dünya, 32 level, düşmanlar, müzik, güçlendirmeler, hepsi bunun içinde.

Ama eğer bir emülatör açıp doğru byte'ları kurcalarsan, 36-1 levelini yükleyebilirsin. Veya 255-1'i. Veya her yerin Bowser sprite'larından ve hiçbir yere götürmeyen borulardan oluştuğu bir world'e düşebilirsin.

Bu glitch world'ler basit bir nedenden dolayı var: SMB1'in level depolama sistemi 8-bit optimizasyonu mucizesidir ve oyunu olmaması gereken yeri okumaya zorladığında, büyüleyici sonuçlar ortaya çıkar.

Retro Game Mechanics Explained bu konuda 4 videoluk bir seri yaptı -- bunları dönemin en çok satan oyununun 6502 kodunda tek bir dalışa derleyeceğiz.

GLITCH OBJECTS -- RGMechEx'in SMB1'in gizli mekanikleri üzerine serisinin başlık kartı

World 9-1 -- Tennis kart değişimi ile erişilebilen ilk glitch world'ün açılış ekranı

Warm start: Tennis'in RAM'i SMB1'de neden hayatta kalıyor

Level depolamadan bahsetmeden önce, SMB1'in nasıl başladığını anlamamız gerekir. Çünkü NES Tennis kart değişimi glitch'i tamamen oyunun warm start / cold start tespit sistemine bağlıdır.

Korunan 41 byte

SMB1 bir cold start tespit ettiğinde (ilk kez açma veya kapatma-açma), tüm RAM'i siler. Ama bir warm start tespit ettiğinde (düğme sıfırlama, güç kesintisi yok), 41 byte'lık bir bellek alanını korur:

; 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]

Bu 41 byte tek bir işlev için kullanılır: oyuncunun game over'dan sonra aynı dünyada devam etmesine izin vermek. 6-3'te ölürsen, oyun world 6'yı başlangıç byte'ına yazar ve açılış ekranında A + Start'a basılı tutarsan, 6-1'den başlarsın.

Warm start sırasında korunan 41 byte -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD ve sihirli byte $A5

Warm start'un çift kontrolü

Cold start vs warm start -- sıfırlama tespit diyagramı

SMB1 açıldığında, tek bir kriter değil ikisini kontrol eder:

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 ve top score'un basamaklarının kontrolü -- warm start'un kalbi

Neden çift kontrol? Çünkü byte $A5 rastgele olabilir (başka bir oyunun bu değeri bırakması veya RAM çipinin varsayılan dinlenme durumu). Top score'un basamaklarının geçerli (0-9) olduğunu kontrol ederek, verilerin tutarlı olduğundan emin oluruz.

Neden Tennis tek çalışan oyun

SMB1'i ilk kez taktığımızda (cold start), oyun:

  1. Tüm RAM'i siler → top score = 0, world byte = 0
  2. $0787 adresine $A5 yazar

Sonra konsolu kapatmadan Tennis'e geçeriz. Tennis:

  • Başlangıçta RAM'i temizlemez (az sayıda NES oyunu bunu yapar)
  • Top score byte'larına yazmaz → 0'da kalırlar (geçerli)
  • $A5 byte'ına dokunmaz → mevcut kalır
  • $075F adresini oyuncu adım sayacı için kullanır
; 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'i geri taktığımızda:

  1. $A5 byte'ı hala orada (Tennis dokunmadı)
  2. Top score basamakları hala 0 (geçerli)
  3. World byte'ı artık 8+ (Tennis adımlarıyla arttı)
  4. SMB1 warm start tespit eder → bozulmuş world byte'ını korur
  5. A + Start'a basılı tut → world 9-1, world A-1, world 36-1, vb.

Neden önce Mario sonra Tennis başlatılmalı

Bir incelik: önce SMB1'i, sonra Tennis'i, sonra tekrar SMB1'i başlatmalısın. Doğrudan Tennis ile başlasaydın, $A5 byte'ı hiç yazılmazdı (Tennis $A5 yazmaz), bu yüzden warm start tespiti başarısız olurdu ve RAM silinirdi.

Tennis'in adım sayacı: her adım world byte'ını artırır

NES Tennis ile Glitch World'lere Erişim -- kart değişimi açıklayan video

SMB1 level'larını 40KB'a nasıl depolar

Nintendo R&D4, dışarıdan basit görünen bir sorunu çözmek zorundaydı: yatay kayan level'ları tile'lar, düşmanlar, nesnelerle temsil etmek, hepsini aşırı sıkı bir ROM bütçesinde.

Çözüm, tamamen bağımsız iki veri katmanına ayrılmadır:

Tile düzeni (level haritası)

Her level, ROM'da sıkıştırılmış bir tile yapısına işaret eden bir göstergeç ile tanımlanır. Sıkıştırma ilkel ama zekice: bir "kontrol" byte'ı ve ardından 1-3 byte veri.

Tile formatı bir çalıştırma (RLE benzeri) sistemi kullanır:

; 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

Her level 16 sütundan oluşan 13 satır tile içerir (13×16 = 208 görünür tile). Ama sıkıştırılmış format çok daha aşağı inebilir -- örneğin, gökyüzü ve boş sütunlar neredeyse hiç yer kaplamaz.

6502'deki işleme döngüsü:

; 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 düzeni (düşmanlar ve nesneler)

Paralel olarak, düşmanlar ve nesneler (soru blokları, borular, goombas, koopas) tamamen ayrı bir yapıda depolanır. Her spawn 2 byte ile tanımlanır:

; 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

Her level, farklı 5 sprite sayfasına kadar (yani, 16 sütundan oluşan 5 "ekran") başvurabilir, ama pratikte çoğu level sadece 2-3 tane kullanır.

Göstergeç tablosu

Tasarım dehası göstergeç tablosudur. Her level bir ROM adresi çifti olarak depolanır:

// 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];

Tablo başına 128 giriş. 4 alan türü. 512 olası kombinasyon, ama sadece bir kısmı resmi oyun tarafından kullanılır. Geri kalanı, başlatılmamış RAM veya göstergeç olarak yorumlanan verilerdir.

Oyun bir level yüklediğinde şunu yapar:

; 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

Doğrulama yok. Göstergeçin geçerli olup olmadığını kontrol etme yok. Oyun tablodaki adresi okur ve o adreste bulunanı sıkıştırır, nokta.

Level ID $06 (Water) -- 9-1, 6-2'nin sualtı versiyonu

Level ID tablosu: 128 olası giriş, 34 atanmış

Tile ve sprite göstergeçlerinin farklı sırası -- Frankenstein level'ların nedeni

34 benzersiz level ve 7-bit ID sistemi

NES'in RAM çipi (MB8416A) -- kartları değiştirdiğimizde verileri koruyan çip

SMB1'de 32 level değil, 34 benzersiz level vardır. Birçok level tekrardır (5-3 = 1-3 ama Bullet Bill'lerle) "hard mode" bayrağıyla işaretlenmiştir. Gerçek benzersiz level'lar:

  • Su (Tür 0): 3 level (2-2, 7-2, bonus alanı 5-2/6-2)
  • Overworld (Tür 1): 22 level (bulut bonus odaları dahil)
  • Underground (Tür 2): 3 level (yeraltı bonus odaları dahil)
  • Castle (Tür 3): 6 level
  • + 1 kesme sahnesi odası (yeraltı/su level'larından önce)
  • + 1 warp bölgesi 4-2'den

Her level 7 bit'lik bir ID'ye sahiptir. 5 alt bit = alt grup içindeki numara, 2 üst bit = alan türü:

; 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 olası ID ($00-$7F), sadece 34'ü gerçek level'lara atanmış. Kullanılmayan ID'ler herhangi bir şeye işaret eder.

Göstergeç tabloları: iki liste, iki sıra

Tile ve sprite göstergeçleri aynı sırayla depolanmaz. Kod, ayrı iki 16-bit liste kullanır (iki farklı tabloda high byte / low byte):

Sprite göstergeçlerinin sırası:
  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 göstergeçlerinin sırası:
  Index 0-2   : Water (3 level)
  Index 3-24  : Overworld (22 level)
  Index 25-27 : Underground (3 level)
  Index 28-33 : Castle (6 level)

Neden farklı sıralar? Teknik bir neden yok -- muhtemelen veriler geliştirme sırasında böyle düzenlendi. Ama bu büyüleyici bir sonuca yol açar: bir level ID'si geçersiz olduğunda, tile ve sprite göstergeçleri farklı level'lar yükler ve Frankenstein level'lar oluşturur.

Bu iki liste arasında gezinmek için oyun küçük offset tabloları kullanır (bir içindekiler tablosu gibi):

; 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 level'ını (ID $23, Overworld numara 3) yüklemek için:

; 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 ✓

Şimdi, $43 (Underground numara 3, var olmayan) gibi geçersiz bir ID ile ne olur?

; 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: 1-4 tile'ları + 5-2 su sprite'ları

Glitch Level Göstergeçlerini Keşfetmek -- offset tabloları açıklanıyor

World index tablosu -- world 9 taşması bir glitch level oluşturduğunda

World index tablosu: world 9 neden taşar

Her world'ün (1-8) ilk level'ının indexini veren 8 byte'lık bir ROM tablosu vardır. Ve hemen ardından, tüm level'ların oyun sırasına göre 36 Level ID tablosu.

; 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'u yüklemeye çalıştığımızda, oyun WorldIndexTable'ın 9. byte'ını okur... bu mevcut değildir. LevelIDTable'a 1 byte taşar, $25 değerini okur, sonra LevelIDTable'da indeks olarak $25 kullanır (37. giriş) -- bu SpriteOffsetTable'a 2 byte taşar ve 6 değerini okur.

; 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) için, taşma daha da ileri gidip 1-2'den önceki kesme sahnesi olan Level ID $01'e düşer:

; 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 world'ler neden var

Oyunun 32 "meşru" level'ı var (8 dünya × 4 level). Ama göstergeç tablosu alan türü başına 128 giriş yapıyor. 32. level'ın ötesindeki girişler, bu adreslerde ROM'da bulunanı içerir -- bazen başka bir level, bazen ses verileri, bazen RAM, bazen de herhangi bir şey.

Level ID $01 Water (Minus World) -- tile göstergeci $AE45, sprite göstergeci $A171

Level ID $01 + AreaType 0 = Minus World

Glitch world'lerin en ünlüsü. AreaType 0 (su) içindeki Level ID $01 şunlara işaret eder:

  • Tile göstergeci: $AE45 → 2-2/7-2'nin sualtı bölgesi
  • Sprite göstergeci: $A171 → 2-2/7-2'nin sprite'ları

Sonuç: 2-2'ye benzeyen ama sonsuza kadar dönen bir su level'ı çünkü flagpole mevcut değil. Level sonu yok, çıkış yok.

Bu 36-1 level'ıdır (veya $-1 dünyasında 36-1).

SMB1'in warm start kontrolü -- Minus World'ün var olmasını sağlayan

; 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'a işaret eden göstergeçler

Tile göstergeci veya sprite göstergeci ROM yerine RAM'daki ($00-$7F) bir adrese işaret ettiğinde, oyun RAM'deki sürekli değişimleri tile olarak yorumlamaya çalışır:

; 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.

Sıfır sayfası değiştiğinde (Mario hareket ettiğinde, zamanlayıcı döndüğünde vb.), level'ın "sprite'ları" da değişir. Bu yüzden bazı glitch world'lerde titreyen ve sürekli dönüşen düşmanlar bulunur.

Level ID $03 Water -- sprite göstergeci $009D RAM'a işaret ediyor, oynanamayan level

Level ID $36: boş level (Overworld)

Overworld'de Level ID $36:

  • Tile göstergeci: $AC35 (1-2)
  • Sprite göstergeci: $A0D8 (1-2)

Sonuç: hiçbir şey. Oyun level'ı yükler ama RGMechEx kataloğunda "levelsiz" olarak işaretlenmiştir. Tile'lar belki geçerlidir ama sprite'lar boş veya çalışmayan bir level üreten bir yere işaret eder.

Level ID $1D (Castle): çökme şampiyonu

Castle'da Level ID $1D:

  • Tile göstergeci: $A210 (4-4)
  • Sprite göstergeci: $7EA0 (RAM!)

Sprite göstergeci RAM'de = tanımsız sprite'lar. Oyun tile'ların ilk satırında bir Spiny topu veya Bullet Bill blaster göstermeye çalışır. Hemen çöker.

; 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

Kataloglanan 256 glitch world

RGMechEx, tüm level'ların haritasını oluşturan bir script yazdı, 4 alan türü ve her biri 128 ID için.

World sayacı 8 bit (0-255) üzerindedir. World 1-8 meşrudur. Kalan 248 potansiyel glitch world vardır. Her glitch world bu world'ün ilk level'ına karşılık gelir ve Level ID'si WorldIndexTable taşma mekanizması tarafından hesaplanır.

Glitch world tablosu -- 248 bozulmuş dünya, 68 erişilebilir ilk level

128 olası ID'den sadece 68'i bir world'ün "ilk level"ıdır (glitch world numarası ile erişilebilir). Diğer 60'ı 2+ level veya erişilemez.

Tür Oynanabilir benzersiz ID'ler Çöken ID'ler Boş ID'ler
Water (0) ~20 ~60 ~48
Overworld (1) ~30 ~55 ~43
Underground (2) ~15 ~65 ~48
Castle (3) ~25 ~58 ~45

Birçok ID aynı adrese düşen göstergeçler nedeniyle aynı level'a gider. Örneğin Level ID $28 (Overworld) -- tile göstergeci $A7CD (2-1) -- 38 farklı glitch world'de görünür, çünkü sprite göstergeci $9F51 ROM'un birçok ID tarafından yeniden kullanılan ses verileri/padding bölgesine işaret eder.

Level ID $28 (Overworld) haritası -- 2-1 tile'ları normal sprite'larla, 38 glitch world

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

Gerçekten benzersiz 6 glitch level

Erişilebilir 19 glitch level ID'sinden sadece 6'sı yükleme sırasında hemen çökmüyor:

World Level ID Açıklama
E-1 (224) $50 Bir uçurumun üzerinde tek bir ? bloğu. Mario anında ölür.
W $57 Mario spawn'da kilitli, hareket edemiyor.
42 (133) $50 Mario yeterince ileri giderse tuzağa düşüren bulut tüneli.
62 (131, 240) $4D Donmuş kale: Mario yukarıda spawn olur, düşemez → kilitli.
127 $4B Yeraltı tüneli, ama çok ileri gidersen çöker.
137 $4B Kesme sahnelerinin otomatik kaydırmayı etkinleştirir. Mario'yu ebediyen bloklayan tek bir tuğla blokla karşılaşır.

Level ID $50 (bulut tüneli) -- glitch world 42-1 ve E-1 Level ID $4D (kale) -- world 62-1, Mario spawn'da kilitli Level ID $4B (tünel) -- world 127-1, çok ileri gidersen çöker

248'den sadece 6 glitch world gerçekten yeni bir şey üretiyor. Geri kalanı, yanlış alan türüne sahip normal level'lar veya siyah ekranlar.

Level formatı detaylı olarak

Level verilerinin kesin formatına bir göz atalım, glitch level'ların neden ayakta durduğunu (ya da durmadığını) anlamak için.

Level başlığı: 2 byte, 6 özellik

Her level, 6 özelliği kontrol eden 2 byte'lık bir başlıkla başlar:

; 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)

Tür değiştirici görsel varyasyonları kontrol eder: su level'larının üstündeki dalgalar, 8-3'ün tuğla arka planı, 4-3'ün gece paleti, 6-2'nin karı vb.

Tile nesneleri: 2 byte, Next Screen Flag, 3 slot'luk kuyruk

Başlıktan sonra bir tile nesnesi listesi gelir, her nesne 2 byte. $FD byte'ı listenin sonunu işaret eder.

; 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

"Next screen" biti ayarlandığında, mevcut çalışma sütunu 1 artırılır. Bu, ilk 16 sütunun ötesine nesneler yerleştirmeye olanak tanır. Nesneler sırayla (soldan sağa) listelenmelidir çünkü oyun bunları sıralı olarak yükler:

; 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

Kuyruk tam olarak 3 slot yapar. Doğrudan sonuç: aynı sütunda başlayan 3'ten fazla nesne olamaz. Kuyruk doluysa, 4. nesne yok sayılır ve hiç yüklenmez.

Bu yüzden iyi tasarlanmış level'lar çok fazla nesne yığmaktan kaçınır. 1-2'deki örnek: tavan içindeki 1up bloğu ile yanındaki tuğlalar, 3 limitine uymak için iki ayrı nesneye bölünmüştür.

Özel Y pozisyonu: 12, 13, 14, 15

Y=12 olduğunda, nesnenin Y pozisyonu yoktur (tür tarafından hardcoded):

; 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 olduğunda, iki alt grup vardır. Byte 1'in 6. biti 1 ise:

; 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 ise, en düşük 5 bit bir ekran atlama (next screen flag ile tek tek geçmek yerine doğrudan N ekranına atlamak) kodlar.

Y=14 olduğunda: bit6=1 ile tür değiştiriciyi, bit6=0 ile arka planı + zemin desenini değiştirmek için aynı prensip.

Zemin desenleri: 16 zemin deseni

Level'ların zemini tek tek nesnelerden yapılmamıştır. SMB1 zemin desenleri kullanır, bir sonraki değişikliğe kadar tüm sütunlara uygulanan bir arka plan deseni:

; 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)

Bu yüzden delikler nesnedir: belirli bir sütunda zemin desenini override eder, geri kalanını değiştirmeye gerek kalmaz.

256 byte limiti ve tekrar

Bir level'ın tüm tile verileri en fazla 256 byte'a sığar. 6502'nin Y register'ı indeks olarak kullanılır ve 8 bit yapar. Oyun verilerin sonuna $FD byte'ı bulamadan ulaşırsa, başa döner ve 256 byte'ı sonsuza kadar tekrarlar:

; 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.

Bazı glitch level'lar bu tekrarı, level'ları "sonsuza kadar" sürecek şekilde üretmek için kullanır.

Sprite sistemi: 2 byte + boru geçişleri

Sprite'lar benzer bir format izler, ama başlıksız ve birkaç temel farkla. $FF byte'ı listenin sonunu işaret eder.

; 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'in en düşük biti hard level flag'idir: 1'e ayarlanırsa, sprite sadece 5-3 ve üzeri level'larda görünür. "Hard mode" level'ları böyle oluşturulur.

Y pozisyonu 15 = ekran atlama (tile'lar ile aynı). Y pozisyonu 14 = boru geçişi (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.

Sprite'ların kuyruk sistemi yoktur. Tek sınırlama, spawn bölgesinde (sağda ekranda) aynı anda 4'ten fazla sprite'ın yüklenememesidir. Aksi takdirde sprite'lar yok sayılır.

Glitch world'lere nasıl erişilir

İki ana yöntem vardır.

Klasik yöntem: wall clip

Wall clip (duvarlardan geçme), normal level'dan çıkıp gizli warp bölgesine yürümemizi sağlar. RAM üzerinden world sayacını manipüle ederek, herhangi bir Level ID yükleyebiliriz.

Teknik:

  1. World 1-2: gizli bitiş borusuna git
  2. Sağdaki duvarda wall clip yap
  3. Warp bölgesine kadar boşlukta yürü
  4. Oyun değerleri world olarak yorumlar

Ama bu yöntem sadece bir kısmına erişim sağlar.

Extreme yöntem: NES Tennis kart değişimi

Ayrıntılar için yukarıdaki "Warm start" bölümüne bakın. Özetle: Tennis'in adım sayacı, SMB1'in world byte'ına aynı RAM byte'ına yazar ve warm start tespiti bu değeri korur.

Hackçiler köşesi: keşfetmek için kod

Tüm glitch world'leri bir emülatörde kendin keşfetmek istersen, Level ID'sini doğrudan yamalayabilirsin:

; 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 level × 4 türün otomatik oluşturulmuş haritalarla tam listesini rgmechex.com adresinde yayınladı. Her giriş tile göstergecini, sprite göstergecini ve level'ın görsel haritasını gösterir.

En çok wtf dedirten level'lar

Level ID $1F (Water): 15 glitch world tek çatı altında

Tile göstergeci $A302 (3-4) ile sprite göstergeci $02A0 kombinasyonu 15 farklı glitch world verir (D-1, J-1, Y-1, Z-1, 55-1, 73-1...). Açıklama: sprite göstergeci, geçerli sprite'lara yeterince yakın veriler içeren bir ROM alanına işaret eder, ama 3-4 kale tile'larının overworld sprite'larıyla kombinasyonu saçma bir render üretir.

Level ID $28 (Overworld): 38 glitch world = rekor

Mutlak rekor. 38 glitch world girişi aynı level'a (2-1 tile + $9F51 sprite) işaret eder. Neden? Çünkü sprite göstergeci $9F51, birçok ID tarafından yeniden kullanılan ses verileri/padding bölgesine düşen bir ROM alanına işaret eder.

Level ID $49 (Underground): FDS level'ı

Tile göstergeci $76AE + sprite göstergeci $1C9D. Tile göstergeci, Famicom Disk System sürümüne ayrılmış ROM alanına işaret eder. Sonuç: standart kartta mevcut olmayan tile'lara sahip level. Bu level 52-1 ve 196-1 level'larını görünür kılar.

Level ID $00-$02: gerçek bonus level'ları

Bu ID'ler oyunun meşru alt level'ları tarafından kullanılır:

  • $00: 5-2/6-2 sualtı bölgesi (H-1, 39-1 tarafından kullanılır)
  • $01: 2-2/7-2 suyu (Minus World, 36-1)
  • $02: 8-4 alt level'ı (136-1, 151-1, 215-1)

Normalde erişilebilir bir "bonus" level ile glitch world arasındaki fark, warp bölgelerinin mevcut world'ü kontrol etmesidir:

; 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

8'den büyük veya 0 numaralı glitch world'ler normal borularla ulaşılamaz. Wall clip veya kart değişimi gerekir.

Bazı level'lar neden çöker: jump tabloları

Oyun bir tile nesnesi yüklediğinde, türünü bir jump tablosunda indeks olarak kullanır:

; 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 tabloları: neden geçersiz bir nesne türü oyunu çökertir

Bir nesnenin geçersiz türü varsa (≥12), oyun tabloda olmayan bir göstergeçe atlar. 4 olası sonuç:

  1. Geçerli göstergeç → nesne normal yüklenir
  2. Başka bir jump tablosuna göstergeç (çakışma) → farklı bir nesne görünür. Örnek: tür 12, Y=13 tablosuna işaret eder, bu bir L-pipe verir.
  3. Çalıştırılabilir koda göstergeç → rastgele kod çalıştırma (muhtemelen çökme)
  4. Açık NOP yer tutucusu → nesne hiçbir şey yapmaz (bazı sprite'lar böyledir, yerinde uçan ama hareket etmeyen düşmanlar üretir)

Glitch level ID $58: sprite göstergeci geçersiz bir adrese işaret ediyor, oyun çöküyor

Glitch level ID $50: bulut tüneli, bozulmuş verilerden üretilen level

Glitch level ID $58 (çöken tünel): sprite göstergeci, ROM mapper'ı olmayan NES'te mevcut olmayan bir bellek bölgesine işaret eder. Oyun aynı Koopa'yı (0,0) konumunda kare başına 5 kez yüklemeye çalışır, bu PPU'yu doyurur ve donmaya neden olur.

; 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

Boru warp paradoksu

target_world BETWEEN 1 AND 8 kontrolünü hatırla. Bir glitch world'de bir boru bulsan bile, oyun hedef world'ün 1 ile 8 arasında olduğunu kontrol eder. Glitch world'lerin numaraları 8'den büyüktür (36-1, 255-1...), bu yüzden warp başarısız olur.

Bu yüzden Minus World'ün sonu yoktur: flagpole sprite'larda mevcut değildir ve borular hiçbir yere götürmez.

Sütunda 5 nesne hilesi

Sütun başına 3 nesme limitini aşmaya izin veren bir köşe durumu vardır. Kuyruk dolduğunda (slot'lar dolu + sonraki nesnede next screen flag eksik), oyun mevcut sütunu bir next screen flag'li nesne bulana kadar döngü içinde "ön işler". Her ön işlem sırasında:

; 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).

Buna "queue skip" denir ve bazı romhacker'lar tarafından formatın normalde izin verdiğinden daha yoğun level'lar oluşturmak için kullanılır.

Sürümler arasındaki farklar

Famicom Disk System

SMB1'in FDS sürümü farklı bir bellek haritasına sahiptir. Tüm level göstergeçleri kaydırılmıştır ama veriler aynıdır. Değişen şey: glitch world endeksleri tamamen farklıdır:

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.

Favori FDS level'ım: ID $5F, 3-3'ün ikinci yarısının yeraltı versiyonu alçak tünelde (otomatik kaydırıcı olduğu için yazık).

The Lost Levels (Super Mario Bros. 2 Japon)

Lost Levels birçok şeyi değiştirir:

  1. Tile/sprite sırası aynı: Artık Frankenstein level'lar yok (tile'lar ve sprite'lar geçersiz ID ile bile aynı level'ı yükler)
  2. Tek 16-bit göstergeç tablosu ayrı high/low tabloları yerine
  3. 4 disk dosyası: ROM FDS için bölünmüş:
    • Dosya 1: world 1-4
    • Dosya 2: world 5-8
    • Dosya 3: world 9 + ses motoru
    • Dosya 4: world A-D (tamamen farklı göstergeç tablosu)
  4. Aynı Level ID = 4 olası level yüklenen dosyaya göre
  5. Artık Tennis glitch'i yok: continue seçeneği (game over'dan sonra aynı world'de devam) warm start'ı gereksiz yapar ve oyun hemen sıfırlar world > 9 ise
  6. Yeni nesneler: zehir mantarı, görünmeyen blok, görünmeyen ateş çiçeği bloğu, ters borular, rüzgar -- ama mevcut listelerin ortasına eklendi → SMB1 ile geriye uyumluluk sorunu
  7. Piranha Plant'lar world 4'ten sonra her zaman kırmızı, çengeller sadece world 2/B/3/C/7'de yeşil

Super Mario All-Stars (SNES)

Aynı 6502 rutinleriyle doğrudan port (SNES, NES kodunu uyumlu modda çalıştırır):

  • Warp bölgesi düzeltildi: Artık Minus World yok (soldaki boruya metinden önce girmek doğru world'e götürür)
  • Çökme: Çoğu glitch level çöker (ID $6A ve 9-1 hariç)
  • Kale nesneleri eklendi: Daha benzersiz render'lar
  • Ama: 4-2 wrong warp hala çalışıyor (yamalanmamış!)

4-2 wrong warp: bir nesne yerleştirme hatası

4-2'de iki boru geçiş nesnesi vardır: asma ipi (warp bölgesi) ve boru (para odası). İlk geçiş nesnesi (asma ipi olan), asma ipi ekranda görmeden çok önce yerleştirilmiştir. İkinci (boru), level'da çok geç yerleştirilmiştir.

; 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.

Döngü level'ları

Döngüler (8-4, 7-4) nasıl çalışır? Level, hardcoded ekran numaraları ve Y pozisyonlarına sahip kontrol noktalarına sahiptir:

; 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.

Formatı değiştir, kodu değil

Bu mimarinin en büyüleyici derslerinden biri, SMB1 geliştiricilerinin 6502 işleme koduna hiç dokunmadan son derece expresif bir level sistemi oluşturmayı başarmış olmasıdır. Level'lar arasındaki tüm çeşitlilik verilerden (göstergeçler, nesneler, sprite'lar, zemin desenleri) gelir, koddan değil.

248 glitch world, göstergeç tablolarının 128 giriş × 4 tür için boyutlandırılmış olmasından ve oyunun okuduğu değerleri hiç doğrulamamasından dolayı var. Bir göstergeç RAM'e düştüğünde, oyun Mario'nun register'larını tile olarak yorumlar. Bir göstergeç ses verilerine düştüğünde, oyun müziği level tasarımı olarak çalar. Ve jump tabloları taştığında, oyun çökene kadar her şeyi çalıştırır.

Daha Fazla Super Mario Bros. Mekanikleri Açıklandı -- 4. video

Tüm bunlardan neler öğrenebiliriz

  1. Tile/sprite ayrımı: iki katmanın toplam bağımsızlığı, benzersiz Frankenstein level'lar oluşturan farklı depolama sıralarıyla
  2. RLE sıkıştırması + nesne sistemi: level'lar bitmap'ler değil, yerleştirilmiş nesne listeleridir, zemin için zemin desenleriyle
  3. 3 slot'luk kuyruk: donanımın (ve level tasarımının) katı limiti
  4. Doğrulama yok: oyun göstergeçlere ve jump tablolarına güvenir, bu da ya oynanabilir glitch'ler ya da çökme üretir
  5. Maksimum 256 byte: 6502 Y register limiti, verilerin çok ileri gidildiğinde tekrar etmesine neden olur
  6. Warm start / cold start: "devam etme" sistemi, Tennis kart değişimi → Mario'nun kapısını açtı

En güzeli: tüm bunlar 40KB'a sığan 6502 kodudur. Soyutlama katmanı yok, bellek erişim doğrulaması yok, istisna yöneticisi yok. Göstergeç bozuksa, oyun çöker. Ve çökmelere glitch world diyoruz.

3 şey

  1. Glitch world'ler yanlış yere düşen göstergeçlerdir -- Oyunun 128 ID × 4 alan türü var ama sadece 34 benzersiz level. World numarası bozulduğunda (Tennis veya wall clip ile), oyun başka bir level için tasarlanmış bir göstergeç yükler ve 512 olası kombinasyon öngörülemeyen sonuçlar üretir.

  2. Minus World, warp hatasının bozulmayla birleşimidir -- 1-2'deki soldaki boru, metin appearing'den önce etkinleştirilirse, 36 (0x24) world'ünü yükler. Bu world, Level ID $01'e (2-2 suyu) işaret eder, flagpole'siz bir level. Ve world 36 için boru geçişi olmadığından, level sonsuza kadar döner. Doğrulama eksikliği ikonu yaratır.

  3. Tennis → Mario, OoT → Paper Mario'dan 15 yıl önce -- NES'in RAM'i, kondansatörler ve SMB1'in warm start / cold start sistemi sayesinde kart değişiminden hayatta kalır. Tennis'in adım sayacı (adım sesini çalarken bir RAM byte'ını artıran), world numarasının tam adresine düşer. Top score'un basamaklarının 0'da kalması, $A5 byte'ının sağlam olması ve oyunun warm start tespit etmesi gerekir -- sadece Tennis ile çalışan mükemmel bir koşullar birleşimi.

Orijinal videolar Retro Game Mechanics Explained tarafından yapılmıştır -- 6502 deassembly, tüm level'ların otomatik haritaları, kart değişimi ve warm start açıklamaları üzerindeki detay seviyesi mükemmel. Seriyi görmediysen, izle, kısa ve her dakikası yoğun.

Map'lerin kaynak kodu rgmechex.com adresinde mevcut ve SMB1'in tam deassembly'si birçok repo'da açık kaynak. 40 yıl önce, Japon programcılar bu level sistemini 6502'de sıfır birim testi ve sıfır hata izleyici ile yazdı ve bugün hala kodlarını açarak yeni şeyler öğreniyoruz.

Super Mario Bros.: il formato dei livelli, i puntatori e i 256 glitch worlds

Come 128 livelli × 4 tipi di zona stanno in 40KB di ROM, perché esiste il Minus World, e come una partita di Tennis NES può caricare dei glitch worlds.

Introduzione

Super Mario Bros. sono 40 kilobyte di ROM. Otto mondi, 32 livelli, nemici, musica, power-up, tutto ci sta dentro.

Ma se apri un emulatore e modifichi i byte giusti, puoi caricare il livello 36-1. Oppure 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 worlds esistono per una ragione semplice: il sistema di archiviazione dei livelli di SMB1 è una meraviglia di ottimizzazione a 8 bit, e quando si costringe il gioco a leggere dove non dovrebbe, i risultati sono affascinanti.

Retro Game Mechanics Explained ha fatto una serie di 4 video a riguardo -- li compileremo in un'unica immersione nel codice 6502 del gioco più venduto della sua epoca.

GLITCH OBJECTS -- il titolo della serie RGMechEx sulle meccaniche nascoste di SMB1

World 9-1 -- la schermata titolo del primo glitch world accessibile tramite il cart swap Tennis

Il warm start: perché la RAM di Tennis sopravvive in SMB1

Prima di parlare di archiviazione dei livelli, bisogna capire come SMB1 si avvia. Perché il glitch del cart swap NES Tennis si basa 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, senza interruzione dell'alimentazione), preserva un'area di 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 selezione mondo (B button) [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 : high 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 nello stesso mondo dopo un game over. Se muori in 6-3, il gioco scrive il mondo 6 nel byte di avvio, e nella schermata titolo, se tieni premuto A + Start, ricominci in 6-1.

I 41 byte preservati in RAM durante un warm start -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, e il byte magico $A5

La doppia verifica del warm start

Cold start vs warm start -- il diagramma di rilevamento del reset

Quando SMB1 si avvia, non verifica un solo criterio ma due:

CheckWarmStart:
  ; 1. Verificare il byte magico $A5 a $0787
  lda $0787
  cmp #$A5
  bne ColdStart        ; non è $A5 → cold start

  ; 2. Verificare le 6 cifre dell'high score ($0781-$0786)
  ;    Ogni cifra deve essere 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 viene cancellata, il mondo di partenza è preservato
  jmp WarmStartBoot

La verifica del byte $A5 e delle cifre dell'high score -- il cuore del warm start

Perché una doppia verifica? Perché il byte $A5 potrebbe essere presente per caso (un altro gioco che lascia questo valore, o lo stato di default del chip RAM). Verificando che le cifre dell'high score siano valide (0-9), ci si assicura che i dati siano coerenti.

Perché Tennis è l'unico gioco che funziona

Quando si inserisce SMB1 per la prima volta (cold start), il gioco:

  1. Cancella tutta la RAM → high score = 0, world byte = 0
  2. Scrive $A5 all'indirizzo $0787

Poi si passa a Tennis senza spegnere la console. Tennis:

  • Non pulisce la RAM all'avvio (pochi giochi NES lo fanno)
  • Non scrive sui byte dell'high score → rimangono a 0 (validi)
  • Non tocca il byte $A5 → rimane presente
  • Usa l'indirizzo $075F per il contatore dei 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 worlds !
;
; Il contore si incrementa solo quando la musica si ferma
; (i suoni dei passi non suonano durante la musica).

Quando si rimette SMB1:

  1. Il byte $A5 è ancora lì (Tennis non lo ha toccato)
  2. Le cifre dell'high score sono ancora 0 (valide)
  3. Il world byte vale ora 8+ (incrementato dai passi di Tennis)
  4. SMB1 rileva un warm start → preserva il world byte corrotto
  5. Mantenere A + Start → world 9-1, world A-1, world 36-1, ecc.

Perché bisogna avviare Mario prima di Tennis

Una sfumatura: bisogna prima avviare SMB1, poi Tennis, poi di nuovo SMB1. Se iniziassi direttamente con Tennis, il byte $A5 non sarebbe mai scritto (Tennis non scrive $A5), quindi il rilevamento warm start fallirebbe e la RAM sarebbe cancellata.

Il contatore dei passi di Tennis: ogni footstep incrementa il world byte

Access Glitch Worlds via NES Tennis -- il video che spiega il cart swap

Come SMB1 archivia i suoi livelli in 40KB

Nintendo R&D4 ha dovuto risolvere un problema semplice in apparenza: rappresentare livelli che scrollano orizzontalmente con tile, nemici, oggetti, il tutto in un budget ROM ultra-stretto.

La soluzione è una separazione in due livelli 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 utilizza un sistema di run (simile all'RLE):

; Formato tile SMB1 (semplificato)
; Ogni "comando" è un byte controllo:
;
; $00-$7F : posiziona una tile, avanza di 1 colonna
; $80-$BF : posiziona 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 compressato permette di scendere molto più in basso -- per esempio, il cielo e le colonne vuote non occupano quasi spazio.

Il loop di rendering in 6502:

; Decompressione tile - loop 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 singola
  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

Il sprite layout (nemici e oggetti)

In parallelo, i nemici e gli oggetti (blocchi ?, tubi, goombas, koopas) sono archiviati in una struttura completamente separata. Ogni spawn è definito da 2 byte:

; Formato sprite SMB1
; Byte 0: posizione X (in colonne)
; Byte 1: tipo sprite + bit 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ò fare riferimento fino a 5 pagine di sprite diverse (beh, 5 "schermate" da 16 colonne), ma in pratica la maggior parte dei livelli ne usa solo 2-3.

La tabella dei puntatori

Il genio del design è la tabella dei puntatori. Ogni livello è archiviato come una coppia di indirizzi ROM:

// Struttura interna (semplificata) della World Map
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 che vengono 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 nella 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

Nessuna validazione. Nessuna verifica che il puntatore sia valido. Il gioco legge l'indirizzo nella tabella e decomprime ciò che si trova a quell'indirizzo, punto.

Level ID $06 (Water) -- 9-1, la versione sottomarina di 6-2

La tabella dei Level ID: 128 voci possibili, 34 assegnate

L'ordine diverso dei puntatori tile e sprite -- la causa dei Frankenstein levels

I 34 livelli unici e il sistema di ID a 7 bit

Il chip RAM della NES (MB8416A) -- è lui che preserva i dati quando si scambiano le cartucce

SMB1 non ha 32 livelli, ma 34 livelli unici. Molti livelli sono duplicati (5-3 = 1-3 ma con dei Bullet Bills) contrassegnati da una 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 (inclusi i 2 locali nuvola bonus)
  • Underground (Tipo 2): 3 livelli (inclusi i locali bonus sotterranei)
  • Castello (Tipo 3): 6 livelli
  • + 1 local cutscene (prima dei livelli sotterranei/acqua)
  • + 1 warp zone di 4-2

Ogni livello ha un ID su 7 bit. I 5 bit meno significativi = numero nel sottogruppo, i 2 bit più significativi = tipo di zona:

; Codifica 7-bit del Level ID
; Bit 6-5: Tipo (00=Water, 01=Overworld, 10=Underground, 11=Castle)
; Bit 4-0: Numero nel sottogruppo
;
; Water ID       : $00-$02  (tipi 00, numeri 0-2)
; Overworld ID   : $20-$35  (tipi 01, numeri 0-21)
; Underground ID : $40-$42  (tipi 10, numeri 0-2)
; Castle ID      : $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 verso qualsiasi cosa.

Le tabelle dei puntatori: due liste, due ordini

I puntatori tile e sprite non sono archiviati 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   : Castello (6 livelli)
  Indice 6-27  : Overworld (22 livelli)
  Indice 28-30 : Underground (3 livelli)
  Indice 31-33 : Acqua (3 livelli)

Ordine dei puntatori tile:
  Indice 0-2   : Acqua (3 livelli)
  Indice 3-24  : Overworld (22 livelli)
  Indice 25-27 : Underground (3 livelli)
  Indice 28-33 : Castello (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 livello non è valido, i puntatori tile e sprite caricano livelli diversi, creando dei Frankenstein levels.

Per navigare tra queste due liste, il gioco usa delle 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 nella tabella di offset = 1
; 2. Sprite offset = SpriteOffsetTable[1] = 6
;    Indice finale = 6 + 3 (numero 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 ✓

Ora, cosa succede con un ID non valido 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 Bloopers della zona acqua di 5-2. Un vero Frankenstein.

Level ID $43 -- Frankenstein level: tile 1-4 + sprite acqua 5-2

Exploring Glitch Level Pointers -- le tabelle di offset spiegate

La world index table -- quando l'overflow di world 9 crea un glitch level

La world index table: perché world 9 fa 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 nell'ordine di gioco.

; WorldIndexTable (8 byte)
  .byte 0, 5, 10, 15, 20, 25, 28, 33
;   -> Il Mondo 1 inizia al livello 0
;   -> Il Mondo 2 inizia al livello 5
;   -> Il Mondo 8 inizia al livello 33

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

Quando si prova a caricare world 9, il gioco legge il 9° byte di WorldIndexTable... che non esiste. Fa overflow di 1 byte in LevelIDTable, legge il valore $25, poi usa $25 come indice in LevelIDTable (37° voce) -- il che fa 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 → livello Water numero 6 (che non esiste)
;   4. Tile pointer = puntatore water numero 6 = tile di 6-2
;   5. Sprite pointer = indice 31+6 = 37 > 33 → puntatore non valido
;   6. Risultato: 6-2 sott'acqua con sprite corrotti
;      → world 9-1 !

Per world G (16), l'overflow va ancora più in là 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 worlds esistono

Il gioco ha 32 livalli "legittimi" (8 mondi × 4 livelli). Ma la tabella dei puntatori ha 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 tra i glitch worlds. 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 acqua che sembra 2-2, ma che si ripete all'infinito perché l'asta non esiste. Nessuna fine livello, nessuna uscita.

È il livello 36-1 (o 36-1 nel mondo $-1).

Il warm start check di SMB1 -- è lui che permette al Minus World di esistere

; Perché manca l'asta nel Minus World:
; Gli sprite di 2-2/7-2 ($A171) non hanno un'asta
; 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 raggiunge zero.

I puntatori che puntano verso la RAM

Quando il tile pointer o lo sprite pointer punta verso un indirizzo in RAM ($00-$7F) piuttosto che in ROM, il gioco cerca di interpretare i costanti cambiamenti 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 tutto questo come una sequenza di sprite,
; e il risultato è un livello con nemici
; che in realtà sono valori di registro.

Quando la pagina zero cambia (perché Mario si muove, il timer scorre, ecc.), gli "sprite" del livello cambiano anche. Ecco perché certi glitch worlds hanno nemici che lampeggiano e si trasformano costantemente.

Level ID $03 Water -- sprite pointer $009D punta verso la RAM, livello ingiocabile

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 è contrassegnato "senza livello" nel catalogo di RGMechEx. Le tile potrebbero essere valide ma gli sprite puntano verso un posto che produce un livello vuoto o non funzionante.

Level ID $1D (Castello): il campione dei crash

Level ID $1D in Castello:

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

Sprite pointer in RAM = sprite indefiniti. Il gioco cerca di visualizzare una Spiny ball o un Bullet Bill blaster nella prima riga di tile. Causo un crash immediato.

; Quando lo sprite pointer punta verso la RAM,
; il gioco decomprime byte che cambiano costantemente
; come istruzioni "spawn". Il risultato:
; - Apparizione di oggetti inesistenti (valore indefinito)
; - Crash PPU quando lo sprite NES cerca di visualizzare una tile non valida
; - Freeze completo della console

I 256 glitch worlds 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 dei mondi è a 8 bit (0-255). I mondi 1-8 sono legittimi. Restano 248 glitch worlds potenziali. Ogni glitch world corrisponde al primo livello di quel mondo, e il suo Level ID è calcolato dal meccanismo di overflow della WorldIndexTable.

Tabella dei glitch worlds -- 248 mondi corrotti, 68 primi livelli accessibili

Dei 128 ID possibili, solo 68 sono "primo livello" di un mondo (accessibili tramite il numero del glitch world). I restanti 60 sono livelli 2+ o inaccessibili.

Tipo ID unici giocabili ID che crashano ID vuoti
Water (0) ~20 ~60 ~48
Overworld (1) ~30 ~55 ~43
Underground (2) ~15 ~65 ~48
Castle (3) ~25 ~58 ~45

Molti ID portano allo stesso livello a causa dei puntatori che cadono sugli stessi indirizzi ROM. Il Level ID $28 (Overworld) per esempio -- tile pointer $A7CD (2-1) -- appare in 38 glitch worlds diversi, perché il suo sprite pointer $9F51 punta verso una zona della ROM che è usata come padding/dati sonori riutilizzato da molti ID.

Mappa del livello ID $28 (Overworld) -- tile di 2-1 con sprite normali, 38 glitch worlds

Super Mario Bros. Glitch Levels Explained -- il 3° video

I 6 glitch levels davvero unici

Tra i 19 ID di glitch level accessibili, solo 6 non crashano immediatamente 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 nuvola che intrappola Mario se va troppo avanti.
62 (131, 240) $4D Castello ghiacciato: Mario spawna in alto, non può cadere → bloccato.
127 $4B Tunnel sotterraneo, ma crasha se si va troppo avanti.
137 $4B Attiva lo scorrimento automatico delle cutscene. Mario incontra un unico blocco mattoni che lo blocca per sempre.

Level ID $50 (cloud tunnel) -- il glitch world 42-1 e E-1 Level ID $4D (castle) -- world 62-1, Mario bloccato allo spawn Level ID $4B (tunnel) -- world 127-1, crasha se si va troppo avanti

Sei glitch worlds su 248 che producono qualcosa di davvero nuovo. Il resto sono livelli normali con il tipo di zona sbagliato, o schermate nere.

Il formato dei livelli nel dettaglio

Al via sul formato esatto dei dati di livello, per capire perché i glitch levels reggono (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 + modificatore
;   Bit 7-6: timer (00=invariato, 01=200, 10=300, 11=400)
;   Bit 5-3: Y start Mario (111/110 = autowalk)
;   Bit 2-0: modificatore tipo livello
;              000=default, 001=onde, 002=muro mattoni,
;              011=fondo acqua, 100=notte, 101=neve,
;              110=neve notte, 111=notte grigia

; Byte 1: piattaforma + sfondo + modello pavimento
;   Bit 7-6: piattaforma speciale (00=albero, 01=fungo,
;                                 10=Bullet Bill, 11=nuvola)
;   Bit 5-4: sfondo (00=nessuno, 01=nuvole,
;                     10=montagne, 11=recinzioni)
;   Bit 3-0: modello pavimento iniziale (0-15)

Il modificatore tipo controlla variazioni visive: le onde in cima ai livelli acqua, il fondo mattoni di 8-3, la palette notte di 4-3, la neve di 6-2, ecc.

Gli oggetti tile: 2 byte, flag Next Screen, coda 3 slot

Dopo lheader 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, corda, blocchi ?)
;     Y=13    : salto schermata / oggetti speciali 2
;     Y=14    : cambio modificatore/paesaggio/pavimento
;     Y=15    : oggetti speciali 3 (castello, scale, tubo grosso)

; Byte 1:
;   Bit  7   : FLAG NEXT SCREEN
;   Bit 6-4  : tipo oggetto (0-7)
;   Bit 3-0  : larghezza/altezza / sottotipo

Quando il bit "next screen" è impostato, la colonna di lavoro corrente viene incrementata di 1. Questo permette di posizionare oggetti oltre le prime 16 colonne. Gli oggetti devono essere elencati in ordine (da sinistra a destra) perché il li carica sequenzialmente:

; La routine di caricamento ha DUE fasi per colonna:
; Fase 1: cercare i nuovi oggetti che iniziano su questa colonna
;          e aggiungerli alla coda (queue)
; Fase 2: processare ogni oggetto nella coda disegnando le tile,
;          e rimuovere quelli che finiscono su questa colonna

La coda ha esattamente 3 slot. Conseguenza diretta: non si possono avere più di 3 oggetti che iniziano sulla stessa colonna. Se la coda è piena, il 4° oggetto viene ignorato e non sarà mai caricato.

Ecco perché i livelli ben progettati evitano di impilare troppi oggetti. Esempio in 1-2: la colonna con il 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 una posizione Y (è hardcodata per tipo):

; Y=12: oggetti senza posizione Y
;   Tipo 0: buco (rimuove il pavimento)
;   Tipo 1: corda 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/martello (fine castello)
;   4 = stop screen, 5 = nemici casuali, 6 = loop level, 7+ = crash possibile

Se bit6=0, i 5 bit meno significativi codificano un salto schermata (saltare direttamente a uno schermo N, senza passare per la flag next screen una per una).

Quando Y=14: stesso principio con bit6=1 per cambiare il modificatore tipo, bit6=0 per cambiare lo sfondo + modello pavimento.

I floor patterns: 16 motivi di pavimento

Il pavimento dei livelli non è fatto di oggetti singolari. SMB1 usa dei floor patterns, un motivo di sfondo che si applica a tutte le colonne fino al prossimo cambio:

; Floor patterns (4 bit = 16 possibilità)
;   0 = vuoto totale
;   1 = pavimento 2 tile alto
;   2 = pavimento 1 tile alto
;   3 = pavimento + fondo
;   4 = pavimento + fondo 2
;   5 = pavimento 1/2 tile
;   6 = 3/4 pavimento
;   ... fino a 15 = riempito totale (pavimento + soffitto)

Ecco perché i buchi sono oggetti: override il 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, ed è a 8 bit. Se il gioco arriva alla fine dei dati senza trovare il byte $FD, ricomincia da capo e ripete i 256 byte all'infinito:

; Indice Y = 8 bit → max 256 byte di dati tile
; Se Y overflow (255 → 0) senza incontrare $FD → repeat
; Stessa cosa per gli sprite, ma gli oggetti pipe (3 byte)
; spostano la parità dell'indice a ogni caricamento.

Certi glitch levels 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 alcune differenze chiave. Il byte $FF segna la fine della lista.

; Formato sprite (2 byte):
; Byte 0: posizione X (colonna)
; Byte 1:
;   Bit 7: FLAG NEXT SCREEN
;   Bit 6-0: tipo 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 meno significativo del byte 1 è la flag hard level: se impostato a 1, lo sprite appare solo nei livelli ≥ 5-3. È così che vengono creati i livelli "hard mode".

Posizione Y 15 = salto schermata (identica alle tile). Posizione Y 14 = transizione pipe (3 byte):

; Sprite Y=14: transizione pipe/vite (3 byte!)
;   Byte 0: posizione X
;   Byte 1: bit 6-0 = Level ID 7-bit (destinazione)
;   Byte 2: bit 4-0 = schermo di destinazione
;            bit 7-5 = mondo dove questa transizione è valida
;
; Perché un mondo? Le stanze bonus vengono riutilizzate tra i 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
; riappariva nel posto giusto.

Gli sprite non hanno un sistema di coda. L'unica limite è che non possono esserci più di 4 sprite caricati simultaneamente nella zona di spawn (appena fuori schermo a destra). Oltre, gli sprite vengono ignorati.

Come accedere ai glitch worlds

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 warp zone nascosta. Manipolando il contatore dei mondi tramite la RAM, si può caricare qualsiasi Level ID.

La tecnica:

  1. World 1-2: andare nel tubo finale 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 worlds.

Il metodo estremo: NES Tennis cart swap

Vedi la sezione "Il warm start" sopra per il dettaglio completo. In sintesi: il contatore dei passi di Tennis scrive sullo stesso byte RAM del mondo di partenza di SMB1, e il rilevamento warm start preserva quel valore.

L'angolo dei hacker: il codice per esplorare tutto

Se vuoi esplorare tutti i glitch tu stesso in un emulatore, puoi modificare 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, aprire il traceur memoria e scrivere:
; $075F = 0x39
; $0760 = 0x01
; Poi entrare in un tubo di warp o morire e ricominciare
; → 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 worlds in uno

Il tile pointer $A302 (3-4) combinato allo sprite pointer $02A0 dà 15 glitch worlds 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 sufficientemente vicini a sprite validi per produrre risultati giocabili, ma la combinazione delle tile del castello 3-4 con sprite di overworld crea un rendering assurdo.

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

Il record assoluto. 38 voci di glitch world puntano verso lo stesso livello (tile di 2-1 + sprite $9F51). Perché? Perché lo sprite pointer $9F51 cade in una zona della ROM che è usata come padding/dati sonori riutilizzato da molti 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 (usato 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 corrente:

; Verifica warp zone (semplificata)
; 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
  ; mondo valido solo tra 1 e 8
  jmp DoWarp

I glitch worlds con numeri > 8 o 0 non possono essere raggiunti da 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 ?, nascosto, flagpole...
  .word Obj_Platform      ; tipo 1: piattaforma speciale
  .word Obj_BrickRow      ; tipo 2: riga di mattoni
  .word Obj_BlockRow      ; tipo 3: riga di blocchi
  .word Obj_CoinRow       ; tipo 4: riga 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

Le jump table: perché un tipo oggetto non valido fa crashare il gioco

Se un oggetto ha un tipo non valido (≥12), il gioco salta a un puntatore che non esiste in questa tabella. 4 risultati 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 alla tabella Y=13, che dà un L-pipe.
  3. Puntatore verso codice eseguibile → esecuzione di codice casuale (crash probabile)
  4. Placeholder esplicito (NOP) → l'oggetto non fa nulla (certi sprite sono così, producendo nemici che volano sul posto senza muoversi)

Glitch level ID $58: lo sprite pointer punta verso un indirizzo non valido, il gioco crasha

Glitch level ID $50: il cloud tunnel, un livello generato da dati corrotti

Il glitch level ID $58 (il tunnel che crasha): il suo sprite pointer punta verso una regione di memoria che non esiste su NES senza mapper ROM. Il gioco cerca di 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 non valido (fuori spazio NES standard)
; → Il gioco legge byte indeterminati come tipi sprite
; → Il Koopa $00 (tipo non valido senza handler) fa un loop con chiamata ricorsiva
; → Stack overflow 6502 → freeze

Il paradosso pipe warp

Ricordati la verifica 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 worlds hanno numeri > 8 (36-1, 255-1...), quindi la warp fallisce.

È anche per questo che il Minus World non ha fine: l'asta non è presente negli sprite, e i tubi non portano da nessuna parte.

Il trucco degli 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 successivo con flag next screen mancante), il gioco "pre-elabora" la colonna corrente in loop fino a trovare un oggetto con flag next screen. Durante ogni pre-elaborazione:

; Durante la pre-elaborazione della colonna:
; 1. Gli oggetti nella coda vedono la loro larghezza rimanente
;    decrementata ad ogni "falsa avanzata" di colonna
; 2. Se un oggetto raggiunge 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 processati sulla stessa colonna.
; Tecnica: posizionare 2 oggetti che attraversano il confine schermo
; (slot 1 e 2), 1 oggetto dummy con X < precedente (blocca la coda),
; poi 3 oggetti a X=0 dello schermo successivo (di cui uno con flag next screen).

Questo è ciò che si chiama "queue skip" ed è usato da certi romhackers per creare livelli più densi di quanto il formato permetta normalmente.

Le differenze tra le 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 worlds sono completamente diversi:

FDS World 36 → Level ID $09 (versione acqua di 5-3)
  → L'asta è 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, a differenza della versione NES.

Il mio livello FDS preferito: ID $5F, una versione sotterranea della seconda metà di 3-3 in tunnel basso (peccato che sia un autoscroller).

The Lost Levels (Super Mario Bros. 2 giapponese)

Lost Levels cambia molte cose:

  1. Ordine identico tile/sprite: niente più Frankenstein levels (tile e sprite caricano lo stesso livello anche con un ID non valido)
  2. Una sola tabella puntatori 16-bit invece di due tabelle separate high/low
  3. 4 file disco: la ROM è stata suddivisa per il FDS:
    • File 1: mondi 1-4
    • File 2: mondi 5-8
    • File 3: world 9 + sound engine
    • File 4: mondi A-D (tabella puntatori completamente diversa)
  4. Stesso Level ID = 4 livelli possibili a seconda del file caricato
  5. Niente più glitch Tennis: l'opzione continue (continuare nello stesso mondo dopo game over) rende il warm start inutile, e il gioco resetta immediatamente se world > 9
  6. Nuovi oggetti: fungo velenoso, blocco invisibile, fiore di fuoco invisibile, tubi capovolti, vento -- ma inseriti nel mezzo delle liste esistenti → incompatibilità retroattiva con SMB1
  7. Piranha Plants sempre rosse dopo world 4, trampolini verdi solo nei mondi 2/B/3/C/7

Super Mario All-Stars (SNES)

Porting diretto con le stesse routine 6502 (il SNES esegue il codice NES in modalità compatibile):

  • Warp zone corretta: niente più Minus World (entrare nel tubo sinistro prima del testo porta al mondo giusto)
  • Crash: la maggior parte dei glitch levels crashano (tranne ID $6A e 9-1)
  • Oggetti castello aggiunti: resi più unici
  • Ma: il 4-2 wrong warp funziona ancora (non corretto!)

Il 4-2 wrong warp: un bug di posizionamento oggetti

In 4-2, ci sono due oggetti di transizione pipe: la vite (warp zone) e il tubo (stanza coin cash). Il primo oggetto di transizione (quello della vite) è posizionato ben prima che la vite appaia sullo schermo. Il secondo (il tubo) è posizionato troppo tardi nel livello.

; Timing delle transizioni in 4-2:
; Oggetto transizione 1 (vite → warp zone): posizionato 3 schermate prima della vite
; Oggetto transizione 2 (tuyau → coin cash): posizionato 1 schermata 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+destra), la transizione della vite
; è ancora attiva quando tocca il tubo!
; → Il gioco carica la warp zone invece del coin cash.
;
; Se l'oggetto fosse stato posizionato 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 dei checkpoints con numeri di schermo e posizioni Y hardcodati:

; Checkpoint: {schermo_numero, posizione_verticale}
; Se Mario passa questo checkpoint alla giusta altezza → il livello continua
; Altrimenti → warp indietro di 4 schermate (64 blocchi)
;
; Per fare un loop infinito: posizione_verticale = $F0
; (sotto il fondo dello schermo) → impossibile validare.
;
; I checkpoints sono semplici (una sola flag) tranne per world 7
; che usa tripletti (3 flag, bisogna fallirne almeno 1)
;
; Il warp indietro è brutale: offset di tile data impostato a un valore
; hardcodato, offset di sprite data rimesso a 0. I nemici presenti
; vengono scaricati istantaneamente → le firebars scompaiono.

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 patterns), non dal codice.

I 256 glitch worlds esistono perché le tabelle dei 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 fanno overflow, il gioco esegue qualsiasi cosa fino al crash.

More Super Mario Bros. Mechanics Explained -- il 4° video

Cosa si può imparare da tutto questo

  1. Separazione tile/sprite: indipendenza totale dei due livelli, con ordini di archiviazione diversi che creano Frankenstein levels unici
  2. Compressione RLE + sistema di oggetti: i livelli non sono bitmap ma liste di oggetti posizionati, con floor patterns per il pavimento
  3. Coda 3 slot: limite rigido dell'hardware (e del design del livello)
  4. Nessuna validazione: il gioco si fida dei puntatori e delle jump table, il che produce o glitch giocabili o crash
  5. 256 byte max: il limite del registro Y del 6502, che fa sì che i dati si ripetano se si va troppo avanti
  6. Warm start / cold start: un sistema di "continuare" che ha aperto la porta al cart swap Tennis → Mario

Il più bello: tutto questo è codice 6502 che sta in 40KB. Nessun livello di astrazione, nessuna validazione degli accessi di memoria, nessun gestore eccezioni. Se il puntatore è marcio, il gioco crasha. E i crash li chiamiamo glitch worlds.

Le 3 cose da ricordare

  1. I glitch worlds sono puntatori che cadono 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 progettato 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 al Level ID $01 (acqua di 2-2), un livello senza asta. E come non c'è una transizione pipe per world 36, il livello si ripete all'infinito. L'assenza di verifica crea l'icona.

  3. Tennis → Mario, 15 anni prima di OoT → Paper Mario -- La RAM della NES sopravvive a uno swap di cartuccia grazie ai condensatori e al sistema di warm start / cold start di SMB1. Il contatore dei passi di Tennis ( che incrementa un byte RAM suonando il suono dei passi) cade esattamente sull'indirizzo del world number. Bisogna che le cifre dell'high score rimangano a 0, che il byte $A5 sia intatto, e che il gioco rilevi un warm start -- una coincidenza perfetta che ha funzionato solo con Tennis.

I video originali di Retro Game Mechanics Explained sono un lavoro immane -- il livello di dettaglio sulla disassembla 6502, le mappe automatiche di tutti i livelli, le spiegazioni del cart swap e del warm start. Se non hai visto la serie, guardala, è breve e ogni minuto è denso.

Il codice sorgente delle mappe è disponibile su rgmechex.com, e la disassembla completa di SMB1 è open source su tanti repository. 40 anni fa, dei programmatori giapponesi hanno scritto questo sistema di livelli in 6502 con zero test unitari e zero bug tracker, e continuiamo a imparare cose aprendo il loro codice oggi.

Super Mario Bros.: Das Level-Format, die Zeiger und die 256 Glitch Worlds

Wie 128 Levels × 4 Zonentypen in 40KB ROM passen, warum die Minus World existiert, und wie ein NES-Tennis-Match Glitch Worlds laden kann.

Einführung

Super Mario Bros. – das sind 40 Kilobyte ROM. Acht Welten, 32 Levels, Gegner, Musik, Power-ups, alles passt da rein.

Aber wenn du einen Emulator öffnest und die richtigen Bytes herumwurstelst, kannst du den Level 36-1 laden. Oder den 255-1. Oder in einer Welt landen, die komplett aus Bowser-Sprites und Rohren besteht, die nirgendwo hinführen.

Diese Glitch Worlds existieren aus einem einfachen Grund: Das Level-Speichersystem von SMB1 ist ein Meisterwerk der 8-Bit-Optimierung, und wenn man das Spiel zwingt, dort zu lesen, wo es nicht sollte, kommt man zu faszinierenden Ergebnissen.

Retro Game Mechanics Explained hat eine 4-teilige Videoserie darüber gemacht -- wir kompilieren das hier zu einem einzigen Deep Dive in den 6502-Code des meistverkauften Spiels seiner Zeit.

GLITCH OBJECTS -- der Titel der RGMechEx-Serie über die versteckten Mechaniken von SMB1

World 9-1 -- der Titelbildschirm der ersten Glitch World, die man über den Kart-Tausch mit Tennis erreichen kann

Der Warm Start: Warum die RAM von Tennis in SMB1 überlebt

Bevor wir über Level-Speicherung reden, müssen wir verstehen, wie SMB1 startet. Denn der Glitch des NES-Tennis-Kart-Tauschs basiert vollständig auf dem Warm-Start / Cold-Start-Erkennungssystem des Spiels.

Die 41 Bytes, die erhalten bleiben

Wenn SMB1 einen Cold Start erkennt (erstes Einschalten oder Power Off/On), löscht es die gesamte RAM. Aber wenn es einen Warm Start erkennt (Reset-Knopf, kein Stromausfall), bleibt ein Speicherbereich von 41 Bytes erhalten:

; 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]

Diese 41 Bytes dienen nur einer einzigen Funktion: Es soll dem Spieler ermöglichen, nach einem Game Over in derselben Welt weiterzuspielen. Wenn du in 6-3 stirbst, schreibt das Spiel die Welt 6 in den Start-Byte, und am Titelbildschirm, wenn du A + Start gedrückt hältst, startest du in 6-1.

Die 41 Bytes, die bei einem Warm Start in der RAM erhalten bleiben -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD und der Magische Byte $A5

Die doppelte Überprüfung des Warm Start

Cold Start vs. Warm Start -- das Erkennungsdiagramm des Resets

Wenn SMB1 hochfährt, überprüft es nicht nur ein Kriterium, sondern zwei:

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

Die Überprüfung des $A5-Bytes und der Top-Score-Ziffern -- das Herz des Warm Starts

Warum eine doppelte Überprüfung? Weil das $A5-Byte zufällig vorhanden sein könnte (ein anderes Spiel, das diesen Wert lässt, oder der Ruhezustand des RAM-Chips). Indem man überprüft, dass die Top-Score-Ziffern gültig sind (0-9), stellt man sicher, dass die Daten konsistent sind.

Warum Tennis das einzige Spiel ist, das funktioniert

Wenn man SMB1 zum ersten Mal einlegt (Cold Start), tut das Spiel:

  1. Löscht die gesamte RAM → Top Score = 0, World-Byte = 0
  2. Schreibt $A5 an die Adresse $0787

Dann tauscht man auf Tennis, ohne die Konsole auszuschalten. Tennis:

  • Löscht die RAM beim Start nicht (wenige NES-Spiele tun das)
  • Schreibt nicht auf die Top-Score-Bytes → sie bleiben bei 0 (gültig)
  • Berührt das $A5-Byte nicht → es bleibt vorhanden
  • Verwendet die Adresse $075F für den Schrittzähler des Spielers
; 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).

Wenn man SMB1 wieder einlegt:

  1. Das $A5-Byte ist immer noch da (Tennis hat es nicht berührt)
  2. Die Top-Score-Ziffern sind immer noch 0 (gültig)
  3. Das World-Byte hat jetzt 8+ (erhöht durch die Schritte von Tennis)
  4. SMB1 erkennt einen Warm Start → bewahrt das korrupte World-Byte auf
  5. A + Start gedrückt halten → World 9-1, World A-1, World 36-1, usw.

Warum man Mario vor Tennis starten muss

Eine Kleinigkeit: Man muss zuerst SMB1 starten, dann Tennis, dann wieder SMB1. Wenn man direkt mit Tennis beginnen würde, würde das $A5-Byte nie geschrieben (Tennis schreibt kein $A5), und die Warm-Start-Erkennung würde fehlschlagen und die RAM würde gelöscht.

Der Schrittzähler von Tennis: Jeder Schritt erhöht das World-Byte

Glitch Worlds über NES Tennis erreichen -- das Video, das den Kart-Tausch erklärt

Wie SMB1 seine Levels in 40KB speichert

Nintendo R&D4 musste ein scheinbar einfaches Problem lösen: Horizontale scrollende Levels mit Tiles, Gegnern und Items darstellen, und das alles in einem extrem knappen ROM-Budget.

Die Lösung: Eine Trennung in zwei komplett unabhängige Datenebenen:

Das Tile-Layout (die Levelkarte)

Jedes Level wird durch einen Zeiger auf eine komprimierte Tile-Struktur in der ROM definiert. Die Kompression ist simpel aber genial: Ein "Kontroll"-Byte gefolgt von 1-3 Datenbytes.

Das Tile-Format verwendet ein System von Runs (RLE-ähnlich):

; 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

Jedes Level enthält 13 Zeilen mit 16 Spalten Tiles (13×16 = 208 sichtbare Tiles). Aber das komprimierte Format erlaubt es, viel weiter unten zu starten -- zum Beispiel der Himmel und leere Spalten nehmen fast keinen Platz ein.

Die Render-Schleife in 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

Das Sprite-Layout (Gegner und Objekte)

Parallel dazu werden Gegner und Objekte (Blöcke ?, Rohren, Goombas, Koopas) in einer komplett separaten Struktur gespeichert. Jeder Spawn ist durch 2 Bytes definiert:

; 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

Jedes Level kann bis zu 5 verschiedene Sprite-Seiten referenzieren (also 5 "Screens" mit 16 Spalten), aber in der Praxis verwenden die meisten Level nur 2-3.

Die Zeigertabelle

Das Genie des Designs ist die Zeigertabelle. Jedes Level wird als Paar von ROM-Adressen gespeichert:

// 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 Einträge pro Tabelle. 4 Zonentypen. 512 mögliche Kombinationen, aber nur ein Bruchteil wird vom offiziellen Spiel genutzt. Der Rest ist nicht initialisierte RAM oder Daten, die als Zeiger interpretiert werden.

Wenn das Spiel ein Level lädt, macht es das:

; 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

Keine Validierung. Keine Prüfung, ob der Zeiger gültig ist. Das Spiel liest die Adresse in der Tabelle und dekomprimiert, was sich an dieser Adresse befindet, 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 der Tile- und Sprite-Zeiger -- die Ursache der Frankenstein-Levels

Die 34 einzigartigen Levels und das 7-Bit-ID-System

Der RAM-Chip der NES (MB8416A) -- er bewahrt die Daten auf, wenn man die Cartridges tauscht

SMB1 hat nicht 32 Levels, sondern 34 einzigartige Levels. Viele Levels sind Duplikate (5-3 = 1-3, aber mit Bullet Bills), die mit einem "Hard Mode"-Markierungs-Flag versehen sind. Die echten einzigartigen Levels:

  • Wasser (Typ 0): 3 Levels (2-2, 7-2, Bonus-Zone 5-2/6-2)
  • Overworld (Typ 1): 22 Levels (inklusive der 2 Wolken-Bonus-Räume)
  • Underground (Typ 2): 3 Levels (inklusive der unterirdischen Bonusräume)
  • Castle (Typ 3): 6 Levels
  • + 1 Cutscene-Raum (vor den unterirdischen/Wasser-Levels)
  • + 1 Warp-Zone von 4-2

Jedes Level hat eine ID auf 7 Bits. Die unteren 5 Bits = Nummer in der Untergruppe, die oberen 2 Bits = Zonentyp:

; 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 mögliche IDs ($00-$7F), nur 34 sind echten Levels zugewiesen. Die ungenutzten IDs zeigen auf irgendetwas.

Die Zeigertabellen: Zwei Listen, zwei Reihenfolgen

Die Tile- und Sprite-Zeiger werden nicht in derselben Reihenfolge gespeichert. Der Code verwendet zwei separate 16-Bit-Listen (High Byte / Low Byte in zwei unterschiedlichen Tabellen):

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)

Warum unterschiedliche Reihenfolgen? Kein technischer Grund -- das war wahrscheinlich einfach so, wie die Daten während der Entwicklung organisiert wurden. Aber das erzeugt eine faszinierende Konsequenz: Wenn eine Level-ID ungültig ist, laden Tile- und Sprite-Zeiger unterschiedliche Levels und erzeugen so Frankenstein-Levels.

Um zwischen diesen beiden Listen zu navigieren, verwendet das Spiel kleine Offset-Tabellen (wie ein Inhaltsverzeichnis):

; 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

Um Level 6-2 zu laden (ID $23, Overworld Nummer 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 ✓

Was passiert jetzt mit einer ungültigen ID wie $43 (Underground Nummer 3, die es nicht gibt)?

; 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 + Wasser-Sprites 5-2

Glitch Level Pointers erkunden -- die Offset-Tabellen erklärt

Die World-Index-Tabelle -- wenn der Overflow von World 9 ein Glitch Level erzeugt

Die World-Index-Tabelle: Warum World 9 überläuft

Es gibt eine ROM-Tabelle mit 8 Bytes, die den Index des ersten Levels jeder Welt (1-8) angibt. Und direkt danach die Tabelle der 36 Level-IDs aller Levels in der Spielreihenfolge.

; 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

Wenn man versucht, World 9 zu laden, liest das Spiel das 9. Byte der WorldIndexTable... das nicht existiert. Es überläuft um 1 Byte in die LevelIDTable, liest den Wert $25 und verwendet dann $25 als Index in der LevelIDTable (37. Eintrag) -- was wiederum um 2 Bytes in die SpriteOffsetTable überläuft und den Wert 6 liest.

; 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 !

Für World G (16) geht der Overflow noch weiter und trifft auf Level ID $01, der der Cutscene-Raum ist, der 1-2 vorausgeht:

; 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

Warum die Glitch Worlds existieren

Das Spiel hat 32 "legitime" Levels (8 Welten × 4 Levels). Aber die Zeigertabelle hat 128 Einträge pro Zonentyp. Die Einträge jenseits von Level 32 enthalten das, was sich in der ROM an diesen Adressen befindet -- manchmal ein anderes Level, manchmal Audiodaten, manchmal RAM, manchmal irgendetwas.

Level ID $01 Water (Minus World) -- Tile-Zeiger $AE45, Sprite-Zeiger $A171

Level ID $01 + AreaType 0 = Minus World

Das berühmteste der Glitch Worlds. Die Level ID $01 in AreaType 0 (Wasser) zeigt auf:

  • Tile-Zeiger: $AE45 → der Unterwasserbereich von 2-2/7-2
  • Sprite-Zeiger: $A171 → die Sprites von 2-2/7-2

Das Ergebnis: Ein Wasserturn, der 2-2 ähnelt, aber endlos loopt, weil es die Flaggenstange nicht gibt. Kein Level-Ende, kein Ausgang.

Das ist Level 36-1 (oder 36-1 in der Welt $-1).

Der Warm-Start-Check von SMB1 -- er lässt die Minus World existieren

; 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.

Die Zeiger, die auf die RAM zeigen

Wenn der Tile-Zeiger oder der Sprite-Zeiger auf eine Adresse in der RAM ($00-$7F) statt in der ROM zeigt, versucht das Spiel, die ständig wechselnden RAM-Werte als Tiles zu interpretieren:

; 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.

Wenn sich die Page Zero ändert (weil Mario sich bewegt, der Timer läuft, usw.), ändern sich auch die "Sprites" des Levels. Deshalb haben manche Glitch Worlds Gegner, die flackern und sich ständig verwandeln.

Level ID $03 Water -- Sprite-Zeiger $009D zeigt auf RAM, unspielbarer Level

Level ID $36: Der leere Level (Overworld)

Level ID $36 in Overworld:

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

Ergebnis: Nichts. Das Spiel lädt das Level, aber es ist als "kein Level" im Katalog von RGMechEx markiert. Die Tiles sind vielleicht gültig, aber die Sprite zeigen auf eine Stelle, die ein leeres oder nicht funktionierendes Level erzeugt.

Level ID $1D (Castle): Der Champion der Abstürze

Level ID $1D in Castle:

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

Sprite-Zeiger in RAM = undefinierte Sprites. Das Spiel versucht, einen Spiny Ball oder einen Bullet Bill Blaster in der ersten Tile-Zeile anzuzeigen. Das crasht sofort.

; 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

Die 256 katalogisierten Glitch Worlds

RGMechEx hat ein Script geschrieben, das die Maps aller Levels für die 4 Zonentypen und je 128 IDs generiert.

Der Welt-Zähler ist 8 Bit breit (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 durch den Overflow-Mechanismus der WorldIndexTable berechnet.

Tabelle der Glitch Worlds -- 248 korrupte Welten, 68 erste Levels zugänglich

Von den 128 möglichen IDs sind nur 68 das "erste Level" einer Welt (zugänglich über die Glitch-World-Nummer). Die restlichen 60 sind Level 2+ oder nicht erreichbar.

Typ Einzigartig spielbare IDs Crash-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 auf dasselbe Level, weil die Zeiger auf dieselben ROM-Adressen treffen. Level ID $28 (Overworld) zum Beispiel -- Tile-Zeiger $A7CD (2-1) -- erscheint in 38 verschiedenen Glitch Worlds, weil sein Sprite-Zeiger $9F51 auf einen Bereich der ROM zeigt, der als Padding/Audiodaten wiederverwendet wird, der von vielen IDs genutzt wird.

Levelkarte 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

Von den 19 zugänglichen Glitch-Level-IDs crashten nur 6 nicht sofort beim Laden:

Welt Level ID Beschreibung
E-1 (224) $50 Ein einzelner ? Block über einem Abgrund. Mario stirbt sofort.
W $57 Mario spawnnt blockiert, kann sich nicht bewegen.
42 (133) $50 Wolken-Tunnel, der Mario einfängt, wenn er weit genug geht.
62 (131, 240) $4D Eingefrorenes Schloss: Mario spawnnt oben, kann nicht fallen → blockiert.
127 $4B Unterirdischer Tunnel, aber crasht, wenn man zu weit geht.
137 $4B Aktiviert automatisches Scrollen der Cutscenes. Mario trifft auf einen einzigen Brick-Block, der ihn für immer blockiert.

Level ID $50 (Wolken-Tunnel) -- Glitch World 42-1 und E-1 Level ID $4D (Schloss) -- World 62-1, Mario beim Spawn blockiert Level ID $4B (Tunnel) -- World 127-1, crasht wenn man zu weit geht

Sechs Glitch Worlds von 248, die etwas wirklich Neues erzeugen. Der Rest sind normale Levels mit dem falschen Zonentyp oder schwarze Bildschirme.

Das Level-Format im Detail

Schauen wir uns das genaue Format der Level-Daten an, um zu verstehen, warum Glitch Levels funktionieren (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=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)

Der Modifier-Typ steuert visuelle Variationen: die Wellen oben in den Wasserturns, der Backstein-Hintergrund von 8-3, die Nacht-Palette von 4-3, der Schnee von 6-2, usw.

Die Tile-Objekte: 2 Bytes, Next-Screen-Flag, 3-Slot-Warteschlange

Nach dem Header folgt eine Liste von Tile-Objekten, jedes Objekt ist 2 Bytes groß. Das Byte $FD markiert das Ende der 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

Wenn das "Next Screen"-Bit gesetzt ist, wird die aktuelle Arbeits-Spalte um 1 erhöht. Das erlaubt es, Objekte jenseits der ersten 16 Spalten zu platzieren. Die Objekte müssen in der Reihenfolge aufgelistet werden (links nach rechts), weil das Spiel sie sequenziell lädt:

; 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

Die Warteschlange hat genau 3 Slots. Direkte Konsequenz: Es können nicht mehr als 3 Objekte auf derselben Spalte beginnen. Wenn die Warteschlange voll ist, wird das 4. Objekt ignoriert und nie geladen.

Deshalb vermeiden gut designte Levels, zu viele Objekte zu stapeln. Beispiel in 1-2: Die Spalte mit dem 1up-Block an der Decke + die Backsteine daneben sind in zwei separate Objekte aufgeteilt, um die 3er-Limit einzuhalten.

Spezielle Y-Positionen: 12, 13, 14, 15

Wenn Y=12, hat das Objekt keine Y-Position (sie ist je nach Typ hardcodiert):

; 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

Wenn Y=13, gibt es zwei Untergruppen. Wenn Bit 6 von Byte 1 auf 1 steht:

; 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

Wenn bit6=0, kodieren die unteren 5 Bits einen Screen-Skip (direkt zu einem Screen N springen, ohne eins nach dem anderen über das Next-Screen-Flag zu gehen).

Wenn Y=14: Gleiches Prinzip mit bit6=1 zum Ändern des Modifier-Typs, bit6=0 zum Ändern des Hintergrunds + Bodenmusters.

Die Floor Patterns: 16 Bodenmuster

Der Boden der Levels besteht nicht aus einzelnen Objekten. SMB1 verwendet Floor Patterns, ein Hintergrundmuster, das für alle Spalten bis zur nächsten Änderung gilt:

; 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)

Deshalb sind Löcher Objekte: Sie überschreiben das Floor Pattern auf einer bestimmten Spalte, ohne das Muster für den Rest ändern zu müssen.

Die 256-Byte-Grenze und das Repeat

Alle Tile-Daten eines Levels passen in maximal 256 Bytes. Das Y-Register des 6502 wird als Index verwendet und hat 8 Bit. Wenn das Spiel am Ende der Daten angelangt, ohne das $FD-Byte zu finden, schlingt es zum Anfang zurück und wiederholt die 256 Bytes endlos:

; 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.

Manche Glitch Levels nutzen dieses Repeat, um Levels zu erzeugen, die "unendlich" dauern.

Das Sprite-System: 2 Bytes + Pipe-Übergänge

Die Sprites folgen einem ähnlichen Format, aber ohne Header und mit einigen Unterschieden. Das Byte $FF markiert das Ende der 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

Das niederwertigste Bit von Byte 1 ist das Hard-Level-Flag: Wenn es auf 1 gesetzt ist, erscheint das Sprite nur in Levels ≥ 5-3. So werden die "Hard Mode"-Levels erstellt.

Y-Position 15 = Screen-Skip (identisch mit Tiles). Y-Position 14 = Pipe-Übergang (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.

Die Sprites haben kein Queue-System. Die einzige Grenze ist, dass nicht mehr als 4 Sprites gleichzeitig im Spawn-Bereich (genau rechts außerhalb des Bildschirms) geladen werden können. Darüber hinaus werden Sprites ignoriert.

Wie man auf die Glitch Worlds zugreift

Es gibt zwei Hauptmethoden.

Die klassische Methode: Der Wall Clip

Der Wall Clip (Durch Wände gehen) erlaubt es, aus dem normalen Level herauszukommen und zur versteckten Warp-Zone zu laufen. Indem man den Welt-Zähler über die RAM manipuliert, kann man jede beliebige Level-ID laden.

Die Technik:

  1. World 1-2: In das versteckte Endrohr gehen
  2. Den Wall Clip an der rechten Wand machen
  3. Durch die Leere zur Warp-Zone laufen
  4. Das Spiel interpretiert die Werte als Welten

Aber diese Methode gibt Zugriff nur auf einen kleinen Teil der Glitch Worlds.

Die extreme Methode: NES-Tennis-Kart-Tausch

Siehe den Abschnitt "Der Warm Start" oben für die vollständige Zusammenfassung. Kurz gefasst: Der Schrittzähler von Tennis schreibt auf dasselbe RAM-Byte wie die Startwelt von SMB1, und die Warm-Start-Erkennung bewahrt diesen Wert auf.

Für Bastler: Der Code zum Erkunden

Wenn du alle Glitch Worlds selbst in einem Emulator erkunden möchtest, kannst du die Level-ID direkt patchen:

; 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 hat die vollständige Liste aller 128 Levels × 4 Typen mit automatisch generierten Maps auf rgmechex.com veröffentlicht. Jeder Eintrag zeigt den Tile-Zeiger, den Sprite-Zeiger und eine visuelle Karte des Levels.

Die verrücktesten Levels

Level ID $1F (Water): 15 Glitch Worlds in einem

Der Tile-Zeiger $A302 (3-4) kombiniert mit dem Sprite-Zeiger $02A0 ergibt 15 verschiedene Glitch Worlds (D-1, J-1, Y-1, Z-1, 55-1, 73-1...). Erklärung: Der Sprite-Zeiger zeigt auf einen Bereich der ROM, der Daten enthält, die nahe genug an gültigen Sprites sind, um spielbare Ergebnisse zu erzeugen, aber die Kombination der Schloss-Tiles von 3-4 mit Overworld-Sprites erzeugt ein absurd erscheinendes 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 der Sprite-Zeiger $9F51 in einen Bereich der ROM trifft, der als Padding/Audiodaten wiederverwendet wird, der von vielen IDs genutzt wird.

Level ID $49 (Underground): Der FDS-Level

Tile-Zeiger $76AE + Sprite-Zeiger $1C9D. Der Tile-Zeiger zeigt auf den Bereich der ROM, der für die Famicom-Disk-System-Version reserviert ist. Ergebnis: Ein Level mit Tiles, die in der Standard-Cartridge nicht existieren. Das ist das Level, das die Levels 52-1 und 196-1 erscheinen lässt.

Level ID $00-$02: Die echten Bonus-Level

Diese IDs werden von legitimen Unterlevels des Spiels verwendet:

  • $00: Unterwasserbereich von 5-2/6-2 (verwendet von H-1, 39-1)
  • $01: Das Wasser von 2-2/7-2 (die Minus World, 36-1)
  • $02: Unterlevel von 8-4 (136-1, 151-1, 215-1)

Der Unterschied zwischen einem normalerweise zugänglichen "Bonus"-Level und einer Glitch World ist, dass die Warp-Zonen die aktuelle Welt überprüfen:

; 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 mit Nummern > 8 oder 0 können nicht durch normale Rohren erreicht werden. Man braucht den Wall Clip oder den Kart-Tausch.

Warum manche Levels crashen: Die Jump-Tabellen

Wenn das Spiel ein Tile-Objekt lädt, verwendet es dessen Typ als Index in einer Jump-Tabelle:

; 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

Die Jump-Tabellen: Warum ein ungültiger Objekttyp das Spiel zum Absturz bringt

Wenn ein Objekt einen ungültigen Typ hat (≥12), springt das Spiel zu einem Zeiger, der in dieser Tabelle nicht existiert. 4 mögliche Ergebnisse:

  1. Gültiger Zeiger → das Objekt lädt normal
  2. Zeiger auf eine andere Jump-Tabelle (Überlappung) → ein anderes Objekt erscheint. Beispiel: Typ 12 zeigt auf die Tabelle Y=13, was einen L-Pipe ergibt.
  3. Zeiger auf ausführbaren Code → Ausführung von zufälligem Code (wahrscheinlicher Crash)
  4. Expliziter Platzhalter (NOP) → das Objekt macht nichts (manche Sprites sind so und erzeugen Gegner, die an Ort und Stelle fliegen, ohne sich zu bewegen)

Glitch Level ID $58: Der Sprite-Zeiger zeigt auf eine ungültige Adresse, das Spiel crasht

Glitch Level ID $50: Der Wolken-Tunnel, ein Level, das durch korrupte Daten erzeugt wird

Das Glitch Level ID $58 (der Tunnel, der crasht): Sein Sprite-Zeiger zeigt auf einen Speicherbereich, der auf NES ohne ROM-Mapper nicht existiert. Das Spiel versucht, denselben Koopa 5 Mal pro Frame an der Position (0,0) zu laden, was die Sättigung der PPU und einen Freeze verursacht.

; 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

Das Pipe-Warp-Paradox

Erinnerst du dich an den Check target_world BETWEEN 1 AND 8? Selbst wenn du eine Rohr in einer Glitch World findest, überprüft das Spiel, ob die Zielswelt zwischen 1 und 8 liegt. Glitch Worlds haben Nummern > 8 (36-1, 255-1...), also schlägt die Warp fehl.

Deshalb hat auch die Minus World kein Ende: Die Flaggenstange ist in den Sprites nicht vorhanden, und die Rohren führen nirgendwo hin.

Der Trick mit den 5 Objekten in einer Spalte

Es gibt einen Edge Case, der es erlaubt, die Grenze von 3 Objekten pro Spalte zu umgehen. Wenn die Warteschlange blockiert (Slots voll + nächstes Objekt ohne Next-Screen-Flag), "vorverarbeitet" das Spiel die aktuelle Spalte in einer Schleife, bis es ein Objekt mit Next-Screen-Flag findet. Bei jeder Vorverarbeitung:

; 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).

Das nennt man einen "Queue Skip" und wird von manlichen Rom-Hackern verwendet, um dichtere Levels zu erstellen, als das Format normalerweise erlaubt.

Die Unterschiede zwischen Versionen

Famicom Disk System

Die FDS-Version von SMB1 hat eine andere Memory Map. Alle Level-Zeiger sind verschoben, aber die Daten sind dieselben. Was sich ändert: Die Indizes der Glitch Worlds sind völlig unterschiedlich:

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.

Mein Lieblings-FDS-Level: ID $5F, eine unterirdische Version der zweiten Hälfte von 3-3 als tiefer Tunnel (schade, dass es ein Autoscroller ist).

The Lost Levels (Super Mario Bros. 2 japanische Version)

Lost Levels ändert viele Dinge:

  1. Identische Tiles/Sprites-Reihenfolge: Keine Frankenstein-Levels mehr (Tiles und Sprites laden dasselbe Level, auch bei ungültiger ID)
  2. Eine einzige 16-Bit-Zeigertabelle statt zwei separater High/Low-Tabellen
  3. 4 Disk-Dateien: Die ROM wurde für das FDS aufgeteilt:
    • Datei 1: Welten 1-4
    • Datei 2: Welten 5-8
    • Datei 3: World 9 + Sound-Engine
    • Datei 4: Welten A-D (völlig andere Zeigertabelle)
  4. Gleiche Level-ID = 4 mögliche Levels je nach geladener Datei
  5. Kein Tennis-Glitch mehr: Die Continue-Option (im selben World nach Game Over weitermachen) macht den Warm Start überflüssig, und das Spiel resettet sofort wenn World > 9
  6. Neue Objekte: Giftiger Pilz, unsichtbarer Block, unsichtbarer Feuerblumen-Block, umgedrehte Rohren, Wind -- aber mitten in bestehende Listen eingefügt → Abwärtsinkompatibilität mit SMB1
  7. Piranha Plants immer rot nach World 4, grüne Springbrünne nur in Welten 2/B/3/C/7

Super Mario All-Stars (SNES)

Direkter Port mit denselben 6502-Routinen (der SNES führt den NES-Code im kompatiblen Modus aus):

  • Warp-Zone gefixt: Keine Minus World mehr (Eingang in die linke Rohr vor dem Text führt zur richtigen Welt)
  • Absturz: Die meisten Glitch Levels crashten (außer ID $6A und 9-1)
  • Schloss-Objekte hinzugefügt: Machen die Levels einzigartiger
  • Aber: Der 4-2 Wrong Warp funktioniert immer noch (nicht gepatcht!)

Der 4-2 Wrong Warp: Ein Bug bei der Objektplatzierung

In 4-2 gibt es zwei Pipe-Übergangsobjekte: Die Ranke (Warp-Zone) und das Rohr (Coin-Cash-Raum). Das erste Übergangsobjekt (das der Ranke) wird weit bevor die Ranke auf dem Bildschirm erscheint, platziert. Das zweite (das Rohr) wird zu spät im Level platziert.

; 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.

Die Loop-Levels

Wie funktionieren die Loops (8-4, 7-4)? Das Level hat Checkpoints mit hardcodierten Screen-Nummern und Y-Positionen:

; 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.

Das Format ändern, nicht den Code

Eine der faszinierendsten Lektionen dieser Architektur ist, dass die Entwickler von SMB1 es geschafft haben, ein sehr ausdrucksstarkes Level-System zu schaffen, ohne jemals den 6502-Render-Code anzufassen. Die gesamte Variation zwischen den Levels kommt von den Daten (Zeiger, Objekte, Sprites, Floor Patterns), nicht vom Code.

Die 256 Glitch Worlds existieren, weil die Zeigertabellen für 128 Einträge × 4 Typen dimensioniert sind und das Spiel nie die Werte validiert, die es liest. Wenn ein Zeiger in die RAM trifft, interpretiert das Spiel die Register von Mario als Tiles. Wenn ein Zeiger in die Audiodaten trifft, spielt das Spiel Musik in Form von Level-Design. Und wenn die Jump-Tabellen überlaufen, führt das Spiel irgendeinen Code aus bis zum Crash.

More Super Mario Bros. Mechanics Explained -- das 4. Video

Was man daraus lernen kann

  1. Tiles/Sprites-Trennung: Volle Unabhängigkeit der beiden Ebenen, mit unterschiedlichen Speicherreihenfolgen, die einzigartige Frankenstein-Levels erzeugen
  2. RLE-Kompression + Objekt-System: Levels sind keine Bitmaps, sondern Listen platzierter Objekte mit Floor Patterns für den Boden
  3. 3-Slot-Warteschlange: Harte Grenze des Hardwares (und des Level-Designs)
  4. Keine Validierung: Das Spiel vertraut den Zeigern und Jump-Tabellen, was entweder spielbare Glitchs oder Abstürze erzeugt
  5. 256 Bytes max: Die Grenze des Y-Registers des 6502, wodurch sich Daten wiederholen, wenn man zu weit geht
  6. Warm Start / Cold Start: Ein "Weiterspielen"-System, das die Tür für den Tennis-Kart-Tausch → Mario geöffnet hat

Das Schönste: All das ist 6502-Code, der in 40KB passt. Keine Abstraktionsschicht, keine Speicherzugriffsvalidierung, keine Ausnahmebehandlung. Wenn der Zeiger Mist ist, crasht das Spiel. Und Abstürze nennen wir Glitch Worlds.

Die 3 wichtigsten Punkte

  1. Glitch Worlds sind Zeiger, die ins Leere treffen -- Das Spiel hat 128 IDs × 4 Zonentypen, aber nur 34 einzigartige Levels. Wenn die World-Nummer korrupt ist (durch Tennis oder Wall Clip), lädt das Spiel einen Zeiger, der für ein anderes Level gedacht war, und die 512 möglichen Kombinationen erzeugen unvorhersehbare Ergebnisse.

  2. Die Minus World ist ein Warp-Bug kombiniert mit Korruption -- Die linke Rohr in 1-2, wenn sie aktiviert wird, bevor der Text erscheint, lädt World 36 (0x24). Diese Welt zeigt auf Level ID $01 (Wasser von 2-2), ein Level ohne Flaggenstange. Und da es keinen Pipe-Übergang für World 36 gibt, loopt das Level endlos. Das Fehlen der Überprüfung erschafft das Ikon.

  3. Tennis → Mario, 15 Jahre vor OoT → Paper Mario -- Die RAM der NES überlebt einen Cartridge-Tausch dank der Kondensatoren und dem Warm-Start-/Cold-Start-System von SMB1. Der Schrittzähler von Tennis (der ein RAM-Byte erhöht, während er den Schritt-Sound abspielt) trifft genau auf die Adresse der World-Nummer. Die Top-Score-Ziffern müssen bei 0 bleiben, das $A5-Byte muss intakt sein und das Spiel muss einen Warm Start erkennen -- eine perfekte Koinzidenz, die nur mit Tennis funktioniert hat.

Die Originalvideos von Retro Game Mechanics Explained sind ein verdammt fleißiges Meisterwerk -- das Detailliveau bei der 6502-Dissassemble, die automatischen Maps aller Levels, die Erklärungen des Kart-Tauschs und des Warm Starts. Wenn du die Serie noch nicht gesehen hast, schau sie an, sie ist kurz und jede Minute ist dicht.

Der Quellcode der Maps ist auf rgmechex.com verfügbar, und die vollständige Dissassemble von SMB1 ist Open Source in vielen Repos. Vor 40 Jahren haben japanische Programmierer dieses Level-System in 6502 geschrieben, mit null Unit-Tests und null Bug-Tracker, und wir lernen immer noch Dinge, indem wir ihren Code heute öffnen.

Super Mario Bros.: формат уровней, указатели и 256 глитч-миров

Как 128 уровней × 4 типа зон влезают в 40КБ 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

World 9-1 -- экран загрузки первого глитч-мира, доступного через cart swap с теннисом

Тёплый старт: почему RAM тенниса выживает в SMB1

Прежде чем говорить о хранении уровней, надо понять, как стартует SMB1. Потому что глитч cart swap с NES Tennis целиком и полностью опирается на систему определения тёплого/холодного старта игры.

41 байт, который сохраняется

Когда SMB1 определяет холодный старт (первая включение или power off/on), он очищает всю RAM. Но когда определяет тёплый старт (сброс кнопкой, без отключения питания), он сохраняет область памяти в 41 байт:

; 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 байт служат одной-единственной цели: дать игроку возможность продолжить с того же мира после game over. Если умереть на 6-3, игра записывает мир 6 в байт старта, и на титульном экране, если зажать A + Start, начнёшь с 6-1.

Сохранённые 41 байт RAM при тёплом старте -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD и магический байт $A5

Двойная проверка тёплого старта

Холодный старт vs тёплый старт -- схема определения сброса

Когда 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

Проверка байта $A5 и цифр top score -- сердце тёплого старта

Зачем двойная проверка? Потому что байт $A5 может оказаться на месте случайно (другая игра, оставившая это значение, или состояние покоя по умолчанию чипа RAM). Проверяя, что цифры top score допустимы (0-9), игра убеждается, что данные согласованы.

Почему только теннис работает

Когда впервые вставляешь SMB1 (холодный старт), игра:

  1. Очищает всю RAM → top score = 0, world byte = 0
  2. Записывает $A5 по адресу $0787

Затем переключаешься на Tennis без выключения консоли. Tennis:

  • Не чистит RAM при запуске (мало какие игры NES это делают)
  • Не пишет в байты top score → они остаются 0 (валидные)
  • Не трогает байт $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. Байт $A5 всё ещё на месте (Tennis его не трогал)
  2. Цифры top score всё ещё 0 (валидные)
  3. World byte теперь 8+ (увеличен шагами тенниса)
  4. SMB1 определяет тёплый старт → сохраняет повреждённый world byte
  5. Зажимаем A + Start → world 9-1, world A-1, world 36-1 и т.д.

Почему надо загружать Mario перед теннисом

Тонкость: сначала нужно загрузить SMB1, потом Tennis, потом снова SMB1. Если начать сразу с тенниса, байт $A5 никогда не будет записан (Tennis не пишет $A5), и определение тёплого старта не сработает -- RAM будет очищена.

Счётчик шагов тенниса: каждый шаг увеличивает world byte

Доступ к глитч-мирам через NES Tennis -- видео, объясняющее cart swap

Как SMB1 хранит свои уровни в 40КБ

Nintendo R&D4 пришлось решить задачу, которая на первый взгляд кажется простой: представить уровни с горизонтальным скроллингом, тайлами, врагами, предметами -- и всё это уместить в ультра-жёсткий бюджет ROM.

Решение -- разделение на два полностью независимых слоя данных:

Tile layout (карта уровня)

Каждый уровень определяется указателем на структуру тайлов, сжатую в ROM. Сжатие примитивное, но гениальное: байт "управления", за которым следуют 1-3 байта данных.

Формат тайлов использует систему run'ов (как 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 колонок тайлов (13×16 = 208 видимых тайлов). Но сжатый формат позволяет снизить размер значительно ниже -- например, небо и пустые колонки почти не занимают места.

Цикл рендеринга на 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 (враги и объекты)

Параллельно враги и объекты (блоки ?, трубы, гумбы, купы) хранятся в полностью отдельной структуре. Каждый спавн определяется 2 байтами:

; 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 страниц различных спрайтов (точнее, 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 назначены

Разный порядок указателей тайлов и спрайтов -- причина Frankenstein levels

34 уникальных уровня и 7-битная система ID

Чип RAM NES (MB8416A) -- он сохраняет данные при замене картриджей

SMB1 имеет не 32, а 34 уникальных уровня. Многие уровни -- дубликаты (5-3 = 1-3, но с Bullet Bills), помеченные флагом "hard mode". Настоящие уникальные уровни:

  • Вода (Тип 0): 3 уровня (2-2, 7-2, бонусная зона 5-2/6-2)
  • Overworld (Тип 1): 22 уровня (включая 2 бонусных облачных комнаты)
  • Underground (Тип 2): 3 уровня (включая подземные бонусные комнаты)
  • Castle (Тип 3): 6 уровней
  • + 1 комната кат-сцены (перед подземными/водными уровнями)
  • + 1 warp-зона 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 возможных ID ($00-$7F), только 34 назначены реальным уровням. Неиспользуемые ID указывают на что угодно.

Таблицы указателей: два списка, два порядка

Указатели тайлов и спрайтов хранятся в разном порядке. Код использует две отдельные 16-битные таблицы (старший байт / младший байт в разных таблицах):

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)

Почему разный порядок? Технических причин нет -- скорее всего, данные были организованы так в процессе разработки. Но это создаёт fascinating последствие: когда ID уровня невалиден, указатели тайлов и спрайтов загружают разные уровни, создавая Frankenstein levels.

Чтобы перемещаться между этими двумя списками, игра использует небольшие таблицы смещений (как оглавление):

; 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: тайлы 1-4 + спрайты воды 5-2

Exploring Glitch Level Pointers -- таблицы смещений объяснены

World index table -- когда overflow world 9 создаёт glitch level

World index table: почему world 9 переполняется

Есть ROM-таблица из 8 байтов, которая даёт индекс первого уровня каждого мира (1-8). И сразу за ней таблица из 36 Level IDs всех уровней в порядке игры.

; 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, игра читает 9-й байт WorldIndexTable... которого не существует. Он переполняется на 1 байт в LevelIDTable, читает значение $25, затем использует $25 как индекс в LevelIDTable (37-я запись) -- что снова переполняется на 2 байта в 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) переполнение идёт ещё дальше и попадает на Level ID $01 -- это кат-сцена перед 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

Почему существуют глитч-миры

У игры 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).

Проверка тёплого старта 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 как тайлы:

; 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 двигается, таймер тикает и т.д.), "спрайты" уровня тоже меняются. Поэтому у некоторых глитч-миров враги мигают и постоянно превращаются во что-то другое.

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 = неопределённые спрайты. Игра пытается отрисовать Spiny ball или Bullet Bill blaster в первой строке тайлов. Это вызывает немедленный краш.

; 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 глитч-миров, каталогизированных

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 -- третье видео

6 по-настоящему уникальных глитч-уровней

Из 19 доступных ID глитч-уровней только 6 не крашатся сразу при загрузке:

Мир Level ID Описание
E-1 (224) $50 Один-единственный ? блок над пропастью. Mario умирает мгновенно.
W $57 Mario застревает при спавне, не может двигаться.
42 (133) $50 Облачный туннель, который ловит Mario, если он уйдёт достаточно далеко.
62 (131, 240) $4D Замёрзший замок: Mario спавнится наверху, не может упасть → застревает.
127 $4B Подземный туннель, но крашится, если уйти слишком далеко.
137 $4B Активирует автоматический скроллинг кат-сцен. Mario встречает единственный brick block, который блокирует его навсегда.

Level ID $50 (облачный туннель) -- глитч-мир 42-1 и E-1 Level ID $4D (замок) -- мир 62-1, Mario застрял при спавне Level ID $4B (туннель) -- мир 127-1, крашится при уходе слишком далеко

Шесть глитч-миров из 248, которые производят что-то действительно новое. Остальные -- это нормальные уровни с неправильным типом зоны или чёрные экраны.

Формат уровней в деталях

Копнём в точный формат данных уровня, чтобы понять, почему глитч-уровни держатся (или нет).

Заголовок уровня: 2 байта, 6 свойств

Каждый уровень начинается с заголовка в 2 байта, который управляет 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)

Модификатор типа управляет визуальными вариациями: волны вверху водных уровней, кирпичный фон 8-3, ночная палитра 4-3, снег 6-2 и т.д.

Объекты тайлов: 2 байта, Next Screen Flag, очередь на 3 слота

После заголовка идёт список объектов тайлов, каждый по 2 байта. Байт $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

Когда установлен бит "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 слота. Прямое следствие: нельзя иметь больше 3 объектов, начинающихся на одной колонке. Если очередь заполнена, 4-й объект игнорируется и никогда не загрузится.

Именно поэтому хорошо спроектированные уровни избегают слишком плотной укладки объектов. Пример из 1-2: колонка с 1up блоком в потолке + кирпичи рядом разделены на два отдельных объекта, чтобы соблюсти лимит в 3.

Специальные Y-позиции: 12, 13, 14, 15

Когда Y=12, объект не имеет Y-позиции (она зашита по типу):

; 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, две подгруппы. Если бит 6 первого байта равен 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 для смены модификатора типа, bit6=0 для смены фона + паттерна пола.

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)

Именно поэтому дыры -- это объекты: они переопределяют floor pattern на конкретной колонке, не изменяя паттерн для всего остального.

Лимит 256 байт и повторение

Все tile-данные уровня помещаются в максимум 256 байт. Y-регистр 6502 используется как индекс, и он 8-битный. Если игра доходит до конца данных, не найдя байт $FD, она возвращается в начало и повторяет 256 байт бесконечно:

; 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.

Некоторые глитч-уровни используют это повторение для генерации уровней, которые длятся "бесконечно".

Система спрайтов: 2 байта + pipe-транзишены

Спрайты следуют похожему формату, но без заголовка и с некоторыми ключевыми отличиями. Байт $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

Младший бит первого байта -- hard level flag: если установлен в 1, спрайт появляется только на уровнях ≥ 5-3. Так создаются уровни "hard mode".

Y-позиция 15 = screen skip (аналогично тайлам). Y-позиция 14 = pipe transition (3 байта):

; 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.

У спрайтов нет системы очереди. Единственное ограничение -- в зоне спавна (просто за экраном справа) не может быть загружено одновременно больше 4 спрайтов. Больше -- спрайты игнорируются.

Как получить доступ к глитч-мирам

Есть два основных метода.

Классический метод: wall clip

Wall clip (прохождение сквозь стены) позволяет выйти из нормального уровня и дойти до спрятанной warp-зоны. Манипулируя счётчиком мира через RAM, можно загрузить любой Level ID.

Техника:

  1. World 1-2: зайти в спрятанную трубу в конце
  2. Сделать wall clip на правой стене
  3. Идти в пустоте до warp-зоны
  4. Интерпретирует значения как миры

Но этот метод даёт доступ только к малой части глитч-миров.

Экстремальный метод: cart swap с NES Tennis

Подробности см. в разделе "Тёплый старт" выше. Вкратце: счётчик шагов тennis пишет в тот же байт RAM, что и стартовый мир SMB1, а определение тёплого старта сохраняет это значение.

Блог для хакеров: код для полного исследования

Если хочешь исследовать все глитчи сам в эмуляторе, можно пропатчить 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 и визуальную карту уровня.

Самые 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-зоны проверяют текущий мир:

; 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

Глитч-миры с номерами > 8 или 0 недоступны через нормальные трубы. Нужен wall clip или cart swap.

Почему некоторые уровни крашатся: jump tables

Когда игра загружает объект тайла, она использует его тип как индекс в 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: почему невалидный тип объекта крашит игру

Если объект имеет невалидный тип (≥12), игра прыгает на указатель, которого нет в этой таблице. 4 возможных исхода:

  1. Валидный указатель → объект загружается нормально
  2. Указатель на другую jump table (перекрытие) → появляется другой объект. Пример: тип 12 указывает на таблицу Y=13, что даёт L-pipe.
  3. Указатель на исполняемый код → выполнение случайного кода (скорее всего краш)
  4. Явный placeholder (NOP) → объект ничего не делает (некоторые спрайты такие -- враги висят на месте без движения)

Глитч-уровень ID $58: sprite pointer указывает на невалидный адрес, игра крашится

Глитч-уровень ID $50: облачный туннель, уровень, сгенерированный повреждёнными данными

Глитч-уровень ID $58 (туннель, который крашится): его sprite pointer указывает на область памяти, которой не существует на NES без mapper ROM. Игра пытается загрузить одного и того же Koopa 5 раз за кадр в позиции (0,0), что перегружает PPU и вызывает фриз.

; 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? Даже если найдёшь трубу в глитч-мире, игра проверит, что мир назначения между 1 и 8. Глитч-миры имеют номера > 8 (36-1, 255-1...), поэтому warp не работает.

Именно поэтому Minus World не имеет конца: flagpole не присутствует в спрайтах, а трубы ведут в никуда.

Трюк с 5 объектами в одной колонке

Существует edge case, который позволяет обойти лимит в 3 объекта на колонку. Когда очередь блокируется (слоты заполнены + следующий объект без next screen flag), игра "предобрабатывает" текущую колонку в цикле, пока не найдёт объект с next screen flag. При каждой предобработке:

; 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" и используется некоторыми ромхакерами для создания уровней плотнее, чем позволяет формат.

Различия между версиями

Famicom Disk System

Версия FDS SMB1 имеет другую memory map. Все указатели уровней сдвинуты, но данные те же. Что меняется: индексы глитч-миров полностью другие:

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 в низком туннеле (жаль, что это автоскроллер).

The Lost Levels (Super Mario Bros. 2 японская)

Lost Levels меняет многое:

  1. Одинаковый порядок tiles/sprites: больше нет Frankenstein levels (тайлы и спрайты загружают один и тот же уровень даже с невалидным 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. Больше нет глитча с теннисом: опция continue (продолжение с того же мира после game over) делает тёплый старт ненужным, и игра мгновенно сбрасывается, если мир > 9
  6. Новые объекты: ядовитый гриб, невидимый блок, невидимый блок с огненным цветком, перевёрнутые трубы, ветер -- но вставлены в середину существующих списков → обратная несовместимость с SMB1
  7. Piranha Plants всегда красные после мира 4, зелёные пружины только в мирах 2/B/3/C/7

Super Mario All-Stars (SNES)

Прямой порт с теми же 6502-рутинами (SNES выполняет код NES в совместимом режиме):

  • Исправленная warp-зона: больше нет Minus World (вход в левую трубу до текста ведёт в правильный мир)
  • Падение: большинство глитч-уровней крашатся (кроме ID $6A и 9-1)
  • Добавлены объекты замка: более уникальный рендер
  • Но: 4-2 wrong warp по-прежнему работает (не исправлен!)

4-2 wrong warp: баг размещения объекта

В 4-2 есть два объекта pipe-транзишена: лоза (warp-зона) и труба (комната с монетами). Первый объект транзишена (лозы) размещён задолго до того, как лоза появляется на экране. Второй (труба) размещён слишком поздно в уровне.

; 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)? У уровня есть чекпоинты с номерами экранов и зашитыми Y-позициями:

; 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. Всё разнообразие между уровнями идёт от данных (указатели, объекты, спрайты, floor patterns), а не от кода.

256 глитч-миров существуют потому, что таблицы указателей рассчитаны на 128 записей × 4 типа, и игра никогда не проверяет значения, которые читает. Когда указатель попадает в RAM, игра интерпретирует регистры Mario как тайлы. Когда указатель попадает в звуковые данные, игра играет музыку в виде level design. А когда jump tables переполняются, игра выполняет что угодно вплоть до краша.

More Super Mario Bros. Mechanics Explained -- четвёртое видео

Чему можно научиться из всего этого

  1. Разделение tiles/sprites: полная независимость двух слоёв с разным порядком хранения, создающим уникальные Frankenstein levels
  2. RLE-сжатие + система объектов: уровни -- не битмапы, а списки размещённых объектов с floor patterns для пола
  3. Очередь на 3 слота: строгое ограничение hardware (и дизайна уровней)
  4. Без проверки: игра доверяет указателям и jump tables, что даёт либо играбельные глитчи, либо краши
  5. 256 байт максимум: лимит Y-регистра 6502, из-за которого данные повторяются при выходе за границу
  6. Warm start / cold start: система "продолжения", открывшая дверь к cart swap Tennis → Mario

Самое прекрасное: всё это -- код 6502, уместившийся в 40КБ. Без слоя абстракции, без проверки доступа к памяти, без обработчика исключений. Если указатель отстойный -- игра крашится. А краши мы называем глитч-мирами.

3 главных вывода

  1. Глитч-миры -- это указатели, которые попадают не туда -- У игры 128 ID × 4 типа зон, но только 34 уникальных уровня. Когда номер мира повреждён (теннисом или wall clip), игра загружает указатель, предназначенный для другого уровня, и 512 возможных комбинаций дают непредсказуемые результаты.

  2. Minus World -- это баг warp'а в сочетании с повреждением данных -- Левая труба в 1-2, если активирована до появления текста, загружает мир 36 (0x24). Этот мир указывает на Level ID $01 (вода 2-2), уровень без flagpole. И поскольку для мира 36 нет pipe-транзишена, уровень зацикливается. Отсутствие проверки создаёт икону.

  3. Tennis → Mario, 15 лет до OoT → Paper Mario -- RAM NES выживает при замене картриджа благодаря конденсаторам и системе warm start / cold start SMB1. Счётчик шагов тенниса (который увеличивает байт RAM, проигрывая звук шагов) попадает точно на адрес номера мира. Нужно, чтобы цифры top score оставались 0, байт $A5 был нетронут, и игра определила тёплый старт -- идеальное стечение обстоятельств, которое сработало только с теннисом.

Оригинальные видео Retro Game Mechanics Explained -- это потрясающий труд: уровень детализации по дизассембли 6502, автоматические карты всех уровней, объяснения cart swap и warm start. Если не смотрел серию -- посмотри, она короткая, и каждая минута насыщена.

Исходный код карт доступен на rgmechex.com, а полный дизассембл SMB1 -- open source на множестве репозиториев. 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 partido de Tennis de la 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 tocas los bytes correctos, 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 ningún lado.

Estos glitch worlds existen por una razón simple: el sistema de almacenamiento de niveaux de SMB1 es una maravilla de optimización de 8 bits, y cuando obligas al juego a leer donde no debe, los resultados son fascinantes.

Retro Game Mechanics Explained hizo una serie de 4 vídeos sobre esto -- vamos a compilarlos 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 de RGMechEx sobre las mecánicas ocultas de SMB1

World 9-1 -- la pantalla de título del primer glitch world accesible mediante el cart swap de Tennis

El warm start: por qué la RAM de Tennis sobrevive en SMB1

Antes de hablar del almacenamiento de niveles, hay que entender cómo arranca SMB1. Porque el glitch del cart swap de NES Tennis se basa completamente en el sistema de detección warm start / cold start del juego.

Los 41 bytes preservados

Cuando SMB1 detecta un cold start (primera vez que se enciende o power off/on), borra toda la RAM. Pero cuando detecta un warm start (reset por botón, sin cortar la alimentación), preserva una zona de memoria 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]

Estos 41 bytes sirven para una sola funcionalidad: 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 de título, si mantienes A + Start, recomienzas en 6-1.

Los 41 bytes preservados en RAM durante un warm start -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, y el byte mágico $A5

La doble verificación del warm start

Cold start vs warm start -- el diagrama de detección del reset

Cuando SMB1 arranca, no verifica un solo criterio sino dos:

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 verificación del byte $A5 y los dígitos del top score -- el corazón del warm start

¿Por qué una doble verificación? Porque el byte $A5 podría estar presente por casualidad (otro juego que deja ese valor, o el estado de reposo por defecto del chip RAM). Al verificar que los dígitos del top score son válidos (0-9), se asegura 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

Luego, cambias a Tennis sin apagar la consola. Tennis:

  • No limpia la RAM al arrancar (pocos juegos de NES lo hacen)
  • No escribe sobre los bytes del top score → se quedan en 0 (válidos)
  • No toca el byte $A5 → sigue presente
  • Usa la dirección $075F para el contador de pasos del jugador
; 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).

Cuando vuelves a SMB1:

  1. El byte $A5 sigue ahí (Tennis no lo tocó)
  2. Los dígitos del top score siguen en 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

Un detalle: primero tienes que arrancar SMB1, luego Tennis, y luego SMB1 otra vez. Si empezaras directamente con Tennis, el byte $A5 nunca se escribiría (Tennis no escribe $A5), por lo que la detección del warm start fallaría y la RAM se borraría.

El contador de pasos de Tennis: cada footstep incrementa el world byte

Acceder a Glitch Worlds mediante NES Tennis -- el vídeo que explica el cart swap

Cómo SMB1 almacena sus niveles en 40KB

Nintendo R&D4 tuvo que resolver un problema que parecía simple: representar niveles que se desplazan horizontalmente con tiles, enemigos, objetos, todo dentro de un presupuesto de ROM ultra ajustado.

La solución es una separación en dos capas de datos completamente independientes:

El tile layout (el mapa del nivel)

Cada nivel está definido por un puntero hacia una estructura de tiles comprimida en ROM. La compresión es rudimentaria pero genial: un byte de "control" seguido de 1-3 bytes de datos.

El formato de tile usa un 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 nivel contiene 13 líneas de 16 columnas de tiles (13×16 = 208 tiles visibles). Pero el formato comprimido permite reducirlo mucho más -- por ejemplo, el cielo y las columnas vacías casi no ocupan espacio.

El bucle de renderizado 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

El sprite layout (los enemigos y objetos)

Paralelamente, los enemigos y objetos (bloques ?, tuberías, goombas, koopas) se almacenan en una estructura completamente separada. Cada spawn está 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 nivel puede referenciar hasta 5 páginas de sprites diferentes (bueno, 5 "pantallas" de 16 columnas), pero en la práctica la mayoría de niveles solo usan 2-3.

La tabla de punteros

El genio del diseño es la tabla de punteros. Cada nivel se almacena como un par de direcciones 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 tabla. 4 tipos de zona. 512 combinaciones posibles, pero solo una fracción es usada por el juego oficial. El resto son RAM no inicializada o datos que se interpretan como punteros.

Cuando el juego carga un nivel, hace esto:

; 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

Sin validación. Sin comprobar que el puntero es válido. El juego lee la dirección en la tabla y descomprime lo que se encuentra en esa dirección, punto final.

Level ID $06 (Water) -- 9-1, la versión submarina de 6-2

La tabla de Level IDs: 128 entradas posibles, 34 asignadas

El orden diferente de los punteros tiles y sprites -- la causa de los Frankenstein levels

Los 34 niveles únicos y el sistema de ID de 7 bits

El chip RAM de la NES (MB8416A) -- es el que preserva los datos cuando se cambian las cartuchos

SMB1 no tiene 32 niveles, sino 34 niveles únicos. Muchos niveles son duplicados (5-3 = 1-3 pero con Bullet Bills) marcados con un flag de "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 (incluyendo las 2 salas bonus de nubes)
  • Underground (Tipo 2): 3 niveles (incluyendo las salas bonus subterráneas)
  • Castle (Tipo 3): 6 niveles
  • + 1 sala de cutscene (antes de los niveles subterráneos/agua)
  • + 1 warp zone de 4-2

Cada nivel tiene un ID de 7 bits. Los 5 bits menos significativos = número dentro del subgrupo, los 2 bits más significativos = 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 posibles ($00-$7F), solo 34 asignados a niveles reales. Los IDs sin usar apuntan a cualquier cosa.

Las tablas de punteros: dos listas, dos órdenes

Los punteros tiles y sprites no se almacenan 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 diferentes? Sin razón técnica -- probablemente es así como 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 diferentes, creando Frankenstein levels.

Para navegar entre estas dos listas, el juego usa pequeñas tablas de offset (como una tabla de contenidos):

; 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 cargar el nivel 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 ✓

Ahora, ¿qué pasa con un ID inválido como $43 (Underground número 3, que no 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 agua 5-2

Exploring Glitch Level Pointers -- las tablas de offset explicadas

El world index table -- cuando el overflow de world 9 crea un glitch level

El world index table: por qué world 9 desborda

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 el orden de juego.

; 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

Cuando intentas cargar world 9, el juego lee el 9º byte de WorldIndexTable... que no existe. Desborda 1 byte en LevelIDTable, lee el valor $25, luego usa $25 como índice en LevelIDTable (entrada 37) -- lo que desborda otra vez 2 bytes en SpriteOffsetTable, y lee el 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), el desbordamiento va aún más lejos y cae en el Level ID $01, que es el nivel cutscene que precede a 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 qué existen los glitch worlds

El juego tiene 32 niveles "legítimos" (8 mundos × 4 niveles). Pero la tabla de punteros tiene 128 entradas por tipo de zona. Las entradas más allá del nivel 32 contienen lo que se encuentra en ROM en esas direcciones -- a veces otro nivel, a veces datos de sonido, 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 a:

  • 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 parece 2-2, pero que se repite infinitamente porque el flagpole no existe. Sin fin de nivel, sin salida.

Es el nivel 36-1 (o 36-1 en el mundo $-1).

El warm start check de SMB1 -- es el que permite que el Minus World exista

; 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.

Los punteros que apuntan a la RAM

Cuando el tile pointer o el sprite pointer apuntan a una dirección en RAM ($00-$7F) en vez de ROM, el juego intenta interpretar los cambios constantes de la 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.

Cuando la página cero cambia (porque Mario se mueve, porque el temporizador avanza, 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 a 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 como "sin nivel" en el catálogo de RGMechEx. Los tiles quizás sean válidos pero los sprites apuntan a un lugar 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 = sprites indefinidos. El juego intenta mostrar un Spiny ball o un Bullet Bill blaster en la primera línea de tiles. Se crashea inmediatamente.

; 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

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 de cada uno.

El contador de mundos es de 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 por el mecanismo de desbordamiento de la WorldIndexTable.

Tabla de glitch worlds -- 248 mundos corruptos, 68 primeros niveles accesibles

De los 128 IDs posibles, solo 68 son el "primer nivel" de un mundo (accesibles a través del número de glitch world). 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 porque los punteros caen en las mismas direcciones ROM. El Level ID $28 (Overworld) por ejemplo -- tile pointer $A7CD (2-1) -- aparece en 38 glitch worlds diferentes, porque su sprite pointer $9F51 apunta a una zona de la ROM que se usa como padding/datos de sonido reutilizados por muchos IDs.

Mapa del nivel ID $28 (Overworld) -- tiles de 2-1 con sprites normales, 38 glitch worlds

Super Mario Bros. Glitch Levels Explained -- el tercer vídeo

Los 6 glitch levels realmente únicos

De los 19 IDs de glitch level accesibles, solo 6 no crashean inmediatamente al cargar:

World Level ID Descripción
E-1 (224) $50 Un solo ? block sobre un abismo. Mario muere al instante.
W $57 Mario spawne bloqueado, incapaz de moverse.
42 (133) $50 Túnel de nubes que atrapa a Mario si va demasiado lejos.
62 (131, 240) $4D Castillo helado: Mario spawna arriba, no puede caer → bloqueado.
127 $4B Túnel subterráneo, pero crashea si vas demasiado lejos.
137 $4B Activa el auto-scroll de las cutscenes. Mario encuentra un único brick block que lo bloquea para siempre.

Level ID $50 (túnel de nubes) -- el glitch world 42-1 y E-1 Level ID $4D (castillo) -- world 62-1, Mario bloqueado al spawn Level ID $4B (túnel) -- 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 del nivel: 2 bytes, 6 propiedades

Cada nivel comienza con un header de 2 bytes que controla 6 propiedades:

; 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)

El tipo modifier controla variaciones visuales: las olas en la parte superior de los niveles de agua, el fondo de ladrillos de 8-3, la paleta de noche de 4-3, la nieve de 6-2, etc.

Los objetos tile: 2 bytes, Next Screen Flag, cola de 3 slots

Después del header viene una lista de objetos tile, cada objeto ocupa 2 bytes. El byte $FD marca el final de la 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

Cuando el bit "next screen" está activo, la columna de trabajo actual se incrementa en 1. Esto permite colocar objetos más allá de las primeras 16 columnas. Los objetos deben listarse en orden (izquierda a derecha) porque el juego los carga secuencialmente:

; 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 cola tiene exactamente 3 slots. Consecuencia directa: no puedes tener más de 3 objetos que comiencen en la misma columna. Si la cola está llena, el 4º objeto se ignora y nunca se cargará.

Por eso los niveles bien diseñados evitan apilar demasiados objetos. Ejemplo en 1-2: la columna con el 1up block en el techo + los ladrillos de al lado se dividen en dos objetos distintos para respetar el límite de 3.

Y posición especial: 12, 13, 14, 15

Cuando Y=12, el objeto no tiene posición Y (está hardcodeada 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

Cuando Y=13, dos subgrupos. Si el bit 6 del byte 1 está en 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, los 5 bits menos significativos codifican un screen skip (saltar directamente a una pantalla N, sin pasar por el next screen flag uno a uno).

Cuando Y=14: mismo principio con bit6=1 para cambiar el tipo modifier, bit6=0 para cambiar el fondo + el patrón de suelo.

Los floor patterns: 16 patrones de suelo

El suelo de los niveles no está hecho de objetos individuales. SMB1 usa floor patterns, un patrón de fondo que se aplica a todas las columnas hasta el próximo cambio:

; 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 eso los agujeros son objetos: sobreescriben el 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 tiene 8 bits. Si el juego llega al final de los datos sin encontrar el byte $FD, vuelve al principio y repite los 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.

Algunos glitch levels aprovechan este repeat para generar niveles que duran "indefinidamente".

El sistema de sprites: 2 bytes + transiciones de pipe

Los sprites siguen un formato similar, pero sin header y con algunas diferencias clave. El byte $FF marca el final de la 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

El bit menos significativo del byte 1 es el hard level flag: si está en 1, el sprite solo aparece en niveles ≥ 5-3. Así se crean los niveles "hard mode".

Y posición 15 = screen skip (igual que los tiles). Y posición 14 = transición de pipe (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.

Los sprites no tienen un sistema de cola. La única limitación es que no puede haber más de 4 sprites cargados simultáneamente en la zona de spawn (justo fuera de la 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 (pasar a través de las paredes) permite salir del nivel normal y caminar hasta la warzone oculta. Manipulando el contador de mundos a través de la RAM, puedes cargar cualquier Level ID.

La técnica:

  1. World 1-2: ir a la tubería de salida oculta
  2. Hacer el wall clip en la pared de la derecha
  3. Caminar en el vacío hasta la zona warp
  4. El juego interpreta los valores como mundos

Pero este método solo da acceso a una pequeña parte de los glitch worlds.

El método extremo: NES Tennis cart swap

Mira la sección "El warm start" más arriba para el detalle completo. En resumen: el contador de pasos de Tennis escribe en el mismo byte RAM que el mundo de partida de SMB1, y la detección del warm start preserva ese valor.

El rincón de los truquistas: el código para explorar todo

Si quieres explorar todos los glitch tú mismo en un emulador, puedes parchear el Level ID directamente:

; 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 publicó la lista completa de 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 produce 15 glitch worlds diferentes (D-1, J-1, Y-1, Z-1, 55-1, 73-1...). Explicación: el sprite pointer apunta a una zona de la ROM que contiene datos lo suficientemente cercanos a sprites válidos para producir resultados jugables, pero la combinación de tiles de castillo de 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 al mismo nivel (tiles de 2-1 + $9F51 sprites). ¿Por qué? Porque el sprite pointer $9F51 cae en una zona de la ROM que se usa como padding/datos de sonido reutilizados por muchos IDs.

Level ID $49 (Underground): el nivel FDS

Tile pointer $76AE + sprite pointer $1C9D. El tile pointer apunta a la zona de la ROM reservada para la versión Famicom Disk System. Resultado: un nivel con tiles que no existen en la cartucho estándar. Es el nivel que hace aparecer el nivel 52-1 y 196-1.

Level ID $00-$02: los verdaderos niveles bonus

Estos IDs son usados por 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 verifican el mundo actual:

; 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

Los glitch worlds con números > 8 o 0 no pueden alcanzarse por 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 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

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 esta tabla. 4 resultados posibles:

  1. Puntero válido → el objeto se carga normalmente
  2. Puntero a otra jump table (superposición) → aparece un objeto diferente. Ejemplo: tipo 12 apunta a la tabla Y=13, lo que produce un L-pipe.
  3. Puntero a código 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 a una dirección inválida, el juego crashea

Glitch level ID $50: el cloud tunnel, un nivel generado por datos corruptos

El glitch level ID $58 (el túnel que crashea): su sprite pointer apunta a una región de memoria que no existe en la 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.

; 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

La paradoja del pipe warp

Recuerda el check target_world BETWEEN 1 AND 8. Incluso si encuentras una tubería en un glitch world, el juego verifica que el mundo de destino esté entre 1 y 8. Los glitch worlds tienen números > 8 (36-1, 255-1...), por lo que la warp falla.

Por eso también el Minus World no tiene fin: el flagpole no está presente en los sprites, y las tuberías no llevan a ningún lado.

El truco de los 5 objetos en una columna

Existe un edge case que permite sobrepasar el límite de 3 objetos por columna. Cuando la cola se bloquea (slots llenos + siguiente objeto con next screen flag faltante), el juego "preprocesa" la columna actual en bucle hasta encontrar un objeto con next screen flag. Durante cada preprocesamiento:

; 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).

Esto se llama un "queue skip" y es usado por 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 un mapa de memoria diferente. Todos los punteros de nivel están desplazados, pero los datos son los mismos. Lo que cambia: los índices de los glitch worlds son 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.

Mi nivel FDS favorito: ID $5F, una versión subterránea de la segunda mitad de 3-3 en túnel bajo (qué pena que sea un autoscroller).

The Lost Levels (Super Mario Bros. 2 japonés)

Lost Levels cambia muchas cosas:

  1. Orden idéntico tiles/sprites: sin más 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 de disco: la ROM se dividió para el FDS:
    • Archivo 1: mundos 1-4
    • Archivo 2: mundos 5-8
    • Archivo 3: world 9 + motor de sonido
    • Archivo 4: mundos A-D (tabla de punteros completamente diferente)
  4. Mismo Level ID = 4 niveles posibles según el archivo cargado
  5. Sin más glitch Tennis: la opción continue (continuar en el mismo mundo tras game over) hace innecesario el warm start, y el juego resetea inmediatamente si world > 9
  6. Nuevos objetos: champiñón venenoso, bloque invisible, bloque invisible fire flower, tuberías al revés, 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)

Puerto directo con las mismas rutinas 6502 (el SNES ejecuta el código de la NES en modo compatible):

  • Warp zone arreglada: sin más Minus World (entrar en la tubería izquierda antes del texto lleva al mundo correcto)
  • Planteamiento: la mayoría de glitch levels crashean (excepto ID $6A y 9-1)
  • Objetos de castillo añadidos: renders más únicos
  • Pero: el 4-2 wrong warp sigue funcionando (¡sin parchear!)

El 4-2 wrong warp: un bug de posicionamiento de objetos

En 4-2, hay dos objetos de transición de pipe: la enredadera (warp zone) y la tubería (sala de monedas). El primer objeto de transición (el de la enredadera) está posicionado mucho antes de que la enredadera aparezca en pantalla. El segundo (la tubería) está posicionado demasiado tarde en el nivel.

; 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.

Los niveles con bucle

¿Cómo funcionan los loops (8-4, 7-4)? El nivel tiene checkpoints con números de pantalla y posiciones Y hardcodeadas:

; 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.

Cambiar el formato, no el código

Una de las lecciones más fascinantes de esta arquitectura es que los desarrolladores de SMB1 lograron crear un sistema de nivel muy expresivo sin tocar nunca el código de renderizado 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 de sonido, el juego toca música en forma de level design. Y cuando las jump tables desbordan, el juego ejecuta cualquier cosa hasta el crash.

Más Super Mario Bros. Mechanics Explained -- el cuarto vídeo

Lo que podemos aprender de todo esto

  1. Separación tiles/sprites: independencia total de las dos capas, con órdenes de almacenamiento diferentes 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 de 3 slots: límite estricto del hardware (y del diseño de nivel)
  4. Sin validación: el juego confía en los punteros y las jump tables, lo que produce o glitchs jugables o crashes
  5. 256 bytes máx: el límite del registro Y del 6502, que hace que los datos se repitan si vas demasiado lejos
  6. Warm start / cold start: un sistema de "continuar" que abrió la puerta al cart swap Tennis → Mario

Lo mejor de todo esto: es código 6502 que cabe en 40KB. Sin capa de abstracción, sin validación de acceso a memoria, sin gestor de excepciones. Si el puntero es una porquería, el juego crashea. Y a los crashes los llamamos glitch worlds.

Los 3 puntos clave para 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 está corrupto (por Tennis o wall clip), el juego carga un puntero diseñado para otro nivel, y las 512 combinaciones posibles producen resultados impredecibles.

  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 aparezca el texto, carga world 36 (0x24). Este world apunta al Level ID $01 (agua de 2-2), un nivel sin flagpole. Y como no hay transición de pipe para world 36, el nivel se repite infinitamente. La falta de verificación crea el icono.

  3. Tennis → Mario, 15 años antes de OoT → Paper Mario -- La RAM de la NES sobrevive a un swap 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 sonar el paso) cae justo en la dirección del world number. Los dígitos del top score tienen que seguir en 0, el byte $A5 tiene que estar intacto, y el juego tiene que detectar un warm start -- un concursal de circunstancias perfecto que solo funcionó con Tennis.

Los vídeos originales de Retro Game Mechanics Explained son un trabajo de hormiga impresionante -- el nivel de detalle en el desensamblado 6502, los mapas automáticos de todos los niveles, las explicaciones del cart swap y del warm start. Si no has visto 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 muchos repos. Hace 40 años, programadores japoneses escribieron este sistema de nivel en 6502 con cero tests unitarios y cero bug tracker, y seguimos aprendiendo cosas al abrir 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, penunjuk, dan 256 glitch world

Bagaimana 128 level × 4 tipe zona muat dalam 40KB ROM, mengapa Minus World ada, dan bagaimana pertandingan Tennis NES bisa memuat glitch world.

Pengantar

Super Mario Bros. adalah 40 kilobyte ROM. Delapan dunia, 32 level, musuh, musik, power-up, semuanya muat di dalamnya.

Tapi jika kamu membuka emulator dan mengutak-atik byte yang tepat, kamu bisa memuat level 36-1. Atau 255-1. Atau mendarat di dunia di mana segalanya terbuat dari sprite Bowser dan pipa yang tidak membawa ke mana pun.

Glitch world ini ada karena alasan sederhana: sistem penyimpanan level SMB1 adalah keajaiban optimasi 8-bit, dan ketika kita memaksa game membaca di tempat yang tidak seharusnya, hasilnya sangat menarik.

Retro Game Mechanics Explained telah membuat seri 4 video tentang ini -- kita akan menggabungkannya menjadi satu penelusuran mendalam ke dalam kode 6502 dari game terlaris pada masanya.

GLITCH OBJECTS -- judul seri RGMechEx tentang mekanik tersembunyi SMB1

World 9-1 -- layar judul glitch world pertama yang dapat diakses melalui cart swap Tennis

Warm start: mengapa RAM Tennis bertahan di SMB1

Sebelum membahas penyimpanan level, kita perlu memahami bagaimana SMB1 memulai. Karena glitch cart swap NES Tennis sepenuhnya bergantung pada sistem deteksi warm start / cold start game.

41 byte yang dipertahankan

Ketika SMB1 mendeteksi cold start (pertama kali dinyalakan atau power off/on), ia menghapus seluruh RAM. Tetapi ketika mendeteksi warm start (tombol reset, tanpa pemadaman daya), ia mempertahankan area memori sebesar 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 ini melayani satu fungsi saja: memungkinkan pemain untuk melanjutkan di dunia yang sama setelah game over. Jika kamu mati di 6-3, game menulis dunia 6 di byte awal, dan di layar judul, jika 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 saja tetapi dua:

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

Verifikasi byte $A5 dan digit top score -- inti warm start

Mengapa verifikasi ganda? Karena byte $A5 mungkin muncul secara kebetulan (game lain yang meninggalkan nilai ini, atau status default chip RAM). Dengan memverifikasi bahwa digit top score valid (0-9), kita memastikan data konsisten.

Mengapa Tennis adalah satu-satunya game yang berfungsi

Ketika kita memasukkan SMB1 untuk pertama kali (cold start), game:

  1. Menghapus seluruh RAM → top score = 0, world byte = 0
  2. Menulis $A5 ke alamat $0787

Selanjutnya, kita ganti ke Tennis tanpa mematikan konsol. Tennis:

  • Tidak membersihkan RAM saat startup (sedikit game NES yang melakukan ini)
  • Tidak menulis ke byte top score → tetap 0 (valid)
  • Tidak menyentuh byte $A5 → tetap ada
  • Menggunakan alamat $075F untuk penghitung langkah pemain
; 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).

Ketika kita memasukkan SMB1 kembali:

  1. Byte $A5 masih ada (Tennis tidak menyentuhnya)
  2. Digit top score masih 0 (valid)
  3. World byte sekarang bernilai 8+ (ditambah oleh langkah Tennis)
  4. SMB1 mendeteksi warm start → mempertahankan world byte yang rusak
  5. Tahan A + Start → world 9-1, world A-1, world 36-1, dll.

Mengapa harus boot Mario sebelum Tennis

Satu kehalusan: kita harus boot SMB1 terlebih dahulu, lalu Tennis, lalu SMB1 lagi. Jika kita langsung mulai dengan Tennis, byte $A5 tidak akan pernah ditulis (Tennis tidak menulis $A5), sehingga deteksi warm start akan gagal dan RAM akan dihapus.

Penghitung langkah Tennis: setiap footstep menambah world byte

Akses Glitch Worlds melalui NES Tennis -- video yang menjelaskan cart swap

Bagaimana SMB1 menyimpan level dalam 40KB

Nintendo R&D4 harus menyelesaikan masalah yang tampak sederhana: merepresentasikan level yang bergulir horizontal dengan tile, musuh, item, semuanya dalam batasan ROM yang sangat ketat.

Solusinya adalah pemisahan menjadi dua lapisan data yang sepenuhnya independen:

Tile layout (peta level)

Setiap level ditentukan oleh penunjuk ke struktur tile terkompresi di ROM. Kompresi primitif tapi brilian: byte "kontrol" diikuti oleh 1-3 byte data.

Format tile menggunakan sistem run (seperti 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

Setiap level berisi 13 baris × 16 kolom tile (13×16 = 208 tile terlihat). Tapi format terkompresi memungkinkan untuk mengurangi jauh lebih banyak -- misalnya, langit dan kolom kosong hampir tidak memakan tempat.

Loop rendering dalam 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 (musuh dan objek)

Secara paralel, musuh dan objek (blok ?, pipa, goomba, koopa) disimpan dalam struktur yang sepenuhnya terpisah. Setiap spawn ditentukan oleh 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

Setiap level dapat merujuk hingga 5 halaman sprite berbeda (yaitu 5 "layar" 16 kolom), tapi praktisnya sebagian besar level hanya menggunakan 2-3.

Tabel penunjuk

Kejeniusan dari desainnya adalah tabel penunjuk. Setiap level disimpan sebagai pasangan alamat 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 entri per tabel. 4 tipe zona. 512 kombinasi yang mungkin, tapi hanya sebagian kecil yang digunakan oleh game resmi. Sisanya adalah RAM yang belum diinisialisasi atau data yang ditafsirkan sebagai penunjuk.

Ketika game memuat level, ini yang dilakukannya:

; 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

Tanpa validasi. Tanpa pengecekan apakah penunjuk valid. Game membaca alamat di tabel dan mendekompresi apa yang ada di alamat tersebut, titik akhir.

Level ID $06 (Water) -- 9-1, versi bawah air dari 6-2

Tabel Level ID: 128 kemungkinan entri, 34 ditetapkan

Urutan penunjuk tile dan sprite yang berbeda -- penyebab Frankenstein level

34 level unik dan sistem ID 7-bit

Chip RAM NES (MB8416A) -- chip ini mempertahankan data saat kita menukar kartu

SMB1 tidak memiliki 32 level, tapi 34 level unik. Banyak level adalah duplikat (5-3 = 1-3 tapi dengan Bullet Bill) yang ditandai dengan 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 bonus awan)
  • 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 dari 4-2

Setiap level memiliki ID 7 bit. 5 bit rendah = nomor di sub-grup, 2 bit tinggi = tipe 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 ID yang mungkin ($00-$7F), hanya 34 yang ditetapkan untuk level nyata. ID yang tidak digunakan menunjuk ke mana saja.

Tabel penunjuk: dua daftar, dua urutan

Penunjuk tile dan sprite tidak disimpan dalam urutan yang sama. Kode menggunakan dua daftar 16-bit terpisah (high byte / low byte dalam dua tabel berbeda):

Urutan penunjuk sprite:
  Index 0-5   : Castle (6 level)
  Index 6-27  : Overworld (22 level)
  Index 28-30 : Underground (3 level)
  Index 31-33 : Water (3 level)

Urutan penunjuk tile:
  Index 0-2   : Water (3 level)
  Index 3-24  : Overworld (22 level)
  Index 25-27 : Underground (3 level)
  Index 28-33 : Castle (6 level)

Mengapa urutan berbeda? Tidak ada alasan teknis -- ini mungkin karena data diorganisir selama pengembangan. Tapi ini menciptakan konsekuensi yang menarik: ketika ID level tidak valid, penunjuk tile dan sprite memuat level yang berbeda, menciptakan Frankenstein level.

Untuk menavigasi antara kedua daftar ini, game menggunakan tabel offset kecil (seperti daftar isi):

; 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

Untuk memuat level 6-2 (ID $23, Overworld nomor 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 ✓

Sekarang, apa yang terjadi dengan ID yang tidak valid seperti $43 (Underground nomor 3, yang tidak ada)?

; 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: tile 1-4 + sprite air 5-2

Exploring Glitch Level Pointers -- tabel offset yang dijelaskan

World index table -- ketika overflow world 9 menciptakan glitch level

World index table: mengapa world 9 overflow

Ada tabel ROM 8 byte yang memberikan index dari level pertama setiap dunia (1-8). Dan tepat setelahnya, tabel 36 Level ID dari semua level dalam urutan bermain.

; 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

Ketika kita mencoba memuat world 9, game membaca byte ke-9 dari WorldIndexTable... yang tidak ada. Ia overflow 1 byte ke LevelIDTable, membaca nilai $25, lalu menggunakan $25 sebagai index di LevelIDTable (entri ke-37) -- yang overflow lagi 2 byte ke SpriteOffsetTable, dan membaca nilai 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 !

Untuk world G (16), overflow berjalan lebih jauh dan jatuh ke Level ID $01, yang adalah level cutscene yang mendahului 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

Mengapa glitch world ada

Game memiliki 32 level "legitimasi" (8 dunia × 4 level). Tapi tabel penunjuk memiliki 128 entri per tipe zona. Entri melampaui level 32 berisi apa yang ada di ROM pada alamat-alamat tersebut -- 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 di AreaType 0 (air) menunjuk ke:

  • Tile pointer: $AE45 → zona bawah air dari 2-2/7-2
  • Sprite pointer: $A171 → sprite dari 2-2/7-2

Hasilnya: level air yang mirip 2-2, tapi looping tanpa batas karena flagpole tidak ada. Tanpa akhir level, tanpa jalan keluar.

Ini adalah level 36-1 (atau 36-1 di dunia $-1).

Warm start check SMB1 -- inilah yang memungkinkan Minus World ada

; 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.

Penunjuk yang menunjuk ke RAM

Ketika tile pointer atau sprite pointer menunjuk ke alamat di RAM ($00-$7F) bukan di ROM, game mencoba menafsirkan perubahan konstan di RAM sebagai tile:

; 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.

Ketika halaman nol berubah (karena Mario bergerak, timer berputar, dll.), "sprite" level juga berubah. Inilah mengapa beberapa glitch world memiliki musuh yang berkedip dan terus-menerus berubah.

Level ID $03 Water -- sprite pointer $009D menunjuk ke RAM, level tidak bisa dimainkan

Level ID $36: level kosong (Overworld)

Level ID $36 di Overworld:

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

Hasilnya: tidak ada. Game memuat level tapi ditandai "tanpa level" di 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 undefined. Game mencoba menampilkan Spiny ball atau Bullet Bill blaster di baris tile pertama. Ini langsung 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 world yang dikatalogkan

RGMechEx telah menulis skrip yang menghasilkan peta dari semua level, untuk 4 tipe zona, dan 128 ID masing-masing.

Penghitung dunia menggunakan 8 bit (0-255). Dunia 1-8 legitimasi. Tersisa 248 glitch world potensial. Setiap glitch world sesuai dengan level pertama dari dunia tersebut, dan Level ID-nya dihitung oleh mekanisme overflow WorldIndexTable.

Tabel glitch world -- 248 dunia yang rusak, 68 level pertama yang dapat diakses

Dari 128 ID yang mungkin, hanya 68 yang merupakan "level pertama" dari suatu dunia (dapat diakses melalui nomor glitch world). 60 lainnya adalah level 2+ atau tidak dapat 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 mengarah ke level yang sama karena penunjuk jatuh ke 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 area ROM yang digunakan sebagai padding/data suara yang digunakan kembali oleh banyak ID.

Peta level ID $28 (Overworld) -- tile 2-1 dengan sprite normal, 38 glitch world

Super Mario Bros. Glitch Levels Explained -- video ke-3

6 glitch level yang benar-benar unik

Dari 19 ID glitch level yang dapat diakses, hanya 6 yang tidak langsung crash saat pemuatan:

World Level ID Deskripsi
E-1 (224) $50 Satu blok ? di atas jurang. Mario mati seketika.
W $57 Mario spawn terjebak, tidak bisa bergerak.
42 (133) $50 Terowongan awan yang menjebak Mario jika pergi cukup jauh.
62 (131, 240) $4D Kastil beku: Mario spawn di atas, tidak bisa jatuh → terjebak.
127 $4B Terowongan bawah tanah, tapi crash jika pergi terlalu jauh.
137 $4B Mengaktifkan scrolling otomatis cutscene. Mario menemui satu blok brick yang memblokirnya selamanya.

Level ID $50 (cloud tunnel) -- glitch world 42-1 dan E-1 Level ID $4D (castle) -- world 62-1, Mario terjebak di spawn Level ID $4B (tunnel) -- world 127-1, crash jika 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 mendalam

Mari kita lihat format data level yang tepat, untuk memahami mengapa glitch level bisa berdiri sendiri (atau tidak).

Header level: 2 byte, 6 properti

Setiap level dimulai dengan header 2 byte yang mengontrol 6 properti:

; 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)

Tipe modifier mengontrol variasi visual: ombak di atas level air, latar belakang bata 8-3, palet malam 4-3, salju 6-2, dll.

Objek tile: 2 byte, Next Screen Flag, antrian 3 slot

Setelah header datang daftar objek tile, setiap objek 2 byte. Byte $FD menandakan akhir daftar.

; 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

Ketika bit "next screen" diaktifkan, kolom kerja saat ini ditambah 1. Ini memungkinkan penempatan objek melampaui 16 kolom pertama. Objek harus didaftar dalam urutan (kiri ke kanan) karena game memuatnya secara sekuensial:

; 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

Antrian memiliki tepat 3 slot. Konsekuensi langsung: kita tidak bisa memiliki lebih dari 3 objek yang dimulai pada kolom yang sama. Jika antrian penuh, objek ke-4 diabaikan dan tidak akan pernah dimuat.

Inilah mengapa level yang dirancang dengan baik menghindari penumpukan terlalu banyak objek. Contoh di 1-2: kolom dengan blok 1up di langit-langit + bata di sebelahnya dipisah menjadi dua objek berbeda untuk mematuhi batas 3.

Y position khusus: 12, 13, 14, 15

Ketika Y=12, objek tidak memiliki posisi Y (hardcoded oleh tipe):

; 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

Ketika Y=13, dua sub-grup. Jika bit 6 byte 1 bernilai 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

Jika bit6=0, 5 bit rendah mengkodekan screen skip (melompat langsung ke layar N, tanpa melewati next screen flag satu per satu).

Ketika Y=14: prinsip yang sama dengan bit6=1 untuk mengubah tipe modifier, bit6=0 untuk mengubah latar belakang + floor pattern.

Floor pattern: 16 pola lantai

Lantai level tidak terbuat dari objek individual. SMB1 menggunakan floor pattern, pola latar belakang yang diterapkan ke semua kolom sampai perubahan berikutnya:

; 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)

Inilah mengapa lubang adalah objek: mereka menimpa floor pattern pada kolom tertentu, tanpa harus mengubah pattern untuk sisanya.

Batas 256 byte dan repeat

Semua data tile level muat dalam maksimal 256 byte. Y register 6502 digunakan sebagai index, dan berukuran 8 bit. Jika game mencapai akhir data tanpa menemukan byte $FD, ia looping kembali ke awal dan mengulang 256 byte tanpa batas:

; 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.

Beberapa glitch level memanfaatkan repeat ini untuk menghasilkan level yang berlangsung "selamanya".

Sistem sprite: 2 byte + transisi pipe

Sprite mengikuti format yang serupa, tapi tanpa header dan dengan beberapa perbedaan kunci. Byte $FF menandakan akhir daftar.

; 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

Bit terendah dari byte 1 adalah hard level flag: jika diaktifkan, sprite hanya muncul di level ≥ 5-3. Inilah cara level "hard mode" dibuat.

Y position 15 = screen skip (identik dengan tile). Y position 14 = transisi pipe (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.

Sprite tidak memiliki queue system. Satu-satunya batas adalah tidak boleh ada lebih dari 4 sprite yang dimuat secara bersamaan di zona spawn (tepat di luar layar di sebelah kanan). Lebih dari itu, 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 sampai ke warp zone tersembunyi. Dengan memanipulasi penghitung dunia melalui RAM, kita dapat memuat Level ID apa pun.

Tekniknya:

  1. World 1-2: masuk ke pipa akhir tersembunyi
  2. Lakukan wall clip di tembok kanan
  3. Berjalan di kekosongan sampai ke zona warp
  4. Game menafsirkan nilai sebagai dunia

Tapi metode ini hanya memberikan akses ke sebagian kecil glitch world.

Metode ekstrem: NES Tennis cart swap

Lihat bagian "Warm start" di atas untuk penjelasan lengkap. Singkatnya: penghitung langkah Tennis menulis ke byte RAM yang sama dengan dunia awal SMB1, dan deteksi warm start mempertahankan nilai tersebut.

sudut tweak: kode untuk menjelajahi semuanya

Jika kamu ingin menjelajahi semua glitch sendiri di emulator, kamu bisa mem-patch Level ID langsung:

; 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 telah menerbitkan daftar lengkap 128 level × 4 tipe dengan peta yang dihasilkan secara otomatis di rgmechex.com. Setiap entri menunjukkan tile pointer, sprite pointer, dan peta visual level.

Level paling WTF

Level ID $1F (Water): 15 glitch world dalam satu

Tile pointer $A302 (3-4) dikombinasikan dengan sprite pointer $02A0 menghasilkan 15 glitch world berbeda (D-1, J-1, Y-1, Z-1, 55-1, 73-1...). Penjelasan: sprite pointer menunjuk ke area ROM yang berisi data yang cukup mirip dengan sprite valid untuk menghasilkan hasil yang bisa dimainkan, tapi kombinasi tile kastil 3-4 dengan sprite overworld menciptakan rendering yang absurd.

Level ID $28 (Overworld): 38 glitch world = rekor

Rekor absolut. 38 entri glitch world menunjuk ke level yang sama (tile 2-1 + sprite $9F51). Mengapa? Karena sprite pointer $9F51 jatuh di area ROM yang digunakan sebagai padding/data suara yang digunakan kembali oleh banyak ID.

Level ID $49 (Underground): level FDS

Tile pointer $76AE + sprite pointer $1C9D. Tile pointer menunjuk ke area ROM yang disediakan untuk versi Famicom Disk System. Hasilnya: level dengan tile yang tidak ada di kartu standar. Inilah level yang memunculkan level 52-1 dan 196-1.

Level ID $00-$02: level bonus yang sebenarnya

ID ini digunakan oleh sub-level legitimasi game:

  • $00: zona bawah air 5-2/6-2 (digunakan oleh 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 dapat diakses secara normal dan glitch world adalah warp zone memeriksa dunia saat ini:

; 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 world dengan nomor > 8 atau 0 tidak dapat dicapai oleh pipa normal. Diperlukan wall clip atau cart swap.

Mengapa beberapa level crash: jump table

Ketika game memuat objek tile, ia menggunakan tipenya sebagai index di 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 table: mengapa tipe objek yang tidak valid menyebabkan game crash

Jika objek memiliki tipe tidak valid (≥12), game melompat ke penunjuk yang tidak ada di tabel ini. 4 kemungkinan hasil:

  1. Penunjuk valid → objek dimuat secara normal
  2. Penunjuk ke jump table lain (overlap) → objek berbeda muncul. Contoh: tipe 12 menunjuk ke tabel Y=13, yang menghasilkan L-pipe.
  3. Penunjuk ke executable → eksekusi kode acak (kemungkinan crash)
  4. Placeholder eksplisit (NOP) → objek tidak melakukan apa-apa (beberapa sprite seperti ini, menghasilkan musuh yang melayang di tempat tanpa bergerak)

Glitch level ID $58: sprite pointer menunjuk ke alamat tidak valid, game crash

Glitch level ID $50: cloud tunnel, level yang dihasilkan dari data yang rusak

Glitch level ID $58 (terowongan yang crash): sprite pointer-nya menunjuk ke area memori yang tidak ada di NES tanpa mapper ROM. Game mencoba memuat Koopa yang sama 5 kali per frame di posisi (0,0, yang jenuhkan PPU dan menyebabkan 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

Paradox pipe warp

Ingat pengecekan target_world BETWEEN 1 AND 8. Bahkan jika kamu menemukan pipa di glitch world, game memverifikasi bahwa dunia tujuan antara 1 dan 8. Glitch world memiliki nomor > 8 (36-1, 255-1...), sehingga warp gagal.

Ini juga mengapa Minus World tidak memiliki akhir: flagpole tidak ada di sprite, dan pipa tidak membawa ke mana pun.

Trik 5 objek dalam satu kolom

Ada edge case yang memungkinkan melewati batas 3 objek per kolom. Ketika antrian terblokir (slot penuh + objek berikutnya dengan next screen flag hilang), game "memproses prakolom" kolom saat ini dalam loop sampai menemukan objek dengan next screen flag. Selama setiap pemrosesan prakolom:

; 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).

Ini disebut "queue skip" dan digunakan oleh beberapa romhacker untuk membuat level yang lebih padat dari yang formatnya secara normal memungkinkan.

Perbedaan antar versi

Famicom Disk System

Versi FDS SMB1 memiliki memory map yang berbeda. Semua penunjuk level digeser, tapi datanya sama. Yang berubah: indeks glitch world benar-benar berbeda:

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.

Level FDS favorit saya: ID $5F, versi bawah tanah dari paruh kedua 3-3 di tunnel rendah (sayangnya ini autoscroller).

The Lost Levels (Super Mario Bros. 2 Jepang)

Lost Levels mengubah banyak hal:

  1. Urutan tile/sprite identik: tidak ada lagi Frankenstein level (tile dan sprite memuat level yang sama meskipun ID tidak valid)
  2. Satu tabel penunjuk 16-bit alih-alih dua tabel terpisah high/low
  3. 4 file disk: ROM dipecah untuk FDS:
    • File 1: dunia 1-4
    • File 2: dunia 5-8
    • File 3: dunia 9 + sound engine
    • File 4: dunia A-D (tabel penunjuk benar-benar berbeda)
  4. Level ID sama = 4 level mungkin tergantung file yang dimuat
  5. Tidak ada lagi glitch Tennis: opsi continue (lanjut di dunia yang sama setelah game over) membuat warm start tidak perlu, dan game reset segera jika world > 9
  6. Objek baru: jamur racun, blok invisible, blok invisible fire flower, 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)

Port langsung dengan rutinitas 6502 yang sama (SNES mengeksekusi kode NES dalam mode kompatibel):

  • Warp zone diperbaiki: tidak ada lagi Minus World (masuk ke pipa kiri sebelum teks mengarah ke dunia yang benar)
  • Crash: sebagian besar glitch level crash (kecuali ID $6A dan 9-1)
  • Objek kastil ditambahkan: lebih unik
  • Tapi: 4-2 wrong warp masih berfungsi (tidak di-patch!)

4-2 wrong warp: bug penempatan objek

Di 4-2, ada dua objek transisi pipa: anggur (warp zone) dan pipa (ruang coin cash). Objek transisi pertama (anggur) ditempatkan jauh sebelum anggur muncul di layar. Objek kedua (pipa) ditempatkan terlalu terlambat di level.

; 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.

Level looping

Bagaimana loop berfungsi (8-4, 7-4)? Level memiliki checkpoint dengan nomor layar dan posisi Y yang hardcoded:

; 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.

Mengubah format, bukan kode

Salah satu pelajaran paling menarik dari arsitektur ini adalah pengembang SMB1 berhasil menciptakan sistem level yang sangat ekspresif tanpa pernah menyentuh kode rendering 6502. Semua variasi antar level berasal dari data (penunjuk, objek, sprite, floor pattern), bukan kode.

256 glitch world ada karena tabel penunjuk didesain untuk 128 entri × 4 tipe, dan game tidak pernah memvalidasi nilai yang dibacanya. Ketika penunjuk jatuh ke RAM, game menafsirkan registri Mario sebagai tile. Ketika penunjuk jatuh ke data suara, game memainkan musik dalam bentuk level design. Dan ketika jump table overflow, game mengeksekusi apa saja sampai crash.

More Super Mario Bros. Mechanics Explained -- video ke-4

Apa yang bisa kita pelajari dari semua ini

  1. Pemisahan tile/sprite: kemandirian total kedua lapisan, dengan urutan penyimpanan berbeda yang menciptakan Frankenstein level unik
  2. Kompresi RLE + sistem objek: level bukan bitmap tapi daftar objek yang ditempatkan, dengan floor pattern untuk lantai
  3. Antrian 3 slot: batasan ketat hardware (dan desain level)
  4. Tanpa validasi: game mempercayai penunjuk dan jump table, yang menghasilkan glitch yang bisa dimainkan atau crash
  5. Maksimal 256 byte: batasan Y register 6502, yang membuat data mengulang jika terlalu jauh
  6. Warm start / cold start: sistem "lanjutkan" yang membuka jalan ke cart swap Tennis → Mario

Yang paling bagus: semua ini adalah kode 6502 yang muat dalam 40KB. Tidak ada lapisan abstraksi, tidak ada validasi akses memori, tidak ada manajemen pengecualian. Jika penunjuknya buruk, game crash. Dan crash, kita sebut glitch world.

3 hal yang perlu diingat

  1. Glitch world adalah penunjuk yang jatuh ke tempat yang salah -- Game memiliki 128 ID × 4 tipe zona, tapi hanya 34 level unik. Ketika world number rusak (oleh Tennis atau wall clip), game memuat penunjuk yang dirancang untuk level lain, dan 512 kombinasi yang mungkin menghasilkan hasil yang tidak terduga.

  2. Minus World adalah bug warp yang dikombinasikan dengan korupsi -- Pipa kiri di 1-2, jika 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 looping tanpa batas. Ketidakhadiran verifikasi menciptakan ikon tersebut.

  3. Tennis → Mario, 15 tahun sebelum OoT → Paper Mario -- RAM NES bertahan dari pertukaran kartu berkat kapasitor dan sistem warm start / cold start SMB1. Penghitung langkah Tennis (yang menambah byte RAM saat memutar suara langkah) jatuh tepat di alamat world number. Diperlukan digit top score tetap 0, byte $A5 utuh, dan game mendeteksi warm start -- kejadian kebetulan sempurna yang hanya berhasil dengan Tennis.

Video asli dari Retro Game Mechanics Explained adalah pekerjaan yang luar biasa mendetail -- tingkat detail pada disasembli 6502, peta otomatis dari semua level, penjelasan cart swap dan warm start. Jika kamu belum melihat seri ini, tonton, pendek dan setiap menitnya padat.

Kode sumber peta tersedia di rgmechex.com, dan disasembli lengkap SMB1 bersumber terbuka di banyak repo. 40 tahun yang lalu, programmer Jepang menulis sistem level 6502 ini tanpa tes unit dan tanpa pelacakan bug, dan kita terus mempelajari hal-hal baru dengan membuka kode mereka hari ini.

Super Mario Bros. : लेवल फ़ॉर्मेट, पॉइंटर्स और 256 ग्लिच वर्ल्ड्स

128 लेवल्स × 4 ज़ोन टाइप्स 40KB ROM में कैसे समाते हैं, Minus World क्यों मौजूद है, और NES Tennis के एक मैच से ग्लिच वर्ल्ड्स कैसे लोड होते हैं।

परिचय

Super Mario Bros. में 40 किलोबाइट ROM है। आठ वर्ल्ड्स, 32 लेवल्स, दुश्मन, संगीत, पावर-अप्स -- सब कुछ इसी में समाया हुआ है।

लेकिन अगर तुम एक एम्यूलेटर खोलो और सही बाइट्स के साथ छेड़छाड़ करो, तो तुम लेवल 36-1 लोड कर सकते हो। या 255-1। या एक ऐसी दुनिया में पहुँच सकते हो जहाँ सब कुछ Bowser के स्प्राइट्स और ऐसी पाइप्स से बना है जो कहीं नहीं ले जातीं।

ये ग्लिच वर्ल्ड्स एक सरल कारण से मौजूद हैं: SMB1 का लेवल स्टोरेज सिस्टम 8-बिट ऑप्टिमाइज़ेशन का एक अद्भुत नमूना है, और जब हम गेम को उस जगह पढ़ने के लिए मजबूर करते हैं जहाँ नहीं पढ़ना चाहिए, तो परिणाम बेहद रोचक आते हैं।

Retro Game Mechanics Explained ने इस पर 4 वीडियो की एक सीरीज़ बनाई है -- हम इसे एक ही लेख में संकलित कर रहे हैं, उस ज़माने के सबसे ज़्यादा बिकने वाले गेम के 6502 कोड में गोता लगाते हुए।

GLITCH OBJECTS -- RGMechEx की SMB1 की छिपी हुई मैकेनिक्स पर सीरीज़ का शीर्षक कार्ड

World 9-1 -- Tennis कार्ट स्वैप से एक्सेस होने वाले पहले ग्लिच वर्ल्ड का टाइटल स्क्रीन

वार्म स्टार्ट: Tennis की RAM SMB1 में क्यों जीवित रहती है

लेवल स्टोरेज की बात करने से पहले, यह समझना ज़रूरी है कि SMB1 कैसे शुरू होता है। क्योंकि NES Tennis का कार्ट स्वैप ग्लिच पूरी तरह से गेम के वार्म स्टार्ट / कोल्ड स्टार्ट डिटेक्शन सिस्टम पर निर्भर करता है।

संरक्षित 41 बाइट्स

जब SMB1 कोल्ड स्टार्ट का पता लगाता है (पहली बार पावर ऑन या पावर ऑफ/ऑन), तो यह पूरी RAM मिटा देता है। लेकिन जब यह वार्म स्टार्ट का पता लगाता है (रीसेट बटन, पावर कट के बिना), तो यह 41 बाइट्स की एक मेमोरी क्षेत्र को संरक्षित रखता है:

; 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 बाइट्स एक ही कार्य के लिए हैं: खिलाड़ी को गेम ओवर के बाद उसी वर्ल्ड में जारी रखने की अनुमति देना। अगर तुम 6-3 में मरते हो, तो गेम स्टार्टअप बाइट में वर्ल्ड 6 लिखता है, और टाइटल स्क्रीन पर, अगर तुम A + Start दबाकर रखते हो, तो तुम 6-1 से शुरू करते हो।

वार्म स्टार्ट के दौरान RAM में संरक्षित 41 बाइट्स -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, और जादुई बाइट $A5

वार्म स्टार्ट की डबल जाँच

कोल्ड स्टार्ट बनाम वार्म स्टार्ट -- रीसेट डिटेक्शन का डायग्राम

जब 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

बाइट $A5 और टॉप स्कोर के डिजिट्स की जाँच -- वार्म स्टार्ट का कोर

डबल जाँच क्यों? क्योंकि $A5 बाइट किसी संयोग से भी मौजूद हो सकता है (कोई अन्य गेम जो यह वैल्यू छोड़ गया हो, या RAM चिप की डिफ़ॉल्ट आराम स्थिति)। टॉप स्कोर के डिजिट्स की वैधता (0-9) जाँचकर, हम सुनिश्चित करते हैं कि डेटा सुसंगत है।

Tennis ही एकमात्र गेम क्यों है जो काम करता है

जब हम पहली बार SMB1 डालते हैं (कोल्ड स्टार्ट), तो गेम:

  1. पूरी RAM मिटाता है → टॉप स्कोर = 0, वर्ल्ड बाइट = 0
  2. पता $0787 पर $A5 लिखता है

इसके बाद, हम कंसोल बंद किए बिना Tennis पर स्विच करते हैं। Tennis:

  • शुरुआत में RAM साफ नहीं करता (बहुत कम NES गेम्स ऐसा करते हैं)
  • टॉप स्कोर बाइट्स पर नहीं लिखता → वे 0 (वैध) बने रहते हैं
  • $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. $A5 बाइट अभी भी वहाँ है (Tennis ने इसे नहीं छुआ)
  2. टॉप स्कोर के डिजिट्स अभी भी 0 हैं (वैध)
  3. वर्ल्ड बाइट अब 8+ है (Tennis के कदमों से बढ़ा)
  4. SMB1 वार्म स्टार्ट का पता लगाता है → खराब वर्ल्ड बाइट को संरक्षित करता है
  5. A + Start दबाकर रखने पर → world 9-1, world A-1, world 36-1, आदि

Tennis से पहले Mario क्यों बूट करना होगा

एक बारीकी: पहले SMB1 बूट करना होगा, फिर Tennis, फिर वापस SMB1। अगर तुम सीधे Tennis से शुरू करो, तो $A5 बाइट कभी नहीं लिखी जाएगी (Tennis $A5 नहीं लिखता), इसलिए वार्म स्टार्ट डिटेक्शन फेल हो जाएगा और RAM मिटा दी जाएगी।

Tennis का कदम काउंटर: हर कदम वर्ल्ड बाइट बढ़ाता है

NES Tennis के माध्यम से Glitch Worlds एक्सेस करना -- कार्ट स्वैप समझाने वाला वीडियो

SMB1 अपने लेवल्स को 40KB में कैसे स्टोर करता है

Nintendo R&D4 को एक सरल दिखने वाली समस्या हल करनी थी: ऐसे लेवल्स को दर्शाना जो हॉरिज़ॉन्टली स्क्रॉल करते हैं -- टाइल्स, दुश्मन, आइटम्स, सब कुछ एक बेहद सीमित ROM बजट में।

समाधान दो पूरी तरह से स्वतंत्र डेटा लेयर्स का विभाजन है:

टाइल लेआउट (लेवल का मानचित्र)

हर लेवल ROM में एक कंप्रेस्ड टाइल स्ट्रक्चर की ओर इंगित करने वाले पॉइंटर द्वारा परिभाषित होता है। कंप्रेशन सरल लेकिन शानदार है: एक "कंट्रोल" बाइट जिसके बाद 1-3 डेटा बाइट्स होते हैं।

टाइल फ़ॉर्मेट रन्स (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

हर लेवल में 16 कॉलम की 13 पंक्तियाँ टाइल्स की होती हैं (13×16 = 208 दृश्य टाइल्स)। लेकिन कंप्रेस्ड फ़ॉर्मेट से काफ़ी कम जगह घेरी जा सकती है -- उदाहरण के लिए, आसमान और खाली कॉलम बिल्कुल जगह नहीं लेते।

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

स्प्राइट लेआउट (दुश्मन और ऑब्जेक्ट्स)

साथ ही, दुश्मन और ऑब्जेक्ट्स (ब्लॉक्स ?, ट्यूब, गूम्बास, कूपास) एक पूरी अलग स्ट्रक्चर में स्टोर होते हैं। हर स्पॉन 2 बाइट्स से परिभाषित होता है:

; 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 अलग-अलग स्प्राइट पेज रेफ़र कर सकता है (यानी, 16 कॉलम के 5 "स्क्रीन")। लेकिन व्यवहार में अधिकतर लेवल्स सिर्फ 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 असाइन की गईं

पॉइंटर्स टाइल्स और स्प्राइट्स का अलग क्रम -- Frankenstein levels का कारण

34 अद्वितीय लेवल्स और 7-बिट ID सिस्टम

NES की RAM चिप (MB8416A) -- यही कार्ट्रिज स्वैप के दौरान डेटा को संरक्षित रखती है

SMB1 के पास 32 लेवल्स नहीं, बल्कि 34 अद्वितीय लेवल्स हैं। बहुत से लेवल्स डुप्लिकेट हैं (5-3 = 1-3 लेकिन Bullet Bills के साथ) जो "हार्ड मोड" फ्लैग से चिह्नित हैं। असली अद्वितीय लेवल्स:

  • पानी (टाइप 0): 3 लेवल्स (2-2, 7-2, बोनस ज़ोन 5-2/6-2)
  • ओवरवर्ल्ड (टाइप 1): 22 लेवल्स (जिनमें 2 बोनस क्लाउड रूम्स शामिल हैं)
  • अंडरग्राउंड (टाइप 2): 3 लेवल्स (जिनमें भूमिगत बोनस रूम्स शामिल हैं)
  • कैस्टल (टाइप 3): 6 लेवल्स
  • + 1 कटसीन रूम (भूमिगत/पानी वाले लेवल्स से पहले)
  • + 1 वार्प ज़ोन 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 किसी भी चीज़ की ओर इंगित करती हैं।

पॉइंटर्स की टेबल्स: दो सूचियाँ, दो क्रम

टाइल और स्प्राइट पॉइंटर्स एक ही क्रम में स्टोर नहीं होते। कोड दो अलग 16-बिट सूचियों का उपयोग करता है (हाई बाइट / लो बाइट दो अलग टेबल्स में):

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 अवैध होता है, तो टाइल और स्प्राइट पॉइंटर्स अलग-अलग लेवल्स लोड करते हैं, जिससे Frankenstein levels बनते हैं।

इन दो सूचियों के बीच नेविगेट करने के लिए, गेम छोटी ऑफसेट टेबल्स का उपयोग करता है (जैसे विषय सूची):

; 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, ओवरवर्ल्ड नंबर 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 ✓

अब, $43 (अंडरग्राउंड नंबर 3, जो मौजूद नहीं है) जैसे अवैध ID के साथ क्या होता है?

; 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: 1-4 की टाइल्स + 5-2 के पानी के स्प्राइट्स

Glitch Level Pointers का अन्वेषण -- ऑफसेट टेबल्स समझाई गईं

वर्ल्ड इंडेक्स टेबल -- जब world 9 का ओवरफ्लो एक ग्लिच लेवल बनाता है

वर्ल्ड इंडेक्स टेबल: world 9 ओवरफ्लो क्यों करता है

8 बाइट्स की एक ROM टेबल है जो हर वर्ल्ड (1-8) के पहले लेवल का इंडेक्स देती है। और ठीक उसके बाद, सभी लेवल्स के 36 Level IDs की टेबल गेम क्रम में है।

; 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 लोड करने की कोशिश करते हैं, तो गेम WorldIndexTable का 9वाँ बाइट पढ़ता है... जो मौजूद ही नहीं है। यह LevelIDTable में 1 बाइट ओवरफ्लो करता है, $25 वैल्यू पढ़ता है, फिर LevelIDTable में $25 को इंडेक्स (37वीं प्रविष्टि) के रूप में उपयोग करता है -- जो दोबारा SpriteOffsetTable में 2 बाइट्स ओवरफ्लो करता है, और 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) के लिए, ओवरफ्लो और भी आगे जाता है और Level ID $01 पर आता है, जो 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

ग्लिच वर्ल्ड्स क्यों मौजूद हैं

गेम में 32 "वैध" लेवल्स हैं (8 वर्ल्ड्स × 4 लेवल्स)। लेकिन पॉइंटर टेबल प्रत्येक ज़ोन टाइप के लिए 128 प्रविष्टियाँ बनाती है। 32 से ऊपर की प्रविष्टियों में वह होता है जो उन पतों पर ROM में होता है -- कभी कोई अन्य लेवल, कभी साउंड डेटा, कभी RAM, कभी कुछ भी।

Level ID $01 Water (Minus World) -- टाइल पॉइंटर $AE45, स्प्राइट पॉइंटर $A171

Level ID $01 + AreaType 0 = Minus World

ग्लिच वर्ल्ड्स में सबसे प्रसिद्ध। Level ID $01 जो AreaType 0 (पानी) में है, इसकी ओर इंगित करता है:

  • टाइल पॉइंटर: $AE45 → 2-2/7-2 का पानी के नीचे का क्षेत्र
  • स्प्राइट पॉइंटर: $A171 → 2-2/7-2 के स्प्राइट्स

परिणाम: एक पानी का लेवल जो 2-2 जैसा दिखता है, लेकिन अनंत काल तक लूप करता रहता है क्योंकि फ्लैगपोल मौजूद ही नहीं है। लेवल का अंत नहीं, कोई बाहर निकलने का रास्ता नहीं।

यह लेवल 36-1 है (या दुनिया $-1 में 36-1)।

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 की ओर इंगित करते हैं

जब टाइल पॉइंटर या स्प्राइट पॉइंटर ROM के बजाय RAM के पते ($00-$7F) की ओर इंगित करता है, तो गेम RAM के लगातार बदलते मानों को टाइल्स के रूप में पढ़ने की कोशिश करता है:

; 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 हिलता है, टाइमर चलता है, आदि), तो लेवल के "स्प्राइट्स" भी बदलते हैं। इसलिए कुछ ग्लिच वर्ल्ड्स में दुश्मन टिमटिमाते रहते हैं और लगातार बदलते रहते हैं।

Level ID $03 Water -- स्प्राइट पॉइंटर $009D RAM की ओर इंगित करता है, अप्रयोग्य लेवल

Level ID $36: खाली लेवल (ओवरवर्ल्ड)

Level ID $36 ओवरवर्ल्ड में:

  • टाइल पॉइंटर: $AC35 (1-2)
  • स्प्राइट पॉइंटर: $A0D8 (1-2)

परिणाम: कुछ नहीं। गेम लेवल लोड करता है लेकिन यह RGMechEx के कैटलॉग में "बिना लेवल" के रूप में चिह्नित है। टाइल्स शायद वैध हैं लेकिन स्प्राइट्स ऐसी जगह की ओर इंगित करते हैं जो एक खाली या गैर-कार्यात्मक लेवल बनाती है।

Level ID $1D (कैस्टल): क्रैश का चैंपियन

Level ID $1D कैस्टल में:

  • टाइल पॉइंटर: $A210 (4-4)
  • स्प्राइट पॉइंटर: $7EA0 (RAM!)

स्प्राइट पॉइंटर RAM में = अपरिभाषित स्प्राइट्स। गेम पहली टाइल पंक्ति में एक स्पाइनी बॉल या बुलेट बिल ब्लास्टर दिखाने की कोशिश करता है। यह तुरंत क्रैश हो जाता है।

; 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 ग्लिच वर्ल्ड्स का कैटलॉग

RGMechEx ने एक स्क्रिप्ट लिखा है जो सभी लेवल्स के मैप्स जनरेट करता है, 4 ज़ोन टाइप्स के लिए, और हर एक के 128 IDs।

वर्ल्ड काउंटर 8 बिट्स पर है (0-255)। वर्ल्ड्स 1-8 वैध हैं। 248 ग्लिच वर्ल्ड्स संभावित बचते हैं। हर ग्लिच वर्ल्ड उस वर्ल्ड के पहले लेवल से मेल खाता है, और उसका Level ID WorldIndexTable के ओवरफ्लो मैकेनिज़्म से गणना होता है।

ग्लिच वर्ल्ड्स की टेबल -- 248 खराब वर्ल्ड्स, 68 पहले लेवल्स एक्सेसिबल

128 संभावित IDs में से, सिर्फ 68 किसी वर्ल्ड के "पहले लेवल" हैं (ग्लिच वर्ल्ड नंबर से एक्सेसेबल)। बाकी 60 लेवल 2+ या अप्रयोग्य हैं।

टाइप अद्वितीय खेलने योग्य IDs क्रैश करने वाले IDs खाली IDs
पानी (0) ~20 ~60 ~48
ओवरवर्ल्ड (1) ~30 ~55 ~43
अंडरग्राउंड (2) ~15 ~65 ~48
कैस्टल (3) ~25 ~58 ~45

बहुत से IDs एक ही लेवल की ओर ले जाते हैं क्योंकि पॉइंटर्स उन्हीं ROM पतों पर गिरते हैं। उदाहरण के लिए, Level ID $28 (ओवरवर्ल्ड) -- टाइल पॉइंटर $A7CD (2-1) -- 38 अलग-अलग ग्लिच वर्ल्ड्स में दिखाई देता है, क्योंकि इसका स्प्राइट पॉइंटर $9F51 ROM के एक ऐसे क्षेत्र की ओर इंगित करता है जिसे कई IDs द्वारा पैडिंग/साउंड डेटा के रूप में उपयोग किया जाता है।

Level ID $28 (ओवरवर्ल्ड) का मानचित्र -- 2-1 की टाइल्स सामान्य स्प्राइट्स के साथ, 38 ग्लिच वर्ल्ड्स

Super Mario Bros. Glitch Levels Explained -- तीसरा वीडियो

6 ग्लिच लेवल्स जो सच में अद्वितीय हैं

19 ग्लिच लेवल IDs में से, सिर्फ 6 लोडिंग पर तुरंत क्रैश नहीं होते:

वर्ल्ड Level ID विवरण
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 (क्लाउड ट्यून) -- ग्लिच वर्ल्ड 42-1 और E-1 Level ID $4D (कैस्टल) -- world 62-1, Mario स्पॉन पर ब्लॉक Level ID $4B (ट्यून) -- world 127-1, क्रैश अगर बहुत आगे जाओ

248 में से सिर्फ छह ग्लिच वर्ल्ड्स ऐसा कुछ बनाते हैं जो सच में नया हो। बाकी सामान्य लेवल्स हैं गलत ज़ोन टाइप के साथ, या काले स्क्रीन।

लेवल फ़ॉर्मेट विस्तार से

लेवल डेटा के सटीक फ़ॉर्मेट पर ध्यान दें, ताकि यह समझा जा सके कि ग्लिच लेवल्स क्यों टिकते हैं (या नहीं)।

लेवल हेडर: 2 बाइट्स, 6 गुण

हर लेवल 2 बाइट्स के एक हेडर से शुरू होता है जो 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)

मॉडिफ़ायर टाइप दृश्य भिन्नताओं को नियंत्रित करता है: पानी के लेवल्स के ऊपर लहरें, 8-3 की ईंट पृष्ठभूमि, 4-3 की रात्रि पैलेट, 6-2 की बर्फ, आदि।

टाइल ऑब्जेक्ट्स: 2 बाइट्स, नेक्स्ट स्क्रीन फ्लैग, 3-स्लॉट क्यू

हेडर के बाद टाइल ऑब्जेक्ट्स की एक सूची आती है, हर ऑब्जेक्ट 2 बाइट्स का होता है। $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

जब "नेक्स्ट स्क्रीन" बिट सेट होता है, तो वर्तमान कार्य कॉलम 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 स्लॉट्स होते हैं। सीधा परिणाम: एक ही कॉलम पर 3 से ज़्यादा ऑब्जेक्ट्स शुरू नहीं हो सकते। अगर क्यू भरी हो, तो चौथा ऑब्जेक्ट अनदेखा कर दिया जाता है और कभी लोड नहीं होगा।

इसलिए अच्छी तरह डिज़ाइन किए गए लेवल्स बहुत सारे ऑब्जेक्ट्स एकत्र करने से बचते हैं। 1-2 में उदाहरण: छत में 1up ब्लॉक वाला कॉलम + उसके बगल की ईंटें 3 की सीमा का सम्मान करने के लिए दो अलग ऑब्जेक्ट्स में विभाजित हैं।

विशेष Y पोज़िशन: 12, 13, 14, 15

जब Y=12 होता है, तो ऑब्जेक्ट की कोई Y पोज़िशन नहीं होती (यह टाइप द्वारा हार्डकोडेड होती है):

; 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 होता है, दो सब-ग्रुप्स। अगर बाइट 1 का बिट 6 = 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 बिट्स एक स्क्रीन स्किप एन्कोड करते हैं (बिना नेक्स्ट स्क्रीन फ्लैग के एक-एक करके गुज़रे, सीधे N स्क्रीन पर जाना)।

जब Y=14: वही सिद्धांत bit6=1 से मॉडिफ़ायर टाइप बदलने के लिए, bit6=0 से पृष्ठभूमि + फ्लोर पैटर्न बदलने के लिए।

फ्लोर पैटर्न्स: 16 ज़मीन के पैटर्न्स

लेवल्स की ज़मीन अलग-अलग ऑब्जेक्ट्स से नहीं बनती। SMB1 फ्लोर पैटर्न्स का उपयोग करता है, एक बैकग्राउंड पैटर्न जो अगले बदलाव तक सभी कॉलम्स पर लागू होता है:

; 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)

इसलिए छेद ऑब्जेक्ट्स हैं: वे एक विशिष्ट कॉलम पर फ्लोर पैटर्न को ओवरराइड करते हैं, बाकी सब के लिए पैटर्न बदले बिना।

256 बाइट्स की सीमा और रीपीट

एक लेवल का सारा टाइल डेटा अधिकतम 256 बाइट्स में समाता है। 6502 का Y रजिस्टर इंडेक्स के रूप में उपयोग होता है, और इसमें 8 बिट्स होते हैं। अगर गेम $FD बाइट मिले बिना डेटा के अंत तक पहुँच जाता है, तो यह शुरुआत पर वापस लूप करता है और 256 बाइट्स को अनंत काल तक दोहराता है:

; 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.

कुछ ग्लिच लेवल्स इस रीपीट का फ़ायदा उठाते हैं ऐसे लेवल्स बनाने के लिए जो "अनिश्चित काल" तक चलते हैं।

स्प्राइट सिस्टम: 2 बाइट्स + पाइप ट्रांज़िशन्स

स्प्राइट्स समान फ़ॉर्मेट का पालन करते हैं, लेकिन हेडर के बिना और कुछ महत्वपूर्ण अंतरों के साथ। $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

बाइट 1 का निचला बिट हार्ड लेवल फ्लैग है: अगर 1 सेट हो, तो स्प्राइट सिर्फ लेवल्स ≥ 5-3 में दिखता है। इस तरह "हार्ड मोड" लेवल्स बनते हैं।

Y पोज़िशन 15 = स्क्रीन स्किप (टाइल्स के समान)। Y पोज़िशन 14 = पाइप ट्रांज़िशन (3 बाइट्स):

; 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.

स्प्राइट्स में कोई क्यू सिस्टम नहीं है। एकमात्र सीमा यह है कि स्पॉन क्षेत्र में (दाईं ओर बस बाहर स्क्रीन) एक साथ 4 से ज़्यादा स्प्राइट्स लोड नहीं हो सकते। इससे ज़्यादा, स्प्राइट्स अनदेखे कर दिए जाते हैं।

ग्लिच वर्ल्ड्स तक कैसे पहुँचें

दो मुख्य विधियाँ हैं।

क्लासिक विधि: वॉल क्लिप

वॉल क्लिप (दीवारों से गुज़रना) सामान्य लेवल से बाहर निकलने और छिपी वार्प ज़ोन तक चलने की अनुमति देता है। RAM के माध्यम से वर्ल्ड काउंटर में हेरफेर करके, हम कोई भी Level ID लोड कर सकते हैं।

तकनीक:

  1. World 1-2: छिपे अंतिम पाइप में जाओ
  2. दाईं दीवार पर वॉल क्लिप करो
  3. वार्प ज़ोन तक खाली में चलो
  4. गेम मानों को वर्ल्ड्स के रूप में पढ़ता है

लेकिन यह विधि सिर्फ ग्लिच वर्ल्ड्स के एक छोटे हिस्से तक पहुँच देती है।

एक्सट्रीम विधि: NES Tennis कार्ट स्वैप

पूरा विवरण ऊपर "वार्म स्टार्ट" अनुभाग में देखें। संक्षेप में: Tennis का कदम काउंटर SMB1 के वर्ल्ड स्टार्ट बाइट के समान RAM बाइट पर लिखता है, और वार्म स्टार्ट डिटेक्शन इस वैल्यू को संरक्षित करता है।

बिडलर्स का कोना: सब कुछ एक्सप्लोर करने के लिए कोड

अगर तुम एम्यूलेटर में खुद सभी ग्लिच एक्सप्लोर करना चाहते हो, तो तुम 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 पर प्रकाशित की है, स्वचालित रूप से जनरेटेड मैप्स के साथ। हर प्रविष्टि टाइल पॉइंटर, स्प्राइट पॉइंटर, और लेवल का एक दृश्य मानचित्र दिखाती है।

सबसे विचित्र लेवल्स

Level ID $1F (पानी): एक में 15 ग्लिच वर्ल्ड्स

टाइल पॉइंटर $A302 (3-4) और स्प्राइट पॉइंटर $02A0 का संयोजन 15 अलग-अलग ग्लिच वर्ल्ड्स देता है (D-1, J-1, Y-1, Z-1, 55-1, 73-1...)। व्याख्या: स्प्राइट पॉइंटर ROM के एक ऐसे क्षेत्र की ओर इंगित करता है जिसमें खेलने योग्य परिणाम देने के लिए पर्याप्त रूप से वैध स्प्राइट्स के करीब डेटा है, लेकिन 3-4 की कैस्टल टाइल्स और ओवरवर्ल्ड स्प्राइट्स का संयोजन एक हास्यास्पद रेंडरिंग बनाता है।

Level ID $28 (ओवरवर्ल्ड): 38 ग्लिच वर्ल्ड्स = रिकॉर्ड

बिल्कुल रिकॉर्ड। 38 ग्लिच वर्ल्ड प्रविष्टियाँ एक ही लेवल (2-1 टाइल्स + $9F51 स्प्राइट्स) की ओर इंगित करती हैं। क्यों? क्योंकि स्प्राइट पॉइंटर $9F51 ROM के एक ऐसे क्षेत्र में गिरता है जिसे कई IDs द्वारा पैडिंग/साउंड डेटा के रूप में पुनः उपयोग किया जाता है।

Level ID $49 (अंडरग्राउंड): FDS लेवल

टाइल पॉइंटर $76AE + स्प्राइट पॉइंटर $1C9D। टाइल पॉइंटर ROM के Famicom Disk System संस्करण के लिए आरक्षित क्षेत्र की ओर इंगित करता है। परिणाम: एक लेवल जिसमें ऐसी टाइल्स हैं जो मानक कार्ट्रिज में मौजूद नहीं हैं। यह वह लेवल है जो 52-1 और 196-1 बनाता है।

Level ID $00-$02: असली बोनस लेवल्स

ये IDs गेम के वैध सब-लेवल्स द्वारा उपयोग होते हैं:

  • $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)

सामान्य रूप से एक्सेसेबल "बोनस" लेवल और ग्लिच वर्ल्ड के बीच अंतर यह है कि वार्प ज़ोन वर्तमान वर्ल्ड की जाँच करते हैं:

; 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

8 से ज़्यादा या 0 नंबर वाले ग्लिच वर्ल्ड्स सामान्य पाइप्स से नहीं पहुँचे जा सकते। वॉल क्लिप या कार्ट स्वैप ज़रूरी है।

कुछ लेवल्स क्रैश क्यों करते हैं: जंप टेबल्स

जब गेम एक टाइल ऑब्जेक्ट लोड करता है, तो वह इसके टाइप को एक जंप टेबल में इंडेक्स के रूप में उपयोग करता है:

; 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

जंप टेबल्स: अवैध ऑब्जेक्ट टाइप गेम को क्यों क्रैश करता है

अगर ऑब्जेक्ट का अवैध टाइप है (≥12), तो गेम एक ऐसे पॉइंटर पर कूदता है जो इस टेबल में मौजूद ही नहीं है। 4 संभावित परिणाम:

  1. वैध पॉइंटर → ऑब्जेक्ट सामान्य रूप से लोड होता है
  2. किसी अन्य जंप टेबल की ओर पॉइंटर (ओवरलैप) → अलग ऑब्जेक्ट दिखता है। उदाहरण: टाइप 12 Y=13 टेबल की ओर इंगित करता है, जिससे L-pipe बनता है।
  3. एग्ज़िक्यूटेबल की ओर पॉइंटर → यादृच्छिक कोड का निष्पादन (संभावित क्रैश)
  4. स्पष्ट प्लेसहोल्डर (NOP) → ऑब्जेक्ट कुछ नहीं करता (कुछ स्प्राइट्स ऐसे ही होते हैं, ऐसे दुश्मन बनाते हैं जो एक ही जगह पर उड़ते रहते हैं बिना हिले)

ग्लिच लेवल ID $58: स्प्राइट पॉइंटर अवैध पते की ओर इंगित करता है, गेम क्रैश

ग्लिच लेवल ID $50: क्लाउड ट्यून, खराब डेटा से जनरेटेड लेवल

ग्लिच लेवल ID $58 (क्रैश करने वाला ट्यून): इसका स्प्राइट पॉइंटर ऐसी मेमोरी रीजन की ओर इंगित करता है जो बिना ROM मैपर वाले NES पर मौजूद ही नहीं है। गेम (0,0) पोज़िशन पर हर फ्रेम में एक ही Koopa को 5 बार लोड करने की कोशिश करता है, जिससे PPU भर जाती है और फ़्रीज़ हो जाता है।

; 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

पाइप वार्प का पैराडॉक्स

याद रखो target_world BETWEEN 1 AND 8 चेक। भले ही तुम ग्लिच वर्ल्ड में एक पाइप ढूंढ लो, गेम जाँचता है कि गंतव्य वर्ल्ड 1 और 8 के बीच है। ग्लिच वर्ल्ड्स के नंबर > 8 हैं (36-1, 255-1...), इसलिए वार्प फेल हो जाता है।

इसलिए Minus World का अंत नहीं है: स्प्राइट्स में फ्लैगपोल मौजूद नहीं है, और पाइप्स कहीं नहीं ले जातीं।

एक कॉलम में 5 ऑब्जेक्ट्स का ट्रिक

एक ऐसा एज केस मौजूद है जो प्रति कॉलम 3 ऑब्जेक्ट्स की सीमा को तोड़ने की अनुमति देता है। जब क्यू ब्लॉक हो जाती है (स्लॉट्स भरे + अगला ऑब्जेक्ट नेक्स्ट स्क्रीन फ्लैग के बिना), तो गेम वर्तमान कॉलम को नेक्स्ट स्क्रीन फ्लैग वाला ऑब्जेक्ट मिलने तक लूप में "प्री-प्रोसेस" करता है। हर प्री-प्रोसेसिंग के दौरान:

; 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).

इसे "क्यू स्किप" कहते हैं और कुछ रोमहैकर्स फ़ॉर्मेट की सामान्य सीमा से ज़्यादा घने लेवल्स बनाने के लिए इसका उपयोग करते हैं।

संस्करणों के बीच अंतर

Famicom Disk System

SMB1 का FDS संस्करण एक अलग मेमोरी मैप रखता है। सभी लेवल पॉइंटर्स शिफ्ट हो जाते हैं, लेकिन डेटा वही होता है। जो बदलता है: ग्लिच वर्ल्ड्स के इंडिकेस पूरी तरह अलग होते हैं:

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 के दूसरे हिस्से का एक भूमिगत संस्करण निचले ट्यून में (दुख की बात है कि यह ऑटोस्क्रोलर है)।

The Lost Levels (Super Mario Bros. जापानी संस्करण)

Lost Levels बहुत सी चीज़ें बदलता है:

  1. टाइल्स/स्प्राइट्स का समान क्रम: कोई Frankenstein levels नहीं (टाइल्स और स्प्राइट्स अवैध 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 ग्लिच नहीं: कंटिन्यू ऑप्शन (गेम ओवर के बाद उसी वर्ल्ड में जारी) वार्म स्टार्ट को अनावश्यक बनाता है, और गेम world > 9 होने पर तुरंत रीसेट करता है
  6. नए ऑब्जेक्ट्स: ज़हरीला मशरूम, अदृश्य ब्लॉक, अदृश्य फ़ायर फ्लावर ब्लॉक, उल्टे पाइप्स, हवा -- लेकिन मौजूदा सूचियों के बीच में डाले गए → SMB1 के साथ बैकवर्ड असंगतता
  7. Piranha Plants हमेशा लाल वर्ल्ड 4 के बाद, हरे स्प्रिंगबोर्ड सिर्फ वर्ल्ड्स 2/B/3/C/7 में

Super Mario All-Stars (SNES)

सीधा पोर्ट समान 6502 रूटीन्स के साथ (SNES NES कोड को संगत मोड में चलाता है):

  • वार्प ज़ोन फिक्स: कोई Minus World नहीं (बाएं पाइप में टेक्स्ट से पहले एंटर करने से सही वर्ल्ड में पहुँचते हैं)
  • क्रैश: अधिकतर ग्लिच लेवल्स क्रैश करते हैं (सिर्फ ID $6A और 9-1 को छोड़कर)
  • कैस्टल ऑब्जेक्ट्स जोड़े गए: और अद्वितीय रेंडरिंग
  • लेकिन: 4-2 रँग वार्प अभी भी काम करता है (पैच नहीं हुआ!)

4-2 रँग वार्प: ऑब्जेक्ट प्लेसमेंट का बग

4-2 में, दो पाइप ट्रांज़िशन ऑब्जेक्ट्स हैं: बेल (वार्प ज़ोन) और पाइप (कॉइन कैश रूम)। पहला ट्रांज़िशन ऑब्जेक्ट (बेल वाला) बेल स्क्रीन पर दिखने से बहुत पहले रखा जाता है। दूसरा (पाइप) लेवल में बहुत देर से रखा जाता है।

; 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.

लूप वाले लेवल्स

लूप्स (8-4, 7-4) कैसे काम करते हैं? लेवल में चेकपॉइंट्स होते हैं जिनमें स्क्रीन नंबर और Y पोज़िशन हार्डकोडेड होते हैं:

; 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 रेंडरिंग कोड को कभी छुए। लेवल्स के बीच सारा भिन्नता डेटा (पॉइंटर्स, ऑब्जेक्ट्स, स्प्राइट्स, फ्लोर पैटर्न्स) से आता है, कोड से नहीं।

256 ग्लिच वर्ल्ड्स इसलिए मौजूद हैं क्योंकि पॉइंटर टेबल्स 128 प्रविष्टियों × 4 टाइप्स के लिए साइज़ की गई हैं, और गेम जो मान पढ़ता है उसकी कभी वैलिडेशन नहीं करता। जब पॉइंटर RAM में गिरता है, तो गेम Mario के रजिस्टर्स को टाइल्स के रूप में पढ़ता है। जब पॉइंटर साउंड डेटा में गिरता है, तो गेम लेवल डिज़ाइन के रूप में संगीत बजाता है। और जब जंप टेबल्स ओवरफ्लो होती हैं, तो गेम क्रैश होने तक कुछ भी चलाता है।

Super Mario Bros. Mechanics Explained -- चौथा वीडियो

इससे क्या सीखा जा सकता है

  1. टाइल्स/स्प्राइट्स का विभाजन: दोनों लेयर्स की पूर्ण स्वतंत्रता, अलग स्टोरेज क्रम के साथ जो अद्वितीय Frankenstein levels बनाता है
  2. RLE कंप्रेशन + ऑब्जेक्ट सिस्टम: लेवल्स बिटमैप्स नहीं बल्कि रखे गए ऑब्जेक्ट्स की सूचियाँ हैं, ज़मीन के लिए फ्लोर पैटर्न्स के साथ
  3. 3-स्लॉट क्यू: हार्डवेयर (और लेवल डिज़ाइन) की कठोर सीमा
  4. कोई वैलिडेशन नहीं: गेम पॉइंटर्स और जंप टेबल्स पर भरोसा करता है, जो या तो खेलने योग्य ग्लिच्स या क्रैश बनाता है
  5. 256 बाइट्स अधिकतम: 6502 Y रजिस्टर की सीमा, जिससे बहुत आगे जाने पर डेटा दोहराता है
  6. वार्म स्टार्ट / कोल्ड स्टार्ट: एक "जारी रखने" का सिस्टम जिसने Tennis कार्ट स्वैप → Mario के लिए दरवाज़ा खोला

सबसे अच्छी बात: यह सब 40KB में समाया हुआ 6502 कोड है। कोई एब्स्ट्रैक्शन लेयर नहीं, कोई मेमोरी एक्सेस वैलिडेशन नहीं, कोई एक्सेप्शन हैंडलर नहीं। अगर पॉइंटर खराब है, तो गेम क्रैश। और क्रैश को हम ग्लिच वर्ल्ड्स कहते हैं।

3 याद रखने योग्य बातें

  1. ग्लिच वर्ल्ड्स गलत जगह गिरने वाले पॉइंटर्स हैं -- गेम में 128 IDs × 4 ज़ोन टाइप्स हैं, लेकिन सिर्फ 34 अद्वितीय लेवल्स। जब वर्ल्ड नंबर खराब हो जाता है (Tennis या वॉल क्लिप से), तो गेम किसी अन्य लेवल के लिए डिज़ाइन किया गया पॉइंटर लोड करता है, और 512 संभावित संयोजन अनपेक्षित परिणाम देते हैं।

  2. Minus World वार्प बग और भ्रष्टाचार का संयोजन है -- 1-2 का बायां पाइप, अगर टेक्स्ट दिखने से पहले एक्टिवेट हो, तो वर्ल्ड 36 (0x24) लोड करता है। यह वर्ल्ड Level ID $01 (2-2 का पानी) की ओर इंगित करता है, एक बिना फ्लैगपोल वाला लेवल। और चूंकि वर्ल्ड 36 के लिए कोई पाइप ट्रांज़िशन नहीं है, लेवल अनंत काल तक लूप करता रहता है। जाँच की कमी आइकन बनाती है।

  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 أنواع منطقة في 40 كيلوبايت من ROM، ولماذا ي Exists Minus World، وكيف يمكن لمباراة تنس NES أن تحمّل عوالم الخلل.

المقدمة

Super Mario Bros. هو 40 كيلوبايت من ROM. ثمانية عوالم، 32 مستوى، أعداء، موسيقى، قوى خارقة، كل ذلك يتسع في ذلك.

لكن إذا فتحت محاكيًا وعبثت بالبايتات الصحيحة، يمكنك تحميل المستوى 36-1. أو 255-1. أو الهبوط في عالم تكون فيه كل شيء مصنوع من سبرايتات Bowser وأنابيب تؤدي إلى لا مكان.

هذه العوالم الخاطئة (Glitch Worlds) موجودة لسبب بسيط: نظام تخزين مستويات SMB1 هو عبارة عن تحفة من التحسين على 8 بت، وعندما تجبر اللعبة على القراءة من المكان الخطأ، فهي تنتج نتائج مثيرة.

قام Retro Game Mechanics Explained بعمل سلسلة من 4 مقاطع فيديو حول هذا الموضوع -- وسنجمعها في غوص واحد في كود 6502 لأكثر لعبة مبيعاً في ذلك العصر.

GLITCH OBJECTS -- عنوان سلسلة RGMechEx حول آليات SMB1 المخفية

World 9-1 -- شاشة العنوان لأول عالم خلل يمكن الوصول إليه عبر تبادل كارترidge Tennis

البدء الدافئ: لماذا تبقى ذاكرة RAM من Tennis في SMB1

قبل التحدث عن تخزين المستويات، يجب أن نفهم كيف يبدأ SMB1. لأن خلل تبادل الكارترidge في NES Tennis يعتمد بالكامل على نظام اكتشاف البدء الدافئ / البدء البارد في اللعبة.

البايتات الحادية والأربعون المحفوظة

عندما يكتشف SMB1 بدءًا باردًا (تشغيل لأول مرة أو إيقاف/تشغيل)، يمسح جميع ذاكرة RAM. لكن عندما يكتشف بدءًا دافئًا (إعادة تشغيل الزر، دون قطع الطاقة)، يحتفظ بمنطقة ذاكرة تحتوي على 41 بايت:

; 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]

هذه البايتات الحادية والأربعون تخدم وظيفة واحدة فقط: السماح للاعب بالاستمرار في نفس العالم بعد خسارة اللعبة. إذا مت في 6-3، تكتب اللعبة العالم 6 في بايت البداية، وعند شاشة العنوان، إذا ضغطت A + Start، تبدأ من جديد في 6-1.

البايتات الحادية والأربعون المحفوظة في ذاكرة RAM أثناء البدء الدافئ -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, والبايت السحري $A5

الفحص المزدوج للبدء الدافئ

البدء البارد مقابل البدء الدافئ -- مخطط اكتشاف إعادة التشغيل

عندما يعمل 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

فحص البايت $A5 وأرقام أعلى نقاط -- جوهر البدء الدافئ

لماذا فحص مزدوج؟ لأن البايت $A5 قد يكون موجودًا بالصدفة (لعبة أخرى تترك هذه القيمة، أو حالة الراحة الافتراضية لشريحة RAM). بالتحقق من أن أرقام أعلى نقاط صالحة (0-9)، نضمن أن البيانات متسقة.

لماذا Tennis هو اللعبة الوحيدة التي تعمل

عندما تدخل SMB1 لأول مرة (بدء بارد)، تقوم اللعبة:

  1. بمسح جميع ذاكرة RAM ← أعلى نقاط = 0، بايت العالم = 0
  2. بكتابة $A5 على العنوان $0787

بعد ذلك، تتحول إلى Tennis دون إيقاف جهاز التحكم. Tennis:

  • لا ينظف ذاكرة RAM عند بدء التشغيل (قليل من ألعاب NES يفعل ذلك)
  • لا يكتب على بايتات أعلى نقاط ← تبقى عند 0 (صالحة)
  • لا يلمس البايت $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. البايت $A5 لا يزال موجودًا (Tennis لم يلمسه)
  2. أرقام أعلى نقاط لا تزال 0 (صالحة)
  3. بايت العالم يساوي الآن 8+ (زاد بسبب خطوات Tennis)
  4. SMB1 يكتشف بدءًا دافئًا ← يحتفظ ببايت العالم المتلف
  5. ضغط A + Start ← World 9-1, World A-1, World 36-1، إلخ

لماذا يجب تشغيل Mario قبل Tennis

تفاصيل دقيقة: يجب تشغيل SMB1 أولاً، ثم Tennis، ثم SMB1 مرة أخرى. إذا بدأت مباشرة بـ Tennis، لن يُكتب البايت $A5 أبدًا (Tennis لا يكتب $A5)، لذلك سيفشل اكتشاف البدء الدافئ وتمسح ذاكرة RAM.

عداد خطوات Tennis: كل خطوة تزيد بايت العالم

الوصول إلى عوالم الخلل عبر NES Tennis -- الفيديو الذي يشرح تبادل الكارترidge

كيفخزّن SMB1 مستوياتها في 40KB

اجبرت R&D4 في نينتندو على حل مشكلة تبدو بسيطة: تمثيل مستويات تتحرك أفقيًا باستخدام بلاطات، أعداء، عناصر، وكل ذلك في ميزانية ROM ضيقة للغاية.

الحل هو فصل البيانات إلى طبقتين مستقلتين تمامًا:

تخطيط البلاطات (خريطة المستوى)

يُعرّف كل مستوى بمؤشر يشير إلى بنية بلاطات مضغوطة في ROM. الضغط بدائي لكنه عبقري: بايت "تحكم" متبوع بـ 1-3 بايتات بيانات.

يستخدم تنسيق البلاطات نظام تسلسلات (يشبه 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 عمودًا من البلاطات (13×16 = 208 بلاطة مرئية). لكن التنسيق المضغوط يسمح بالانخفاض أكثر بكثير -- على سبيل المثال، السماء والأعمدة الفارغة لا تشغل تقريبًا أي مكان.

حلقة العرض في 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

تخطيط السبرايتات (الأعداء والأشياء)

بشكل متزامن، الأعداء والأشياء (بلوكات ؟، أنابيب، goombas، koopas) مخزنة في بنية منفصلة تمامًا. كل ظهور يُعرّف بـ 2 بايت:

; 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 صفحات سبرايتات مختلفة (أو بدقة، 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 معينًا

الترتيب المختلف لمؤشرات البلاطات والسبرايتات -- سبب المستويات الكائن فرانكنشتاين

الـ 34 مستوى فريدًا ونظام المعرّف 7 بت

شريحة RAM في NES (MB8416A) -- هي التي تحافظ البيانات عند تبادل الكارترidgeات

ليس لدى SMB1 32 مستوى، بل 34 مستوى فريدًا. كثير من المستويات مكررة (5-3 = 1-3 لكن مع Bullet Bills) وulozhyz بعلم "الوضع الصعب". المستويات الفريدة الحقيقية:

  • الماء (النوع 0): 3 مستويات (2-2، 7-2، منطقة مكافأة 5-2/6-2)
  • العالم العلوي (النوع 1): 22 مستوى (بما في ذلك غرفتي مكافأة السحاب)
  • تحت الأرض (النوع 2): 3 مستويات (بما في ذلك غرف المكافأة تحت الأرض)
  • القلعة (النوع 3): 6 مستويات
  • + 1 غرفة مشهد (قبل المستويات تحت الأرض/الماء)
  • + 1 منطقة تéléchargement من 4-2

لكل مستوى معرّف على 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 معرّفًا ممكنًا ($00-$7F)، فقط 34 معرّفًا معينًا لمستويات حقيقية. المعرّفات غير المستخدمة تشير إلى أي شيء.

جداول المؤشرات: قائمتان، ترتيبان

مؤشرات البلاطات والسبرايتات ليست مخزنة بنفس الترتيب. يستخدم الكود قائمتين منفصلتين من 16 بت (بايت عالي/بايت منخفض في جدولين مختلفين):

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)

لماذا ترتيبان مختلفان؟ لا يوجد سبب فني -- من المحتمل أنه بهذه الطريقة تم تنظيم البيانات أثناء التطوير. لكن هذا يخلق نتيجة مثيرة: عندما يكون معرّف مستوى غير صالح، تحمل مؤشرات البلاطات والسبرايتات مستويات مختلفة، مما يخلق مستويات فرانكنشتاين.

للتنقل بين هاتين القائمتين، تستخدم اللعبة جداول إزاحة صغيرة (مثل جدول محتويات):

; 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 (Mعرّف $23، العالم العلوي رقم 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 ✓

الآن، ماذا يحدث مع معرّف غير صالح مثل $43 (تحت الأرض رقم 3، الذي لاExists)؟

; 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 -- مستوى فرانكنشتاين: بلاطات 1-4 + سبرايتات ماء 5-2

استكشاف مؤشرات مستوى الخلل -- جداول الإزاحة المفسرة

جدول فهرس العالم -- عندما يسبب تجاوز World 9 مستوى خلل

جدول فهرس العالم: لماذا يتجاوز World 9

هناك جدول ROM من 8 بايتات يعطي فهرس أول مستوى لكل عالم (1-8). وبعده مباشرة، جدول الـ 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، تقرأ اللعبة البايت التاسع من WorldIndexTable... الذي لاExists. يتجاوزه ببايت واحد إلى LevelIDTable، يقرأ القيمة $25، ثم يستخدم $25 كفهرس في LevelIDTable (الإدخال 37) -- مما يتخطى مرتين في 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)، يتجاوز التخطي أبعد ويرسخ على Level ID $01، وهو مستوى المشهد الذي يسبق 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

لماذا توجد عوالم الخلل

تiliki اللعبة 32 مستوى "شرعيًا" (8 عوالم × 4 مستويات). لكن جدول المؤشرات يحتوي على 128 إدخالًا لكل نوع منطقة. الإدخالات التي تتجاوز المستوى 32 تحتوي على ما يوجد في ROM على هذه العناوين -- أحيانًا مستوى آخر، وأحيانًا بيانات صوتية، وأحيانًا RAM، وأحيانًا أي شيء.

Level ID $01 Water (Minus World) -- مؤشر البلاطات $AE45، مؤشر السبرايتات $A171

Level ID $01 + AreaType 0 = Minus World

أشهر عوالم الخلل. Level ID $01 في AreaType 0 (ماء) يشير إلى:

  • مؤشر البلاطات: $AE45 ← المنطقة تحت الماء من 2-2/7-2
  • مؤشر السبرايتات: $A171 ← سبرايتات 2-2/7-2

النتيجة: مستوى ماء يشبه 2-2، لكنه يتكرر إلى الأبد لأن عمود العلم (flagpole) لا Exists. لا نهاية للمستوى، لا مخرج.

هذا هو المستوى 36-1 (أو 36-1 في العالم $-1).

فحص البدء الدافئ في 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

عندما يشير مؤشر البلاطات أو مؤشر السبرايتات إلى عنوان في RAM ($00-$7F) بدلاً من ROM، تحاول اللعبة تفسير التغييرات المستمرة في RAM كبلاطات:

; 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 يتحرك، أو المؤقت يدور، إلخ)، تتغير "سبرايتات" المستوى أيضًا. لهذا السبب بعض عوالم الخلل بها أعداء يتوهجون ويتحولون باستمرار.

Level ID $03 Water -- مؤشر السبرايتات $009D يشير إلى RAM، مستوى غير قابل للعب

Level ID $36: المستوى الفارغ (العالم العلوي)

Level ID $36 في العالم العلوي:

  • مؤشر البلاطات: $AC35 (1-2)
  • مؤشر السبرايتات: $A0D8 (1-2)

النتيجة: لا شيء. تحمل اللعبة المستوى لكنه مulozhenn "بدون مستوى" في كتالوج RGMechEx. البلاطات قد تكون صالحة لكن السبرايتات تشير إلى مكان ينتج مستوى فارغًا أو غير قابل للتشغيل.

Level ID $1D (Castle): بطل الأعطال

Level ID $1D في القلعة:

  • مؤشر البلاطات: $A210 (4-4)
  • مؤشر السبرايتات: $7EA0 (RAM !)

مؤشر السبرايتات في RAM = سبرايتات غير معرفة. تحاول اللعبة عرض Spiny ball أو Bullet Bill blaster في صف البلاطات الأول. يتعطل فورًا.

; 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 عالم خلل الموثقة

كتب RGMechEx نصًا يولّد خرائط جميع المستويات، لأنواع المنطقة الأربعة، والـ 128 معرّفًا لكل منها.

عداد العالم على 8 بت (0-255). العوالم 1-8 شرعية. تبقى 248 عالم خلل محتملًا. كل عالم خلل يتوافق مع أول مستوى لذلك العالم، ويتم حساب معرّف مستوى عن طريق آلية تجاوز WorldIndexTable.

جدول عوالم الخلل -- 248 عالمًا متلفًا، 68 مستوى أول يمكن الوصول إليها

من بين الـ 128 معرّفًا ممكنًا، فقط 68 هي "مستوى أول" لعالم (يمكن الوصول إليها عبر رقم عالم الخلل). الـ 60 الأخرى هي مستويات 2+ أو غير قابلة للوصول.

النوع معرّفات قابلة للعب فريدة معرّفات تسبب تعطل معرّفات فارغة
الماء (0) ~20 ~60 ~48
العالم العلوي (1) ~30 ~55 ~43
تحت الأرض (2) ~15 ~65 ~48
القلعة (3) ~25 ~58 ~45

كثير من المعرّفات تؤدي إلى نفس المستوى بسبب المؤشرات التي تقع على نفس العناوين ROM. Level ID $28 (العالم العلوي) على سبيل المثال -- مؤشر البلاطات $A7CD (2-1) -- يظهر في 38 عالم خلل مختلفًا، لأنه مؤشر السبرايتات $9F51 يشير إلى منطقة في ROM تستخدم كحشو/بيانات صوتية يعيد استخدامها كثير من المعرّفات.

خريطة المستوى ID $28 (العالم العلوي) -- بلاطات 2-1 مع سبرايتات عادية، 38 عالم خلل

شرح آليات Super Mario Bros. الخلل -- الفيديو الثالث

الـ 6 مستويات خلل فريدة حقًا

من بين معرّفات الخلل الـ 19 القابلة للوصول، فقط 6 لا تتعرض للتعطل فورًا عند التحميل:

العالم Level ID الوصف
E-1 (224) $50 بلوك ؟ واحد فوق هاوية. يموت Mario فورًا.
W $57 Mario يظهر محتجزًا، غير قادر على الحركة.
42 (133) $50 نفق سحاب يحبس Mario إذا ذهب بعيدًا بما يكفي.
62 (131, 240) $4D قلعة جليدية: Mario يظهر في الأعلى، لا يمكنه السقوط ← محتجز.
127 $4B نفق تحت الأرض، لكنه يتعرض للتعطل إذا ذهب بعيدًا.
137 $4B ي.active التمرير التلقائي لمشاهد cutscenes. Mario يلتقي ببلوك brick واحد يحجبه إلى الأبد.

Level ID $50 (نفق السحاب) -- عالم الخلل 42-1 و E-1 Level ID $4D (قلعة) -- World 62-1، Mario محتجز عند الظهور Level ID $4B (نفق) -- World 127-1، يتعرض للتعطل إذا ذهب بعيدًا

ستة عوالم خلل من أصل 248 تنتج شيئًا جديدًا حقًا. الباقي هي مستويات عادية مع نوع المنطقة الخاطئ، أو شاشات سوداء.

تنسيق المستويات بالتفصيل

نبحث في التنسيق الدقيق لبيانات المستوى، لفهم لماذا تثبت مستويات الخلل (أو لا تثبت).

رأس المستوى: 2 بايت، 6 خصائص

يبدأ كل مستوى برأس من 2 بايت يتحكم في 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)

يتحكم نوع التعديل في الاختلافات البصرية: الأمواج في أعلى مستويات الماء، خلفية الطوب في 8-3، لوحة الليل في 4-3، الثلج في 6-2، إلخ.

أشياء البلاطات: 2 بايت، علامة الشاشة التالية، طابور 3 أماكن

بعد الرأس تأتي قائمة أشياء البلاطات، كل شيء يساوي 2 بايت. البايت $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

عندما يتم تعيين علامة "الشاشة التالية"، يتم زيادة عمود العمل الحالي بـ 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 أماكن. نتيجة مباشرة: لا يمكن أن يكون هناك أكثر من 3 أشياء تبدأ في نفس العمود. إذا كان الطابور ممتلئًا، يتم تجاهل الشيء الرابع ولن يُحمّل أبدًا.

لهذا السبب تتجنب المستويات المصممة جيدًا تكدس الكثير من الأشياء. مثال في 1-2: العمود الذي يحتوي على بلوك 1up في السقف + الطوب بجانبه مقسمان إلى شيئين منفصلين للالتزام بحد 3.

Y مميز: 12، 13، 14، 15

عندما Y=12، ليس للشيء موقع Y (إنه مبرمج حسب النوع):

; 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 من البايت 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 بت الأقل وزنًا تشفّر تخطي شاشة (انتقل مباشرة إلى شاشة N، دون المرور بعلامة الشاشة التالية واحدة تلو الأخرى).

عندما Y=14: نفس المبدأ مع bit6=1 لتغيير نوع التعديل، bit6=0 لتغيير الخلفية + نمط الأرضية.

أنماط الأرضية: 16 نمطًا أرضيًا

أرضية المستويات ليست مصنوعة من أشياء فردية. يستخدم SMB1 أنماط أرضية، نمط خلفية ينطبق على جميع الأعمدة حتى التغيير التالي:

; 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)

لهذا السبب الثقوب هي أشياء: فهي تتجاوز نمط الأرضية على عمود محدد، دون الحاجة إلى تغيير النمط لبقية كل شيء.

حد 256 بايت والتكرار

جميع بيانات البلاطات للمستوى تندرج في 256 بايت كحد أقصى. السجل Y في 6502 يُستخدم كفهرس، وهو 8 بت. إذا وصلت اللعبة إلى نهاية البيانات دون العثور على البايت $FD، تعود إلى البداية وتكرر الـ 256 بايت إلى الأبد:

; 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.

بعض مستويات الخلل تستغل هذا التكرار لتوليد مستويات تدوم "إلى الأبد".

نظام السبرايتات: 2 بايت + انتقالات الأنابيب

تتبع السبرايتات تنسيقًا مشابهًا، لكن بدون رأس وبعض الاختلافات الجوهرية. البايت $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

البت الأقل وزنًا في البايت 1 هو علم المستوى الصعب: إذا تم تعيينه بـ 1، لا يظهر السبرايت إلا في المستويات ≥ 5-3. هكذا يتم إنشاء مستويات "الوضع الصعب".

الموقع Y 15 = تخطي شاشة (مطابق للبلاطات). الموقع Y 14 = انتقال أنبوب (3 بايت):

; 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.

السبرايتات ليس لها نظام طابور. الحد الوحيد هو عدم وجود أكثر من 4 سبرايتات محملة في نفس الوقت في منطقة الظهور (خارج الشاشة على اليمين فقط). فوق ذلك، يتم تجاهل السبرايتات.

كيفية الوصول إلى عوالم الخلل

هناك طرقتان رئيستان.

الطريقة الكلاسيكية: تسلق الجدار (wall clip)

يسمح تسلق الجدار بالخروج من المستوى العادي والمشي إلى منطقة التحويل المخفية. بالتعامل مع عداد العالم عبر RAM، يمكنك تحميل أي Level ID.

التقنية:

  1. World 1-2: اذهب إلى الأنبوب الخفي النهائي
  2. افعل تسلق الجدار على الجدار الأيمن
  3. امشي في الفراغ حتى منطقة التحويل
  4. تفسر اللعبة القيم كعوالم

لكن هذه الطريقة لا تمنح الوصول إلا إلى جزء صغير من عوالم الخلل.

الطريقة المتطرفة: تبادل كارترidge NES Tennis

انظر قسم "البدء الدافئ" أعلاه للتفاصيل الكاملة. باختصار: عداد خطوات Tennis يكتب على نفس بايت RAM الذي يحتوي على عالم البداية في SMB1، واكتشاف البدء الدافئ يحافظ على هذه القيمة.

زاوية المخترعين: الكود للاستكشاف الكامل

إذا أردت استكشاف جميع الخلل بنفسك في محاكي، يمكنك تثبيت 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. كل إدخال يعرض مؤشر البلاطات، مؤشر السبرايتات، وخريطة بصرية للمستوى.

المستويات الأكثر غرابة

Level ID $1F (Water): 15 عالم خلل في واحد

مؤشر البلاطات $A302 (3-4) المُجمّع مع مؤشر السبرايتات $02A0 ينتج 15 عالم خلل مختلفًا (D-1, J-1, Y-1, Z-1, 55-1, 73-1...). التفسير: مؤشر السبرايتات يشير إلى منطقة في ROM تحتوي على بيانات قريبة بما يكفي من السبرايتاتصالحة لإنتاج نتائج قابلة للعب، لكن مزيج بلاطات القلعة 3-4 مع سبرايتات العالم العلوي يخلق عرضًا عبثيًا.

Level ID $28 (Overworld): 38 عالم خلل = ريكورد

الريكورد المطلق. 38 إدخال عالم خلل تشير إلى نفس المستوى (بلاطات 2-1 + سبرايتات $9F51). لماذا؟ لأن مؤشر السبرايتات $9F51 يقع في منطقة في ROM تستخدم كحشو/بيانات صوتية يعيد استخدامها كثير من المعرّفات.

Level ID $49 (Underground): مستوى FDS

مؤشر البلاطات $76AE + مؤشر السبرايتات $1C9D. مؤشر البلاطات يشير إلى منطقة في ROM المحجوزة لإصدار Famicom Disk System. النتيجة: مستوى به بلاطات لاExists في الكارترidge القياسي. هذا هو المستوى الذي يجعل Levels 52-1 و 196-1 تظهر.

Level ID $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)

الفرق بين مستوى "مكافأة" يمكن الوصول إليه بشكل طبيعي وعالم خلل هو أن مناطق التحويل تتحقق من العالم الحالي:

; 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

عوالم الخلل بأرقام > 8 أو 0 لا يمكن الوصول إليها عبر أنابيب عادية. تحتاج تسلق الجدار أو تبادل الكارترidge.

لماذا تتعرض بعض المستويات للتعطل: جداول القفز

عندما تحمل اللعبة شيئًا بلاطات، تستخدم نوعه كفهرس في جدول قفز:

; 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

جداول القفز: لماذا يتسبب نوع شيء غير صالح في تعطل اللعبة

إذا كان للشيء نوع غير صالح (≥12)، تقفز اللعبة إلى مؤشر غير موجود في هذا الجدول. 4 نتائج ممكنة:

  1. مؤشر صالح ← يُحمّل الشيء بشكل طبيعي
  2. مؤشر إلى جدول قفز آخر (تراكب) ← يظهر شيء مختلف. مثال: النوع 12 يشير إلى جدول Y=13، مما ينتج L-pipe.
  3. مؤشر إلى كود قابل للتنفيذ ← تنفيذ كود عشوائي (تعطل محتمل)
  4. بديل صريح (NOP) ← لا يفعل الشيء شيئًا (بعض السبرايتات هكذا، تنتج أعداء يطيران في مكانهم دون الحركة)

Level ID $58 الخلل: مؤشر السبرايتات يشير إلى عنوان غير صالح، تتعرض اللعبة للتعطل

Level ID $50 الخلل: نفق السحاب، مستوى ولّد من بيانات متلفة

Level ID $58 الخلل (النفق الذي يتعرض للتعطل): مؤشر السبرايتات يشير إلى منطقة ذاكرة لاExists في NES بدون مخطط ROM. تحاول اللعبة تحميل نفس Koopa 5 مرات في كل إطار عند الموقع (0، 0)، مما يشبع PPU ويسبب تجمد.

; 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

مفارقة أنبوب التحويل

تذكر فحص target_world BETWEEN 1 AND 8. حتى لو وجدت أنبوبًا في عالم خلل، تتحقق اللعبة من أن العالم المقصد بين 1 و 8. عوالم الخلل بها أرقام > 8 (36-1، 255-1...)، لذلك يفشل التحويل.

لهذا السبب أيضًا لا Exists نهاية لـ Minus World: عمود العلم غير موجود في السبرايتات، والأنابيب لا تؤدي إلى أي مكان.

حيلة الـ 5 أشياء في عمود

هناك حالة حد تسمح بتجاوز حد 3 أشياء لكل عمود. عندما يتوقف الطابور (أماكن ممتلئة + الشيء التالي مع علامة الشاشة التالية مفقودة)، "تعالج مسبقًا" اللعبة العمود الحالي في حلقة حتى تجد شيئًا مع علامة الشاشة التالية. أثناء كل معالجة مسبقة:

; 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).

يُسمى هذا "تخطي الطابور" ويستخدمه بعض مخترعي الروم لإنشاء مستويات أكثر كثافة مما يسمح به التنسيق عادةً.

الفروقات بين الإصدارات

Famicom Disk System

إصدار FDS من SMB1 له خريطة ذاكرة مختلفة. جميع مؤشرات المستويات مزاحة، لكن البيانات متسقة. ما يتغير: فهارس عوالم الخلل مختلفة تمامًا:

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. ترتيب مطابق للبلاطات/السبرايتات: لا Exists مستويات فرانكنشتاين (البلاطات والسبرايتات تحمل نفس المستوى حتى مع معرّف غير صالح)
  2. جدول مؤشرات واحد من 16 بت بدلاً من جدولين منفصلين عالي/منخفض
  3. 4 ملفات قرص: تم تقسيم ROM لـ FDS:
    • الملف 1: العوالم 1-4
    • الملف 2: العوالم 5-8
    • الملف 3: World 9 + محرك الصوت
    • الملف 4: العوالم A-D (جدول مؤشرات مختلف تمامًا)
  4. نفس Level ID = 4 مستويات ممكنة حسب الملف المحمل
  5. لا Exists خلل Tennis: خيار الاستمرار (الاستمرار في نفس العالم بعد الخسارة) يجعل البدء الدافئ غير ضروري، واللعبة تعيد التشغيل فورًا إذا World > 9
  6. أشياء جديدة: فطر السام، بلوك غير مرئي، بلوك زهرة النار غير المرئي، أنابيب مقلوبة، رياح -- لكنها مدرجة في منتصف القوائم الموجودة → عدم توافق عكسي مع SMB1
  7. نباتات Piranha حمراء دائمًا بعد World 4، منصات قفز خضراء فقط في العوالم 2/B/3/C/7

Super Mario All-Stars (SNES)

منقل مباشر مع نفس الروتينات 6502 (يشغل SNES كود NES في وضع متوافق):

  • منطقة التحويل مصححة: لا Exists Minus World (دخول الأنبوب الأيسر قبل النص يؤدي إلى العالم الصحيح)
  • تجمد: معظم مستويات الخلل تتعرض للتعطل (باستثناء ID $6A و 9-1)
  • أشياء القلعة المضافة: أكثر فردية
  • لكن: التحويل الخاطئ 4-2 لا يزال يعمل (لم يتم تثبيته!)

التحويل الخاطئ 4-2: خطأ في وضع الأشياء

في 4-2، هناك شيئان من انتقال الأنبوب: الكرمة (منطقة التحويل) والأنبوب (غرفة عملات). الشيء الأول من الانتقال (الكرمة) يوضع قبل ظهور الكرمة على الشاشة. الثاني (الأنبوب) يوضع متأخرًا في المستوى.

; 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.

مستويات الحلقة

كيف تعمل الحلقات (8-4، 7-4)؟ المستوى يحتوي على نقاط تفتيش مع أرقام شاشة ومواقع Y مبرمجة:

; 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 أبدًا. كل التباين بين المستويات يأتي من البيانات (المؤشرات، الأشياء، السبرايتات، أنماط الأرضية)، لا من الكود.

توجد عوالم الخلل الـ 256 لأن جداول المؤشرات مصممة لـ 128 إدخالًا × 4 أنواع، واللعبة لا تتحقق أبدًا من القيم التي تقرأها. عندما يقع مؤشر في RAM، تفسر اللعبة سجلات Mario كبلاطات. عندما يقع مؤشر في البيانات الصوتية، تلعب اللعبة موسيقى على شكل تصميم مستوى. وعندما تتخطى جداول القفز، تنفيذ اللعبة أي شيء حتى التعطل.

شرح آليات Super Mario Bros. الإضافية -- الفيديو الرابع

ما يمكن أن نتعلمه من كل هذا

  1. فصل البلاطات/السبرايتات: استقلالية تامة للطبقتين، مع ترتيبات تخزين مختلفة تخلق مستويات فرانكنشتاين فريدة
  2. ضغط RLE + نظام الأشياء: المستويات ليست صور نقطية بل قوائم أشياء مع أنماط أرضية للأرضية
  3. طابور 3 أماكن: حد صلب للعتاد (وتصميم المستوى)
  4. لا تحقق: اللعبة تثق بالمؤشرات وجداول القفز، مما ينتج إما خلل قابل للعب أو تعطل
  5. 256 بايت كحد أقصى: حد سجل Y في 6502، مما يجعل البيانات تتكرر إذا ذهبت بعيدًا
  6. بدء دافئ / بارد: نظام "استمرار" فتح الباب لتبادل كارترidge Tennis ← Mario

الأجمل من ذلك كله: كل هذا هو كود 6502 يتسع في 40KB. لا طبقة تجريد، لا تحقق من الوصول للذاكرة، لا مدير استثناءات. إذا كان المؤشر تالفًا، تتعرض اللعبة للتعطل. والتعطلات، نسميها عوالم الخلل.

الـ 3 أشياء للتذكر

  1. عوالم الخلل هي مؤشرات تسقط في المكان الخطأ -- تiliki اللعبة 128 معرّفًا × 4 أنواع منطقة، لكن فقط 34 مستوى فريدًا. عندما يتلف رقم العالم (بوسيلة Tennis أو تسلق الجدار)، تحمل اللعبة مؤشرًا مصممًا لمستوى آخر، والـ 512 تركيبة ممكنة تنتج نتائج غير متوقعة.

  2. Minus World هو خطأ تحويل مُجمّع مع تلف -- الأنبوب الأيسر في 1-2، إذا تم تفعيله قبل ظهور النص، يحمّل World 36 (0x24). هذا العالم يشير إلى Level ID $01 (ماء 2-2)، مستوى بدون عمود العلم. وبما أن لا Exists انتقال أنبوب لـ World 36، يتكرر المستوى إلى الأبد. غياب التحقق يخلق الأيقونة.

  3. Tennis ← Mario، 15 عامًا قبل OoT ← Paper Mario -- ذاكرة RAM في NES تنجو من تبادل كارترidge بفضل المكثفات ونظام البدء الدافئ / البارد في SMB1. عداد خطوات Tennis (الذي يزيد بايت RAM أثناء تشغيل صوت الخطوات) يقع بالضبط على عنوان رقم العالم. يجب أن تبقى أرقام أعلى نقاط عند 0، وأن البايت $A5 سليمًا، وأن تكتشف اللعبة بدءًا دافئًا -- سلسلة ظروف مثالية لم تعمل إلا مع Tennis.

مقاطع فيديو Retro Game Mechanics Explained الأصلية هي عمل نشيط للغاية -- مستوى التفاصيل في تفكيك 6502، خرائط جميع المستويات المولّدة تلقائيًا، شروحات تبادل الكارترidge والبدء الدافئ. إذا لم تشاهد السلسلة، شاهدها، فهي قصيرة وكل دقيقة كثيفة.

كود الخرائط متاح على rgmechex.com، والتفكيك الكامل لـ SMB1 مفتوح المصدر في كثير من المستودعات. منذ 40 عامًا، كتب مبرمجون يابانيون نظام المستوى هذا في 6502 بدون اختبار وحدة واحد أو متتبع أخطاء واحد، ونواصل التعلم من فتح كودهم اليوم.

Super Mario Bros.: Định dạng level, con trỏ và 256 glitch world

Cách 128 level × 4 loại khu vực vừa trong 40KB ROM, tại sao Minus World tồn tại, và cách một trận Tennis NES có thể tải glitch world.

Giới thiệu

Super Mario Bros., đó là 40 kilobyte ROM. Tám thế giới, 32 level, kẻ thù, nhạc, power-ups, tất cả đều chứa trong đó.

Nhưng nếu bạn mở giả lập và thay đổi đúng byte, bạn có thể tải level 36-1. Hoặc 255-1. Hoặc rơi vào một thế giới nơi mọi thứ đều được tạo từ sprite của Bowser và ống nước dẫn đến hư không.

Những glitch world này tồn tại vì lý do đơn giản: hệ thống lưu trữ level của SMB1 là một kỳ quan tối ưu hóa 8-bit, và khi bạn buộc game đọc ở nơi không nên đọc, kết quả sẽ rất hấp dẫn.

Retro Game Mechanics Explained đã thực hiện một loạt 4 video về chủ đề này -- chúng ta sẽ tổng hợp chúng thành một lần đi sâu vào mã 6502 của game bán chạy nhất thời đại.

GLITCH OBJECTS -- tiêu đề loạt phim RGMechEx về các cơ chế ẩn của SMB1

World 9-1 -- màn hình tiêu đề của glitch world đầu tiên có thể truy cập qua việc hoán đổi cartridge Tennis

Warm start: Tại sao RAM của Tennis tồn tại trong SMB1

Trước khi nói về lưu trữ level, cần hiểu cách SMB1 khởi động. Bởi vì lỗi cart swap NES Tennis dựa hoàn toàn vào hệ thống phát hiện warm start / cold start của game.

41 byte được bảo toàn

Khi SMB1 phát hiện cold start (lần bật nguồn đầu tiên hoặc tắt/mở nguồn), nó xóa toàn bộ RAM. Nhưng khi phát hiện warm start (nhấn nút reset, không tắt nguồn), nó bảo toàn một vùng nhớ 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 này phục vụ một chức năng duy nhất: cho phép người chơi tiếp tục ở cùng thế giới sau khi game over. Nếu bạn chết ở 6-3, game ghi thế giới 6 vào byte bắt đầu, và ở màn hình tiêu đề, nếu giữ A + Start, bạn bắt đầu lại ở 6-1.

41 byte được bảo toàn trong RAM khi warm start -- TOP SCORE, MARIO SCORE, TIMER, WORLD SELECT, CONTINUE WORLD, và byte ma thuật $A5

Kiểm tra kép warm start

Cold start so với warm start -- sơ đồ phát hiện reset

Khi SMB1 khởi động, nó không kiểm tra một tiêu chí mà là hai:

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

Kiểm tra byte $A5 và các chữ số top score -- cốt lõi của warm start

Tại sao kiểm tra kép? Bởi vì byte $A5 có thể xuất hiện ngẫu nhiên (một game khác để lại giá trị này, hoặc trạng thái mặc định của chip RAM). Bằng cách kiểm tra các chữ số top score hợp lệ (0-9), chúng ta đảm bảo dữ liệu nhất quán.

Tại sao Tennis là game duy nhất hoạt động

Khi cắm SMB1 lần đầu (cold start), game:

  1. Xóa toàn bộ RAM → top score = 0, world byte = 0
  2. Ghi $A5 tại địa chỉ $0787

Sau đó, hoán đổi sang Tennis mà không tắt máy. Tennis:

  • Không xóa RAM khi khởi động (ít game NES làm vậy)
  • Không ghi vào các byte top score → chúng vẫn là 0 (hợp lệ)
  • Không chạm vào byte $A5 → nó vẫn còn
  • Sử dụng địa chỉ $075F cho bộ đếm bước chân của người chơi
; 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).

Khi cắm lại SMB1:

  1. Byte $A5 vẫn còn (Tennis không chạm vào nó)
  2. Các chữ số top score vẫn là 0 (hợp lệ)
  3. World byte giờ có giá trị 8+ (được tăng bởi các bước chân Tennis)
  4. SMB1 phát hiện warm start → bảo toàn world byte bị hỏng
  5. Giữ A + Start → world 9-1, world A-1, world 36-1, v.v.

Tại sao phải khởi động Mario trước Tennis

Một điểm tinh tế: phải khởi động SMB1 trước, rồi Tennis, rồi SMB1 lại. Nếu bắt đầu trực tiếp với Tennis, byte $A5 sẽ không bao giờ được ghi (Tennis không ghi $A5), nên việc phát hiện warm start sẽ thất bại và RAM sẽ bị xóa.

Bộ đếm bước chân Tennis: mỗi footstep tăng world byte

Truy cập Glitch Worlds qua NES Tennis -- video giải thích cart swap

Cách SMB1 lưu trữ các level trong 40KB

Nintendo R&D4 phải giải quyết một vấn đề đơn giản trên bề mặt: biểu diễn các level cuộn ngang với tile, kẻ thù, item, tất cả trong ngân sách ROM cực kỳ chặt chẽ.

Giải pháp là sự tách biệt thành hai lớp dữ liệu hoàn toàn độc lập:

Tile layout (bản đồ level)

Mỗi level được định nghĩa bằng một con trỏ tới cấu trúc nén tile trong ROM. Nén đơn giản nhưng thông minh: byte "điều khiển" theo sau bởi 1-3 byte dữ liệu.

Định dạng tile sử dụng hệ thống runs (giống 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

Mỗi level chứa 13 hàng × 16 cột tile (13×16 = 208 tile hiển thị). Nhưng định dạng nén cho phép giảm đáng kể -- ví dụ, bầu trời và cột trống gần như không chiếm dung lượng.

Vòng lặp render bằng 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 (kẻ thù và vật thể)

Song song đó, kẻ thù và vật thể (khối ?, ống nước, goomba, koopa) được lưu trữ trong cấu trúc hoàn toàn riêng biệt. Mỗi spawn được định nghĩa bởi 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

Mỗi level có thể tham chiếu tối đa 5 trang sprite khác nhau (hay 5 "màn hình" 16 cột), nhưng trong thực tế hầu hết level chỉ dùng 2-3.

Bảng con trỏ

Thiết kế thiên tài nằm ở bảng con trỏ. Mỗi level được lưu trữ dưới dạng cặp địa chỉ 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 mục mỗi bảng. 4 loại khu vực. 512 tổ hợp có thể, nhưng chỉ một phần nhỏ được sử dụng bởi game chính thức. Phần còn lại là RAM chưa khởi tạo hoặc dữ liệu được giải thích sai thành con trỏ.

Khi game tải level, nó làm như sau:

; 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

Không có kiểm tra. Không kiểm tra con trỏ có hợp lệ không. Game đọc địa chỉ trong bảng và nén dữ liệu tại địa chỉ đó, xong.

Level ID $06 (Water) -- 9-1, phiên bản dưới nước của 6-2

Bảng Level ID: 128 mục có thể, 34 được gán

Thứ tự khác nhau của con trỏ tile và sprite -- nguyên nhân tạo ra Frankenstein level

34 level độc đáo và hệ thống ID 7-bit

Chip RAM của NES (MB8416A) -- chip này giữ dữ liệu khi hoán đổi cartridge

SMB1 không có 32 level, mà là 34 level độc đáo. Nhiều level là bản sao (5-3 = 1-3 nhưng có Bullet Bill) được đánh dấu bằng cờ "hard mode". Các level độc đáo thực sự:

  • Nước (Loại 0): 3 level (2-2, 7-2, vùng bonus 5-2/6-2)
  • Overworld (Loại 1): 22 level (bao gồm 2 phòng mây bonus)
  • Underground (Loại 2): 3 level (bao gồm các phòng bonus ngầm)
  • Castle (Loại 3): 6 level
  • + 1 phòng cutscene (trước các level ngầm/dưới nước)
  • + 1 warp zone của 4-2

Mỗi level có một ID trên 7 bit. 5 bit thấp = số trong nhóm con, 2 bit cao = loại khu vực:

; 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 ID có thể ($00-$7F), chỉ 34 được gán cho level thật. Các ID chưa sử dụng trỏ đến bất kỳ thứ gì.

Bảng con trỏ: hai danh sách, hai thứ tự

Con trỏ tile và sprite không được lưu trữ cùng thứ tự. Code sử dụng hai danh sách 16-bit riêng biệt (byte cao / byte thấp trong hai bảng khác nhau):

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)

Tại sao thứ tự khác nhau? Không có lý do kỹ thuật -- có thể dữ liệu được sắp xếp như vậy trong quá trình phát triển. Nhưng nó tạo ra hậu quả hấp dẫn: khi ID level không hợp lệ, con trỏ tile và sprite tải các level khác nhau, tạo ra Frankenstein level.

Để di chuyển giữa hai danh sách này, game sử dụng các bảng offset nhỏ (như mục lục):

; 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

Để tải level 6-2 (ID $23, Overworld số 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 ✓

Bây giờ, điều gì xảy ra với ID không hợp lệ như $43 (Underground số 3, không tồn tại)?

; 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: tile 1-4 + sprite nước 5-2

Exploring Glitch Level Pointers -- bảng offset được giải thích

World index table -- khi overflow của world 9 tạo glitch level

World index table: Tại sao world 9 bị overflow

Có một bảng ROM 8 byte cho index của level đầu tiên mỗi thế giới (1-8). Và ngay sau đó là bảng 36 Level ID của tất cả level theo thứ tự chơi.

; 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

Khi 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) -- điều này tràn thêm 2 byte vào SpriteOffsetTable, và đọc giá trị 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 !

Với world G (16), overflow đi xa hơn nữa và rơi vào Level ID $01, là level cutscene đứng trước 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

Tại sao glitch world tồn tại

Game có 32 level "hợp pháp" (8 thế giới × 4 level). Nhưng bảng con trỏ có 128 mục mỗi loại khu vực. Các mục vượt quá level 32 chứa dữ liệu ROM tại các địa chỉ đó -- đôi khi là level khác, đôi khi là dữ liệu âm thanh, đôi khi là RAM, đôi khi là bất kỳ thứ 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ỏ đến:

  • 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ột level nước giống 2-2, nhưng lặp vô hạn vì cột cờ không tồn tại. Không có kết thúc level, không có lối ra.

Đó là level 36-1 (hay 36-1 trong thế giới $-1).

Warm start check của SMB1 -- chính nó cho phép Minus World tồn tại

; 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.

Các con trỏ trỏ đến RAM

Khi tile pointer hoặc sprite pointer trỏ đến địa chỉ RAM ($00-$7F) thay vì ROM, game cố giải thích các thay đổi liên tục của RAM như tile:

; 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.

Khi trang zero thay đổi (vì Mario di chuyển, timer chạy, v.v.), "sprite" của level cũng thay đổi. Đó là lý do một số glitch world có kẻ thù nhấp nháy và biến đổi liên tục.

Level ID $03 Water -- sprite pointer $009D trỏ đến RAM, level không thể chơi

Level ID $36: Level trống (Overworld)

Level ID $36 ở Overworld:

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

Kết quả: không gì cả. Game tải level nhưng nó được đánh dấu "không có level" trong danh mục của RGMechEx. Tile có thể hợp lệ nhưng sprite trỏ đến nơi tạo ra level trống hoặc không hoạt động.

Level ID $1D (Castle): Vua crash

Level ID $1D ở Castle:

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

Sprite pointer ở RAM = sprite chưa xác định. Game cố hiển thị Spiny ball hoặc Bullet Bill blaster ở hàng tile đầu tiên. Nó crash ngay lập tức.

; 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 world được phân loại

RGMechEx đã viết script tạo bản đồ của tất cả level, cho 4 loại khu vực, mỗi loại 128 ID.

Bộ đếm thế giới 8 bit (0-255). Thế giới 1-8 hợp pháp. Còn lại 248 glitch world tiềm năng. Mỗi glitch world tương ứng với level đầu tiên của thế giới đó, và Level ID của nó được tính bởi cơ chế overflow của WorldIndexTable.

Bảng glitch world -- 248 thế giới bị hỏng, 68 level đầu tiên có thể truy cập

Trong 128 ID có thể, chỉ 68 là "level đầu tiên" của một thế giới (có thể truy cập qua số glitch world). 60 ID còn lại là level 2+ hoặc không thể truy cập.

Loại ID hợp lệ có thể chơi 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 đến cùng level vì con trỏ rơi vào cùng địa chỉ ROM. Ví dụ Level ID $28 (Overworld) -- tile pointer $A7CD (2-1) -- xuất hiện trong 38 glitch world khác nhau, vì sprite pointer $9F51 trỏ đến vùng ROM được dùng làm padding/dữ liệu âm thanh tái sử dụng bởi nhiều ID.

Bản đồ level ID $28 (Overworld) -- tile 2-1 với sprite bình thường, 38 glitch world

Super Mario Bros. Glitch Levels Explained -- video thứ 3

6 glitch level thực sự độc đáo

Trong 19 ID glitch level có thể truy cập, chỉ 6 không crash ngay khi tải:

Thế giới Level ID Mô tả
E-1 (224) $50 Chỉ một ? block phía trên vực thẳm. Mario chết ngay lập tức.
W $57 Mario spawn bị chặn, không thể di chuyển.
42 (133) $50 Đường hầm mây giam Mario nếu đi quá xa.
62 (131, 240) $4D Lâu đài đóng băng: Mario spawn ở trên, không thể rơi → bị chặn.
127 $4B Đường hầm ngầm, nhưng crash nếu đi quá xa.
137 $4B Kích hoạt cuộn tự động của cutscene. Mario gặp một brick block duy nhất chặ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 bị chặn khi spawn Level ID $4B (đường hầm) -- world 127-1, crash nếu đi quá xa

Sáu glitch world trong 248 tạo ra điều gì đó thực sự mới. Phần còn lại là level bình thường với sai loại khu vực, hoặc màn hình đen.

Định dạng level chi tiết

Đi vào định dạng dữ liệu level chính xác, để hiểu tại sao glitch level đứng vững (hay không).

Header level: 2 byte, 6 thuộc tính

Mỗi level bắt đầu bằng header 2 byte kiểm soát 6 thuộc tính:

; 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)

Thuộc tính modifier kiểm soát các biến thể hình ảnh: sóng trên cùng level nước, nền gạch của 8-3, bảng màu đêm của 4-3, tuyết của 6-2, v.v.

Vật thể tile: 2 byte, cờ Next Screen, hàng đợi 3 vị trí

Sau header đến danh sách vật thể tile, mỗi vật thể 2 byte. Byte $FD đánh dấu kết thúc danh sách.

; 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

Khi bit "next screen" được đặt, cột làm việc hiện tại tăng 1. Điều này cho phép đặt vật thể vượt quá 16 cột đầu tiên. Các vật thể phải được liệt kê theo thứ tự (trái sang phải) vì game tải chúng tuần tự:

; 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

Hàng đợi có đúng 3 vị trí. Hậu 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ị bỏ qua và không bao giờ được tải.

Đó là lý do level được thiết kế tốt tránh xếp quá nhiều vật thể. Ví dụ trong 1-2: cột có block 1up trên trần + gạch bên cạnh được chia thành hai vật thể riêng biệt để tuân thủ 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ó được hardcode theo loại):

; 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

Khi Y=13, hai nhóm con. Nếu bit 6 của byte 1 bằng 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

Nếu bit6=0, 5 bit thấp mã hóa một screen skip (nhảy trực tiếp đến màn hình N, không qua cờ next screen từng cái một).

Khi Y=14: nguyên lý tương tự với bit6=1 để thay đổi thuộc tính modifier, bit6=0 để thay đổi nền + floor pattern.

Floor pattern: 16 mẫu sàn

Sàn level không được tạo từ vật thể riêng lẻ. SMB1 sử dụng floor pattern, mẫu nền áp dụng cho tất cả cột cho đến khi thay đổi tiếp theo:

; 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)

Đó là lý do lỗ là vật thể: chúng ghi đè floor pattern ở cột cụ thể, không cần thay đổi pattern cho phần còn lại.

Giới hạn 256 byte và repeat

Tất cả dữ liệu tile của level chứa trong tối đa 256 byte. Register Y của 6502 được dùng làm index, và nó 8 bit. Nếu game đến cuối dữ liệu mà không tìm thấy byte $FD, nó lặp lại từ đầu và lặp 256 byte vô hạn:

; 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.

Một số level exploit exploit repeat này để tạo level kéo dài "vô thời hạn".

Hệ thống sprite: 2 byte + chuyển tiếp pipe

Sprite theo định dạng tương tự, nhưng không có header và có vài khác biệt chính. Byte $FF đánh dấu kết thúc danh sách.

; 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

Bit thấp của byte 1 là hard level flag: nếu đặt bằng 1, sprite chỉ xuất hiện ở level ≥ 5-3. Các level "hard mode" được tạo theo cách này.

Vị trí Y 15 = screen skip (giống tile). Vị trí Y 14 = chuyển tiếp pipe (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.

Sprite không có hệ thống hàng đợi. Giới hạn duy nhất là không thể có hơn 4 sprite tải đồng thời ở vùng spawn (ngay ngoài màn hình bên phải). Nhiều hơn, sprite bị bỏ qua.

Cách truy cập glitch world

Có hai phương pháp chính.

Phương pháp truyền thống: wall clip

Wall clip (đi xuyên tường) cho phép thoát level bình thường và đi đến warp zone ẩn. Bằng cách thao túng bộ đếm thế giới qua RAM, bạn có thể tải bất kỳ Level ID nào.

Kỹ thuật:

  1. World 1-2: đi vào ống ẩn cuối level
  2. Thực hiện wall clip ở tường bên phải
  3. Đi trong khoảng trống đến vùng warp
  4. Game giải thích giá trị như thế giới

Nhưng phương pháp này chỉ truy cập được một phần nhỏ glitch world.

Phương pháp cực đoan: NES Tennis cart swap

Xem phần "Warm start" ở trên để biết chi tiết. Tóm lại: bộ đếm bước chân Tennis ghi vào cùng byte RAM với thế giới bắt đầu của SMB1, và việc phát hiện warm start bảo toàn giá trị đó.

Góc cho người hack: code để khám phá tất cả

Nếu bạn muốn tự khám phá tất cả glitch trong giả lập, bạn có thể patch Level ID trực tiếp:

; 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 đã phát hành danh sách đầy đủ 128 level × 4 loại với bản đồ tự động tạo trên rgmechex.com. Mỗi mục hiển thị tile pointer, sprite pointer, và bản đồ hình ảnh của level.

Các level khó hiểu nhất

Level ID $1F (Water): 15 glitch world trong một

Tile pointer $A302 (3-4) kết hợp với sprite pointer $02A0 tạo 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ỏ đến vùng ROM chứa dữ liệu gần đủ với sprite hợp lệ để tạo kết quả chơi được, nhưng sự kết hợp tile lâu đài 3-4 với sprite overworld tạo ra kết xuất vô lý.

Level ID $28 (Overworld): 38 glitch world = kỷ lục

Kỷ lục tuyệt đối. 38 mục glitch world trỏ đến cùng level (tile 2-1 + sprite $9F51). Tại sao? Bởi vì sprite pointer $9F51 rơi vào vùng ROM được dùng làm padding/dữ liệu âm thanh tái sử dụng bởi nhiều ID.

Level ID $49 (Underground): Level FDS

Tile pointer $76AE + sprite pointer $1C9D. Tile pointer trỏ đến vùng ROM dành riêng cho phiên bản Famicom Disk System. Kết quả: level với tile không tồn tại trong cartridge chuẩn. Đây là level tạo ra level 52-1 và 196-1.

Level ID $00-$02: Level bonus thật sự

Các ID này được dùng bởi các level con hợp pháp của game:

  • $00: vùng dưới nước 5-2/6-2 (dùng bởi H-1, 39-1)
  • $01: nước 2-2/7-2 (Minus World, 36-1)
  • $02: level con 8-4 (136-1, 151-1, 215-1)

Sự khác biệt giữa level "bonus" có thể truy cập bình thường và glitch world là warp zone kiểm tra thế giới hiện tại:

; 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

Các glitch world có số > 8 hoặc 0 không thể được tiếp cận qua ống nước bình thường. Cần wall clip hoặc cart swap.

Tại sao một số level crash: Jump table

Khi game tải vật thể tile, nó dùng loại làm index trong một 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 table: tại sao loại vật thể không hợp lệ khiến game crash

Nếu vật thể có loại không hợp lệ (≥12), game nhảy đến con trỏ không tồn tại trong bảng này. 4 kết quả có thể:

  1. Con trỏ hợp lệ → vật thể tải bình thường
  2. Con trỏ đến jump table khác (chồng lấn) → vật thể khác xuất hiện. Ví dụ: loại 12 trỏ đến bảng Y=13, tạo ra L-pipe.
  3. Con trỏ đến mã thực thi → thực thi code ngẫu nhiên (crash có thể)
  4. Placeholder rõ ràng (NOP) → vật thể không làm gì (một số sprite như vậy, tạo ra kẻ thù bay tại chỗ không di chuyển)

Glitch level ID $58: sprite pointer trỏ đến địa chỉ không hợp lệ, game crash

Glitch level ID $50: đường hầm mây, level được tạo bởi dữ liệu bị hỏng

Glitch level ID $58 (đường hầm gây crash): sprite pointer trỏ đến vùng nhớ không tồn tại trên NES không có 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 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

Nghịch lý pipe warp

Nhớ kiểm tra target_world BETWEEN 1 AND 8. Ngay cả khi bạn tìm thấy ống trong glitch world, game kiểm tra thế giới đích nằm giữa 1 và 8. 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 thúc: cột cờ không có trong sprite, và ống không dẫn đến đâu cả.

Trick 5 vật thể trong một cột

Có một edge case cho phép vượt quá giới hạn 3 vật thể mỗi cột. Khi hàng đợi bị chặn (vị trí đầy + vật thể tiếp theo thiếu cờ next screen), game "xử lý trước" cột hiện tại trong vòng lặp cho đến khi tìm được vật thể có cờ next screen. Trong mỗi lần xử lý trước:

; 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).

Đây được gọi là "queue skip" và được một số romhacker sử dụng để tạo level dày hơn định dạng cho phép.

Sự khác biệt giữa các phiên bản

Famicom Disk System

Phiên bản FDS của SMB1 có bộ nhớ khác. Tất cả con trỏ level bị dịch chuyển, nhưng dữ liệu giống nhau. Điều thay đổi: chỉ số glitch world hoàn toàn khác:

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.

Level FDS yêu thích của tôi: ID $5F, phiên bản ngầm của nửa sau 3-3 ở đường hầm thấp (tiếc là autoscroller).

The Lost Levels (Super Mario Bros. 2 Nhật Bản)

Lost Levels thay đổi nhiều thứ:

  1. Thứ tự tile/sprite giống nhau: không còn Frankenstein level (tile và sprite tải cùng level ngay cả với ID không hợp lệ)
  2. Một bảng con trỏ 16-bit thay vì hai bảng riêng high/low
  3. 4 file disk: ROM đã được chia cho FDS:
    • File 1: thế giới 1-4
    • File 2: thế giới 5-8
    • File 3: thế giới 9 + sound engine
    • File 4: thế giới A-D (bảng con trỏ hoàn toàn khác)
  4. Cùng Level ID = 4 level có thể tùy file được tải
  5. Không còn glitch Tennis: tùy chọn continue (tiếp tục cùng thế giới sau game over) khiến warm start vô dụng, và game reset ngay lập tức nếu world > 9
  6. Vật thể mới: nấm độc, block vô hình, block vô hình fire flower, ống đảo ngược, gió -- nhưng được chèn vào giữa danh sách hiện tại → không tương thích ngược với SMB1
  7. Piranha Plants luôn đỏ sau world 4, springboard xanh chỉ ở thế giới 2/B/3/C/7

Super Mario All-Stars (SNES)

Port trực tiếp với cùng routines 6502 (SNES thực thi mã NES ở chế độ tương thích):

  • Warp zone đã sửa: không còn Minus World (vào ống bên trái trước văn bản dẫn đến đúng thế giới)
  • Crash: hầu hết glitch level crash (trừ ID $6A và 9-1)
  • Vật thể lâu đài được thêm: render độc đáo hơn
  • Nhưng: 4-2 wrong warp vẫn hoạt động (chưa patch!)

4-2 wrong warp: Lỗi đặt vật thể

Trong 4-2, có hai vật thể chuyển tiếp pipe: dây leo (warp zone) và ống (phòng coin cash). Vật thể chuyển tiếp đầu tiên (dây leo) được đặt trước rất lâu khi dây leo xuất hiện trên màn hình. Vật thể thứ hai (ống) được đặt quá muộn trong level.

; 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.

Level lặp

Loop hoạt động như thế nào (8-4, 7-4)? Level có checkpoint với số màn hình và vị trí 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.

Thay đổi định dạng, không thay đổi code

Một trong những bài học hấp dẫn nhất của kiến trúc này là các nhà phát triển SMB1 đã tạo được hệ thống level rất biểu cảm mà không bao giờ chạm vào code render 6502. Tất cả sự khác biệt giữa level đến từ dữ liệu (con trỏ, vật thể, sprite, floor pattern), không phải code.

248 glitch world tồn tại vì bảng con trỏ được thiết kế cho 128 mục × 4 loại, và game không bao giờ kiểm tra giá trị nó đọc. Khi con trỏ rơi vào RAM, game giải thích các register của Mario như tile. Khi con trỏ rơi vào dữ liệu âm thanh, game phát nhạc dưới dạng level design. Và khi jump table bị overflow, game thực thi bất kỳ thứ gì cho đến khi crash.

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

Những gì có thể học từ đây

  1. Tách biệt tile/sprite: độc lập hoàn toàn giữa hai lớp, với thứ tự lưu trữ khác nhau tạo ra Frankenstein level độc đáo
  2. Nén RLE + hệ thống vật thể: level không phải bitmap mà là danh sách vật thể được đặt, với floor pattern cho sàn
  3. Hàng đợi 3 vị trí: giới hạn cứng của phần cứng (và thiết kế level)
  4. Không kiểm tra: game tin vào con trỏ và jump table, tạo ra glitch chơi được hoặc crash
  5. Tối đa 256 byte: giới hạn của register Y 6502, khiến dữ liệu lặp lại nếu đi quá xa
  6. Warm start / cold start: hệ thống "tiếp tục" mở đường cho cart swap Tennis → Mario

Điều tuyệt vời nhất: tất cả là code 6502 chứa trong 40KB. Không tầng trừu tượng, không kiểm tra truy cập bộ nhớ, không trình quản lý ngoại lệ. Nếu con trỏ hỏng, game crash. Và crash, chúng ta gọi là glitch world.

3 điều cần nhớ

  1. Glitch world là con trỏ rơi sai chỗ -- Game có 128 ID × 4 loại khu vực, nhưng chỉ 34 level độc đáo. Khi world number bị hỏng (do Tennis hoặc wall clip), game tải con trỏ được thiết kế cho level khác, và 512 tổ hợp có thể tạo ra kết quả không thể đoán trước.

  2. Minus World là lỗi warp kết hợp với hỏng dữ liệu -- ống bên trái trong 1-2, nếu kích hoạt trước khi văn bản xuất hiện, tải world 36 (0x24). World này trỏ đến Level ID $01 (nước 2-2), level không có cột cờ. Và vì không có chuyển tiếp pipe cho world 36, level lặp vô hạn. Sự thiếu kiểm tra tạo ra biểu tượng.

  3. Tennis → Mario, 15 năm trước OoT → Paper Mario -- RAM của NES tồn tại qua hoán đổi cartridge nhờ tụ điện và hệ thống warm start / cold start của SMB1. Bộ đếm bước chân Tennis (tăng byte RAM khi phát âm thanh bước chân) rơi đúng vào địa chỉ world number. Cần các chữ số top score giữ ở 0, byte $A5 nguyên vẹn, và game phát hiện warm start -- một sự trùng hợp hoàn hảo chỉ hoạt động với Tennis.

Video gốc của Retro Game Mechanics Explained là một công trình phi thường -- mức độ chi tiết về disassemble 6502, bản đồ tự động của tất cả level, giải thích cart swap và warm start. Nếu bạn chưa xem loạt phim này, hãy xem, nó ngắn và mỗi phút đều đậm đặc.

Code nguồn bản đồ có trên rgmechex.com, và bản disassemble đầy đủ SMB1 là open source trên nhiều repo. 40 năm trước, các lập trình viên Nhật Bản đã viết hệ thống level này bằng 6502 với zero unit test và zero bug tracker, và chúng ta vẫn học được điều gì đó khi mở code của họ ngày nay.

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