gb: decode the whole current map into a walkability grid
The walkable predicate answered for the ten-by-nine screen window and Unknown everywhere else. MapGrid is the same rule over every tile of the loaded map: block ids out of wOverworldMap, a blocks-to-tiles read of the tileset header s blockset through the new bank-aware ROM read, the tileset s collision list as before, and the TilePairCollisionsLand values as directed walls both ways. The reader checks itself before it answers: the decode is compared against the window predicate over the player s own neighbourhood, and a frame where the window can answer for none of it -- a battle, a text box, a frame mid-warp -- is refused, because wOverworldMap shares its bytes with the picture buffer. Every refusal is named (GridRefusal) and leaves the window predicate in charge. Cached per map id and size, so a map is decoded once on arrival rather than once per question, and owned beside the session ledgers: never checkpointed.
This commit is contained in:
parent
d75b7320ea
commit
68228ef2a1
9 changed files with 1051 additions and 17 deletions
|
|
@ -32,12 +32,22 @@ pub const WALL_TILE: u8 = 0x60;
|
|||
|
||||
pub struct Wram {
|
||||
bytes: Vec<u8>,
|
||||
/// Fake cartridge banks, for the one read that needs one.
|
||||
///
|
||||
/// A bank nothing has written answers `None`, which is what a seam with no cartridge behind
|
||||
/// it answers and what the whole-map grid has to narrow on
|
||||
/// (`docs/design/macros.md` section 15).
|
||||
rom: std::collections::HashMap<(u8, u16), u8>,
|
||||
}
|
||||
|
||||
impl MemoryReader for Wram {
|
||||
fn read8(&mut self, address: u16) -> u8 {
|
||||
self.bytes[address as usize]
|
||||
}
|
||||
|
||||
fn read_rom(&mut self, bank: u8, address: u16) -> Option<u8> {
|
||||
self.rom.get(&(bank, address)).copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Wram {
|
||||
|
|
@ -49,7 +59,7 @@ impl Default for Wram {
|
|||
impl Wram {
|
||||
/// All zero: the title screen, since nothing has set the game-timer bit.
|
||||
pub fn new() -> Self {
|
||||
Self { bytes: vec![0; 0x1_0000] }
|
||||
Self { bytes: vec![0; 0x1_0000], rom: std::collections::HashMap::new() }
|
||||
}
|
||||
|
||||
pub fn set(&mut self, address: u16, value: u8) -> &mut Self {
|
||||
|
|
@ -331,6 +341,76 @@ impl Wram {
|
|||
self
|
||||
}
|
||||
|
||||
|
||||
/// The ROM bank and address this fake keeps a tileset's blockset at.
|
||||
///
|
||||
/// Any non-zero bank: the point of the number is that it is *not* bank 0, because a bank the
|
||||
/// CPU bus does not have mapped is the whole reason the seam grew
|
||||
/// [`MemoryReader::read_rom`] (`docs/design/macros.md` section 15).
|
||||
pub const BLOCKSET_BANK: u8 = 0x11;
|
||||
pub const BLOCKSET_BASE: u16 = 0x4000;
|
||||
|
||||
/// Which tileset the loaded map uses, for the tile-pair collision lists.
|
||||
pub fn tileset(&mut self, id: u8) -> &mut Self {
|
||||
self.set(ram::wCurMapTileset, id)
|
||||
}
|
||||
|
||||
/// A tileset header's blockset, written where a cartridge keeps one: sixteen tile ids per
|
||||
/// block, in block-id order, in a ROM bank that is not bank 0.
|
||||
pub fn blockset(&mut self, blocks: &[[u8; 16]]) -> &mut Self {
|
||||
for (id, block) in blocks.iter().enumerate() {
|
||||
for (offset, tile) in block.iter().enumerate() {
|
||||
let address = Self::BLOCKSET_BASE + (id * 16 + offset) as u16;
|
||||
self.rom.insert((Self::BLOCKSET_BANK, address), *tile);
|
||||
}
|
||||
}
|
||||
self.set(ram::wTilesetBank, Self::BLOCKSET_BANK)
|
||||
.set(ram::wTilesetBlocksPtr, (Self::BLOCKSET_BASE & 0xff) as u8)
|
||||
.set(ram::wTilesetBlocksPtr + 1, (Self::BLOCKSET_BASE >> 8) as u8)
|
||||
}
|
||||
|
||||
/// The loaded map's block ids, as `LoadTileBlockMap` leaves them in `wOverworldMap`: rows of
|
||||
/// `wCurMapWidth + MAP_BORDER * 2` bytes with the map itself three rows and three columns in.
|
||||
///
|
||||
/// `blocks` is row-major and `wCurMapWidth * wCurMapHeight` long; the border is left as
|
||||
/// whatever it was, exactly as a map with no connections leaves it.
|
||||
pub fn map_blocks(&mut self, blocks: &[u8]) -> &mut Self {
|
||||
let width = u16::from(self.peek(ram::wCurMapWidth));
|
||||
let height = u16::from(self.peek(ram::wCurMapHeight));
|
||||
let border = crate::pokemon_red::mapgrid::MAP_BORDER as u16;
|
||||
let stride = width + border * 2;
|
||||
for row in 0..height {
|
||||
for column in 0..width {
|
||||
let index = usize::from(row * width + column);
|
||||
let Some(block) = blocks.get(index) else { continue };
|
||||
self.set(ram::wOverworldMap + (row + border) * stride + column + border, *block);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Write the screen buffer so that it agrees with the block data, tile for tile.
|
||||
///
|
||||
/// The grid reader cross-checks its decode against the window predicate before it trusts it
|
||||
/// ([`crate::pokemon_red::state::map_grid`]), and on a cartridge the two agree because they
|
||||
/// are two readings of one map. This is that agreement in a fake: every map tile inside the
|
||||
/// ten-by-nine window gets the tile id the blockset gives it, and the tiles outside it keep
|
||||
/// whatever the screen held, which is what makes them `Unknown` to the window and answerable
|
||||
/// only by the grid.
|
||||
pub fn screen_from_blocks(&mut self, blocks: &[u8], blockset: &[[u8; 16]]) -> &mut Self {
|
||||
let width = usize::from(self.peek(ram::wCurMapWidth));
|
||||
let height = usize::from(self.peek(ram::wCurMapHeight));
|
||||
for y in 0..height * 2 {
|
||||
for x in 0..width * 2 {
|
||||
let Some(block) = blocks.get((y / 2) * width + (x / 2)) else { continue };
|
||||
let Some(tiles) = blockset.get(usize::from(*block)) else { continue };
|
||||
let tile = tiles[(y % 2) * 2 * 4 + (x % 2) * 2];
|
||||
self.map_tile(x as u8, y as u8, tile);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// A playable overworld frame: Red's ground floor, the fly standing where a cold boot's walk
|
||||
/// out of the bedroom lands it, every tile a wall until a test opens one.
|
||||
pub fn overworld() -> Self {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
use crate::adapter::PlaceKind;
|
||||
|
||||
use super::geography::Amenity;
|
||||
use super::state::{Facing, GameState};
|
||||
use super::state::{Facing, GameState, MapGrid};
|
||||
|
||||
/// `constants/pokemon_data_constants.asm`: `PARTY_LENGTH`, how many Pokémon fit in the party.
|
||||
///
|
||||
|
|
@ -315,6 +315,23 @@ pub trait MacroState: GameState {
|
|||
false
|
||||
}
|
||||
|
||||
/// The whole loaded map's walkability, when the cartridge's tables can be decoded.
|
||||
///
|
||||
/// `docs/design/macros.md` section 15, the operator 2026-09-22: "the frontier and warp macros
|
||||
/// need to be map aware: A* over walkable tiles." [`GameState::walkable`] answers for the
|
||||
/// ten-by-nine window of the screen buffer and [`super::state::Walkable::Unknown`] for
|
||||
/// everything else, so before this every walk planned through guesses, re-planned at every
|
||||
/// window edge, and `GO FRONTIER` aimed at whatever unstood ground happened to be on screen.
|
||||
///
|
||||
/// The default is `None`, which is this trait's usual narrowing and here it is also the
|
||||
/// documented fallback: [`super::path::route`] and [`super::path::frontier`] use the window
|
||||
/// predicate when the grid is absent, exactly as they did before, and
|
||||
/// [`crate::pokemon_red::state::GridRefusal`] is what says why it is absent on a frame the
|
||||
/// reader could not decode.
|
||||
fn map_grid(&mut self) -> Option<std::sync::Arc<MapGrid>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Whether this run has already been into `area`'s mart or Pokémon Center.
|
||||
///
|
||||
/// Section 13's `areaVisited(kind, area)`: **one visit per area per run**, so the errand is
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use crate::macros::{
|
|||
MacroPalette, Observed, Outcome, PaletteMode, RunLedger, SLOTS, SceneId, SlotBinding, Started,
|
||||
};
|
||||
|
||||
use super::super::mapgrid::MapGrids;
|
||||
use super::super::state::PokeState;
|
||||
use super::cartridge::{Areas, MacroState, Pushed, Stood, Talked, Targets, Tile};
|
||||
use super::geography;
|
||||
|
|
@ -70,6 +71,14 @@ pub struct PokemonPalette {
|
|||
/// and walks the fly down on every frame it stands there until the Pokédex exists, and a map
|
||||
/// does not stop being like that ten brain minutes later.
|
||||
pushed: Pushed,
|
||||
/// The decoded walkability of the map the fly is on (`docs/design/macros.md` section 15).
|
||||
///
|
||||
/// Owned here beside the session ledgers because it is the same shape of thing: built from
|
||||
/// the cartridge, valid for as long as the map is loaded, dropped on arrival somewhere else,
|
||||
/// and never checkpointed -- a restored run decodes the map again on its first overworld
|
||||
/// frame. It is a *cache* rather than a ledger: nothing about the run is in it, only what the
|
||||
/// cartridge's own tables say about the ground.
|
||||
grids: MapGrids,
|
||||
/// The brain clock of the frame being decided, from [`MacroPalette::clock`].
|
||||
///
|
||||
/// The blocked ledger is a *window*, so it needs the same clock the loop publishes rather
|
||||
|
|
@ -93,6 +102,7 @@ impl PokemonPalette {
|
|||
stood: Stood::default(),
|
||||
areas: Areas::default(),
|
||||
pushed: Pushed::default(),
|
||||
grids: MapGrids::default(),
|
||||
now_ms: 0.0,
|
||||
}
|
||||
}
|
||||
|
|
@ -158,11 +168,13 @@ impl MacroPalette for PokemonPalette {
|
|||
|
||||
fn observe(&mut self, memory: &mut dyn MemoryReader, ledger: &dyn RunLedger) -> Observed {
|
||||
let (scene, bindings, standing) = {
|
||||
let Self { machine, mode, palette: cached, talked, targets, stood, areas, pushed, .. } =
|
||||
self;
|
||||
let Self {
|
||||
machine, mode, palette: cached, talked, targets, stood, areas, pushed, grids, ..
|
||||
} = self;
|
||||
let mut state = PokeState::with_ledgers(
|
||||
memory, ledger, talked, targets, &*stood, &*areas, &*pushed,
|
||||
);
|
||||
)
|
||||
.caching_grid(grids);
|
||||
// `GameState::scene` is `pokemon_red::scene::detect` over the same reader, so the
|
||||
// palette and the scene the feed reports cannot disagree about which frame they are
|
||||
// for.
|
||||
|
|
@ -227,7 +239,8 @@ impl MacroPalette for PokemonPalette {
|
|||
&self.stood,
|
||||
&self.areas,
|
||||
&self.pushed,
|
||||
);
|
||||
)
|
||||
.caching_grid(&mut self.grids);
|
||||
self.machine.start(&palette, slot, &mut state)
|
||||
};
|
||||
let started = match begun {
|
||||
|
|
@ -267,7 +280,8 @@ impl MacroPalette for PokemonPalette {
|
|||
&self.stood,
|
||||
&self.areas,
|
||||
&self.pushed,
|
||||
);
|
||||
)
|
||||
.caching_grid(&mut self.grids);
|
||||
self.machine.step(&mut state)
|
||||
};
|
||||
// A macro that just finished may have been the press that talked to something; the ledger
|
||||
|
|
|
|||
|
|
@ -392,6 +392,179 @@ impl Walkable {
|
|||
}
|
||||
}
|
||||
|
||||
/// Every tile of one map, answered: `docs/design/macros.md` section 15.
|
||||
///
|
||||
/// [`GameState::walkable`] answers about the ten-by-nine window of the screen buffer and
|
||||
/// [`Walkable::Unknown`] elsewhere. This is the same predicate over the whole loaded map, decoded
|
||||
/// from the block and collision tables the cartridge has loaded
|
||||
/// ([`crate::pokemon_red::mapgrid`]), so a walk can be planned once instead of guessed at and
|
||||
/// re-planned at every window edge.
|
||||
///
|
||||
/// It carries three things and no policy at all:
|
||||
///
|
||||
/// - the walkability of every tile, in map-tile coordinates -- the same unit the player's
|
||||
/// coordinates, the warp table and the sign table are in;
|
||||
/// - the tile id each answer came from, which is what the cross-check against the window
|
||||
/// predicate compares and what the tile-pair rules are keyed on;
|
||||
/// - **directed walls**: one step out of one tile in one direction that the cartridge refuses
|
||||
/// although both tiles are passable. Pokered has two such rules and the tile-pair lists are the
|
||||
/// one that can be read ahead of time; a ledge is already [`Walkable::No`] in the collision
|
||||
/// list, and a person in the way is the sprite list's answer, not the ground's.
|
||||
///
|
||||
/// Nothing here is a fact about the run: no ledger, no visit, no target. Those stay where they
|
||||
/// are, session state in the executor layer.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MapGrid {
|
||||
map: u8,
|
||||
width: u8,
|
||||
height: u8,
|
||||
/// Row-major, `width * height`, [`Walkable::Unknown`] until [`MapGrid::set`] says otherwise.
|
||||
tiles: Vec<Walkable>,
|
||||
/// Row-major screen tile id per map tile, `None` where the tile was never decoded.
|
||||
ids: Vec<Option<u8>>,
|
||||
/// Row-major bitmask of the directions a step out of this tile is refused in
|
||||
/// ([`MapGrid::wall`]).
|
||||
walls: Vec<u8>,
|
||||
}
|
||||
|
||||
impl MapGrid {
|
||||
/// An all-[`Walkable::Unknown`] grid of this size, for a decoder to fill in.
|
||||
pub fn new(map: u8, width: u8, height: u8) -> Self {
|
||||
let cells = usize::from(width) * usize::from(height);
|
||||
Self {
|
||||
map,
|
||||
width,
|
||||
height,
|
||||
tiles: vec![Walkable::Unknown; cells],
|
||||
ids: vec![None; cells],
|
||||
walls: vec![0; cells],
|
||||
}
|
||||
}
|
||||
|
||||
/// Which map this grid is of. A grid is only ever valid for the loaded map.
|
||||
pub fn map(&self) -> u8 {
|
||||
self.map
|
||||
}
|
||||
|
||||
pub fn width(&self) -> u8 {
|
||||
self.width
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u8 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn index(&self, x: u8, y: u8) -> Option<usize> {
|
||||
(x < self.width && y < self.height)
|
||||
.then(|| usize::from(y) * usize::from(self.width) + usize::from(x))
|
||||
}
|
||||
|
||||
/// Record a decoded tile: its screen tile id and what the collision list makes of it.
|
||||
pub fn set(&mut self, x: u8, y: u8, tile: u8, walkable: Walkable) {
|
||||
if let Some(index) = self.index(x, y) {
|
||||
self.tiles[index] = walkable;
|
||||
self.ids[index] = Some(tile);
|
||||
}
|
||||
}
|
||||
|
||||
/// Record that the cartridge refuses the step out of `(x, y)` in `facing`.
|
||||
pub fn wall(&mut self, x: u8, y: u8, facing: Facing) {
|
||||
if let Some(index) = self.index(x, y) {
|
||||
self.walls[index] |= wall_bit(facing);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the player could stand on this tile. Off the map is [`Walkable::No`], which is the
|
||||
/// window predicate's own answer for it.
|
||||
pub fn walkable(&self, x: u8, y: u8) -> Walkable {
|
||||
match self.index(x, y) {
|
||||
None => Walkable::No,
|
||||
Some(index) => self.tiles[index],
|
||||
}
|
||||
}
|
||||
|
||||
/// The screen tile id this tile's answer came from, or `None` for a tile never decoded.
|
||||
pub fn tile_id(&self, x: u8, y: u8) -> Option<u8> {
|
||||
self.index(x, y).and_then(|index| self.ids[index])
|
||||
}
|
||||
|
||||
/// Whether the step out of `(x, y)` in `facing` is one the cartridge refuses.
|
||||
pub fn walled(&self, x: u8, y: u8, facing: Facing) -> bool {
|
||||
self.index(x, y).is_some_and(|index| self.walls[index] & wall_bit(facing) != 0)
|
||||
}
|
||||
|
||||
/// How many tiles of the map the player could stand on.
|
||||
pub fn walkable_count(&self) -> usize {
|
||||
self.tiles.iter().filter(|tile| tile.is_walkable()).count()
|
||||
}
|
||||
|
||||
/// How many tiles the map has that were never decoded, i.e. [`Walkable::Unknown`].
|
||||
pub fn unknown_count(&self) -> usize {
|
||||
self.tiles.iter().filter(|tile| matches!(tile, Walkable::Unknown)).count()
|
||||
}
|
||||
|
||||
/// Every walkable tile, in row-major order, as `(x, y)`.
|
||||
pub fn walkable_tiles(&self) -> Vec<(u8, u8)> {
|
||||
let mut out = Vec::with_capacity(self.walkable_count());
|
||||
for y in 0..self.height {
|
||||
for x in 0..self.width {
|
||||
if self.walkable(x, y).is_walkable() {
|
||||
out.push((x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// How many walkable tiles a walk from `(x, y)` could reach, the directed walls respected.
|
||||
///
|
||||
/// The starting tile counts whether or not it is walkable, for the same reason the route
|
||||
/// search treats it as passable: the fly is standing on it. What this number is *for* is
|
||||
/// reading a stalled walk at a glance -- a fly on a route with 600 walkable tiles and 4
|
||||
/// reachable ones is fenced in, and no amount of re-planning is going to help it
|
||||
/// (`docs/design/macros.md` section 15, `examples/scene_probe.rs`).
|
||||
pub fn reachable_from(&self, x: u8, y: u8) -> usize {
|
||||
if self.index(x, y).is_none() {
|
||||
return 0;
|
||||
}
|
||||
let mut seen = vec![false; self.tiles.len()];
|
||||
let mut queue = std::collections::VecDeque::new();
|
||||
if let Some(index) = self.index(x, y) {
|
||||
seen[index] = true;
|
||||
}
|
||||
queue.push_back((x, y));
|
||||
let mut count = 0;
|
||||
while let Some((tx, ty)) = queue.pop_front() {
|
||||
count += 1;
|
||||
for facing in [Facing::Up, Facing::Down, Facing::Left, Facing::Right] {
|
||||
if self.walled(tx, ty, facing) {
|
||||
continue;
|
||||
}
|
||||
let (dx, dy) = facing.delta();
|
||||
let Ok(nx) = u8::try_from(i16::from(tx) + dx) else { continue };
|
||||
let Ok(ny) = u8::try_from(i16::from(ty) + dy) else { continue };
|
||||
let Some(index) = self.index(nx, ny) else { continue };
|
||||
if seen[index] || !self.tiles[index].is_walkable() {
|
||||
continue;
|
||||
}
|
||||
seen[index] = true;
|
||||
queue.push_back((nx, ny));
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
/// Which bit of a [`MapGrid`] wall mask a direction is.
|
||||
fn wall_bit(facing: Facing) -> u8 {
|
||||
match facing {
|
||||
Facing::Up => 1,
|
||||
Facing::Down => 2,
|
||||
Facing::Left => 4,
|
||||
Facing::Right => 8,
|
||||
}
|
||||
}
|
||||
|
||||
/// One entry of the current map's warp table: a door, a staircase, a cave mouth.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Warp {
|
||||
|
|
|
|||
246
services/flysim/crates/flybrain-gb/src/pokemon_red/mapgrid.rs
Normal file
246
services/flysim/crates/flybrain-gb/src/pokemon_red/mapgrid.rs
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
//! The whole current map's walkability, decoded from the tables the cartridge has loaded.
|
||||
//!
|
||||
//! `docs/design/macros.md` section 15, the operator 2026-09-22: "the frontier and warp macros need
|
||||
//! to be map aware: A* over walkable tiles." [`super::state::walkable`] answers for the ten-by-nine
|
||||
//! window of the screen buffer and [`super::macros::state::Walkable::Unknown`] for everything
|
||||
//! else, which is honest and is also why every walk planned through guesses, re-planned at each
|
||||
//! window edge, and called "frontier" whatever unstood ground happened to be on screen.
|
||||
//!
|
||||
//! This module is the same predicate over the whole map. Nothing about the *rule* changes -- a tile
|
||||
//! is walkable when the current tileset's collision list holds its tile id, which is
|
||||
//! `CheckTilePassable` -- what changes is where the tile id comes from:
|
||||
//!
|
||||
//! | what | where the cartridge keeps it | how it is read |
|
||||
//! | --- | --- | --- |
|
||||
//! | the map's blocks | `wOverworldMap`, one byte per 4x4-tile block, rows of `width + MAP_BORDER * 2` with the map three rows and three columns in (`LoadTileBlockMap`) | WRAM, through the ordinary reader |
|
||||
//! | a block's tiles | the tileset header's blockset, 16 bytes per block id, four rows of four tile ids (`DrawTileBlock`) | ROM, through [`crate::adapter::MemoryReader::read_rom`], because the blockset is not in bank 0 |
|
||||
//! | which tiles are passable | `wTilesetCollisionPtr`, a `$ff`-terminated list in bank 0 | WRAM pointer, ROM bank 0 read, exactly as the window predicate already did |
|
||||
//! | which steps are refused between two passable tiles | `TilePairCollisionsLand`, keyed by `wCurMapTileset` | the table's values, quoted below |
|
||||
//!
|
||||
//! Two coordinate systems meet here and keeping them apart is the whole of the arithmetic. The
|
||||
//! cartridge's *blocks* are 4x4 screen tiles; the player moves in *map tiles* of 2x2 screen tiles,
|
||||
//! which is the unit `wXCoord`, the warp table and everything in `macros/` is in. So one block is
|
||||
//! [`TILES_PER_BLOCK`] map tiles each way, and the screen tile a map tile's walkability is read
|
||||
//! from is the top left of its 2x2 quadrant -- the same corner `_GetTileAndCoordsInFrontOfPlayer`
|
||||
//! reads at screen `(8, 9)` for the tile the player stands on, which is what
|
||||
//! [`super::state::map_grid`]'s cross-check against the window predicate proves on a cartridge.
|
||||
//!
|
||||
//! What it does **not** model is what it did not model before: sprites standing on ground (the
|
||||
//! sprite list answers that, and [`super::macros::path::frontier`] reads it), warps that fire on
|
||||
//! the step onto them, and scripts that push the fly off a tile (a session ledger answers that).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::macros::state::{Facing, MapGrid, Walkable};
|
||||
|
||||
/// Screen tiles across and down one block: `BLOCK_WIDTH` and `BLOCK_HEIGHT`
|
||||
/// (`constants/gfx_constants.asm`).
|
||||
pub const BLOCK_TILES: usize = 4;
|
||||
|
||||
/// Bytes one block takes in a tileset's blockset: its sixteen tile ids, four rows of four.
|
||||
pub const BLOCK_BYTES: usize = BLOCK_TILES * BLOCK_TILES;
|
||||
|
||||
/// Blocks of border `wOverworldMap` keeps on every side: `MAP_BORDER`
|
||||
/// (`constants/map_data_constants.asm`), which is what lets the view centre on a map smaller than
|
||||
/// the screen and what a map's own blocks are offset by.
|
||||
pub const MAP_BORDER: usize = 3;
|
||||
|
||||
/// Map tiles across one block. A block is four screen tiles and the player walks two at a time,
|
||||
/// which is why `wCurMapWidth` in blocks is `map_size().width` in tiles divided by this.
|
||||
pub const TILES_PER_BLOCK: u8 = 2;
|
||||
|
||||
/// Tileset ids the tile-pair lists name (`constants/tileset_constants.asm`, counted in the order
|
||||
/// that file declares them: OVERWORLD 0 … FOREST 3 … CAVERN 17).
|
||||
pub mod tileset {
|
||||
pub const FOREST: u8 = 3;
|
||||
pub const CAVERN: u8 = 17;
|
||||
}
|
||||
|
||||
/// `TilePairCollisionsLand` at the pinned pokered commit, as `(tileset, one tile, the other)`.
|
||||
///
|
||||
/// `data/tilesets/pair_collision_tile_ids.asm`. Values rather than an address, like the item
|
||||
/// prices in [`super::macros::cartridge`]: what the cartridge keeps here is eleven triples, and
|
||||
/// the file they came from is quoted so a wrong one is a review comment rather than a mystery.
|
||||
///
|
||||
/// `CheckForTilePairCollisions` walks the list comparing the tile the player stands on against
|
||||
/// *either* member of the pair and the tile in front against the other, so the refusal is
|
||||
/// symmetric -- a forest's tree line refuses the step in both directions -- and each triple
|
||||
/// becomes two directed walls. Both tiles are passable on their own, which is why nothing in the
|
||||
/// collision list can predict it and why the walk used to learn it one refusal at a time
|
||||
/// ([`super::macros::path::Refusal`]).
|
||||
///
|
||||
/// `TilePairCollisionsWater` is deliberately absent: it is the list
|
||||
/// `CheckForJumpingAndTilePairCollisions` uses while surfing, the fly has no Surf, and a rule for
|
||||
/// a movement mode the palette cannot enter is not knowledge this grid should carry.
|
||||
pub const TILE_PAIRS_LAND: [(u8, u8, u8); 11] = [
|
||||
(tileset::CAVERN, 0x20, 0x05),
|
||||
(tileset::CAVERN, 0x41, 0x05),
|
||||
(tileset::FOREST, 0x30, 0x2e),
|
||||
(tileset::CAVERN, 0x2a, 0x05),
|
||||
(tileset::CAVERN, 0x05, 0x21),
|
||||
(tileset::FOREST, 0x52, 0x2e),
|
||||
(tileset::FOREST, 0x55, 0x2e),
|
||||
(tileset::FOREST, 0x56, 0x2e),
|
||||
(tileset::FOREST, 0x20, 0x2e),
|
||||
(tileset::FOREST, 0x5e, 0x2e),
|
||||
(tileset::FOREST, 0x5f, 0x2e),
|
||||
];
|
||||
|
||||
/// The tileset's two tables, as the decoder needs them.
|
||||
///
|
||||
/// Gathered by [`super::state::map_grid`] from WRAM and ROM; separate from the decoding so that
|
||||
/// the decoding is a pure function of bytes and can be tested against a made-up tileset.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Tileset {
|
||||
/// `wCurMapTileset`: which tileset, for the tile-pair lists.
|
||||
pub id: u8,
|
||||
/// The blockset: sixteen tile ids per block id, in block-id order from zero.
|
||||
pub blocks: Vec<u8>,
|
||||
/// The `$ff`-terminated passable-tile list, terminator included or not.
|
||||
pub passable: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Tileset {
|
||||
/// The screen tile id at `(column, row)` of block `block`, or `None` past the blockset.
|
||||
fn tile(&self, block: u8, column: usize, row: usize) -> Option<u8> {
|
||||
let offset = usize::from(block) * BLOCK_BYTES + row * BLOCK_TILES + column;
|
||||
self.blocks.get(offset).copied()
|
||||
}
|
||||
|
||||
/// Whether the collision list holds `tile`, which is `CheckTilePassable` and nothing else.
|
||||
fn passable(&self, tile: u8) -> bool {
|
||||
for candidate in &self.passable {
|
||||
if *candidate == TERMINATOR {
|
||||
return false;
|
||||
}
|
||||
if *candidate == tile {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// The `$ff` that ends a collision list.
|
||||
pub const TERMINATOR: u8 = 0xff;
|
||||
|
||||
/// Decode the whole map into a [`MapGrid`].
|
||||
///
|
||||
/// `blocks` is the map's block ids, row-major, `width_blocks * height_blocks` of them, already
|
||||
/// lifted out of `wOverworldMap`'s bordered rows. Every map tile gets one answer:
|
||||
/// [`Walkable::Yes`] or [`Walkable::No`] from the collision list, and [`Walkable::Unknown`] only
|
||||
/// for a tile whose block id is past the end of the blockset -- which is a table that was read
|
||||
/// short rather than a tile the game is unsure about.
|
||||
pub fn decode(map: u8, width_blocks: u8, height_blocks: u8, blocks: &[u8], tiles: &Tileset) -> MapGrid {
|
||||
let width = width_blocks.saturating_mul(TILES_PER_BLOCK);
|
||||
let height = height_blocks.saturating_mul(TILES_PER_BLOCK);
|
||||
let mut grid = MapGrid::new(map, width, height);
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let block_index =
|
||||
usize::from(y / TILES_PER_BLOCK) * usize::from(width_blocks)
|
||||
+ usize::from(x / TILES_PER_BLOCK);
|
||||
let Some(block) = blocks.get(block_index).copied() else {
|
||||
continue;
|
||||
};
|
||||
// The top left screen tile of the map tile's own 2x2 quadrant of the block, which is
|
||||
// the corner every collision read in the game uses.
|
||||
let column = usize::from(x % TILES_PER_BLOCK) * usize::from(TILES_PER_BLOCK);
|
||||
let row = usize::from(y % TILES_PER_BLOCK) * usize::from(TILES_PER_BLOCK);
|
||||
let Some(tile) = tiles.tile(block, column, row) else {
|
||||
continue;
|
||||
};
|
||||
grid.set(x, y, tile, if tiles.passable(tile) { Walkable::Yes } else { Walkable::No });
|
||||
}
|
||||
}
|
||||
add_pair_walls(&mut grid, tiles.id);
|
||||
grid
|
||||
}
|
||||
|
||||
/// Turn every tile-pair collision the loaded tileset has into two directed walls.
|
||||
fn add_pair_walls(grid: &mut MapGrid, tileset: u8) {
|
||||
let pairs: Vec<(u8, u8)> = TILE_PAIRS_LAND
|
||||
.iter()
|
||||
.filter(|(id, _, _)| *id == tileset)
|
||||
.map(|(_, one, other)| (*one, *other))
|
||||
.collect();
|
||||
if pairs.is_empty() {
|
||||
return;
|
||||
}
|
||||
for y in 0..grid.height() {
|
||||
for x in 0..grid.width() {
|
||||
let Some(here) = grid.tile_id(x, y) else { continue };
|
||||
for facing in [Facing::Down, Facing::Right] {
|
||||
let (dx, dy) = facing.delta();
|
||||
let Some(nx) = checked_step(x, dx) else { continue };
|
||||
let Some(ny) = checked_step(y, dy) else { continue };
|
||||
if nx >= grid.width() || ny >= grid.height() {
|
||||
continue;
|
||||
}
|
||||
let Some(there) = grid.tile_id(nx, ny) else { continue };
|
||||
if pairs.iter().any(|(one, other)| {
|
||||
(*one == here && *other == there) || (*one == there && *other == here)
|
||||
}) {
|
||||
grid.wall(x, y, facing);
|
||||
grid.wall(nx, ny, opposite(facing));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn checked_step(value: u8, delta: i16) -> Option<u8> {
|
||||
u8::try_from(i16::from(value) + delta).ok()
|
||||
}
|
||||
|
||||
fn opposite(facing: Facing) -> Facing {
|
||||
match facing {
|
||||
Facing::Down => Facing::Up,
|
||||
Facing::Up => Facing::Down,
|
||||
Facing::Left => Facing::Right,
|
||||
Facing::Right => Facing::Left,
|
||||
}
|
||||
}
|
||||
|
||||
/// The decoded grid of the map that is loaded, kept for as long as it is the loaded one.
|
||||
///
|
||||
/// One slot, keyed by map id and size: arriving on another map drops it, and so does a map whose
|
||||
/// header reads a different size, because both mean the block data under it has been replaced.
|
||||
/// A decode is a few thousand WRAM reads and a walk of the blockset, so it happens once per
|
||||
/// arrival rather than once per plan -- and never per frame, which is what a precondition asking
|
||||
/// for the frontier would otherwise cost.
|
||||
///
|
||||
/// Session state like the other ledgers of `docs/design/macros.md` section 12: owned by
|
||||
/// [`super::macros::driver::PokemonPalette`], never checkpointed, and rebuilt from the cartridge
|
||||
/// on the first frame after a restore.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MapGrids {
|
||||
current: Option<Arc<MapGrid>>,
|
||||
}
|
||||
|
||||
impl MapGrids {
|
||||
/// The cached grid for `map` at this size, or `None` when the cache is for somewhere else.
|
||||
///
|
||||
/// Handed out behind an [`Arc`] so that a caller that asks once a frame -- a precondition
|
||||
/// wanting to know whether the frontier is empty -- pays a refcount rather than a copy of the
|
||||
/// map.
|
||||
pub fn get(&self, map: u8, width: u8, height: u8) -> Option<Arc<MapGrid>> {
|
||||
self.current
|
||||
.as_ref()
|
||||
.filter(|grid| grid.map() == map && grid.width() == width && grid.height() == height)
|
||||
.map(Arc::clone)
|
||||
}
|
||||
|
||||
/// Keep `grid`, dropping whatever map the cache held before.
|
||||
pub fn store(&mut self, grid: MapGrid) -> Arc<MapGrid> {
|
||||
Arc::clone(self.current.insert(Arc::new(grid)))
|
||||
}
|
||||
|
||||
/// Which map the cache is holding, for a log line and the tests.
|
||||
pub fn held(&self) -> Option<u8> {
|
||||
self.current.as_ref().map(|grid| grid.map())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
//! The decoder, against a made-up tileset.
|
||||
//!
|
||||
//! Every byte here is synthetic on purpose: a block table, a blockset and a collision list of this
|
||||
//! module's own making, so a failure says which of the three readings is wrong rather than "the
|
||||
//! cartridge disagrees". The cartridge half — that the decode matches real presses on Pallet Town
|
||||
//! and Viridian Forest — is `tests/rom_map_grid.rs`.
|
||||
|
||||
use super::*;
|
||||
use crate::pokemon_red::macros::state::{Facing, Walkable};
|
||||
|
||||
/// A blockset with three blocks: all floor, all wall, and one whose four map-tile quadrants are
|
||||
/// four different tile ids.
|
||||
///
|
||||
/// A block is four screen tiles each way and a map tile is two, so the quadrants are the four
|
||||
/// corners and the tile a map tile's walkability comes from is the top left of its own quadrant.
|
||||
fn blockset() -> Vec<u8> {
|
||||
let floor = [FLOOR; 16];
|
||||
let wall = [WALL; 16];
|
||||
let quadrants = [
|
||||
NORTH_WEST, 0x90, NORTH_EAST, 0x91, //
|
||||
0x92, 0x93, 0x94, 0x95, //
|
||||
SOUTH_WEST, 0x96, SOUTH_EAST, 0x97, //
|
||||
0x98, 0x99, 0x9a, 0x9b,
|
||||
];
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(&floor);
|
||||
out.extend_from_slice(&wall);
|
||||
out.extend_from_slice(&quadrants);
|
||||
out
|
||||
}
|
||||
|
||||
const FLOOR: u8 = 0x01;
|
||||
const WALL: u8 = 0x60;
|
||||
const NORTH_WEST: u8 = 0x11;
|
||||
const NORTH_EAST: u8 = 0x12;
|
||||
const SOUTH_WEST: u8 = 0x13;
|
||||
const SOUTH_EAST: u8 = 0x14;
|
||||
|
||||
/// Floor and the four quadrant tiles are passable; the wall tile is in no list.
|
||||
fn tileset(id: u8) -> Tileset {
|
||||
Tileset {
|
||||
id,
|
||||
blocks: blockset(),
|
||||
passable: vec![FLOOR, NORTH_WEST, NORTH_EAST, SOUTH_WEST, SOUTH_EAST, TERMINATOR],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_block_becomes_four_map_tiles_from_its_four_quadrants() {
|
||||
let grid = decode(7, 1, 1, &[2], &tileset(0));
|
||||
assert_eq!((grid.map(), grid.width(), grid.height()), (7, 2, 2));
|
||||
assert_eq!(grid.tile_id(0, 0), Some(NORTH_WEST));
|
||||
assert_eq!(grid.tile_id(1, 0), Some(NORTH_EAST));
|
||||
assert_eq!(grid.tile_id(0, 1), Some(SOUTH_WEST));
|
||||
assert_eq!(grid.tile_id(1, 1), Some(SOUTH_EAST));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_collision_list_answers_every_tile_of_a_map_larger_than_the_window() {
|
||||
// Ten blocks by nine is twenty tiles by eighteen: wider than the ten-by-nine window the
|
||||
// screen buffer can answer for, which is the point of the whole grid.
|
||||
let (wide, high) = (10u8, 9u8);
|
||||
let mut blocks = vec![0u8; usize::from(wide) * usize::from(high)];
|
||||
// A wall down the middle column of blocks.
|
||||
for row in 0..usize::from(high) {
|
||||
blocks[row * usize::from(wide) + 5] = 1;
|
||||
}
|
||||
let grid = decode(0, wide, high, &blocks, &tileset(0));
|
||||
assert_eq!((grid.width(), grid.height()), (20, 18));
|
||||
assert_eq!(grid.walkable(0, 0), Walkable::Yes);
|
||||
assert_eq!(grid.walkable(19, 17), Walkable::Yes, "the far corner, which no window reaches");
|
||||
assert_eq!(grid.walkable(10, 9), Walkable::No);
|
||||
assert_eq!(grid.walkable(11, 9), Walkable::No);
|
||||
assert_eq!(grid.unknown_count(), 0);
|
||||
assert_eq!(grid.walkable_count(), 20 * 18 - 18 * 2);
|
||||
// Off the map is not a tile to stand on, which is the window predicate's own answer for it.
|
||||
assert_eq!(grid.walkable(20, 0), Walkable::No);
|
||||
assert_eq!(grid.walkable(0, 18), Walkable::No);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tile_pair_collision_is_a_wall_in_both_directions_and_only_in_its_own_tileset() {
|
||||
// `FOREST, $30, $2E`: two tiles that are each passable on their own and that the cartridge
|
||||
// refuses the step between (`data/tilesets/pair_collision_tile_ids.asm`).
|
||||
let (one, other) = (0x30, 0x2e);
|
||||
let tiles = Tileset {
|
||||
id: tileset::FOREST,
|
||||
blocks: [[one; 16], [other; 16]].concat(),
|
||||
passable: vec![one, other, TERMINATOR],
|
||||
};
|
||||
let grid = decode(0, 2, 1, &[0, 1], &tiles);
|
||||
assert_eq!(grid.walkable(1, 0), Walkable::Yes);
|
||||
assert_eq!(grid.walkable(2, 0), Walkable::Yes);
|
||||
assert!(grid.walled(1, 0, Facing::Right), "the step onto the other tile");
|
||||
assert!(grid.walled(2, 0, Facing::Left), "and the step back, which is the same rule");
|
||||
assert!(!grid.walled(0, 0, Facing::Right), "two tiles of the same id are not a pair");
|
||||
|
||||
// The same two tile ids in a tileset the list does not name are ordinary ground.
|
||||
let elsewhere = Tileset { id: tileset::CAVERN, ..tiles };
|
||||
let grid = decode(0, 2, 1, &[0, 1], &elsewhere);
|
||||
assert!(!grid.walled(1, 0, Facing::Right));
|
||||
assert!(!grid.walled(2, 0, Facing::Left));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_block_id_the_blockset_does_not_reach_stays_unknown() {
|
||||
// A blockset read short — a header pointer near the end of its bank — is not a tile the game
|
||||
// is unsure about, and the search prices `Unknown` as plausible ground rather than refusing
|
||||
// it, so saying so is the honest answer.
|
||||
let grid = decode(0, 2, 1, &[0, 9], &tileset(0));
|
||||
assert_eq!(grid.walkable(0, 0), Walkable::Yes);
|
||||
assert_eq!(grid.walkable(2, 0), Walkable::Unknown);
|
||||
assert_eq!(grid.tile_id(2, 0), None);
|
||||
// Block 9 is one block, which is four map tiles: two rows of two.
|
||||
assert_eq!(grid.unknown_count(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reachable_from_counts_what_a_walk_can_get_to_and_not_what_it_can_see() {
|
||||
// Two floor blocks with a wall block between them: eight walkable tiles, four of them fenced
|
||||
// off from the fly. This is the number that tells a stalled walk from a long one.
|
||||
let grid = decode(0, 3, 1, &[0, 1, 0], &tileset(0));
|
||||
assert_eq!(grid.walkable_count(), 8);
|
||||
assert_eq!(grid.reachable_from(0, 0), 4);
|
||||
assert_eq!(grid.reachable_from(4, 0), 4);
|
||||
|
||||
// With the wall gone, the whole row is one region.
|
||||
let grid = decode(0, 3, 1, &[0, 0, 0], &tileset(0));
|
||||
assert_eq!(grid.reachable_from(0, 0), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_directed_wall_fences_a_region_off_for_the_reachable_count_too() {
|
||||
let (one, other) = (0x30, 0x2e);
|
||||
let tiles = Tileset {
|
||||
id: tileset::FOREST,
|
||||
blocks: [[one; 16], [other; 16]].concat(),
|
||||
passable: vec![one, other, TERMINATOR],
|
||||
};
|
||||
// A column of `$30` and a column of `$2e`, four tiles each, with the tile-pair rule between
|
||||
// every pair of them: every tile is walkable and half of them are unreachable.
|
||||
let grid = decode(0, 2, 1, &[0, 1], &tiles);
|
||||
assert_eq!(grid.walkable_count(), 8);
|
||||
assert_eq!(grid.reachable_from(0, 0), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_cache_holds_one_map_and_drops_it_on_arrival_somewhere_else() {
|
||||
let mut grids = MapGrids::default();
|
||||
assert_eq!(grids.held(), None);
|
||||
let stored = grids.store(decode(3, 2, 2, &[0, 0, 0, 0], &tileset(0)));
|
||||
assert_eq!(grids.held(), Some(3));
|
||||
assert_eq!(grids.get(3, 4, 4).as_deref(), Some(&*stored));
|
||||
// The same map at another size is another map's block data under the same id, which is what a
|
||||
// half-loaded header looks like.
|
||||
assert!(grids.get(3, 8, 8).is_none());
|
||||
assert!(grids.get(4, 4, 4).is_none());
|
||||
grids.store(decode(4, 1, 1, &[0], &tileset(0)));
|
||||
assert_eq!(grids.held(), Some(4));
|
||||
assert!(grids.get(3, 4, 4).is_none());
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ pub mod catalog;
|
|||
#[cfg(test)]
|
||||
pub(crate) mod fake_wram;
|
||||
pub mod macros;
|
||||
pub mod mapgrid;
|
||||
pub mod maps;
|
||||
pub mod scene;
|
||||
pub mod state;
|
||||
|
|
@ -343,6 +344,13 @@ impl MemoryReader for SampleCache<'_> {
|
|||
self.bytes.insert(address, value);
|
||||
value
|
||||
}
|
||||
|
||||
/// Straight through, uncached: a ROM byte cannot change, so there is nothing
|
||||
/// for a per-sample cache to save, and the caller that reads a blockset
|
||||
/// (`docs/design/macros.md` section 15) caches the decoded map instead.
|
||||
fn read_rom(&mut self, bank: u8, address: u16) -> Option<u8> {
|
||||
self.source.read_rom(bank, address)
|
||||
}
|
||||
}
|
||||
|
||||
fn word(memory: &mut impl MemoryReader, address: u16) -> u32 {
|
||||
|
|
|
|||
|
|
@ -29,9 +29,10 @@ use super::macros::cartridge::{
|
|||
Objective, PushedLedger, StoodLedger, TalkLedger, TalkTarget, TargetKey, TargetLedger, Tile,
|
||||
};
|
||||
use super::macros::geography::Amenity;
|
||||
use super::mapgrid::{self, MapGrids};
|
||||
use super::macros::state::{
|
||||
BagItem, Battle, BattleKind, BattleMenu, Connections, Cursor, EnemyMon, Facing, GameState,
|
||||
MapSize, Mon, Move, Npc, Party, Pc, Player, Scene, Shop, ShopScreen, Sign, StartMenu,
|
||||
MapGrid, MapSize, Mon, Move, Npc, Party, Pc, Player, Scene, Shop, ShopScreen, Sign, StartMenu,
|
||||
Status, TextBox, Walkable, Warp,
|
||||
};
|
||||
use super::symbols::ram;
|
||||
|
|
@ -775,12 +776,16 @@ pub fn npcs(memory: &mut dyn MemoryReader) -> Vec<Npc> {
|
|||
npcs
|
||||
}
|
||||
|
||||
/// Whether the current tileset calls this tile id passable.
|
||||
/// The current tileset's list of passable tile ids, terminator included.
|
||||
///
|
||||
/// `CheckTilePassable` walks the list at `wTilesetCollisionPtr` — a little-endian pointer into the
|
||||
/// collision tables, which all live in ROM bank 0 at this commit and so are always mapped — until
|
||||
/// it matches or hits `$ff`. `None` means the pointer is not one this module will follow.
|
||||
fn passable(memory: &mut dyn MemoryReader, tile: u8) -> Option<bool> {
|
||||
/// it matches or hits `$ff`. `None` means the pointer is not one this module will follow, or the
|
||||
/// list is not terminated inside the bound below.
|
||||
///
|
||||
/// One read of the list serves both callers: [`walkable`] asks about one tile of the window, and
|
||||
/// [`map_grid`] asks about every tile of the map, and neither is allowed its own copy of the rule.
|
||||
fn collision_list(memory: &mut dyn MemoryReader) -> Option<Vec<u8>> {
|
||||
let low = u16::from(read(memory, ram::wTilesetCollisionPtr));
|
||||
let high = u16::from(read(memory, ram::wTilesetCollisionPtr + 1));
|
||||
let base = high * 256 + low;
|
||||
|
|
@ -792,16 +797,162 @@ fn passable(memory: &mut dyn MemoryReader, tile: u8) -> Option<bool> {
|
|||
}
|
||||
// No collision list in the game is longer than this; the bound is what stops a bad pointer
|
||||
// from walking the cartridge.
|
||||
let mut list = Vec::new();
|
||||
for offset in 0..64u16 {
|
||||
match read(memory, base + offset) {
|
||||
0xff => return Some(false),
|
||||
found if found == tile => return Some(true),
|
||||
_ => {}
|
||||
let byte = read(memory, base + offset);
|
||||
list.push(byte);
|
||||
if byte == mapgrid::TERMINATOR {
|
||||
return Some(list);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Whether the current tileset calls this tile id passable.
|
||||
///
|
||||
/// [`collision_list`]'s own walk, and `CheckTilePassable`'s: match or `$ff`, whichever comes
|
||||
/// first. `None` means the list could not be read at all.
|
||||
fn passable(memory: &mut dyn MemoryReader, tile: u8) -> Option<bool> {
|
||||
let list = collision_list(memory)?;
|
||||
for candidate in list {
|
||||
if candidate == mapgrid::TERMINATOR {
|
||||
return Some(false);
|
||||
}
|
||||
if candidate == tile {
|
||||
return Some(true);
|
||||
}
|
||||
}
|
||||
Some(false)
|
||||
}
|
||||
|
||||
/// Why a whole-map grid could not be decoded on this frame.
|
||||
///
|
||||
/// `docs/design/macros.md` section 15 asks the fallback to *say when*, so every way out of
|
||||
/// [`map_grid`] is named rather than being one `None`. Each one leaves the window predicate in
|
||||
/// charge, which is what the walks did before the grid existed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GridRefusal {
|
||||
/// The map header is not loaded, or its size is out of range (`map_size` said `None`).
|
||||
NoHeader,
|
||||
/// The player's coordinates are not readable, so nothing can be cross-checked.
|
||||
NoPlayer,
|
||||
/// The tileset's collision list could not be followed ([`collision_list`]).
|
||||
NoCollisionList,
|
||||
/// The blockset could not be read: the seam has no cartridge behind it
|
||||
/// ([`MemoryReader::read_rom`] answered `None`), or the header's pointer runs off the image.
|
||||
NoBlockset,
|
||||
/// The screen is not showing the map — a battle, a text box, a frame mid-warp — so there is
|
||||
/// nothing to check the decode against, and `wOverworldMap` shares its bytes with the picture
|
||||
/// buffer (`ram/wram.asm`'s own union), which is exactly when it must not be trusted.
|
||||
NoScreen,
|
||||
/// The decode and the screen buffer disagree about a tile the window can answer for. A wrong
|
||||
/// stride, a wrong quadrant or a half-loaded map all land here, and all of them answer
|
||||
/// plausibly, which is why this check is not optional.
|
||||
ScreenDisagrees,
|
||||
}
|
||||
|
||||
impl GridRefusal {
|
||||
/// A short label for a log line and the probes.
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
GridRefusal::NoHeader => "no map header",
|
||||
GridRefusal::NoPlayer => "no player",
|
||||
GridRefusal::NoCollisionList => "no collision list",
|
||||
GridRefusal::NoBlockset => "no blockset",
|
||||
GridRefusal::NoScreen => "map not on screen",
|
||||
GridRefusal::ScreenDisagrees => "screen disagrees",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The whole loaded map's walkability, decoded from the tables the cartridge has loaded.
|
||||
///
|
||||
/// `docs/design/macros.md` section 15. The rule is [`walkable`]'s rule — the tileset's collision
|
||||
/// list — and what this adds is the tile id of every tile of the map rather than of the ten-by-nine
|
||||
/// window:
|
||||
///
|
||||
/// - the map's **blocks** come from `wOverworldMap`, which `LoadTileBlockMap` fills from the map's
|
||||
/// own ROM bank as rows of `wCurMapWidth + MAP_BORDER * 2` bytes with the map itself three rows
|
||||
/// and three columns in. That is WRAM, so it needs no bank at all.
|
||||
/// - a block's **tiles** come from the tileset header's blockset, sixteen bytes per block id
|
||||
/// (`DrawTileBlock`). That is ROM, and not bank 0, so it is the one read that goes through
|
||||
/// [`MemoryReader::read_rom`] — the cartridge image as the process already holds it, because the
|
||||
/// alternative would be *writing* the mapper's bank register and the joypad is the only write
|
||||
/// this workspace makes into a running game.
|
||||
/// - the **tile-pair** refusals come from the values of `TilePairCollisionsLand`, keyed by
|
||||
/// `wCurMapTileset`, and become directed walls ([`mapgrid::TILE_PAIRS_LAND`]).
|
||||
///
|
||||
/// The last thing it does is check itself: the decoded tile ids are compared against
|
||||
/// [`map_tile_id`] for the player's own tile and its four neighbours, every one the window can
|
||||
/// answer for. A frame where the window can answer for none of them is refused
|
||||
/// ([`GridRefusal::NoScreen`]) rather than trusted, because `wOverworldMap` shares its bytes with
|
||||
/// the picture buffer and a battle is exactly when the blocks under it are somebody else's.
|
||||
pub fn map_grid(memory: &mut dyn MemoryReader) -> Result<MapGrid, GridRefusal> {
|
||||
let size = map_size(memory).ok_or(GridRefusal::NoHeader)?;
|
||||
let player = player(memory).ok_or(GridRefusal::NoPlayer)?;
|
||||
let passable = collision_list(memory).ok_or(GridRefusal::NoCollisionList)?;
|
||||
let width_blocks = read(memory, ram::wCurMapWidth);
|
||||
let height_blocks = read(memory, ram::wCurMapHeight);
|
||||
let stride = u16::from(width_blocks) + (mapgrid::MAP_BORDER as u16) * 2;
|
||||
let border = mapgrid::MAP_BORDER as u16;
|
||||
let mut blocks = Vec::with_capacity(usize::from(width_blocks) * usize::from(height_blocks));
|
||||
for row in 0..u16::from(height_blocks) {
|
||||
for column in 0..u16::from(width_blocks) {
|
||||
blocks.push(read(memory, ram::wOverworldMap + (row + border) * stride + column + border));
|
||||
}
|
||||
}
|
||||
// Only as much of the blockset as this map's blocks index into: a tileset has up to 256 of
|
||||
// them and a room uses a dozen, and a read that stops at the highest block id used is a read
|
||||
// that cannot run off the end of a bank for tiles nothing asks about.
|
||||
let highest = blocks.iter().copied().max().unwrap_or(0);
|
||||
let bank = read(memory, ram::wTilesetBank);
|
||||
let base = u16::from(read(memory, ram::wTilesetBlocksPtr))
|
||||
+ u16::from(read(memory, ram::wTilesetBlocksPtr + 1)) * 256;
|
||||
let wanted = (usize::from(highest) + 1) * mapgrid::BLOCK_BYTES;
|
||||
let mut blockset = Vec::with_capacity(wanted);
|
||||
for offset in 0..wanted {
|
||||
let address = base.checked_add(u16::try_from(offset).map_err(|_| GridRefusal::NoBlockset)?);
|
||||
let byte = address
|
||||
.and_then(|address| memory.read_rom(bank, address))
|
||||
.ok_or(GridRefusal::NoBlockset)?;
|
||||
blockset.push(byte);
|
||||
}
|
||||
let tiles = mapgrid::Tileset { id: read(memory, ram::wCurMapTileset), blocks: blockset, passable };
|
||||
let grid = mapgrid::decode(player.map, width_blocks, height_blocks, &blocks, &tiles);
|
||||
if grid.width() != size.width || grid.height() != size.height {
|
||||
return Err(GridRefusal::NoHeader);
|
||||
}
|
||||
// The cross-check. `map_tile_id` reads the screen buffer at the offset
|
||||
// `_GetTileAndCoordsInFrontOfPlayer` uses, so agreeing with it on the tiles it can answer for
|
||||
// is agreeing with the cartridge's own reading of the same ground.
|
||||
let mut checked = 0;
|
||||
for (x, y) in neighbourhood(player.x, player.y) {
|
||||
let Some(screen) = map_tile_id(memory, x, y) else { continue };
|
||||
if grid.tile_id(x, y) != Some(screen) {
|
||||
return Err(GridRefusal::ScreenDisagrees);
|
||||
}
|
||||
checked += 1;
|
||||
}
|
||||
if checked == 0 {
|
||||
return Err(GridRefusal::NoScreen);
|
||||
}
|
||||
Ok(grid)
|
||||
}
|
||||
|
||||
/// The player's own tile and its four neighbours, which is every tile the window is certain to be
|
||||
/// able to answer for from where the fly is standing.
|
||||
fn neighbourhood(x: u8, y: u8) -> Vec<(u8, u8)> {
|
||||
let mut out = vec![(x, y)];
|
||||
for (dx, dy) in [(0i16, 1i16), (0, -1), (-1, 0), (1, 0)] {
|
||||
if let (Ok(nx), Ok(ny)) =
|
||||
(u8::try_from(i16::from(x) + dx), u8::try_from(i16::from(y) + dy))
|
||||
{
|
||||
out.push((nx, ny));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Whether the player could stand on this tile of the current map.
|
||||
///
|
||||
/// Mirrors `CheckTilePassable`: the tile id comes out of the screen buffer at the offset
|
||||
|
|
@ -918,6 +1069,16 @@ pub struct PokeState<'a> {
|
|||
/// Tiles the cartridge has pushed the fly off ([`MacroState::pushed_tile`]). Session state
|
||||
/// beside the four above, owned by the same type (`infra/docs/macros-traps.md` row 37).
|
||||
pushed: &'a dyn PushedLedger,
|
||||
/// Where the decoded map grid is kept between frames ([`MacroState::map_grid`],
|
||||
/// `docs/design/macros.md` section 15).
|
||||
///
|
||||
/// Mutable, unlike every ledger above, because this is the one thing the state *computes*
|
||||
/// rather than looks up: a decode is a few thousand reads and a walk of the blockset, and it
|
||||
/// is valid for as long as the map is loaded. Without a cache every caller decodes again,
|
||||
/// which is correct and is what the tests do; the sim loop passes one
|
||||
/// ([`PokeState::caching_grid`]) so that a precondition asking for the frontier costs a
|
||||
/// refcount instead of a map.
|
||||
grids: Option<&'a mut MapGrids>,
|
||||
}
|
||||
|
||||
impl<'a> PokeState<'a> {
|
||||
|
|
@ -935,6 +1096,7 @@ impl<'a> PokeState<'a> {
|
|||
stood: &NoStood,
|
||||
areas: &NoAreas,
|
||||
pushed: &NoPushed,
|
||||
grids: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -948,6 +1110,7 @@ impl<'a> PokeState<'a> {
|
|||
stood: &NoStood,
|
||||
areas: &NoAreas,
|
||||
pushed: &NoPushed,
|
||||
grids: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -965,7 +1128,18 @@ impl<'a> PokeState<'a> {
|
|||
areas: &'a dyn AreaLedger,
|
||||
pushed: &'a dyn PushedLedger,
|
||||
) -> Self {
|
||||
Self { memory, ledger, talk, targets, stood, areas, pushed }
|
||||
Self { memory, ledger, talk, targets, stood, areas, pushed, grids: None }
|
||||
}
|
||||
|
||||
/// Keep the decoded map grid in `grids` instead of decoding it per question.
|
||||
///
|
||||
/// The cache is keyed by map id and size and holds one map, so arriving somewhere else drops
|
||||
/// it (`docs/design/macros.md` section 15). Session state: it is owned by
|
||||
/// [`super::macros::driver::PokemonPalette`], never checkpointed, and rebuilt from the
|
||||
/// cartridge on the first overworld frame after a restore.
|
||||
pub fn caching_grid(mut self, grids: &'a mut MapGrids) -> Self {
|
||||
self.grids = Some(grids);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1051,6 +1225,28 @@ impl MacroState for PokeState<'_> {
|
|||
!controllable(self.memory)
|
||||
}
|
||||
|
||||
/// The whole loaded map's walkability, from the cache when it is for this map
|
||||
/// (`docs/design/macros.md` section 15).
|
||||
///
|
||||
/// `None` is the honest answer on every frame [`map_grid`] refuses — no cartridge behind the
|
||||
/// seam, a battle or a text box over the map, a header that is not loaded — and every caller
|
||||
/// falls back to the ten-by-nine window predicate then, which is what all of them did before
|
||||
/// this existed. [`GridRefusal`] names which, for the probes.
|
||||
fn map_grid(&mut self) -> Option<std::sync::Arc<MapGrid>> {
|
||||
let size = map_size(self.memory)?;
|
||||
let map = player(self.memory)?.map;
|
||||
if let Some(grids) = self.grids.as_deref()
|
||||
&& let Some(grid) = grids.get(map, size.width, size.height)
|
||||
{
|
||||
return Some(grid);
|
||||
}
|
||||
let grid = map_grid(self.memory).ok()?;
|
||||
match self.grids.as_deref_mut() {
|
||||
Some(grids) => Some(grids.store(grid)),
|
||||
None => Some(std::sync::Arc::new(grid)),
|
||||
}
|
||||
}
|
||||
|
||||
/// What the open mart sells, in menu order (`docs/design/macros.md` section 13).
|
||||
///
|
||||
/// Gated on the mart scene being up, and that gate is the whole of the accuracy here:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
//! passable-tile list walked out of ROM bank 0.
|
||||
|
||||
use super::*;
|
||||
use crate::pokemon_red::fake_wram::{REDS_HOUSE_1F, WALL_TILE, Wram};
|
||||
use crate::pokemon_red::fake_wram::{self, REDS_HOUSE_1F, WALL_TILE, Wram};
|
||||
use crate::pokemon_red::macros::cartridge::MacroState;
|
||||
use crate::pokemon_red::macros::state::{BattleKind, BattleMenu, ShopScreen, Sign};
|
||||
|
||||
/// A walkable tile id from `RedsHouse1_Coll`.
|
||||
|
|
@ -603,3 +604,141 @@ fn the_live_implementation_answers_the_whole_trait() {
|
|||
assert_eq!(state.warps().len(), 1);
|
||||
assert!(state.connections().south);
|
||||
}
|
||||
|
||||
/// A map ten blocks by nine — twenty tiles by eighteen, wider than the ten-by-nine window — with
|
||||
/// a wall down one column of blocks, and a screen buffer that agrees with it.
|
||||
///
|
||||
/// The three tables the grid is decoded from, all synthetic: block ids in `wOverworldMap`, a
|
||||
/// blockset in a ROM bank that is not bank 0, and a collision list in bank 0 where
|
||||
/// `wTilesetCollisionPtr` points.
|
||||
fn town() -> (Wram, Vec<u8>, Vec<[u8; 16]>) {
|
||||
const FLOOR: u8 = 0x01;
|
||||
let blockset = vec![[FLOOR; 16], [WALL_TILE; 16]];
|
||||
let (wide, high) = (10usize, 9usize);
|
||||
let mut blocks = vec![0u8; wide * high];
|
||||
for row in 0..high {
|
||||
blocks[row * wide + 5] = 1;
|
||||
}
|
||||
let mut wram = Wram::new();
|
||||
wram.started()
|
||||
.map(fake_wram::PALLET_TOWN, wide as u8, high as u8, 3, 4)
|
||||
.facing(0)
|
||||
.house_collision()
|
||||
.tileset(0)
|
||||
.blockset(&blockset)
|
||||
.map_blocks(&blocks)
|
||||
.fill_screen(WALL_TILE)
|
||||
.screen_from_blocks(&blocks, &blockset);
|
||||
(wram, blocks, blockset)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_whole_map_decodes_from_the_block_and_collision_tables() {
|
||||
let (mut wram, _, _) = town();
|
||||
let grid = map_grid(&mut wram).expect("a decodable map");
|
||||
assert_eq!((grid.width(), grid.height()), (20, 18));
|
||||
assert_eq!(grid.unknown_count(), 0);
|
||||
// The far corner of the map, which the window predicate cannot answer for at all: the grid
|
||||
// does, and that is the whole of section 15.
|
||||
assert_eq!(walkable(&mut wram, 19, 17), Walkable::Unknown);
|
||||
assert_eq!(grid.walkable(19, 17), Walkable::Yes);
|
||||
// The wall column, again outside the window.
|
||||
assert_eq!(grid.walkable(10, 17), Walkable::No);
|
||||
assert_eq!(grid.walkable(11, 17), Walkable::No);
|
||||
// Inside the window the two readings agree tile for tile, which is what the reader checks
|
||||
// itself with before it trusts a decode.
|
||||
for y in 0..18u8 {
|
||||
for x in 0..20u8 {
|
||||
if let Some(tile) = map_tile_id(&mut wram, x, y) {
|
||||
assert_eq!(grid.tile_id(x, y), Some(tile), "({x}, {y})");
|
||||
assert_eq!(grid.walkable(x, y), walkable(&mut wram, x, y), "({x}, {y})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_decode_the_screen_disagrees_with_is_refused() {
|
||||
let (mut wram, _, _) = town();
|
||||
// One screen tile the block data does not account for: a wrong stride, a wrong quadrant or a
|
||||
// half-loaded map all look like this, and all of them answer plausibly.
|
||||
wram.map_tile(3, 4, 0x77);
|
||||
assert_eq!(map_grid(&mut wram), Err(GridRefusal::ScreenDisagrees));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_a_cartridge_behind_the_seam_there_is_no_grid() {
|
||||
let (mut wram, blocks, _) = town();
|
||||
// The same WRAM with no blockset in any bank: `read_rom` answers `None`, which is what every
|
||||
// reader that is not the emulator answers, and the grid narrows to nothing rather than
|
||||
// decoding the map out of whatever bytes were to hand.
|
||||
let mut bare = Wram::new();
|
||||
bare.started()
|
||||
.map(fake_wram::PALLET_TOWN, 10, 9, 3, 4)
|
||||
.facing(0)
|
||||
.house_collision()
|
||||
.map_blocks(&blocks)
|
||||
.fill_screen(WALL_TILE);
|
||||
assert_eq!(map_grid(&mut bare), Err(GridRefusal::NoBlockset));
|
||||
// And the window predicate still answers, which is the fallback the whole thing rests on.
|
||||
assert_eq!(walkable(&mut wram, 3, 4), Walkable::Yes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_frame_that_is_not_showing_the_map_has_no_grid_to_check() {
|
||||
let (mut wram, _, _) = town();
|
||||
// `wOverworldMap` shares its bytes with the picture buffer (`ram/wram.asm`'s own union), so a
|
||||
// battle is exactly when the blocks under it belong to somebody else. Nothing can be
|
||||
// cross-checked then, and a decode nothing can check is refused.
|
||||
wram.battle(1);
|
||||
assert_eq!(map_grid(&mut wram), Err(GridRefusal::NoScreen));
|
||||
|
||||
let (mut wram, _, _) = town();
|
||||
wram.dialogue_box();
|
||||
assert_eq!(map_grid(&mut wram), Err(GridRefusal::NoScreen));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_frame_with_no_map_header_has_no_grid() {
|
||||
let mut wram = Wram::new();
|
||||
wram.started();
|
||||
assert_eq!(map_grid(&mut wram), Err(GridRefusal::NoHeader));
|
||||
assert_eq!(GridRefusal::NoHeader.label(), "no map header");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_grid_is_decoded_once_per_map_and_dropped_on_arrival_somewhere_else() {
|
||||
let (mut wram, blocks, blockset) = town();
|
||||
let mut grids = MapGrids::default();
|
||||
{
|
||||
let mut state = PokeState::new(&mut wram).caching_grid(&mut grids);
|
||||
let first = state.map_grid().expect("a decodable map");
|
||||
let again = state.map_grid().expect("the cached map");
|
||||
assert!(std::sync::Arc::ptr_eq(&first, &again), "the second question is the same grid");
|
||||
}
|
||||
assert_eq!(grids.held(), Some(fake_wram::PALLET_TOWN));
|
||||
|
||||
// Walking through a door: another map id, so the cache is somebody else's and is dropped.
|
||||
wram.map(fake_wram::OAKS_LAB, 10, 9, 3, 4)
|
||||
.map_blocks(&blocks)
|
||||
.screen_from_blocks(&blocks, &blockset);
|
||||
{
|
||||
let mut state = PokeState::new(&mut wram).caching_grid(&mut grids);
|
||||
let grid = state.map_grid().expect("a decodable map");
|
||||
assert_eq!(grid.map(), fake_wram::OAKS_LAB);
|
||||
}
|
||||
assert_eq!(grids.held(), Some(fake_wram::OAKS_LAB));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_state_with_no_cache_still_answers_and_a_state_with_no_cartridge_answers_none() {
|
||||
let (mut wram, _, _) = town();
|
||||
let mut state = PokeState::new(&mut wram);
|
||||
assert!(state.map_grid().is_some(), "no cache is slower, not blinder");
|
||||
|
||||
let mut bare = Wram::overworld();
|
||||
let mut state = PokeState::new(&mut bare);
|
||||
assert!(state.map_grid().is_none(), "no blockset, no grid");
|
||||
// Which is the frame the window predicate is for.
|
||||
assert_eq!(state.walkable(3, 6), Walkable::No);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue