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