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