Real object ids, fixed warping bug

This commit is contained in:
2026-06-06 20:01:07 -05:00
parent 374a69f0ca
commit ea18fe8fc2
6 changed files with 161 additions and 107 deletions
+98 -52
View File
@@ -3,7 +3,7 @@ use crate::script::{Action, Direction, ScriptHost};
use color::Rgba8;
use serde::{Deserialize, Serialize};
use std::cell::{Ref, RefCell, RefMut};
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::hash::{Hash, Hasher};
use std::rc::Rc;
use std::time::Duration;
@@ -280,6 +280,15 @@ pub const ALL_ARCHETYPES: &[Archetype] = &[
Archetype::VCrate,
];
/// A stable, unique identifier for a board object.
///
/// Ids are handed out by [`Board::add_object`] from the per-board
/// [`Board::next_object_id`] counter (starting at 1) and never reused, so a
/// reference to an object stays valid even as other objects are added or removed.
/// This replaces the old "index into `Board::objects`" scheme, which shifted when
/// objects were reordered/destroyed.
pub type ObjectId = u32;
/// A scripted object placed on the board, loaded from a map file.
///
/// `ObjectDef` represents a tile that has Rhai script attached to it.
@@ -440,8 +449,14 @@ pub struct Board {
pub(crate) floor_spec: Option<crate::floor::FloorSpec>,
/// Current player position. See [`Player`] for caveats about its future.
pub player: Player,
/// Scripted objects on this board. Parsed from the map file; not yet active.
pub objects: Vec<ObjectDef>,
/// Scripted objects on this board, keyed by stable [`ObjectId`]. A `BTreeMap`
/// (not a `Vec`) so an object can be removed without invalidating other
/// objects' ids; iteration is in ascending-id order, which equals load order
/// (ids are assigned sequentially as the map loads).
pub objects: BTreeMap<ObjectId, ObjectDef>,
/// The next [`ObjectId`] to hand out (starts at 1, monotonically increasing).
/// See [`Board::add_object`].
pub next_object_id: ObjectId,
/// Portals on this board. Parsed from the map file; not yet active.
pub portals: Vec<PortalDef>,
/// Optional font override for this board. When `None`, the app default is used.
@@ -545,8 +560,7 @@ impl Board {
return Some(Solid::Player);
}
// A solid object shadows the grid cell it sits on.
if let Some(obj) = self.object_at(x, y)
&& obj.solid
if let Some(obj) = self.solid_object_at(x, y)
{
return Some(Solid::Object(obj));
}
@@ -645,11 +659,11 @@ impl Board {
if self.player.x == x as i32 && self.player.y == y as i32 {
self.player.x = tx as i32;
self.player.y = ty as i32;
} else if let Some(idx) = self.object_index_at(x, y)
&& self.objects[idx].solid
} else if let Some(id) = self.solid_object_id_at(x, y)
{
self.objects[idx].x = tx;
self.objects[idx].y = ty;
let obj = self.objects.get_mut(&id).expect("id from object_id_at");
obj.x = tx;
obj.y = ty;
} else {
let moved = *self.get(x, y);
*self.get_mut(tx, ty) = moved;
@@ -657,14 +671,36 @@ impl Board {
}
}
/// Returns the index into [`Board::objects`] of the object at `(x, y)`, if any.
pub fn object_index_at(&self, x: usize, y: usize) -> Option<usize> {
self.objects.iter().position(|o| o.x == x && o.y == y)
/// Returns the [`ObjectId`]s of the objects at `(x, y)`, if any.
pub fn object_ids_at(&self, x: usize, y: usize) -> Vec<ObjectId> {
self.objects
.iter()
.filter(|(_, o)| o.x == x && o.y == y)
.map(|(&id, _)| id).collect()
}
/// Returns a borrow of the actual object at `(x, y)` if any
pub fn object_at(&self, x: usize, y: usize) -> Option<&ObjectDef> {
self.object_index_at(x, y).map(|idx| &self.objects[idx])
pub fn solid_object_at(&self, x: usize, y: usize) -> Option<&ObjectDef> {
self.objects.values().find(|o| o.x == x && o.y == y && o.solid)
}
/// Returns a borrow of the actual object at `(x, y)` if any
pub fn solid_object_id_at(&self, x: usize, y: usize) -> Option<ObjectId> {
self.objects
.iter()
.find(|(id, o)| o.x == x && o.y == y && o.solid)
.map(|(&id, _)| id)
}
/// Inserts `object`, assigning it the next free [`ObjectId`], and returns that id.
///
/// Ids start at 1 and increase monotonically; an id is never reused, so it
/// stays a valid handle to this object for the board's lifetime.
pub fn add_object(&mut self, object: ObjectDef) -> ObjectId {
let id = self.next_object_id;
self.next_object_id += 1;
self.objects.insert(id, object);
id
}
}
@@ -759,7 +795,7 @@ impl GameState {
// Logs are collected here rather than pushed inline, since the board borrow
// below also borrows `self`.
let mut logs: Vec<LogLine> = Vec::new();
let mut bumps: Vec<(usize, i64)> = Vec::new();
let mut bumps: Vec<(ObjectId, i64)> = Vec::new();
{
let mut board = self.board_mut();
for ba in actions {
@@ -770,7 +806,7 @@ impl GameState {
}
}
Action::SetTile(tile) => {
if let Some(obj) = board.objects.get_mut(ba.source) {
if let Some(obj) = board.objects.get_mut(&ba.source) {
obj.glyph.tile = tile;
}
}
@@ -803,7 +839,7 @@ impl GameState {
let (nx, ny) = (target.0 as usize, target.1 as usize);
// A solid object in the way is bumped by the player (id -1).
bumped = match board.solid_at(nx, ny) {
Some(Solid::Object(_)) => board.object_index_at(nx, ny),
Some(Solid::Object(_)) => board.solid_object_id_at(nx, ny),
_ => None,
};
if board.is_passable(nx, ny) || board.can_push(nx, ny, dir) {
@@ -819,30 +855,30 @@ impl GameState {
}
}
/// Moves object `idx` one cell in `dir` on `board`, returning the index of a solid
/// object it bumped (its target cell was occupied by that object), if any.
/// Moves object `id` one cell in `dir` on `board`, returning the [`ObjectId`] of a
/// solid object it bumped (its target cell was occupied by that object), if any.
///
/// The move itself proceeds only if the target is in bounds and either passable or a
/// pushable solid the object can shove out of the way (see [`Board::can_push`]). The
/// bump is recorded whenever a solid object occupies the target — whether it gets
/// pushed aside or blocks the move — since something tried to move into it. Walls and
/// crates carry no script, so only solid objects yield a bump.
fn step_object(board: &mut Board, idx: usize, dir: Direction) -> Option<usize> {
fn step_object(board: &mut Board, id: ObjectId, dir: Direction) -> Option<ObjectId> {
let (dx, dy): (i32, i32) = dir.into();
let (ox, oy) = board.objects.get(idx).map(|o| (o.x, o.y))?;
let (ox, oy) = board.objects.get(&id).map(|o| (o.x, o.y))?;
let target = (ox as i32 + dx, oy as i32 + dy);
if !board.in_bounds(target) {
return None;
}
let (nx, ny) = (target.0 as usize, target.1 as usize);
// Capture the bumped object before any push relocates it (its index is stable).
// Capture the bumped object before any push relocates it (its id is stable).
let bumped = match board.solid_at(nx, ny) {
Some(Solid::Object(_)) => board.object_index_at(nx, ny),
Some(Solid::Object(_)) => board.solid_object_id_at(nx, ny),
_ => None,
};
if board.is_passable(nx, ny) || board.can_push(nx, ny, dir) {
board.push(nx, ny, dir); // shoves a crate/object out of the way; no-op otherwise
let obj = &mut board.objects[idx];
let obj = board.objects.get_mut(&id).expect("id checked above");
obj.x = nx;
obj.y = ny;
}
@@ -866,7 +902,8 @@ mod tests {
floor: vec![Archetype::Empty.default_glyph()],
floor_spec: None,
player: Player { x: 0, y: 0 },
objects: vec![object],
objects: BTreeMap::from([(1, object)]),
next_object_id: 2,
portals: Vec::new(),
font: None,
zoom: 1,
@@ -896,6 +933,14 @@ mod tests {
objects: Vec<ObjectDef>,
scripts: &[(&str, &str)],
) -> Board {
// Assign sequential ids (1..=n) just like the map loader, so tests that
// index by id can rely on first-object == id 1.
let mut object_map: BTreeMap<ObjectId, ObjectDef> = BTreeMap::new();
let mut next_object_id: ObjectId = 1;
for o in objects {
object_map.insert(next_object_id, o);
next_object_id += 1;
}
Board {
name: "test".into(),
width: w,
@@ -907,7 +952,8 @@ mod tests {
x: player.0,
y: player.1,
},
objects,
objects: object_map,
next_object_id,
portals: Vec::new(),
font: None,
zoom: 1,
@@ -935,7 +981,7 @@ mod tests {
let mut board = open_board(4, 1, (3, 0), vec![], &[]);
board.cells[1] = (Archetype::Wall.default_glyph(), Archetype::Wall);
// A solid object on the otherwise-empty cell (2, 0).
board.objects.push(ObjectDef::new(2, 0)); // solid by default
board.add_object(ObjectDef::new(2, 0)); // solid by default
// Empty floor: nothing solid.
assert!(board.solid_at(0, 0).is_none());
@@ -1078,7 +1124,7 @@ mod tests {
game.try_move(Direction::East);
let b = game.board();
assert_eq!((b.player.x, b.player.y), (1, 0));
assert_eq!((b.objects[0].x, b.objects[0].y), (2, 0)); // object shoved east
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0)); // object shoved east
}
#[test]
@@ -1095,7 +1141,7 @@ mod tests {
let mut game = GameState::new(board);
game.run_init();
let b = game.board();
assert_eq!((b.objects[0].x, b.objects[0].y), (2, 0)); // object advanced
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0)); // object advanced
assert_eq!(b.get(2, 0).1, Archetype::Empty); // crate left (2,0)
assert_eq!(b.get(3, 0).1, Archetype::Crate); // crate pushed to (3,0)
}
@@ -1268,7 +1314,7 @@ mod tests {
game.run_init();
let b = game.board();
// East increments x by one; the object started at (2, 1).
assert_eq!((b.objects[0].x, b.objects[0].y), (3, 1));
assert_eq!((b.objects[&1].x, b.objects[&1].y), (3, 1));
}
#[test]
@@ -1283,7 +1329,7 @@ mod tests {
);
let mut game = GameState::new(board);
game.run_init();
assert_eq!(game.board().objects[0].x, 0);
assert_eq!(game.board().objects[&1].x, 0);
}
#[test]
@@ -1297,7 +1343,7 @@ mod tests {
);
let mut game = GameState::new(board);
game.run_init();
assert_eq!(game.board().objects[0].glyph.tile, 7);
assert_eq!(game.board().objects[&1].glyph.tile, 7);
}
#[test]
@@ -1316,7 +1362,7 @@ mod tests {
"greeter init should log a greeting"
);
assert!(
game.board().objects.iter().any(|o| o.glyph.tile == 2),
game.board().objects.values().any(|o| o.glyph.tile == 2),
"greeter set_tile(2) should change its glyph"
);
}
@@ -1339,8 +1385,8 @@ mod tests {
let mut game = GameState::new(board);
game.run_init();
let b = game.board();
assert_eq!(b.objects[0].x, 1); // moved east from 0
assert_eq!(b.objects[1].x, 3); // moved west from 4
assert_eq!(b.objects[&1].x, 1); // moved east from 0
assert_eq!(b.objects[&2].x, 3); // moved west from 4
}
#[test]
@@ -1356,16 +1402,16 @@ mod tests {
);
let mut game = GameState::new(board);
game.run_init();
assert_eq!(game.board().objects[0].x, 2); // first move applied (1 -> 2)
assert_eq!(game.board().objects[&1].x, 2); // first move applied (1 -> 2)
// 200 ms of ticks: still inside the cooldown, no further movement.
game.tick(Duration::from_millis(100));
game.tick(Duration::from_millis(100));
assert_eq!(game.board().objects[0].x, 2);
assert_eq!(game.board().objects[&1].x, 2);
// Crossing the 250 ms mark releases the queued second move.
game.tick(Duration::from_millis(100));
assert_eq!(game.board().objects[0].x, 3);
assert_eq!(game.board().objects[&1].x, 3);
}
#[test]
@@ -1384,7 +1430,7 @@ mod tests {
game.run_init();
// First (eastward) move is blocked by the wall: object hasn't moved.
assert_eq!(
(game.board().objects[0].x, game.board().objects[0].y),
(game.board().objects[&1].x, game.board().objects[&1].y),
(1, 1)
);
@@ -1392,14 +1438,14 @@ mod tests {
game.tick(Duration::from_millis(100));
game.tick(Duration::from_millis(100));
assert_eq!(
(game.board().objects[0].x, game.board().objects[0].y),
(game.board().objects[&1].x, game.board().objects[&1].y),
(1, 1)
);
// Past 250 ms the queued South move resolves.
game.tick(Duration::from_millis(100));
assert_eq!(
(game.board().objects[0].x, game.board().objects[0].y),
(game.board().objects[&1].x, game.board().objects[&1].y),
(1, 2)
);
}
@@ -1421,7 +1467,7 @@ mod tests {
let mut game = GameState::new(board);
game.run_init();
assert!(log_texts(&game).iter().any(|t| t == "len=2"));
assert_eq!(game.board().objects[0].glyph.tile, 6); // last set_tile won
assert_eq!(game.board().objects[&1].glyph.tile, 6); // last set_tile won
}
#[test]
@@ -1437,7 +1483,7 @@ mod tests {
let mut game = GameState::new(board);
game.run_init();
game.tick(Duration::from_millis(300));
assert_eq!(game.board().objects[0].x, 1); // never moved
assert_eq!(game.board().objects[&1].x, 1); // never moved
}
#[test]
@@ -1456,7 +1502,7 @@ mod tests {
wall_at(&mut board, 2, 0);
let mut game = GameState::new(board);
game.run_init();
assert_eq!(game.board().objects[0].glyph.tile, 9);
assert_eq!(game.board().objects[&1].glyph.tile, 9);
// Open ahead, nothing pending: blocked() is false.
let board = open_board(
@@ -1471,7 +1517,7 @@ mod tests {
);
let mut game = GameState::new(board);
game.run_init();
assert_eq!(game.board().objects[0].glyph.tile, 7);
assert_eq!(game.board().objects[&1].glyph.tile, 7);
}
#[test]
@@ -1497,8 +1543,8 @@ mod tests {
let mut game = GameState::new(board);
game.tick(Duration::from_millis(16));
let b = game.board();
assert_eq!((b.objects[0].x, b.objects[0].y), (2, 1)); // mover advanced
assert_eq!(b.objects[1].glyph.tile, 1); // checker saw the pending move
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 1)); // mover advanced
assert_eq!(b.objects[&2].glyph.tile, 1); // checker saw the pending move
}
#[test]
@@ -1528,11 +1574,11 @@ mod tests {
game.tick(Duration::from_millis(300));
{
let b = game.board();
assert_eq!((b.objects[0].x, b.objects[0].y), (1, 0)); // obj0 won the cell
assert_eq!((b.objects[1].x, b.objects[1].y), (2, 0)); // obj1 blocked
assert_eq!((b.objects[&1].x, b.objects[&1].y), (1, 0)); // obj0 won the cell
assert_eq!((b.objects[&2].x, b.objects[&2].y), (2, 0)); // obj1 blocked
}
let logs = log_texts(&game);
assert!(logs.iter().any(|t| t == "o0 by 1")); // obj0 bumped by obj1
assert!(logs.iter().any(|t| t == "o0 by 2")); // obj0 bumped by obj1
assert!(!logs.iter().any(|t| t.starts_with("o1 by"))); // obj1 not bumped
}
@@ -1663,7 +1709,7 @@ mod tests {
game.run_init();
{
let b = game.board();
assert_eq!((b.objects[0].x, b.objects[0].y), (2, 0)); // object took player's cell
assert_eq!((b.objects[&1].x, b.objects[&1].y), (2, 0)); // object took player's cell
assert_eq!((b.player.x, b.player.y), (3, 0)); // player shoved east
}
@@ -1679,7 +1725,7 @@ mod tests {
let mut game = GameState::new(board);
game.run_init();
let b = game.board();
assert_eq!((b.objects[0].x, b.objects[0].y), (1, 0)); // object blocked
assert_eq!((b.objects[&1].x, b.objects[&1].y), (1, 0)); // object blocked
assert_eq!((b.player.x, b.player.y), (2, 0)); // player not pushed
}
}
+14 -9
View File
@@ -1,9 +1,9 @@
use crate::floor::{FloorSpec, build_floor};
use crate::game::{Archetype, Board, FontSpec, Glyph, ObjectDef, Player, PortalDef};
use crate::game::{Archetype, Board, FontSpec, Glyph, ObjectDef, ObjectId, Player, PortalDef};
use crate::log::LogLine;
use color::Rgba8;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::collections::{BTreeMap, HashMap};
use std::convert::TryFrom;
use std::path::Path;
@@ -462,7 +462,10 @@ impl TryFrom<MapFile> for Board {
}
solid_occupied[pidx] = true;
let mut objects: Vec<ObjectDef> = Vec::new();
// Objects get sequential ids in load order (so BTreeMap iteration order ==
// load order, preserving the documented "lowest id wins a collision" rule).
let mut objects: BTreeMap<ObjectId, ObjectDef> = BTreeMap::new();
let mut next_object_id: ObjectId = 1;
for (i, e) in mf.objects.into_iter().enumerate() {
if skip[i] {
continue;
@@ -516,7 +519,8 @@ impl TryFrom<MapFile> for Board {
}
solid_occupied[idx] = true;
}
objects.push(obj);
objects.insert(next_object_id, obj);
next_object_id += 1;
}
Ok(Board {
@@ -531,6 +535,7 @@ impl TryFrom<MapFile> for Board {
y: py as i32,
},
objects,
next_object_id,
portals: mf.portals,
font,
zoom: mf.map.zoom,
@@ -621,7 +626,7 @@ impl From<&Board> for MapFile {
// Convert ObjectDef → MapFileObjectEntry, encoding the glyph as TOML-compatible fields.
let objects: Vec<MapFileObjectEntry> = board
.objects
.iter()
.values()
.map(|o| MapFileObjectEntry {
// Saving always emits concrete coordinates; the palette form is a
// load-time convenience only.
@@ -742,7 +747,7 @@ content = """
// `G` appears once, isn't a palette key → object lands there, cell is Empty.
let board = load_board(&map_3x1("G..", &obj_palette("G", "")));
assert_eq!(board.objects.len(), 1);
assert_eq!((board.objects[0].x, board.objects[0].y), (0, 0));
assert_eq!((board.objects[&1].x, board.objects[&1].y), (0, 0));
// The cell under the object is canonical Empty floor.
assert_eq!(board.get(0, 0).1, Archetype::Empty);
}
@@ -760,7 +765,7 @@ content = """
// map is flagged invalid (an extra appearance was reported).
let board = load_board(&map_3x1("G.G", &obj_palette("G", "")));
assert_eq!(board.objects.len(), 1);
assert_eq!((board.objects[0].x, board.objects[0].y), (0, 0));
assert_eq!((board.objects[&1].x, board.objects[&1].y), (0, 0));
assert!(!board.is_valid());
}
@@ -1009,7 +1014,7 @@ bg = "#000000"
let mf: MapFile = toml::from_str(toml).unwrap();
let board = Board::try_from(mf).unwrap();
assert_eq!(board.objects.len(), 1);
let obj = &board.objects[0];
let obj = &board.objects[&1];
assert_eq!(obj.x, 1);
assert_eq!(obj.y, 1);
assert_eq!(obj.glyph.tile, 64);
@@ -1037,7 +1042,7 @@ bg = "#000000"
let toml_out = toml::to_string_pretty(&map_file).unwrap();
let mf2: MapFile = toml::from_str(&toml_out).unwrap();
let board2 = Board::try_from(mf2).unwrap();
let obj2 = &board2.objects[0];
let obj2 = &board2.objects[&1];
assert_eq!(obj2.glyph.tile, obj.glyph.tile);
assert_eq!(obj2.glyph.fg, obj.glyph.fg);
assert_eq!(obj2.glyph.bg, obj.glyph.bg);
+36 -33
View File
@@ -31,7 +31,7 @@
//! [`take_board_queue`]: ScriptHost::take_board_queue
//! [`GameState`]: crate::game::GameState
use crate::game::Board;
use crate::game::{Board, ObjectId};
use crate::log::LogLine;
use rhai::{AST, CallFnOptions, Engine, FuncArgs, ImmutableString, NativeCallContext, Scope};
use std::cell::RefCell;
@@ -68,18 +68,14 @@ impl Action {
/// An action promoted from an object's output queue onto the board queue, tagged
/// with the object that issued it.
pub struct BoardAction {
/// The object that issued the action.
// TODO: `source` is an index into `Board::objects`, which is a stopgap — it
// breaks under object spawn/destroy/reorder. Replace with a monotonically
// increasing unique object id, with objects stored keyed by it (e.g.
// `BTreeMap<ObjectId, ObjectDef>`). Mirrored on `ObjectRuntime::object_index`.
pub source: usize,
/// The stable [`ObjectId`] of the object that issued the action.
pub source: ObjectId,
/// The action to apply.
pub action: Action,
}
/// A cardinal movement direction.
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub enum Direction {
North,
South,
@@ -112,9 +108,9 @@ type ObjQueue = Rc<RefCell<VecDeque<Action>>>;
/// so the `blocked` host function can scan pending moves during script execution.
type BoardQueue = Rc<RefCell<Vec<BoardAction>>>;
/// Maps an object index to its output queue, so the global write host functions can
/// Maps an [`ObjectId`] to its output queue, so the global write host functions can
/// route an emitted action to the right object via the per-call tag.
type QueueMap = Rc<RefCell<HashMap<usize, ObjQueue>>>;
type QueueMap = Rc<RefCell<HashMap<ObjectId, ObjQueue>>>;
/// Channel for engine/compile/runtime errors, surfaced straight to the game log
/// (errors never go through the action queues).
@@ -135,10 +131,9 @@ struct CompiledScript {
/// The runtime state of one scripted object: which script it runs, its persistent
/// Rhai scope, its pending actions, and its cooldown timer.
struct ObjectRuntime {
/// Index into [`crate::game::Board::objects`]; passed to scripts as the
/// per-call tag so host functions know which object issued an action.
// TODO: see [`BoardAction::source`] — array indices should become stable ids.
object_index: usize,
/// The stable [`ObjectId`] of this object; passed to scripts as the per-call
/// tag so host functions know which object issued an action.
object_id: ObjectId,
/// Key into [`ScriptHost::scripts`] naming this object's compiled script.
script_name: String,
/// Per-object scope (holds the `Board`/`Queue` views + direction constants, and
@@ -187,7 +182,7 @@ impl ScriptHost {
// object that references the script.
let mut scripts: HashMap<String, CompiledScript> = HashMap::new();
let mut failed: HashSet<String> = HashSet::new();
for obj in board.objects.iter() {
for obj in board.objects.values() {
let Some(name) = obj.script_name.as_ref() else {
continue;
};
@@ -230,7 +225,7 @@ impl ScriptHost {
// compiled. The queue is registered in `queues` so host functions can find
// it by the object's index, and shared into the scope as the `Queue` object.
let mut objects = Vec::new();
for (i, obj) in board.objects.iter().enumerate() {
for (&id, obj) in board.objects.iter() {
let Some(name) = obj.script_name.as_ref() else {
continue;
};
@@ -238,9 +233,9 @@ impl ScriptHost {
continue;
}
let queue: ObjQueue = Rc::new(RefCell::new(VecDeque::new()));
queues.borrow_mut().insert(i, queue.clone());
queues.borrow_mut().insert(id, queue.clone());
objects.push(ObjectRuntime {
object_index: i,
object_id: id,
script_name: name.clone(),
scope: new_object_scope(board_cell, &queue),
output_queue: queue,
@@ -271,12 +266,12 @@ impl ScriptHost {
self.run("tick", |c| c.has_tick, (dt,));
}
/// Calls `bump(id)` on the object at `object_index`, if it defines the hook.
/// Calls `bump(id)` on the object with [`ObjectId`] `object_id`, if it defines the hook.
///
/// `bumper` is the index of the object that moved into it, or `-1` for the player.
/// `bumper` is the id of the object that moved into it, or `-1` for the player.
/// Does **not** pump: any actions the hook emits wait for the next tick's pump, so
/// a bump can't cascade within the same resolution pass.
pub fn run_bump(&mut self, object_index: usize, bumper: i64) {
pub fn run_bump(&mut self, object_id: ObjectId, bumper: i64) {
let Self {
engine,
scripts,
@@ -284,7 +279,7 @@ impl ScriptHost {
errors,
..
} = self;
let Some(obj) = objects.iter_mut().find(|o| o.object_index == object_index) else {
let Some(obj) = objects.iter_mut().find(|o| o.object_id == object_id) else {
return;
};
let Some(compiled) = scripts.get(&obj.script_name) else {
@@ -293,7 +288,7 @@ impl ScriptHost {
if !compiled.has_bump {
return;
}
let options = CallFnOptions::default().with_tag(object_index as i64);
let options = CallFnOptions::default().with_tag(object_id as i64);
if let Err(err) = engine.call_fn_with_options::<()>(
options,
&mut obj.scope,
@@ -333,7 +328,7 @@ impl ScriptHost {
/// action (which arms the timer and ends the pump). The board-queue guard keeps a
/// second pump in the same tick from double-promoting.
fn pump(&mut self, i: usize) {
let oid = self.objects[i].object_index;
let oid = self.objects[i].object_id;
// Don't re-promote if this object already has actions awaiting resolution.
if self.board_queue.borrow().iter().any(|ba| ba.source == oid) {
return;
@@ -388,8 +383,8 @@ impl ScriptHost {
if let Some(compiled) = scripts.get(&obj.script_name)
&& defined(compiled)
{
// The tag carries this object's index to the host functions.
let options = CallFnOptions::default().with_tag(obj.object_index as i64);
// The tag carries this object's id to the host functions.
let options = CallFnOptions::default().with_tag(obj.object_id as i64);
if let Err(err) = engine.call_fn_with_options::<()>(
options,
&mut obj.scope,
@@ -436,9 +431,14 @@ fn register_read_api(engine: &mut Engine, board: &BoardRef, board_queue: &BoardQ
///
/// Pending moves are matched by their *immediate* target cell only; a queued move that
/// would push a chain of solids into the cell is not accounted for (an approximation).
fn is_blocked(board: &BoardRef, board_queue: &BoardQueue, source: usize, dir: Direction) -> bool {
fn is_blocked(
board: &BoardRef,
board_queue: &BoardQueue,
source: ObjectId,
dir: Direction,
) -> bool {
let board = board.borrow();
let Some(obj) = board.objects.get(source) else {
let Some(obj) = board.objects.get(&source) else {
return true;
};
let (dx, dy): (i32, i32) = dir.into();
@@ -453,7 +453,7 @@ fn is_blocked(board: &BoardRef, board_queue: &BoardQueue, source: usize, dir: Di
// Any pending move whose immediate target is this cell would put a solid here.
board_queue.borrow().iter().any(|ba| {
if let Action::Move(d) = &ba.action
&& let Some(mover) = board.objects.get(ba.source)
&& let Some(mover) = board.objects.get(&ba.source)
{
let (mdx, mdy): (i32, i32) = (*d).into();
return (mover.x as i32 + mdx, mover.y as i32 + mdy) == target;
@@ -500,17 +500,20 @@ fn register_queue_api(engine: &mut Engine) {
}
/// Appends `action` to the output queue of the object identified by `source`.
fn emit(queues: &QueueMap, source: usize, action: Action) {
fn emit(queues: &QueueMap, source: ObjectId, action: Action) {
if let Some(queue) = queues.borrow().get(&source) {
queue.borrow_mut().push_back(action);
}
}
/// Reads the issuing object's index from the call tag (set in [`ScriptHost::run`]).
fn source_of(ctx: &NativeCallContext) -> usize {
/// Reads the issuing object's [`ObjectId`] from the call tag (set in [`ScriptHost::run`]).
///
/// Falls back to `0` (never a valid id, since ids start at 1) if the tag is missing
/// or out of range, which makes [`emit`] a harmless no-op for an unknown source.
fn source_of(ctx: &NativeCallContext) -> ObjectId {
ctx.tag()
.and_then(|t| t.as_int().ok())
.and_then(|n| usize::try_from(n).ok())
.and_then(|n| ObjectId::try_from(n).ok())
.unwrap_or(0)
}