You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

604 lines
30 KiB

use std::convert::TryInto;
use libpso::packet::ship::*;
use libpso::packet::messages::*;
use networking::serverstate::ClientId;
use crate::{SendShipPacket, ShipError, Clients};
use location::{ClientLocation};
use items::ClientItemId;
use items::state::{ItemState, ItemStateError};
use items::inventory::InventoryItemDetail;
use items::trade::TradeItem;
use entity::gateway::EntityGateway;
use pktbuilder as builder;
use items::tasks::trade_items;
use location::{AreaClient, RoomId};
use entity::item::Meseta;
use trade::{ClientTradeState, TradeState, TradeStatus};
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,
}
async fn do_trade_action<F>(id: ClientId,
pkt: TradeRequest,
client_location: &ClientLocation,
target: u32,
this: &mut ClientTradeState,
other: &mut ClientTradeState,
action: F)
-> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
where
F: Fn(&mut ClientTradeState, &mut ClientTradeState) -> Result<(), anyhow::Error>,
{
Ok(match action(this, other) {
Ok(_) => {
client_location.get_all_clients_by_client(id).await?.into_iter()
.filter(move |client| client.local_client.id() == target as u8)
.map(move |client| {
(client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(pkt.clone()))))
})
.collect()
},
Err(_) => {
// TODO: some sort of error logging?
client_location.get_all_clients_by_client(id).await?.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 {}))))
.collect()
}
})
}
// TODO: remove target
pub async fn trade_request(id: ClientId,
trade_request: TradeRequest,
target: u32,
client_location: &ClientLocation,
clients: &Clients,
item_state: &mut ItemState,
trades: &mut TradeState)
-> Result<Vec<(ClientId, SendShipPacket)>, 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).await?
.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(client_location.get_all_clients_by_client(id).await?.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()))))
})
.collect())
},
TradeRequestInitializeCommand::Respond => {
trades
.with(&id, |mut this, mut other| {
let trade_request = trade_request.clone();
async move {
do_trade_action(id, trade_request, client_location, target, &mut this, &mut other, |this, other| {
if this.status == TradeStatus::ReceivedRequest && other.status == TradeStatus::SentRequest {
this.status = TradeStatus::Trading;
other.status = TradeStatus::Trading;
Ok(())
}
else {
Err(TradeError::MismatchedStatus.into())
}
}).await
}}).await?
}
}
},
TradeRequestCommand::AddItem(item_id, amount) => {
trades
.with(&id, |mut this, mut other| {
let trade_request = trade_request.clone();
async move {
let inventory = clients.with(this.client(), |client| {
let item_state = item_state.clone();
Box::pin(async move {
item_state.get_character_inventory(&client.character).await
})}).await??;
do_trade_action(id, trade_request, client_location, target, &mut this, &mut other, |this, other| {
if this.status == TradeStatus::Trading && other.status == TradeStatus::Trading {
if ClientItemId(item_id) == MESETA_ITEM_ID {
this.meseta += amount as usize;
}
else {
let item = inventory.get_by_client_id(&ClientItemId(item_id)).ok_or_else(|| ItemStateError::InvalidItemId(ClientItemId(item_id)))?;
match &item.item {
InventoryItemDetail::Individual(_) => {
this.items.push(TradeItem::Individual(ClientItemId(item_id)));
},
InventoryItemDetail::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));
},
}
}
Ok(())
}
else {
Err(TradeError::MismatchedStatus.into())
}
}).await
}}).await?
},
TradeRequestCommand::RemoveItem(item_id, amount) => {
trades
.with(&id, |mut this, mut other| {
let trade_request = trade_request.clone();
async move {
let inventory = clients.with(this.client(), |client| {
let item_state = item_state.clone();
Box::pin(async move {
item_state.get_character_inventory(&client.character).await
})}).await??;
do_trade_action(id, trade_request, client_location, target, &mut this, &mut other, |this, other| {
if this.status == TradeStatus::Trading && other.status == TradeStatus::Trading {
if ClientItemId(item_id) == MESETA_ITEM_ID {
this.meseta -= amount as usize;
}
else {
let item = inventory.get_by_client_id(&ClientItemId(item_id)).ok_or_else(|| ItemStateError::InvalidItemId(ClientItemId(item_id)))?;
match &item.item {
InventoryItemDetail::Individual(_) => {
this.items.retain(|item| {
item.item_id() != ClientItemId(item_id)
})
},
InventoryItemDetail::Stacked(_stacked_item) => {
let trade_item_index = this.items.iter()
.position(|item| {
item.item_id() == ClientItemId(item_id)
})
.ok_or(TradeError::InvalidItemId(ClientItemId(item_id)))?;
match this.items[trade_item_index].stacked().ok_or_else(|| ItemStateError::InvalidItemId(ClientItemId(item_id)))?.1.cmp(&(amount as usize)) {
std::cmp::Ordering::Greater => {
*this.items[trade_item_index].stacked_mut().ok_or_else(|| ItemStateError::InvalidItemId(ClientItemId(item_id)))?.1 -= amount as usize;
},
std::cmp::Ordering::Equal => {
this.items.remove(trade_item_index);
},
std::cmp::Ordering::Less => {
return Err(TradeError::SketchyTrade.into())
}
}
},
}
}
Ok(())
}
else {
Err(TradeError::MismatchedStatus.into())
}
}).await
}
}).await?
},
TradeRequestCommand::Confirm => {
trades
.with(&id, |mut this, mut other| {
let trade_request = trade_request.clone();
async move {
do_trade_action(id, trade_request, client_location, target, &mut this, &mut other, |this, other| {
if status_is(&this.status, &[TradeStatus::Trading]) && status_is(&other.status, &[TradeStatus::Trading, TradeStatus::Confirmed]) {
this.status = TradeStatus::Confirmed;
Ok(())
}
else {
Err(TradeError::MismatchedStatus.into())
}
}).await
}
}).await?
},
TradeRequestCommand::FinalConfirm => {
trades
.with(&id, |mut this, mut other| {
let trade_request = trade_request.clone();
async move {
do_trade_action(id, trade_request, client_location, target, &mut this, &mut other, |this, other| {
if this.status == TradeStatus::Confirmed && (other.status == TradeStatus::Confirmed || other.status == TradeStatus::FinalConfirm) {
this.status = TradeStatus::FinalConfirm;
Ok(())
}
else {
Err(TradeError::MismatchedStatus.into())
}
}).await
}
}).await?
},
TradeRequestCommand::Cancel => {
trades.remove_trade(&id).await;
Ok(client_location.get_all_clients_by_client(id).await?.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 {}))))
.collect())
}
}
}
fn status_is<const N: usize>(status: &TradeStatus, statuses: &[TradeStatus; N]) -> bool {
statuses.iter().any(|s| s == status)
}
fn status_is_not<const N: usize>(status: &TradeStatus, statuses: &[TradeStatus; N]) -> bool {
!status_is(status, statuses)
}
async fn inner_items_to_trade(id: ClientId,
items_to_trade: ItemsToTrade,
client_location: &ClientLocation,
clients: &Clients,
item_state: &mut ItemState,
trades: &mut TradeState)
-> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
{
let pkts = trades
.with(&id, |mut this, other| async move {
if status_is_not(&this.status, &[TradeStatus::FinalConfirm]) || status_is_not(&other.status, &[TradeStatus::FinalConfirm, TradeStatus::ItemsChecked]) {
return Err(anyhow::Error::from(ShipError::from(TradeError::MismatchedStatus)))
}
let other_client = other.client();
let (this_inventory, other_inventory) = clients.with(this.client(), |client| {
let item_state = item_state.clone();
let clients = clients.clone();
Box::pin(async move {
let this = item_state.get_character_inventory(&client.character).await?;
let other_inventory = clients.with(other_client, |client| {
let item_state = item_state.clone();
Box::pin(async move {
item_state.get_character_inventory(&client.character).await
})}).await??;
Ok::<_, anyhow::Error>((this, other_inventory))
})}).await??;
if items_to_trade.count as usize != (this.items.len() + usize::from(this.meseta != 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 = this_inventory.meseta;
let other_character_meseta = other_inventory.meseta;
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 = this_inventory.get_by_client_id(&ClientItemId(item.item_id))
.ok_or_else(|| ItemStateError::InvalidItemId(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::<Vec<u8>>()
.try_into()
.unwrap();
match &real_item.item {
InventoryItemDetail::Individual(_individual_inventory_item) => {
if real_item.item.as_client_bytes() == trade_item_bytes {
Ok(())
}
else {
Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into())
}
},
InventoryItemDetail::Stacked(stacked_inventory_item) => {
if real_item.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(real_item.item_id, amount).into())
}
}
else {
Err(TradeError::InvalidStackAmount(real_item.item_id, amount).into())
}
}
else {
Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into())
}
}
}
}
})
.collect::<Result<Vec<_>, anyhow::Error>>()?;
this.status = TradeStatus::ItemsChecked;
if this.status == TradeStatus::ItemsChecked && other.status == TradeStatus::ItemsChecked {
Ok(vec![
(this.client(), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})),
(other.client(), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})),
])
}
else {
Ok(Vec::new())
}
}).await?;
match pkts {
Ok(pkts) => Ok(pkts),
Err(err) => {
log::warn!("trade error: {:?}", err);
let (_this, other) = trades.remove_trade(&id).await;
Ok(client_location.get_all_clients_by_client(id).await?.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 {}))))
.collect())
}
}
}
pub async fn items_to_trade(id: ClientId,
items_to_trade_pkt: ItemsToTrade,
client_location: &ClientLocation,
clients: &Clients,
item_state: &mut ItemState,
trades: &mut TradeState)
-> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
{
let t = inner_items_to_trade(id, items_to_trade_pkt, client_location, clients, item_state, trades).await;
match t {
Ok(p) => Ok(p),
Err(err) => {
log::warn!("atrade error: {:?}", err);
let (_this, other) = trades.remove_trade(&id).await;
Ok(client_location.get_all_clients_by_client(id).await?.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 {}))))
.collect())
}
}
}
async fn trade_confirmed_inner<EG>(id: ClientId,
entity_gateway: &mut EG,
client_location: &ClientLocation,
clients: &Clients,
item_state: &mut ItemState,
trades: &mut TradeState)
-> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
where
EG: EntityGateway + Clone + 'static,
{
enum TradeReady/*<'a>*/ {
OnePlayer,
BothPlayers(RoomId,
(AreaClient, ClientTradeState),
(AreaClient, ClientTradeState)),
//(AreaClient, &'a crate::ship::ship::ClientState, crate::ship::trade::ClientTradeState),
//(AreaClient, &'a crate::ship::ship::ClientState, crate::ship::trade::ClientTradeState)),
}
let trade = trades
.with(&id, |mut this, other| {
async move {
if status_is_not(&this.status, &[TradeStatus::ItemsChecked]) || status_is_not(&other.status, &[TradeStatus::ItemsChecked, TradeStatus::TradeComplete]) {
return Err(anyhow::Error::from(ShipError::TradeError(TradeError::MismatchedStatus)))
}
this.status = TradeStatus::TradeComplete;
if this.status == TradeStatus::TradeComplete && other.status == TradeStatus::TradeComplete {
let this_local_client = client_location.get_local_client(this.client()).await?;
let other_local_client = client_location.get_local_client(other.client()).await?;
let room_id = client_location.get_room(id).await?;
Ok(TradeReady::BothPlayers(room_id,
(this_local_client, /*this_client, */this.clone()),
(other_local_client, /*other_client, */other.clone())))
}
else {
Ok(TradeReady::OnePlayer)
}
}
}).await??;
match trade {
TradeReady::OnePlayer => {
Ok(Vec::new())
},
TradeReady::BothPlayers(_room_id, (this_local_client, /*this_client,*/ this), (other_local_client, /*other_client,*/ other)) => {
let remove_item_packets = this.items
.clone()
.into_iter()
.map(move |item| {
(this_local_client, item)
})
.chain(other.items
.clone()
.into_iter()
.map(move |item| {
(other_local_client, item)
}))
.map(|(client, item)| {
GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(client, item.item_id(), item.amount() as u32))
});
let this_items = this.items.clone();
let other_items = other.items.clone();
let (this_new_items, other_new_items) = clients.with_many(
[this_local_client.client, other_local_client.client],
|[this_client, other_client]| {
let mut entity_gateway = entity_gateway.clone();
let mut item_state = item_state.clone();
Box::pin(async move {
trade_items(&mut item_state,
&mut entity_gateway,
(&this_local_client, &this_client.character, &this_items, Meseta(this.meseta as u32)),
(&other_local_client, &other_client.character, &other_items, Meseta(other.meseta as u32))).await
})}).await??;
let create_item_packets = this_new_items
.into_iter()
.map(move |item| {
(this_local_client, item)
})
.chain(other_new_items
.into_iter()
.map(move |item| {
(other_local_client, item)
}))
.map(|(client, item)| {
match item.item {
InventoryItemDetail::Individual(individual_item) => {
GameMessage::CreateItem(builder::message::create_individual_item(client, item.item_id, &individual_item))
},
InventoryItemDetail::Stacked(stacked_item) => {
GameMessage::CreateItem(builder::message::create_stacked_item(client, item.item_id, &stacked_item.tool, stacked_item.count()))
}
}
});
let meseta_packets = vec![(this_local_client, other_local_client, this.meseta), (other_local_client, this_local_client, other.meseta)]
.into_iter()
.filter(|(_, _, meseta)| *meseta != 0)
.flat_map(|(this, other, meseta)| {
[
GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(this, MESETA_ITEM_ID, meseta as u32)),
GameMessage::CreateItem(builder::message::create_meseta(other, meseta)),
]
});
let clients_in_room = client_location.get_all_clients_by_client(id).await?;
let traded_item_packets = remove_item_packets
.chain(create_item_packets)
.chain(meseta_packets)
.flat_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()))))
}
})
});
let close_trade = vec![
(this.client(), SendShipPacket::TradeSuccessful(TradeSuccessful::default())),
(other.client(), SendShipPacket::TradeSuccessful(TradeSuccessful::default()))
].into_iter();
Ok(traded_item_packets.chain(close_trade).collect())
}
}
}
pub async fn trade_confirmed<EG>(id: ClientId,
entity_gateway: &mut EG,
client_location: &ClientLocation,
clients: &Clients,
item_state: &mut ItemState,
trades: &mut TradeState)
-> Result<Vec<(ClientId, SendShipPacket)>, anyhow::Error>
where
EG: EntityGateway + Clone + 'static,
{
match trade_confirmed_inner(id, entity_gateway, client_location, clients, item_state, trades).await {
Ok(result) => Ok(result),
Err(_err) => {
let (_this, other) = trades.remove_trade(&id).await;
Ok(client_location.get_all_clients_by_client(id).await?.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 {}))))
.collect())
}
}
}