Skip to content

Other World code analysis

micro500 edited this page Jun 27, 2026 · 7 revisions

The password entered at the start of the game is used as a decryption key for two encrypted blocks of data, one for each bonus world. These blocks are assembly code which is critical to making that bonus world function correctly. Since the encryption is a one-time pad it is impossible to know the plaintext without the encryption key. However, the game provides a fair amount of information about what could be in the block, including knowing it has to be (at least partially) 6502 assembly, as well as some hints about how it would be used and what it would need to do.

This document describes findings that relate to the Other World block and a proposal of what the assembly code possibly contains. This is not the definite encrypted code, just a best guess based on the evidence in the game.

Based on available evidence the best-guess for the assembly in the encrypted block is:

$06A5  LDY #$05             ; preset slot 5 (Other World, map $16)
$06A7  JMP $0:8595          ; respawn_at_preset

$06AA  LDX #$04             ; teleporter slot 4
$06AC  JSR $0:80B4          ; teleport_trigger

$06AF  LDX $F3              ; current_map ($14 / $16 / $17 / $15 on fall-through)
$06B1  LDA $045A            ; player coarse-X
$06B4  CMP $0:B96B,X        ; vs threshold  (field 1; $0:B96B+map indexes the $0:B97F table)
$06B7  LDA $0:B96F,X        ; A = left slot  (field 2, the default)
$06BA  BCC $06BF            ; coarse < threshold -> keep left slot
$06BC  LDA $0:B973,X        ; else A = right slot (field 3)
$06BF  CMP $042F            ; same slot as last latched? (debounce)
$06C2  BEQ $06CA
$06C4  STA $042F            ; latch
$06C7  STA $0425            ; arm the sprite-context reload
$06CA  JMP $0:8464          ; tail-call: frame_end_actor_dispatch (move the platform)

$06CD  LDY #$15             ; win-check trigger slot ($15 -> map $18)
$06CF  JSR $0:AF7F          ; check_at_trigger_spot (carry clear = on the spot)
$06D2  BCS $06F5            ; not on the spot -> +$50's shared RTS
$06D4  LDX #$12             ; --- win: copy the 19-byte respawn template ---
$06D6  LDA $0:B98B,X        ; respawn_template, X
$06D9  STA $0157,X          ;   -> respawn checkpoint $0157..$0169
$06DC  DEX
$06DD  BPL $06D6
$06DF  JSR $0:8952          ; pre_respawn_reset
$06E2  JMP $0:81EE          ; respawn_restorer -> warp to map $31

[no suggestion for +$40 handler $06E5..$06F4]

$06F5  RTS

$06F6 Checksum LSB
$06F7 Checksum MSB 

Decryption

Both bonus worlds in the game rely on the password process to unlock them and make them functional. The end of the password process shows us how this unlock happens:

; $40/$41 point at the encrypted blob, $42 = its length
; $0200.. holds the keystream derived from the player's password.
        LDX #$7F            ; keystream index
        LDY #$00            ; blob index
decrypt:
        LDA ($40),Y         ; take an encrypted byte
        EOR $0200,X         ;   XOR it with the password keystream
        STA $06A5,Y         ;   write the plaintext byte to $06A5 + Y
        DEX
        INY
        CPY $42             ; loop over every byte of the blob
        BNE decrypt

        JSR checksum        ; 16-bit additive sum vs the 2 trailing bytes
        BCC run             ;   match  -> this was the right blob
        ...                 ;   no match -> try the other blob, else give up
run:
        JMP $06A5           ; execute the decrypted block in place

Upon leaving world 5 the final password mutation step is performed on the 128 byte working data, and then that data is used to decrypt one of the two encrypted blocks. The working data is XORed against the first block (114 bytes) for the Carnival world, and then possibly against the other block (83 bytes) which is presumed to be for the Other World.

The decrypted data includes a 16-bit checksum as the final two bytes, and the rest of the data is summed and compared to that value. On the first pass if this checksum succeeds, the game uses it to try to run the Carnival world. If the first checksum fails the decryption tries the second encrypted block, and if the checksum passes then the data is presumed to be used to run the Other World.

The data is decrypted into ram starting at $06A5. When the checksum succeeds the game jumps directly to $06A5 with no additional checks. This hints that the encrypted data is meant to be a block of 6502 assembly, and analysis of the decrypted Carnival data show that to be true for that world. The Carnival world uses the entire rest of the block throughout the gameplay of that bonus world via the map handler dispatch.

The per-map handler table

Every map has a per-frame handler. The handler function pointer is installed once, on map setup, by the routine at $5:9A9A. That routine loads Y with the current map, then later indexes two parallel tables to get the hander address and stashes the result as an indirect-jump vector in $6A/$6B:

$5:9A9A  LDY $F3                ; Y = current_map        [top of L5_9A9A]
         ...                    ; (loads the map's other parameters)
$5:9B08  LDA $5:9CE8,Y          ; per_map_handler_lo[map]
$5:9B0B  STA $6A
$5:9B0D  LDA $5:9D1A,Y          ; per_map_handler_hi[map]
$5:9B10  STA $6B                ; ($6A/$6B) = this map's handler

Then on every frame the main game loop runs that handler:

$0:8061  JSR $80AF              ; per-frame: run the current map's handler
$0:80AF  JMP ($006A)            ; -> the handler pointer installed above

Through normal gameplay it is found that maps $14 - $1A are never accessed, even with the known password which unlocks the Carnival world. When viewed together these maps form a seemingly cohesive world which is assumed to be the locked Other World.

other_world_map_ids.png

Most entries in the handler table point at fixed addresses in PRG0 (eg. $0:86A6, etc.). The interesting entries are the ones that point into RAM, specifically in the range of the decrypted data. Maps $2B - $2D use this pattern, and so do maps $14 - $1A, again reinforcing that those maps are meant to be handled via decrypted assembly.

Map Handler vector Block offset World Notes
$14 $06AF +$0A other shared entry point for $14, $16, and$17
$15 $06AA +$05 other
$16 $06AF +$0A other shared entry point for $14, $16, and$17
$17 $06AF +$0A other shared entry point for $14, $16, and$17
$18 $06CD +$28 other
$19 $06F5 +$50 other
$1A $06E5 +$40 other
$2A $FC52 special case - see below
$2B $06D0 +$2B carnival
$2C $06D8 +$33 carnival
$2D $06E3 +$3E carnival

Note: The table entry for $2A points to the decryption function, but this gets overridden later. On first pass the map load routine ($5:9A9A) installs the decryption function address as the map handler. This causes the decryptor to run on the next frame, and one of the actions in that code is to overwrite the handler to $06A5. If checksum result is valid the code then does JMP $06A5. The Carnival code then resets address $3F to $00 as one of its first actions. This leaves the map handler as $06A5 for the rest of the time the player is on map $2A.

Any time later that the player leaves and returns to map $2A the map load routine hits a special case which specifically looks for map $2A. This code path checks $3F bit 7 and, when that bit is clear, overwrites the handler address with a hardcoded $06A5.

$5:9B3C  LDA $3F
$5:9B3E  BMI $9B4E              ; $3F bit7 SET   -> keep handler = decrypt_world_block
$5:9B40  LDA $F3                ; current_map
$5:9B42  CMP #$2A
$5:9B44  BNE $9B4E
$5:9B46  LDA #$A5
$5:9B48  STA $6A                ; $3F bit7 CLEAR -> override handler to $06A5
$5:9B4A  LDA #$06
$5:9B4C  STA $6B
$5:9B4E  ; (map setup continues)

The end result is that the map $2A handler essentially becomes $06A5. For the Other World, +$00 is assumed to warp the player away from map $2A as one of its first actions, so $2A is never re-entered meaning that the Other World code is not required to clear that flag.

The map handler table provides evidence for the block's internal layout, showing that the Other World has 5 distinct handler entry points: +$05, +$0A, +$28, +$40, +$50 covering its seven maps ($14$1A), with +$0A shared by three of the maps. Since the game will jump to these addresses any time the player is on the corresponding map, this tells us each entry point must be a valid opcode. From there all of these entry points must gracefully return control back to the game's main loop otherwise the game may crash.

These tables also give us an idea of how many bytes are between each handler, which can be used as a byte budget when deciding what instructions could go in that section:

Handler Start End Length Maps
+$00 $06A5 $06A9 5 none
+$05 $06AA $06AE 5 $15
+$0A $06AF $06CC 30 $14, $16, $17
+$28 $06CD $06E4 24 $18
+$40 $06E5 $06F4 16 $1A
+$50 $06F5 $06F5 1 $19
checksum $06F6 $06F7 2 (data)
Total 83

Notes on the thought process for this analysis

  • Each handler is only an entry point, so that offset must be a valid opcode. It is assumed the level functions correctly which means each handler will eventually return control back to the main game loop. This means opcodes must be valid throughout each handler, at least until a point where it leaves the block. However, handlers do not have to be self contained. Execution could fall through into the next handler, and this is actually seen in the Carnival code. That code also used a byte packing/opcode trick to over lap opcodes and reuse a handler for multiple maps, but change how a register is set based on which entry point is used. This sort of trick could be used for the Other World as well.

  • This code exists in RAM, which means the data that is this assembly code is volatile. It is possible the handler code modifies itself throughout its runtime. This is not seen in the Carnival code and can be challenging to program. It is assumed this is not used in the Other World block.

  • It is assumed the developers had control over the length of the encrypted block, up to 128 bytes, since it would be trivial to encrypt any 128-byte or less block they wanted.

    The process for them would be: pick an 8-byte IV (and generate an enterable password from it), run it through the full process, get the keystream out, and XOR it with their desired assembly output. In a typical game development environment it would be common for a marketing team to want that password as early as possible to start preparing for the contest while game is still being coded. The developers could choose the IV very early in development and never change it and still have the freedom to alter the encrypted block at any time.

    Given this freedom it seems safe to assume they would not go to extreme lengths to optimize the code for length, as long as it was under 128 bytes. However in the Carnival code they do use overlapping instructions, so this is not fully guaranteed. They have control over the length of each handler, so adding or removing bytes wouldn't be an issue. It is assumed they would size the block to exactly fit their code, no left over/dead bytes.


+$00 Handler - 5 bytes

This handler is jumped into immediately after a passing Other World checksum with no other setup/configuration. At that moment the game is still set to map $2A, the first map of the Carnival world. If this handler does nothing (just RTS) the game will load that Carnival map and the player is able to move around, however Carnival-specific objects like the mini game that is used to enable rides will not work. This gives a hint that this handler must do something to move the player onto one of the maps for the Other World.

There are a few ways this could be done:

World entry presets

The game has a function ($0:8595) that is used by when exiting worlds 1-5 to set up the player for the next world. The function loads a number of world specific values from a preset lookup table based on an arbitrary slot ID (passed in Y):

  • pending map → $AF (stored as map | $80)
  • coarse world-X → $A0
  • on-screen X → $0558
  • on-screen Y → $0568
Slot Start map ($AF) World-X ($A0) Screen-X ($0558) Screen-Y ($0568)
0 $00 $00 $80 $98
1 $05 $0C $88 $48
2 $07 $00 $18 $00
3 $0E $00 $20 $98
4 $24 $00 $2C $68
5 $16 $00 $28 $A8
6 $2A $00 $20 $A8
7 $20 $00 $28 $A8
  • Slots 2, 3, 4, 6, and 7 are seen in the game code.
  • Slot 0 is not used because world 1 boots through a special direct game initialization rather than a preset.
  • Slot 1 is interesting because map $05 is a part of world 1. The spawn point listed doesn't seem to be in a logical place (with the player slightly in the floor). The purpose of this slot is unknown, maybe an old world layout that changed during development, but this slot doesn't seem relevant for the Other World.

Slot 5 points to map $16, which gives a reasonable hint of that being the starting map for the Other World. This slot can be triggered like this:

$06A5  LDY #$05               ; preset slot 5 (Other World, map $16)
$06A7  JMP $0:8595            ; respawn_at_preset

This is exactly 5 bytes, matching the byte budget for this handler.

Note: Since slots 0 and 1 are unused, it is possible this slot is also unused/dead code, so world entry being on map $16 is not guaranteed, but is currently assumed to be correct.

Direct map warp

If the goal of this handler is to change the game state to one of the Other World maps, this would require a map reset/repaint, otherwise the world will not look correct. In the game engine RAM address $AF is used for that purpose. If that address is set to target_map | $80 (high bit set + map ID), on the next frame the game will process this as a pending map change and repaint into that map.

On each frame the orchestrator's main loop ($0:807E) calls $0:824E, which sees the pending-warp bit in $AF and dispatches the map change. This functionality is used in numerous places, including as one of the steps done in the function suggested above. Writing to this value directly is an option, and can also fit the 5 byte budget:

LDA #$96               ; map $16 | $80 (warp-pending)
STA $AF
RTS

This seems unlikely as it would normally mean spawning the player in an arbitrary location in the map. However, by this point the game has already run preset 6 to move the player to map $2A, and the preset X/Y for map $2A (Carnival start) and map $16 are not far apart ($2A: X=$20, Y=$A8; $16: X=$28, Y=$A8, same Y, 8 apart in X), so the player would roughly start in the same position, making this a viable but odd option.

It seems more likely that the developers would use the existing preset, however given that slot 1 in this table is already thought to be dead, it is possible slot 5 is also unused and the player is moved in some other fashion.

Tail-call vs fall-through

One other consideration is how this handler terminates. A JMP to a game engine function which ends with RTS is essentially a tail-call which returns control back to the game. An RTS directly in the handler will do the same thing. However this return is not necessary if the developers intended to fall-through into the next handler (+$05 - map $15). This could change the tail-call to use JSR instead.

With the assumption that map $16 is where the Other World starts this would be odd since it would run the wrong handler on that map, possibly with odd side effects. But if map $15 is the starting level then this could be plausible, or if the side effects from the map $15 handler are benign enough to run on a different map then this also could be an option. For now it is assumed this handler does not use fall-through.


+$05 Handler - 5 bytes

This handler is run for map $15. That map (and $1A) both have a sprite which appears to be a teleporter.

Teleporter mechanics

The teleporter-looking sprite (type $85) has no handler, so the functionality would need to be provided in another way. The game has several other teleporters throughout, and each use the function $0:80B4 to handle them:

                                  ; X = slot 0-6
$0:80B4  BIT $4D                  ; input_buttons+2 — require Down held
$0:80B6  BPL $8122
$0:80B8  BIT $52                  ; input_buttons+7 — require A held
$0:80BA  BPL $8122
$0:80BC  BVS $8122                ; Check that A was not pressed last frame
$0:80BE  LDA $A0
$0:80C0  CMP $8143,X              ; player's coarse screen vs slot's screen
$0:80C3  BNE $8122
$0:80C5  LDA $0558
$0:80C8  SEC
$0:80C9  SBC $814A,X
$0:80CC  CMP #$10                 ; within 16px of the spot in X?
$0:80CE  BCS $8122
$0:80D0  LDA $0568
$0:80D3  CMP $8151,X              ; exact Y match?
$0:80D6  BNE $8122
$0:80D8  LDA #$0C
$0:80DA  STA $05A8                ; player enters "teleporting" mode
$0:80DD  STX $05D8                ; remember the slot for the deferred warp
$0:80E0  ...                      ; spawn the teleport-effect sprite, etc
$0:8122  RTS                      ; every "no match" branch lands here

This function checks if the player is in a specific location and is pressing the Down+A button combination. If so it triggers a teleport. This function uses a collection of tables for the 7 preset teleporter locations, with the X register being an arbitrary value used to index into those tables.

The seven tables are:

  • Destination map $0:815F
  • Destination screen $0:816D
  • Trigger screen $0:8143
  • x-ref $0:814A
  • y-ref $0:8151
  • effect anim $0:8158
  • post-warp x-offset $0:8166

This table below shows five of those values key to this mechanic:

Slot Dest map Dest scroll Source screen scroll x-ref y-ref Owner / caller
0 $0A $70 $70 $C0 $98 map $08
1 $08 $70 $70 $D0 $98 map $08
2 $0F $54 $5A $80 $A0 map $0F
3 $0F $5B $53 $80 $A0 map $0F
4 $15 $0F $03 $80 $A8
5 $1A $20 $00 $20 $A8
6 $FF $00 $00 $C8 $98 map $31

Note: slot 6's destination map is $FF which triggers a special code path to send the player to the win-screen, not a real map.

Two entries have a destination in the Other World, landing on maps $15 and $1A, and their trigger locations match the teleporter sprite locations.

map_15_scroll_03.png

map_1a_pos_20_2f.png

There are no known callers in the game code which uses these two teleporter slots, indicating either they are dead entries or they are only used in the encrypted code. Seeing that they do reference maps that are assumed to be in the Other World, it seems likely that they are used in this encrypted block.

All other slots are called by their map's handler, in the form of:

LDX #slot             ; this map's teleporter slot
JSR $0:80B4           ; teleport_trigger

Handler code

For this handler to trigger the $15 slot, the code would need to be similar to this:

$06AA  LDX #$04              ; teleporter slot 4
$06AC  JSR $0:80B4           ; teleport_trigger

This fills the 5 byte budget exactly, leaving no room for any other functionality. The key is that this handler must call into $0:80B4 somehow, but this could be done via a tail-call JMP, terminating the handler at the handler boundary. However it is more likely that this handler is intended to fall through into the next handler so as to reuse the code in that handler, and this can be done with the JSR suggested in the code above. The description for the next handler will provide more information as to why this fall through seems more likely.


+$0A Handler - 30 bytes

This handler is used on maps $14, $16, and $17. Being a shared handler likely indicates some kind of shared functionality across all 3 maps. This handler is the largest, so it could have separate code paths for each map, but it seems more likely that the developers would have added new entry points for each map instead of having branches in this section.

Assuming the maps need some kind of shared functionality can give some insights into what this handler is doing. Loading into the middle of map $17 (around the black rectangle, assumed to be a doorway), things seems normal, including enemies moving around. However when you move towards the right you reach a wall you cannot jump over, and an enemy embedded in the wall which does not move. It is possible to get above the enemy and find that you are able to stand on it as if it were a platform.

map_17_platform.png

Moving platforms

This object which is being rendered as an enemy is type $10 and is the game engine's rideable moving platform type. This object is used on many maps, the heaviest use being maps $0A and $0B (the section in the space world with many moving platforms, after the car scene). The handler for this object ($0:8CFF) latches on to the player and moves them along with the platform.

The motion for these platforms is driven by a separate "actor" mechanism which, unlike sprites, is not cleared on map exit. Instead the actor entries are kept and may continue to be updated when the objects are off screen and not rendered. This allows moving platforms to seem to continue their movement even if they are not on screen or even on the current map.

The actor slots aren't owned by a sprite or a single map. Instead they are owned by a group of maps, which is how a platform can continue its movement across a screen boundary. This group behavior is seen in the map-transition setup function ($0:8308). As part of its setup (which also wipes the sprite slots via JSR $8936, loads the new map, etc.), it calls the actor-group seed/preserve routine, which looks up the incoming map's group id from a table:

$0:8327  far_jsr $5:B9B4        ; actor-group seed/preserve routine
$5:B9B4  LDY $F3                ; Y = current_map
$5:B9B6  LDX $B9F7,Y            ; group id = actor_group_table[current_map]

On any map exit the sprites table is cleared, but the actor table is only cleared if the next map is not part of the previous group. The group identifier for each map is provided in a table:

Group Maps
0 $03 $04 $05
1 $06 $07 $08
2 $09 $0A $0B $0C
3 $0E$13
4 $14 $15 $16 $17 (Other World)
$FF - N/A All other maps

Moving between maps in the same group preserves the actor slots, so a platform's counter carries over and it appears to continue across screens. Only entering a new group re-seeds the actor list. As the platform scrolls onto screen and is rendered the object re-binds to its actor from its own object record so the disposable sprite always re-attaches to the same actor.

These platforms normally do not move on their own, but require a specific updater function to process their next tick. On all non-bonus world maps with moving platforms those map handlers call $0:8464, usually as a tail-jump (JMP $0:8464). That routine walks the 8 actor slots and advances the position counter of each active one, and also runs the per-frame update for every type-$10 platform sprite on screen.

The Other World maps $14$17 are a single group (id 4), so any actor across those maps share state. Given that maps $14, $16, and $17 specifically point to this +$0A handler, it would make sense for this handler to call $0:8464 at some point in it's 30 byte budget.

If the handler is only set to JMP $0:8464 the "enemy" sprite now moves up and down like a platform, allowing the player to continue past that wall and further into the world.

Sprite rendering

The enemy sprite that is displayed seems out of place since that enemy typically does damage when the player touches it. It seems unlikely that the developers would have reused that sprite for a platform since the player would avoid it for fear of damage. Instead it seems like there is an issue with this sprite.

Additionally, if you continue into the water past the wall and wait around you will find that you suddenly get killed in a similar fashion to the sharks from world 1. Sure enough the object entries in this area are type $0A, the engine's generic multi-state enemy (handler $2:8831), the shell that the shark is built on. Due to the sprite issue they are not being rendered, however their control logic still works and they can still bite and kill you.

These hints tell us the problem is localized to sprite display, not spawning or control logic. A sprite's graphics come from a per-zone context, which consists of a CHR bank and the metasprite pointer tables. This context is loaded by $2:802E, and is changed by writing a new slot id to $0425 (debounced through $042F). On the next frame the main loop ($0:801F) purges the sprites and reloads the context for that slot. The purge is done to prevent sprites which relied on the old context from being rendered incorrectly.

$0:8027  LDY $0425              ; reload requested? ($FF = none)
$0:802A  BMI $8049              ; nothing pending -> skip
$0:802C  LDX #$0F               ; --- despawn every live sprite slot ($0F..1) ---
$0:802E  LDA #$FF
$0:8030  CMP $0588,x            ; slot empty? ($0588 = sprite_type_id, $FF = empty)
$0:8033  BEQ $803C
$0:8035  LDY #$00
$0:8037  JSR $F897              ; despawn + cleanup
$0:803A  LDA #$FF
$0:803C  DEX
$0:803D  BNE $8030
$0:803F  LDY $0425              ; reload slot
$0:8042  STA $0425              ; clear the request ($FF)
$0:8045  far_jsr $2:802E        ; -> $4A = CHR bank, $E2..$E5 = metasprite pointers

The map handlers for maps $24$27 (Forest world) utilize this functionality mid-map to split the maps into two zones, left and right. Those four maps funnel into a shared handler at $0:8840 that compares $045A (player coarse X position) against a hardcoded threshold and picks a left slot ($0E) or a right slot ($00), which the main loop consumes on the next frame.

$0:8840  STA $40                ; threshold, hardcoded per map: 
$0:8842  JSR $B77C              ; $4C on $24/$27, $70 on $25/$26
$0:8845  LDY #$0E               ; default = left-zone slot
$0:8847  LDA $045A              ; player_coarse_pos
$0:884A  CMP $40                ; compare to threshold
$0:884C  BCC $8850
$0:884E  LDY #$00               ; past threshold -> right-zone slot
$0:8850  CPY $042F              ; debounce
$0:8853  BEQ $885B
$0:8855  STY $042F              ; latch
$0:8858  STY $0425              ; arm the reload

This position based zone swap idea may also apply to the Other World. Setting $0425 to $01 and then walking towards the wall causes the platform object to render as a reasonable looking platform, and the shark objects now render as sharks. However, the enemies on the left side of the map now render incorrectly. This makes it seem like a context change is needed at some point in the level, between the Other World enemies and the moving platform.

The ROM has a table which appears designed for this purpose:

$0:B8BA   [encrypted carnival code]
$0:B92C   [encrypted other world code]
$0:B97F   61 5C 5C 4A   0F 0F 0B 0F   03 01 01 01
$0:B98B   [respawn template for winner room]

No readable code in the game references it. Seeing that it is positioned directly next to other bonus world data it seems likely to be needed by one of the two worlds. The Carnival code does not read, hinting that it is used by the Other World.

The table appears to show the three critical values for the zone swap: the position where it should happen, and the slot ID for the left and right sides. The bytes appear to split into 4 rows of 3 values:

Map (proposed) Position ($045A threshold) Left slot Right slot
$14 $61 $0F $03
$15 $5C $0F $01
$16 $5C $0B $01
$17 $4A $0F $01

Assuming this table is 4 rows x 3 bytes (and not 3 rows x 4 bytes, or some other encoding), this could be related to maps $14-$17. For map $17 this is exactly the swap observed above: right slot $01. Map $17 also initially loads in with slot $0F, again matching the table. Coarse X position $4A is directly between the Other World enemies and the wall. At this position the enemies are usually no longer on screen and the platform has not yet scrolled in, making this a good place to hide the swap without any visual glitch.

map_17_pos_4a.png

The left slot values for $14,$15, and $16 also match how those maps are initially loaded, and the swap position is also in a logical spot, reinforcing the idea that these entries directly relate to these 4 maps. These maps also render sprites oddly on their right side without the swap.

map_14_pos_61.png

map_15_pos_5C.png

map_16_pos_5b.png

Note: Map $16 is shown here at position $5B. Position $5C is in the wall (see map $15 above), so this transition doesn't happen mid-map, instead the context swap happens when the player re-enters the map.

Handler code

To fix both identified issues the +$0A handler would need to swap render slots at the correct time, and also call the actor update function to ensure the platforms move. An option for the code that could do all this is:

$06AF  LDX $F3              ; current_map ($14 / $16 / $17 / $15 on fall-through)
$06B1  LDA $045A            ; player coarse-X
$06B4  CMP $0:B96B,X        ; vs threshold  ($0:B96B + $14 = $0:B97F table)
$06B7  LDA $0:B96F,X        ; A = left slot
$06BA  BCC $06BF            ; coarse < threshold -> keep left slot
$06BC  LDA $0:B973,X        ; else A = right slot
$06BF  CMP $042F            ; same slot as last latched? (debounce)
$06C2  BEQ $06CA
$06C4  STA $042F            ; latch
$06C7  STA $0425            ; arm the sprite-context reload
$06CA  JMP $0:8464          ; tail-call: frame_end_actor_dispatch

This is a parameterized version of the map handlers for maps $24$27 ($0:8840). The phantom base addresses are chosen so that indexing by current_map ($14$17) lands inside the $B97F table:

  • $B96B + $14 = $B97F (thresholds)
  • $B96F + $14 = $B983 (left slots)
  • $B973 + $14 = $B987 (right slots) It debounces through $042F (only re-arms $0425 when the zone actually changes) and tail-calls $0:8464 which advances the platform actor every frame. This is exactly 30 bytes, filling the budget and ending right at +$28.

The parameterization in this code is also directly set up to support map $15. If the +$05 handler falls through into this code, map $15 will benefit from this code, allowing that map to check the teleporter, handle sprite zone swapping, and keep platforms moving. Without this fall through platforms do not move on map $15 and the map appears to become unplayable, hinting that fall-through is correct for the handler above.

Note: the suggested code above may not be exactly right since a few of the instructions can be reordered with no change to the logic, like the LDX/LDA at the beginning and the 2x STA at the end. The comparisons can also be reordered by flipping the left/right handling (< vs >). This makes determining the exact encoding difficult, but the code suggested above tries to mimic the conventions in the game code to make it more likely to be correct.


+$28 Handler - 24 bytes

This handler is for map $18. This map is only 2 screens wide and rather barren, just containing 2 enemies, and no other interactive objects. There is a series of platforms leading towards the top right of the map, and that's it.

map_18_enemies.png

A few things hint at this map being the intended final map of the world:

  • This map is the top of the 7-map world. If it is assumed the player is meant to start near the bottom and work their way up, this would be the logical conclusion.

  • The ascending platforms give the feeling of some climax/conclusion, however ending at a wall is an odd choice. The Carnival world also ends at the top-right of a map, but in that case the player is expected to jump off screen, unlike on this world.

  • The map does not contain any collectibles or really anything else to do. If it was meant as an intermediate map it would make more sense to put more interactions in the map. Also being at the top of the world makes this map seemingly avoidable, so it's only other purpose would be as a dead-end for the player to explore, waste time, and risk getting damaged. Other worlds do have some short dead-ends, but they usually have a reward at the end, like a special but not-critical item, or an extra life. This map has neither, so being an intermediate map seems out of place.

  • It is assumed the Other World has a finish, and so the exit would have to exist somewhere. We are limited on the byte budget for the other map handlers. The handlers before this have a lot to do on a small budget, already taken up by their required actions. If either the +$05 or +$0A handler is incorrect and uses too much of it's budget, this could mean the exit would exist on one of maps $14-$17.

    If it was on maps $14, $16, or $17 there would need to be some conditional to say "if you are on map $XX", in addition to checking if the player has met the criteria to win. For the same reasons as described in that handler detail, it seems unlikely the developers would have mixed that much logic together, and if they did it likely would take more bytes than that handler has available.

    The finish could be on map $15, but that handler has a very small budget to allow for a check of the win condition. Given the small byte budget of that handler the check would likely need to exist in ROM for the handler to jump to, but so far nothing of the sort has been found. The handler also could be an unconditional win if the player reaches that map, but given the size of that map and the amount of things to interact with that seems unlikely.

    The finish could be on map $1A which also has its own unique handler with a 16 byte budget. This could be enough budget to do it, but it would raise similar questions about the size of map $1A and the possible interactions. It would also raise a question of why the handler for map $18 is so large for such a small map.

    The last remaining map is $19, which has a 1 byte budget. This will be discussed in detail below, but that is not enough to accomplish anything, so almost definitely cannot be the correct map.

  • The Carnival code's win-detection and warp logic ($06E8$06FF in the decrypted Carnival block) is exactly 24 bytes long, the same as this handler's budget. The analysis of that handler shows it purposely sending the player to map $31 which is presented as a winner's room to take you to the winning screen. If the Other World does something similar, that warp procedure takes several instructions, which this handler has the budget for.

For all of these reasons it is currently assumed map $18 is meant to be the final map of the Other World, and send the player to the win screen.

Win screen

To "win" the game, or at least get to the "you win" screen where a confirmation code is presented, the bare minimum is JMP $0:BA66. The criteria for this to work is rather minimal:

  • PRG bank 0 must be mapped in
  • It must be executed not in the NMI, and not when the NMI is disabled. The screen relies on the NMI for frame ticks, so triggering this while in the NMI is not valid, and triggering when the NMI is disabled would mean the loop would deadlock looking for the PPU transition which will never happen.

The Carnival code does not do this. Instead it warps you to another room, map $31, which has a black background and a single transporter. Using that transporter will then launch the "you win" screen. This intermediate room provides a better climax feel for beating the game rather than just jumping off the screen and suddenly being presenting with the winning details. It seems likely that they would reuse this sort of mechanism for the Other World as well. There also is no other map like this in the game, so if they did want a finish like that it seems likely they would reuse this specific map.

The only call in the game code to $0:BA66 is in the handler for the transporter on map $31 — specifically the JMP $0:BA66 at $0:9252, inside the type-$2C teleport-effect handler. Once the transporter animation completes, that handler checks the teleport slot's destination byte and, when it has bit 7 set (only slot 6, map $31's transporter), jumps to the winner setup. This reinforces the idea that the developers would reuse this room since it is specifically set up for transitioning to the "you win" screen.

If map $31 is not used to conclude the Other World, then this world would need to make its own call into $0:BA66, and likely with some other form of fanfare. This would be an opportunity to either provide a different feeling for the winner, or to obfuscate this code path to be different than the Carnival code. For now it is assumed map $31 is used to finish the Other World.

Map $31 warp

Warping to map $31 has the same considerations as the +$00 handler, with the main criteria being to move the player to the map. Simply setting a pending warp with LDA #B1 / STA $AF would move the player to that map, but it would keep too many screen rendering details and the map would not load correctly:

map_18_31_direct_warp.png

The table suggested in the +$00 handler also does not have a preset entry which would move the player to map $31.

The Carnival code reuses the checkpoint system in the game to move the player to map $31. The game typically uses this checkpoint system to respawn the player when they die. The game occasionally writes the status of some important player datapoints into a respawn checkpoint in RAM. The respawn function $0:81EE can then be called to restore this checkpoint. That function rebuilds the player state from a 19-byte checkpoint at $0157-$0169. These values are used to restore player position and scroll position to ensure the map loads correctly, among other values.

The Carnival code reuses this checkpoint system to fake a respawn into map $31. Near the encrypted blocks in ROM is a small table which matches the respawn data exactly:

$0:B8BA   [encrypted carnival code]
$0:B92C   [encrypted other world code]
$0:B97F   [table of zone values for other world]
$0:B98B   [19-byte respawn template for winner room]       <--------

The 19-byte template is FF FF FF FF FF FF B1 00 00 00 00 23 30 00 20 B8 04 00 00. These are loaded as:

Bytes Value Restored to Meaning
$0157$015C FF×6 $03DE$03E3 "rebuild the whole screen" sentinel
$015D B1 $AF warp-pending dest = map $31
$015E 00 $F9 motion (reset)
$015F 00 (ignored)
$0160 00 sprite_flags facing / flip
$0161 00 $A0 coarse-X / scroll origin
$0162 23 sprite_onscreen_x landing X
$0163 30 sprite_onscreen_y landing Y
$0164$0167 00 20 B8 04 $D4 $D7 $D5 $D6 camera/scroll + motion vector (reset)
$0168 00 $C2 state (reset)
$0169 00 (ignored)

After copying this data to the checkpoint RAM, the Carnival code then calls the checkpoint restore ($0:81EE) function which fades the screen, loads the assigned map, repositions the player, and fades the screen back in.

It is likely the developers would reuse this respawn template for the Other World as well, and given that the specific code used in the Carnival code fits perfectly into this handler makes it plausible.

Win condition

The other consideration is what the condition would be that the player must achieve to trigger the win. The Carnival code uses a position based trigger, checking if the player's on-screen X position reaches $F0 (the right side of the map). It is possible the Other World also uses a condition like this, and the logical position for it would be at the topmost platform, making the final challenge to climb those platforms. The on-screen X position value would be >=$C9 to line up with that platform. Really any value could work, but values above roughly $84 make more sense because the player can't reach that on map $18 without using the platforms.

The game has a built-in "check if on a specific spot" function, $0:AF7F, indexed by an arbitrary index id in Y. It compares the player's current_map, coarse-X ($0460), and on-screen Y ($0568) against four parallel tables at $0:AFC2 / $0:AFD8 / $0:AFEE / $0:B004. This function is primarily used for item-usage checks like keys to ensure the player is in the correct position to use the item.

Three of the 22 slots target Other World maps:

Slot Map coarse-X ($0460) on-screen Y approach
$0C $16 $2F $A8 $00
$12 $15 $1F $38 $00
$15 $18 $1C $48 $00

Slot $0C is the key check on map $16:

map_16_pos_2f.png

Slot $12 is the key check on map $15:

map_15_pos_1f.png

Slot $15 targets map $18, with a position that matches the top platform:

map_18_x_1c.png

Nothing has been found in the ROM which uses this slot. This could be a dead entry, or it could be meant for the final win check. If this position was used then the condition could be as simple as LDY #$15 / JSR $0:AF7F / BCS fail (carry is clear when the player is on the spot), fitting in the same space as the position check the Carnival code uses.

Handler code

Assuming the developers reused the respawn template and copy code directly from the Carnival code, along with the "check if on spot" slot, the handler code would be:

$06CD  LDY #$15             ; win-check trigger slot ($15 -> map $18)
$06CF  JSR $0:AF7F          ; check_at_trigger_spot (carry clear = on the spot)
$06D2  BCS $06F5            ; not on the spot -> +$50's shared RTS
$06D4  LDX #$12             ; --- win: copy the 19-byte respawn template ---
$06D6  LDA $0:B98B,X        ; respawn_template, X
$06D9  STA $0157,X          ;   -> respawn checkpoint $0157..$0169
$06DC  DEX
$06DD  BPL $06D6
$06DF  JSR $0:8952          ; pre_respawn_reset
$06E2  JMP $0:81EE          ; respawn_restorer -> warp to map $31

This fills the available 24 bytes exactly.

If the position check slot is unused and a position is used instead, the code could be an even more direct copy from the Carnival code, potentially with a different position check value. This would change the beginning of this handler back to:

$06CD  LDA $045A            ; player coarse-X
$06D0  CMP #$XX             ; Range of $85-$FF
$06D2  BCC $06F5            ; not yet -> +$50's shared RTS

Given that both teleporters and item usage relies on a button combo to trigger the interaction it is possible this handler is meant to have a check for a specific button combination (like down+A or down+B to fit with the conventions of the game). This sort of check would require more budget in the handler which is not available as-is. If the copy code is not identical to the Carnival code, or the Other World doesn't use the respawn template at all, this could free up some bytes from the budget for this handler, allowing for more logic.


+$40 Handler - 16 bytes

This handler is for map $1A. A quick look through the level shows another teleporter like in map $15. Reviewing the teleporter presets table again, slot 5 has a destination map in $1A, with a trigger location again lining up with the teleporter sprite at the far left of map $1A. This points to a minimal handler of: LDX #$05 / JMP $80B4. This is only 5 bytes, but unlike the +$05 handler, this handler is 16 bytes, raising a question about the remaining 11 bytes.

The spring object

One potentially broken puzzle/challenge in the world involves a spring object. Map $19 has two related object entries of type $86 and $87. The second, type $87, spawns in the middle of the map (column $30) directly below the tall wall that the player cannot jump over normally.

map_19_wall.png

When this object is present the player can jump on it to be launched higher into the air and move past the wall. This spring functions very similarly to the one found on map $26, just with a lower launch height. Map $19 uses a stronger gravity value of $D7 = $32 vs map $26's value of $19, resulting in the player being launched higher on map $26. The spring on map $26 only appears after the player hits the spring in map $29 causing it to fall off screen.

If the player gets to map $19 and world 4 has been completed as normal, it is found that the spring is already in place in the middle of the map. It is also found that nothing spawns where the first object, type $86 should be.

The disappearance is not caused by that object's handler. Instead the map object spawner ( $2:822B) checks a byte and skips the spawn if that byte is non-zero:

$2:822B  lda ($46),y                     ; collected-flag index
$2:822D  bmi $2:8235                     ;   $FF -> ignore it, always spawn
$2:822F  tay
$2:8230  lda player_objects_collected,y  ;   index $15 -> reads $0487
$2:8233  bne $2:827F                     ; already set -> skip the spawn
$2:8235  ...                             ; otherwise allocate a slot and spawn

On map $26 the second spring object looks at inventory slot $15 which is $0487. Once the player touches the hit-spring the flag gets set, and that hit-spring will not be rendered again even on map re-entry. Map $19 has the same render suppression for it's hit spring, again checking $0487 to decide if it should render or not.

This flag is set by the hit-spring's own contact handler ($2:8C93), which is shared by object type $63 (map $29) and type $86 (map $19):

$2:8C93  LDA $0608,X                    ; live player contact this frame?
$2:8C96  BEQ $8CA2                      ;   no -> idle tail
$2:8C98  LDA #$80
$2:8C9A  STA $0487                      ; set $0487 = $80
$2:8C9D  LDY #$00
$2:8C9F  JMP $2:933D                    ; scatter / fly off-screen (same bank)
$2:8CA2  JMP $0:F7B3                    ; idle tail

This sets the flag to $80 when the player touches the spring, and triggers the animation of it flying off screen.

The jump-spring at the other spawn position uses the opposite condition. This object is always created when the screen scrolls it in. However, its handler will destroy the sprite on the next frame if the flag is not set. Map $19's type-$87 jump-spring is handled by $5:B654

$5:B654  LDA $0487                      ; spring flag
$5:B657  CMP #$80
$5:B659  BNE $B651                      ; not set -> despawn
$5:B65B  LDA #$53                       ; animation frame id
$5:B65D  STA $0548,X                    ; set -> show the OW spring 
$5:B660  LDA $0608,X                    ; player contact this frame?
$5:B663  BEQ $B677                      ;   no -> don't arm
$5:B665  LDA $D4                        ; grounded?
$5:B667  BEQ $B677
$5:B669  LDA $D6                        ; moving downward?
$5:B66B  BMI $B677
$5:B66D  LDA #$F9                       ; --- arm the launch ---
$5:B66F  STA $D5
$5:B671  STA $D6                        ;   initial up velocity $F9
$5:B673  LDA #$32
$5:B675  STA $D7                        ;   gravity $32 
$5:B677  JMP $0:F7B3                    ; idle tail
;        ---
$5:B651  JMP $0:F7C9                    ; despawn

The readable map $26 jump-spring (type $64) does the same check in $0:8F4C (lda $0487 / cmp #$80 / bnejmp $F7C9). $0487 is essentially a sticky flag that toggles the spring between the two locations after the player has interacted with it.

Looking at the layout of maps $19 and $1A, and accounting for the transporter, it appears the intended path through the world is to use the transporter on the left of map $1A to transport 2 screens to the right. The player would then either go up a screen to map $19 and hit the spring, or continue to the right on map $1A. Without hitting the spring the player would traverse right across map $1A, left across map $19, only to come across the unpassable wall. They would then have to backtrack to find the spring, and then again cross the maps to now use the spring and continue.

This sort of puzzle is present in other maps like the machine world where the player must collect all machine parts to get past the UFOs, or backtrack to collect them. This is also a sort of extended version of the spring challenge on map $29, where if you bypass the door and continue left you just reach the end of the map as a dead end, again requiring backtracking.

Given the precedent for these tricky dead-end style puzzles, it seems odd that the spring would already in place when the player reaches that wall, especially given the developers specified a spawn point where the spring should start.

It is currently assumed the second contest would have operated the same as the first, requiring players to traverse all 5 regular worlds before reaching a bonus world. This would mean they would have already traversed world 4, setting the $0487 sticky flag, and causing the spring to already be in place, breaking that puzzle. It is possible the second contest somehow bypassed world 4, but there is currently no evidence to show how that would happen given that world 3 appears to be hardcoded to proceed to the 4th world. It is also possible this puzzle being broken is an oversight, but that seems unlikely.

The more likely scenario is that the developers intended to reset the flag before this puzzle to have it ready for the player. For the puzzle to function correctly the flag just needs to be reset any time before the hit spring is encountered, and then never reset again after. Theoretically the best time to reset this flag would be when the player is sent to the world as a sort of world setup procedure since this would only run once. However given the small budget for the +$00 handler, the code for that sort of initialization would likely need to exist in ROM to both warp the player and reset this flag. So far no code has been found in the ROM to reset this flag outside of the initial game reset routine which resets all game state to start the game fresh. Since that routine clears a large amount of game state is is very unlikely it would be used for this situation.

If the reset is not done in rom then it likely would need to happen in one of the map handlers. As discussed above all handlers before this are byte budget constrained. Only the +$28 handle is possible if the win condition is not actually on map $18. If the win condition is on map $1A, then maybe the reset code could be in the map $18. This would require the player to somehow reach that map before encountering the wall on map $19, and then need a reason to get over that wall. The map layout does not support this idea since the teleporter on map $1A locks the player into maps $18-$1A, with no option to return to maps $16 and $17. This would shrink the size of the world or require some mechanism to get to those maps which has not been found. This would also raise a question again of what the win condition is and where it is located.

The only other option aside from this handler is the +$50 handler for map $19. While it seems logical that the developers would put logic intended for map $19 in the handler for map $19, that handler has only a single byte budget, not enough to accomplish this reset.

Handler code

Unlike the other handlers there is not one single best guess for what would be in this section. None of the options below feel clearly right, so the candidates and their trade-offs are presented as-is, with no opinion on which one is correct.

Assuming the logic is meant to be in this handler raises a question about why the developers would put it in this handler at all. Again, if the developers wanted a simple reset the best place would have been the world set up, but that handler is not long enough. They also could have put it on a handler for maps $14-$17 since once the player uses the transporter, the player will never return to those maps, so the reset could have been unconditional on those maps, but again there is not currently enough room in those handlers.

If they intended to reset the spring right before the player interacts with it, map $1A could make sense as it is the last map the player will see before map $19. One consideration is that the player can (and will) move from map $19 back to map $1A, so the flag reset must be gated in a way that it doesn't trigger any time the player is on map $1A. Without some logic to only reset the flag once the spring could reset immediately after the player hits it, creating an unwinnable situation.

The developers could have used some criteria specific to map $1A to gate the logic. A few ideas are possible which will be discussed in detail. Note that many of these assume the handler after this will just be RTS, so fall-through or direct jumping is done here. See the next handler to understand why it is likely to be RTS.

Teleporter use

Assuming the player has to use the teleporter to get to the rest of the world, the logic could check if the player successfully uses the transporter. The transporter is also one-way, so the reset would only fire once in a playthrough.

When a transporter fires, the player state ($05A8) gets set to $0C. This can be used as a detection for a successful transport, and is actually used on map $0F. That map has a two-way transporter, and the game code checks if the player is on one of them, if not it tries the other.

A straightforward way to use this mechanic would be:

LDX #$05               ; Slot for map $1A transporter
JSR $0:80B4            ; try to transport
LDA $05A8
CMP #$0C               ; transport successful?
BNE $06F5              ; no -> RTS
STA $0487              ; yes -> clear spring
[1 unused byte]

This comes out to only 15 bytes, leaving one extra byte unused, which doesn't seem correct. Note that EOR #$0C could be used in place of the CMP to set the Z flag in the same way for the following BNE, but CMP is the more common way to do this.

The STA $0487 is 3 bytes, so another possible encoding is to rely on the 5 still being in the X register:

STA $0482,X      ; $0482 + $05 = $0487

Another option which would use all 16 bytes available is:

LDX #$05
JSR $0:80B4       ; transport
LDA $05A8         ; get player state
SEC               ; prep the subtract
SBC #$0C          ; A = 0 iff the player just entered the transporter
BNE $06F5         ; not transporting -> fall to $06F5's RTS
STA $0487         ; transporting -> A ($00) clears the spring flag

This relies on a subtraction trick to both compare the output value from the transporter function against $0C, but also if successful then register A would hold $00, making the final STA write a 0 as normal.

This form of logic can also be encoded using ADC instead:

LDX #$05
JSR $0:80B4       ; transport
LDA $05A8         ; get player state
CLC               ; clear carry for the add
ADC #$F4          ; +$F4 == -$0C; A = 0 iff state was $0C
BNE $06F5         ; not transporting -> fall to $06F5's RTS
STA $0487         ; transporting -> A ($00) clears the spring flag

This sort of trick seems unlikely as it is not an idiom seen elsewhere in the game code.

Anti-cheat mechanism

Assuming the second contest requires the players to complete all 5 worlds before this Other World, the player would be required to set the spring flag in the 4th world. It is possible the developers intended to check for that that as a form of anti-cheat protection. The handler could prevent the teleporter from functioning correctly if the flag is not set, trapping the player if they didn't follow the rules. Unfortunately fitting this style of logic into 16 bytes gets tight. One option is:

LDA $0487        ; spring flag
BPL $06F5        ; bit7 clear (not set) -> exit
LDX #$05
JSR $0:80B4      ; teleport
LDA $05A8
CMP #$0C
BNE $06F5        ; teleport successful?
ASL $0487        ; clear flag

This code checks the spring flag first, and only attempts to run the teleporter if the flag is set. If the player successfully transports then the flag is reset. This however comes out at 20 bytes, too much for this handler.

Another (not-functional) option:

LDX #$05
JSR $0:80B4      ; transport
LDA $05A8
CMP #$0C
BNE $06F5        ; on-pad? (A=0 if so)
ASL $0487        ; clear flag, carry = was it set
BCS $06F5        ; was set -> teleport allowed (and now reset)
STA $05A8        ; was clear -> A($00) cancels the teleport (trapped)

Theoretically this allows the teleport to begin, but if the spring is not set it will be immediately cancelled. This almost works, but after the first teleport frame the flag will have been reset, this handler will see that and cancel the in-progress transport, trapping the player. This code also comes out at 20 bytes, again too long for the handler.

There are many other ways to accomplish this sort of logic, but so far none have been found to use 16 bytes or less.

Position based

To reach the rest of the level it is assumed the player must use the transporter, which requires visiting the first screen in map $1A. The win condition could leverage this and check if the player position is in the range of that teleporter. After using the transporter the player can no longer visit the left most 2 screens of map $1A, so the position check could also be if the player is on either of those 2 screens.

The simplest logic for checking position could be:

LDA $045A         ; player_coarse_pos, $00 if at the far-left edge
BNE exit          ; not at the edge -> skip the reset
STA $0487         ; at the edge: A ($00) clears the spring flag

The player has to get to the left side of the map to get onto the transporter, so checking for a coarse X position of 0 could work, but it is possible to trigger the the teleporter at a coarse position of $01, so this specific condition would not be fully safe.

Another possibility would be a less-than style:

LDA $045A      ; player_coarse_pos
CMP #$xx       ; threshold
BCS exit       ; carry set if $045A >= $xx
STA $0487

After using the teleporter the player is locked out from the first two screens of map $1A, limiting them to a coarse position value of $21 or higher, so the threshold value could be anything $21-$01.

When combined with the transporter function call LDX #$05 / JSR $0:80B4 (5 bytes), these options are still only 13 or 15 bytes, again leaving bytes unused.

Another possibility is to re-use an existing function ($0:88B6) in the game code which bounds checks player position based on a preset:

LDY #$06          ; slot 6 = $00-$05
JSR $0:88B6       ; X = $00 if in $00-$05, else $FF
TXA               
BNE exit          ; not in range -> exit
STA $0487         ; in range -> clear the flag

This function has a table of preset min and max position values:

slot (Y) range ($045A) used by
0 $51–$58 carnival $2B
1 $31–$32 carnival $2A, $2C
2 $4B–$50 carnival $2C
3 $38–$4B carnival $2D
4 $3F–$41 map $23
5 $7C–$80 map $22
6 $00–$05 map $22
7 $23–$3A map $22
8 $00–$05 map $0C

In the game code only slot 1 is used by more than one map, and both of those maps are in the Carnival world. All other slots are used by a single map. Slots 6 and 8 have a range of 0-5 which is reasonable for the position of the teleporter on map $1A. Either of these slots could be used to check the player's position and conditionally trigger the flag reset. A benefit of this code is that it comes out to 11 bytes, so adding the 5 bytes for the transporter brings it to exactly 16 bytes.

Since this logic is self contained, the 5 bytes for the teleporter code be either before this logic or after it and still function correctly. The JSR for that function could also potentially be changed to a tail-call JMP.

Run-once guard

All the options above lean on either the teleport event or the map geometry to make the reset fire only once. A more explicit approach is a dedicated one-shot guard byte. This idiom is seen in the Carnival code which runs level setup only once (LDX #$05 / CPX $0451 / BEQ / STX $0451, a run-once guard on $0451). This functionality needs a persistent byte that holds a known value on entry to the world and is touched by nothing else.

Ram address $003F is a possible location for this guard flag. This address is set to $FF at game initialization and is nearly untouched throughout the game. The Carnival world uses it as a flag to indicate if it has run the world setup yet, and then resets it to $00. However if the Carnival code is not run then this address will remain at $FF for the entirety of the Other World. Another benefit of this address is that is in zero page which allows for shorter instructions and can better fit into this byte constrained handler

A few encodings to do this (which end up at 16 bytes):

Carnival-style CPX guard

LDX #$05 doubles as the teleporter slot and the guard marker, mirroring the Carnival prefix exactly:

$06E5  LDX #$05             ; slot 5 / guard value
$06E7  CPX $3F              ; guard already == 5? (reset done)
$06E9  BEQ $06F2            ; yes -> teleport only
$06EB  STX $3F              ; no -> mark guard = $05
$06ED  LDA #$00
$06EF  STA $0487            ; clear spring flag
$06F2  JMP $0:80B4          ; teleport_trigger slot 5

As long as the guard address is not $05 this logic will work, which is the case for address $3F.

Sign-test guard

Exploits bit 7 of $3F being set on entry, and marks "done" by clearing bit 7:

$06E5  BIT $3F              ; test bit7 of guard
$06E7  BPL $06F0            ; bit7 clear -> already done
$06E9  LSR $3F              ; $FF -> $7F (mark done, clears bit7)
$06EB  LDA #$00
$06ED  STA $0487            ; clear spring flag
$06F0  LDX #$05
$06F2  JMP $0:80B4          ; teleport_trigger slot 5

The mark-done op is interchangeable: LSR or INC both clear bit 7 in 2 bytes (swap BIT for LDA if using INC).

Load/test-zero guard

A = 0 feeds both the guard-mark and the flag-clear:

$06E5  LDA $3F              ; $FF on entry (nonzero)
$06E7  BEQ $06F0            ; == 0 -> already done
$06E9  LDA #$00
$06EB  STA $3F              ; mark guard = $00
$06ED  STA $0487            ; clear spring (same A=0)
$06F0  LDX #$05
$06F2  JMP $0:80B4          ; teleport_trigger slot 5

This relies only on the guard byte being non-zero on entry, which $FF satisfies.

Encoding

Across all of these options there are numerous ways to encode the same logic. For example, clearing the spring flag can be done like this: ASL $0487, since it is known the value will be exactly $80. A handler previously proposed by Alyosha is:

$06E5  LDA $045A            ; player coarse-X
$06E8  CMP #$04             ; near the left edge?
$06EA  BCS $06F0            ; yes -> skip the reset, just teleport
$06EC  ASL $0487            ; no -> clear spring ($80 -> $00, bit7 -> carry)
$06EF  CLC                  ; clear the leftover carry
$06F0  LDX #$05             ; teleporter slot 5
$06F2  JMP $0:80B4          ; teleport_trigger

Most code which relies on the carry flag either explicitly sets/clears it directly with SEC/CLC, or uses an instruction which sets/clears it, before using the value. This makes the CLC seem unnecessary in this handler, and this handler would have an unused byte.

The common idiom in the game code to reset a flag like this is to set it to 0 explicitly in the form of: LDA #$00 / STA $0487. It seems likely the developers would keep this idiom in this code. But since the render suppression specifically checks for $80 any other value would work, which makes some of the above options possible.

The jump to the teleport code could be a JSR or JMP depending on how the code is structured, possibly relying on a fall-through. This handler could also rely on a jump to the next hander which is assumed to be RTS.


+$50 Handler - 1 byte

This handler is meant for map $19. At a single byte this handler is very space constrained. There are a few possible options of what could be going on here:

  • This handler jumps somewhere else for its logic. This doesn't work because any branch or jump requires a minimum of 2 bytes.

  • The handler falls through to the next bytes, or the opcode starting at +$50 uses the next bytes. The two bytes after this handler are used for the checksum of the block, and thus are unlikely to come out as sane assembly. The developers would either need to get lucky to have something useful, or tweak the full data block to result in a useful value ending up at that position.

    The checksum is stored in little-endian format, so byte $51 is the LSB of the checksum. This makes adjusting this value easier as any changes to the data block directly affect this value (unlike the MSB which would almost definitely require more than one byte to be changed). The developers would have limited freedom to adjust the full assembly to get a useful value, so it is possible, but it seems more likely that they would have extended this handler if they wanted more bytes here instead of relying on a flaky checksum value coming out correctly. They also could have left a purposely unused byte anywhere else in this data block which could be used to fix this checksum value any time they made a change elsewhere in the block. This again points to needing 2 bytes to do a job in an odd way instead of just extending this handler.

    Even if the handler does use later bytes, byte $50 would still need to be some kind of branch instruction, otherwise this handler would fall through again, this time even closer to leaving this data block and into undefined ram.

  • The handler is a single byte opcode

It is most likely that this handler is a single byte instruction. There are a few categories of one-byte opcodes to review:

Register movement/flags

Opcode Instruction Description
$AA TAX Copies value in A to X
$A8 TAY Copies value in A to Y
$8A TXA Copies value in X to A
$98 TYA Copies value in Y to A
$BA TSX Copies value in SP to X
$E8 INX Increments X
$C8 INY Increments Y
$CA DEX Decrements X
$88 DEY Decrements Y
$18 CLC Clears the C flag
$38 SEC Sets the C flag
$B8 CLV Clears the V flag
$0A ASL Shifts A left, MSB -> C
$2A ROL Shifts A left, C -> LSB, MSB -> C
$4A LSR Shifts A right, LSB -> C
$6A ROR Shifts A right, C -> MSB, LSB -> C

All of these simple change a value in either the A, X, or Y register, or change a flag value. All of these would naturally fall through and treat byte $51 as an opcode.

Stack manipulation

Opcode Instruction Description
$9A TXS Copies value in X to SP
$48 PHA Pushes A to the stack
$68 PLA Pulls a value from the stack into A
$08 PHP Pushes processor flags to the stack
$28 PLP Pulls processor flags from the stack

Every opcode in this section would change the stack pointer which likely would corrupt the stack state when/if this handler returned control back to the main loop. These all also would fall through and treat byte $51 as an opcode.

NOP

Opcode Instruction Description
$EA NOP No-op

While this is a valid instruction it would be an odd choice for this handler since it would just fall through to $51.

Control flow/system opcodes

Opcode Instruction Description
$00 BRK Calls the IRQ vector. The vector code for this game is usually just RTI
$40 RTI Pulls processor flags from the stack, then pulls 2 bytes for the PC and resumes
$60 RTS Pulls 2 bytes for PC off the stack to return control to a function caller
$58 CLI Clears the I flag (enables IRQs)
$78 SEI Sets the I flag (disables IRQs)
$D8 CLD Disables BCD mode
$F8 SED Enables BCD mode

BRK is an uncommon instruction to use, and given that the IRQ vector essentially just runs RTI, this instruction would just return control back to $52.

RTI wouldn't make sense in this context. This instruction relies on the processor flags and previous PC having already been pushed to the stack by the system. Running RTI outside of that context would have odd side effects.

CLI would enable maskable interrupts, but since the game services no IRQ source (the IRQ vector is essentially just an RTI) this has no real effect. SEI would be redundant since interrupts are already disabled by the game's reset routine. Both of these would fall through to $51.

CLD and SED are essentially no-ops on the NES. The normal MOS6502 supports Binary Coded Decimal mode which is controlled with the D flag, however the 2A03 used in the NES removed that functionality. The D flag still exists and would be flipped, but nothing else would happen. These also would fall through to $51.

Handler code

Process of elimination leaves us with RTS as the only viable option for this handler. This would make the handler simply:

$06F5  RTS

RTS would make this handler fully a no-op while still correctly returning control back to the game loop. Besides the spring, map $19 does not appear to need any other special logic, so an RTS makes sense. This RTS could then be re-used by other handlers in this block (and several suggestions above do this). Using a simple RTS as a map handler is also a convention seen elsewhere in the game on maps $00, $01, $02, $0D, $13, $1C, $1D, $20, $21, $2F, and $30.


+$51-52 Checksum - 2 bytes

This section is thought to be used only as a checksum for the block. After decryption the game sums bytes $00-$50, then compares the sum to $51 (LSB) and $52 (MSB). If these match, then the game jumps directly into the +$00 handler.

Since this sum is based directly off the other bytes in this block, the developers do have some freedom to change what the final sum is. Byte $51 would be easier to tweak since it is the LSB. Any change to bytes $00-$50 would directly change this value, so the developers could potentially force this LSB to a specific value. This would be flaky as they would need to continuously adjust as they were developing the world. It would make more sense for them to add an additional byte to this encrypted block as needed instead of relying on this flaky mechanism.

For now it is assumed these bytes are only used as the checksum, and are then ignored as soon as the checksum process completes. If that is the case these bytes give no information about what would be in this block and are not useful for this analysis.


Other thoughts

  • The developers could have shared the ending of the Carnival code with the Other World code. Both blocks are decrypted starting at $06A5 and going only as far as the block is long. This means the Carnival code decrypts to $06A5-$0716. If that checksum fails the Other World is decrypted over top of it to $06A5-$06F7, leaving $06F8-$0716 as it was from the Carnival decryption. It is conceivable that the developers could have used this fact and had the same keystream decrypt to sane/useful assembly from the Carnival data (but with a checksum failure so it moves on to the next decryption), and then the Other World block would install the rest of the world's logic above it.

    This seems highly unlikely since there is not an easy way for the developers to control the output of the XOR decryption for two cipher texts. While a one-time pad/XOR encryption can be used to turn any block of data into any other block of data, that requires controlling the encryption key to apply it to your plaintext. This encryption key would be the keystream output from the IV for the Carnival world password, which is applied to create the encrypted data we have for the Carnival code. To then take that encrypted data and decrypt it to some other arbitrary block of data (presumably useful assembly) they would need to be able to control the keystream precisely. By design the keystream is hard to predict given an IV, and they only have limited control over the output (64 bits input), so it is unlikely they could produce a useful keystream for two blocks, or find one by chance.