Bundles carry sprites + --export-bundle/--bundle flags (MRP3GI M1 prereqs)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e014a0c54e
commit
64a2db6605
@ -30,7 +30,9 @@ fn main() {
|
|||||||
--sample write the demo game into games/neon-precinct\n\
|
--sample write the demo game into games/neon-precinct\n\
|
||||||
--seed N seed the deterministic RNG\n\
|
--seed N seed the deterministic RNG\n\
|
||||||
--room N jump to room N at boot (with --render-room etc)\n\
|
--room N jump to room N at boot (with --render-room etc)\n\
|
||||||
--ticks N advance N cycles at boot (palette cycling, NPCs)\n"
|
--ticks N advance N cycles at boot (palette cycling, NPCs)\n\
|
||||||
|
--bundle FILE boot from a single-JSON world bundle (the wasm path)\n\
|
||||||
|
--export-bundle F write the loaded game as one bundle JSON and exit\n"
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -48,8 +50,39 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let game_dir = get("--game").unwrap_or_else(|| ".".into());
|
let game_dir = get("--game").unwrap_or_else(|| ".".into());
|
||||||
let world = World::load_game(&game_dir);
|
// `--bundle FILE` boots from a single-JSON world instead of a folder —
|
||||||
|
// the exact path a filesystem-less (wasm) build takes, testable here.
|
||||||
|
let world = match get("--bundle") {
|
||||||
|
Some(path) => {
|
||||||
|
let s = std::fs::read_to_string(&path).unwrap_or_else(|e| {
|
||||||
|
eprintln!("can't read bundle {}: {}", path, e);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
let bundle: mrpci_core::WorldBundle = serde_json::from_str(&s).unwrap_or_else(|e| {
|
||||||
|
eprintln!("bad bundle {}: {}", path, e);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
let mut sprites: Vec<_> = bundle.sprites.clone().into_iter().collect();
|
||||||
|
sprites.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
World::from_memory(
|
||||||
|
bundle.manifest,
|
||||||
|
bundle.rooms,
|
||||||
|
bundle.scripts,
|
||||||
|
sprites,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None => World::load_game(&game_dir),
|
||||||
|
};
|
||||||
let mut gs = GameState::new(world);
|
let mut gs = GameState::new(world);
|
||||||
|
|
||||||
|
if let Some(path) = get("--export-bundle") {
|
||||||
|
let bundle = gs.world.to_bundle();
|
||||||
|
let j = serde_json::to_string(&bundle).expect("bundle serializes");
|
||||||
|
std::fs::write(&path, &j).expect("write bundle");
|
||||||
|
println!("bundle exported: {} ({} rooms, {} scripts, {} sprites, {} bytes)",
|
||||||
|
path, bundle.rooms.len(), bundle.scripts.len(), bundle.sprites.len(), j.len());
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Some(seed) = get("--seed").and_then(|s| s.parse::<u64>().ok()) {
|
if let Some(seed) = get("--seed").and_then(|s| s.parse::<u64>().ok()) {
|
||||||
gs.apply(Command::SetSeed { seed });
|
gs.apply(Command::SetSeed { seed });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -280,6 +280,11 @@ pub struct WorldBundle {
|
|||||||
/// Script sources by file name (bundles travel with their logic).
|
/// Script sources by file name (bundles travel with their logic).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub scripts: HashMap<String, String>,
|
pub scripts: HashMap<String, String>,
|
||||||
|
/// Sprite art by name (raw RGBA; quantized per room at bake time).
|
||||||
|
/// Optional so old bundles still load — an empty map keeps whatever
|
||||||
|
/// sprites the receiving World already has.
|
||||||
|
#[serde(default)]
|
||||||
|
pub sprites: HashMap<String, SpriteSrc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The loaded game: manifest + sprites + the current room baked into the
|
/// The loaded game: manifest + sprites + the current room baked into the
|
||||||
@ -485,15 +490,22 @@ impl World {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorldBundle { manifest: self.manifest.clone(), rooms, scripts }
|
let sprites = self.sprites.iter().map(|(n, s)| (n.clone(), s.clone())).collect();
|
||||||
|
WorldBundle { manifest: self.manifest.clone(), rooms, scripts, sprites }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the loaded world with a bundle (sprites are kept — bundles
|
/// Replace the loaded world with a bundle. A bundle that carries
|
||||||
/// carry world data, not art).
|
/// sprites replaces the art wholesale (the wasm/bootstrap path); one
|
||||||
|
/// that doesn't keeps whatever this World already loaded.
|
||||||
pub fn apply_bundle(&mut self, b: WorldBundle) {
|
pub fn apply_bundle(&mut self, b: WorldBundle) {
|
||||||
self.manifest = b.manifest;
|
self.manifest = b.manifest;
|
||||||
self.overlay = b.rooms.into_iter().collect();
|
self.overlay = b.rooms.into_iter().collect();
|
||||||
self.script_overlay = b.scripts.into_iter().collect();
|
self.script_overlay = b.scripts.into_iter().collect();
|
||||||
|
if !b.sprites.is_empty() {
|
||||||
|
let mut sprites: Vec<(String, SpriteSrc)> = b.sprites.into_iter().collect();
|
||||||
|
sprites.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
self.sprites = sprites;
|
||||||
|
}
|
||||||
let start = self.manifest.start_room;
|
let start = self.manifest.start_room;
|
||||||
self.goto_room(start);
|
self.goto_room(start);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,8 +19,10 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// A raw RGBA sprite as loaded from disk — cooked into [`Cel`]s per room so
|
/// A raw RGBA sprite as loaded from disk — cooked into [`Cel`]s per room so
|
||||||
/// quantization always matches the room's actual palette.
|
/// quantization always matches the room's actual palette. Serde so bundles
|
||||||
#[derive(Clone)]
|
/// can carry art: a filesystem-less build (wasm, tests) gets sprites the
|
||||||
|
/// same way it gets rooms — inside one JSON document.
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct SpriteSrc {
|
pub struct SpriteSrc {
|
||||||
pub w: usize,
|
pub w: usize,
|
||||||
pub h: usize,
|
pub h: usize,
|
||||||
|
|||||||
44
mrpci-core/tests/bundle.rs
Normal file
44
mrpci-core/tests/bundle.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
//! Bundle round-trip: a game folder → one JSON document → a fully working
|
||||||
|
//! World with NO filesystem. This is exactly how a wasm build boots, so it
|
||||||
|
//! gets a native test.
|
||||||
|
|
||||||
|
use mrpci_core::{Command, GameState, World, WorldBundle};
|
||||||
|
|
||||||
|
fn game_dir() -> Option<String> {
|
||||||
|
for p in ["games/neon-precinct", "../games/neon-precinct"] {
|
||||||
|
if std::path::Path::new(p).join("game.json").exists() {
|
||||||
|
return Some(p.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bundle_round_trip_boots_without_fs() {
|
||||||
|
let Some(dir) = game_dir() else {
|
||||||
|
eprintln!("demo game not generated; skipping");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let world = World::load_game(dir);
|
||||||
|
let json = serde_json::to_string(&world.to_bundle()).expect("bundle serializes");
|
||||||
|
let bundle: WorldBundle = serde_json::from_str(&json).expect("bundle parses");
|
||||||
|
assert_eq!(bundle.rooms.len(), 3, "all three demo rooms travel");
|
||||||
|
assert!(bundle.scripts.len() >= 4, "main.rhai + room scripts travel");
|
||||||
|
assert!(bundle.sprites.contains_key("keycard"), "art travels");
|
||||||
|
|
||||||
|
let mut sprites: Vec<_> = bundle.sprites.clone().into_iter().collect();
|
||||||
|
sprites.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
let world2 = World::from_memory(bundle.manifest, bundle.rooms, bundle.scripts, sprites);
|
||||||
|
let mut gs = GameState::new(world2);
|
||||||
|
gs.apply(Command::NewGame { room: None });
|
||||||
|
|
||||||
|
// The world is really alive: art cooked, scripts compiled, sim ticking.
|
||||||
|
assert!(gs.world.cel("keycard").is_some(), "sprites quantized from bundle art");
|
||||||
|
let snap = gs.snapshot();
|
||||||
|
assert_eq!(snap.room_name, "Neon Row");
|
||||||
|
assert!(!snap.props.is_empty());
|
||||||
|
gs.apply(Command::Tick { n: 60 });
|
||||||
|
let frame = gs.render_indices(true);
|
||||||
|
assert_eq!(frame.len(), 320 * 190);
|
||||||
|
assert!(frame.iter().any(|&p| p != 0), "frame isn't blank");
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user