veloren_server/events/
trade.rs

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