1
Fork 0

Compare commits

...

10 commits

Author SHA1 Message Date
191fee36e1
probabilistic world. 32 bit keys instead of a string
This causes most of the big benchmarks to fail, and it makes the
positive_many_set three times faster. More integration is required to
create a actual performance benefit by adjusting the search code
2024-06-12 16:04:50 -05:00
0717f6f4f0
fail silently when work is done 2024-01-10 22:04:58 -06:00
d57867e630
more validation 2023-12-31 13:25:45 -06:00
ce8f3ccd6e
updated to match form 2023-12-30 15:11:29 -06:00
5eaee49b7d
assume schematic placement zones are traversable 2023-12-30 11:49:22 -06:00
c49f5735ec
switched to SwarmBot's schematic parser 2023-12-30 10:20:42 -06:00
70c096b6c7
vendored the SwarmBot schematic parsing code (of dubious licensure) 2023-12-30 09:59:17 -06:00
44837cfc1d
printed a tree! 2023-12-29 22:30:01 -06:00
d309a4264c
initial construct implementation 2023-12-29 15:28:42 -06:00
f9531a6961
schematic download 2023-12-29 12:58:17 -06:00
10 changed files with 456 additions and 24 deletions

View file

@ -18,7 +18,7 @@ indoc = "2.0.4"
nalgebra = { version = "0.32.3", features = ["serde-serialize"] }
pathfinding = "4.6.0"
rstar = { version = "0.11.0", features = ["serde"] }
rustmatica = "0.1.1"
rustmatica = { git = "https://github.com/RubixDev/rustmatica" }
serde = { version = "1.0.193", features = ["rc", "derive"] }
serde_json = "1.0.108"
time = { version = "0.3.31", features = ["serde"] }
@ -44,3 +44,7 @@ memoize = "0.4.2"
tracing-appender = "0.2.3"
ron = "0.8.1"
crossbeam = "0.8.3"
reqwest = "0.11.23"
swarmbot-interfaces = { git = "https://github.com/SwarmBotMC/SwarmBot" }
hematite-nbt = "0.5.2"
more-asserts = "0.3.1"

View file

@ -1,23 +1,24 @@
extern crate test;
use std::{sync::Arc, ops::Sub, collections::HashMap};
use std::{collections::HashMap, num::NonZero, ops::Sub, sync::Arc};
use anyhow::{Ok, anyhow};
use anyhow::{anyhow, Context, Ok};
use nalgebra::Vector3;
use rstar::{PointDistance, RTree, RTreeObject, AABB, Envelope};
use serde::{Deserialize, Serialize};
use tokio::sync::{RwLock, OwnedRwLockReadGuard};
use tokio::sync::{RwLock, OwnedRwLockReadGuard, OwnedRwLockWriteGuard};
use crate::{turtle::TurtleCommand, paths::{self, TRANSPARENT}};
use crate::{pallette::{self, Pallette}, paths::{self, TRANSPARENT}, turtle::TurtleCommand};
const CHUNK_SIZE: usize = 8;
const CHUNK_VOLUME: usize = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE;
const CHUNK_VEC: Vec3 = Vec3::new(CHUNK_SIZE as i32, CHUNK_SIZE as i32, CHUNK_SIZE as i32);
#[derive(Serialize, Deserialize)]
pub struct World { // TODO: make r-trees faster than this, for my sanity
pub struct World {
index: HashMap<Vec3, usize>,
data: Vec<Chunk>,
last: Option<usize>,
pallette: Pallette,
}
impl World {
@ -26,11 +27,12 @@ impl World {
index: HashMap::new(),
data: Vec::new(),
last: None,
pallette: Pallette::new(),
}
}
pub fn get(&self, block: Vec3) -> Option<Block> {
let chunk = self.get_chunk(block)?;
Some(chunk.get(block)?)
Some(chunk.get(block, &self.pallette)?)
}
pub fn set(&mut self, block: Block) {
@ -45,11 +47,11 @@ impl World {
match chunk {
Some(chunk) => {
self.data[chunk].set(block).unwrap();
self.data[chunk].set(block, &mut self.pallette).unwrap();
},
None => {
let mut new_chunk = Chunk::new(chunk_coords);
new_chunk.set(block).unwrap();
new_chunk.set(block, &mut self.pallette).unwrap();
self.data.push(new_chunk);
self.index.insert(chunk_coords, self.data.len() - 1);
},
@ -98,6 +100,10 @@ impl SharedWorld {
pub async fn lock(self) -> OwnedRwLockReadGuard<World> {
self.state.read_owned().await
}
pub async fn lock_mut(self) -> OwnedRwLockWriteGuard<World> {
self.state.write_owned().await
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@ -109,31 +115,33 @@ pub struct Block {
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Chunk {
pos: Vec3, /// position in chunk coordinates (world/16)
data: [[[Option<String>;CHUNK_SIZE];CHUNK_SIZE];CHUNK_SIZE]
data: [[[Option<NonZero<u32>>;CHUNK_SIZE];CHUNK_SIZE];CHUNK_SIZE]
}
impl Chunk {
fn new(pos: Vec3) -> Self {
let data :[[[Option<String>;CHUNK_SIZE];CHUNK_SIZE];CHUNK_SIZE]= Default::default();
let data :[[[Option<NonZero<u32>>;CHUNK_SIZE];CHUNK_SIZE];CHUNK_SIZE]= Default::default();
Self {
pos,
data
}
}
fn set(&mut self, pos: Block) -> anyhow::Result<()> {
fn set(&mut self, pos: Block, pallette: &mut Pallette) -> anyhow::Result<()> {
let chunk = self.pos.component_mul(&CHUNK_VEC);
if !self.contains(&pos.pos) {
return Err(anyhow!("out of bounds"));
}
let local: Vector3<usize> = (pos.pos - chunk).map(|n| n as usize);
self.data[local.x][local.y][local.z] = Some(pos.name);
let idx = pallette.number(&pos.name);
self.data[local.x][local.y][local.z] = Some(idx);
Ok(())
}
fn get(&self, pos: Vec3) -> Option<Block> {
fn get(&self, pos: Vec3, pallette: &Pallette) -> Option<Block> {
let chunk = self.pos.component_mul(&CHUNK_VEC);
let local = pos - chunk;
if !self.contains(&pos) {
@ -141,8 +149,10 @@ impl Chunk {
}
let local = local.map(|n| n as usize);
let idx = self.data[local.x][local.y][local.z]?;
Some(Block {
name: self.data[local.x][local.y][local.z].clone()?,
name: pallette.name(idx)?,
pos,
})
}

183
server/src/construct.rs Normal file
View file

@ -0,0 +1,183 @@
use std::{sync::{atomic::{AtomicBool, AtomicUsize, Ordering, AtomicI32}, Arc}, borrow::Cow};
use anyhow::{Context, Ok};
use serde::{Serialize, Deserialize};
use swarmbot_interfaces::types::BlockState;
use tokio::task::AbortHandle;
use tracing::{error, info, trace};
use typetag::serde;
use crate::{blocks::{Vec3, Position, World, Block, SharedWorld, Direction}, mine::{ChunkedTask, fill}, turtle::{TurtleCommander, TurtleCommandResponse, TurtleCommand}, tasks::{Task, TaskState}, vendored::schematic::Schematic};
fn schematic2world(region: &Schematic) -> anyhow::Result<World> {
let mut world = World::new();
let min = region.origin().context("bad schematic")?;
for (position, block) in region.blocks() {
let name = match block {
BlockState::AIR => None,
BlockState(20) => None, // Glass
BlockState(102) => None, // Glass pane
BlockState(95) => None, // Stained glass
BlockState(160) => None, // Stained glass pane
// who cares
_ => Some("terrestria:hemlock_planks")
}.map(|s| s.to_string());
if let Some(name) = name {
let block = Block {
name,
pos: position - min,
};
world.set(block);
}
}
Ok(world)
}
#[derive(Serialize, Deserialize,Clone)]
pub struct BuildSimple {
pos: Vec3,
size: Vec3,
#[serde(skip)]
region: Option<SharedWorld>,
/// Input chest with the block to use, assumed infinite
input: Position,
#[serde(skip_deserializing)]
miners: Arc<AtomicUsize>,
progress: Arc<AtomicI32>,
height: i32,
}
impl BuildSimple {
pub fn new(position: Vec3, schematic: &Schematic, input: Position) -> Self {
let size = Vec3::new(
schematic.width() as i32,
schematic.height() as i32,
schematic.length() as i32,
);
Self {
pos: position,
size,
region: Some(SharedWorld::from_world(schematic2world(schematic).unwrap())),
input,
miners: Default::default(),
progress: Default::default(),
height: size.y,
}
}
async fn place_block(&self, turtle: TurtleCommander, at: Vec3) -> Option<()> {
let mut near = turtle.goto_adjacent(at).await?;
while let TurtleCommandResponse::Failure = turtle.execute(near.place(at)?).await.ret {
if turtle.world().occupied(at).await {
trace!("{at} already filled");
return None;
};
trace!("failed, looking for blocks");
if let Some(slot) = turtle.inventory().await.iter().enumerate()
.filter(|n| n.1.clone().is_some_and(|s| s.count > 0))
.map(|n| n.0).next() {
turtle.execute(TurtleCommand::Select(slot as u32 + 1)).await;
} else {
trace!("docking");
turtle.goto(self.input).await;
for _ in 1..=16 {
turtle.execute(TurtleCommand::SuckFront(64)).await;
}
near = turtle.goto_adjacent(at).await?;
}
}
Some(())
}
async fn build_layer(&self, turtle: TurtleCommander, layer: i32) -> Option<()> {
let layer_size = Vec3::new(self.size.x, 1, self.size.z);
// assume the layer is empty for better pathfinding
let mut world = turtle.world().lock_mut().await;
for point in (0..layer_size.product()).map(|n| fill(layer_size, n)) {
if let None = world.get(point) {
world.set(Block { name: "minecraft:air".into(), pos: point })
}
}
drop(world);
for point in (0..layer_size.product())
.map(|n| fill(layer_size, n)) {
let point = point + Vec3::y() * layer;
trace!("block {point}");
if self.region.as_ref()?.get(point).await.is_none() {
trace!("empty: {:?}", self.region.as_ref()?.get(point).await);
continue;
}
let point = point + self.pos;
if turtle.world().occupied(point).await {
trace!("already full: {:?}", turtle.world().get(point).await);
continue;
}
self.place_block(turtle.clone(), point).await;
}
Some(())
}
}
#[serde]
impl Task for BuildSimple {
fn run(&mut self,turtle:TurtleCommander) -> AbortHandle {
let owned = self.clone();
tokio::spawn(async move {
if turtle.fuel() < 5000 {
turtle.dock().await;
}
let layer = owned.progress.fetch_add(1, Ordering::AcqRel);
if owned.height < layer {
error!("scheduled layer out of range");
return;
}
info!("layer {}", layer);
if let None = owned.build_layer(turtle, layer).await {
error!("building layer {} failed", layer);
owned.progress.fetch_sub(1, Ordering::AcqRel);
} else {
trace!("building layer {} successful", layer);
}
owned.miners.fetch_sub(1, Ordering::AcqRel);
}).abort_handle()
}
fn poll(&mut self) -> TaskState {
if self.region.is_none() {
error!("attempted to restart schematic printing, which is unimplemented");
return TaskState::Complete;
}
let layer = self.progress.load(Ordering::SeqCst);
if layer > self.height {
return TaskState::Complete;
}
let only = self.miners.fetch_update(Ordering::AcqRel, Ordering::Acquire, |n| {
if n < 1 {
Some(n+1)
}else {
None
}
}).is_ok();
if only {
return TaskState::Ready(Position::new(self.pos, Direction::North));
}
TaskState::Waiting
}
}

View file

@ -1,13 +1,14 @@
use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
use anyhow::{Ok, Context, anyhow};
use anyhow::{Ok, Context, anyhow, Result};
use axum::{Router, routing::post, extract::State, Json};
use hyper::body::Buf;
use serde::{Deserialize, Serialize};
use tokio::task::AbortHandle;
use tracing::{info, error};
use typetag::serde;
use crate::{SharedControl, mine::{Remove, ChunkedTask, Quarry}, blocks::{Vec3, Direction, Position}, tasks::{TaskState, Task}, turtle::TurtleCommander};
use crate::{SharedControl, mine::{Remove, ChunkedTask, Quarry}, blocks::{Vec3, Direction, Position}, tasks::{TaskState, Task}, turtle::TurtleCommander, construct::BuildSimple, vendored::schematic::Schematic};
pub fn forms_api() -> Router<SharedControl> {
Router::new()
@ -82,7 +83,7 @@ struct GoogleOmniForm{
y2: Option<String>,
#[serde(rename(deserialize = "Z coordinate (to)"))]
z2: Option<String>,
#[serde(rename(deserialize = "Upload a .litematic file"))]
#[serde(rename(deserialize = "Upload a .schematic file"))]
schematic: Option<Vec<String>>,
}
@ -104,7 +105,24 @@ async fn omni_inner(state: SharedControl, req: GoogleOmniForm) -> anyhow::Result
let mut schedule = state.tasks.lock().await;
let position = { Vec3::new(req.x.parse()?,req.y.parse()?,req.z.parse()?) };
match req.operation {
GoogleOmniFormMode::Schematic => Err(anyhow!("unimplemented"))?,
GoogleOmniFormMode::Schematic => {
let schematic = req.schematic.context("no schematic uploaded")?.get(0).context("zero schematics")?.to_owned();
let schematic = reqwest::get(format!("https://docs.google.com/uc?export=download&id={schematic}")).await?.bytes().await?;
// TODO: not this
let input = Position::new(
Vec3::new(-22,91,42),
Direction::West,
);
// this converts to my memory representation so it can take a while
let builder = tokio::task::spawn_blocking(move || {
let schematic = Schematic::load(&mut schematic.reader()).unwrap();
BuildSimple::new(position, &schematic, input)
}).await.unwrap();
schedule.add_task(Box::new(builder));
},
GoogleOmniFormMode::RemoveVein => {
let block = req.block.context("missing block name")?;
info!("new remove {block} command from the internet at {position}");
@ -117,7 +135,19 @@ async fn omni_inner(state: SharedControl, req: GoogleOmniForm) -> anyhow::Result
req.z2.context("z2")?.parse()?,
);
let quarry = Quarry::new(position, upper);
let min = Vec3::new(
upper.x.min(position.x),
upper.y.min(position.y),
upper.z.min(position.z),
);
let max = Vec3::new(
upper.x.max(position.x),
upper.y.max(position.y),
upper.z.max(position.z),
);
let quarry = Quarry::new(min, max);
schedule.add_task(Box::new(quarry));
},
GoogleOmniFormMode::Goto => {

View file

@ -30,9 +30,11 @@ use indoc::formatdoc;
use crate::blocks::Block;
mod blocks;
mod pallette;
mod names;
mod mine;
mod fell;
mod construct;
mod paths;
mod safe_kill;
mod turtle;
@ -40,6 +42,7 @@ mod turtle_api;
mod tasks;
mod depot;
mod googleforms;
mod vendored;
static PORT: OnceCell<u16> = OnceCell::const_new();
static SAVE: OnceCell<path::PathBuf> = OnceCell::const_new();
@ -70,6 +73,7 @@ async fn main() -> Result<(), Error> {
.with_target("server::googleforms", Level::TRACE)
.with_target("server::fell", Level::WARN)
.with_target("server::mine", Level::INFO)
.with_target("server::construct", Level::INFO)
.with_target("server::depot", Level::TRACE);
let log = fs::OpenOptions::new().append(true).create(true).open(SAVE.get().unwrap().join("avarus.log")).await?;

View file

@ -466,7 +466,7 @@ impl ChunkedTask {
}
#[derive(Clone)]
struct ChunkedTaskGuard {
pub struct ChunkedTaskGuard {
parent: ChunkedTask,
chunk: i32,
complete: bool,
@ -481,16 +481,16 @@ impl ChunkedTaskGuard {
}
}
fn id(&self) -> i32 {
pub fn id(&self) -> i32 {
self.chunk
}
fn finish(mut self) {
pub fn finish(mut self) {
self.parent.mark_done(self.chunk);
self.complete = true;
}
fn cancel(self) {
pub fn cancel(self) {
drop(self) // nop
}
}

30
server/src/pallette.rs Normal file
View file

@ -0,0 +1,30 @@
use std::{collections::HashMap, hash::{DefaultHasher, Hasher, SipHasher}, num::NonZero};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Pallette {
names: HashMap<u32, String>,
}
impl Pallette {
pub fn new() -> Self {
let names = HashMap::new();
Self { names }
}
pub fn number(&mut self, name: &str) -> NonZero<u32> {
let mut hasher = DefaultHasher::new();
hasher.write(name.as_bytes());
let mut key = hasher.finish() as u32;
if key == 0 {key += 1}; // double the collisions!
self.names.insert(key,name.to_owned());
let key = unsafe { NonZero::new_unchecked(key) }; // SAFETY: see
// two lines above
key
}
pub fn name(&self, number: NonZero<u32>) -> Option<String> {
self.names.get(&number.get()).map(|s| s.to_owned())
}
}

View file

@ -3,6 +3,8 @@ use tracing::trace;
use tokio;
use blocks::Vec3;
use tokio::time::Instant;
use crate::blocks::Direction;
use crate::construct::BuildSimple;
use crate::fell::TreeFarm;
use crate::mine::Mine;
use crate::mine::Quarry;
@ -10,6 +12,7 @@ use crate::turtle::IDLE_TIME;
use crate::turtle::TurtleCommandResponse;
use crate::turtle::TurtleCommander;
use crate::turtle::TurtleInfo;
use crate::vendored::schematic::Schematic;
use axum::extract::Path;
use crate::turtle::TurtleCommand;
use crate::names::Name;
@ -51,6 +54,7 @@ pub fn turtle_api() -> Router<SharedControl> {
.route("/:id/register", get(register_turtle))
.route("/createTreeFarm", post(fell))
.route("/createMine", post(dig))
.route("/build", post(build))
.route("/registerDepot", post(new_depot))
.route("/pollScheduler", get(poll))
.route("/shutdown", get(shutdown)) // probably tramples the rfc
@ -286,6 +290,29 @@ pub(crate) async fn command(
Json(command)
}
pub(crate) async fn build(
State(state): State<SharedControl>,
Json(req): Json<Vec3>,
) -> &'static str {
let state = state.read().await;
let mut schedule = state.tasks.lock().await;
let schematic = Schematic::load(&mut fs::File::open("schematics/greek-athelete1.schematic").await.unwrap().into_std().await).unwrap();
let input = Position::new(
Vec3::new(53,73,77),
Direction::West,
);
// this converts to my memory representation so it can take a while
let builder = tokio::task::spawn_blocking(move || {
BuildSimple::new(req, &schematic, input)
}).await.unwrap();
schedule.add_task(Box::new(builder));
"ACK"
}
pub(crate) async fn client() -> String {
formatdoc!(r#"
local ipaddr = {}

View file

@ -0,0 +1,2 @@
/// MIT (Andrew Gazelka) from [SwarmBot](https://github.com/SwarmBotMC/SwarmBot/tree/b25367843f30ae72797db694d95ceeb0d49da82a)
pub mod schematic;

View file

@ -0,0 +1,142 @@
use std::io::Read;
use anyhow::Context;
use swarmbot_interfaces::types::BlockState;
use serde::{Deserialize, Serialize};
use crate::blocks::Vec3;
/// The `WorldEdit` schematic format
/// <https://minecraft.fandom.com/wiki/Schematic_file_format>
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
pub struct Schematic {
pub width: i16,
pub height: i16,
pub length: i16,
materials: String,
blocks: Vec<i8>,
add_blocks: Option<Vec<i8>>,
data: Vec<i8>,
w_e_origin_x: Option<i32>,
w_e_origin_y: Option<i32>,
w_e_origin_z: Option<i32>,
w_e_offset_x: Option<i32>,
w_e_offset_y: Option<i32>,
w_e_offset_z: Option<i32>,
}
impl Schematic {
#[allow(unused)]
pub const fn volume(&self) -> u64 {
let v = (self.width as i64) * (self.height as i64) * (self.length as i64);
v.abs_diff(0)
}
#[allow(unused)]
pub fn load(reader: &mut impl Read) -> anyhow::Result<Self> {
let res: Result<Self, _> =
nbt::from_gzip_reader(reader).context("could not load schematic");
res
}
#[allow(unused)]
pub fn is_valid(&self) -> bool {
self.volume() == self.blocks.len() as u64
}
#[allow(unused)]
pub fn origin(&self) -> Option<Vec3> {
match (self.w_e_origin_x, self.w_e_origin_y, self.w_e_origin_z) {
(Some(x), Some(y), Some(z)) => Some(Vec3::new(x, y, z)),
_ => None,
}
}
#[allow(unused)]
pub fn offset(&self) -> Option<Vec3> {
match (self.w_e_offset_x, self.w_e_offset_y, self.w_e_offset_z) {
(Some(x), Some(y), Some(z)) => Some(Vec3::new(x, y, z)),
_ => None,
}
}
#[allow(unused, clippy::unwrap_used)]
pub fn width(&self) -> u64 {
u64::try_from(self.width).unwrap()
}
#[allow(unused, clippy::unwrap_used)]
pub fn height(&self) -> u64 {
u64::try_from(self.width).unwrap()
}
#[allow(unused, clippy::unwrap_used)]
pub fn length(&self) -> u64 {
u64::try_from(self.length).unwrap()
}
#[allow(unused, clippy::unwrap_used, clippy::indexing_slicing)]
pub fn blocks(&self) -> impl Iterator<Item = (Vec3, BlockState)> + '_ {
let origin = self.origin().unwrap_or_default();
(0..self.volume()).map(move |idx| {
let x = idx % self.width();
let leftover = idx / self.width();
let z = leftover % self.length();
let y = leftover / self.length();
let location = Vec3::new(x as i32, y as i32, z as i32) + origin;
let id = self.blocks[idx as usize].abs_diff(0);
let data = self.data[idx as usize].abs_diff(0);
let state = BlockState::from(u32::from(id), u16::from(data));
(location, state)
})
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashMap, fs::OpenOptions};
use more_asserts::*;
use crate::blocks::Vec3;
use super::Schematic;
#[test]
fn test_load() {
let mut reader = OpenOptions::new()
.read(true)
.open("test-data/parkour.schematic")
.unwrap();
let schematic = Schematic::load(&mut reader).unwrap();
assert!(schematic.is_valid());
let origin = schematic.origin().unwrap_or_default();
let mut map = HashMap::new();
for (loc, state) in schematic.blocks() {
assert_ge!(loc.x, origin.x);
assert_lt!(loc.x, origin.x + schematic.width as i32);
assert_ge!(loc.y, origin.y);
assert_lt!(loc.y, origin.y + schematic.height as i32);
assert_ge!(loc.z, origin.z);
assert_lt!(loc.z, origin.z + schematic.length as i32);
map.insert(loc, state);
}
let stained_glass = map[&Vec3::new(-162, 81, -357)];
assert_eq!(stained_glass.id(), 95);
}
}