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