jevsnes.git / packages / alttp / src / lib.rs
lib.rsannotatedlib.rssource723 lines · 25.8 KB · raw

A Link to the Past, read out of SNES work RAM.

Every address here is an offset into the 128 KiB of work RAM, which the SNES maps at $7E0000: $7E0010 is offset 0x10, $7EF36D is 0xF36D. The addresses and their meanings come from research/alttp-ram-map.md, which cites the disassemblies they were read from.

8use std::fmt;
10use serde::Serialize;

A direction on the pad, which is also a direction on the map: up is north.

13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub enum Direction {
16    Up,
17    Down,
18    Left,
19    Right,
20}
22impl Direction {

$7E002F, link_direction_facing. Two independent places in zelda3 pin the values: kDashTab2[] = {8, 4, 2, 1} indexed by link_direction_facing >> 1 as the joypad bits that continue a dash (src/player.c:1184,1212), where 8/4/2/1 are Up/Down/Left/Right (src/zelda_rtl.h:89-92); and src/misc.c:706-708, which sets link_direction = 8 (up) and link_direction_facing = 0 together.

29    pub fn facing(byte: u8) -> Option<Self> {
30        match byte {
31            0 => Some(Self::Up),
32            2 => Some(Self::Down),
33            4 => Some(Self::Left),
34            6 => Some(Self::Right),
35            _ => None,
36        }
37    }
38}

$7E0010: which of the game's top-level modules is running. The value is the index into the game's own dispatch table, and the names follow it.

42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
43#[serde(rename_all = "snake_case")]
44pub enum Mode {
45    Title,
46    FileSelect,
47    CopyFile,
48    EraseFile,
49    NameFile,
50    LoadFile,
51    EnteringDungeon,
52    Dungeon,
53    LoadingOverworld,

Light world and dark world alike; which one is [State::world].

55    Overworld,
56    LoadingSpecialOverworld,

The areas off the overworld grid: the Master Sword grove, Zora's domain.

58    SpecialOverworld,

A text box, a map, a menu, a potion being drunk: see [Interface].

60    Interface,
61    SpotlightClosing,
62    SpotlightOpening,
63    FallingIntoDungeon,
64    GameOver,
65    PendantBossVictory,
66    AttractDemo,
67    MirrorWarpFromAgahnim,
68    CrystalBossVictory,
69    SaveAndQuit,
70    GanonEmerges,
71    TriforceRoom,
72    Credits,
73    SpawnSelect,

0x0C and 0x0D are empty slots in the dispatch table, and nothing above 0x1B is in it at all.

76    Unrecognised(u8),
77}
79impl Mode {
80    pub fn read(byte: u8) -> Self {
81        match byte {
82            0x00 => Self::Title,
83            0x01 => Self::FileSelect,
84            0x02 => Self::CopyFile,
85            0x03 => Self::EraseFile,
86            0x04 => Self::NameFile,
87            0x05 => Self::LoadFile,
88            0x06 => Self::EnteringDungeon,
89            0x07 => Self::Dungeon,
90            0x08 => Self::LoadingOverworld,
91            0x09 => Self::Overworld,
92            0x0A => Self::LoadingSpecialOverworld,
93            0x0B => Self::SpecialOverworld,
94            0x0E => Self::Interface,
95            0x0F => Self::SpotlightClosing,
96            0x10 => Self::SpotlightOpening,
97            0x11 => Self::FallingIntoDungeon,
98            0x12 => Self::GameOver,
99            0x13 => Self::PendantBossVictory,
100            0x14 => Self::AttractDemo,
101            0x15 => Self::MirrorWarpFromAgahnim,
102            0x16 => Self::CrystalBossVictory,
103            0x17 => Self::SaveAndQuit,
104            0x18 => Self::GanonEmerges,
105            0x19 => Self::TriforceRoom,
106            0x1A => Self::Credits,
107            0x1B => Self::SpawnSelect,
108            other => Self::Unrecognised(other),
109        }
110    }

The modes in which Link walks about.

113    pub fn is_play(self) -> bool {
114        matches!(self.kind(), Kind::Play)
115    }

What kind of thing this mode is, for a caller that has to do something about every one of them.

Exhaustive on purpose, with no wildcard arm. A mode added to the enum is a compile error here until somebody says which kind it is, which is the whole point: the app once sat on the full-screen world map pressing A at it - a button that screen does not answer to - because "not a play mode" was a single else and nothing had to name the case.

125    pub fn kind(self) -> Kind {
126        match self {
127            Self::Dungeon | Self::Overworld | Self::SpecialOverworld => Kind::Play,
128            // A screen laid over the game. Which one is the `Interface` step.
129            Self::Interface => Kind::Overlay,
130            Self::Title | Self::AttractDemo => Kind::Title,
131            Self::FileSelect => Kind::FileSelect,
132            Self::NameFile => Kind::NameFile,
133            // Flows we never want to be in. Backing out is the answer.
134            Self::CopyFile | Self::EraseFile => Kind::Unwanted,
135            Self::GameOver => Kind::GameOver,
136            Self::SpawnSelect => Kind::SpawnSelect,
137            // Loads, transitions and cutscenes: they end by themselves, and
138            // a button pressed into one is at best ignored.
139            Self::LoadFile
140            | Self::EnteringDungeon
141            | Self::LoadingOverworld
142            | Self::LoadingSpecialOverworld
143            | Self::SpotlightClosing
144            | Self::SpotlightOpening
145            | Self::FallingIntoDungeon
146            | Self::PendantBossVictory
147            | Self::MirrorWarpFromAgahnim
148            | Self::CrystalBossVictory
149            | Self::SaveAndQuit
150            | Self::GanonEmerges
151            | Self::TriforceRoom
152            | Self::Credits => Kind::Passing,
153            Self::Unrecognised(_) => Kind::Unrecognised,
154        }
155    }
156}

What a [Mode] is, to something that must handle every one of them.

159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
160#[serde(rename_all = "snake_case")]
161pub enum Kind {

Link walks about and the pad is his. The only kind worth a question.

163    Play,

A screen over the game: a text box, a map, a menu, a potion being drunk. [Mode::Interface] alone; the [Interface] step says which.

166    Overlay,
167    Title,
168    FileSelect,
169    NameFile,

Copying or erasing a file: flows there is never a reason to be in.

171    Unwanted,
172    GameOver,
173    SpawnSelect,

A load, a transition or a cutscene. It ends by itself.

175    Passing,

Not in the game's own dispatch table.

177    Unrecognised,
178}
180impl fmt::Display for Mode {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        f.write_str(match self {
183            Self::Title => "title",
184            Self::FileSelect => "file select",
185            Self::CopyFile => "copy file",
186            Self::EraseFile => "erase file",
187            Self::NameFile => "name file",
188            Self::LoadFile => "load file",
189            Self::EnteringDungeon => "entering dungeon",
190            Self::Dungeon => "dungeon",
191            Self::LoadingOverworld => "loading overworld",
192            Self::Overworld => "overworld",
193            Self::LoadingSpecialOverworld => "loading special overworld",
194            Self::SpecialOverworld => "special overworld",
195            Self::Interface => "interface",
196            Self::SpotlightClosing => "spotlight closing",
197            Self::SpotlightOpening => "spotlight opening",
198            Self::FallingIntoDungeon => "falling into dungeon",
199            Self::GameOver => "game over",
200            Self::PendantBossVictory => "pendant boss victory",
201            Self::AttractDemo => "attract demo",
202            Self::MirrorWarpFromAgahnim => "mirror warp from Agahnim",
203            Self::CrystalBossVictory => "crystal boss victory",
204            Self::SaveAndQuit => "save and quit",
205            Self::GanonEmerges => "Ganon emerges",
206            Self::TriforceRoom => "Triforce room",
207            Self::Credits => "credits",
208            Self::SpawnSelect => "spawn select",
209            Self::Unrecognised(byte) => return write!(f, "unrecognised mode {byte}"),
210        })
211    }
212}

A mode's steps, as its dispatch table in the game names them. Several bytes share a name where the table points them at the same routine.

216macro_rules! steps {
217    ($(#[$doc:meta])* $name:ident { $($variant:ident $text:literal = $($byte:literal)|+,)+ }) => {
218        $(#[$doc])*
219        #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
220        #[serde(rename_all = "snake_case")]
221        pub enum $name { $($variant,)+ }
222
223        impl $name {
224            fn read(byte: u8) -> Option<Self> {
225                match byte {
226                    $($($byte)|+ => Some(Self::$variant),)+
227                    _ => None,
228                }
229            }
230        }
231
232        impl fmt::Display for $name {
233            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234                f.write_str(match self { $(Self::$variant => $text,)+ })
235            }
236        }
237    };
238}
240steps! {

Module00_Intro, zelda3 src/ending.c:513-525.

242    TitleStep {
243        SettingUp "setting up" = 0 | 1,
244        PreparingTriforce "preparing the triforce" = 2 | 10,
245        TriforceSpinning "triforce spinning in" = 3 | 4 | 9 | 11,
246        LogoFadingIn "logo fading in" = 5,
247        SwordComingDown "sword coming down" = 6,
248        BackgroundFadingIn "background fading in" = 7,
249        WaitingForStart "waiting for start" = 8,
250    }
251}
253steps! {

kDungeonSubmodules, zelda3 src/dungeon.c:401-429. Step 0 is [Step::PlayerControl].

256    DungeonStep {
257        ScrollingWithinRoom "scrolling within the room" = 1,
258        ScrollingToNextRoom "scrolling to the next room" = 2,
259        OverlayChanging "overlay changing" = 3,
260        UnlockingDoor "unlocking a door" = 4,
261        ShuttersMoving "shutter doors moving" = 5,
262        WideStairsBetweenRooms "taking wide stairs between rooms" = 6,
263        FallingToRoomBelow "falling to the room below" = 7,
264        StairsNorthWithinRoom "taking stairs north within the room" = 8,
265        OpeningCrackedDoor "opening a cracked door" = 9,
266        BrightnessChanging "brightness changing" = 10,
267        SwampPoolDraining "swamp pool draining" = 11,
268        SwampWaterFlooding "swamp water flooding" = 12,
269        DamFlooding "dam flooding" = 13,
270        SpiralStairs "taking spiral stairs" = 14,
271        LandingFromFall "landing from a fall" = 15,
272        StairsSouthWithinRoom "taking stairs south within the room" = 16,
273        StraightStairsBetweenRooms "taking straight stairs between rooms" = 17 | 18 | 19,
274        RecoveringFromFall "recovering from a fall" = 20,
275        WarpPad "on a warp pad" = 21,
276        PegsMoving "crystal pegs moving" = 22,
277        PressurePlate "pressure plate pressed" = 23,
278        MaidenRescued "maiden rescued" = 24,
279        MirrorFading "mirror fading" = 25,
280        TriforceDoorOpening "triforce door opening" = 26,
281    }
282}
284steps! {

kOverworldSubmodules, zelda3 src/overworld.c:191-240, which [Mode::SpecialOverworld] runs too. Step 0 is [Step::PlayerControl]; the entries the source leaves as Overworld_FuncNN stay numbered.

288    OverworldStep {
289        LoadingGraphics "loading graphics" = 1 | 15 | 26 | 38,
290        FinishingGraphics "finishing graphics" = 2 | 16 | 27 | 39,
291        LoadingMap "loading the next map" = 3 | 17,
292        LoadingSprites "loading sprites" = 4 | 18,
293        ScrollStarting "scroll starting" = 5 | 19,
294        Scrolling "scrolling to the next screen" = 6 | 20,
295        ScrollEasingOff "scroll easing off" = 7 | 21,
296        ArrivingOnScreen "arriving on the screen" = 8,
297        BigDoorOpeningFromInside "big door opening from inside" = 9,
298        WalkingOutFacingDown "walking out, facing down" = 10,
299        WalkingOutFacingUp "walking out, facing up" = 11,
300        BigDoorOpening "big door opening" = 12,
301        MosaicStarting "mosaic fade starting" = 13 | 23 | 36,
302        LoadingOverlays "loading overlays" = 14 | 32 | 33 | 37,
303        MosaicFadingBackIn "fading back in from mosaic" = 22 | 41,
304        MirrorWarp "mirror warp" = 35 | 44,
305        BuildingScreen "building the screen" = 40,
306        RecoveringFromDrowning "recovering from drowning" = 42,
307        WeathervaneExploding "weathervane exploding" = 45,
308        Whirlpool "in a whirlpool" = 46,
309    }
310}

What [Mode::Interface] is showing.

313#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
314#[serde(rename_all = "snake_case")]
315pub enum Interface {
316    Hud,
317    TextBox,
318    DungeonMap,
319    RedPotion,
320    DesertPrayer,
321    WorldMap,
322    GreenPotion,
323    BluePotion,
324    FluteMenu,
325    SaveMenu,
326}
328impl fmt::Display for Interface {
329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330        f.write_str(match self {
331            Self::Hud => "HUD update",
332            Self::TextBox => "text box",
333            Self::DungeonMap => "dungeon map",
334            Self::RedPotion => "drinking red potion",
335            Self::DesertPrayer => "desert prayer",
336            Self::WorldMap => "world map",
337            Self::GreenPotion => "drinking green potion",
338            Self::BluePotion => "drinking blue potion",
339            Self::FluteMenu => "flute menu",
340            Self::SaveMenu => "save menu",
341        })
342    }
343}

$7E0011: the step within the mode. The same number means a different thing in each mode, so it is only read together with one.

347#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
348#[serde(rename_all = "snake_case")]
349pub enum Step {

Step 0 of a mode Link walks about in: the routine that reads the pad. Necessary for the player to have control, and not sufficient — the game can still hold Link in place from here.

353    PlayerControl,
354    Interface(Interface),
355    Title(TitleStep),
356    Dungeon(DungeonStep),
357    Overworld(OverworldStep),

A step this mode's dispatch table has no name for, or a mode whose table has not been read.

360    Numbered(u8),
361}
363impl Step {
364    pub fn read(mode: Mode, byte: u8) -> Self {
365        match (mode, byte) {
366            (mode, 0) if mode.is_play() => Self::PlayerControl,
367            (Mode::Interface, 1) => Self::Interface(Interface::Hud),
368            (Mode::Interface, 2) => Self::Interface(Interface::TextBox),
369            (Mode::Interface, 3) => Self::Interface(Interface::DungeonMap),
370            (Mode::Interface, 4) => Self::Interface(Interface::RedPotion),
371            (Mode::Interface, 5) => Self::Interface(Interface::DesertPrayer),
372            (Mode::Interface, 7) => Self::Interface(Interface::WorldMap),
373            (Mode::Interface, 8) => Self::Interface(Interface::GreenPotion),
374            (Mode::Interface, 9) => Self::Interface(Interface::BluePotion),
375            (Mode::Interface, 10) => Self::Interface(Interface::FluteMenu),
376            (Mode::Interface, 11) => Self::Interface(Interface::SaveMenu),
377            (Mode::Title, byte) => TitleStep::read(byte).map_or(Self::Numbered(byte), Self::Title),
378            (Mode::Dungeon, byte) => {
379                DungeonStep::read(byte).map_or(Self::Numbered(byte), Self::Dungeon)
380            }
381            (Mode::Overworld | Mode::SpecialOverworld, byte) => {
382                OverworldStep::read(byte).map_or(Self::Numbered(byte), Self::Overworld)
383            }
384            (_, other) => Self::Numbered(other),
385        }
386    }
387}
388
389impl fmt::Display for Step {
390    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391        match self {
392            Self::PlayerControl => f.write_str("player control"),
393            Self::Interface(interface) => interface.fmt(f),
394            Self::Title(step) => step.fmt(f),
395            Self::Dungeon(step) => step.fmt(f),
396            Self::Overworld(step) => step.fmt(f),
397            Self::Numbered(byte) => write!(f, "step {byte}"),
398        }
399    }
400}

$7EF3CA.

403#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
404#[serde(rename_all = "snake_case")]
405pub enum World {
406    Light,
407    Dark,
408}
410impl fmt::Display for World {
411    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412        f.write_str(match self {
413            Self::Light => "light world",
414            Self::Dark => "dark world",
415        })
416    }
417}

$7EF3C5, the save file's own account of how far the story has got. Four values, enumerated in jpdasm as GAMESTATE (research/alttp-ram-map.md §3) - the signal to drive intro automation from, because it needs no room-id table.

424#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
425#[serde(rename_all = "snake_case")]
426pub enum Progress {

The very start; progress cannot even be saved yet.

428    Beginning,
429    UncleReached,
430    ZeldaRescued,
431    AgahnimDefeated,
432    Unrecognised(u8),
433}
435impl Progress {
436    pub fn read(byte: u8) -> Self {
437        match byte {
438            0 => Self::Beginning,
439            1 => Self::UncleReached,
440            2 => Self::ZeldaRescued,
441            3 => Self::AgahnimDefeated,
442            other => Self::Unrecognised(other),
443        }
444    }
445}
446
447impl fmt::Display for Progress {
448    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449        f.write_str(match self {
450            Self::Beginning => "the very beginning",
451            Self::UncleReached => "uncle reached",
452            Self::ZeldaRescued => "Zelda rescued",
453            Self::AgahnimDefeated => "Agahnim defeated",
454            Self::Unrecognised(byte) => return write!(f, "progress {byte}"),
455        })
456    }
457}

$7EF3C6, the opening's one-time events. jpdasm names the bits (PROGLITE, research/alttp-ram-map.md §3); these three are the cleanest checkpoints the whole game exposes for the first half hour.

462#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
463pub struct Opening {

Uncle has left the house, so Link is free to follow.

465    pub uncle_left_house: bool,

Uncle has been found in the secret passage - the sword and shield.

467    pub uncle_found: bool,

Zelda has been brought to the Sanctuary.

469    pub zelda_at_sanctuary: bool,
470}
472impl Opening {
473    pub fn read(byte: u8) -> Self {
474        Self {
475            uncle_left_house: byte & 0x10 != 0,
476            uncle_found: byte & 0x01 != 0,
477            zelda_at_sanctuary: byte & 0x04 != 0,
478        }
479    }
480}

$7E005D, Link's own state machine, as far as it has names (research/alttp-ram-map.md §2, from zelda3 src/player.h:7-34).

484#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
485#[serde(rename_all = "snake_case")]
486pub enum Player {
487    Ground,
488    FallingIntoHole,
489    Recoiling,
490    SpinAttacking,
491    Swimming,
492    Dashing,
493    Hookshot,
494    UsingMirror,
495    HoldingSomethingUp,

The rainy night the game opens on.

497    AsleepInBed,
498    Bunny,
499    Numbered(u8),
500}
502impl Player {
503    pub fn read(byte: u8) -> Self {
504        match byte {
505            0 => Self::Ground,
506            1 => Self::FallingIntoHole,
507            2 | 6 => Self::Recoiling,
508            3 | 30 => Self::SpinAttacking,
509            4 => Self::Swimming,
510            17 | 18 => Self::Dashing,
511            19 => Self::Hookshot,
512            20 => Self::UsingMirror,
513            21 => Self::HoldingSomethingUp,
514            22 => Self::AsleepInBed,
515            23 | 28 => Self::Bunny,
516            other => Self::Numbered(other),
517        }
518    }
519}
520
521impl fmt::Display for Player {
522    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
523        f.write_str(match self {
524            Self::Ground => "on his feet",
525            Self::FallingIntoHole => "falling into a hole",
526            Self::Recoiling => "recoiling",
527            Self::SpinAttacking => "spin attacking",
528            Self::Swimming => "swimming",
529            Self::Dashing => "dashing",
530            Self::Hookshot => "on the hookshot",
531            Self::UsingMirror => "using the mirror",
532            Self::HoldingSomethingUp => "holding something up",
533            Self::AsleepInBed => "asleep in bed",
534            Self::Bunny => "a bunny",
535            Self::Numbered(byte) => return write!(f, "player state {byte}"),
536        })
537    }
538}

Where Link is, coarsely: one number, and which kind of number it is.

541#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
542#[serde(rename_all = "snake_case")]
543pub enum Place {

$7E008A, the overworld area.

545    Outdoors(u8),

$7E00A0, the indoor room - a house, a cave or a dungeon room.

547    Indoors(u16),
548}
550impl fmt::Display for Place {
551    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
552        match self {
553            Self::Outdoors(area) => write!(f, "outdoors, area {area}"),
554            Self::Indoors(room) => write!(f, "indoors, room {room}"),
555        }
556    }
557}

What the game is doing and where Link is, as of one frame.

560#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
561pub struct State {
562    pub mode: Mode,
563    pub step: Step,
564    pub world: World,

$7E0022, pixels.

566    pub link_x: u16,

$7E0020, pixels.

568    pub link_y: u16,

$7EF36D, in eighths of a heart.

570    pub health: u8,

$7EF36C, in eighths of a heart.

572    pub health_capacity: u8,

$7EF362.

574    pub rupees: u16,

Whether the player's own buttons move Link this frame. The exact rule the engine itself gates on, not a guess: see [State::has_control].

577    pub control: bool,
578    pub place: Place,

$7E00EE: which of a two-storey room's layers Link stands on, and which half of the tile-attribute grid therefore describes him.

581    pub lower_level: bool,

$7E002F. None for a byte outside the four-value convention.

583    pub facing: Option<Direction>,
584    pub player: Player,
585    pub progress: Progress,
586    pub opening: Opening,

$7EF359: 0 is no sword at all, which is how the game starts.

588    pub sword: u8,

$7EF35A.

590    pub shield: u8,

$7EF3CC: who is following Link.

592    pub follower: Follower,
593}

$7EF3CC, follower_indicator (zelda3 src/variables.h:1121): who is walking behind Link.

This, and not $7EF3C5, is what freeing Zelda sets. Her cell script writes follower_indicator = 1 the moment she joins Link (zelda3 src/sprite_main.c:6300-6305); the progress byte only becomes 2 when she reaches the Sanctuary (:6342). Reading "Zelda rescued" off the progress byte made the rescue and the sanctuary one and the same event.

603#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
604#[serde(rename_all = "snake_case")]
605pub enum Follower {
606    Nobody,
607    Zelda,

Another follower, by its number. Not named until a table is read.

609    Numbered(u8),
610}
612impl Follower {
613    pub fn read(byte: u8) -> Self {
614        match byte {
615            0 => Self::Nobody,
616            1 => Self::Zelda,
617            other => Self::Numbered(other),
618        }
619    }
620}
621
622impl State {

Read the state out of work RAM. Takes the whole of it, so that no offset above can be out of range.

625    pub fn read(wram: &[u8; 0x20000]) -> Self {
626        let word = |at: usize| u16::from_le_bytes([wram[at], wram[at + 1]]);
627        let mode = Mode::read(wram[0x10]);
628        let indoors = wram[0x1B] != 0;
629        Self {
630            mode,
631            step: Step::read(mode, wram[0x11]),
632            world: if wram[0xF3CA] == 0 { World::Light } else { World::Dark },
633            link_x: word(0x22),
634            link_y: word(0x20),
635            health: wram[0xF36D],
636            health_capacity: wram[0xF36C],
637            rupees: word(0xF362),
638            control: Self::has_control(wram, mode),
639            place: if indoors { Place::Indoors(word(0xA0)) } else { Place::Outdoors(wram[0x8A]) },
640            lower_level: wram[0xEE] != 0,
641            facing: Direction::facing(wram[0x2F]),
642            player: Player::read(wram[0x5D]),
643            progress: Progress::read(wram[0xF3C5]),
644            opening: Opening::read(wram[0xF3C6]),
645            sword: wram[0xF359],
646            follower: Follower::read(wram[0xF3CC]),
647            shield: wram[0xF35A],
648        }
649    }

Whether the pad moves Link this frame.

The engine's own gate, copied out of the two functions that implement it - Module09_00_PlayerControl and Module07_00_PlayerControl guard identically (research/alttp-ram-map.md §1, citing zelda3 src/overworld.c:750-758 and src/dungeon.c:6594-6602). Being in a walking mode on step zero is necessary and NOT sufficient: four flags can still hold Link where he stands.

659    fn has_control(wram: &[u8; 0x20000], mode: Mode) -> bool {
660        mode.is_play()
661            && wram[0x11] == 0
662            // flag_custom_spell_anim_active, flag_is_link_immobilized,
663            // flag_block_link_menu.
664            && wram[0x112] == 0
665            && wram[0x2E4] == 0
666            && wram[0xFFC] == 0
667            // trigger_special_entrance, which gates the overworld only.
668            && (mode == Mode::Dungeon || wram[0x4C6] == 0)
669    }

Hearts, as the HUD draws them: work RAM keeps eighths.

672    pub fn hearts(&self) -> f32 {
673        f32::from(self.health) / 8.0
674    }
675}
677#[cfg(test)]
678mod tests {
679    use super::*;
680
681    fn wram() -> Box<[u8; 0x20000]> {
682        vec![0u8; 0x20000].into_boxed_slice().try_into().expect("the right size")
683    }
684
685    #[test]
686    fn control_needs_all_of_the_gate_and_not_just_the_mode() {
687        let mut ram = wram();
688        ram[0x10] = 0x09;
689        assert!(State::read(&ram).control, "overworld, step zero, no flags set");
690        for flag in [0x112usize, 0x2E4, 0xFFC, 0x4C6] {
691            ram[flag] = 1;
692            assert!(!State::read(&ram).control, "flag ${flag:X} must take control away");
693            ram[flag] = 0;
694        }
695        ram[0x11] = 1;
696        assert!(!State::read(&ram).control, "a nonzero step is a cutscene or a scroll");
697    }
698
699    #[test]
700    fn a_dungeon_ignores_the_overworld_only_flag() {
701        let mut ram = wram();
702        ram[0x10] = 0x07;
703        ram[0x4C6] = 1;
704        assert!(State::read(&ram).control, "trigger_special_entrance gates the overworld only");
705    }
706
707    #[test]
708    fn a_text_box_is_never_control() {
709        let mut ram = wram();
710        ram[0x10] = 0x0E;
711        ram[0x11] = 2;
712        let state = State::read(&ram);
713        assert!(!state.control);
714        assert_eq!(state.step, Step::Interface(Interface::TextBox));
715    }
716
717    #[test]
718    fn the_opening_flags_are_the_bits_jpdasm_names() {
719        let opening = Opening::read(0x15);
720        assert!(opening.uncle_left_house && opening.uncle_found && opening.zelda_at_sanctuary);
721        assert_eq!(Opening::read(0x00).uncle_found, false);
722    }
723}