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 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 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 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}