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 let prices = site_id
48 .and_then(|site_id| index.get_site_prices(site_id))
49 .unwrap_or(boxval.2);
50 agent
52 .inbox
53 .push_back(AgentEvent::UpdatePendingTrade(Box::new((
54 boxval.0, boxval.1, prices, boxval.3,
55 ))));
56 }
57}
58
59pub(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 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 for i in 0..2 {
136 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 #[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
189pub(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
220fn 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 let mut delta_slots: [i64; 2] = [0, 0];
241
242 #[derive(Default)]
245 struct ItemQuantities {
246 full_stacks: u64,
247 quantity_sold: u128,
248 freed_capacity: u128,
249 unusable_capacity: u128,
250 }
251
252 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 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 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 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; delta_slots[1 - who] += 1; }
314 },
315 Ordering::Greater => {
316 if item.is_stackable() {
317 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 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 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 delta_slots[who] -= trade_quantities[who].full_stacks as i64;
357
358 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 if other_party_capacity < trade_quantities[who].quantity_sold {
380 let surplus = trade_quantities[who].quantity_sold - other_party_capacity;
381 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 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 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 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}