jake
3 years ago
49 changed files with 7044 additions and 1081 deletions
-
4Cargo.toml
-
89src/bin/main.rs
-
6src/common/mainloop/client.rs
-
2src/common/serverstate.rs
-
22src/entity/account.rs
-
61src/entity/character.rs
-
18src/entity/gateway/entitygateway.rs
-
45src/entity/gateway/inmemory.rs
-
7src/entity/gateway/postgres/migrations/V0003__item_notes.sql
-
15src/entity/gateway/postgres/migrations/V0004__meseta.sql
-
122src/entity/gateway/postgres/models.rs
-
111src/entity/gateway/postgres/postgres.rs
-
1src/entity/item/mag.rs
-
54src/entity/item/mod.rs
-
3src/lib.rs
-
85src/login/character.rs
-
2src/login/login.rs
-
54src/ship/character.rs
-
26src/ship/items/bank.rs
-
14src/ship/items/floor.rs
-
208src/ship/items/inventory.rs
-
816src/ship/items/manager.rs
-
3src/ship/items/mod.rs
-
337src/ship/items/transaction.rs
-
26src/ship/location.rs
-
9src/ship/map/area.rs
-
46src/ship/map/enemy.rs
-
60src/ship/map/object.rs
-
1src/ship/mod.rs
-
37src/ship/packet/builder/message.rs
-
2src/ship/packet/builder/mod.rs
-
0src/ship/packet/builder/trade.rs
-
48src/ship/packet/handler/direct_message.rs
-
2src/ship/packet/handler/lobby.rs
-
48src/ship/packet/handler/message.rs
-
1src/ship/packet/handler/mod.rs
-
544src/ship/packet/handler/trade.rs
-
44src/ship/ship.rs
-
10src/ship/shops/weapon.rs
-
133src/ship/trade.rs
-
3tests/common.rs
-
212tests/test_bank.rs
-
27tests/test_item_actions.rs
-
190tests/test_item_pickup.rs
-
15tests/test_item_use.rs
-
19tests/test_mags.rs
-
6tests/test_rooms.rs
-
155tests/test_shops.rs
-
4382tests/test_trade.rs
@ -0,0 +1,7 @@ |
|||||
|
drop table item_location; |
||||
|
|
||||
|
create table item_note ( |
||||
|
item integer references item (id) not null, |
||||
|
note jsonb not null, |
||||
|
created_at timestamptz default current_timestamp not null |
||||
|
); |
@ -0,0 +1,15 @@ |
|||||
|
create table character_meseta ( |
||||
|
pchar integer references character (id) not null unique, |
||||
|
meseta integer not null, |
||||
|
); |
||||
|
|
||||
|
create table bank_meseta ( |
||||
|
pchar integer references character (id) not null, |
||||
|
bank varchar(128) not null, |
||||
|
meseta integer not null, |
||||
|
unique (pchar, bank) |
||||
|
); |
||||
|
|
||||
|
|
||||
|
alter table player_character |
||||
|
drop column meseta, bank_meseta; |
816
src/ship/items/manager.rs
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,337 @@ |
|||||
|
use crate::entity::gateway::EntityGateway;
|
||||
|
use thiserror::Error;
|
||||
|
use crate::ship::items::manager::{ItemManager, ItemManagerError};
|
||||
|
use crate::entity::gateway::GatewayError;
|
||||
|
|
||||
|
#[derive(Error, Debug)]
|
||||
|
pub enum TransactionCommitError {
|
||||
|
#[error("transaction commit gateway error {0}")]
|
||||
|
Gateway(#[from] GatewayError),
|
||||
|
#[error("transaction commit itemmanager error {0}")]
|
||||
|
ItemManager(#[from] ItemManagerError),
|
||||
|
}
|
||||
|
|
||||
|
#[async_trait::async_trait]
|
||||
|
pub trait ItemAction<EG: EntityGateway>: std::marker::Send + std::marker::Sync + std::fmt::Debug {
|
||||
|
async fn commit(&self, manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError>;
|
||||
|
}
|
||||
|
|
||||
|
pub struct ItemTransactionActions<'a, EG: EntityGateway> {
|
||||
|
action_queue: Vec<Box<dyn ItemAction<EG>>>,
|
||||
|
pub manager: &'a ItemManager,
|
||||
|
}
|
||||
|
|
||||
|
|
||||
|
impl<'a, EG: EntityGateway> ItemTransactionActions<'a, EG> {
|
||||
|
fn new(manager: &'a ItemManager) -> ItemTransactionActions<'a, EG> {
|
||||
|
ItemTransactionActions {
|
||||
|
action_queue: Vec::new(),
|
||||
|
manager
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
pub fn action(&mut self, action: Box<dyn ItemAction<EG>>) {
|
||||
|
self.action_queue.push(action)
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
|
||||
|
pub struct ItemTransaction<'a, T, EG: EntityGateway> {
|
||||
|
data: T,
|
||||
|
actions: ItemTransactionActions<'a, EG>,
|
||||
|
}
|
||||
|
|
||||
|
impl<'a, T, EG: EntityGateway> ItemTransaction<'a, T, EG> {
|
||||
|
pub fn new(manager: &'a ItemManager, arg: T) -> ItemTransaction<'a, T, EG> {
|
||||
|
ItemTransaction {
|
||||
|
data: arg,
|
||||
|
actions: ItemTransactionActions::new(manager),
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
pub fn act<E: std::fmt::Debug, U>(mut self, action: fn(&mut ItemTransactionActions<EG>, &T) -> Result<U, E>) -> FinalizedItemTransaction<U, E, EG> {
|
||||
|
match action(&mut self.actions, &self.data) {
|
||||
|
Ok(k) => {
|
||||
|
FinalizedItemTransaction {
|
||||
|
value: Ok(k),
|
||||
|
action_queue: self.actions.action_queue,
|
||||
|
}
|
||||
|
},
|
||||
|
Err(err) => {
|
||||
|
FinalizedItemTransaction {
|
||||
|
value: Err(err),
|
||||
|
action_queue: Vec::new(),
|
||||
|
}
|
||||
|
}
|
||||
|
}
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
|
||||
|
#[derive(Error, Debug)]
|
||||
|
pub enum TransactionError<E: std::fmt::Debug> {
|
||||
|
#[error("transaction action error {0:?}")]
|
||||
|
Action(E),
|
||||
|
#[error("transaction commit error {0}")]
|
||||
|
Commit(#[from] TransactionCommitError),
|
||||
|
|
||||
|
}
|
||||
|
|
||||
|
// this only exists to drop the ItemManager borrow of ItemTransaction so a mutable ItemTransaction can be passed in later
|
||||
|
pub struct FinalizedItemTransaction<T, E: std::fmt::Debug, EG: EntityGateway> {
|
||||
|
value: Result<T, E>,
|
||||
|
action_queue: Vec<Box<dyn ItemAction<EG>>>,
|
||||
|
}
|
||||
|
|
||||
|
impl<T, E: std::fmt::Debug, EG: EntityGateway> FinalizedItemTransaction<T, E, EG> {
|
||||
|
pub async fn commit(self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<T, TransactionError<E>> {
|
||||
|
match self.value {
|
||||
|
Ok(value) => {
|
||||
|
for action in self.action_queue.into_iter() {
|
||||
|
// TODO: better handle rolling back if this ever errors out
|
||||
|
action.commit(item_manager, entity_gateway).await.map_err(|err| TransactionError::Commit(err))?;
|
||||
|
}
|
||||
|
Ok(value)
|
||||
|
},
|
||||
|
Err(err) => Err(TransactionError::Action(err)),
|
||||
|
}
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
|
||||
|
#[cfg(test)]
|
||||
|
mod test {
|
||||
|
use super::*;
|
||||
|
use crate::entity::account::{UserAccountId, NewUserAccountEntity, UserAccountEntity};
|
||||
|
use crate::entity::character::{NewCharacterEntity, CharacterEntity};
|
||||
|
use crate::entity::gateway::GatewayError;
|
||||
|
use thiserror::Error;
|
||||
|
|
||||
|
#[async_std::test]
|
||||
|
async fn test_item_transaction() {
|
||||
|
#[derive(Debug)]
|
||||
|
struct DummyAction1 {
|
||||
|
name: String,
|
||||
|
}
|
||||
|
#[derive(Debug)]
|
||||
|
struct DummyAction2 {
|
||||
|
value: u32,
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Error, Debug)]
|
||||
|
#[error("")]
|
||||
|
enum DummyError {
|
||||
|
Error
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Default, Clone)]
|
||||
|
struct DummyGateway {
|
||||
|
d1_set: String,
|
||||
|
d2_inc: u32,
|
||||
|
}
|
||||
|
|
||||
|
#[async_trait::async_trait]
|
||||
|
impl EntityGateway for DummyGateway {
|
||||
|
async fn create_user(&mut self, user: NewUserAccountEntity) -> Result<UserAccountEntity, GatewayError> {
|
||||
|
self.d1_set = user.username;
|
||||
|
Ok(UserAccountEntity::default())
|
||||
|
}
|
||||
|
|
||||
|
async fn create_character(&mut self, char: NewCharacterEntity) -> Result<CharacterEntity, GatewayError> {
|
||||
|
self.d2_inc += char.slot;
|
||||
|
Ok(CharacterEntity::default())
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
|
||||
|
#[async_trait::async_trait]
|
||||
|
impl<EG: EntityGateway> ItemAction<EG> for DummyAction1 {
|
||||
|
async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> {
|
||||
|
item_manager.id_counter = 55555;
|
||||
|
entity_gateway.create_user(NewUserAccountEntity {
|
||||
|
username: self.name.clone(),
|
||||
|
..NewUserAccountEntity::default()
|
||||
|
})
|
||||
|
.await?;
|
||||
|
Ok(())
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
#[async_trait::async_trait]
|
||||
|
impl<EG: EntityGateway> ItemAction<EG> for DummyAction2 {
|
||||
|
async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> {
|
||||
|
item_manager.id_counter += self.value;
|
||||
|
entity_gateway.create_character(NewCharacterEntity {
|
||||
|
slot: self.value,
|
||||
|
..NewCharacterEntity::new(UserAccountId(0))
|
||||
|
})
|
||||
|
.await?;
|
||||
|
Ok(())
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
let mut item_manager = ItemManager::default();
|
||||
|
let mut entity_gateway = DummyGateway::default();
|
||||
|
|
||||
|
let result = ItemTransaction::new(&item_manager, 12)
|
||||
|
.act(|it, k| {
|
||||
|
it.action(Box::new(DummyAction1 {name: "asdf".into()}));
|
||||
|
it.action(Box::new(DummyAction2 {value: 11}));
|
||||
|
it.action(Box::new(DummyAction2 {value: *k}));
|
||||
|
if *k == 99 {
|
||||
|
return Err(DummyError::Error)
|
||||
|
}
|
||||
|
Ok(String::from("hello"))
|
||||
|
})
|
||||
|
.commit(&mut item_manager, &mut entity_gateway)
|
||||
|
.await;
|
||||
|
|
||||
|
assert!(entity_gateway.d1_set == "asdf");
|
||||
|
assert!(entity_gateway.d2_inc == 23);
|
||||
|
assert!(item_manager.id_counter == 55578);
|
||||
|
assert!(result.unwrap() == "hello");
|
||||
|
}
|
||||
|
|
||||
|
#[async_std::test]
|
||||
|
async fn test_item_transaction_with_action_error() {
|
||||
|
#[derive(Debug)]
|
||||
|
struct DummyAction1 {
|
||||
|
}
|
||||
|
#[derive(Debug)]
|
||||
|
struct DummyAction2 {
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Error, Debug, PartialEq, Eq)]
|
||||
|
#[error("")]
|
||||
|
enum DummyError {
|
||||
|
Error
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Default, Clone)]
|
||||
|
struct DummyGateway {
|
||||
|
d1_set: String,
|
||||
|
d2_inc: u32,
|
||||
|
}
|
||||
|
|
||||
|
#[async_trait::async_trait]
|
||||
|
impl EntityGateway for DummyGateway {
|
||||
|
async fn create_character(&mut self, char: NewCharacterEntity) -> Result<CharacterEntity, GatewayError> {
|
||||
|
self.d2_inc += char.slot;
|
||||
|
Ok(CharacterEntity::default())
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
|
||||
|
#[async_trait::async_trait]
|
||||
|
impl<EG: EntityGateway> ItemAction<EG> for DummyAction1 {
|
||||
|
async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> {
|
||||
|
entity_gateway.create_character(NewCharacterEntity {
|
||||
|
slot: 1,
|
||||
|
..NewCharacterEntity::new(UserAccountId(0))
|
||||
|
})
|
||||
|
.await?;
|
||||
|
Ok(())
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
#[async_trait::async_trait]
|
||||
|
impl<EG: EntityGateway> ItemAction<EG> for DummyAction2 {
|
||||
|
async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> {
|
||||
|
entity_gateway.create_character(NewCharacterEntity {
|
||||
|
slot: 1,
|
||||
|
..NewCharacterEntity::new(UserAccountId(0))
|
||||
|
})
|
||||
|
.await?;
|
||||
|
Ok(())
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
let mut item_manager = ItemManager::default();
|
||||
|
let mut entity_gateway = DummyGateway::default();
|
||||
|
|
||||
|
let result = ItemTransaction::new(&item_manager, 12)
|
||||
|
.act(|it, _| -> Result<(), _> {
|
||||
|
it.action(Box::new(DummyAction1 {}));
|
||||
|
it.action(Box::new(DummyAction2 {}));
|
||||
|
it.action(Box::new(DummyAction2 {}));
|
||||
|
Err(DummyError::Error)
|
||||
|
})
|
||||
|
.commit(&mut item_manager, &mut entity_gateway)
|
||||
|
.await;
|
||||
|
|
||||
|
assert!(entity_gateway.d2_inc == 0);
|
||||
|
assert!(matches!(result, Err(TransactionError::Action(DummyError::Error))));
|
||||
|
}
|
||||
|
|
||||
|
#[async_std::test]
|
||||
|
async fn test_item_transaction_with_commit_error() {
|
||||
|
#[derive(Debug)]
|
||||
|
struct DummyAction1 {
|
||||
|
}
|
||||
|
#[derive(Debug)]
|
||||
|
struct DummyAction2 {
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Error, Debug, PartialEq, Eq)]
|
||||
|
#[error("")]
|
||||
|
enum DummyError {
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Default, Clone)]
|
||||
|
struct DummyGateway {
|
||||
|
d1_set: String,
|
||||
|
d2_inc: u32,
|
||||
|
}
|
||||
|
|
||||
|
#[async_trait::async_trait]
|
||||
|
impl EntityGateway for DummyGateway {
|
||||
|
async fn create_character(&mut self, char: NewCharacterEntity) -> Result<CharacterEntity, GatewayError> {
|
||||
|
self.d2_inc += char.slot;
|
||||
|
Ok(CharacterEntity::default())
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
|
||||
|
#[async_trait::async_trait]
|
||||
|
impl<EG: EntityGateway> ItemAction<EG> for DummyAction1 {
|
||||
|
async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> {
|
||||
|
entity_gateway.create_character(NewCharacterEntity {
|
||||
|
slot: 1,
|
||||
|
..NewCharacterEntity::new(UserAccountId(0))
|
||||
|
})
|
||||
|
.await?;
|
||||
|
Err(GatewayError::Error.into())
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
#[async_trait::async_trait]
|
||||
|
impl<EG: EntityGateway> ItemAction<EG> for DummyAction2 {
|
||||
|
async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> {
|
||||
|
entity_gateway.create_character(NewCharacterEntity {
|
||||
|
slot: 1,
|
||||
|
..NewCharacterEntity::new(UserAccountId(0))
|
||||
|
})
|
||||
|
.await?;
|
||||
|
Ok(())
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
let mut item_manager = ItemManager::default();
|
||||
|
let mut entity_gateway = DummyGateway::default();
|
||||
|
|
||||
|
let result = ItemTransaction::new(&item_manager, 12)
|
||||
|
.act(|it, _| -> Result<_, DummyError> {
|
||||
|
it.action(Box::new(DummyAction1 {}));
|
||||
|
it.action(Box::new(DummyAction2 {}));
|
||||
|
it.action(Box::new(DummyAction2 {}));
|
||||
|
Ok(())
|
||||
|
})
|
||||
|
.commit(&mut item_manager, &mut entity_gateway)
|
||||
|
.await;
|
||||
|
|
||||
|
// in an ideal world this would be 0 as rollbacks would occur
|
||||
|
assert!(entity_gateway.d2_inc == 1);
|
||||
|
assert!(matches!(result, Err(TransactionError::Commit(TransactionCommitError::Gateway(GatewayError::Error)))));
|
||||
|
}
|
||||
|
}
|
||||
|
|
@ -0,0 +1,544 @@ |
|||||
|
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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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<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)
|
||||
|
}
|
||||
|
|
||||
|
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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + Send>, anyhow::Error>
|
||||
|
{
|
||||
|
Ok(trades
|
||||
|
.with(&id, |this, other| -> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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::<Vec<u8>>()
|
||||
|
.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::<Result<Vec<_>, 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<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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<EG>(id: ClientId,
|
||||
|
entity_gateway: &mut EG,
|
||||
|
client_location: &ClientLocation,
|
||||
|
clients: &mut Clients,
|
||||
|
item_manager: &mut ItemManager,
|
||||
|
trades: &mut TradeState)
|
||||
|
-> Result<Box<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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<dyn Iterator<Item = (ClientId, SendShipPacket)> + 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 {}))))))
|
||||
|
}
|
||||
|
}
|
||||
|
}
|
@ -0,0 +1,133 @@ |
|||||
|
use std::collections::HashMap;
|
||||
|
use std::cell::RefCell;
|
||||
|
|
||||
|
use crate::common::serverstate::ClientId;
|
||||
|
use crate::ship::items;
|
||||
|
|
||||
|
#[derive(Debug, Clone)]
|
||||
|
pub enum TradeItem {
|
||||
|
Individual(items::ClientItemId),
|
||||
|
Stacked(items::ClientItemId, usize),
|
||||
|
}
|
||||
|
|
||||
|
impl TradeItem {
|
||||
|
pub fn stacked(&self) -> Option<(items::ClientItemId, usize)> {
|
||||
|
match self {
|
||||
|
TradeItem::Stacked(item_id, amount) => Some((*item_id, *amount)),
|
||||
|
_ => None
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
pub fn stacked_mut(&mut self) -> Option<(items::ClientItemId, &mut usize)> {
|
||||
|
match self {
|
||||
|
TradeItem::Stacked(item_id, ref mut amount) => Some((*item_id, amount)),
|
||||
|
_ => None
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
pub fn item_id(&self) -> items::ClientItemId {
|
||||
|
match self {
|
||||
|
TradeItem::Individual(item_id) => *item_id,
|
||||
|
TradeItem::Stacked(item_id, _) => *item_id,
|
||||
|
}
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
|
||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
|
pub enum TradeStatus {
|
||||
|
SentRequest,
|
||||
|
ReceivedRequest,
|
||||
|
Trading,
|
||||
|
Confirmed,
|
||||
|
FinalConfirm,
|
||||
|
ItemsChecked,
|
||||
|
TradeComplete,
|
||||
|
}
|
||||
|
|
||||
|
|
||||
|
#[derive(Debug, Clone)]
|
||||
|
pub struct ClientTradeState {
|
||||
|
client: ClientId,
|
||||
|
other_client: ClientId,
|
||||
|
pub items: Vec<TradeItem>,
|
||||
|
pub meseta: usize,
|
||||
|
pub status: TradeStatus,
|
||||
|
}
|
||||
|
|
||||
|
|
||||
|
impl ClientTradeState {
|
||||
|
pub fn client(&self) -> ClientId {
|
||||
|
self.client
|
||||
|
}
|
||||
|
|
||||
|
pub fn other_client(&self) -> ClientId {
|
||||
|
self.other_client
|
||||
|
}
|
||||
|
}
|
||||
|
|
||||
|
#[derive(thiserror::Error, Debug)]
|
||||
|
#[error("")]
|
||||
|
pub enum TradeStateError {
|
||||
|
ClientNotInTrade(ClientId),
|
||||
|
MismatchedTrade(ClientId, ClientId),
|
||||
|
}
|
||||
|
|
||||
|
#[derive(Default, Debug)]
|
||||
|
pub struct TradeState {
|
||||
|
trades: HashMap<ClientId, RefCell<ClientTradeState>>,
|
||||
|
}
|
||||
|
|
||||
|
impl TradeState {
|
||||
|
pub fn new_trade(&mut self, sender: &ClientId, receiver: &ClientId) {
|
||||
|
let state = ClientTradeState {
|
||||
|
client: *sender,
|
||||
|
other_client: *receiver,
|
||||
|
items: Default::default(),
|
||||
|
meseta: 0,
|
||||
|
status: TradeStatus::SentRequest,
|
||||
|
};
|
||||
|
self.trades.insert(*sender, RefCell::new(state));
|
||||
|
|
||||
|
let state = ClientTradeState {
|
||||
|
client: *receiver,
|
||||
|
other_client: *sender,
|
||||
|
items: Default::default(),
|
||||
|
meseta: 0,
|
||||
|
status: TradeStatus::ReceivedRequest,
|
||||
|
};
|
||||
|
self.trades.insert(*receiver, RefCell::new(state));
|
||||
|
}
|
||||
|
|
||||
|
pub fn in_trade(&self, client: &ClientId) -> bool {
|
||||
|
self.trades.contains_key(client)
|
||||
|
}
|
||||
|
|
||||
|
pub fn with<T, F> (&self, client: &ClientId, func: F) -> Result<T, TradeStateError>
|
||||
|
where
|
||||
|
F: Fn(&mut ClientTradeState, &mut ClientTradeState) -> T
|
||||
|
{
|
||||
|
let mut c1 = self.trades.get(client).ok_or_else(|| TradeStateError::ClientNotInTrade(*client))?.borrow_mut();
|
||||
|
let mut c2 = self.trades.get(&c1.other_client).ok_or(TradeStateError::ClientNotInTrade(c1.other_client))?.borrow_mut();
|
||||
|
|
||||
|
// sanity check
|
||||
|
if c1.client != c2.other_client {
|
||||
|
return Err(TradeStateError::MismatchedTrade(c1.client, c2.client));
|
||||
|
}
|
||||
|
|
||||
|
Ok(func(&mut *c1, &mut *c2))
|
||||
|
}
|
||||
|
|
||||
|
// TODO: is it possible for this to not return Options?
|
||||
|
pub fn remove_trade(&mut self, client: &ClientId) -> (Option<ClientTradeState>, Option<ClientTradeState>) {
|
||||
|
let c1 = self.trades.remove(client).map(|c| c.into_inner());
|
||||
|
let c2 = if let Some(ref state) = c1 {
|
||||
|
self.trades.remove(&state.other_client).map(|c| c.into_inner())
|
||||
|
}
|
||||
|
else {
|
||||
|
None
|
||||
|
};
|
||||
|
|
||||
|
(c1, c2)
|
||||
|
}
|
||||
|
}
|
4382
tests/test_trade.rs
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue