use std::path::PathBuf;
use std::time::Duration;
use glam::Vec3;
use infinite_manifest::{Locator, WorldManifest};
use crate::assets::AssetSource;
use crate::engine::Loom;
use crate::navigator::{InteractableNav, LoadedWorldMeta, PortalNav, SkyNav, WorldLoader};
#[derive(Debug, Clone)]
pub enum Step {
ByLabel(String),
ByIndex(usize),
ToHost(String),
}
#[derive(Debug, Clone)]
pub struct Script {
pub start: String,
pub steps: Vec<Step>,
}
#[derive(Debug, Clone)]
pub struct WalkOutcome {
pub visited: Vec<String>,
pub error: Option<String>,
}
impl WalkOutcome {
pub fn ok(&self) -> bool {
self.error.is_none()
}
}
struct HeadlessLoader;
impl WorldLoader for HeadlessLoader {
fn load(
&mut self,
manifest: &WorldManifest,
_assets: &AssetSource,
_anchor: Vec3,
) -> LoadedWorldMeta {
LoadedWorldMeta {
title: manifest.world.title.clone(),
world_id: manifest.world.id.clone(),
spawn: Vec3::from_array(manifest.default_spawn().position),
portals: manifest
.portals
.iter()
.map(|p| PortalNav {
id: p.id.clone(),
position: Vec3::from_array(p.position),
to: p.to.clone(),
label: p.label.clone(),
preview: p.preview != infinite_manifest::PreviewPolicy::None,
yaw: 0.0, })
.collect(),
interactables: manifest
.placements
.iter()
.filter(|pl| {
pl.codex.is_some() || pl.behavior.is_some() || pl.interaction.is_some()
})
.map(|pl| InteractableNav {
position: Vec3::from_array(pl.position),
label: pl.name.clone(),
codex: pl.codex.clone(),
web: pl
.data
.get("url")
.and_then(|u| u.as_str())
.map(str::to_string),
name: pl.name.clone(),
behavior: pl.behavior.clone(),
interaction: pl.interaction.clone(),
hits_done: 0,
data: pl.data.clone(),
})
.collect(),
sky: SkyNav::default(),
presence_relays: manifest
.presence
.as_ref()
.map(|p| p.relay_list())
.unwrap_or_default(),
colliders: vec![],
}
}
}
const DT: f32 = 1.0 / 30.0;
pub fn walk(worlds_root: impl Into<PathBuf>, script: &Script, max_frames: usize) -> WalkOutcome {
let mut loom = Loom::new(worlds_root, HeadlessLoader);
let mut visited = Vec::new();
let mut player = Vec3::ZERO;
let mut budget = max_frames;
loom.open(&script.start, Vec3::ZERO);
if !pump_until_arrival(&mut loom, &mut player, &mut visited, &mut budget) {
return WalkOutcome {
visited,
error: Some(format!("could not open '{}'", script.start)),
};
}
for (i, step) in script.steps.iter().enumerate() {
if !pump_until_armed(&mut loom, &mut player, &mut budget) {
return WalkOutcome {
visited,
error: Some(format!("step {i}: engine never armed (out of budget)")),
};
}
let Some(dest) = choose(loom.portals(), step) else {
return WalkOutcome {
visited,
error: Some(format!(
"step {i}: no veil matching {step:?} in '{}'",
loom.title().unwrap_or("?")
)),
};
};
player = dest;
if !pump_until_arrival(&mut loom, &mut player, &mut visited, &mut budget) {
return WalkOutcome {
visited,
error: Some(format!("step {i}: veilwalk did not complete")),
};
}
}
WalkOutcome {
visited,
error: None,
}
}
fn choose(veils: &[PortalNav], step: &Step) -> Option<Vec3> {
let veil = match step {
Step::ByIndex(n) => veils.get(*n),
Step::ByLabel(l) => veils.iter().find(|p| p.label.eq_ignore_ascii_case(l)),
Step::ToHost(h) => veils
.iter()
.find(|p| Locator::parse(&p.to).is_some_and(|loc| loc.host == *h)),
};
veil.map(|v| v.position)
}
fn pump_until_arrival(
loom: &mut Loom<HeadlessLoader>,
player: &mut Vec3,
visited: &mut Vec<String>,
budget: &mut usize,
) -> bool {
while *budget > 0 {
*budget -= 1;
if let Some(spawn) = loom.update(DT, *player) {
*player = spawn;
visited.push(loom.title().unwrap_or("?").to_string());
return true;
}
if loom.is_traveling() {
std::thread::sleep(Duration::from_millis(2));
}
}
false
}
fn pump_until_armed(
loom: &mut Loom<HeadlessLoader>,
player: &mut Vec3,
budget: &mut usize,
) -> bool {
const WALK_SPEED: f32 = 4.0;
while *budget > 0 {
if loom.armed() {
return true;
}
*budget -= 1;
loom.update(DT, *player);
if let Some(dir) = away_from_nearest_veil(loom.portals(), *player) {
*player += dir * (WALK_SPEED * DT);
}
}
false
}
fn away_from_nearest_veil(veils: &[PortalNav], player: Vec3) -> Option<Vec3> {
let clearance = crate::navigator::ENTER_RADIUS + 0.5;
let nearest = veils.iter().min_by(|a, b| {
a.position
.distance(player)
.total_cmp(&b.position.distance(player))
})?;
if nearest.position.distance(player) >= clearance {
return None;
}
let mut dir = player - nearest.position;
dir.y = 0.0;
Some(dir.try_normalize().unwrap_or(Vec3::X))
}
#[cfg(test)]
mod tests {
use super::*;
fn worlds_root() -> String {
format!("{}/../../worlds", env!("CARGO_MANIFEST_DIR"))
}
fn corpus_present() -> bool {
let there = std::path::Path::new(&worlds_root()).is_dir();
if !there {
eprintln!("no reference corpus beside this crate — skipping");
}
there
}
#[test]
fn walks_the_constellation_by_label() {
if !corpus_present() {
return;
}
let script = Script {
start: "thread://nexus.pixygon.io/nexus".into(),
steps: vec![
Step::ByLabel("The Forge".into()),
Step::ByLabel("The Nexus".into()),
],
};
let out = walk(worlds_root(), &script, 8000);
assert!(out.ok(), "walk failed: {:?}", out.error);
assert_eq!(out.visited, ["The Nexus", "The Forge", "The Nexus"]);
}
#[test]
fn walking_by_index_lands_somewhere_real() {
if !corpus_present() {
return;
}
let script = Script {
start: "thread://nexus.pixygon.io/nexus".into(),
steps: vec![Step::ByIndex(0)],
};
let out = walk(worlds_root(), &script, 8000);
assert!(out.ok(), "walk failed: {:?}", out.error);
assert_eq!(out.visited.first().map(String::as_str), Some("The Nexus"));
assert_eq!(out.visited.len(), 2, "start + one hop");
}
#[test]
fn a_missing_veil_is_reported_not_panicked() {
if !corpus_present() {
return;
}
let script = Script {
start: "thread://nexus.pixygon.io/nexus".into(),
steps: vec![Step::ByLabel("No Such Veil".into())],
};
let out = walk(worlds_root(), &script, 8000);
assert!(!out.ok());
assert!(out.error.as_deref().unwrap().contains("No Such Veil"));
assert_eq!(
out.visited,
["The Nexus"],
"we still recorded the start world"
);
}
}