veloren_server/events/
trade.rs

1use crate::Server;
2use common::{
3    comp::{
4        CharacterState, Health,
5        agent::{Agent, AgentEvent},
6        inventory::{
7            Inventory,
8            item::{ItemDefinitionIdOwned, MaterialStatManifest, tool::AbilityMap},
9        },
10    },
11    event::ProcessTradeActionEvent,
12    trade::{PendingTrade, ReducedInventory, TradeAction, TradeResult, Trades},
13};
14use common_net::{
15    msg::ServerGeneral,
16    sync::{Uid, WorldSyncExt},
17};
18use hashbrown::{HashMap, hash_map::Entry};
19use specs::{Entity as EcsEntity, world::WorldExt};
20use std::{cmp::Ordering, num::NonZeroU32};
21use tracing::{error, trace};
22#[cfg(feature = "worldgen")]
23use world::IndexOwned;
24
25pub fn notify_agent_simple(
26    agents: &mut specs::WriteStorage<Agent>,
27    entity: EcsEntity,
28    event: AgentEvent,
29) {
30    if let Some(agent) = agents.get_mut(entity) {
31        agent.inbox.push_back(event);
32    }
33}
34
35#[cfg(feature = "worldgen")]
36fn notify_agent_prices(
37    mut agents: specs::WriteStorage<Agent>,
38    index: &IndexOwned,
39    entity: EcsEntity,
40    event: AgentEvent,
41) {
42    if let Some((site_id, agent)) = agents.get_mut(entity).map(|a| (a.behavior.trade_site(), a)) {
43        if let AgentEvent::UpdatePendingTrade(boxval) = event {
44            // Prefer using this Agent's price data, but use the counterparty's price
45            // data if we don't have price data
46            let prices = site_id
47                .and_then(|site_id| index.get_site_prices(site_id))
48                .unwrap_or(boxval.2);
49            // Box<(tid, pend, _, inventories)>) = event {
50            agent
51                .inbox
52                .push_back(AgentEvent::UpdatePendingTrade(Box::new((
53                    boxval.0, boxval.1, prices, boxval.3,
54                ))));
55        }
56    }
57}
58
59/// Invoked when the trade UI is up, handling item changes, accepts, etc
60pub(super) fn handle_process_trade_action(
61    server: &mut Server,
62    ProcessTradeActionEvent(entity, trade_id, action): ProcessTradeActionEvent,
63) {
64    if let Some(uid) = server.state.ecs().uid_from_entity(entity) {
65        let mut trades = server.state.ecs().write_resource::<Trades>();
66        if let TradeAction::Decline = action {
67            let to_notify = trades.decline_trade(trade_id, uid);
68            to_notify
69                .and_then(|u| server.state.ecs().entity_from_uid(u))
70                .map(|e| {
71                    server.notify_client(e, ServerGeneral::FinishedTrade(TradeResult::Declined));
72                    notify_agent_simple(
73                        &mut server.state.ecs().write_storage(),
74                        e,
75                        AgentEvent::FinishedTrade(TradeResult::Declined),
76                    );
77                });
78        }
79        // Check that the entity doing the trade action is alive and well.
80        else if server
81            .state
82            .ecs()
83            .read_storage::<Health>()
84            .get(entity)
85            .is_none_or(|health| {
86                !(health.is_dead
87                    || (health.has_consumed_death_protection()
88                        && matches!(
89                            server
90                                .state
91                                .ecs()
92                                .read_storage::<CharacterState>()
93                                .get(entity),
94                            Some(CharacterState::Crawl)
95                        )))
96            })
97        {
98            {
99                let ecs = server.state.ecs();
100                let inventories = ecs.read_component::<Inventory>();
101                let get_inventory = |uid: Uid| {
102                    if let Some(entity) = ecs.entity_from_uid(uid) {
103                        inventories.get(entity)
104                    } else {
105                        None
106                    }
107                };
108                trades.process_trade_action(trade_id, uid, action, get_inventory);
109            }
110            if let Entry::Occupied(entry) = trades.trades.entry(trade_id) {
111                let parties = entry.get().parties;
112                if entry.get().should_commit() {
113                    let result = commit_trade(server.state.ecs(), entry.get());
114                    entry.remove();
115                    for party in parties.iter() {
116                        if let Some(e) = server.state.ecs().entity_from_uid(*party) {
117                            server.notify_client(e, ServerGeneral::FinishedTrade(result.clone()));
118                            notify_agent_simple(
119                                &mut server.state.ecs().write_storage(),
120                                e,
121                                AgentEvent::FinishedTrade(result.clone()),
122                            );
123                        }
124                        trades.entity_trades.remove_entry(party);
125                    }
126                } else {
127                    let mut entities: [Option<specs::Entity>; 2] = [None, None];
128                    let mut inventories: [Option<ReducedInventory>; 2] = [None, None];
129                    #[cfg(feature = "worldgen")]
130                    let mut prices = None;
131                    #[cfg(not(feature = "worldgen"))]
132                    let prices = None;
133                    let agents = server.state.ecs().read_storage::<Agent>();
134                    // sadly there is no map and collect on arrays
135                    for i in 0..2 {
136                        // parties.len()) {
137                        entities[i] = server.state.ecs().entity_from_uid(parties[i]);
138                        if let Some(e) = entities[i] {
139                            inventories[i] = server
140                                .state
141                                .ecs()
142                                .read_component::<Inventory>()
143                                .get(e)
144                                .map(ReducedInventory::from);
145                            // Get price info from the first Agent in the trade (currently, an
146                            // Agent will never initiate a trade with another agent though)
147                            #[cfg(feature = "worldgen")]
148                            {
149                                prices = prices.or_else(|| {
150                                    agents
151                                        .get(e)
152                                        .and_then(|a| a.behavior.trade_site())
153                                        .and_then(|id| server.index.get_site_prices(id))
154                                });
155                            }
156                        }
157                    }
158                    drop(agents);
159                    for party in entities.iter() {
160                        if let Some(e) = *party {
161                            server.notify_client(
162                                e,
163                                ServerGeneral::UpdatePendingTrade(
164                                    trade_id,
165                                    entry.get().clone(),
166                                    prices.clone(),
167                                ),
168                            );
169                            #[cfg(feature = "worldgen")]
170                            notify_agent_prices(
171                                server.state.ecs().write_storage::<Agent>(),
172                                &server.index,
173                                e,
174                                AgentEvent::UpdatePendingTrade(Box::new((
175                                    trade_id,
176                                    entry.get().clone(),
177                                    prices.clone().unwrap_or_default(),
178                                    inventories.clone(),
179                                ))),
180                            );
181                        }
182                    }
183                }
184            }
185        }
186    }
187}
188
189/// Cancel all trades registered for a given UID.
190///
191/// Note: This doesn't send any notification to the provided entity (only other
192/// participants in the trade). It is assumed that the supplied entity either no
193/// longer exists or is awareof this cancellation through other means (e.g.
194/// client getting ExitInGameSuccess message knows that it should clear any
195/// trades).
196pub(crate) fn cancel_trades_for(state: &mut common_state::State, entity: EcsEntity) {
197    let ecs = state.ecs();
198    if let Some(uid) = ecs.uid_from_entity(entity) {
199        let mut trades = ecs.write_resource::<Trades>();
200
201        let active_trade = match trades.entity_trades.get(&uid) {
202            Some(n) => *n,
203            None => return,
204        };
205
206        let to_notify = trades.decline_trade(active_trade, uid);
207        to_notify.and_then(|u| ecs.entity_from_uid(u)).map(|e| {
208            if let Some(c) = ecs.read_storage::<crate::Client>().get(e) {
209                c.send_fallible(ServerGeneral::FinishedTrade(TradeResult::Declined));
210            }
211            notify_agent_simple(
212                &mut ecs.write_storage::<Agent>(),
213                e,
214                AgentEvent::FinishedTrade(TradeResult::Declined),
215            );
216        });
217    }
218}
219
220/// Commit a trade that both parties have agreed to, modifying their respective
221/// inventories
222fn commit_trade(ecs: &specs::World, trade: &PendingTrade) -> TradeResult {
223    let mut entities = Vec::new();
224    for party in trade.parties.iter() {
225        match ecs.entity_from_uid(*party) {
226            Some(entity) => entities.push(entity),
227            None => return TradeResult::Declined,
228        }
229    }
230    let mut inventories = ecs.write_storage::<Inventory>();
231    for entity in entities.iter() {
232        if inventories.get_mut(*entity).is_none() {
233            return TradeResult::Declined;
234        }
235    }
236    let invmsg = "inventories.get_mut(entities[who]).is_none() should have returned already";
237    trace!("committing trade: {:?}", trade);
238    // Compute the net change in slots of each player during the trade, to detect
239    // out-of-space-ness before transferring any items
240    let mut delta_slots: [i64; 2] = [0, 0];
241
242    // local struct used to calculate delta_slots for stackable items.
243    // Uses u128 as an intermediate value to prevent overflow.
244    #[derive(Default)]
245    struct ItemQuantities {
246        full_stacks: u64,
247        quantity_sold: u128,
248        freed_capacity: u128,
249        unusable_capacity: u128,
250    }
251
252    // local struct used to calculate delta_slots for stackable items
253    struct TradeQuantities {
254        max_stack_size: u32,
255        trade_quantities: [ItemQuantities; 2],
256    }
257
258    impl TradeQuantities {
259        fn new(max_stack_size: u32) -> Self {
260            Self {
261                max_stack_size,
262                trade_quantities: [ItemQuantities::default(), ItemQuantities::default()],
263            }
264        }
265    }
266
267    // Hashmap to compute merged stackable stacks, including overflow checks
268    let mut stackable_items: HashMap<ItemDefinitionIdOwned, TradeQuantities> = HashMap::new();
269    for who in [0, 1].iter().cloned() {
270        for (slot, quantity) in trade.offers[who].iter() {
271            let inventory = inventories.get_mut(entities[who]).expect(invmsg);
272            let item = match inventory.get(*slot) {
273                Some(item) => item,
274                None => {
275                    error!(
276                        "PendingTrade invariant violation in trade {:?}: slots offered in a trade \
277                         should be non-empty",
278                        trade
279                    );
280                    return TradeResult::Declined;
281                },
282            };
283
284            // assuming the quantity is never 0
285            match item.amount().cmp(quantity) {
286                Ordering::Less => {
287                    error!(
288                        "PendingTrade invariant violation in trade {:?}: party {} offered more of \
289                         an item than they have",
290                        trade, who
291                    );
292                    return TradeResult::Declined;
293                },
294                Ordering::Equal => {
295                    if item.is_stackable() {
296                        // Marks a full stack to remove. Can no longer accept items from the other
297                        // party, and therefore adds the remaining capacity it holds to
298                        // `unusable_capacity`.
299                        let TradeQuantities {
300                            max_stack_size,
301                            trade_quantities,
302                        } = stackable_items
303                            .entry(item.item_definition_id().to_owned())
304                            .or_insert_with(|| TradeQuantities::new(item.max_amount()));
305
306                        trade_quantities[who].full_stacks += 1;
307                        trade_quantities[who].quantity_sold += *quantity as u128;
308                        trade_quantities[who].unusable_capacity +=
309                            *max_stack_size as u128 - item.amount() as u128;
310                    } else {
311                        delta_slots[who] -= 1; // exact, removes the whole stack
312                        delta_slots[1 - who] += 1; // item is not stackable, so the stacks won't merge
313                    }
314                },
315                Ordering::Greater => {
316                    if item.is_stackable() {
317                        // Marks a partial stack to remove, able to accepts items from the other
318                        // party, and therefore adds the additional capacity freed after the item
319                        // exchange to `freed_capacity`.
320                        let TradeQuantities {
321                            max_stack_size: _,
322                            trade_quantities,
323                        } = stackable_items
324                            .entry(item.item_definition_id().to_owned())
325                            .or_insert_with(|| TradeQuantities::new(item.max_amount()));
326
327                        trade_quantities[who].quantity_sold += *quantity as u128;
328                        trade_quantities[who].freed_capacity += *quantity as u128;
329                    } else {
330                        // unreachable in theory
331                        error!(
332                            "Inventory invariant violation in trade {:?}: party {} has a stack \
333                             larger than 1 of an unstackable item",
334                            trade, who
335                        );
336                        return TradeResult::Declined;
337                    }
338                },
339            }
340        }
341    }
342    // at this point delta_slots only contains the slot variations for unstackable
343    // items. The following loops calculates capacity for stackable items and
344    // computes the final delta_slots in consequence.
345
346    for (
347        item_id,
348        TradeQuantities {
349            max_stack_size,
350            trade_quantities,
351        },
352    ) in stackable_items.iter()
353    {
354        for who in [0, 1].iter().cloned() {
355            // removes all exchanged full stacks.
356            delta_slots[who] -= trade_quantities[who].full_stacks as i64;
357
358            // calculates the available item capacity in the other party's inventory,
359            // substracting the unusable space calculated previously,
360            // and adding the capacity freed by the trade.
361            let other_party_capacity = inventories
362                .get_mut(entities[1 - who])
363                .expect(invmsg)
364                .slots()
365                .flatten()
366                .filter_map(|it| {
367                    if it.item_definition_id() == item_id.as_ref() {
368                        Some(*max_stack_size as u128 - it.amount() as u128)
369                    } else {
370                        None
371                    }
372                })
373                .sum::<u128>()
374                - trade_quantities[1 - who].unusable_capacity
375                + trade_quantities[1 - who].freed_capacity;
376
377            // checks if the capacity in remaining partial stacks of the other party is
378            // enough to contain everything, creates more stacks otherwise
379            if other_party_capacity < trade_quantities[who].quantity_sold {
380                let surplus = trade_quantities[who].quantity_sold - other_party_capacity;
381                // the total amount of exchanged slots can never exceed the max inventory size
382                // (around 4 * 2^32 slots), so the cast to i64 should be safe
383                delta_slots[1 - who] += (surplus / *max_stack_size as u128) as i64 + 1;
384            }
385        }
386    }
387
388    trace!("delta_slots: {:?}", delta_slots);
389    for who in [0, 1].iter().cloned() {
390        // Inventories should never exceed 2^{63} slots, so the usize -> i64
391        // conversions here should be safe
392        let inv = inventories.get_mut(entities[who]).expect(invmsg);
393        if inv.populated_slots() as i64 + delta_slots[who] > inv.capacity() as i64 {
394            return TradeResult::NotEnoughSpace;
395        }
396    }
397
398    let mut items = [Vec::new(), Vec::new()];
399    let ability_map = ecs.read_resource::<AbilityMap>();
400    let msm = ecs.read_resource::<MaterialStatManifest>();
401    for who in [0, 1].iter().cloned() {
402        for (slot, quantity) in trade.offers[who].iter() {
403            if let Some(quantity) = NonZeroU32::new(*quantity) {
404                if let Some(item) = inventories
405                    .get_mut(entities[who])
406                    .expect(invmsg)
407                    .take_amount(*slot, quantity, &ability_map, &msm)
408                {
409                    items[who].push(item);
410                }
411            }
412        }
413    }
414
415    for who in [0, 1].iter().cloned() {
416        if let Err(leftovers) = inventories
417            .get_mut(entities[1 - who])
418            .expect(invmsg)
419            .push_all(items[who].drain(..))
420        {
421            // This should only happen if the arithmetic above for delta_slots says there's
422            // enough space and there isn't (i.e. underapproximates)
423            panic!(
424                "Not enough space for all the items, leftovers are {:?}",
425                leftovers
426            );
427        }
428    }
429
430    TradeResult::Completed
431}
432
433#[cfg(test)]
434mod tests {
435    use hashbrown::HashMap;
436
437    use super::*;
438    use common::{comp::slot::InvSlotId, uid::IdMaps};
439
440    use specs::{Builder, World};
441
442    // Creates a specs World containing two Entities which have Inventory
443    // components. Left over inventory size is determined by input. Mapping to the
444    // returned Entities. Any input over the maximum default inventory size will
445    // result in maximum left over space.
446    fn create_mock_trading_world(
447        player_inv_size: usize,
448        merchant_inv_size: usize,
449    ) -> (World, EcsEntity, EcsEntity) {
450        let mut mockworld = World::new();
451        mockworld.insert(IdMaps::new());
452        mockworld.insert(MaterialStatManifest::load().cloned());
453        mockworld.insert(AbilityMap::load().cloned());
454        mockworld.register::<Inventory>();
455        mockworld.register::<Uid>();
456
457        let player: EcsEntity = mockworld
458            .create_entity()
459            .with(Inventory::with_empty())
460            .build();
461
462        let merchant: EcsEntity = mockworld
463            .create_entity()
464            .with(Inventory::with_empty())
465            .build();
466
467        {
468            let mut uids = mockworld.write_component::<Uid>();
469            let mut id_maps = mockworld.write_resource::<IdMaps>();
470            uids.insert(player, id_maps.allocate(player))
471                .expect("inserting player uid failed");
472            uids.insert(merchant, id_maps.allocate(merchant))
473                .expect("inserting merchant uid failed");
474        }
475
476        let invmsg = "inventories.get_mut().is_none() should have returned already";
477        let capmsg = "There should be enough space here";
478        let mut inventories = mockworld.write_component::<Inventory>();
479        let mut playerinv = inventories.get_mut(player).expect(invmsg);
480        if player_inv_size < playerinv.capacity() {
481            for _ in player_inv_size..playerinv.capacity() {
482                playerinv
483                    .push(common::comp::Item::new_from_asset_expect(
484                        "common.items.npc_armor.pants.plate_red",
485                    ))
486                    .expect(capmsg);
487            }
488        }
489
490        let mut merchantinv = inventories.get_mut(merchant).expect(invmsg);
491        if merchant_inv_size < merchantinv.capacity() {
492            for _ in merchant_inv_size..merchantinv.capacity() {
493                merchantinv
494                    .push(common::comp::Item::new_from_asset_expect(
495                        "common.items.armor.cloth_purple.foot",
496                    ))
497                    .expect(capmsg);
498            }
499        }
500        drop(inventories);
501
502        (mockworld, player, merchant)
503    }
504
505    fn prepare_merchant_inventory(mockworld: &World, merchant: EcsEntity) {
506        let mut inventories = mockworld.write_component::<Inventory>();
507        let invmsg = "inventories.get_mut().is_none() should have returned already";
508        let capmsg = "There should be enough space here";
509        let mut merchantinv = inventories.get_mut(merchant).expect(invmsg);
510        for _ in 0..10 {
511            merchantinv
512                .push(common::comp::Item::new_from_asset_expect(
513                    "common.items.consumable.potion_minor",
514                ))
515                .expect(capmsg);
516            merchantinv
517                .push(common::comp::Item::new_from_asset_expect(
518                    "common.items.food.meat.fish_cooked",
519                ))
520                .expect(capmsg);
521        }
522        drop(inventories);
523    }
524
525    #[test]
526    fn commit_trade_with_stackable_item_test() {
527        use common::{assets::AssetExt, comp::item::ItemDef};
528        use std::sync::Arc;
529
530        let (mockworld, player, merchant) = create_mock_trading_world(1, 20);
531
532        prepare_merchant_inventory(&mockworld, merchant);
533
534        let invmsg = "inventories.get_mut().is_none() should have returned already";
535        let capmsg = "There should be enough space here";
536        let mut inventories = mockworld.write_component::<Inventory>();
537
538        let mut playerinv = inventories.get_mut(player).expect(invmsg);
539        playerinv
540            .push(common::comp::Item::new_from_asset_expect(
541                "common.items.consumable.potion_minor",
542            ))
543            .expect(capmsg);
544
545        let potion_asset = "common.items.consumable.potion_minor";
546
547        let potion = common::comp::Item::new_from_asset_expect(potion_asset);
548        let potion_def = Arc::<ItemDef>::load_expect_cloned(potion_asset);
549
550        let merchantinv = inventories.get_mut(merchant).expect(invmsg);
551
552        let potioninvid = merchantinv
553            .get_slot_of_item(&potion)
554            .expect("expected get_slot_of_item to return");
555
556        let playerid = mockworld
557            .uid_from_entity(player)
558            .expect("mockworld.uid_from_entity(player) should have returned");
559        let merchantid = mockworld
560            .uid_from_entity(merchant)
561            .expect("mockworld.uid_from_entity(player) should have returned");
562
563        let playeroffers: HashMap<InvSlotId, u32> = HashMap::new();
564        let mut merchantoffers: HashMap<InvSlotId, u32> = HashMap::new();
565        merchantoffers.insert(potioninvid, 1);
566
567        let trade = PendingTrade {
568            parties: [playerid, merchantid],
569            accept_flags: [true, true],
570            offers: [playeroffers, merchantoffers],
571            phase: common::trade::TradePhase::Review,
572        };
573
574        drop(inventories);
575
576        let traderes = commit_trade(&mockworld, &trade);
577        assert_eq!(traderes, TradeResult::Completed);
578
579        let mut inventories = mockworld.write_component::<Inventory>();
580        let playerinv = inventories.get_mut(player).expect(invmsg);
581        let potioncount = playerinv.item_count(&potion_def);
582        assert_eq!(potioncount, 2);
583    }
584
585    #[test]
586    fn commit_trade_with_full_inventory_test() {
587        let (mockworld, player, merchant) = create_mock_trading_world(1, 20);
588
589        prepare_merchant_inventory(&mockworld, merchant);
590
591        let invmsg = "inventories.get_mut().is_none() should have returned already";
592        let capmsg = "There should be enough space here";
593        let mut inventories = mockworld.write_component::<Inventory>();
594
595        let mut playerinv = inventories.get_mut(player).expect(invmsg);
596        playerinv
597            .push(common::comp::Item::new_from_asset_expect(
598                "common.items.consumable.potion_minor",
599            ))
600            .expect(capmsg);
601
602        let fish = common::comp::Item::new_from_asset_expect("common.items.food.meat.fish_cooked");
603        let merchantinv = inventories.get_mut(merchant).expect(invmsg);
604
605        let fishinvid = merchantinv
606            .get_slot_of_item(&fish)
607            .expect("expected get_slot_of_item to return");
608
609        let playerid = mockworld
610            .uid_from_entity(player)
611            .expect("mockworld.uid_from_entity(player) should have returned");
612        let merchantid = mockworld
613            .uid_from_entity(merchant)
614            .expect("mockworld.uid_from_entity(player) should have returned");
615
616        let playeroffers: HashMap<InvSlotId, u32> = HashMap::new();
617        let mut merchantoffers: HashMap<InvSlotId, u32> = HashMap::new();
618        merchantoffers.insert(fishinvid, 1);
619        let trade = PendingTrade {
620            parties: [playerid, merchantid],
621            accept_flags: [true, true],
622            offers: [playeroffers, merchantoffers],
623            phase: common::trade::TradePhase::Review,
624        };
625
626        drop(inventories);
627
628        let traderes = commit_trade(&mockworld, &trade);
629        assert_eq!(traderes, TradeResult::NotEnoughSpace);
630    }
631
632    #[test]
633    fn commit_trade_with_empty_inventory_test() {
634        let (mockworld, player, merchant) = create_mock_trading_world(20, 20);
635
636        prepare_merchant_inventory(&mockworld, merchant);
637
638        let invmsg = "inventories.get_mut().is_none() should have returned already";
639        let mut inventories = mockworld.write_component::<Inventory>();
640
641        let fish = common::comp::Item::new_from_asset_expect("common.items.food.meat.fish_cooked");
642        let merchantinv = inventories.get_mut(merchant).expect(invmsg);
643
644        let fishinvid = merchantinv
645            .get_slot_of_item(&fish)
646            .expect("expected get_slot_of_item to return");
647
648        let playerid = mockworld
649            .uid_from_entity(player)
650            .expect("mockworld.uid_from_entity(player) should have returned");
651        let merchantid = mockworld
652            .uid_from_entity(merchant)
653            .expect("mockworld.uid_from_entity(player) should have returned");
654
655        let playeroffers: HashMap<InvSlotId, u32> = HashMap::new();
656        let mut merchantoffers: HashMap<InvSlotId, u32> = HashMap::new();
657        merchantoffers.insert(fishinvid, 1);
658        let trade = PendingTrade {
659            parties: [playerid, merchantid],
660            accept_flags: [true, true],
661            offers: [playeroffers, merchantoffers],
662            phase: common::trade::TradePhase::Review,
663        };
664
665        drop(inventories);
666
667        let traderes = commit_trade(&mockworld, &trade);
668        assert_eq!(traderes, TradeResult::Completed);
669    }
670
671    #[test]
672    fn commit_trade_with_both_full_inventories_test() {
673        let (mockworld, player, merchant) = create_mock_trading_world(2, 2);
674
675        prepare_merchant_inventory(&mockworld, merchant);
676
677        let invmsg = "inventories.get_mut().is_none() should have returned already";
678        let capmsg = "There should be enough space here";
679        let mut inventories = mockworld.write_component::<Inventory>();
680
681        let fish = common::comp::Item::new_from_asset_expect("common.items.food.meat.fish_cooked");
682        let merchantinv = inventories.get_mut(merchant).expect(invmsg);
683        let fishinvid = merchantinv
684            .get_slot_of_item(&fish)
685            .expect("expected get_slot_of_item to return");
686
687        let potion =
688            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
689        let mut playerinv = inventories.get_mut(player).expect(invmsg);
690        playerinv
691            .push(common::comp::Item::new_from_asset_expect(
692                "common.items.consumable.potion_minor",
693            ))
694            .expect(capmsg);
695        let potioninvid = playerinv
696            .get_slot_of_item(&potion)
697            .expect("expected get_slot_of_item to return");
698
699        let playerid = mockworld
700            .uid_from_entity(player)
701            .expect("mockworld.uid_from_entity(player) should have returned");
702        let merchantid = mockworld
703            .uid_from_entity(merchant)
704            .expect("mockworld.uid_from_entity(player) should have returned");
705
706        let mut playeroffers: HashMap<InvSlotId, u32> = HashMap::new();
707        playeroffers.insert(potioninvid, 1);
708
709        let mut merchantoffers: HashMap<InvSlotId, u32> = HashMap::new();
710        merchantoffers.insert(fishinvid, 10);
711        let trade = PendingTrade {
712            parties: [playerid, merchantid],
713            accept_flags: [true, true],
714            offers: [playeroffers, merchantoffers],
715            phase: common::trade::TradePhase::Review,
716        };
717
718        drop(inventories);
719
720        let traderes = commit_trade(&mockworld, &trade);
721        assert_eq!(traderes, TradeResult::Completed);
722    }
723
724    #[test]
725    fn commit_trade_with_overflow() {
726        let (mockworld, player, merchant) = create_mock_trading_world(2, 20);
727
728        prepare_merchant_inventory(&mockworld, merchant);
729
730        let invmsg = "inventories.get_mut().is_none() should have returned already";
731        let capmsg = "There should be enough space here";
732        let mut inventories = mockworld.write_component::<Inventory>();
733
734        let mut playerinv = inventories.get_mut(player).expect(invmsg);
735        let mut potion =
736            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
737        potion
738            .set_amount(potion.max_amount() - 2)
739            .expect("Should be below the max amount");
740        playerinv.push(potion).expect(capmsg);
741
742        let potion =
743            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
744        let merchantinv = inventories.get_mut(merchant).expect(invmsg);
745
746        let potioninvid = merchantinv
747            .get_slot_of_item(&potion)
748            .expect("expected get_slot_of_item to return");
749
750        let playerid = mockworld
751            .uid_from_entity(player)
752            .expect("mockworld.uid_from_entity(player) should have returned");
753        let merchantid = mockworld
754            .uid_from_entity(merchant)
755            .expect("mockworld.uid_from_entity(player) should have returned");
756
757        let playeroffers: HashMap<InvSlotId, u32> = HashMap::new();
758        let mut merchantoffers: HashMap<InvSlotId, u32> = HashMap::new();
759        merchantoffers.insert(potioninvid, 10);
760        let trade = PendingTrade {
761            parties: [playerid, merchantid],
762            accept_flags: [true, true],
763            offers: [playeroffers, merchantoffers],
764            phase: common::trade::TradePhase::Review,
765        };
766
767        drop(inventories);
768
769        let traderes = commit_trade(&mockworld, &trade);
770        assert_eq!(traderes, TradeResult::Completed);
771
772        let mut inventories = mockworld.write_component::<Inventory>();
773        let mut playerinv = inventories.get_mut(player).expect(invmsg);
774
775        let slot1 = playerinv
776            .get_slot_of_item(&potion)
777            .expect("There should be a slot here");
778        let item1 = playerinv
779            .remove(slot1)
780            .expect("The slot should not be empty");
781
782        let slot2 = playerinv
783            .get_slot_of_item(&potion)
784            .expect("There should be a slot here");
785        let item2 = playerinv
786            .remove(slot2)
787            .expect("The slot should not be empty");
788
789        assert_eq!(item1.amount(), potion.max_amount());
790        assert_eq!(item2.amount(), 8);
791    }
792
793    #[test]
794    fn commit_trade_with_inventory_overflow_failure() {
795        let (mockworld, player, merchant) = create_mock_trading_world(2, 20);
796
797        prepare_merchant_inventory(&mockworld, merchant);
798
799        let invmsg = "inventories.get_mut().is_none() should have returned already";
800        let capmsg = "There should be enough space here";
801        let mut inventories = mockworld.write_component::<Inventory>();
802
803        let mut playerinv = inventories.get_mut(player).expect(invmsg);
804        let mut potion =
805            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
806        potion
807            .set_amount(potion.max_amount() - 2)
808            .expect("Should be below the max amount");
809        playerinv.push(potion).expect(capmsg);
810        let mut potion =
811            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
812        potion
813            .set_amount(potion.max_amount() - 2)
814            .expect("Should be below the max amount");
815        playerinv.push(potion).expect(capmsg);
816
817        let potion =
818            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
819        let merchantinv = inventories.get_mut(merchant).expect(invmsg);
820
821        let potioninvid = merchantinv
822            .get_slot_of_item(&potion)
823            .expect("expected get_slot_of_item to return");
824
825        let playerid = mockworld
826            .uid_from_entity(player)
827            .expect("mockworld.uid_from_entity(player) should have returned");
828        let merchantid = mockworld
829            .uid_from_entity(merchant)
830            .expect("mockworld.uid_from_entity(player) should have returned");
831
832        let playeroffers: HashMap<InvSlotId, u32> = HashMap::new();
833        let mut merchantoffers: HashMap<InvSlotId, u32> = HashMap::new();
834        merchantoffers.insert(potioninvid, 5);
835        let trade = PendingTrade {
836            parties: [playerid, merchantid],
837            accept_flags: [true, true],
838            offers: [playeroffers, merchantoffers],
839            phase: common::trade::TradePhase::Review,
840        };
841
842        drop(inventories);
843
844        let traderes = commit_trade(&mockworld, &trade);
845        assert_eq!(traderes, TradeResult::NotEnoughSpace);
846    }
847
848    #[test]
849    fn commit_trade_with_inventory_overflow_success() {
850        let (mockworld, player, merchant) = create_mock_trading_world(2, 20);
851
852        prepare_merchant_inventory(&mockworld, merchant);
853
854        let invmsg = "inventories.get_mut().is_none() should have returned already";
855        let capmsg = "There should be enough space here";
856        let mut inventories = mockworld.write_component::<Inventory>();
857
858        let mut playerinv = inventories.get_mut(player).expect(invmsg);
859        let mut potion =
860            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
861        potion
862            .set_amount(potion.max_amount() - 2)
863            .expect("Should be below the max amount");
864        playerinv.push(potion).expect(capmsg);
865        let mut potion =
866            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
867        potion
868            .set_amount(potion.max_amount() - 2)
869            .expect("Should be below the max amount");
870        playerinv.push(potion).expect(capmsg);
871
872        let potion =
873            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
874        let merchantinv = inventories.get_mut(merchant).expect(invmsg);
875
876        let potioninvid = merchantinv
877            .get_slot_of_item(&potion)
878            .expect("expected get_slot_of_item to return");
879
880        let playerid = mockworld
881            .uid_from_entity(player)
882            .expect("mockworld.uid_from_entity(player) should have returned");
883        let merchantid = mockworld
884            .uid_from_entity(merchant)
885            .expect("mockworld.uid_from_entity(player) should have returned");
886
887        let playeroffers: HashMap<InvSlotId, u32> = HashMap::new();
888        let mut merchantoffers: HashMap<InvSlotId, u32> = HashMap::new();
889        merchantoffers.insert(potioninvid, 4);
890        let trade = PendingTrade {
891            parties: [playerid, merchantid],
892            accept_flags: [true, true],
893            offers: [playeroffers, merchantoffers],
894            phase: common::trade::TradePhase::Review,
895        };
896
897        drop(inventories);
898
899        let traderes = commit_trade(&mockworld, &trade);
900        assert_eq!(traderes, TradeResult::Completed);
901    }
902}