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        && let AgentEvent::UpdatePendingTrade(boxval) = event
44    {
45        // Prefer using this Agent's price data, but use the counterparty's price
46        // data if we don't have price data
47        let prices = site_id
48            .and_then(|site_id| index.get_site_prices(site_id))
49            .unwrap_or(boxval.2);
50        // Box<(tid, pend, _, inventories)>) = event {
51        agent
52            .inbox
53            .push_back(AgentEvent::UpdatePendingTrade(Box::new((
54                boxval.0, boxval.1, prices, boxval.3,
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                && 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    for who in [0, 1].iter().cloned() {
415        if let Err(leftovers) = inventories
416            .get_mut(entities[1 - who])
417            .expect(invmsg)
418            .push_all(items[who].drain(..))
419        {
420            // This should only happen if the arithmetic above for delta_slots says there's
421            // enough space and there isn't (i.e. underapproximates)
422            panic!(
423                "Not enough space for all the items, leftovers are {:?}",
424                leftovers
425            );
426        }
427    }
428
429    TradeResult::Completed
430}
431
432#[cfg(test)]
433mod tests {
434    use hashbrown::HashMap;
435
436    use super::*;
437    use common::{comp::slot::InvSlotId, uid::IdMaps};
438
439    use specs::{Builder, World};
440
441    // Creates a specs World containing two Entities which have Inventory
442    // components. Left over inventory size is determined by input. Mapping to the
443    // returned Entities. Any input over the maximum default inventory size will
444    // result in maximum left over space.
445    fn create_mock_trading_world(
446        player_inv_size: usize,
447        merchant_inv_size: usize,
448    ) -> (World, EcsEntity, EcsEntity) {
449        let mut mockworld = World::new();
450        mockworld.insert(IdMaps::new());
451        mockworld.insert(MaterialStatManifest::load().cloned());
452        mockworld.insert(AbilityMap::load().cloned());
453        mockworld.register::<Inventory>();
454        mockworld.register::<Uid>();
455
456        let player: EcsEntity = mockworld
457            .create_entity()
458            .with(Inventory::with_empty())
459            .build();
460
461        let merchant: EcsEntity = mockworld
462            .create_entity()
463            .with(Inventory::with_empty())
464            .build();
465
466        {
467            let mut uids = mockworld.write_component::<Uid>();
468            let mut id_maps = mockworld.write_resource::<IdMaps>();
469            uids.insert(player, id_maps.allocate(player))
470                .expect("inserting player uid failed");
471            uids.insert(merchant, id_maps.allocate(merchant))
472                .expect("inserting merchant uid failed");
473        }
474
475        let invmsg = "inventories.get_mut().is_none() should have returned already";
476        let capmsg = "There should be enough space here";
477        let mut inventories = mockworld.write_component::<Inventory>();
478        let mut playerinv = inventories.get_mut(player).expect(invmsg);
479        if player_inv_size < playerinv.capacity() {
480            for _ in player_inv_size..playerinv.capacity() {
481                playerinv
482                    .push(common::comp::Item::new_from_asset_expect(
483                        "common.items.npc_armor.pants.plate_red",
484                    ))
485                    .expect(capmsg);
486            }
487        }
488
489        let mut merchantinv = inventories.get_mut(merchant).expect(invmsg);
490        if merchant_inv_size < merchantinv.capacity() {
491            for _ in merchant_inv_size..merchantinv.capacity() {
492                merchantinv
493                    .push(common::comp::Item::new_from_asset_expect(
494                        "common.items.armor.cloth_purple.foot",
495                    ))
496                    .expect(capmsg);
497            }
498        }
499        drop(inventories);
500
501        (mockworld, player, merchant)
502    }
503
504    fn prepare_merchant_inventory(mockworld: &World, merchant: EcsEntity) {
505        let mut inventories = mockworld.write_component::<Inventory>();
506        let invmsg = "inventories.get_mut().is_none() should have returned already";
507        let capmsg = "There should be enough space here";
508        let mut merchantinv = inventories.get_mut(merchant).expect(invmsg);
509        for _ in 0..10 {
510            merchantinv
511                .push(common::comp::Item::new_from_asset_expect(
512                    "common.items.consumable.potion_minor",
513                ))
514                .expect(capmsg);
515            merchantinv
516                .push(common::comp::Item::new_from_asset_expect(
517                    "common.items.food.meat.fish_cooked",
518                ))
519                .expect(capmsg);
520        }
521        drop(inventories);
522    }
523
524    #[test]
525    fn commit_trade_with_stackable_item_test() {
526        use common::{assets::AssetExt, comp::item::ItemDef};
527        use std::sync::Arc;
528
529        let (mockworld, player, merchant) = create_mock_trading_world(1, 20);
530
531        prepare_merchant_inventory(&mockworld, merchant);
532
533        let invmsg = "inventories.get_mut().is_none() should have returned already";
534        let capmsg = "There should be enough space here";
535        let mut inventories = mockworld.write_component::<Inventory>();
536
537        let mut playerinv = inventories.get_mut(player).expect(invmsg);
538        playerinv
539            .push(common::comp::Item::new_from_asset_expect(
540                "common.items.consumable.potion_minor",
541            ))
542            .expect(capmsg);
543
544        let potion_asset = "common.items.consumable.potion_minor";
545
546        let potion = common::comp::Item::new_from_asset_expect(potion_asset);
547        let potion_def = Arc::<ItemDef>::load_expect_cloned(potion_asset);
548
549        let merchantinv = inventories.get_mut(merchant).expect(invmsg);
550
551        let potioninvid = merchantinv
552            .get_slot_of_item(&potion)
553            .expect("expected get_slot_of_item to return");
554
555        let playerid = mockworld
556            .uid_from_entity(player)
557            .expect("mockworld.uid_from_entity(player) should have returned");
558        let merchantid = mockworld
559            .uid_from_entity(merchant)
560            .expect("mockworld.uid_from_entity(player) should have returned");
561
562        let playeroffers: HashMap<InvSlotId, u32> = HashMap::new();
563        let mut merchantoffers: HashMap<InvSlotId, u32> = HashMap::new();
564        merchantoffers.insert(potioninvid, 1);
565
566        let trade = PendingTrade {
567            parties: [playerid, merchantid],
568            accept_flags: [true, true],
569            offers: [playeroffers, merchantoffers],
570            phase: common::trade::TradePhase::Review,
571        };
572
573        drop(inventories);
574
575        let traderes = commit_trade(&mockworld, &trade);
576        assert_eq!(traderes, TradeResult::Completed);
577
578        let mut inventories = mockworld.write_component::<Inventory>();
579        let playerinv = inventories.get_mut(player).expect(invmsg);
580        let potioncount = playerinv.item_count(&potion_def);
581        assert_eq!(potioncount, 2);
582    }
583
584    #[test]
585    fn commit_trade_with_full_inventory_test() {
586        let (mockworld, player, merchant) = create_mock_trading_world(1, 20);
587
588        prepare_merchant_inventory(&mockworld, merchant);
589
590        let invmsg = "inventories.get_mut().is_none() should have returned already";
591        let capmsg = "There should be enough space here";
592        let mut inventories = mockworld.write_component::<Inventory>();
593
594        let mut playerinv = inventories.get_mut(player).expect(invmsg);
595        playerinv
596            .push(common::comp::Item::new_from_asset_expect(
597                "common.items.consumable.potion_minor",
598            ))
599            .expect(capmsg);
600
601        let fish = common::comp::Item::new_from_asset_expect("common.items.food.meat.fish_cooked");
602        let merchantinv = inventories.get_mut(merchant).expect(invmsg);
603
604        let fishinvid = merchantinv
605            .get_slot_of_item(&fish)
606            .expect("expected get_slot_of_item to return");
607
608        let playerid = mockworld
609            .uid_from_entity(player)
610            .expect("mockworld.uid_from_entity(player) should have returned");
611        let merchantid = mockworld
612            .uid_from_entity(merchant)
613            .expect("mockworld.uid_from_entity(player) should have returned");
614
615        let playeroffers: HashMap<InvSlotId, u32> = HashMap::new();
616        let mut merchantoffers: HashMap<InvSlotId, u32> = HashMap::new();
617        merchantoffers.insert(fishinvid, 1);
618        let trade = PendingTrade {
619            parties: [playerid, merchantid],
620            accept_flags: [true, true],
621            offers: [playeroffers, merchantoffers],
622            phase: common::trade::TradePhase::Review,
623        };
624
625        drop(inventories);
626
627        let traderes = commit_trade(&mockworld, &trade);
628        assert_eq!(traderes, TradeResult::NotEnoughSpace);
629    }
630
631    #[test]
632    fn commit_trade_with_empty_inventory_test() {
633        let (mockworld, player, merchant) = create_mock_trading_world(20, 20);
634
635        prepare_merchant_inventory(&mockworld, merchant);
636
637        let invmsg = "inventories.get_mut().is_none() should have returned already";
638        let mut inventories = mockworld.write_component::<Inventory>();
639
640        let fish = common::comp::Item::new_from_asset_expect("common.items.food.meat.fish_cooked");
641        let merchantinv = inventories.get_mut(merchant).expect(invmsg);
642
643        let fishinvid = merchantinv
644            .get_slot_of_item(&fish)
645            .expect("expected get_slot_of_item to return");
646
647        let playerid = mockworld
648            .uid_from_entity(player)
649            .expect("mockworld.uid_from_entity(player) should have returned");
650        let merchantid = mockworld
651            .uid_from_entity(merchant)
652            .expect("mockworld.uid_from_entity(player) should have returned");
653
654        let playeroffers: HashMap<InvSlotId, u32> = HashMap::new();
655        let mut merchantoffers: HashMap<InvSlotId, u32> = HashMap::new();
656        merchantoffers.insert(fishinvid, 1);
657        let trade = PendingTrade {
658            parties: [playerid, merchantid],
659            accept_flags: [true, true],
660            offers: [playeroffers, merchantoffers],
661            phase: common::trade::TradePhase::Review,
662        };
663
664        drop(inventories);
665
666        let traderes = commit_trade(&mockworld, &trade);
667        assert_eq!(traderes, TradeResult::Completed);
668    }
669
670    #[test]
671    fn commit_trade_with_both_full_inventories_test() {
672        let (mockworld, player, merchant) = create_mock_trading_world(2, 2);
673
674        prepare_merchant_inventory(&mockworld, merchant);
675
676        let invmsg = "inventories.get_mut().is_none() should have returned already";
677        let capmsg = "There should be enough space here";
678        let mut inventories = mockworld.write_component::<Inventory>();
679
680        let fish = common::comp::Item::new_from_asset_expect("common.items.food.meat.fish_cooked");
681        let merchantinv = inventories.get_mut(merchant).expect(invmsg);
682        let fishinvid = merchantinv
683            .get_slot_of_item(&fish)
684            .expect("expected get_slot_of_item to return");
685
686        let potion =
687            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
688        let mut playerinv = inventories.get_mut(player).expect(invmsg);
689        playerinv
690            .push(common::comp::Item::new_from_asset_expect(
691                "common.items.consumable.potion_minor",
692            ))
693            .expect(capmsg);
694        let potioninvid = playerinv
695            .get_slot_of_item(&potion)
696            .expect("expected get_slot_of_item to return");
697
698        let playerid = mockworld
699            .uid_from_entity(player)
700            .expect("mockworld.uid_from_entity(player) should have returned");
701        let merchantid = mockworld
702            .uid_from_entity(merchant)
703            .expect("mockworld.uid_from_entity(player) should have returned");
704
705        let mut playeroffers: HashMap<InvSlotId, u32> = HashMap::new();
706        playeroffers.insert(potioninvid, 1);
707
708        let mut merchantoffers: HashMap<InvSlotId, u32> = HashMap::new();
709        merchantoffers.insert(fishinvid, 10);
710        let trade = PendingTrade {
711            parties: [playerid, merchantid],
712            accept_flags: [true, true],
713            offers: [playeroffers, merchantoffers],
714            phase: common::trade::TradePhase::Review,
715        };
716
717        drop(inventories);
718
719        let traderes = commit_trade(&mockworld, &trade);
720        assert_eq!(traderes, TradeResult::Completed);
721    }
722
723    #[test]
724    fn commit_trade_with_overflow() {
725        let (mockworld, player, merchant) = create_mock_trading_world(2, 20);
726
727        prepare_merchant_inventory(&mockworld, merchant);
728
729        let invmsg = "inventories.get_mut().is_none() should have returned already";
730        let capmsg = "There should be enough space here";
731        let mut inventories = mockworld.write_component::<Inventory>();
732
733        let mut playerinv = inventories.get_mut(player).expect(invmsg);
734        let mut potion =
735            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
736        potion
737            .set_amount(potion.max_amount() - 2)
738            .expect("Should be below the max amount");
739        playerinv.push(potion).expect(capmsg);
740
741        let potion =
742            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
743        let merchantinv = inventories.get_mut(merchant).expect(invmsg);
744
745        let potioninvid = merchantinv
746            .get_slot_of_item(&potion)
747            .expect("expected get_slot_of_item to return");
748
749        let playerid = mockworld
750            .uid_from_entity(player)
751            .expect("mockworld.uid_from_entity(player) should have returned");
752        let merchantid = mockworld
753            .uid_from_entity(merchant)
754            .expect("mockworld.uid_from_entity(player) should have returned");
755
756        let playeroffers: HashMap<InvSlotId, u32> = HashMap::new();
757        let mut merchantoffers: HashMap<InvSlotId, u32> = HashMap::new();
758        merchantoffers.insert(potioninvid, 10);
759        let trade = PendingTrade {
760            parties: [playerid, merchantid],
761            accept_flags: [true, true],
762            offers: [playeroffers, merchantoffers],
763            phase: common::trade::TradePhase::Review,
764        };
765
766        drop(inventories);
767
768        let traderes = commit_trade(&mockworld, &trade);
769        assert_eq!(traderes, TradeResult::Completed);
770
771        let mut inventories = mockworld.write_component::<Inventory>();
772        let mut playerinv = inventories.get_mut(player).expect(invmsg);
773
774        let slot1 = playerinv
775            .get_slot_of_item(&potion)
776            .expect("There should be a slot here");
777        let item1 = playerinv
778            .remove(slot1)
779            .expect("The slot should not be empty");
780
781        let slot2 = playerinv
782            .get_slot_of_item(&potion)
783            .expect("There should be a slot here");
784        let item2 = playerinv
785            .remove(slot2)
786            .expect("The slot should not be empty");
787
788        assert_eq!(item1.amount(), potion.max_amount());
789        assert_eq!(item2.amount(), 8);
790    }
791
792    #[test]
793    fn commit_trade_with_inventory_overflow_failure() {
794        let (mockworld, player, merchant) = create_mock_trading_world(2, 20);
795
796        prepare_merchant_inventory(&mockworld, merchant);
797
798        let invmsg = "inventories.get_mut().is_none() should have returned already";
799        let capmsg = "There should be enough space here";
800        let mut inventories = mockworld.write_component::<Inventory>();
801
802        let mut playerinv = inventories.get_mut(player).expect(invmsg);
803        let mut potion =
804            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
805        potion
806            .set_amount(potion.max_amount() - 2)
807            .expect("Should be below the max amount");
808        playerinv.push(potion).expect(capmsg);
809        let mut potion =
810            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
811        potion
812            .set_amount(potion.max_amount() - 2)
813            .expect("Should be below the max amount");
814        playerinv.push(potion).expect(capmsg);
815
816        let potion =
817            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
818        let merchantinv = inventories.get_mut(merchant).expect(invmsg);
819
820        let potioninvid = merchantinv
821            .get_slot_of_item(&potion)
822            .expect("expected get_slot_of_item to return");
823
824        let playerid = mockworld
825            .uid_from_entity(player)
826            .expect("mockworld.uid_from_entity(player) should have returned");
827        let merchantid = mockworld
828            .uid_from_entity(merchant)
829            .expect("mockworld.uid_from_entity(player) should have returned");
830
831        let playeroffers: HashMap<InvSlotId, u32> = HashMap::new();
832        let mut merchantoffers: HashMap<InvSlotId, u32> = HashMap::new();
833        merchantoffers.insert(potioninvid, 5);
834        let trade = PendingTrade {
835            parties: [playerid, merchantid],
836            accept_flags: [true, true],
837            offers: [playeroffers, merchantoffers],
838            phase: common::trade::TradePhase::Review,
839        };
840
841        drop(inventories);
842
843        let traderes = commit_trade(&mockworld, &trade);
844        assert_eq!(traderes, TradeResult::NotEnoughSpace);
845    }
846
847    #[test]
848    fn commit_trade_with_inventory_overflow_success() {
849        let (mockworld, player, merchant) = create_mock_trading_world(2, 20);
850
851        prepare_merchant_inventory(&mockworld, merchant);
852
853        let invmsg = "inventories.get_mut().is_none() should have returned already";
854        let capmsg = "There should be enough space here";
855        let mut inventories = mockworld.write_component::<Inventory>();
856
857        let mut playerinv = inventories.get_mut(player).expect(invmsg);
858        let mut potion =
859            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
860        potion
861            .set_amount(potion.max_amount() - 2)
862            .expect("Should be below the max amount");
863        playerinv.push(potion).expect(capmsg);
864        let mut potion =
865            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
866        potion
867            .set_amount(potion.max_amount() - 2)
868            .expect("Should be below the max amount");
869        playerinv.push(potion).expect(capmsg);
870
871        let potion =
872            common::comp::Item::new_from_asset_expect("common.items.consumable.potion_minor");
873        let merchantinv = inventories.get_mut(merchant).expect(invmsg);
874
875        let potioninvid = merchantinv
876            .get_slot_of_item(&potion)
877            .expect("expected get_slot_of_item to return");
878
879        let playerid = mockworld
880            .uid_from_entity(player)
881            .expect("mockworld.uid_from_entity(player) should have returned");
882        let merchantid = mockworld
883            .uid_from_entity(merchant)
884            .expect("mockworld.uid_from_entity(player) should have returned");
885
886        let playeroffers: HashMap<InvSlotId, u32> = HashMap::new();
887        let mut merchantoffers: HashMap<InvSlotId, u32> = HashMap::new();
888        merchantoffers.insert(potioninvid, 4);
889        let trade = PendingTrade {
890            parties: [playerid, merchantid],
891            accept_flags: [true, true],
892            offers: [playeroffers, merchantoffers],
893            phase: common::trade::TradePhase::Review,
894        };
895
896        drop(inventories);
897
898        let traderes = commit_trade(&mockworld, &trade);
899        assert_eq!(traderes, TradeResult::Completed);
900    }
901}