jevsnes.git / packages / alttp / src / lib.rs
lib.rsannotatedlib.rssource723 lines · 25.8 KB · raw
1//! A Link to the Past, read out of SNES work RAM.
2//!
3//! Every address here is an offset into the 128 KiB of work RAM, which the
4//! SNES maps at `$7E0000`: `$7E0010` is offset `0x10`, `$7EF36D` is `0xF36D`.
5//! The addresses and their meanings come from `research/alttp-ram-map.md`,
6//! which cites the disassemblies they were read from.
7
8use std::fmt;
9
10use serde::Serialize;
11
12/// 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}
21
22impl Direction {
23    /// `$7E002F`, `link_direction_facing`. Two independent places in zelda3
24    /// pin the values: `kDashTab2[] = {8, 4, 2, 1}` indexed by
25    /// `link_direction_facing >> 1` as the joypad bits that continue a dash
26    /// (`src/player.c:1184,1212`), where 8/4/2/1 are Up/Down/Left/Right
27    /// (`src/zelda_rtl.h:89-92`); and `src/misc.c:706-708`, which sets
28    /// `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}
39
40/// `$7E0010`: which of the game's top-level modules is running. The value is
41/// 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,
54    /// Light world and dark world alike; which one is [`State::world`].
55    Overworld,
56    LoadingSpecialOverworld,
57    /// The areas off the overworld grid: the Master Sword grove, Zora's domain.
58    SpecialOverworld,
59    /// 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,
74    /// `0x0C` and `0x0D` are empty slots in the dispatch table, and nothing
75    /// above `0x1B` is in it at all.
76    Unrecognised(u8),
77}
78
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    }
111
112    /// The modes in which Link walks about.
113    pub fn is_play(self) -> bool {
114        matches!(self.kind(), Kind::Play)
115    }
116
117    /// What kind of thing this mode is, for a caller that has to do something
118    /// about every one of them.
119    ///
120    /// **Exhaustive on purpose, with no wildcard arm.** A mode added to the
121    /// enum is a compile error here until somebody says which kind it is,
122    /// which is the whole point: the app once sat on the full-screen world map
123    /// pressing A at it - a button that screen does not answer to - because
124    /// "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}
157
158/// 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 {
162    /// Link walks about and the pad is his. The only kind worth a question.
163    Play,
164    /// A screen over the game: a text box, a map, a menu, a potion being
165    /// drunk. [`Mode::Interface`] alone; the [`Interface`] step says which.
166    Overlay,
167    Title,
168    FileSelect,
169    NameFile,
170    /// Copying or erasing a file: flows there is never a reason to be in.
171    Unwanted,
172    GameOver,
173    SpawnSelect,
174    /// A load, a transition or a cutscene. It ends by itself.
175    Passing,
176    /// Not in the game's own dispatch table.
177    Unrecognised,
178}
179
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}
213
214/// A mode's steps, as its dispatch table in the game names them. Several
215/// 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}
239
240steps! {
241    /// `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}
252
253steps! {
254    /// `kDungeonSubmodules`, zelda3 `src/dungeon.c:401-429`. Step 0 is
255    /// [`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}
283
284steps! {
285    /// `kOverworldSubmodules`, zelda3 `src/overworld.c:191-240`, which
286    /// [`Mode::SpecialOverworld`] runs too. Step 0 is [`Step::PlayerControl`];
287    /// 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}
311
312/// 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}
327
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}
344
345/// `$7E0011`: the step within the mode. The same number means a different
346/// 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 {
350    /// Step 0 of a mode Link walks about in: the routine that reads the pad.
351    /// Necessary for the player to have control, and not sufficient — the
352    /// game can still hold Link in place from here.
353    PlayerControl,
354    Interface(Interface),
355    Title(TitleStep),
356    Dungeon(DungeonStep),
357    Overworld(OverworldStep),
358    /// A step this mode's dispatch table has no name for, or a mode whose
359    /// table has not been read.
360    Numbered(u8),
361}
362
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}
401
402/// `$7EF3CA`.
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
404#[serde(rename_all = "snake_case")]
405pub enum World {
406    Light,
407    Dark,
408}
409
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}
418
419
420/// `$7EF3C5`, the save file's own account of how far the story has got. Four
421/// values, enumerated in jpdasm as `GAMESTATE` (`research/alttp-ram-map.md`
422/// §3) - the signal to drive intro automation from, because it needs no
423/// room-id table.
424#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
425#[serde(rename_all = "snake_case")]
426pub enum Progress {
427    /// The very start; progress cannot even be saved yet.
428    Beginning,
429    UncleReached,
430    ZeldaRescued,
431    AgahnimDefeated,
432    Unrecognised(u8),
433}
434
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}
458
459/// `$7EF3C6`, the opening's one-time events. jpdasm names the bits
460/// (`PROGLITE`, `research/alttp-ram-map.md` §3); these three are the cleanest
461/// checkpoints the whole game exposes for the first half hour.
462#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
463pub struct Opening {
464    /// Uncle has left the house, so Link is free to follow.
465    pub uncle_left_house: bool,
466    /// Uncle has been found in the secret passage - the sword and shield.
467    pub uncle_found: bool,
468    /// Zelda has been brought to the Sanctuary.
469    pub zelda_at_sanctuary: bool,
470}
471
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}
481
482/// `$7E005D`, Link's own state machine, as far as it has names
483/// (`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,
496    /// The rainy night the game opens on.
497    AsleepInBed,
498    Bunny,
499    Numbered(u8),
500}
501
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}
539
540/// 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 {
544    /// `$7E008A`, the overworld area.
545    Outdoors(u8),
546    /// `$7E00A0`, the indoor room - a house, a cave or a dungeon room.
547    Indoors(u16),
548}
549
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}
558
559/// 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,
565    /// `$7E0022`, pixels.
566    pub link_x: u16,
567    /// `$7E0020`, pixels.
568    pub link_y: u16,
569    /// `$7EF36D`, in eighths of a heart.
570    pub health: u8,
571    /// `$7EF36C`, in eighths of a heart.
572    pub health_capacity: u8,
573    /// `$7EF362`.
574    pub rupees: u16,
575    /// Whether the player's own buttons move Link this frame. The exact rule
576    /// the engine itself gates on, not a guess: see [`State::has_control`].
577    pub control: bool,
578    pub place: Place,
579    /// `$7E00EE`: which of a two-storey room's layers Link stands on, and
580    /// which half of the tile-attribute grid therefore describes him.
581    pub lower_level: bool,
582    /// `$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,
587    /// `$7EF359`: 0 is no sword at all, which is how the game starts.
588    pub sword: u8,
589    /// `$7EF35A`.
590    pub shield: u8,
591    /// `$7EF3CC`: who is following Link.
592    pub follower: Follower,
593}
594
595/// `$7EF3CC`, `follower_indicator` (zelda3 `src/variables.h:1121`): who is
596/// walking behind Link.
597///
598/// **This, and not `$7EF3C5`, is what freeing Zelda sets.** Her cell script
599/// writes `follower_indicator = 1` the moment she joins Link (zelda3
600/// `src/sprite_main.c:6300-6305`); the progress byte only becomes 2 when she
601/// reaches the Sanctuary (`:6342`). Reading "Zelda rescued" off the progress
602/// 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,
608    /// Another follower, by its number. Not named until a table is read.
609    Numbered(u8),
610}
611
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 {
623    /// Read the state out of work RAM. Takes the whole of it, so that no
624    /// 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    }
650
651    /// Whether the pad moves Link this frame.
652    ///
653    /// The engine's own gate, copied out of the two functions that implement
654    /// it - `Module09_00_PlayerControl` and `Module07_00_PlayerControl` guard
655    /// identically (`research/alttp-ram-map.md` §1, citing zelda3
656    /// `src/overworld.c:750-758` and `src/dungeon.c:6594-6602`). Being in a
657    /// walking mode on step zero is necessary and NOT sufficient: four flags
658    /// 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    }
670
671    /// 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}
676
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}