Something went wrong. Try again.
HEXWORK - A Lantern in the Deep: 3D ASCII terminal roguelike RPG in Rust.
Entirely designed and built by Muse Spark 1.3 to evaluate if I like the model.
Something went wrong. Try again.
30 kB · 914 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915//! Owned, plain-data checkpoint image for the deterministic kernel.//! This is an in-memory boundary, not a serialized wire format.
use std::collections::BTreeMap;
use crate::actor::Actor;use crate::bargain::Ledger;use crate::character::Character;use crate::connector::Connector;use crate::dungeon::{Chalk, MulePath, VaultChoice};use crate::hymn::HymnCase;use crate::inventory::Pack;use crate::item::Instance;use crate::light::LightSource;use crate::modes::ChapelDebt;use crate::npc::{ShopMemory, Townsfolk};use crate::observation::{ObservationError, ObservationRow};use crate::pending::Pending;use crate::pressure::{PressureEdge, PressureNode};use crate::reputation::Reputation;use crate::rot::CorpseClock;use crate::runtime::{ ActiveContract, ActorIdentity, RunKnowledge, RunLocation, RuntimeInvariantError, Wallet,};use crate::sound::SoundEvent;use crate::state::{FarField, GossipPacket, Spill};use crate::voxel::{GridLoadError, Voxel};
#[derive(Clone, Debug, PartialEq, Eq)]pub struct ChunkImage { pub coord: (i32, i32, i32), pub voxels: Vec<Voxel>,}
#[derive(Clone, Debug, PartialEq, Eq)]pub struct CauseImage { pub tick: u64, pub rule: String, pub detail: String,}
#[derive(Clone, Debug, PartialEq, Eq)]pub struct LawImage { pub rule: String, pub first_seen_tick: u64,}
/// Exact projection of every future-affecting `State` field.#[derive(Clone, Debug)]pub struct StateImage { pub tick: u64, pub seed: u64, /// Durable Story branch epoch (docs/fluid §7). `0` for a fresh run; a Story /// resurrection increments it. Carried in the image so the epoch survives a /// save/reload and replay reproduces the same fold. pub branch_epoch: u64, pub rng_master: [u64; 4], pub ecs_entity_count: u32, pub chunks: Vec<ChunkImage>, pub actors: Vec<Actor>, pub actor_identities: BTreeMap<u64, ActorIdentity>, pub wallets: BTreeMap<u64, Wallet>, pub contracts: BTreeMap<String, ActiveContract>, pub run_location: RunLocation, pub connectors: Vec<Connector>, pub pressure_nodes: Vec<PressureNode>, pub pressure_edges: Vec<PressureEdge>, pub causes: Vec<CauseImage>, pub far: FarField, pub laws: Vec<LawImage>, pub fall_odo: BTreeMap<u64, u64>, pub lights: Vec<LightSource>, pub sounds: Vec<SoundEvent>, pub corpses: BTreeMap<(i32, i32, i32), CorpseClock>, pub signal_sources: Vec<(i32, i32, i32)>, pub spill: Spill, pub characters: BTreeMap<u64, Character>, pub reputations: BTreeMap<u64, Reputation>, pub items: BTreeMap<u64, Instance>, pub ground_items: BTreeMap<u64, (i32, i32, i32)>, pub packs: BTreeMap<u64, Pack>, pub knowledge: RunKnowledge, pub idf: BTreeMap<String, u8>, pub hymn_cases: BTreeMap<u64, HymnCase>, pub cast_queue: Vec<(u64, String, [i32; 3])>, pub bargains: BTreeMap<u64, Ledger>, pub deep_weather: i8, pub npcs: BTreeMap<u64, Townsfolk>, pub shops: BTreeMap<(i32, i32, i32), ShopMemory>, pub gossip: Vec<GossipPacket>, pub chalk: Chalk, pub mule: Option<MulePath>, pub vault_choice: Option<VaultChoice>, pub chapel_debts: BTreeMap<u64, ChapelDebt>, pub next_id: u64, pub step_mode: bool, /// The pending-action slot as a 0-or-1 element vector so a malformed image /// carrying a *second* concurrent slot can be rejected rather than silently /// truncated (docs/fluid §4.3: exactly one pending action at a time). pub pending: Vec<Pending>, /// Canonical player terrain-observation memory as a bounded, canonical-order /// row vector (docs/fluid §5). A malformed image with duplicate or reversed /// rows is rejected rather than silently coalesced. pub observations: Vec<ObservationRow>,}
#[derive(Clone, Debug, PartialEq)]pub enum StateImageError { NonEmptyEcs { entities: u32 }, InvalidRng, Grid(GridLoadError), UnknownRule { rule: String }, DuplicateLaw { rule: String }, NonCanonicalLawOrder { previous: String, current: String }, Runtime(RuntimeInvariantError), /// A malformed observation row vector (duplicate/reversed or over-cap). Observation(ObservationError),}
#[cfg(test)]mod tests { use super::*; use crate::connector::ConnectorKind; use crate::reputation::{Faction, GodId, Reputation}; use crate::runtime::{ActorIdentity, ActorRole, RunLocation}; use crate::state::{State, TICKS_PER_DAY}; use crate::voxel::{material, CHUNK_VOL};
#[test] fn fixed_body_bounds_and_staged_moves_rejected() { use crate::spatial::{FixedVec3, MAX_POSITION_MM}; let mut image = valid_image(); image.actors[0].position = FixedVec3::new([MAX_POSITION_MM, 500, 0]).unwrap(); assert!(State::from_image(image).is_err()); use crate::locomotion::MovementSample; for (id, speed_mm_s) in [(0, 3000), (999, 3000), (1, 2001), (1, u32::MAX)] { let mut image = valid_image(); image.spill.desired_moves.insert(id, MovementSample { heading: None, speed_mm_s, stop_at: None }); assert!(State::from_image(image).is_err()); } let mut image = valid_image(); image.actors[0].alive = false; image.spill.desired_moves.insert(1, MovementSample::player(None)); assert!(State::from_image(image).is_err()); let mut image = valid_image(); image.actors[0].position = FixedVec3::new([MAX_POSITION_MM - 300, 500, 0]).unwrap(); image.spill.desired_moves.insert(1, MovementSample::player(Some(crate::heading::Heading::EAST))); assert!(State::from_image(image).is_ok(), "blocked samples remain valid committed time"); }
fn valid_image() -> StateImage { let mut state = State::new(9); state.spawn_actor_with_identity( crate::spatial::FixedVec3::from_cell_feet([0; 3]).unwrap(), ActorIdentity::new("delver", "Ada", ActorRole::Player), ); state.initialize_town("milthollow").unwrap(); state.to_image().unwrap() }
fn image_with_stone_floor() -> StateImage { let mut image = valid_image(); let mut voxels = vec![Voxel::default(); CHUNK_VOL]; let index = 47 * 48 * 48; voxels[index].material = material::STONE; image.chunks.push(ChunkImage { coord: (0, 0, -1), voxels, }); image }
fn connector() -> Connector { Connector { id: 17, kind: ConnectorKind::Shaft, from: [0.0, 0.0, -3.0], to: [0.0, 0.0, 0.0], length: 3.0, anchor_quality: 80, load_kg: 0.0, blocked: false, } }
fn rejects_nonfinite( mut mutate: impl FnMut(&mut StateImage, f32), table: &'static str, row: u64, field: &'static str, ) { for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] { let mut image = valid_image(); mutate(&mut image, value); assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::NonFiniteField { table, row, field, }) ); } }
fn rejects_range( mut mutate: impl FnMut(&mut StateImage), table: &'static str, row: u64, field: &'static str, ) { let mut image = valid_image(); mutate(&mut image); assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::InvalidFloatRange { table, row, field, }) ); }
#[test] fn knowledge_roundtrips_and_continues_deterministically() { let mut state = State::new(9); state.learn_mapped_region("town:milthollow").unwrap(); state.learn_recipe("brine-poultice").unwrap(); state.learn_translation("fiat_lux").unwrap(); state.idf.insert("tried_relic".into(), 1); state.initialize_known_template("lamp_oil").unwrap(); let image = state.to_image().unwrap(); assert_eq!(image.knowledge, *state.knowledge());
let mut restored = State::from_image(image).unwrap(); assert_eq!(restored.knowledge(), state.knowledge()); assert_eq!(restored.idf, state.idf); assert_eq!(restored.try_hash_state(), state.try_hash_state()); state.step(); restored.step(); assert_eq!(restored.try_hash_state(), state.try_hash_state()); }
#[test] fn malformed_knowledge_and_idf_ids_are_rejected_from_images() { use crate::runtime::{KnowledgeCategory, KnowledgeError};
let mut image = valid_image(); image.knowledge.recipes.insert("bad/id".into()); assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::Knowledge( KnowledgeError::InvalidId { category: KnowledgeCategory::Recipes, id: "bad/id".into(), } )) );
let mut image = valid_image(); image.idf.insert("Bad Template".into(), 2); assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::Knowledge( KnowledgeError::InvalidId { category: KnowledgeCategory::IdentifiedTemplates, id: "Bad Template".into(), } )) ); }
#[test] fn oversized_knowledge_image_is_rejected() { use crate::runtime::{KnowledgeCategory, KnowledgeError, MAPPED_REGION_LIMIT};
let mut image = valid_image(); image .knowledge .mapped_regions .extend((0..=MAPPED_REGION_LIMIT).map(|index| format!("region:{index}"))); assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::Knowledge( KnowledgeError::FieldLimit { category: KnowledgeCategory::MappedRegions, limit: MAPPED_REGION_LIMIT, } )) ); }
#[test] fn malformed_image_markers_and_rng_are_rejected() { let mut image = valid_image(); image.ecs_entity_count = 1; assert_eq!( State::from_image(image).unwrap_err(), StateImageError::NonEmptyEcs { entities: 1 } ); let mut image = valid_image(); image.rng_master = [0; 4]; assert_eq!( State::from_image(image).unwrap_err(), StateImageError::InvalidRng ); }
#[test] fn malformed_chunks_are_rejected_without_voxel_writes() { let mut image = valid_image(); image.chunks.push(ChunkImage { coord: (0, 0, 0), voxels: Vec::new(), }); assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Grid(GridLoadError::ChunkLength { actual: 0 }) );
let mut image = valid_image(); image.chunks = vec![ ChunkImage { coord: (0, 0, 0), voxels: vec![Voxel::default(); CHUNK_VOL], }, ChunkImage { coord: (0, 0, 0), voxels: vec![Voxel::default(); CHUNK_VOL], }, ]; assert!(matches!( State::from_image(image), Err(StateImageError::Grid( GridLoadError::DuplicateCoordinate { .. } )) ));
let mut image = valid_image(); image.chunks = vec![ ChunkImage { coord: (1, 0, 0), voxels: vec![Voxel::default(); CHUNK_VOL], }, ChunkImage { coord: (0, 0, 0), voxels: vec![Voxel::default(); CHUNK_VOL], }, ]; assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Grid(GridLoadError::NonCanonicalOrder { previous: (1, 0, 0), current: (0, 0, 0), }) ); }
#[test] fn unknown_and_noncanonical_static_rules_are_rejected() { let mut image = valid_image(); image.causes.push(CauseImage { tick: 1, rule: "invented".into(), detail: "bad".into(), }); assert_eq!( State::from_image(image).unwrap_err(), StateImageError::UnknownRule { rule: "invented".into() } );
let mut image = valid_image(); image.laws = vec![ LawImage { rule: "gravity".into(), first_seen_tick: 1, }, LawImage { rule: "gravity".into(), first_seen_tick: 2, }, ]; assert_eq!( State::from_image(image).unwrap_err(), StateImageError::DuplicateLaw { rule: "gravity".into() } );
let mut image = valid_image(); image.laws = vec![ LawImage { rule: "gravity".into(), first_seen_tick: 1, }, LawImage { rule: "death".into(), first_seen_tick: 2, }, ]; assert_eq!( State::from_image(image).unwrap_err(), StateImageError::NonCanonicalLawOrder { previous: "gravity".into(), current: "death".into(), } ); }
#[test] fn reputation_arrays_require_canonical_tags_and_order() { let mut image = valid_image(); let actor = image.actors[0].id; image.reputations.insert(actor, Reputation::new()); assert!(State::from_image(image.clone()).is_ok());
let cases = [ ( "factions", 1, Faction::Lampwrights.canonical_tag(), true, false, ), ( "factions", 0, Faction::Congregation.canonical_tag(), true, true, ), ( "god_credit", 1, GodId::Lamplighter.canonical_tag(), false, false, ), ( "god_credit", 0, GodId::Tollkeeper.canonical_tag(), false, true, ), ]; for (table, index, found, faction_table, reordered) in cases { let mut malformed = image.clone(); let reputation = malformed.reputations.get_mut(&actor).unwrap(); if faction_table { if reordered { reputation.factions.swap(0, 1); } else { reputation.factions[1].0 = Faction::Lampwrights; } } else if reordered { reputation.god_credit.swap(0, 1); } else { reputation.god_credit[1].0 = GodId::Lamplighter; } assert_eq!( State::from_image(malformed).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::NonCanonicalReputationArray { actor_id: actor, table, index, expected: index as u8, found, }) ); } }
#[test] fn runtime_corruption_is_rejected() { let mut image = valid_image(); image.next_id = 1; assert!(matches!( State::from_image(image), Err(StateImageError::Runtime( RuntimeInvariantError::NextIdNotAboveLiveId { .. } )) ));
let mut image = valid_image(); image.run_location = RunLocation::Town(" ".into()); assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::InvalidRunLocation) );
let mut image = valid_image(); image.actors[0].id = 0; assert!(matches!( State::from_image(image), Err(StateImageError::Runtime( RuntimeInvariantError::ZeroStableId { .. } )) ));
let mut image = valid_image(); let actor = image.actors[0].id; image.contracts.insert( "bad".into(), ActiveContract { id: "different".into(), taker: actor, accepted_tick: 0, floor: 1, pay_cu: 1, objective_target: 1, objective_progress: 0, status: crate::runtime::ContractStatus::Active, reward_town: "milthollow".into(), standing_delta: 1, }, ); assert!(matches!( State::from_image(image), Err(StateImageError::Runtime( RuntimeInvariantError::ContractKeyMismatch { .. } )) ));
let mut image = valid_image(); image.ground_items.insert(999, (0, 0, 0)); assert!(matches!( State::from_image(image), Err(StateImageError::Runtime( RuntimeInvariantError::DanglingGroundItem { item_id: 999 } )) ));
let mut image = valid_image(); let actor = image.actors[0].id; image.chapel_debts.insert( actor, ChapelDebt { burials_owed: 0, chapel: crate::modes::LAMPLIGHTER_CHAPEL.into(), }, ); assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::ZeroChapelDebt { actor_id: actor }) ); }
#[test] fn every_authoritative_float_category_rejects_all_nonfinite_values() { // Actor geometry is validated integer data, with no nonfinite encoding. for (index, field) in ["from[0]", "from[1]", "from[2]"].into_iter().enumerate() { rejects_nonfinite( |image, value| { let mut row = connector(); row.from[index] = value; image.connectors.push(row); }, "connectors", 17, field, ); } for (index, field) in ["to[0]", "to[1]", "to[2]"].into_iter().enumerate() { rejects_nonfinite( |image, value| { let mut row = connector(); row.to[index] = value; image.connectors.push(row); }, "connectors", 17, field, ); } rejects_nonfinite( |image, value| { let mut row = connector(); row.length = value; image.connectors.push(row); }, "connectors", 17, "length", ); rejects_nonfinite( |image, value| { let mut row = connector(); row.load_kg = value; image.connectors.push(row); }, "connectors", 17, "load_kg", ); rejects_nonfinite( |image, value| { image.pressure_nodes.push(PressureNode { id: 23, pressure: value, }); }, "pressure_nodes", 23, "pressure", ); rejects_nonfinite( |image, value| { image.pressure_edges.push(PressureEdge { from: 23, to: 24, openness: value, hidden: false, }); }, "pressure_edges", 0, "openness", ); rejects_nonfinite( |image, value| image.spill.gas_flows.push((23, 24, value)), "spill.gas_flows", 0, "flow", ); rejects_nonfinite( |image, value| { let mut pack = Pack::new(); pack.max_weight_kg = value; image.packs.insert(1, pack); }, "packs", 1, "max_weight_kg", ); }
#[test] fn authoritative_float_ranges_are_enforced_without_rejecting_signed_flow() { use crate::spatial::{BodyExtents, FixedVec3}; assert!(BodyExtents::new(0, 300, 1800).is_err()); assert!(BodyExtents::new(-1, 300, 1800).is_err()); assert!(FixedVec3::new([i64::MAX, 0, 0]).is_err()); assert!(FixedVec3::new([0, 0, i64::MIN]).is_err()); rejects_range( |image| { let mut row = connector(); row.length = -1.0; image.connectors.push(row); }, "connectors", 17, "length", ); rejects_range( |image| { let mut row = connector(); row.length = 2.0; image.connectors.push(row); }, "connectors", 17, "length", ); rejects_range( |image| { let mut row = connector(); row.load_kg = -0.5; image.connectors.push(row); }, "connectors", 17, "load_kg", ); rejects_range( |image| { image.pressure_edges.push(PressureEdge { from: 23, to: 24, openness: -0.01, hidden: false, }); }, "pressure_edges", 0, "openness", ); rejects_range( |image| { image.pressure_edges.push(PressureEdge { from: 23, to: 24, openness: 1.01, hidden: true, }); }, "pressure_edges", 0, "openness", ); rejects_range( |image| { let mut pack = Pack::new(); pack.max_weight_kg = 0.0; image.packs.insert(1, pack); }, "packs", 1, "max_weight_kg", ); rejects_range( |image| { let mut pack = Pack::new(); pack.max_weight_kg = -0.1; image.packs.insert(1, pack); }, "packs", 1, "max_weight_kg", );
let mut image = valid_image(); image.pressure_nodes.push(PressureNode { id: 23, pressure: -1.0, }); image.pressure_edges.push(PressureEdge { from: 23, to: 24, openness: 0.75, hidden: true, }); image.spill.gas_flows.push((23, 24, -0.25)); assert!(State::from_image(image).is_ok()); }
#[test] fn exhausted_counters_and_unadvanceable_far_clocks_are_rejected() { let mut image = valid_image(); image.tick = u64::MAX; assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::TickExhausted) );
let mut image = valid_image(); image.far.day_tick = TICKS_PER_DAY; assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::InvalidFarDayTick { day_tick: TICKS_PER_DAY, }) );
let mut image = valid_image(); image.far.day = u64::MAX; image.far.day_tick = TICKS_PER_DAY - 1; assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::FarDayExhausted) );
let mut image = valid_image(); image.next_id = u64::MAX; assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::NextIdExhausted) ); }
#[test] fn one_pending_corpse_at_last_safe_id_restores_and_steps_once() { let mut image = image_with_stone_floor(); image.next_id = u64::MAX - 1; image.actors[0].hp = 0; let mut state = State::from_image(image).unwrap(); state.step(); assert_eq!(state.items.get(&(u64::MAX - 1)).unwrap().id, u64::MAX - 1); assert_eq!(state.ground_items.get(&(u64::MAX - 1)), Some(&(0, 0, 0))); assert!(format!("{state:?}").contains(&format!("next_id: {}", u64::MAX))); }
#[test] fn multiple_pending_corpses_reject_when_id_headroom_overflows() { let mut image = valid_image(); image.next_id = u64::MAX - 1; image.actors[0].hp = 0; let mut second = image.actors[0].clone(); second.id = 2; image.actors.push(second); image .actor_identities .insert(2, ActorIdentity::new("delver", "Bea", ActorRole::Player)); image.wallets.insert(2, Wallet::default()); assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::NextIdExhausted) ); }
#[test] fn accepted_clock_boundary_can_step_once() { let mut image = valid_image(); image.tick = u64::MAX - 1; image.far.day = u64::MAX; image.far.day_tick = TICKS_PER_DAY - 2; let mut state = State::from_image(image).unwrap(); state.step(); assert_eq!(state.tick, u64::MAX); assert_eq!(state.far.day, u64::MAX); assert_eq!(state.far.day_tick, TICKS_PER_DAY - 1); }
#[test] fn restored_falling_actor_saturates_hp_and_dies_in_gravity_path() { let mut image = image_with_stone_floor(); image.actors[0].position = crate::spatial::FixedVec3::from_cell_feet([0, 0, 32]).unwrap(); image.actors[0].hp = i32::MIN + 1; let mut state = State::from_image(image).unwrap(); state.step();
assert_eq!(state.actors[0].position.millimetres()[2], 0); assert_eq!(state.actors[0].hp, i32::MIN); assert!(!state.actors[0].alive); assert_eq!( state.items.len(), 0, "gravity death predates Phase 12 corpse discovery" ); assert!(state.causes.links().iter().any(|link| { link.rule == "gravity" && link .detail .contains("falls 32.0m and lands hard (30 damage)") })); assert!(state.causes.links().iter().any(|link| { link.rule == "gravity" && link.detail.contains("killed by the fall (fell 32.0m)") })); assert!(!state.causes.links().iter().any(|link| link.rule == "death")); }
#[test] fn exotic_finite_float_bits_survive_image_restore_and_hash() { let mut state = State::new(41); state.pressure_nodes.push(PressureNode { id: 7, pressure: -0.0, }); state.spill.gas_flows.push((7, 8, f32::from_bits(1))); let hash_before = state.try_hash_state().unwrap(); let image = state.to_image().unwrap(); assert_eq!( image.pressure_nodes[0].pressure.to_bits(), (-0.0f32).to_bits() ); assert_eq!(image.spill.gas_flows[0].2.to_bits(), 1);
let restored = State::from_image(image).unwrap(); let round_trip = restored.to_image().unwrap(); assert_eq!( round_trip.pressure_nodes[0].pressure.to_bits(), (-0.0f32).to_bits() ); assert_eq!(round_trip.spill.gas_flows[0].2.to_bits(), 1); assert_eq!(restored.try_hash_state().unwrap(), hash_before); }
#[test] fn chapel_debt_restore_requires_a_living_actor_positive_count_and_exact_chapel() { let mut image = valid_image(); image .chapel_debts .insert(999, crate::modes::resurrection_debt()); assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::OrphanChapelDebt { actor_id: 999 }) );
let mut image = valid_image(); image.actors[0].alive = false; image .chapel_debts .insert(1, crate::modes::resurrection_debt()); assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::DeadChapelDebtor { actor_id: 1 }) );
let mut image = valid_image(); image.chapel_debts.insert( 1, ChapelDebt { burials_owed: 2, chapel: "Chapel of the Lamplighter".into(), }, ); assert_eq!( State::from_image(image).unwrap_err(), StateImageError::Runtime(RuntimeInvariantError::NonCanonicalChapelDebt { actor_id: 1 }) );
let mut image = valid_image(); image.chapel_debts.insert( 1, ChapelDebt { burials_owed: u8::MAX, chapel: crate::modes::LAMPLIGHTER_CHAPEL.into(), }, ); assert!(State::from_image(image).is_ok()); }}