veloren_server/events/inventory_manip.rs
1use hashbrown::HashSet;
2use rand::seq::IteratorRandom;
3use specs::{
4 DispatcherBuilder, Entities, Entity as EcsEntity, Read, ReadExpect, ReadStorage, SystemData,
5 Write, WriteStorage, join::Join, shred,
6};
7use tracing::{debug, error, warn};
8use vek::{Rgb, Vec3};
9
10use common::{
11 comp::{
12 self, InventoryUpdate, LootOwner, PickupItem,
13 group::members,
14 item::{self, MaterialStatManifest, flatten_counted_items, tool::AbilityMap},
15 loot_owner::LootOwnerKind,
16 slot::{self, Slot},
17 },
18 consts::MAX_PICKUP_RANGE,
19 event::{
20 BuffEvent, CreateItemDropEvent, CreateObjectEvent, DeleteEvent, EmitExt, HealthChangeEvent,
21 InventoryManipEvent, PoiseChangeEvent, TamePetEvent,
22 },
23 event_emitters,
24 mounting::VolumePos,
25 recipe::{self, RecipeBookManifest, default_component_recipe_book, default_repair_recipe_book},
26 resources::{ProgramTime, Time},
27 terrain::{Block, SpriteKind, sprite},
28 trade::Trades,
29 uid::{IdMaps, Uid},
30 util::find_dist::{self, FindDist},
31 vol::ReadVol,
32};
33use comp::LightEmitter;
34
35use crate::client::Client;
36use common::comp::{Alignment, CollectFailedReason, Group, InventoryUpdateEvent, pet::is_tameable};
37use common_net::msg::ServerGeneral;
38
39use super::{ServerEvent, entity_manipulation::emit_effect_events, event_dispatch};
40
41pub(super) fn register_event_systems(builder: &mut DispatcherBuilder) {
42 event_dispatch::<InventoryManipEvent>(builder, &[]);
43}
44
45pub fn swap_lantern(
46 storage: &mut WriteStorage<LightEmitter>,
47 entity: EcsEntity,
48 (lantern_color, lantern_strength): (Rgb<f32>, f32),
49) {
50 if let Some(mut light) = storage.get_mut(entity) {
51 light.strength = lantern_strength;
52 light.col = lantern_color;
53 }
54}
55
56pub fn snuff_lantern(storage: &mut WriteStorage<LightEmitter>, entity: EcsEntity) {
57 storage.remove(entity);
58}
59
60event_emitters! {
61 struct Events[Emitters] {
62 tame_pet: TamePetEvent,
63 delete: DeleteEvent,
64 create_item_drop: CreateItemDropEvent,
65 create_object: CreateObjectEvent,
66 health_change: HealthChangeEvent,
67 poise_change: PoiseChangeEvent,
68 buff: BuffEvent,
69 }
70}
71#[derive(SystemData)]
72pub struct InventoryManipData<'a> {
73 entities: Entities<'a>,
74 events: Events<'a>,
75 block_change: Write<'a, common_state::BlockChange>,
76 trades: Write<'a, Trades>,
77 #[cfg(feature = "worldgen")]
78 rtsim: specs::WriteExpect<'a, crate::rtsim::RtSim>,
79 terrain: ReadExpect<'a, common::terrain::TerrainGrid>,
80 id_maps: Read<'a, IdMaps>,
81 time: Read<'a, Time>,
82 #[cfg(feature = "worldgen")]
83 world: ReadExpect<'a, std::sync::Arc<world::World>>,
84 #[cfg(feature = "worldgen")]
85 index: ReadExpect<'a, world::IndexOwned>,
86 program_time: ReadExpect<'a, ProgramTime>,
87 ability_map: ReadExpect<'a, AbilityMap>,
88 msm: ReadExpect<'a, MaterialStatManifest>,
89 rbm: ReadExpect<'a, RecipeBookManifest>,
90 inventories: WriteStorage<'a, comp::Inventory>,
91 items: WriteStorage<'a, comp::PickupItem>,
92 inventory_updates: WriteStorage<'a, comp::InventoryUpdate>,
93 light_emitters: WriteStorage<'a, comp::LightEmitter>,
94 positions: ReadStorage<'a, comp::Pos>,
95 scales: ReadStorage<'a, comp::Scale>,
96 colliders: ReadStorage<'a, comp::Collider>,
97 character_states: ReadStorage<'a, comp::CharacterState>,
98 healths: ReadStorage<'a, comp::Health>,
99 uids: ReadStorage<'a, Uid>,
100 loot_owners: ReadStorage<'a, comp::LootOwner>,
101 alignments: ReadStorage<'a, comp::Alignment>,
102 bodies: ReadStorage<'a, comp::Body>,
103 players: ReadStorage<'a, comp::Player>,
104 groups: ReadStorage<'a, comp::Group>,
105 stats: ReadStorage<'a, comp::Stats>,
106 clients: ReadStorage<'a, Client>,
107 orientations: ReadStorage<'a, comp::Ori>,
108 agents: ReadStorage<'a, comp::Agent>,
109 pets: ReadStorage<'a, comp::Pet>,
110 masses: ReadStorage<'a, comp::Mass>,
111 #[cfg(feature = "worldgen")]
112 presences: ReadStorage<'a, comp::Presence>,
113 #[cfg(feature = "worldgen")]
114 rtsim_entities: ReadStorage<'a, common::rtsim::RtSimEntity>,
115}
116
117impl ServerEvent for InventoryManipEvent {
118 type SystemData<'a> = InventoryManipData<'a>;
119
120 fn handle(events: impl ExactSizeIterator<Item = Self>, mut data: Self::SystemData<'_>) {
121 let mut emitters = data.events.get_emitters();
122 let get_cylinder = |entity| {
123 data.positions.get(entity).map(|p| {
124 find_dist::Cylinder::from_components(
125 p.0,
126 data.scales.get(entity).copied(),
127 data.colliders.get(entity),
128 data.character_states.get(entity),
129 )
130 })
131 };
132 let mut rng = rand::thread_rng();
133
134 let mut dropped_items = Vec::new();
135
136 for InventoryManipEvent(entity, manip) in events {
137 let uid = if let Some(uid) = data.uids.get(entity) {
138 uid
139 } else {
140 warn!(
141 "Couldn't get uid for entity {:?} at start of handle_inventory",
142 entity
143 );
144 continue;
145 };
146 if data.trades.in_immutable_trade(uid) {
147 // manipulating the inventory can mutate the trade
148 continue;
149 }
150
151 if comp::is_downed_or_dead(data.healths.get(entity), data.character_states.get(entity))
152 {
153 // Can't manipulate the inventory while downed or dead.
154 continue;
155 }
156
157 let mut inventory = if let Some(inventory) = data.inventories.get_mut(entity) {
158 inventory
159 } else {
160 error!(
161 ?entity,
162 "Can't manipulate inventory, entity doesn't have one"
163 );
164 continue;
165 };
166 match manip {
167 comp::InventoryManip::Pickup(pickup_uid) => {
168 let item_entity = if let Some(item_entity) = data.id_maps.uid_entity(pickup_uid)
169 {
170 item_entity
171 } else {
172 // Item entity could not be found - most likely because the entity
173 // attempted to pick up the same item very quickly before its deletion
174 // of the world from the first pickup
175 // attempt was processed.
176 debug!("Failed to get entity for item Uid: {}", pickup_uid);
177 continue;
178 };
179 let entity_cylinder = get_cylinder(entity);
180
181 // FIXME: Raycast so we can't pick up items through walls.
182 if !within_pickup_range(entity_cylinder, || get_cylinder(item_entity)) {
183 debug!(
184 ?entity_cylinder,
185 "Failed to pick up item as not within range, Uid: {}", pickup_uid
186 );
187 continue;
188 }
189
190 // If there's a loot owner for the item being picked up, then
191 // determine whether the pickup should be rejected.
192 let ownership_check_passed =
193 data.loot_owners.get(item_entity).is_none_or(|loot_owner| {
194 let can_pickup = loot_owner.can_pickup(
195 *uid,
196 data.groups.get(entity),
197 data.alignments.get(entity),
198 data.stats
199 .get(entity)
200 .map(|stats| &stats.original_body)
201 .or_else(|| data.bodies.get(entity)),
202 data.players.get(entity),
203 );
204 if !can_pickup {
205 let event = comp::InventoryUpdate::new(
206 InventoryUpdateEvent::EntityCollectFailed {
207 entity: pickup_uid,
208 reason: CollectFailedReason::LootOwned {
209 owner: loot_owner.owner(),
210 expiry_secs: loot_owner
211 .time_until_expiration()
212 .as_secs(),
213 },
214 },
215 );
216 data.inventory_updates.insert(entity, event).unwrap();
217 }
218 can_pickup
219 });
220
221 if !ownership_check_passed {
222 continue;
223 }
224
225 // First, we remove the item, assuming picking it up will succeed (we do this to
226 // avoid cloning the item, as we should not call Item::clone and it
227 // may be removed!).
228 let item = if let Some(item) = data.items.remove(item_entity) {
229 item
230 } else {
231 // Item component could not be found - most likely because the entity
232 // attempted to pick up the same item very quickly before its deletion of
233 // the world from the first pickup attempt was
234 // processed.
235 debug!(
236 "Failed to delete item component for entity, Uid: {}",
237 pickup_uid
238 );
239 continue;
240 };
241
242 const ITEM_ENTITY_EXPECT_MESSAGE: &str = "We know item_entity still exist \
243 since we just successfully removed \
244 its PickupItem component.";
245
246 let (item, reinsert_item) = item.pick_up();
247
248 let mut item_msg = item.frontend_item(&data.ability_map, &data.msm);
249
250 // Next, we try to equip the picked up item
251 let event = match inventory.try_equip(item).or_else(|returned_item| {
252 // If we couldn't equip it (no empty slot for it or unequippable) then
253 // attempt to add the item to the entity's inventory
254 inventory.pickup_item(returned_item)
255 }) {
256 Err((returned_item, inserted)) => {
257 // If we had a `reinsert_item`, merge returned_item into it
258 let returned_item = if let Some(mut reinsert_item) = reinsert_item {
259 reinsert_item
260 .try_merge(PickupItem::new(returned_item, *data.program_time))
261 .expect(
262 "We know this item must be mergeable since it is a \
263 duplicate",
264 );
265 reinsert_item
266 } else {
267 PickupItem::new(returned_item, *data.program_time)
268 };
269
270 // Inventory was full, so we need to put back the item (note that we
271 // know there was no old item component for
272 // this entity).
273 data.items
274 .insert(item_entity, returned_item)
275 .expect(ITEM_ENTITY_EXPECT_MESSAGE);
276
277 // If the item was partially picked up, send a loot annoucement.
278 if let Some(inserted) = inserted {
279 // Update the frontend item to the new amount
280 item_msg
281 .set_amount(inserted.get())
282 .expect("Inserted must be > 0 and <= item.max_amount()");
283
284 if let Some(group_id) = data.groups.get(entity) {
285 announce_loot_to_group(
286 group_id,
287 entity,
288 item_msg.duplicate(&data.ability_map, &data.msm),
289 &data.clients,
290 &data.uids,
291 &data.groups,
292 &data.alignments,
293 &data.entities,
294 &data.ability_map,
295 &data.msm,
296 );
297 }
298 comp::InventoryUpdate::new(InventoryUpdateEvent::Collected(
299 item_msg,
300 ))
301 } else {
302 comp::InventoryUpdate::new(
303 InventoryUpdateEvent::EntityCollectFailed {
304 entity: pickup_uid,
305 reason: CollectFailedReason::InventoryFull,
306 },
307 )
308 }
309 },
310 Ok(_) => {
311 // We succeeded in picking up the item, so we may now delete its old
312 // entity entirely.
313 if let Some(reinsert_item) = reinsert_item {
314 data.items
315 .insert(item_entity, reinsert_item)
316 .expect(ITEM_ENTITY_EXPECT_MESSAGE);
317 } else {
318 emitters.emit(DeleteEvent(item_entity));
319 }
320
321 if let Some(group_id) = data.groups.get(entity) {
322 announce_loot_to_group(
323 group_id,
324 entity,
325 item_msg.duplicate(&data.ability_map, &data.msm),
326 &data.clients,
327 &data.uids,
328 &data.groups,
329 &data.alignments,
330 &data.entities,
331 &data.ability_map,
332 &data.msm,
333 );
334 }
335 comp::InventoryUpdate::new(InventoryUpdateEvent::Collected(item_msg))
336 },
337 };
338
339 data.inventory_updates
340 .insert(entity, event)
341 .expect("We know entity exists since we got its inventory.");
342 },
343 comp::InventoryManip::Collect {
344 sprite_pos,
345 required_item,
346 } => {
347 let block = data.terrain.get(sprite_pos).ok().copied();
348 let mut drop_items = Vec::new();
349 let inventory_update = data
350 .inventory_updates
351 .entry(entity)
352 .expect("We know entity exists since we got its inventory.")
353 .or_insert_with(InventoryUpdate::default);
354
355 if let Some(block) = block {
356 // If there are items to be reclaimed from the block, add it to the
357 // inventory
358 let sprite_cfg = data.terrain.sprite_cfg_at(sprite_pos);
359 if block.is_collectible(sprite_cfg)
360 && data.block_change.can_set_block(sprite_pos)
361 {
362 // Send event to rtsim if something was stolen.
363 #[cfg(feature = "worldgen")]
364 if block.is_owned()
365 && let Some(actor) = super::entity_manipulation::entity_as_actor(
366 entity,
367 &data.rtsim_entities,
368 &data.presences,
369 )
370 {
371 data.rtsim.hook_pickup_owned_sprite(
372 &data.world,
373 data.index.as_index_ref(),
374 block
375 .get_sprite()
376 .expect("If the block is owned, it is a sprite"),
377 sprite_pos,
378 actor,
379 );
380 }
381 // If an item was required to collect the sprite, consume it now
382 if let Some((inv_slot, true)) = required_item {
383 inventory.take(inv_slot, &data.ability_map, &data.msm);
384 }
385
386 if let Some(items) =
387 comp::Item::try_reclaim_from_block(block, sprite_cfg)
388 {
389 for item in
390 flatten_counted_items(&items, &data.ability_map, &data.msm)
391 {
392 let mut item_msg =
393 item.frontend_item(&data.ability_map, &data.msm);
394 let do_announce = match inventory.push(item) {
395 Ok(_) => true,
396 Err((item, inserted)) => {
397 drop_items.push(item);
398 if let Some(inserted) = inserted {
399 // Update the amount of the frontend item
400 item_msg.set_amount(inserted.get()).expect(
401 "Inserted must be > 0 and <= item.max_amount()",
402 );
403 true
404 } else {
405 false
406 }
407 },
408 };
409
410 if do_announce {
411 if let Some(group_id) = data.groups.get(entity) {
412 announce_loot_to_group(
413 group_id,
414 entity,
415 item_msg.duplicate(&data.ability_map, &data.msm),
416 &data.clients,
417 &data.uids,
418 &data.groups,
419 &data.alignments,
420 &data.entities,
421 &data.ability_map,
422 &data.msm,
423 );
424 }
425 inventory_update
426 .push(InventoryUpdateEvent::Collected(item_msg));
427 }
428 }
429 }
430
431 // We made sure earlier the block was not already modified this tick
432 match block.get_sprite() {
433 Some(SpriteKind::Lettuce) => {
434 let new_block =
435 block.with_attr(sprite::Collectable(false)).expect(
436 "Setting collectable will not fail as this scope \
437 requires block::is_collectible to return true.",
438 );
439 data.block_change.set(sprite_pos, new_block)
440 },
441 _ => data.block_change.set(sprite_pos, block.into_vacant()),
442 }
443
444 // If the block was a keyhole, remove nearby door blocks
445 // TODO: Abstract this code into a generalised way to do block updates?
446 if let Some(kind_to_destroy) = match block.get_sprite() {
447 Some(SpriteKind::Keyhole) => Some(SpriteKind::KeyDoor),
448 Some(SpriteKind::BoneKeyhole) => Some(SpriteKind::BoneKeyDoor),
449 Some(SpriteKind::HaniwaKeyhole) => Some(SpriteKind::HaniwaKeyDoor),
450 Some(SpriteKind::SahaginKeyhole) => {
451 Some(SpriteKind::SahaginKeyDoor)
452 },
453 Some(SpriteKind::GlassKeyhole) => Some(SpriteKind::GlassBarrier),
454 Some(SpriteKind::KeyholeBars) => Some(SpriteKind::DoorBars),
455 Some(SpriteKind::TerracottaKeyhole) => {
456 Some(SpriteKind::TerracottaKeyDoor)
457 },
458 Some(SpriteKind::VampireKeyhole) => {
459 Some(SpriteKind::VampireKeyDoor)
460 },
461 Some(SpriteKind::MyrmidonKeyhole | SpriteKind::MinotaurKeyhole) => {
462 Some(SpriteKind::MyrmidonKeyDoor)
463 },
464 _ => None,
465 } {
466 let dirs = [
467 Vec3::unit_x(),
468 -Vec3::unit_x(),
469 Vec3::unit_y(),
470 -Vec3::unit_y(),
471 Vec3::unit_z(),
472 -Vec3::unit_z(),
473 ];
474 let mut destroyed = HashSet::<Vec3<i32>>::default();
475 let mut pending = dirs
476 .into_iter()
477 .map(|dir| sprite_pos + dir)
478 .collect::<HashSet<_>>();
479 // TODO: Replace with `entry` eventually
480 while destroyed.len() < 450 {
481 if let Some(pos) = pending.iter().next().copied() {
482 pending.remove(&pos);
483
484 if !destroyed.contains(&pos)
485 && data
486 .terrain
487 .get(pos)
488 .ok()
489 .and_then(|b| b.get_sprite())
490 == Some(kind_to_destroy)
491 {
492 data.block_change.try_set(pos, Block::empty());
493 destroyed.insert(pos);
494 pending.extend(dirs.into_iter().map(|dir| pos + dir));
495 }
496 } else {
497 break;
498 }
499 }
500 }
501 } else {
502 debug!(
503 "Can't reclaim item from block at pos={}: block is not \
504 collectable or was already set this tick.",
505 sprite_pos
506 );
507 }
508 }
509 if !drop_items.is_empty() {
510 inventory_update.push(InventoryUpdateEvent::BlockCollectFailed {
511 pos: sprite_pos,
512 reason: CollectFailedReason::InventoryFull,
513 })
514 }
515
516 for item in drop_items {
517 emitters.emit(CreateItemDropEvent {
518 pos: comp::Pos(
519 Vec3::new(
520 sprite_pos.x as f32,
521 sprite_pos.y as f32,
522 sprite_pos.z as f32,
523 ) + Vec3::one().with_z(0.0) * 0.5,
524 ),
525 vel: comp::Vel(Vec3::zero()),
526 ori: data.orientations.get(entity).copied().unwrap_or_default(),
527 item: PickupItem::new(item, *data.program_time),
528 loot_owner: Some(LootOwner::new(LootOwnerKind::Player(*uid), false)),
529 });
530 }
531 },
532 comp::InventoryManip::Use(slot) => {
533 let mut maybe_effect = None;
534
535 let event = match slot {
536 Slot::Inventory(slot) => {
537 use item::ItemKind;
538
539 let (is_equippable, lantern_info) =
540 inventory.get(slot).map_or((false, None), |i| {
541 let kind = i.kind();
542 let is_equippable = kind.is_equippable();
543 let lantern_info = match &*kind {
544 ItemKind::Lantern(lantern) => {
545 Some((lantern.color(), lantern.strength()))
546 },
547 _ => None,
548 };
549 (is_equippable, lantern_info)
550 });
551 if is_equippable {
552 if let Some(lantern_info) = lantern_info {
553 swap_lantern(&mut data.light_emitters, entity, lantern_info);
554 }
555 if let Some(pos) = data.positions.get(entity) {
556 dropped_items.extend(
557 inventory
558 .equip(slot, *data.time, &data.ability_map, &data.msm)
559 .into_iter()
560 .map(|item| {
561 (
562 *pos,
563 data.orientations
564 .get(entity)
565 .copied()
566 .unwrap_or_default(),
567 PickupItem::new(item, *data.program_time),
568 *uid,
569 )
570 }),
571 );
572 }
573 Some(InventoryUpdateEvent::Used)
574 } else if let Some(item) =
575 inventory.take(slot, &data.ability_map, &data.msm)
576 {
577 match &*item.kind() {
578 ItemKind::Consumable { effects, .. } => {
579 maybe_effect = Some(effects.clone());
580 Some(InventoryUpdateEvent::Consumed((&item).into()))
581 },
582 ItemKind::Utility {
583 kind: item::Utility::Collar,
584 ..
585 } => {
586 const MAX_PETS: usize = 3;
587 let reinsert = if let Some(pos) = data.positions.get(entity)
588 {
589 if (&data.alignments, &data.agents, data.pets.mask())
590 .join()
591 .filter(|(alignment, _, _)| {
592 alignment == &&comp::Alignment::Owned(*uid)
593 })
594 .count()
595 >= MAX_PETS
596 {
597 true
598 } else if let Some(tameable_entity) = {
599 (
600 &data.entities,
601 &data.bodies,
602 &data.positions,
603 &data.alignments,
604 )
605 .join()
606 .filter(|(_, _, wild_pos, _)| {
607 wild_pos.0.distance_squared(pos.0)
608 < 5.0f32.powi(2)
609 })
610 .filter(|(_, body, _, alignment)| {
611 alignment == &&Alignment::Wild
612 && is_tameable(body)
613 })
614 .min_by_key(|(_, _, wild_pos, _)| {
615 (wild_pos.0.distance_squared(pos.0) * 100.0)
616 as i32
617 })
618 .map(|(entity, _, _, _)| entity)
619 } {
620 emitters.emit(TamePetEvent {
621 owner_entity: entity,
622 pet_entity: tameable_entity,
623 });
624 false
625 } else {
626 true
627 }
628 } else {
629 true
630 };
631
632 if reinsert {
633 let _ = inventory.insert_or_stack_at(slot, item);
634 }
635
636 Some(InventoryUpdateEvent::Used)
637 },
638 ItemKind::RecipeGroup { .. } => {
639 match inventory.push_recipe_group(item) {
640 Ok(()) => {
641 if let Some(client) = data.clients.get(entity) {
642 client.send_fallible(
643 ServerGeneral::UpdateRecipes,
644 );
645 }
646 Some(InventoryUpdateEvent::Used)
647 },
648 Err(item) => {
649 inventory.insert_or_stack_at(slot, item).expect(
650 "slot was just vacated of item, so it \
651 definitely fits there.",
652 );
653 None
654 },
655 }
656 },
657 _ => {
658 inventory.insert_or_stack_at(slot, item).expect(
659 "slot was just vacated of item, so it definitely fits \
660 there.",
661 );
662 None
663 },
664 }
665 } else {
666 None
667 }
668 },
669 Slot::Equip(slot) => {
670 if slot == slot::EquipSlot::Lantern {
671 snuff_lantern(&mut data.light_emitters, entity);
672 }
673
674 if let Some(pos) = data.positions.get(entity) {
675 // Unequip the item, any items that no longer fit within the
676 // inventory (due to unequipping a
677 // bag for example) will be dropped on the floor
678 if let Ok(Some(leftover_items)) =
679 inventory.unequip(slot, *data.time)
680 {
681 dropped_items.extend(leftover_items.into_iter().map(|item| {
682 (
683 *pos,
684 data.orientations
685 .get(entity)
686 .copied()
687 .unwrap_or_default(),
688 PickupItem::new(item, *data.program_time),
689 *uid,
690 )
691 }));
692 }
693 }
694 Some(InventoryUpdateEvent::Used)
695 },
696 // Items in overflow slots cannot be used
697 Slot::Overflow(_) => None,
698 };
699
700 if let Some(effects) = maybe_effect {
701 match effects {
702 item::Effects::Any(effects) => {
703 if let Some(effect) = effects.into_iter().choose(&mut rng) {
704 emit_effect_events(
705 &mut emitters,
706 *data.time,
707 entity,
708 effect,
709 None,
710 data.inventories.get(entity),
711 &data.msm,
712 data.character_states.get(entity),
713 data.stats.get(entity),
714 data.masses.get(entity),
715 None,
716 );
717 }
718 },
719 item::Effects::All(effects) => {
720 for effect in effects {
721 emit_effect_events(
722 &mut emitters,
723 *data.time,
724 entity,
725 effect,
726 None,
727 data.inventories.get(entity),
728 &data.msm,
729 data.character_states.get(entity),
730 data.stats.get(entity),
731 data.masses.get(entity),
732 None,
733 );
734 }
735 },
736 item::Effects::One(effect) => {
737 emit_effect_events(
738 &mut emitters,
739 *data.time,
740 entity,
741 effect,
742 None,
743 data.inventories.get(entity),
744 &data.msm,
745 data.character_states.get(entity),
746 data.stats.get(entity),
747 data.masses.get(entity),
748 None,
749 );
750 },
751 }
752 }
753 if let Some(event) = event {
754 data.inventory_updates
755 .insert(entity, comp::InventoryUpdate::new(event))
756 .expect("We know entity exists since we got its inventory.");
757 }
758 },
759 comp::InventoryManip::Swap(a, b) => {
760 use item::ItemKind;
761
762 if let Some(lantern_info) = match (a, b) {
763 // Only current possible lantern swap is between Slot::Inventory and
764 // Slot::Equip add more cases if needed
765 (Slot::Equip(slot::EquipSlot::Lantern), Slot::Inventory(slot))
766 | (Slot::Inventory(slot), Slot::Equip(slot::EquipSlot::Lantern)) => {
767 inventory.get(slot).and_then(|i| match &*i.kind() {
768 ItemKind::Lantern(lantern) => {
769 Some((lantern.color(), lantern.strength()))
770 },
771 _ => None,
772 })
773 },
774 _ => None,
775 } {
776 swap_lantern(&mut data.light_emitters, entity, lantern_info);
777 }
778
779 if let Some(pos) = data.positions.get(entity) {
780 let mut merged_stacks = false;
781
782 // If both slots have items and we're attempting to drag from one stack
783 // into another, stack the items.
784 if let (Slot::Inventory(slot_a), Slot::Inventory(slot_b)) = (a, b) {
785 merged_stacks |= inventory.merge_stack_into(slot_a, slot_b);
786 }
787
788 // If the stacks weren't mergable carry out a swap.
789 if !merged_stacks {
790 dropped_items.extend(inventory.swap(a, b, *data.time).into_iter().map(
791 |item| {
792 (
793 *pos,
794 data.orientations.get(entity).copied().unwrap_or_default(),
795 PickupItem::new(item, *data.program_time),
796 *uid,
797 )
798 },
799 ));
800 }
801 }
802
803 data.inventory_updates
804 .insert(
805 entity,
806 comp::InventoryUpdate::new(InventoryUpdateEvent::Swapped),
807 )
808 .expect("We know entity exists since we got its inventory.");
809 },
810 comp::InventoryManip::SplitSwap(slot, target) => {
811 // If both slots have items and we're attempting to split from one stack
812 // into another, ensure that they are the same type of item. If they are
813 // the same type do nothing, as you don't want to overwrite the existing item.
814
815 if let (
816 Slot::Inventory(source_inv_slot_id),
817 Slot::Inventory(target_inv_slot_id),
818 ) = (slot, target)
819 {
820 if let Some(source_item) = inventory.get(source_inv_slot_id) {
821 if let Some(target_item) = inventory.get(target_inv_slot_id) {
822 if source_item != target_item {
823 continue;
824 }
825 }
826 }
827 }
828
829 let item = match slot {
830 Slot::Inventory(slot) => {
831 inventory.take_half(slot, &data.ability_map, &data.msm)
832 },
833 Slot::Equip(_) => None,
834 Slot::Overflow(_) => None,
835 };
836
837 if let Some(item) = item {
838 if let Slot::Inventory(target) = target {
839 inventory.insert_or_stack_at(target, item).ok();
840 }
841 }
842
843 data.inventory_updates
844 .insert(
845 entity,
846 comp::InventoryUpdate::new(InventoryUpdateEvent::Swapped),
847 )
848 .expect("We know entity exists since we got its inventory.");
849 },
850 comp::InventoryManip::Drop(slot) => {
851 let item = match slot {
852 Slot::Inventory(slot) => inventory.remove(slot),
853 Slot::Equip(slot) => inventory.replace_loadout_item(slot, None, *data.time),
854 Slot::Overflow(slot) => inventory.overflow_remove(slot),
855 };
856
857 // FIXME: We should really require the drop and write to be atomic!
858 if let (Some(mut item), Some(pos)) = (item, data.positions.get(entity)) {
859 item.put_in_world();
860 dropped_items.push((
861 *pos,
862 data.orientations.get(entity).copied().unwrap_or_default(),
863 PickupItem::new(item, *data.program_time),
864 *uid,
865 ));
866 }
867 data.inventory_updates
868 .insert(
869 entity,
870 comp::InventoryUpdate::new(InventoryUpdateEvent::Dropped),
871 )
872 .expect("We know entity exists since we got its inventory.");
873 },
874 comp::InventoryManip::SplitDrop(slot) => {
875 let item = match slot {
876 Slot::Inventory(slot) => {
877 inventory.take_half(slot, &data.ability_map, &data.msm)
878 },
879 Slot::Equip(_) => None,
880 Slot::Overflow(o) => {
881 inventory.overflow_take_half(o, &data.ability_map, &data.msm)
882 },
883 };
884
885 // FIXME: We should really require the drop and write to be atomic!
886 if let (Some(mut item), Some(pos)) = (item, data.positions.get(entity)) {
887 item.put_in_world();
888 dropped_items.push((
889 *pos,
890 data.orientations.get(entity).copied().unwrap_or_default(),
891 PickupItem::new(item, *data.program_time),
892 *uid,
893 ));
894 }
895 data.inventory_updates
896 .insert(
897 entity,
898 comp::InventoryUpdate::new(InventoryUpdateEvent::Dropped),
899 )
900 .expect("We know entity exists since we got its inventory.");
901 },
902 comp::InventoryManip::CraftRecipe {
903 craft_event,
904 craft_sprite,
905 } => {
906 use comp::controller::CraftEvent;
907 use recipe::ComponentKey;
908
909 let get_craft_sprite = |sprite_pos: Option<VolumePos>| {
910 sprite_pos
911 .filter(|pos| {
912 let entity_cylinder = get_cylinder(entity);
913 let in_range = within_pickup_range(entity_cylinder, || {
914 pos.get_block_and_transform(
915 &data.terrain,
916 &data.id_maps,
917 |e| {
918 data.positions
919 .get(e)
920 .copied()
921 .zip(data.orientations.get(e).copied())
922 },
923 &data.colliders,
924 )
925 .map(|(mat, _)| mat.mul_point(Vec3::broadcast(0.5)))
926 });
927 if !in_range {
928 debug!(
929 ?entity_cylinder,
930 "Failed to craft recipe as not within range of required \
931 sprite, sprite pos: {:?}",
932 pos
933 );
934 }
935 in_range
936 })
937 .and_then(|pos| {
938 pos.get_block(&data.terrain, &data.id_maps, &data.colliders)
939 })
940 .and_then(|block| block.get_sprite())
941 };
942
943 let crafted_items = match craft_event {
944 CraftEvent::Simple {
945 recipe,
946 slots,
947 amount,
948 } => {
949 let filtered_recipe = inventory
950 .get_recipe(&recipe, &data.rbm)
951 .cloned()
952 .filter(|r| {
953 if let Some(needed_sprite) = r.craft_sprite {
954 let sprite = get_craft_sprite(craft_sprite);
955 Some(needed_sprite) == sprite
956 } else {
957 true
958 }
959 });
960 if let Some(recipe) = filtered_recipe {
961 let items = (0..amount)
962 .filter_map(|_| {
963 recipe
964 .craft_simple(
965 &mut inventory,
966 slots.clone(),
967 &data.ability_map,
968 &data.msm,
969 )
970 .ok()
971 })
972 .flatten()
973 .collect::<Vec<_>>();
974
975 if items.is_empty() { None } else { Some(items) }
976 } else {
977 None
978 }
979 },
980 CraftEvent::Salvage(slot) => {
981 let sprite = get_craft_sprite(craft_sprite);
982 if matches!(sprite, Some(SpriteKind::DismantlingBench)) {
983 recipe::try_salvage(
984 &mut inventory,
985 slot,
986 &data.ability_map,
987 &data.msm,
988 )
989 .ok()
990 } else {
991 None
992 }
993 },
994 CraftEvent::ModularWeapon {
995 primary_component,
996 secondary_component,
997 } => {
998 let sprite = get_craft_sprite(craft_sprite);
999 if matches!(sprite, Some(SpriteKind::CraftingBench)) {
1000 recipe::modular_weapon(
1001 &mut inventory,
1002 primary_component,
1003 secondary_component,
1004 &data.ability_map,
1005 &data.msm,
1006 )
1007 .ok()
1008 .map(|item| vec![item])
1009 } else {
1010 None
1011 }
1012 },
1013 CraftEvent::ModularWeaponPrimaryComponent {
1014 toolkind,
1015 material,
1016 modifier,
1017 slots,
1018 } => {
1019 let component_recipes = default_component_recipe_book().read();
1020 let item_id = |slot| {
1021 inventory.get(slot).and_then(|item| {
1022 item.item_definition_id().itemdef_id().map(String::from)
1023 })
1024 };
1025 if let Some(material_item_id) = item_id(material) {
1026 component_recipes
1027 .get(&ComponentKey {
1028 toolkind,
1029 material: material_item_id,
1030 modifier: modifier.and_then(item_id),
1031 })
1032 .filter(|r| {
1033 let sprite = if let Some(needed_sprite) = r.craft_sprite {
1034 let sprite = get_craft_sprite(craft_sprite);
1035 Some(needed_sprite) == sprite
1036 } else {
1037 true
1038 };
1039 let known = inventory.recipe_is_known(&r.recipe_book_key);
1040 sprite && known
1041 })
1042 .and_then(|r| {
1043 r.craft_component(
1044 &mut inventory,
1045 material,
1046 modifier,
1047 slots,
1048 &data.ability_map,
1049 &data.msm,
1050 )
1051 .ok()
1052 })
1053 } else {
1054 None
1055 }
1056 },
1057 CraftEvent::Repair { item, slots } => {
1058 let repair_recipes = default_repair_recipe_book().read();
1059 let sprite = get_craft_sprite(craft_sprite);
1060 if matches!(sprite, Some(SpriteKind::RepairBench)) {
1061 let _ = repair_recipes.repair_item(
1062 &mut inventory,
1063 item,
1064 slots,
1065 &data.ability_map,
1066 &data.msm,
1067 );
1068 }
1069 None
1070 },
1071 };
1072
1073 // Attempt to insert items into inventory, dropping them if there is not enough
1074 // space
1075 let items_were_crafted = if let Some(crafted_items) = crafted_items {
1076 let mut dropped: Vec<PickupItem> = Vec::new();
1077 for item in crafted_items {
1078 if let Err((item, _inserted)) = inventory.push(item) {
1079 let item = PickupItem::new(item, *data.program_time);
1080 if let Some(can_merge) =
1081 dropped.iter_mut().find(|other| other.can_merge(&item))
1082 {
1083 can_merge
1084 .try_merge(item)
1085 .expect("We know these items can be merged");
1086 } else {
1087 dropped.push(item);
1088 }
1089 }
1090 }
1091
1092 if !dropped.is_empty()
1093 && let Some(pos) = data.positions.get(entity)
1094 {
1095 for item in dropped {
1096 dropped_items.push((
1097 *pos,
1098 data.orientations.get(entity).copied().unwrap_or_default(),
1099 item,
1100 *uid,
1101 ));
1102 }
1103 }
1104
1105 true
1106 } else {
1107 false
1108 };
1109
1110 // FIXME: We should really require the drop and write to be atomic!
1111 if items_were_crafted {
1112 let _ = data.inventory_updates.insert(
1113 entity,
1114 comp::InventoryUpdate::new(InventoryUpdateEvent::Craft),
1115 );
1116 }
1117 },
1118 comp::InventoryManip::Sort => {
1119 inventory.sort();
1120 },
1121 comp::InventoryManip::SwapEquippedWeapons => {
1122 inventory.swap_equipped_weapons(*data.time);
1123 },
1124 }
1125 if data.trades.in_mutable_trade(uid) {
1126 // manipulating the inventory mutated the trade, so reset the accept flags
1127 data.trades.implicit_mutation_occurred(uid);
1128 }
1129 }
1130
1131 // Drop items, Debug items should simply disappear when dropped
1132 for (pos, ori, mut item, owner) in dropped_items
1133 .into_iter()
1134 .filter(|(_, _, i, _)| !matches!(i.item().quality(), item::Quality::Debug))
1135 {
1136 item.remove_debug_items();
1137
1138 emitters.emit(CreateItemDropEvent {
1139 pos,
1140 vel: comp::Vel::default(),
1141 ori,
1142 item,
1143 loot_owner: Some(LootOwner::new(LootOwnerKind::Player(owner), true)),
1144 })
1145 }
1146 }
1147}
1148
1149fn within_pickup_range<S: FindDist<find_dist::Cylinder>>(
1150 entity_cylinder: Option<find_dist::Cylinder>,
1151 shape_fn: impl FnOnce() -> Option<S>,
1152) -> bool {
1153 entity_cylinder
1154 .and_then(|entity_cylinder| {
1155 shape_fn().map(|shape| shape.min_distance(entity_cylinder) < MAX_PICKUP_RANGE)
1156 })
1157 .unwrap_or(false)
1158}
1159
1160fn announce_loot_to_group(
1161 group_id: &Group,
1162 entity: EcsEntity,
1163 item: comp::FrontendItem,
1164 clients: &ReadStorage<Client>,
1165 uids: &ReadStorage<Uid>,
1166 groups: &ReadStorage<comp::Group>,
1167 alignments: &ReadStorage<comp::Alignment>,
1168 entities: &Entities,
1169 ability_map: &AbilityMap,
1170 msm: &MaterialStatManifest,
1171) {
1172 if let Some(uid) = uids.get(entity) {
1173 members(*group_id, groups, entities, alignments, uids)
1174 .filter(|(member_e, _)| member_e != &entity)
1175 .for_each(|(e, _)| {
1176 clients.get(e).map(|c| {
1177 c.send_fallible(ServerGeneral::GroupInventoryUpdate(
1178 item.duplicate(ability_map, msm),
1179 *uid,
1180 ));
1181 });
1182 });
1183 }
1184}
1185
1186#[cfg(test)]
1187mod tests {
1188 use vek::Vec3;
1189
1190 use common::comp::Pos;
1191 use find_dist::*;
1192
1193 use super::*;
1194
1195 // Helper function
1196 fn test_cylinder(pos: Pos) -> Option<Cylinder> {
1197 Some(Cylinder::from_components(pos.0, None, None, None))
1198 }
1199
1200 #[test]
1201 fn pickup_distance_within_range() {
1202 let position = Pos(Vec3::zero());
1203 let item_position = Pos(Vec3::one());
1204
1205 assert!(within_pickup_range(test_cylinder(position), || {
1206 test_cylinder(item_position)
1207 },),);
1208 }
1209
1210 #[test]
1211 fn pickup_distance_not_within_range() {
1212 let position = Pos(Vec3::zero());
1213 let item_position = Pos(Vec3::one() * 500.0);
1214
1215 assert!(!within_pickup_range(test_cylinder(position), || {
1216 test_cylinder(item_position)
1217 },),);
1218 }
1219}