use crate::Server;
use common::{
comp::{
agent::{Agent, AgentEvent},
inventory::{
item::{tool::AbilityMap, ItemDefinitionIdOwned, MaterialStatManifest},
Inventory,
},
},
event::ProcessTradeActionEvent,
trade::{PendingTrade, ReducedInventory, TradeAction, 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, num::NonZeroU32};
use tracing::{error, trace};
#[cfg(feature = "worldgen")]
use world::IndexOwned;
pub fn notify_agent_simple(
agents: &mut specs::WriteStorage<Agent>,
entity: EcsEntity,
event: AgentEvent,
) {
if let Some(agent) = agents.get_mut(entity) {
agent.inbox.push_back(event);
}
}
#[cfg(feature = "worldgen")]
fn notify_agent_prices(
mut agents: specs::WriteStorage<Agent>,
index: &IndexOwned,
entity: EcsEntity,
event: AgentEvent,
) {
if let Some((site_id, agent)) = agents.get_mut(entity).map(|a| (a.behavior.trade_site(), a)) {
if let AgentEvent::UpdatePendingTrade(boxval) = event {
let prices = site_id
.and_then(|site_id| index.get_site_prices(site_id))
.unwrap_or(boxval.2);
agent
.inbox
.push_back(AgentEvent::UpdatePendingTrade(Box::new((
boxval.0, boxval.1, prices, boxval.3,
))));
}
}
}
pub(super) fn handle_process_trade_action(
server: &mut Server,
ProcessTradeActionEvent(entity, trade_id, action): ProcessTradeActionEvent,
) {
if let Some(uid) = server.state.ecs().uid_from_entity(entity) {
let mut trades = server.state.ecs().write_resource::<Trades>();
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))
.map(|e| {
server.notify_client(e, ServerGeneral::FinishedTrade(TradeResult::Declined));
notify_agent_simple(
&mut server.state.ecs().write_storage(),
e,
AgentEvent::FinishedTrade(TradeResult::Declined),
);
});
} else {
{
let ecs = server.state.ecs();
let inventories = ecs.read_component::<Inventory>();
let get_inventory = |uid: Uid| {
if let Some(entity) = ecs.entity_from_uid(uid) {
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) {
server.notify_client(e, ServerGeneral::FinishedTrade(result.clone()));
notify_agent_simple(
&mut server.state.ecs().write_storage(),
e,
AgentEvent::FinishedTrade(result.clone()),
);
}
trades.entity_trades.remove_entry(party);
}
} else {
let mut entities: [Option<specs::Entity>; 2] = [None, None];
let mut inventories: [Option<ReducedInventory>; 2] = [None, None];
#[cfg(feature = "worldgen")]
let mut prices = None;
#[cfg(not(feature = "worldgen"))]
let prices = None;
let agents = server.state.ecs().read_storage::<Agent>();
for i in 0..2 {
entities[i] = server.state.ecs().entity_from_uid(parties[i]);
if let Some(e) = entities[i] {
inventories[i] = server
.state
.ecs()
.read_component::<Inventory>()
.get(e)
.map(ReducedInventory::from);
#[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::<Agent>(),
&server.index,
e,
AgentEvent::UpdatePendingTrade(Box::new((
trade_id,
entry.get().clone(),
prices.clone().unwrap_or_default(),
inventories.clone(),
))),
);
}
}
}
}
}
}
}
pub(crate) fn cancel_trades_for(state: &mut common_state::State, entity: EcsEntity) {
let ecs = state.ecs();
if let Some(uid) = ecs.uid_from_entity(entity) {
let mut trades = ecs.write_resource::<Trades>();
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| ecs.entity_from_uid(u)).map(|e| {
if let Some(c) = ecs.read_storage::<crate::Client>().get(e) {
c.send_fallible(ServerGeneral::FinishedTrade(TradeResult::Declined));
}
notify_agent_simple(
&mut ecs.write_storage::<Agent>(),
e,
AgentEvent::FinishedTrade(TradeResult::Declined),
);
});
}
}
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) {
Some(entity) => entities.push(entity),
None => return TradeResult::Declined,
}
}
let mut inventories = ecs.write_storage::<Inventory>();
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);
let mut delta_slots: [i64; 2] = [0, 0];
#[derive(Default)]
struct ItemQuantities {
full_stacks: u64,
quantity_sold: u128,
freed_capacity: u128,
unusable_capacity: u128,
}
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()],
}
}
}
let mut stackable_items: HashMap<ItemDefinitionIdOwned, TradeQuantities> = 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;
},
};
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() {
let TradeQuantities {
max_stack_size,
trade_quantities,
} = stackable_items
.entry(item.item_definition_id().to_owned())
.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; delta_slots[1 - who] += 1; }
},
Ordering::Greater => {
if item.is_stackable() {
let TradeQuantities {
max_stack_size: _,
trade_quantities,
} = stackable_items
.entry(item.item_definition_id().to_owned())
.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 {
error!(
"Inventory invariant violation in trade {:?}: party {} has a stack \
larger than 1 of an unstackable item",
trade, who
);
return TradeResult::Declined;
}
},
}
}
}
for (
item_id,
TradeQuantities {
max_stack_size,
trade_quantities,
},
) in stackable_items.iter()
{
for who in [0, 1].iter().cloned() {
delta_slots[who] -= trade_quantities[who].full_stacks as i64;
let other_party_capacity = inventories
.get_mut(entities[1 - who])
.expect(invmsg)
.slots()
.flatten()
.filter_map(|it| {
if it.item_definition_id() == item_id.as_ref() {
Some(*max_stack_size as u128 - it.amount() as u128)
} else {
None
}
})
.sum::<u128>()
- trade_quantities[1 - who].unusable_capacity
+ trade_quantities[1 - who].freed_capacity;
if other_party_capacity < trade_quantities[who].quantity_sold {
let surplus = trade_quantities[who].quantity_sold - other_party_capacity;
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() {
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::<AbilityMap>();
let msm = ecs.read_resource::<MaterialStatManifest>();
for who in [0, 1].iter().cloned() {
for (slot, quantity) in trade.offers[who].iter() {
if let Some(quantity) = NonZeroU32::new(*quantity) {
if let Some(item) = inventories
.get_mut(entities[who])
.expect(invmsg)
.take_amount(*slot, quantity, &ability_map, &msm)
{
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(..))
{
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::IdMaps};
use specs::{Builder, World};
fn create_mock_trading_world(
player_inv_size: usize,
merchant_inv_size: usize,
) -> (World, EcsEntity, EcsEntity) {
let mut mockworld = World::new();
mockworld.insert(IdMaps::new());
mockworld.insert(MaterialStatManifest::load().cloned());
mockworld.insert(AbilityMap::load().cloned());
mockworld.register::<Inventory>();
mockworld.register::<Uid>();
let player: EcsEntity = mockworld
.create_entity()
.with(Inventory::with_empty())
.build();
let merchant: EcsEntity = mockworld
.create_entity()
.with(Inventory::with_empty())
.build();
{
let mut uids = mockworld.write_component::<Uid>();
let mut id_maps = mockworld.write_resource::<IdMaps>();
uids.insert(player, id_maps.allocate(player))
.expect("inserting player uid failed");
uids.insert(merchant, id_maps.allocate(merchant))
.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::<Inventory>();
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::<Inventory>();
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::<Inventory>();
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::<ItemDef>::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<InvSlotId, u32> = HashMap::new();
let mut merchantoffers: HashMap<InvSlotId, u32> = 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::<Inventory>();
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::<Inventory>();
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<InvSlotId, u32> = HashMap::new();
let mut merchantoffers: HashMap<InvSlotId, u32> = 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::<Inventory>();
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<InvSlotId, u32> = HashMap::new();
let mut merchantoffers: HashMap<InvSlotId, u32> = 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::<Inventory>();
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<InvSlotId, u32> = HashMap::new();
playeroffers.insert(potioninvid, 1);
let mut merchantoffers: HashMap<InvSlotId, u32> = 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::<Inventory>();
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<InvSlotId, u32> = HashMap::new();
let mut merchantoffers: HashMap<InvSlotId, u32> = 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::<Inventory>();
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::<Inventory>();
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<InvSlotId, u32> = HashMap::new();
let mut merchantoffers: HashMap<InvSlotId, u32> = 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::<Inventory>();
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<InvSlotId, u32> = HashMap::new();
let mut merchantoffers: HashMap<InvSlotId, u32> = 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);
}
}