Compare commits
10 commits
c4e7ac9f8b
...
191fee36e1
Author | SHA1 | Date | |
---|---|---|---|
191fee36e1 | |||
0717f6f4f0 | |||
d57867e630 | |||
ce8f3ccd6e | |||
5eaee49b7d | |||
c49f5735ec | |||
70c096b6c7 | |||
44837cfc1d | |||
d309a4264c | |||
f9531a6961 |
10 changed files with 456 additions and 24 deletions
|
@ -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"
|
||||
|
|
|
@ -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
183
server/src/construct.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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 => {
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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
30
server/src/pallette.rs
Normal 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())
|
||||
}
|
||||
}
|
|
@ -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 = {}
|
||||
|
|
2
server/src/vendored/mod.rs
Normal file
2
server/src/vendored/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
/// MIT (Andrew Gazelka) from [SwarmBot](https://github.com/SwarmBotMC/SwarmBot/tree/b25367843f30ae72797db694d95ceeb0d49da82a)
|
||||
pub mod schematic;
|
142
server/src/vendored/schematic.rs
Normal file
142
server/src/vendored/schematic.rs
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue