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}