use rgb::RGB8;
use serde::{Deserialize, Serialize};
use std::{
error::Error as StdError,
time::{Duration, Instant},
};
use thiserror::Error;
use uuid::Uuid;
use crate::{
client::{Device, DeviceBase, HlapiClient, HlapiClientError},
devices::{ItemStack, TankContentInfo},
};
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum RobotSide {
Front,
Up,
Down,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
#[allow(missing_docs)] pub enum Facing {
North,
East,
South,
West,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Position {
pub x: i32,
pub y: i32,
pub z: i32,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum DetectResult {
Solid,
Fluid,
Air,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "UPPERCASE")]
pub enum ActionResult {
Incomplete,
Success,
Failure,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
#[allow(missing_docs)] pub enum MoveDirection {
Forward,
Backward,
Upward,
Downward,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
#[allow(missing_docs)] pub enum TurnDirection {
Left,
Right,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(transparent)]
#[serde(transparent)]
pub struct ActionId(u32);
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ActionCompleted {
pub action_id: ActionId,
pub result: ActionResult,
}
#[derive(Debug, Clone)]
pub struct Robot {
client: HlapiClient,
uuid: Uuid,
}
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum RobotError {
#[error("HLAPI client error: {0}")]
HlapiClientError(#[from] HlapiClientError),
#[error("Status bar fill value invalid")]
FillValueInvalid,
#[error("Timed out enqueuing action")]
EnqueueTimeout,
#[error("Timed out performing action")]
ActionTimeout,
}
impl Robot {
pub fn calibrate_position(&self) -> Result<(), RobotError> {
self.call::<()>("calibratePosition", &[], &[])?;
Ok(())
}
pub fn detect(&self, side: RobotSide) -> Result<DetectResult, RobotError> {
Ok(self.call("detect", &[&side as _], &[])?.0)
}
pub fn get_action_result(
&self,
action_id: ActionId,
) -> Result<Option<ActionResult>, RobotError> {
Ok(self.call("getActionResult", &[&action_id as _], &[])?.0)
}
pub fn get_energy_capacity(&self) -> Result<u32, RobotError> {
Ok(self.call("getEnergyCapacity", &[], &[])?.0)
}
pub fn get_energy_stored(&self) -> Result<u32, RobotError> {
Ok(self.call("getEnergyStored", &[], &[])?.0)
}
pub fn get_facing(&self) -> Result<Facing, RobotError> {
Ok(self.call("getFacing", &[], &[])?.0)
}
pub fn get_fluid(&self) -> Result<Option<TankContentInfo>, RobotError> {
Ok(self.call("getFluid", &[], &[])?.0)
}
pub fn get_last_action_id(&self) -> Result<ActionId, RobotError> {
Ok(self.call("getLastActionId", &[], &[])?.0)
}
pub fn get_position(&self) -> Result<Position, RobotError> {
Ok(self.call("getPosition", &[], &[])?.0)
}
pub fn get_queued_action_count(&self) -> Result<u32, RobotError> {
Ok(self.call("getQueuedActionCount", &[], &[])?.0)
}
pub fn get_selected_slot(&self) -> Result<u32, RobotError> {
Ok(self.call("getSelectedSlot", &[], &[])?.0)
}
pub fn get_stack_in_slot(&self, slot: u32) -> Result<Option<ItemStack>, RobotError> {
Ok(self.call("getStackInSlot", &[&slot as _], &[])?.0)
}
pub fn get_status_color(&self) -> Result<RGB8, RobotError> {
let raw: u32 = self.call("getStatusColor", &[], &[])?.0;
Ok(RGB8 {
r: ((raw >> 16) & 0xFF) as u8,
g: ((raw >> 8) & 0xFF) as u8,
b: (raw & 0xFF) as u8,
})
}
pub fn get_status_fill(&self) -> Result<f32, RobotError> {
Ok(self.call("getStatusValue", &[], &[])?.0)
}
pub fn enqueue_move(
&self,
direction: MoveDirection,
timeout: Option<Duration>,
) -> Result<ActionId, RobotError> {
let deadline = timeout.map(|t| Instant::now() + t);
loop {
let event_stream = self.event_stream();
if self.call("move", &[&direction as _], &[])?.0 {
return self.get_last_action_id();
}
if deadline.is_some_and(|x| Instant::now() >= x) {
return Err(RobotError::EnqueueTimeout);
}
if event_stream
.get_event(deadline.map(|d| d.saturating_duration_since(Instant::now())))?
.is_none()
{
return Err(RobotError::EnqueueTimeout);
}
}
}
pub fn move_blocking(
&self,
direction: MoveDirection,
timeout: Option<Duration>,
) -> Result<bool, RobotError> {
let deadline = timeout.map(|t| Instant::now() + t);
let id = self.enqueue_move(
direction,
deadline.map(|d| d.saturating_duration_since(Instant::now())),
)?;
let event_stream = self.event_stream();
let mut result = self.get_action_result(id)?;
while result == Some(ActionResult::Incomplete) {
if deadline.is_some_and(|x| Instant::now() >= x) {
return Err(RobotError::ActionTimeout);
}
loop {
if let Some(event) = event_stream
.get_event(deadline.map(|d| d.saturating_duration_since(Instant::now())))?
{
if event.action_id == id {
result = Some(event.result);
break;
}
} else {
return Err(RobotError::ActionTimeout);
}
}
}
Ok(result == Some(ActionResult::Success))
}
pub fn set_selected_slot(&self, slot: u32) -> Result<u32, RobotError> {
Ok(self.call("setSelectedSlot", &[&slot as _], &[])?.0)
}
pub fn set_status_color(&self, color: RGB8) -> Result<(), RobotError> {
let color = ((color.r as u32) << 16) | ((color.g as u32) << 8) | (color.b as u32);
self.call::<u32>("setStatusColor", &[&color as _], &[])?;
Ok(())
}
pub fn set_status_fill(&self, fill: f32) -> Result<(), RobotError> {
if !(0.0..=1.0).contains(&fill) {
return Err(RobotError::FillValueInvalid);
}
self.call::<f32>("setStatusValue", &[&fill as _], &[])?;
Ok(())
}
pub fn enqueue_turn(
&self,
direction: TurnDirection,
timeout: Option<Duration>,
) -> Result<ActionId, RobotError> {
let deadline = timeout.map(|t| Instant::now() + t);
loop {
let event_stream = self.event_stream();
if self.call("turn", &[&direction as _], &[])?.0 {
return self.get_last_action_id();
}
if deadline.is_some_and(|x| Instant::now() >= x) {
return Err(RobotError::EnqueueTimeout);
}
if event_stream
.get_event(deadline.map(|d| d.saturating_duration_since(Instant::now())))?
.is_none()
{
return Err(RobotError::EnqueueTimeout);
}
}
}
pub fn turn_blocking(
&self,
direction: TurnDirection,
timeout: Option<Duration>,
) -> Result<bool, RobotError> {
let deadline = timeout.map(|t| Instant::now() + t);
let id = self.enqueue_turn(
direction,
deadline.map(|d| d.saturating_duration_since(Instant::now())),
)?;
let event_stream = self.event_stream();
let mut result = self.get_action_result(id)?;
while result == Some(ActionResult::Incomplete) {
if deadline.is_some_and(|x| Instant::now() >= x) {
return Err(RobotError::ActionTimeout);
}
loop {
if let Some(event) = event_stream
.get_event(deadline.map(|d| d.saturating_duration_since(Instant::now())))?
{
if event.action_id == id {
result = Some(event.result);
break;
}
} else {
return Err(RobotError::ActionTimeout);
}
}
}
Ok(result == Some(ActionResult::Success))
}
}
impl DeviceBase for Robot {
const TYPE: &'static str = "robot";
type Event = ActionCompleted;
fn new(client: HlapiClient, uuid: Uuid) -> Result<Self, HlapiClientError>
where
Self: Sized,
{
Ok(Self { client, uuid })
}
fn uuid_base(&self) -> Uuid {
self.uuid
}
fn client(&self) -> &HlapiClient {
&self.client
}
fn parse_event(
typ: &str,
data: &serde_json::Value,
) -> Result<Self::Event, Box<dyn StdError + Send + Sync>> {
if typ != "robotActionCompleted" {
return Err("invalid event type".into());
}
Ok(serde_json::from_value(data.clone())?)
}
}