use crate::Server; use common::{ comp::{ agent::{Agent, AgentEvent}, inventory::{ item::{tool::AbilityMap, MaterialStatManifest}, Inventory, }, }, trade::{PendingTrade, ReducedInventory, TradeAction, TradeId, TradeResult, Trades}, }; use common_net::{ msg::ServerGeneral, sync::{Uid, WorldSyncExt}, }; use hashbrown::{hash_map::Entry, HashMap}; use specs::{world::WorldExt, Entity as EcsEntity}; use std::cmp::Ordering; use tracing::{error, trace}; use world::IndexOwned; fn notify_agent_simple( mut agents: specs::WriteStorage, entity: EcsEntity, event: AgentEvent, ) { if let Some(agent) = agents.get_mut(entity) { agent.inbox.push_back(event); } } fn notify_agent_prices( mut agents: specs::WriteStorage, index: &IndexOwned, entity: EcsEntity, event: AgentEvent, ) { if let Some((Some(site_id), agent)) = agents.get_mut(entity).map(|a| (a.behavior.trade_site, a)) { let prices = index.get_site_prices(site_id); if let AgentEvent::UpdatePendingTrade(boxval) = event { // Box<(tid, pend, _, inventories)>) = event { agent .inbox .push_back(AgentEvent::UpdatePendingTrade(Box::new(( // Prefer using this Agent's price data, but use the counterparty's price // data if we don't have price data boxval.0, boxval.1, prices.unwrap_or(boxval.2), boxval.3, )))); } } } /// Invoked when the trade UI is up, handling item changes, accepts, etc pub fn handle_process_trade_action( server: &mut Server, entity: EcsEntity, trade_id: TradeId, action: TradeAction, ) { if let Some(uid) = server.state.ecs().uid_from_entity(entity) { let mut trades = server.state.ecs().write_resource::(); if let TradeAction::Decline = action { let to_notify = trades.decline_trade(trade_id, uid); to_notify .and_then(|u| server.state.ecs().entity_from_uid(u.0)) .map(|e| { server.notify_client(e, ServerGeneral::FinishedTrade(TradeResult::Declined)); notify_agent_simple( server.state.ecs().write_storage::(), e, AgentEvent::FinishedTrade(TradeResult::Declined), ); }); } else { { let ecs = server.state.ecs(); let inventories = ecs.read_component::(); let get_inventory = |uid: Uid| { if let Some(entity) = ecs.entity_from_uid(uid.0) { inventories.get(entity) } else { None } }; trades.process_trade_action(trade_id, uid, action, get_inventory); } if let Entry::Occupied(entry) = trades.trades.entry(trade_id) { let parties = entry.get().parties; if entry.get().should_commit() { let result = commit_trade(server.state.ecs(), entry.get()); entry.remove(); for party in parties.iter() { if let Some(e) = server.state.ecs().entity_from_uid(party.0) { server.notify_client(e, ServerGeneral::FinishedTrade(result.clone())); notify_agent_simple( server.state.ecs().write_storage::(), e, AgentEvent::FinishedTrade(result.clone()), ); } trades.entity_trades.remove_entry(party); } } else { let mut entities: [Option; 2] = [None, None]; let mut inventories: [Option; 2] = [None, None]; let mut prices = None; let agents = server.state.ecs().read_storage::(); // sadly there is no map and collect on arrays for i in 0..2 { // parties.len()) { entities[i] = server.state.ecs().entity_from_uid(parties[i].0); if let Some(e) = entities[i] { inventories[i] = server .state .ecs() .read_component::() .get(e) .map(ReducedInventory::from); // Get price info from the first Agent in the trade (currently, an // Agent will never initiate a trade with another agent though) #[cfg(feature = "worldgen")] { prices = prices.or_else(|| { agents .get(e) .and_then(|a| a.behavior.trade_site) .and_then(|id| server.index.get_site_prices(id)) }); } } } drop(agents); for party in entities.iter() { if let Some(e) = *party { server.notify_client( e, ServerGeneral::UpdatePendingTrade( trade_id, entry.get().clone(), prices.clone(), ), ); #[cfg(feature = "worldgen")] notify_agent_prices( server.state.ecs().write_storage::(), &server.index, e, AgentEvent::UpdatePendingTrade(Box::new(( trade_id, entry.get().clone(), prices.clone().unwrap_or_default(), inventories.clone(), ))), ); } } } } } } } //Cancel all trades registered for a given UID. pub fn cancel_trade_for(server: &mut Server, entity: EcsEntity) { if let Some(uid) = server.state().ecs().uid_from_entity(entity) { let mut trades = server.state.ecs().write_resource::(); let active_trade = match trades.entity_trades.get(&uid) { Some(n) => *n, None => { return; }, }; let to_notify = trades.decline_trade(active_trade, uid); to_notify .and_then(|u| server.state.ecs().entity_from_uid(u.0)) .map(|e| { server.notify_client(e, ServerGeneral::FinishedTrade(TradeResult::Declined)); notify_agent_simple( server.state.ecs().write_storage::(), e, AgentEvent::FinishedTrade(TradeResult::Declined), ); }); } } /// Commit a trade that both parties have agreed to, modifying their respective /// inventories fn commit_trade(ecs: &specs::World, trade: &PendingTrade) -> TradeResult { let mut entities = Vec::new(); for party in trade.parties.iter() { match ecs.entity_from_uid(party.0) { Some(entity) => entities.push(entity), None => return TradeResult::Declined, } } let mut inventories = ecs.write_storage::(); for entity in entities.iter() { if inventories.get_mut(*entity).is_none() { return TradeResult::Declined; } } let invmsg = "inventories.get_mut(entities[who]).is_none() should have returned already"; trace!("committing trade: {:?}", trade); // Compute the net change in slots of each player during the trade, to detect // out-of-space-ness before transferring any items let mut delta_slots: [i64; 2] = [0, 0]; // local struct used to calculate delta_slots for stackable items. // Uses u128 as an intermediate value to prevent overflow. #[derive(Default)] struct ItemQuantities { full_stacks: u64, quantity_sold: u128, freed_capacity: u128, unusable_capacity: u128, } // local struct used to calculate delta_slots for stackable items struct TradeQuantities { max_stack_size: u32, trade_quantities: [ItemQuantities; 2], } impl TradeQuantities { fn new(max_stack_size: u32) -> Self { Self { max_stack_size, trade_quantities: [ItemQuantities::default(), ItemQuantities::default()], } } } // Hashmap to compute merged stackable stacks, including overflow checks // TODO: add a ComponentKey struct to compare items properly, see issue #1226 let mut stackable_items: HashMap = HashMap::new(); for who in [0, 1].iter().cloned() { for (slot, quantity) in trade.offers[who].iter() { let inventory = inventories.get_mut(entities[who]).expect(invmsg); let item = match inventory.get(*slot) { Some(item) => item, None => { error!( "PendingTrade invariant violation in trade {:?}: slots offered in a trade \ should be non-empty", trade ); return TradeResult::Declined; }, }; // assuming the quantity is never 0 match item.amount().cmp(quantity) { Ordering::Less => { error!( "PendingTrade invariant violation in trade {:?}: party {} offered more of \ an item than they have", trade, who ); return TradeResult::Declined; }, Ordering::Equal => { if item.is_stackable() { // Marks a full stack to remove. Can no longer accept items from the other // party, and therefore adds the remaining capacity it holds to // `unusable_capacity`. let TradeQuantities { max_stack_size, trade_quantities, } = stackable_items .entry(item.item_definition_id().to_string()) .or_insert_with(|| TradeQuantities::new(item.max_amount())); trade_quantities[who].full_stacks += 1; trade_quantities[who].quantity_sold += *quantity as u128; trade_quantities[who].unusable_capacity += *max_stack_size as u128 - item.amount() as u128; } else { delta_slots[who] -= 1; // exact, removes the whole stack delta_slots[1 - who] += 1; // item is not stackable, so the stacks won't merge } }, Ordering::Greater => { if item.is_stackable() { // Marks a partial stack to remove, able to accepts items from the other // party, and therefore adds the additional capacity freed after the item // exchange to `freed_capacity`. let TradeQuantities { max_stack_size: _, trade_quantities, } = stackable_items .entry(item.item_definition_id().to_string()) .or_insert_with(|| TradeQuantities::new(item.max_amount())); trade_quantities[who].quantity_sold += *quantity as u128; trade_quantities[who].freed_capacity += *quantity as u128; } else { // unreachable in theory error!( "Inventory invariant violation in trade {:?}: party {} has a stack \ larger than 1 of an unstackable item", trade, who ); return TradeResult::Declined; } }, } } } // at this point delta_slots only contains the slot variations for unstackable // items. The following loops calculates capacity for stackable items and // computes the final delta_slots in consequence. for ( item_id, TradeQuantities { max_stack_size, trade_quantities, }, ) in stackable_items.iter() { for who in [0, 1].iter().cloned() { // removes all exchanged full stacks. delta_slots[who] -= trade_quantities[who].full_stacks as i64; // calculates the available item capacity in the other party's inventory, // substracting the unusable space calculated previously, // and adding the capacity freed by the trade. let other_party_capacity = inventories .get_mut(entities[1 - who]) .expect(invmsg) .slots() .flatten() .filter_map(|it| { if it.item_definition_id() == item_id { Some(*max_stack_size as u128 - it.amount() as u128) } else { None } }) .sum::() - trade_quantities[1 - who].unusable_capacity + trade_quantities[1 - who].freed_capacity; // checks if the capacity in remaining partial stacks of the other party is // enough to contain everything, creates more stacks otherwise if other_party_capacity < trade_quantities[who].quantity_sold { let surplus = trade_quantities[who].quantity_sold - other_party_capacity; // the total amount of exchanged slots can never exceed the max inventory size // (around 4 * 2^32 slots), so the cast to i64 should be safe delta_slots[1 - who] += (surplus / *max_stack_size as u128) as i64 + 1; } } } trace!("delta_slots: {:?}", delta_slots); for who in [0, 1].iter().cloned() { // Inventories should never exceed 2^{63} slots, so the usize -> i64 // conversions here should be safe let inv = inventories.get_mut(entities[who]).expect(invmsg); if inv.populated_slots() as i64 + delta_slots[who] > inv.capacity() as i64 { return TradeResult::NotEnoughSpace; } } let mut items = [Vec::new(), Vec::new()]; let ability_map = ecs.read_resource::(); let msm = ecs.read_resource::(); for who in [0, 1].iter().cloned() { for (slot, quantity) in trade.offers[who].iter() { // Take the items one by one, to benefit from Inventory's stack handling for _ in 0..*quantity { inventories .get_mut(entities[who]) .expect(invmsg) .take(*slot, &ability_map, &msm) .map(|item| items[who].push(item)); } } } for who in [0, 1].iter().cloned() { if let Err(leftovers) = inventories .get_mut(entities[1 - who]) .expect(invmsg) .push_all(items[who].drain(..)) { // This should only happen if the arithmetic above for delta_slots says there's // enough space and there isn't (i.e. underapproximates) panic!( "Not enough space for all the items, leftovers are {:?}", leftovers ); } } TradeResult::Completed } #[cfg(test)] mod tests { use hashbrown::HashMap; use super::*; use common::{comp::slot::InvSlotId, uid::UidAllocator}; use specs::{Builder, World}; // Creates a specs World containing two Entities which have Inventory // components. Left over inventory size is determined by input. Mapping to the // returned Entities. Any input over the maximum default inventory size will // result in maximum left over space. fn create_mock_trading_world( player_inv_size: usize, merchant_inv_size: usize, ) -> (World, EcsEntity, EcsEntity) { let mut mockworld = World::new(); mockworld.insert(UidAllocator::new()); mockworld.insert(MaterialStatManifest::load().cloned()); mockworld.insert(AbilityMap::load().cloned()); mockworld.register::(); mockworld.register::(); let player: EcsEntity = mockworld .create_entity() .with(Inventory::new_empty()) .build(); let merchant: EcsEntity = mockworld .create_entity() .with(Inventory::new_empty()) .build(); { use specs::saveload::MarkerAllocator; let mut uids = mockworld.write_component::(); let mut uid_allocator = mockworld.write_resource::(); uids.insert(player, uid_allocator.allocate(player, None)) .expect("inserting player uid failed"); uids.insert(merchant, uid_allocator.allocate(merchant, None)) .expect("inserting merchant uid failed"); } let invmsg = "inventories.get_mut().is_none() should have returned already"; let capmsg = "There should be enough space here"; let mut inventories = mockworld.write_component::(); let mut playerinv = inventories.get_mut(player).expect(invmsg); if player_inv_size < playerinv.capacity() { for _ in player_inv_size..playerinv.capacity() { playerinv .push(common::comp::Item::new_from_asset_expect( "common.items.npc_armor.pants.plate_red", )) .expect(capmsg); } } let mut merchantinv = inventories.get_mut(merchant).expect(invmsg); if merchant_inv_size < merchantinv.capacity() { for _ in merchant_inv_size..merchantinv.capacity() { merchantinv .push(common::comp::Item::new_from_asset_expect( "common.items.armor.cloth_purple.foot", )) .expect(capmsg); } } drop(inventories); (mockworld, player, merchant) } fn prepare_merchant_inventory(mockworld: &World, merchant: EcsEntity) { let mut inventories = mockworld.write_component::(); let invmsg = "inventories.get_mut().is_none() should have returned already"; let capmsg = "There should be enough space here"; let mut merchantinv = inventories.get_mut(merchant).expect(invmsg); for _ in 0..10 { merchantinv .push(common::comp::Item::new_from_asset_expect( "common.items.consumable.potion_minor", )) .expect(capmsg); merchantinv .push(common::comp::Item::new_from_asset_expect( "common.items.food.meat.fish_cooked", )) .expect(capmsg); } drop(inventories); } #[test] fn commit_trade_with_stackable_item_test() { use common::{assets::AssetExt, comp::item::ItemDef}; use std::sync::Arc; let (mockworld, player, merchant) = create_mock_trading_world(1, 20); prepare_merchant_inventory(&mockworld, merchant); let invmsg = "inventories.get_mut().is_none() should have returned already"; let capmsg = "There should be enough space here"; let mut inventories = mockworld.write_component::(); let mut playerinv = inventories.get_mut(player).expect(invmsg); playerinv .push(common::comp::Item::new_from_asset_expect( "common.items.consumable.potion_minor", )) .expect(capmsg); let potion_asset = "common.items.consumable.potion_minor"; let potion = common::comp::Item::new_from_asset_expect(potion_asset); let potion_def = Arc::::load_expect_cloned(potion_asset); let merchantinv = inventories.get_mut(merchant).expect(invmsg); let potioninvid = merchantinv .get_slot_of_item(&potion) .expect("expected get_slot_of_item to return"); let playerid = mockworld .uid_from_entity(player) .expect("mockworld.uid_from_entity(player) should have returned"); let merchantid = mockworld .uid_from_entity(merchant) .expect("mockworld.uid_from_entity(player) should have returned"); let playeroffers: HashMap = HashMap::new(); let mut merchantoffers: HashMap = HashMap::new(); merchantoffers.insert(potioninvid, 1); let trade = PendingTrade { parties: [playerid, merchantid], accept_flags: [true, true], offers: [playeroffers, merchantoffers], phase: common::trade::TradePhase::Review, }; drop(inventories); let traderes = commit_trade(&mockworld, &trade); assert_eq!(traderes, TradeResult::Completed); let mut inventories = mockworld.write_component::(); let playerinv = inventories.get_mut(player).expect(invmsg); let potioncount = playerinv.item_count(&potion_def); assert_eq!(potioncount, 2); } #[test] fn commit_trade_with_full_inventory_test() { let (mockworld, player, merchant) = create_mock_trading_world(1, 20); prepare_merchant_inventory(&mockworld, merchant); let invmsg = "inventories.get_mut().is_none() should have returned already"; let capmsg = "There should be enough space here"; let mut inventories = mockworld.write_component::(); let mut playerinv = inventories.get_mut(player).expect(invmsg); playerinv .push(common::comp::Item::new_from_asset_expect( "common.items.consumable.potion_minor", )) .expect(capmsg); let fish = common::comp::Item::new_from_asset_expect("common.items.food.meat.fish_cooked"); let merchantinv = inventories.get_mut(merchant).expect(invmsg); let fishinvid = merchantinv .get_slot_of_item(&fish) .expect("expected get_slot_of_item to return"); let playerid = mockworld .uid_from_entity(player) .expect("mockworld.uid_from_entity(player) should have returned"); let merchantid = mockworld .uid_from_entity(merchant) .expect("mockworld.uid_from_entity(player) should have returned"); let playeroffers: HashMap = HashMap::new(); let mut merchantoffers: HashMap = HashMap::new(); merchantoffers.insert(fishinvid, 1); let trade = PendingTrade { parties: [playerid, merchantid], accept_flags: [true, true], offers: [playeroffers, merchantoffers], phase: common::trade::TradePhase::Review, }; drop(inventories); let traderes = commit_trade(&mockworld, &trade); assert_eq!(traderes, TradeResult::NotEnoughSpace); } #[test] fn commit_trade_with_empty_inventory_test() { let (mockworld, player, merchant) = create_mock_trading_world(20, 20); prepare_merchant_inventory(&mockworld, merchant); let invmsg = "inventories.get_mut().is_none() should have returned already"; let mut inventories = mockworld.write_component::(); let fish = common::comp::Item::new_from_asset_expect("common.items.food.meat.fish_cooked"); let merchantinv = inventories.get_mut(merchant).expect(invmsg); let fishinvid = merchantinv .get_slot_of_item(&fish) .expect("expected get_slot_of_item to return"); let playerid = mockworld .uid_from_entity(player) .expect("mockworld.uid_from_entity(player) should have returned"); let merchantid = mockworld .uid_from_entity(merchant) .expect("mockworld.uid_from_entity(player) should have returned"); let playeroffers: HashMap = HashMap::new(); let mut merchantoffers: HashMap = HashMap::new(); merchantoffers.insert(fishinvid, 1); let trade = PendingTrade { parties: [playerid, merchantid], accept_flags: [true, true], offers: [playeroffers, merchantoffers], phase: common::trade::TradePhase::Review, }; drop(inventories); let traderes = commit_trade(&mockworld, &trade); assert_eq!(traderes, TradeResult::Completed); } #[test] fn commit_trade_with_both_full_inventories_test() { let (mockworld, player, merchant) = create_mock_trading_world(2, 2); prepare_merchant_inventory(&mockworld, merchant); let invmsg = "inventories.get_mut().is_none() should have returned already"; let capmsg = "There should be enough space here"; let mut inventories = mockworld.write_component::(); let fish = common::comp::Item::new_from_asset_expect("common.items.food.meat.fish_cooked"); let merchantinv = inventories.get_mut(merchant).expect(invmsg); let fishinvid = merchantinv .get_slot_of_item(&fish) .expect("expected get_slot_of_item to return"); let potion = common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor"); let mut playerinv = inventories.get_mut(player).expect(invmsg); playerinv .push(common::comp::Item::new_from_asset_expect( "common.items.consumable.potion_minor", )) .expect(capmsg); let potioninvid = playerinv .get_slot_of_item(&potion) .expect("expected get_slot_of_item to return"); let playerid = mockworld .uid_from_entity(player) .expect("mockworld.uid_from_entity(player) should have returned"); let merchantid = mockworld .uid_from_entity(merchant) .expect("mockworld.uid_from_entity(player) should have returned"); let mut playeroffers: HashMap = HashMap::new(); playeroffers.insert(potioninvid, 1); let mut merchantoffers: HashMap = HashMap::new(); merchantoffers.insert(fishinvid, 10); let trade = PendingTrade { parties: [playerid, merchantid], accept_flags: [true, true], offers: [playeroffers, merchantoffers], phase: common::trade::TradePhase::Review, }; drop(inventories); let traderes = commit_trade(&mockworld, &trade); assert_eq!(traderes, TradeResult::Completed); } #[test] fn commit_trade_with_overflow() { let (mockworld, player, merchant) = create_mock_trading_world(2, 20); prepare_merchant_inventory(&mockworld, merchant); let invmsg = "inventories.get_mut().is_none() should have returned already"; let capmsg = "There should be enough space here"; let mut inventories = mockworld.write_component::(); let mut playerinv = inventories.get_mut(player).expect(invmsg); let mut potion = common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor"); potion .set_amount(potion.max_amount() - 2) .expect("Should be below the max amount"); playerinv.push(potion).expect(capmsg); let potion = common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor"); let merchantinv = inventories.get_mut(merchant).expect(invmsg); let potioninvid = merchantinv .get_slot_of_item(&potion) .expect("expected get_slot_of_item to return"); let playerid = mockworld .uid_from_entity(player) .expect("mockworld.uid_from_entity(player) should have returned"); let merchantid = mockworld .uid_from_entity(merchant) .expect("mockworld.uid_from_entity(player) should have returned"); let playeroffers: HashMap = HashMap::new(); let mut merchantoffers: HashMap = HashMap::new(); merchantoffers.insert(potioninvid, 10); let trade = PendingTrade { parties: [playerid, merchantid], accept_flags: [true, true], offers: [playeroffers, merchantoffers], phase: common::trade::TradePhase::Review, }; drop(inventories); let traderes = commit_trade(&mockworld, &trade); assert_eq!(traderes, TradeResult::Completed); let mut inventories = mockworld.write_component::(); let mut playerinv = inventories.get_mut(player).expect(invmsg); let slot1 = playerinv .get_slot_of_item(&potion) .expect("There should be a slot here"); let item1 = playerinv .remove(slot1) .expect("The slot should not be empty"); let slot2 = playerinv .get_slot_of_item(&potion) .expect("There should be a slot here"); let item2 = playerinv .remove(slot2) .expect("The slot should not be empty"); assert_eq!(item1.amount(), potion.max_amount()); assert_eq!(item2.amount(), 8); } #[test] fn commit_trade_with_inventory_overflow_failure() { let (mockworld, player, merchant) = create_mock_trading_world(2, 20); prepare_merchant_inventory(&mockworld, merchant); let invmsg = "inventories.get_mut().is_none() should have returned already"; let capmsg = "There should be enough space here"; let mut inventories = mockworld.write_component::(); let mut playerinv = inventories.get_mut(player).expect(invmsg); let mut potion = common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor"); potion .set_amount(potion.max_amount() - 2) .expect("Should be below the max amount"); playerinv.push(potion).expect(capmsg); let mut potion = common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor"); potion .set_amount(potion.max_amount() - 2) .expect("Should be below the max amount"); playerinv.push(potion).expect(capmsg); let potion = common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor"); let merchantinv = inventories.get_mut(merchant).expect(invmsg); let potioninvid = merchantinv .get_slot_of_item(&potion) .expect("expected get_slot_of_item to return"); let playerid = mockworld .uid_from_entity(player) .expect("mockworld.uid_from_entity(player) should have returned"); let merchantid = mockworld .uid_from_entity(merchant) .expect("mockworld.uid_from_entity(player) should have returned"); let playeroffers: HashMap = HashMap::new(); let mut merchantoffers: HashMap = HashMap::new(); merchantoffers.insert(potioninvid, 5); let trade = PendingTrade { parties: [playerid, merchantid], accept_flags: [true, true], offers: [playeroffers, merchantoffers], phase: common::trade::TradePhase::Review, }; drop(inventories); let traderes = commit_trade(&mockworld, &trade); assert_eq!(traderes, TradeResult::NotEnoughSpace); } #[test] fn commit_trade_with_inventory_overflow_success() { let (mockworld, player, merchant) = create_mock_trading_world(2, 20); prepare_merchant_inventory(&mockworld, merchant); let invmsg = "inventories.get_mut().is_none() should have returned already"; let capmsg = "There should be enough space here"; let mut inventories = mockworld.write_component::(); let mut playerinv = inventories.get_mut(player).expect(invmsg); let mut potion = common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor"); potion .set_amount(potion.max_amount() - 2) .expect("Should be below the max amount"); playerinv.push(potion).expect(capmsg); let mut potion = common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor"); potion .set_amount(potion.max_amount() - 2) .expect("Should be below the max amount"); playerinv.push(potion).expect(capmsg); let potion = common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor"); let merchantinv = inventories.get_mut(merchant).expect(invmsg); let potioninvid = merchantinv .get_slot_of_item(&potion) .expect("expected get_slot_of_item to return"); let playerid = mockworld .uid_from_entity(player) .expect("mockworld.uid_from_entity(player) should have returned"); let merchantid = mockworld .uid_from_entity(merchant) .expect("mockworld.uid_from_entity(player) should have returned"); let playeroffers: HashMap = HashMap::new(); let mut merchantoffers: HashMap = HashMap::new(); merchantoffers.insert(potioninvid, 4); let trade = PendingTrade { parties: [playerid, merchantid], accept_flags: [true, true], offers: [playeroffers, merchantoffers], phase: common::trade::TradePhase::Review, }; drop(inventories); let traderes = commit_trade(&mockworld, &trade); assert_eq!(traderes, TradeResult::Completed); } }