use std::convert::TryInto; use libpso::packet::ship::*; use libpso::packet::messages::*; use crate::common::serverstate::ClientId; use crate::ship::ship::{SendShipPacket, ShipError, Clients}; use crate::ship::location::{ClientLocation, ClientLocationError}; use crate::ship::items::{ItemManager, ItemManagerError, ClientItemId, ItemToTradeDetail}; use crate::ship::items::inventory::InventoryItem; use crate::ship::trade::{TradeItem, TradeState, TradeStatus}; use crate::entity::gateway::EntityGateway; use crate::ship::packet::builder; pub const MESETA_ITEM_ID: ClientItemId = ClientItemId(0xFFFFFF01); pub const OTHER_MESETA_ITEM_ID: ClientItemId = ClientItemId(0xFFFFFFFF); #[derive(thiserror::Error, Debug, PartialEq, Eq)] pub enum TradeError { #[error("no partner")] CouldNotFindTradePartner, #[error("invalid item id")] InvalidItemId(ClientItemId), #[error("item does not match id")] ClientItemIdDidNotMatchItem(ClientItemId, [u8; 16]), #[error("invalid stack {1}")] InvalidStackAmount(ClientItemId, usize), #[error("not in trade menu")] NotInTradeMenu, #[error("trade menu at an invalid point")] MismatchedStatus, #[error("no space in inventory")] NoInventorySpace, #[error("no space in stack")] NoStackSpace, #[error("invalid meseta amount")] InvalidMeseta, #[error("tried starting a trade while in one already")] ClientAlreadyInTrade, #[error("tried starting a trade while with player already in a trade")] OtherAlreadyInTrade, #[error("tried to trade item not specified in trade request")] SketchyTrade, #[error("items in trade window and items attempted to trade do not match")] MismatchedTradeItems, } // TODO: remove target pub async fn trade_request(id: ClientId, trade_request: &TradeRequest, target: u32, client_location: &ClientLocation, clients: &mut Clients, item_manager: &mut ItemManager, trades: &mut TradeState) -> Result + Send>, anyhow::Error> { let trade_request = trade_request.clone(); // TODO: make this function take ownership of packet match trade_request.trade { TradeRequestCommand::Initialize(ref act, _meseta) => { match act { TradeRequestInitializeCommand::Initialize => { if trades.in_trade(&id) { return Err(TradeError::ClientAlreadyInTrade.into()) } let trade_partner = client_location.get_client_neighbors(id)? .into_iter() .find(|ac| { ac.local_client.id() == target as u8 //trade_request.client }) .ok_or(TradeError::CouldNotFindTradePartner)?; if trades.in_trade(&trade_partner.client) { return Err(TradeError::OtherAlreadyInTrade.into()) } trades.new_trade(&id, &trade_partner.client); Ok(Box::new(client_location.get_all_clients_by_client(id)?.into_iter() .filter(move |client| client.local_client.id() == target as u8) .map(move |client| { (client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone())))) }))) }, TradeRequestInitializeCommand::Respond => { Ok(trades .with(&id, |this, other| -> Option + Send>> { if this.status == TradeStatus::ReceivedRequest && other.status == TradeStatus::SentRequest { this.status = TradeStatus::Trading; other.status = TradeStatus::Trading; let trade_request = trade_request.clone(); Some(Box::new(client_location.get_all_clients_by_client(id).ok()?.into_iter() .filter(move |client| client.local_client.id() == target as u8) .map(move |client| { (client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone())))) }))) } else { None } })? .unwrap_or_else(|| -> Box + Send> { trades.remove_trade(&id); Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() .filter(move |client| client.local_client.id() == target as u8) .map(move |client| { (client.client, SendShipPacket::CancelTrade(CancelTrade {})) }) .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))) })) } } }, TradeRequestCommand::AddItem(item_id, amount) => { Ok(trades .with(&id, |this, other| -> Result + Send>, anyhow::Error> { if this.status == TradeStatus::Trading && other.status == TradeStatus::Trading { let client = clients.get(&this.client()).ok_or_else(|| ShipError::ClientNotFound(this.client()))?; let inventory = item_manager.get_character_inventory(&client.character)?; if ClientItemId(item_id) == MESETA_ITEM_ID { this.meseta += amount as usize; } else { let item = inventory.get_item_by_id(ClientItemId(item_id)).ok_or(ItemManagerError::NoSuchItemId(ClientItemId(item_id)))?; match item { InventoryItem::Individual(_) => { this.items.push(TradeItem::Individual(ClientItemId(item_id))); }, InventoryItem::Stacked(stacked_item) => { if stacked_item.count() < amount as usize { return Err(TradeError::InvalidStackAmount(ClientItemId(item_id), amount as usize).into()); } this.items.push(TradeItem::Stacked(ClientItemId(item_id), amount as usize)); }, } } let trade_request = trade_request.clone(); Ok(Box::new(client_location.get_all_clients_by_client(id)?.into_iter() .filter(move |client| client.local_client.id() == target as u8) .map(move |client| { (client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone())))) }))) } else { Err(TradeError::MismatchedStatus.into()) } })? .unwrap_or_else(|_err| { trades.remove_trade(&id); Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() .filter(move |client| client.local_client.id() == target as u8) .map(move |client| { (client.client, SendShipPacket::CancelTrade(CancelTrade {})) }) .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))) })) }, TradeRequestCommand::RemoveItem(item_id, amount) => { Ok(trades .with(&id, |this, other| -> Option + Send>> { if this.status == TradeStatus::Trading && other.status == TradeStatus::Trading { let client = clients.get(&this.client())?; //.ok_or(ShipError::ClientNotFound(id)).ok()?; let inventory = item_manager.get_character_inventory(&client.character).ok()?; if ClientItemId(item_id) == MESETA_ITEM_ID { this.meseta -= amount as usize; } else { let item = inventory.get_item_by_id(ClientItemId(item_id))?; match item { InventoryItem::Individual(_) => { this.items.retain(|item| { item.item_id() != ClientItemId(item_id) }) }, InventoryItem::Stacked(_stacked_item) => { let trade_item_index = this.items.iter() .position(|item| { item.item_id() == ClientItemId(item_id) })?; match this.items[trade_item_index].stacked()?.1.cmp(&(amount as usize)) { std::cmp::Ordering::Greater => { *this.items[trade_item_index].stacked_mut()?.1 -= amount as usize; }, std::cmp::Ordering::Equal => { this.items.remove(trade_item_index); }, std::cmp::Ordering::Less => { return None } } }, } } let trade_request = trade_request.clone(); Some(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() .filter(move |client| client.local_client.id() == target as u8) .map(move |client| { (client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone())))) }))) } else { None } })? .unwrap_or_else(|| { trades.remove_trade(&id); Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() .filter(move |client| client.local_client.id() == target as u8) .map(move |client| { (client.client, SendShipPacket::CancelTrade(CancelTrade {})) }) .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))) })) }, TradeRequestCommand::Confirm => { Ok(trades .with(&id, |this, other| -> Option + Send>> { if status_is(&this.status, &[TradeStatus::Trading]) && status_is(&other.status, &[TradeStatus::Trading, TradeStatus::Confirmed]) { this.status = TradeStatus::Confirmed; let trade_request = trade_request.clone(); Some(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() .filter(move |client| client.local_client.id() == target as u8) .map(move |client| { (client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone())))) }))) } else { None } })? .unwrap_or_else(|| { trades.remove_trade(&id); Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() .filter(move |client| client.local_client.id() == target as u8) .map(move |client| { (client.client, SendShipPacket::CancelTrade(CancelTrade {})) }) .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))) })) }, TradeRequestCommand::FinalConfirm => { Ok(trades .with(&id, |this, other| -> Option + Send>> { if this.status == TradeStatus::Confirmed && (other.status == TradeStatus::Confirmed || other.status == TradeStatus::FinalConfirm) { this.status = TradeStatus::FinalConfirm; let trade_request = trade_request.clone(); Some(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() .filter(move |client| client.local_client.id() == target as u8) .map(move |client| { (client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone())))) }))) } else { None } })? .unwrap_or_else(|| { trades.remove_trade(&id); Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() .filter(move |client| client.local_client.id() == target as u8) .map(move |client| { (client.client, SendShipPacket::CancelTrade(CancelTrade {})) }) .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))) })) }, TradeRequestCommand::Cancel => { trades.remove_trade(&id); Ok(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() .filter(move |client| client.local_client.id() == target as u8) .map(move |client| { (client.client, SendShipPacket::CancelTrade(CancelTrade {})) }) .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {})))))) } } } fn status_is(status: &TradeStatus, statuses: &[TradeStatus; N]) -> bool { statuses.iter().any(|s| s == status) } fn status_is_not(status: &TradeStatus, statuses: &[TradeStatus; N]) -> bool { !status_is(status, statuses) } pub async fn inner_items_to_trade(id: ClientId, items_to_trade: &ItemsToTrade, client_location: &ClientLocation, clients: &mut Clients, item_manager: &mut ItemManager, trades: &mut TradeState) -> Result + Send>, anyhow::Error> { Ok(trades .with(&id, |this, other| -> Result + Send>, anyhow::Error> { if status_is_not(&this.status, &[TradeStatus::FinalConfirm]) || status_is_not(&other.status, &[TradeStatus::FinalConfirm, TradeStatus::ItemsChecked]) { return Err(TradeError::MismatchedStatus.into()) } let client = clients.get(&this.client()).ok_or_else(|| ShipError::ClientNotFound(this.client()))?; let other_client = clients.get(&other.client()).ok_or_else(|| ShipError::ClientNotFound(other.client()))?; let inventory = item_manager.get_character_inventory(&client.character)?; if items_to_trade.count as usize != (this.items.len() + (if this.meseta != 0 { 1 } else { 0 })) { return Err(TradeError::MismatchedTradeItems.into()) } items_to_trade.items .iter() .take(items_to_trade.count as usize) .map(|item| { if ClientItemId(item.item_id) == OTHER_MESETA_ITEM_ID { if item.item_data[0] != 4 { return Err(TradeError::InvalidItemId(ClientItemId(item.item_id)).into()) } let amount = u32::from_le_bytes(item.item_data2); let character_meseta = item_manager.get_character_meseta(&client.character.id).map_err(|_| TradeError::InvalidMeseta)?; let other_character_meseta = item_manager.get_character_meseta(&other_client.character.id).map_err(|_| TradeError::InvalidMeseta)?; if amount > character_meseta.0 { return Err(TradeError::InvalidMeseta.into()) } if (amount + other_character_meseta.0) > 999999 { return Err(TradeError::InvalidMeseta.into()) } if amount != this.meseta as u32{ return Err(TradeError::InvalidMeseta.into()) } Ok(()) } else { let real_item = inventory.get_item_by_id(ClientItemId(item.item_id)) .ok_or(ItemManagerError::NoSuchItemId(ClientItemId(item.item_id)))?; let real_trade_item = this.items .iter() .find(|i| i.item_id() == ClientItemId(item.item_id)) .ok_or(TradeError::SketchyTrade)?; let trade_item_bytes: [u8; 16] = item.item_data.iter() .chain(item.item_data2.iter()) .cloned().collect::>() .try_into() .unwrap(); match real_item { InventoryItem::Individual(_individual_inventory_item) => { if real_item.as_client_bytes() == trade_item_bytes { Ok(()) } else { Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into()) } }, InventoryItem::Stacked(stacked_inventory_item) => { if real_item.as_client_bytes()[0..4] == trade_item_bytes[0..4] { let amount = trade_item_bytes[5] as usize; if amount <= stacked_inventory_item.entity_ids.len() { if real_trade_item.stacked().ok_or(TradeError::SketchyTrade)?.1 == amount { Ok(()) } else { Err(TradeError::InvalidStackAmount(stacked_inventory_item.item_id, amount).into()) } } else { Err(TradeError::InvalidStackAmount(stacked_inventory_item.item_id, amount).into()) } } else { Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into()) } } } } }) .collect::, anyhow::Error>>()?; this.status = TradeStatus::ItemsChecked; if this.status == TradeStatus::ItemsChecked && other.status == TradeStatus::ItemsChecked { Ok(Box::new(vec![ (this.client(), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})), (other.client(), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})), ].into_iter())) } else { Ok(Box::new(None.into_iter())) } })? .unwrap_or_else(|err| { log::warn!("trade error: {:?}", err); let (_this, other) = trades.remove_trade(&id); Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() .filter(move |client| other.as_ref().map(|other| client.client == other.client() ).unwrap_or_else(|| false)) .map(move |client| { (client.client, SendShipPacket::CancelTrade(CancelTrade {})) }) .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))) })) } pub async fn items_to_trade(id: ClientId, items_to_trade_pkt: &ItemsToTrade, client_location: &ClientLocation, clients: &mut Clients, item_manager: &mut ItemManager, trades: &mut TradeState) -> Result + Send>, anyhow::Error> { let t = inner_items_to_trade(id, items_to_trade_pkt, client_location, clients, item_manager, trades).await; match t { Ok(p) => Ok(p), Err(err) => { log::warn!("atrade error: {:?}", err); let (_this, other) = trades.remove_trade(&id); Ok(Box::new(client_location.get_all_clients_by_client(id)?.into_iter() .filter(move |client| other.as_ref().map(|other| client.client == other.client()).unwrap_or_else(|| false)) .map(move |client| { (client.client, SendShipPacket::CancelTrade(CancelTrade {})) }) .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {})))))) } } } pub async fn trade_confirmed(id: ClientId, entity_gateway: &mut EG, client_location: &ClientLocation, clients: &mut Clients, item_manager: &mut ItemManager, trades: &mut TradeState) -> Result + Send>, anyhow::Error> where EG: EntityGateway { enum TradeReady<'a> { OnePlayer, BothPlayers(crate::ship::location::RoomId, (crate::ship::location::AreaClient, &'a crate::ship::ship::ClientState, crate::ship::trade::ClientTradeState), (crate::ship::location::AreaClient, &'a crate::ship::ship::ClientState, crate::ship::trade::ClientTradeState)), } let trade_instructions = trades .with(&id, |this, other| -> Result<_, anyhow::Error> { if status_is_not(&this.status, &[TradeStatus::ItemsChecked]) || status_is_not(&other.status, &[TradeStatus::ItemsChecked, TradeStatus::TradeComplete]) { return Err(TradeError::MismatchedStatus.into()) } this.status = TradeStatus::TradeComplete; if this.status == TradeStatus::TradeComplete && other.status == TradeStatus::TradeComplete { let this_client = clients.get(&this.client()).ok_or_else(|| ShipError::ClientNotFound(this.client()))?; let other_client = clients.get(&other.client()).ok_or_else(|| ShipError::ClientNotFound(other.client()))?; let this_local_client = client_location.get_local_client(this.client())?; let other_local_client = client_location.get_local_client(other.client())?; let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?; Ok(TradeReady::BothPlayers(room_id, (this_local_client, this_client, this.clone()), (other_local_client, other_client, other.clone()))) } else { Ok(TradeReady::OnePlayer) } }); // TODO: this match needs to handle errors better match trade_instructions { Ok(Ok(trade)) => { match trade { TradeReady::OnePlayer => { Ok(Box::new(None.into_iter()) as Box + Send>) }, TradeReady::BothPlayers(room_id, (this_local_client, this_client, this), (other_local_client, other_client, other)) => { let traded_items = item_manager.trade_items(entity_gateway, room_id, (&this_local_client, &this_client.character, &this.items, this.meseta), (&other_local_client, &other_client.character, &other.items, other.meseta)).await?; let clients_in_room = client_location.get_all_clients_by_client(id)?; let traded_item_packets = traded_items .into_iter() .map(|item| { match item.item_detail { ItemToTradeDetail::Individual(item_detail) => { [ GameMessage::CreateItem(builder::message::create_individual_item(item.add_to, item.new_item_id, &item_detail).unwrap()), GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(item.remove_from, item.current_item_id, 1)) // TODO: amount = ? ] }, ItemToTradeDetail::Stacked(tool, amount) => { [ GameMessage::CreateItem(builder::message::create_stacked_item(item.add_to, item.new_item_id, &tool, amount).unwrap()), GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(item.remove_from, item.current_item_id, amount as u32)) ] }, ItemToTradeDetail::Meseta(amount) => { [ GameMessage::CreateItem(builder::message::create_meseta(item.add_to, amount)), GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(item.remove_from, item.current_item_id, amount as u32)) ] }, } }) .flatten() .map(move |packet| { clients_in_room .clone() .into_iter() .filter_map(move |client| { match packet { GameMessage::PlayerNoLongerHasItem(ref no_longer) => { if client.local_client == no_longer.client { None } else { Some((client.client, SendShipPacket::Message(Message::new(packet.clone())))) } } _ => Some((client.client, SendShipPacket::Message(Message::new(packet.clone())))) } }) }) .flatten(); let close_trade = vec![ (this.client(), SendShipPacket::TradeSuccessful(TradeSuccessful::default())), (other.client(), SendShipPacket::TradeSuccessful(TradeSuccessful::default())) ].into_iter(); Ok(Box::new(traded_item_packets.chain(close_trade))) } } }, _ => { let (_this, other) = trades.remove_trade(&id); Ok(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() .filter(move |client| other.as_ref().map(|other| client.client == other.client()).unwrap_or_else(|| false)) .map(move |client| { (client.client, SendShipPacket::CancelTrade(CancelTrade {})) }) .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {})))))) } } }