From 9be02c161d0fa2e9a152ed15dd645acf5138c7a2 Mon Sep 17 00:00:00 2001 From: Andy Killorin <37423245+Speedy6451@users.noreply.github.com> Date: Tue, 19 Dec 2023 09:59:08 -0600 Subject: [PATCH] world interior mutability --- server/src/blocks.rs | 84 ++++++++++++++++++++++-- server/src/main.rs | 88 +++++++++++++++---------- server/src/paths.rs | 40 +++++++----- server/src/turtle.rs | 152 +++++++++++++------------------------------ 4 files changed, 197 insertions(+), 167 deletions(-) diff --git a/server/src/blocks.rs b/server/src/blocks.rs index e416a23..afba9f5 100644 --- a/server/src/blocks.rs +++ b/server/src/blocks.rs @@ -1,8 +1,43 @@ +use std::{sync::Arc, ops::Index}; + use nalgebra::Vector3; use rstar::{self, PointDistance, RTree, RTreeObject, AABB}; use serde::{Deserialize, Serialize}; +use tokio::sync::{RwLock, RwLockReadGuard, OwnedRwLockReadGuard}; -pub type World = RTree; +use crate::turtle::TurtleCommand; + +pub type WorldReadLock = OwnedRwLockReadGuard>; + +#[derive(Clone)] +pub struct World { + state: Arc>>, // interior mutability to get around the + // questionable architecture of this project +} + +impl World { + pub fn new() -> Self { Self { state: Arc::new(RwLock::new(RTree::new())) } } + pub fn from_tree(tree: RTree) -> Self { Self { state: Arc::new(RwLock::new(tree)) } } + pub async fn to_tree(self) -> RTree { self.state.write().await.to_owned() } + pub async fn tree(&self) -> RTree { self.state.read().await.clone() } + + pub async fn get(&self, block: Vec3) -> Option { + self.state.read().await.locate_at_point(&block.into()).map(|b| b.to_owned()) + } + + pub async fn set(&self, block: Block) { + self.state.write().await.remove_at_point(&block.pos.into()); + self.state.write().await.insert(block); + } + + pub async fn occupied(&self, block: Vec3) -> bool { + self.state.read().await.locate_at_point(&block.into()).is_some_and(|b| b.name != "minecraft:air") + } + + pub async fn lock(self) -> WorldReadLock { + self.state.read_owned().await + } +} #[derive(Serialize, Deserialize, Clone)] pub struct Block { @@ -25,7 +60,46 @@ impl PointDistance for Block { } pub type Vec3 = Vector3; -pub type Position = (Vec3, Direction); + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct Position { + pub pos: Vec3, + pub dir: Direction, +} + +impl Position { + pub fn new(pos: Vec3, dir: Direction) -> Self { Self { pos, dir } } + + /// Get a turtle command to map two adjacent positions + pub fn difference(self, to: Position) -> Option { + use crate::turtle::TurtleCommand::*; + + if self.pos == to.pos { + if to.dir == self.dir.left() { + Some(Left) + } else if to.dir == self.dir.right() { + Some(Right) + } else { + None + } + } else if to.dir == self.dir { + if to.pos == self.pos + self.dir.unit() { + Some(Forward(1)) + } else if to.pos == self.pos - self.dir.unit() { + Some(Backward(1)) + } else if to.pos == self.pos + Vec3::y() { + Some(Up(1)) + } else if to.pos == self.pos - Vec3::y() { + Some(Down(1)) + } else { + None + } + } else { + None + } + } + +} #[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq, Copy, Debug)] pub enum Direction { @@ -80,8 +154,8 @@ pub fn nearest(from: Vec3, to: Vec3) -> Position { Direction::South } }; - ( - to - dir.unit(), + Position { + pos: to - dir.unit(), dir - ) + } } diff --git a/server/src/main.rs b/server/src/main.rs index 810a554..70949a6 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -11,7 +11,7 @@ use axum::{ }; use blocks::{World, Position}; use mine::TurtleMineJob; -use rstar::{self, AABB}; +use rstar::{self, AABB, RTree}; use const_format::formatcp; use hyper::body::Incoming; @@ -23,7 +23,7 @@ use tokio::sync::{ Mutex, RwLock, mpsc }; use tower::Service; -use turtle::{TurtleTask, Iota, Receiver, Sender, Turtle, TurtleUpdate, TurtleInfo}; +use turtle::{TurtleTask, Iota, Receiver, Sender, Turtle, TurtleUpdate, TurtleInfo, goto, TurtleCommand}; use crate::{blocks::Block, paths::route}; @@ -37,16 +37,34 @@ mod turtle; #[derive(Serialize, Deserialize)] struct SavedState { turtles: Vec, - tasks: Vec>, - world: blocks::World, + world: RTree, //chunkloaders: unimplemented!(), } -struct ControlState { - saved: SavedState, +struct LiveState { + turtles: Vec, + tasks: Vec>, + world: blocks::World, } -type SharedControl = Arc>; +impl LiveState { + async fn to_save(self) -> SavedState { + SavedState { turtles: self.turtles, world: self.world.to_tree().await } + } + + async fn save(&self) -> SavedState { + let turtles = self.turtles.iter().map(|t| t.info()); + SavedState { turtles: turtles.collect(), world: self.world.tree().await } + } + + fn from_save(save: SavedState) -> Self { + Self { turtles: save.turtles, tasks: Vec::new(), world: World::from_tree(save.world) } + } + +} + + +type SharedControl = Arc>; #[tokio::main] async fn main() -> Result<(), Error> { @@ -59,15 +77,13 @@ async fn main() -> Result<(), Error> { tokio::io::Result::Err(e) => match e.kind() { ErrorKind::NotFound => SavedState { turtles: Vec::new(), - world: World::new(), - tasks: Vec::new(), + world: RTree::new(), }, _ => panic!(), }, }; - let state = ControlState { saved: state, - }; + let state = LiveState::from_save(state); let state = SharedControl::new(RwLock::new(state)); @@ -78,7 +94,7 @@ async fn main() -> Result<(), Error> { .route("/turtle/:id/setGoal", post(set_goal)) .route("/turtle/:id/cancelTask", post(cancel)) .route("/turtle/:id/info", get(turtle_info)) - .route("/turtle/:id/placeUp", get(place_up)) + //.route("/turtle/:id/placeUp", get(place_up)) .route("/turtle/updateAll", get(update_turtles)) .route("/flush", get(flush)) .with_state(state.clone()); @@ -86,35 +102,34 @@ async fn main() -> Result<(), Error> { let server = safe_kill::serve(server).await; println!("writing"); - write_to_disk(state).await?; + write_to_disk(state.read().await.save().await).await?; server.closed().await; Ok(()) } - -async fn write_to_disk(state: SharedControl) -> anyhow::Result<()> { - let json = serde_json::to_string_pretty(&state.read().await.saved)?; - tokio::fs::write("state.json", json).await?; - Ok(()) -} - async fn flush(State(state): State) -> &'static str { - write_to_disk(state).await.unwrap(); + write_to_disk(state.read().await.save().await).await.unwrap(); "ACK" } +async fn write_to_disk(state: SavedState) -> anyhow::Result<()> { + let json = serde_json::to_string_pretty(&state)?; + tokio::fs::write("state.json", json).await?; + Ok(()) +} + async fn create_turtle( State(state): State, Json(req): Json, ) -> Json { let state = &mut state.write().await; - let id = state.saved.turtles.len() as u32; + let id = state.turtles.len() as u32; let (send, receive) = mpsc::channel(1); - state.saved.turtles.push(turtle::Turtle::with_channel(id, req.position, req.facing, req.fuel, send,receive)); - state.saved.tasks.push(VecDeque::new()); + state.turtles.push(turtle::Turtle::with_channel(id, Position::new(req.position, req.facing), req.fuel, send,receive)); + state.tasks.push(VecDeque::new()); println!("new turtle: {id}"); @@ -130,10 +145,11 @@ async fn place_up( Path(id): Path, State(state): State, ) -> Json { - let turtle = state.write().await.saved.turtles.get(id as usize).unwrap() + let turtle = state.read().await.turtles.get(id as usize).unwrap() .cmd(); + let response = turtle.execute(turtle::TurtleCommand::PlaceUp).await; - Json(turtle.execute(turtle::TurtleCommand::PlaceUp).await) + Json(response) } async fn set_goal( @@ -141,10 +157,11 @@ async fn set_goal( State(state): State, Json(req): Json, ) -> &'static str { - state.read().await.saved.turtles[id as usize].cmd().goto(req).await; - //state.write().await.saved.tasks[id as usize].push_back( - // TurtleMineJob::chunk(req.0) - //); + let state = state.read().await; + let turtle = state.turtles[id as usize].cmd(); + let info = turtle.execute(TurtleCommand::Wait(0)).await; + + tokio::spawn(goto(turtle.clone(), info, req, state.world.clone())); "ACK" } @@ -153,7 +170,7 @@ async fn cancel( Path(id): Path, State(state): State, ) -> &'static str { - state.write().await.saved.tasks[id as usize].pop_front(); + state.write().await.tasks[id as usize].pop_front(); "ACK" } @@ -162,7 +179,7 @@ async fn update_turtles(State(state): State) -> &'static str { state .write() .await - .saved.turtles + .turtles .iter_mut() .for_each(|t| t.pending_update = true); "ACK" @@ -173,12 +190,11 @@ async fn turtle_info( State(state): State, ) -> Json { let state = &mut state.read().await; - let turtle = &state.saved.turtles[id as usize]; + let turtle = &state.turtles[id as usize]; let cloned = Turtle::new( turtle.name.to_num(), - turtle.position.to_owned().0, - turtle.position.to_owned().1, + turtle.position, turtle.fuel ); @@ -194,7 +210,7 @@ async fn command( println!("{:?}", &req); - if id as usize > state.saved.turtles.len() { + if id as usize > state.turtles.len() { return Json(turtle::TurtleCommand::Update); } diff --git a/server/src/paths.rs b/server/src/paths.rs index 8fed9ff..f9eb540 100644 --- a/server/src/paths.rs +++ b/server/src/paths.rs @@ -1,58 +1,62 @@ use std::rc::Rc; use crate::{ - blocks::{Block, World, Position, Direction, Vec3}, + blocks::{Block, World, Position, Direction, Vec3, WorldReadLock}, }; use pathfinding::prelude::astar; -pub fn route(from: Position, to: Position, world: &World) -> Option> { +pub async fn route(from: Position, to: Position, world: &World) -> Option> { + // lock once, we'll be doing a lot of lookups + let world = world.clone().lock().await; + // attempt at not crashing by looking infinitely into the abyss if world - .locate_at_point(&to.0.into()) + .locate_at_point(&to.pos.into()) .is_some_and(|b| difficulty(&b.name).is_none()) { return None; } let route = astar( &from, - move |p| next(p, world), - |p1| (p1.0 - &to.0).abs().sum() as u32, + move |p| next(p, &world), + |p1| (p1.pos - &to.pos).abs().sum() as u32, |p| p == &to, ) .unwrap(); Some(route.0) } -fn next(from: &Position, world: &World) -> Vec<(Position, u32)> { +fn next(from: &Position, world: &WorldReadLock) -> Vec<(Position, u32)> { let mut vec: Vec<(Position, u32)> = Vec::new(); - vec.push(((from.0, from.1.left()), 1)); - vec.push(((from.0, from.1.right()), 1)); fn insert( vec: &mut Vec<(Position, u32)>, point: Vec3, orientation: Direction, - world: &World, + world: &WorldReadLock, unknown: Option, ) { world .locate_at_point(&point.into()) .map_or(unknown, |b| difficulty(&b.name)) - .map(|d| vec.push(((point, orientation), d))); + .map(|d| vec.push((Position::new(point, orientation), d))); } - let ahead = from.0 + from.1.unit(); - insert(&mut vec, ahead, from.1, world, UNKNOWN); + vec.push((Position::new(from.pos, from.dir.left()), 1)); + vec.push((Position::new(from.pos, from.dir.right()), 1)); - //let behind = from.0 - from.1.unit(); - //insert(&mut vec, behind, from.1, world, None); + let ahead = from.pos + from.dir.unit(); + insert(&mut vec, ahead, from.dir, world, UNKNOWN); - let above = from.0 + Vec3::y(); - insert(&mut vec, above, from.1, world, UNKNOWN); + //let behind = from.pos - from.dir.unit(); + //insert(&mut vec, behind, from.dir, world, None); - let below = from.0 - Vec3::y(); - insert(&mut vec, below, from.1, world, UNKNOWN); + let above = from.pos + Vec3::y(); + insert(&mut vec, above, from.dir, world, UNKNOWN); + + let below = from.pos - Vec3::y(); + insert(&mut vec, below, from.dir, world, UNKNOWN); vec } diff --git a/server/src/turtle.rs b/server/src/turtle.rs index 17926da..d955427 100644 --- a/server/src/turtle.rs +++ b/server/src/turtle.rs @@ -3,6 +3,7 @@ use crate::blocks::Block; use crate::blocks::Direction; use crate::blocks::Position; use crate::blocks::Vec3; +use crate::blocks::World; use crate::blocks::nearest; use crate::mine::TurtleMineJob; @@ -13,8 +14,9 @@ use anyhow::Context; use tokio::sync::RwLock; use tokio::sync::mpsc; use tokio::sync::oneshot; +use tokio::sync::oneshot::channel; -use super::ControlState; +use super::LiveState; use std::collections::VecDeque; use std::future::Ready; @@ -69,39 +71,52 @@ pub type Receiver = mpsc::Receiver<(TurtleCommand, oneshot::Sender)> impl Default for Turtle { fn default() -> Self { + let (sender, receiver) = mpsc::channel(1); Self { name: Name::from_num(0), fuel: Default::default(), queued_movement: Default::default(), - position: (Vec3::zeros(), Direction::North), + position: Position::new(Vec3::zeros(), Direction::North), goal: None, pending_update: Default::default(), callback: None, - sender: None, - receiver: None, + sender: Some(Arc::new(sender)), + receiver: Some(receiver), } } } impl Turtle { - pub(crate) fn new(id: u32, position: Vec3, facing: Direction, fuel: usize) -> Self { + pub(crate) fn new(id: u32, position: Position, fuel: usize) -> Self { Self { name: Name::from_num(id), fuel, queued_movement: Vec3::new(0, 0, 0), - position: (position, facing), + position, pending_update: true, ..Default::default() } } - pub fn with_channel(id: u32, position: Vec3, facing: Direction, fuel: usize, sender: Sender, receiver: Receiver) -> Self { + /// Similar turtle for serialization + pub fn info(&self) -> Self { + Self { + name: self.name, + fuel: self.fuel, + position: self.position, + pending_update: self.pending_update, + queued_movement: self.queued_movement, + ..Default::default() + } + } + + pub fn with_channel(id: u32, position: Position, fuel: usize, sender: Sender, receiver: Receiver) -> Self { Self { name: Name::from_num(id), fuel, queued_movement: Vec3::new(0, 0, 0), - position: (position, facing), + position, pending_update: true, sender: Some(Arc::new(sender)), receiver: Some(receiver), @@ -116,6 +131,7 @@ impl Turtle { } +#[derive(Clone)] pub struct TurtleCommander { sender: Arc, } @@ -130,28 +146,27 @@ impl TurtleCommander { } } -async fn goto(cmd: &TurtleCommander, recent: TurtleInfo, pos: Position, world: &rstar::RTree) -> Option<()> { +pub async fn goto(cmd: TurtleCommander, recent: TurtleInfo, pos: Position, world: World) -> Option<()> { let mut recent = recent.pos; loop { if recent == pos { break; } - let route = route(recent, pos, world)?; + let route = route(recent, pos, &world).await?; - let steps: Vec = route.iter().map_windows(|[from,to]| difference(**from,**to).unwrap()).collect(); + let steps: Vec = route.iter().map_windows(|[from,to]| from.difference(**to).unwrap()).collect(); 'route: for (next_position, command) in route.into_iter().skip(1).zip(steps) { // reroute if the goal point is not empty before moving // valid routes will explicitly tell you to break ground - if world.locate_at_point(&next_position.0.into()).unwrap().name != "minecraft:air" { + if world.occupied(next_position.pos).await { break 'route; } let state = cmd.execute(command).await; recent = state.pos; - } } Some(()) @@ -159,18 +174,18 @@ async fn goto(cmd: &TurtleCommander, recent: TurtleInfo, pos: Position, world: & pub(crate) async fn process_turtle_update( id: u32, - state: &mut ControlState, + state: &mut LiveState, update: TurtleUpdate, ) -> anyhow::Result { let turtle = state - .saved.turtles + .turtles .get_mut(id as usize) .context("nonexisting turtle")?; let tasks = state - .saved.tasks + .tasks .get_mut(id as usize) .context("state gone?????").unwrap(); - let world = &mut state.saved.world; + let world = &mut state.world; if turtle.pending_update { turtle.pending_update = false; @@ -180,30 +195,27 @@ pub(crate) async fn process_turtle_update( if turtle.fuel != update.fuel { turtle.fuel = update.fuel; - turtle.position.0 += turtle.queued_movement; + turtle.position.pos += turtle.queued_movement; turtle.queued_movement = Vec3::zeros(); } let above = Block { name: update.above.clone(), - pos: turtle.position.0 + Vec3::y(), + pos: turtle.position.pos + Vec3::y(), }; - world.remove_at_point(&above.pos.into()); - world.insert(above.clone()); + world.set(above.clone()).await; let ahead = Block { name: update.ahead.clone(), - pos: turtle.position.0 + turtle.position.1.clone().unit(), + pos: turtle.position.pos + turtle.position.dir.unit(), }; - world.remove_at_point(&ahead.pos.into()); - world.insert(ahead.clone()); + world.set(ahead.clone()).await; let below = Block { name: update.below.clone(), - pos: turtle.position.0 - Vec3::y(), + pos: turtle.position.pos - Vec3::y(), }; - world.remove_at_point(&below.pos.into()); - world.insert(below.clone()); + world.set(below.clone()).await; let info = TurtleInfo::from_update(update, turtle.name.clone(), turtle.position.clone()); @@ -215,65 +227,18 @@ pub(crate) async fn process_turtle_update( if let Some((cmd, ret)) = recv.try_recv().ok() { turtle.callback = Some(ret); + match cmd { + TurtleCommand::Left => turtle.position.dir = turtle.position.dir.left(), + TurtleCommand::Right => turtle.position.dir = turtle.position.dir.right(), + _ => {} + } return Ok(cmd); } } - if let Some(goal) = turtle.goal.take().or_else(|| tasks.front_mut().map(|t| t.next(&turtle))) { - let command = match goal { - Iota::End => { - tasks.pop_front(); - TurtleCommand::Wait(0) // TODO: fix - }, - Iota::Goto(pos) => { - println!("gogto: {:?}", pos); - pathstep(turtle, world, pos).unwrap() - }, - Iota::Mine(pos) => { - let pos = nearest(turtle.position.0, pos); - - if pos == turtle.position { - TurtleCommand::Dig - } else { - pathstep(turtle, world, pos).unwrap() - } - }, - Iota::Execute(cmd) => { - cmd - }, - }; - - println!("Order: {:?}", command); - return Ok(command); - }; - Ok(TurtleCommand::Wait(3)) } -fn pathstep(turtle: &mut Turtle, world: &mut rstar::RTree, goal: Position) -> Option { - // TODO: memoize this whenever we aren't digging - let route = route(turtle.position, goal, world)?; - let mut next_move = difference(route[0], route[1])?; - if world - .locate_at_point(&route[1].0.into()) - .is_some_and(|b| b.name != "minecraft:air") - { - next_move = match next_move { - TurtleCommand::Up(_) => TurtleCommand::DigUp, - TurtleCommand::Down(_) => TurtleCommand::DigDown, - TurtleCommand::Forward(_) => TurtleCommand::Dig, - _ => next_move, - } - } - turtle.queued_movement = next_move.delta(turtle.position.1); - match next_move { - TurtleCommand::Left => turtle.position.1 = turtle.position.1.left(), - TurtleCommand::Right => turtle.position.1 = turtle.position.1.right(), - _ => {} - } - return Some(next_move); -} - #[derive(Serialize, Deserialize, Clone, Debug)] pub(crate) enum TurtleCommand { Wait(u32), @@ -354,35 +319,6 @@ pub(crate) struct TurtleResponse { pub(crate) command: TurtleCommand, } -/// Get a turtle command to map two adjacent positions -fn difference(from: Position, to: Position) -> Option { - use TurtleCommand::*; - - if from.0 == to.0 { - if to.1 == from.1.left() { - Some(Left) - } else if to.1 == from.1.right() { - Some(Right) - } else { - None - } - } else if to.1 == from.1 { - if to.0 == from.0 + from.1.unit() { - Some(Forward(1)) - } else if to.0 == from.0 - from.1.unit() { - Some(Backward(1)) - } else if to.0 == from.0 + Vec3::y() { - Some(Up(1)) - } else if to.0 == from.0 - Vec3::y() { - Some(Down(1)) - } else { - None - } - } else { - None - } -} - #[derive(Serialize, Deserialize, Clone)] pub enum Iota { End,