diff --git a/.cargo/config.toml b/.cargo/config.toml index 8640b80..25273b4 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -5,6 +5,7 @@ rustflags = [ "-Zfmt-debug=none", "-Zunstable-options", "-Cpanic=immediate-abort", + "-Zpanic_abort_tests", ] [unstable] diff --git a/README.md b/README.md index fd50934..2977a7d 100644 --- a/README.md +++ b/README.md @@ -58,20 +58,23 @@ For common peripherals you can rely on typed wrappers. The redstone helper mirrors the behaviour of `redstone.lua` and performs basic clamping/parsing: ```rust -use oc2r_rust::devices::redstone::{Redstone, Side}; -use oc2r_rust::{DeviceBus, Result, DEFAULT_DEVICE_PATH}; +use oc2r_rust::{DeviceBus, Redstone, Result, Side, DEFAULT_DEVICE_PATH}; fn main() -> Result<()> { let mut bus = DeviceBus::connect(DEFAULT_DEVICE_PATH)?; let mut redstone = Redstone::attach(&mut bus)? .expect("no redstone interface attached"); - redstone.set_output_state(Side::East, true)?; - println!("in: {}", redstone.input(Side::East)?); - Ok(()) +redstone.set_output_state(Side::East, true)?; +println!("in: {}", redstone.input(Side::East)?); +Ok(()) } ``` +Available wrappers: `Redstone`, `EnergyStorage`, `FluidHandler`, `ItemHandler`, +`Cpu`, `SoundCard`, `BlockOperations`, `InventoryOperations`, and +`FileImportExport`. + ### Building the examples Two runnable examples live in `examples/`: diff --git a/examples/redstone.rs b/examples/redstone.rs index c2ea454..6d721af 100644 --- a/examples/redstone.rs +++ b/examples/redstone.rs @@ -1,7 +1,6 @@ // Build with: cross build --release --example redstone -use oc2r_rust::devices::redstone::{Redstone, Side}; -use oc2r_rust::{DEFAULT_DEVICE_PATH, DeviceBus, Result}; +use oc2r_rust::{DEFAULT_DEVICE_PATH, DeviceBus, Redstone, Result, Side}; use std::env; use std::str::FromStr; diff --git a/src/devices/block_operations.rs b/src/devices/block_operations.rs new file mode 100644 index 0000000..dae387e --- /dev/null +++ b/src/devices/block_operations.rs @@ -0,0 +1,56 @@ +use crate::Result; +use crate::value::JsonValueExt; + +crate::devices::define_wrapper!( + BlockOperations, + "block_operations", + "Wrapper for the block operations robot module." +); + +impl<'bus> BlockOperations<'bus> { + pub fn excavate(&mut self) -> Result { + self.device.call("excavate", ())?.into_bool("excavate") + } + + pub fn excavate_on(&mut self, side: RobotSide) -> Result { + self.device + .call("excavate", (side.as_str(),))? + .into_bool("excavate") + } + + pub fn place(&mut self) -> Result { + self.device.call("place", ())?.into_bool("place") + } + + pub fn place_on(&mut self, side: RobotSide) -> Result { + self.device + .call("place", (side.as_str(),))? + .into_bool("place") + } + + pub fn durability(&mut self) -> Result { + self.device.call("durability", ())?.into_i64("durability") + } + + pub fn repair(&mut self) -> Result { + self.device.call("repair", ())?.into_bool("repair") + } +} + +/// Robot operation sides recognised by OC2R. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RobotSide { + Front, + Up, + Down, +} + +impl RobotSide { + pub const fn as_str(self) -> &'static str { + match self { + RobotSide::Front => "front", + RobotSide::Up => "up", + RobotSide::Down => "down", + } + } +} diff --git a/src/devices/cpu.rs b/src/devices/cpu.rs new file mode 100644 index 0000000..8ec0d72 --- /dev/null +++ b/src/devices/cpu.rs @@ -0,0 +1,12 @@ +use crate::Result; +use crate::value::JsonValueExt; + +crate::devices::define_wrapper!(Cpu, "cpu", "Wrapper exposing CPU metadata."); + +impl<'bus> Cpu<'bus> { + pub fn frequency(&mut self) -> Result { + self.device + .call("getFrequency", ())? + .into_i64("getFrequency") + } +} diff --git a/src/devices/energy_storage.rs b/src/devices/energy_storage.rs new file mode 100644 index 0000000..cfc5cbd --- /dev/null +++ b/src/devices/energy_storage.rs @@ -0,0 +1,34 @@ +use crate::Result; +use crate::value::JsonValueExt; + +crate::devices::define_wrapper!( + EnergyStorage, + "energy_storage", + "Wrapper for OC2R energy storage devices." +); + +impl<'bus> EnergyStorage<'bus> { + pub fn energy_stored(&mut self) -> Result { + self.device + .call("getEnergyStored", ())? + .into_i64("getEnergyStored") + } + + pub fn max_energy_stored(&mut self) -> Result { + self.device + .call("getMaxEnergyStored", ())? + .into_i64("getMaxEnergyStored") + } + + pub fn can_extract(&mut self) -> Result { + self.device + .call("canExtractEnergy", ())? + .into_bool("canExtractEnergy") + } + + pub fn can_receive(&mut self) -> Result { + self.device + .call("canReceiveEnergy", ())? + .into_bool("canReceiveEnergy") + } +} diff --git a/src/devices/file_import_export.rs b/src/devices/file_import_export.rs new file mode 100644 index 0000000..4029656 --- /dev/null +++ b/src/devices/file_import_export.rs @@ -0,0 +1,87 @@ +use crate::value::JsonValueExt; +use crate::{Error, JsonValue, Result}; +use miniserde::json::{Array, Number}; + +crate::devices::define_wrapper!( + FileImportExport, + "file_import_export", + "Wrapper for the file import/export card." +); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ImportedFileInfo { + pub name: Option, + pub size: u64, +} + +impl<'bus> FileImportExport<'bus> { + pub fn reset(&mut self) -> Result<()> { + self.device.call("reset", ()).map(|_| ()) + } + + pub fn begin_export(&mut self, name: &str) -> Result<()> { + self.device.call("beginExportFile", (name,)).map(|_| ()) + } + + pub fn write_export_chunk(&mut self, chunk: &[u8]) -> Result<()> { + let payload = JsonValue::Array( + chunk + .iter() + .copied() + .map(|byte| JsonValue::Number(Number::U64(byte as u64))) + .collect::(), + ); + self.device.call("writeExportFile", (payload,)).map(|_| ()) + } + + pub fn finish_export(&mut self) -> Result<()> { + self.device.call("finishExportFile", ()).map(|_| ()) + } + + pub fn request_import(&mut self) -> Result { + self.device + .call("requestImportFile", ())? + .into_bool("requestImportFile") + } + + pub fn begin_import(&mut self) -> Result> { + match self.device.call("beginImportFile", ())? { + JsonValue::Null => Ok(None), + value => { + let mut object = value.into_object("beginImportFile")?; + let name = match object.remove("name") { + Some(JsonValue::Null) | None => None, + Some(other) => Some(other.into_string("beginImportFile.name")?), + }; + let size_value = object + .remove("size") + .ok_or_else(|| Error::Protocol("beginImportFile missing size".into()))?; + let size = size_value.into_u64("beginImportFile.size")?; + Ok(Some(ImportedFileInfo { name, size })) + } + } + } + + pub fn read_import_chunk(&mut self) -> Result>> { + match self.device.call("readImportFile", ())? { + JsonValue::Null => Ok(None), + value => { + let bytes = value + .into_array("readImportFile")? + .into_iter() + .map(|item| { + let value = item.into_u64("readImportFile byte")?; + if value > 255 { + Err(Error::Protocol(format!( + "readImportFile returned byte value {value} > 255" + ))) + } else { + Ok(value as u8) + } + }) + .collect::>>()?; + Ok(Some(bytes)) + } + } + } +} diff --git a/src/devices/fluid_handler.rs b/src/devices/fluid_handler.rs new file mode 100644 index 0000000..2f380b0 --- /dev/null +++ b/src/devices/fluid_handler.rs @@ -0,0 +1,29 @@ +use crate::value::JsonValueExt; +use crate::{JsonValue, Result}; + +crate::devices::define_wrapper!( + FluidHandler, + "fluid_handler", + "Wrapper for Forge fluid handler devices." +); + +impl<'bus> FluidHandler<'bus> { + pub fn tank_count(&mut self) -> Result { + self.device + .call("getFluidTanks", ())? + .into_i64("getFluidTanks") + } + + pub fn fluid_in_tank(&mut self, tank: usize) -> Result> { + match self.device.call("getFluidInTank", (tank,))? { + JsonValue::Null => Ok(None), + other => Ok(Some(other)), + } + } + + pub fn tank_capacity(&mut self, tank: usize) -> Result { + self.device + .call("getFluidTankCapacity", (tank,))? + .into_i64("getFluidTankCapacity") + } +} diff --git a/src/devices/inventory_operations.rs b/src/devices/inventory_operations.rs new file mode 100644 index 0000000..8a6f429 --- /dev/null +++ b/src/devices/inventory_operations.rs @@ -0,0 +1,100 @@ +use crate::Result; +use crate::devices::RobotSide; +use crate::value::JsonValueExt; + +crate::devices::define_wrapper!( + InventoryOperations, + "inventory_operations", + "Wrapper for the inventory operations robot module." +); + +impl<'bus> InventoryOperations<'bus> { + pub fn move_items(&mut self, from_slot: usize, into_slot: usize, count: u32) -> Result<()> { + self.device + .call("move", (from_slot, into_slot, count)) + .map(|_| ()) + } + + pub fn drop(&mut self, count: u32) -> Result { + self.drop_internal(count, None) + } + + pub fn drop_on(&mut self, count: u32, side: RobotSide) -> Result { + self.drop_internal(count, Some(side)) + } + + pub fn drop_into(&mut self, into_slot: usize, count: u32) -> Result { + self.drop_into_internal(into_slot, count, None) + } + + pub fn drop_into_on(&mut self, into_slot: usize, count: u32, side: RobotSide) -> Result { + self.drop_into_internal(into_slot, count, Some(side)) + } + + pub fn take(&mut self, count: u32) -> Result { + self.take_internal(count, None) + } + + pub fn take_on(&mut self, count: u32, side: RobotSide) -> Result { + self.take_internal(count, Some(side)) + } + + pub fn take_from_slot(&mut self, from_slot: usize, count: u32) -> Result { + self.take_from_internal(from_slot, count, None) + } + + pub fn take_from_slot_on( + &mut self, + from_slot: usize, + count: u32, + side: RobotSide, + ) -> Result { + self.take_from_internal(from_slot, count, Some(side)) + } + + fn drop_internal(&mut self, count: u32, side: Option) -> Result { + let value = match side { + Some(side) => self.device.call("drop", (count, side.as_str()))?, + None => self.device.call("drop", (count,))?, + }; + value.into_i64("drop") + } + + fn drop_into_internal( + &mut self, + into_slot: usize, + count: u32, + side: Option, + ) -> Result { + let value = match side { + Some(side) => self + .device + .call("dropInto", (into_slot, count, side.as_str()))?, + None => self.device.call("dropInto", (into_slot, count))?, + }; + value.into_i64("dropInto") + } + + fn take_internal(&mut self, count: u32, side: Option) -> Result { + let value = match side { + Some(side) => self.device.call("take", (count, side.as_str()))?, + None => self.device.call("take", (count,))?, + }; + value.into_i64("take") + } + + fn take_from_internal( + &mut self, + from_slot: usize, + count: u32, + side: Option, + ) -> Result { + let value = match side { + Some(side) => self + .device + .call("takeFrom", (from_slot, count, side.as_str()))?, + None => self.device.call("takeFrom", (from_slot, count))?, + }; + value.into_i64("takeFrom") + } +} diff --git a/src/devices/item_handler.rs b/src/devices/item_handler.rs new file mode 100644 index 0000000..eff918a --- /dev/null +++ b/src/devices/item_handler.rs @@ -0,0 +1,29 @@ +use crate::value::JsonValueExt; +use crate::{JsonValue, Result}; + +crate::devices::define_wrapper!( + ItemHandler, + "item_handler", + "Wrapper for Forge item handler devices." +); + +impl<'bus> ItemHandler<'bus> { + pub fn slot_count(&mut self) -> Result { + self.device + .call("getItemSlotCount", ())? + .into_i64("getItemSlotCount") + } + + pub fn stack_in_slot(&mut self, slot: usize) -> Result> { + match self.device.call("getItemStackInSlot", (slot,))? { + JsonValue::Null => Ok(None), + other => Ok(Some(other)), + } + } + + pub fn slot_limit(&mut self, slot: usize) -> Result { + self.device + .call("getItemSlotLimit", (slot,))? + .into_i64("getItemSlotLimit") + } +} diff --git a/src/devices/mod.rs b/src/devices/mod.rs index 839705a..ff4d7b6 100644 --- a/src/devices/mod.rs +++ b/src/devices/mod.rs @@ -1 +1,65 @@ +macro_rules! define_wrapper { + ($name:ident, $type_name:literal, $doc:literal) => { + #[doc = $doc] + pub struct $name<'bus> { + pub(crate) device: crate::Device<'bus>, + } + + impl<'bus> $name<'bus> { + pub const TYPE_NAME: &'static str = $type_name; + + pub fn attach(bus: &'bus mut crate::DeviceBus) -> crate::Result> { + crate::devices::attach_device(bus, Self::TYPE_NAME) + .map(|opt| opt.map(Self::from_device)) + } + + pub fn from_device(device: crate::Device<'bus>) -> Self { + Self { device } + } + + pub fn info(&self) -> &crate::DeviceEntry { + self.device.info() + } + + pub fn as_device(&mut self) -> &mut crate::Device<'bus> { + &mut self.device + } + + pub fn into_device(self) -> crate::Device<'bus> { + self.device + } + } + }; +} + +pub(crate) use define_wrapper; + +pub(crate) fn attach_device<'bus>( + bus: &'bus mut crate::DeviceBus, + type_name: &str, +) -> crate::Result>> { + match bus.find(type_name)? { + Some(entry) => bus.device(&entry.device_id), + None => Ok(None), + } +} + +pub mod block_operations; +pub mod cpu; +pub mod energy_storage; +pub mod file_import_export; +pub mod fluid_handler; +pub mod inventory_operations; +pub mod item_handler; pub mod redstone; +pub mod sound; + +pub use block_operations::{BlockOperations, RobotSide}; +pub use cpu::Cpu; +pub use energy_storage::EnergyStorage; +pub use file_import_export::{FileImportExport, ImportedFileInfo}; +pub use fluid_handler::FluidHandler; +pub use inventory_operations::InventoryOperations; +pub use item_handler::ItemHandler; +pub use redstone::{ParseSideError, Redstone, Side}; +pub use sound::SoundCard; diff --git a/src/devices/redstone.rs b/src/devices/redstone.rs index cb191d8..c6fb8de 100644 --- a/src/devices/redstone.rs +++ b/src/devices/redstone.rs @@ -1,59 +1,35 @@ -use crate::bus::Device; -use crate::{DeviceBus, DeviceEntry, Error, JsonValue, Result}; -use miniserde::json::Number; +use crate::Result; +use crate::value::JsonValueExt; use std::fmt; use std::str::FromStr; -/// Typed wrapper around the HLAPI redstone interface. -pub struct Redstone<'bus> { - device: Device<'bus>, -} +crate::devices::define_wrapper!( + Redstone, + "redstone", + "Typed wrapper around the HLAPI redstone interface." +); impl<'bus> Redstone<'bus> { - pub const TYPE_NAME: &'static str = "redstone"; - - /// Attempt to find and wrap the first redstone-compatible device on the bus. - pub fn attach(bus: &'bus mut DeviceBus) -> Result> { - let Some(entry) = bus.find(Self::TYPE_NAME)? else { - return Ok(None); - }; - Ok(bus.device(&entry.device_id)?.map(|device| Self { device })) - } - - /// Create a typed wrapper from an existing [`Device`]. - pub fn from_device(device: Device<'bus>) -> Self { - Self { device } - } - - /// Access the underlying device entry metadata. - pub fn info(&self) -> &DeviceEntry { - self.device.info() - } - - /// Borrow the underlying dynamic device handle. - pub fn as_device(&mut self) -> &mut Device<'bus> { - &mut self.device - } - /// Read the redstone input level for `side`. pub fn input(&mut self, side: Side) -> Result { - let value = self.device.call("getRedstoneInput", (side.as_str(),))?; - expect_number(value, "getRedstoneInput") + self.device + .call("getRedstoneInput", (side.as_str(),))? + .into_f64("getRedstoneInput") } /// Read the currently configured output level for `side`. pub fn output(&mut self, side: Side) -> Result { - let value = self.device.call("getRedstoneOutput", (side.as_str(),))?; - expect_number(value, "getRedstoneOutput") + self.device + .call("getRedstoneOutput", (side.as_str(),))? + .into_f64("getRedstoneOutput") } /// Set the output strength on `side` (0-15). pub fn set_output_level(&mut self, side: Side, level: u8) -> Result<()> { - let clamped = level.min(15) as u64; - let _ = self - .device - .call("setRedstoneOutput", (side.as_str(), clamped))?; - Ok(()) + let clamped = level.min(15); + self.device + .call("setRedstoneOutput", (side.as_str(), clamped)) + .map(|_| ()) } /// Convenience for enabling (`true`) or disabling (`false`) an output. @@ -62,17 +38,6 @@ impl<'bus> Redstone<'bus> { } } -fn expect_number(value: JsonValue, context: &str) -> Result { - match value { - JsonValue::Number(Number::F64(v)) => Ok(v), - JsonValue::Number(Number::I64(v)) => Ok(v as f64), - JsonValue::Number(Number::U64(v)) => Ok(v as f64), - other => Err(Error::Protocol(format!( - "expected numeric response from {context}, got {other:?}" - ))), - } -} - /// Redstone interface sides used by OC2R. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Side { diff --git a/src/devices/sound.rs b/src/devices/sound.rs new file mode 100644 index 0000000..ee60d45 --- /dev/null +++ b/src/devices/sound.rs @@ -0,0 +1,38 @@ +use crate::Result; +use crate::value::JsonValueExt; + +crate::devices::define_wrapper!( + SoundCard, + "sound", + "Wrapper around the OC2R sound card HLAPI." +); + +impl<'bus> SoundCard<'bus> { + pub fn play(&mut self, name: &str) -> Result<()> { + self.device.call("playSound", (name,)).map(|_| ()) + } + + pub fn play_with_volume(&mut self, name: &str, volume: f32) -> Result<()> { + self.device.call("playSound", (name, volume)).map(|_| ()) + } + + pub fn play_with_volume_and_pitch( + &mut self, + name: &str, + volume: f32, + pitch: f32, + ) -> Result<()> { + self.device + .call("playSound", (name, volume, pitch)) + .map(|_| ()) + } + + pub fn find(&mut self, query: &str) -> Result> { + self.device + .call("findSound", (query,))? + .into_array("findSound")? + .into_iter() + .map(|value| value.into_string("findSound result")) + .collect::>>() + } +} diff --git a/src/lib.rs b/src/lib.rs index a1e186f..87f51e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,10 @@ mod transport; mod value; pub use bus::{Device, DeviceBus}; -pub use devices::redstone::{ParseSideError, Redstone, Side}; +pub use devices::{ + BlockOperations, Cpu, EnergyStorage, FileImportExport, FluidHandler, ImportedFileInfo, + InventoryOperations, ItemHandler, ParseSideError, Redstone, RobotSide, Side, SoundCard, +}; pub use error::{Error, Result}; pub use rpc::{DeviceEntry, JsonValue, MethodEntry, MethodParameter}; pub use value::{IntoJsonArgs, IntoJsonValue}; diff --git a/src/value.rs b/src/value.rs index 22dbc46..440e4d1 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,5 +1,7 @@ +use crate::error::{Error, Result}; use crate::rpc::JsonValue; use miniserde::json::Number; +use std::collections::BTreeMap; /// Convert common Rust types into `JsonValue`. pub trait IntoJsonValue { @@ -125,3 +127,88 @@ where self.iter().cloned().map(IntoJsonValue::into_json).collect() } } + +/// Convenience conversions from raw JSON values returned by the bus. +pub trait JsonValueExt { + fn into_bool(self, context: &str) -> Result; + fn into_i64(self, context: &str) -> Result; + fn into_u64(self, context: &str) -> Result; + fn into_f64(self, context: &str) -> Result; + fn into_string(self, context: &str) -> Result; + fn into_array(self, context: &str) -> Result>; + fn into_object(self, context: &str) -> Result>; +} + +impl JsonValueExt for JsonValue { + fn into_bool(self, context: &str) -> Result { + match self { + JsonValue::Bool(value) => Ok(value), + other => Err(Error::Protocol(format!( + "expected boolean from {context}, got {other:?}" + ))), + } + } + + fn into_i64(self, context: &str) -> Result { + match self { + JsonValue::Number(Number::I64(value)) => Ok(value), + JsonValue::Number(Number::U64(value)) => value.try_into().map_err(|_| { + Error::Protocol(format!("{context} returned value too large for i64")) + }), + JsonValue::Number(Number::F64(value)) => Ok(value as i64), + other => Err(Error::Protocol(format!( + "expected integer from {context}, got {other:?}" + ))), + } + } + + fn into_u64(self, context: &str) -> Result { + match self { + JsonValue::Number(Number::U64(value)) => Ok(value), + JsonValue::Number(Number::I64(value)) if value >= 0 => Ok(value as u64), + JsonValue::Number(Number::F64(value)) if value >= 0.0 => Ok(value as u64), + other => Err(Error::Protocol(format!( + "expected unsigned integer from {context}, got {other:?}" + ))), + } + } + + fn into_f64(self, context: &str) -> Result { + match self { + JsonValue::Number(Number::F64(value)) => Ok(value), + JsonValue::Number(Number::I64(value)) => Ok(value as f64), + JsonValue::Number(Number::U64(value)) => Ok(value as f64), + other => Err(Error::Protocol(format!( + "expected floating point number from {context}, got {other:?}" + ))), + } + } + + fn into_string(self, context: &str) -> Result { + match self { + JsonValue::String(value) => Ok(value), + other => Err(Error::Protocol(format!( + "expected string from {context}, got {other:?}" + ))), + } + } + + fn into_array(self, context: &str) -> Result> { + match self { + JsonValue::Array(array) => Ok(array.into_iter().collect()), + JsonValue::Null => Ok(Vec::new()), + other => Err(Error::Protocol(format!( + "expected array from {context}, got {other:?}" + ))), + } + } + + fn into_object(self, context: &str) -> Result> { + match self { + JsonValue::Object(object) => Ok(object.into_iter().collect()), + other => Err(Error::Protocol(format!( + "expected object from {context}, got {other:?}" + ))), + } + } +}