2021-02-12 10:51:32 +00:00
|
|
|
use crate::{
|
|
|
|
comp::inventory::{slot::InvSlotId, Inventory},
|
|
|
|
uid::Uid,
|
|
|
|
};
|
2021-02-11 04:54:31 +00:00
|
|
|
use hashbrown::HashMap;
|
|
|
|
use serde::{Deserialize, Serialize};
|
2021-02-12 23:09:18 +00:00
|
|
|
use tracing::{trace, warn};
|
2021-02-11 04:54:31 +00:00
|
|
|
|
2021-02-13 23:32:55 +00:00
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
|
|
pub enum TradePhase {
|
|
|
|
Mutate,
|
|
|
|
Review,
|
|
|
|
Complete,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Clients submit `TradeAction` to the server, which adds the Uid of the
|
2021-02-11 04:54:31 +00:00
|
|
|
/// player out-of-band (i.e. without trusting the client to say who it's
|
|
|
|
/// accepting on behalf of)
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
2021-02-13 23:32:55 +00:00
|
|
|
pub enum TradeAction {
|
|
|
|
AddItem {
|
|
|
|
item: InvSlotId,
|
|
|
|
quantity: u32,
|
2021-02-28 02:44:57 +00:00
|
|
|
ours: bool,
|
2021-02-13 23:32:55 +00:00
|
|
|
},
|
|
|
|
RemoveItem {
|
|
|
|
item: InvSlotId,
|
|
|
|
quantity: u32,
|
2021-02-28 02:44:57 +00:00
|
|
|
ours: bool,
|
2021-02-13 23:32:55 +00:00
|
|
|
},
|
|
|
|
/// Accept needs the phase indicator to avoid progressing too far in the
|
|
|
|
/// trade if there's latency and a player presses the accept button
|
|
|
|
/// multiple times
|
|
|
|
Accept(TradePhase),
|
2021-02-11 19:35:36 +00:00
|
|
|
Decline,
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
|
|
|
|
2021-02-12 20:47:45 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
|
|
pub enum TradeResult {
|
|
|
|
Completed,
|
|
|
|
Declined,
|
|
|
|
NotEnoughSpace,
|
|
|
|
}
|
|
|
|
|
2021-02-11 04:54:31 +00:00
|
|
|
/// Items are not removed from the inventory during a PendingTrade: all the
|
|
|
|
/// items are moved atomically (if there's space and both parties agree) upon
|
|
|
|
/// completion
|
2021-02-12 23:09:18 +00:00
|
|
|
///
|
|
|
|
/// Since this stores `InvSlotId`s (i.e. references into inventories) instead of
|
|
|
|
/// items themselves, there aren't any duplication/loss risks from things like
|
|
|
|
/// dropped connections or declines, since the server doesn't have to move items
|
|
|
|
/// from a trade back into a player's inventory.
|
|
|
|
///
|
|
|
|
/// On the flip side, since they are references to *slots*, if a player could
|
2021-02-13 23:32:55 +00:00
|
|
|
/// swap items in their inventory during a trade, they could mutate the trade,
|
2021-02-12 23:09:18 +00:00
|
|
|
/// enabling them to remove an item from the trade even after receiving the
|
|
|
|
/// counterparty's phase2 accept. To prevent this, we disallow all
|
|
|
|
/// forms of inventory manipulation in `server::events::inventory_manip` if
|
|
|
|
/// there's a pending trade that's past phase1 (in phase1, the trade should be
|
|
|
|
/// mutable anyway).
|
|
|
|
///
|
|
|
|
/// Inventory manipulation in phase1 may be beneficial to trade (e.g. splitting
|
|
|
|
/// a stack of items, once that's implemented), but should reset both phase1
|
|
|
|
/// accept flags to make the changes more visible.
|
|
|
|
///
|
|
|
|
/// Another edge case prevented by using `InvSlotId`s is that it disallows
|
|
|
|
/// trading currently-equipped items (since `EquipSlot`s are disjoint from
|
|
|
|
/// `InvSlotId`s), which avoids the issues associated with trading equipped bags
|
|
|
|
/// that may still have contents.
|
2021-02-11 04:54:31 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
|
|
pub struct PendingTrade {
|
|
|
|
/// `parties[0]` is the entity that initiated the trade, parties[1] is the
|
|
|
|
/// other entity that's being traded with
|
|
|
|
pub parties: [Uid; 2],
|
|
|
|
/// `offers[i]` represents the items and quantities of the party i's items
|
|
|
|
/// being offered
|
2021-02-12 02:53:25 +00:00
|
|
|
pub offers: [HashMap<InvSlotId, u32>; 2],
|
2021-02-13 23:32:55 +00:00
|
|
|
/// The current phase of the trade
|
|
|
|
pub phase: TradePhase,
|
|
|
|
/// `accept_flags` indicate that which parties wish to proceed to the next
|
|
|
|
/// phase of the trade
|
|
|
|
pub accept_flags: [bool; 2],
|
|
|
|
}
|
|
|
|
|
|
|
|
impl TradePhase {
|
|
|
|
fn next(self) -> TradePhase {
|
|
|
|
match self {
|
|
|
|
TradePhase::Mutate => TradePhase::Review,
|
|
|
|
TradePhase::Review => TradePhase::Complete,
|
|
|
|
TradePhase::Complete => TradePhase::Complete,
|
|
|
|
}
|
|
|
|
}
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl PendingTrade {
|
|
|
|
pub fn new(party: Uid, counterparty: Uid) -> PendingTrade {
|
|
|
|
PendingTrade {
|
|
|
|
parties: [party, counterparty],
|
|
|
|
offers: [HashMap::new(), HashMap::new()],
|
2021-02-13 23:32:55 +00:00
|
|
|
phase: TradePhase::Mutate,
|
|
|
|
accept_flags: [false, false],
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-13 23:32:55 +00:00
|
|
|
pub fn phase(&self) -> TradePhase { self.phase }
|
2021-02-11 04:54:31 +00:00
|
|
|
|
2021-02-13 23:32:55 +00:00
|
|
|
pub fn should_commit(&self) -> bool { matches!(self.phase, TradePhase::Complete) }
|
2021-02-11 04:54:31 +00:00
|
|
|
|
|
|
|
pub fn which_party(&self, party: Uid) -> Option<usize> {
|
|
|
|
self.parties
|
|
|
|
.iter()
|
|
|
|
.enumerate()
|
|
|
|
.find(|(_, x)| **x == party)
|
|
|
|
.map(|(i, _)| i)
|
|
|
|
}
|
|
|
|
|
2021-02-12 10:51:32 +00:00
|
|
|
/// Invariants:
|
|
|
|
/// - A party is never shown as offering more of an item than they own
|
|
|
|
/// - Offers with a quantity of zero get removed from the trade
|
|
|
|
/// - Modifications can only happen in phase 1
|
2021-02-12 23:09:18 +00:00
|
|
|
/// - Whenever a trade is modified, both accept flags get reset
|
2021-02-12 10:51:32 +00:00
|
|
|
/// - Accept flags only get set for the current phase
|
2021-02-28 02:44:57 +00:00
|
|
|
pub fn process_trade_action(
|
|
|
|
&mut self,
|
|
|
|
mut who: usize,
|
|
|
|
action: TradeAction,
|
|
|
|
inventories: &[&Inventory],
|
|
|
|
) {
|
2021-02-13 23:32:55 +00:00
|
|
|
use TradeAction::*;
|
|
|
|
match action {
|
2021-02-12 10:51:32 +00:00
|
|
|
AddItem {
|
|
|
|
item,
|
|
|
|
quantity: delta,
|
2021-02-28 02:44:57 +00:00
|
|
|
ours,
|
2021-02-12 10:51:32 +00:00
|
|
|
} => {
|
2021-02-13 23:32:55 +00:00
|
|
|
if self.phase() == TradePhase::Mutate && delta > 0 {
|
2021-02-28 02:44:57 +00:00
|
|
|
if !ours {
|
|
|
|
who = 1 - who;
|
|
|
|
}
|
2021-02-12 23:09:18 +00:00
|
|
|
let total = self.offers[who].entry(item).or_insert(0);
|
2021-02-28 02:44:57 +00:00
|
|
|
let owned_quantity =
|
|
|
|
inventories[who].get(item).map(|i| i.amount()).unwrap_or(0);
|
2021-02-12 23:09:18 +00:00
|
|
|
*total = total.saturating_add(delta).min(owned_quantity);
|
2021-02-13 23:32:55 +00:00
|
|
|
self.accept_flags = [false, false];
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
|
|
|
},
|
2021-02-12 10:51:32 +00:00
|
|
|
RemoveItem {
|
|
|
|
item,
|
|
|
|
quantity: delta,
|
2021-02-28 02:44:57 +00:00
|
|
|
ours,
|
2021-02-12 10:51:32 +00:00
|
|
|
} => {
|
2021-02-13 23:32:55 +00:00
|
|
|
if self.phase() == TradePhase::Mutate {
|
2021-02-28 02:44:57 +00:00
|
|
|
if !ours {
|
|
|
|
who = 1 - who;
|
|
|
|
}
|
2021-02-12 10:51:32 +00:00
|
|
|
self.offers[who]
|
|
|
|
.entry(item)
|
|
|
|
.and_replace_entry_with(|_, mut total| {
|
|
|
|
total = total.saturating_sub(delta);
|
|
|
|
if total > 0 { Some(total) } else { None }
|
|
|
|
});
|
2021-02-13 23:32:55 +00:00
|
|
|
self.accept_flags = [false, false];
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
|
|
|
},
|
2021-02-13 23:32:55 +00:00
|
|
|
Accept(phase) => {
|
|
|
|
if self.phase == phase {
|
|
|
|
self.accept_flags[who] = true;
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
2021-02-13 23:32:55 +00:00
|
|
|
if self.accept_flags[0] && self.accept_flags[1] {
|
|
|
|
self.phase = self.phase.next();
|
|
|
|
self.accept_flags = [false, false];
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
|
|
|
},
|
2021-02-11 19:35:36 +00:00
|
|
|
Decline => {},
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-13 23:32:55 +00:00
|
|
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
|
|
|
pub struct TradeId(usize);
|
|
|
|
|
2021-02-11 04:54:31 +00:00
|
|
|
pub struct Trades {
|
2021-02-13 23:32:55 +00:00
|
|
|
pub next_id: TradeId,
|
|
|
|
pub trades: HashMap<TradeId, PendingTrade>,
|
|
|
|
pub entity_trades: HashMap<Uid, TradeId>,
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Trades {
|
2021-02-13 23:32:55 +00:00
|
|
|
pub fn begin_trade(&mut self, party: Uid, counterparty: Uid) -> TradeId {
|
2021-02-11 04:54:31 +00:00
|
|
|
let id = self.next_id;
|
2021-02-13 23:32:55 +00:00
|
|
|
self.next_id = TradeId(id.0.wrapping_add(1));
|
2021-02-11 04:54:31 +00:00
|
|
|
self.trades
|
|
|
|
.insert(id, PendingTrade::new(party, counterparty));
|
2021-02-12 23:09:18 +00:00
|
|
|
self.entity_trades.insert(party, id);
|
|
|
|
self.entity_trades.insert(counterparty, id);
|
2021-02-11 04:54:31 +00:00
|
|
|
id
|
|
|
|
}
|
|
|
|
|
2021-02-28 02:44:57 +00:00
|
|
|
pub fn process_trade_action<'a, F: Fn(Uid) -> Option<&'a Inventory>>(
|
2021-02-12 10:51:32 +00:00
|
|
|
&mut self,
|
2021-02-13 23:32:55 +00:00
|
|
|
id: TradeId,
|
2021-02-12 10:51:32 +00:00
|
|
|
who: Uid,
|
2021-02-13 23:32:55 +00:00
|
|
|
action: TradeAction,
|
2021-02-28 02:44:57 +00:00
|
|
|
get_inventory: F,
|
2021-02-12 10:51:32 +00:00
|
|
|
) {
|
2021-02-13 23:32:55 +00:00
|
|
|
trace!("for trade id {:?}, message {:?}", id, action);
|
2021-02-11 04:54:31 +00:00
|
|
|
if let Some(trade) = self.trades.get_mut(&id) {
|
|
|
|
if let Some(party) = trade.which_party(who) {
|
2021-02-28 02:44:57 +00:00
|
|
|
let mut inventories = Vec::new();
|
|
|
|
for party in trade.parties.iter() {
|
|
|
|
match get_inventory(*party) {
|
|
|
|
Some(inventory) => inventories.push(inventory),
|
|
|
|
None => return,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
trade.process_trade_action(party, action, &*inventories);
|
2021-02-11 04:54:31 +00:00
|
|
|
} else {
|
2021-02-11 19:35:36 +00:00
|
|
|
warn!(
|
2021-02-13 23:32:55 +00:00
|
|
|
"An entity who is not a party to trade {:?} tried to modify it",
|
2021-02-11 19:35:36 +00:00
|
|
|
id
|
|
|
|
);
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
|
|
|
} else {
|
2021-02-13 23:32:55 +00:00
|
|
|
warn!("Attempt to modify nonexistent trade id {:?}", id);
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-13 23:32:55 +00:00
|
|
|
pub fn decline_trade(&mut self, id: TradeId, who: Uid) -> Option<Uid> {
|
2021-02-11 19:35:36 +00:00
|
|
|
let mut to_notify = None;
|
2021-02-11 04:54:31 +00:00
|
|
|
if let Some(trade) = self.trades.remove(&id) {
|
2021-02-11 19:35:36 +00:00
|
|
|
match trade.which_party(who) {
|
|
|
|
Some(i) => {
|
2021-02-12 23:09:18 +00:00
|
|
|
self.entity_trades.remove(&trade.parties[0]);
|
|
|
|
self.entity_trades.remove(&trade.parties[1]);
|
2021-02-11 19:35:36 +00:00
|
|
|
// let the other person know the trade was declined
|
|
|
|
to_notify = Some(trade.parties[1 - i])
|
|
|
|
},
|
|
|
|
None => {
|
|
|
|
warn!(
|
2021-02-13 23:32:55 +00:00
|
|
|
"An entity who is not a party to trade {:?} tried to decline it",
|
2021-02-11 19:35:36 +00:00
|
|
|
id
|
|
|
|
);
|
|
|
|
// put it back
|
|
|
|
self.trades.insert(id, trade);
|
|
|
|
},
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
|
|
|
} else {
|
2021-02-13 23:32:55 +00:00
|
|
|
warn!("Attempt to decline nonexistent trade id {:?}", id);
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
2021-02-11 19:35:36 +00:00
|
|
|
to_notify
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
2021-02-12 23:09:18 +00:00
|
|
|
|
|
|
|
/// See the doc comment on `common::trade::PendingTrade` for the
|
|
|
|
/// significance of these checks
|
|
|
|
pub fn in_trade_with_property<F: FnOnce(&PendingTrade) -> bool>(
|
|
|
|
&self,
|
|
|
|
uid: &Uid,
|
|
|
|
f: F,
|
|
|
|
) -> bool {
|
|
|
|
self.entity_trades
|
|
|
|
.get(uid)
|
|
|
|
.and_then(|trade_id| self.trades.get(trade_id))
|
|
|
|
.map(f)
|
|
|
|
// if any of the option lookups failed, we're not in any trade
|
|
|
|
.unwrap_or(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn in_immutable_trade(&self, uid: &Uid) -> bool {
|
2021-02-13 23:32:55 +00:00
|
|
|
self.in_trade_with_property(uid, |trade| trade.phase() != TradePhase::Mutate)
|
2021-02-12 23:09:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn in_mutable_trade(&self, uid: &Uid) -> bool {
|
2021-02-13 23:32:55 +00:00
|
|
|
self.in_trade_with_property(uid, |trade| trade.phase() == TradePhase::Mutate)
|
2021-02-12 23:09:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn implicit_mutation_occurred(&mut self, uid: &Uid) {
|
|
|
|
if let Some(trade_id) = self.entity_trades.get(uid) {
|
|
|
|
self.trades
|
|
|
|
.get_mut(trade_id)
|
2021-02-13 23:32:55 +00:00
|
|
|
.map(|trade| trade.accept_flags = [false, false]);
|
2021-02-12 23:09:18 +00:00
|
|
|
}
|
|
|
|
}
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for Trades {
|
|
|
|
fn default() -> Trades {
|
|
|
|
Trades {
|
2021-02-13 23:32:55 +00:00
|
|
|
next_id: TradeId(0),
|
2021-02-11 04:54:31 +00:00
|
|
|
trades: HashMap::new(),
|
2021-02-12 23:09:18 +00:00
|
|
|
entity_trades: HashMap::new(),
|
2021-02-11 04:54:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|