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, ChangeBodyEvent, CreateItemDropEvent, CreateObjectEvent, DeleteEvent, EmitExt,
21 HealthChangeEvent, InventoryManipEvent, PoiseChangeEvent, TamePetEvent,
22 },
23 event_emitters, match_some,
24 mounting::VolumePos,
25 outcome::Outcome,
26 recipe::{self, RecipeBookManifest, default_component_recipe_book, default_repair_recipe_book},
27 resources::{ProgramTime, Time},
28 terrain::{Block, SpriteKind, sprite},
29 trade::Trades,
30 uid::{IdMaps, Uid},
31 util::find_dist::{self, FindDist},
32 vol::ReadVol,
33};
34use comp::LightEmitter;
35
36use crate::client::Client;
37use common::comp::{Alignment, CollectFailedReason, Group, InventoryUpdateEvent, pet::is_tameable};
38use common_net::msg::ServerGeneral;
39
40use super::{ServerEvent, entity_manipulation::emit_effect_events, event_dispatch};
41
42pub(super) fn register_event_systems(builder: &mut DispatcherBuilder) {
43 event_dispatch::<InventoryManipEvent>(builder, &[]);
44}
45
46pub fn swap_lantern(
47 storage: &mut WriteStorage<LightEmitter>,
48 entity: EcsEntity,
49 (lantern_color, lantern_strength): (Rgb<f32>, f32),
50) {
51 if let Some(mut light) = storage.get_mut(entity) {
52 light.strength = lantern_strength;
53 light.col = lantern_color;
54 }
55}
56
57pub fn snuff_lantern(storage: &mut WriteStorage<LightEmitter>, entity: EcsEntity) {
58 storage.remove(entity);
59}
60
61event_emitters! {
62 struct Events[Emitters] {
63 tame_pet: TamePetEvent,
64 delete: DeleteEvent,
65 create_item_drop: CreateItemDropEvent,
66 create_object: CreateObjectEvent,
67 health_change: HealthChangeEvent,
68 poise_change: PoiseChangeEvent,
69 buff: BuffEvent,
70 change_body: ChangeBodyEvent,
71 outcome: Outcome,
72 }
73}
74#[derive(SystemData)]
75pub struct InventoryManipData<'a> {
76 entities: Entities<'a>,
77 events: Events<'a>,
78 block_change: Write<'a, common_state::BlockChange>,
79 trades: Write<'a, Trades>,
80 #[cfg(feature = "worldgen")]
81 rtsim: specs::WriteExpect<'a, crate::rtsim::RtSim>,
82 terrain: ReadExpect<'a, common::terrain::TerrainGrid>,
83 id_maps: Read<'a, IdMaps>,
84 time: Read<'a, Time>,
85 #[cfg(feature = "worldgen")]
86 world: ReadExpect<'a, std::sync::Arc<world::World>>,
87 #[cfg(feature = "worldgen")]
88 index: ReadExpect<'a, world::IndexOwned>,
89 program_time: ReadExpect<'a, ProgramTime>,
90 ability_map: ReadExpect<'a, AbilityMap>,
91 msm: ReadExpect<'a, MaterialStatManifest>,
92 rbm: ReadExpect<'a, RecipeBookManifest>,
93 inventories: WriteStorage<'a, comp::Inventory>,
94 items: WriteStorage<'a, comp::PickupItem>,
95 inventory_updates: WriteStorage<'a, comp::InventoryUpdate>,
96 light_emitters: WriteStorage<'a, comp::LightEmitter>,
97 positions: ReadStorage<'a, comp::Pos>,
98 scales: ReadStorage<'a, comp::Scale>,
99 colliders: ReadStorage<'a, comp::Collider>,
100 character_states: ReadStorage<'a, comp::CharacterState>,
101 healths: ReadStorage<'a, comp::Health>,
102 uids: ReadStorage<'a, Uid>,
103 loot_owners: ReadStorage<'a, comp::LootOwner>,
104 alignments: ReadStorage<'a, comp::Alignment>,
105 bodies: ReadStorage<'a, comp::Body>,
106 players: ReadStorage<'a, comp::Player>,
107 groups: ReadStorage<'a, comp::Group>,
108 stats: ReadStorage<'a, comp::Stats>,
109 clients: ReadStorage<'a, Client>,
110 orientations: ReadStorage<'a, comp::Ori>,
111 agents: ReadStorage<'a, comp::Agent>,
112 pets: ReadStorage<'a, comp::Pet>,
113 masses: ReadStorage<'a, comp::Mass>,
114 #[cfg(feature = "worldgen")]
115 presences: ReadStorage<'a, comp::Presence>,
116 #[cfg(feature = "worldgen")]
117 rtsim_entities: ReadStorage<'a, common::rtsim::RtSimEntity>,
118}
119
120impl ServerEvent for InventoryManipEvent {
121 type SystemData<'a> = InventoryManipData<'a>;
122
123 fn handle(events: impl ExactSizeIterator<Item = Self>, mut data: Self::SystemData<'_>) {
124 let mut emitters = data.events.get_emitters();
125 let get_cylinder = |entity| {
126 data.positions.get(entity).map(|p| {
127 find_dist::Cylinder::from_components(
128 p.0,
129 data.scales.get(entity).copied(),
130 data.colliders.get(entity),
131 data.character_states.get(entity),
132 )
133 })
134 };
135 let mut rng = rand::thread_rng();
136
137 let mut dropped_items = Vec::new();
138
139 for InventoryManipEvent(entity, manip) in events {
140 let uid = if let Some(uid) = data.uids.get(entity) {
141 uid
142 } else {
143 warn!(
144 "Couldn't get uid for entity {:?} at start of handle_inventory",
145 entity
146 );
147 continue;
148 };
149 if data.trades.in_immutable_trade(uid) {
150 // manipulating the inventory can mutate the trade
151 continue;
152 }
153
154 if comp::is_downed_or_dead(data.healths.get(entity), data.character_states.get(entity))
155 {
156 // Can't manipulate the inventory while downed or dead.
157 continue;
158 }
159
160 let mut inventory = if let Some(inventory) = data.inventories.get_mut(entity) {
161 inventory
162 } else {
163 error!(
164 ?entity,
165 "Can't manipulate inventory, entity doesn't have one"
166 );
167 continue;
168 };
169 match manip {
170 comp::InventoryManip::Pickup(pickup_uid) => {
171 let item_entity = if let Some(item_entity) = data.id_maps.uid_entity(pickup_uid)
172 {
173 item_entity
174 } else {
175 // Item entity could not be found - most likely because the entity
176 // attempted to pick up the same item very quickly before its deletion
177 // of the world from the first pickup
178 // attempt was processed.
179 debug!("Failed to get entity for item Uid: {}", pickup_uid);
180 continue;
181 };
182 let entity_cylinder = get_cylinder(entity);
183
184 // FIXME: Raycast so we can't pick up items through walls.
185 if !within_pickup_range(entity_cylinder, || get_cylinder(item_entity)) {
186 debug!(
187 ?entity_cylinder,
188 "Failed to pick up item as not within range, Uid: {}", pickup_uid
189 );
190 continue;
191 }
192
193 // If there's a loot owner for the item being picked up, then
194 // determine whether the pickup should be rejected.
195 let ownership_check_passed =
196 data.loot_owners.get(item_entity).is_none_or(|loot_owner| {
197 let can_pickup = loot_owner.can_pickup(
198 *uid,
199 data.groups.get(entity),
200 data.alignments.get(entity),
201 data.stats
202 .get(entity)
203 .map(|stats| &stats.original_body)
204 .or_else(|| data.bodies.get(entity)),
205 data.players.get(entity),
206 );
207 if !can_pickup {
208 let event = comp::InventoryUpdate::new(
209 InventoryUpdateEvent::EntityCollectFailed {
210 entity: pickup_uid,
211 reason: CollectFailedReason::LootOwned {
212 owner: loot_owner.owner(),
213 expiry_secs: loot_owner
214 .time_until_expiration()
215 .as_secs(),
216 },
217 },
218 );
219 data.inventory_updates.insert(entity, event).unwrap();
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 comp::InventoryUpdate::new(InventoryUpdateEvent::Collected(
306 item_msg,
307 ))
308 } else {
309 comp::InventoryUpdate::new(
310 InventoryUpdateEvent::EntityCollectFailed {
311 entity: pickup_uid,
312 reason: CollectFailedReason::InventoryFull,
313 },
314 )
315 }
316 },
317 Ok(_) => {
318 // We succeeded in picking up the item, so we may now delete its old
319 // entity entirely.
320 if let Some(reinsert_item) = reinsert_item {
321 data.items
322 .insert(item_entity, reinsert_item)
323 .expect(ITEM_ENTITY_EXPECT_MESSAGE);
324 } else {
325 emitters.emit(DeleteEvent(item_entity));
326 }
327
328 if let Some(group_id) = data.groups.get(entity) {
329 announce_loot_to_group(
330 group_id,
331 entity,
332 item_msg.duplicate(&data.ability_map, &data.msm),
333 &data.clients,
334 &data.uids,
335 &data.groups,
336 &data.alignments,
337 &data.entities,
338 &data.ability_map,
339 &data.msm,
340 );
341 }
342 comp::InventoryUpdate::new(InventoryUpdateEvent::Collected(item_msg))
343 },
344 };
345
346 data.inventory_updates
347 .insert(entity, event)
348 .expect("We know entity exists since we got its inventory.");
349 },
350 comp::InventoryManip::Collect {
351 sprite_pos,
352 required_item,
353 } => {
354 let block = data.terrain.get(sprite_pos).ok().copied();
355 let mut drop_items = Vec::new();
356 let inventory_update = data
357 .inventory_updates
358 .entry(entity)
359 .expect("We know entity exists since we got its inventory.")
360 .or_insert_with(InventoryUpdate::default);
361
362 if let Some(block) = block {
363 // If there are items to be reclaimed from the block, add it to the
364 // inventory
365 let sprite_cfg = data.terrain.sprite_cfg_at(sprite_pos);
366 if block.is_collectible(sprite_cfg)
367 && data.block_change.can_set_block(sprite_pos)
368 {
369 // Send event to rtsim if something was stolen.
370 #[cfg(feature = "worldgen")]
371 if block.is_owned()
372 && let Some(actor) = super::entity_manipulation::entity_as_actor(
373 entity,
374 &data.rtsim_entities,
375 &data.presences,
376 )
377 {
378 data.rtsim.hook_pickup_owned_sprite(
379 &data.world,
380 data.index.as_index_ref(),
381 block
382 .get_sprite()
383 .expect("If the block is owned, it is a sprite"),
384 sprite_pos,
385 actor,
386 );
387 }
388 // If an item was required to collect the sprite, consume it now
389 if let Some((inv_slot, true)) = required_item {
390 inventory.take(inv_slot, &data.ability_map, &data.msm);
391 }
392
393 if let Some(items) =
394 comp::Item::try_reclaim_from_block(block, sprite_cfg)
395 {
396 for item in
397 flatten_counted_items(&items, &data.ability_map, &data.msm)
398 {
399 let mut item_msg =
400 item.frontend_item(&data.ability_map, &data.msm);
401 let do_announce = match inventory.push(item) {
402 Ok(_) => true,
403 Err((item, inserted)) => {
404 drop_items.push(item);
405 if let Some(inserted) = inserted {
406 // Update the amount of the frontend item
407 item_msg.set_amount(inserted.get()).expect(
408 "Inserted must be > 0 and <= item.max_amount()",
409 );
410 true
411 } else {
412 false
413 }
414 },
415 };
416
417 if do_announce {
418 if let Some(group_id) = data.groups.get(entity) {
419 announce_loot_to_group(
420 group_id,
421 entity,
422 item_msg.duplicate(&data.ability_map, &data.msm),
423 &data.clients,
424 &data.uids,
425 &data.groups,
426 &data.alignments,
427 &data.entities,
428 &data.ability_map,
429 &data.msm,
430 );
431 }
432 inventory_update
433 .push(InventoryUpdateEvent::Collected(item_msg));
434 }
435 }
436 }
437
438 // We made sure earlier the block was not already modified this tick
439 match block.get_sprite() {
440 Some(SpriteKind::Lettuce) => {
441 let new_block =
442 block.with_attr(sprite::Collectable(false)).expect(
443 "Setting collectable will not fail as this scope \
444 requires block::is_collectible to return true.",
445 );
446 data.block_change.set(sprite_pos, new_block)
447 },
448 _ => data.block_change.set(sprite_pos, block.into_vacant()),
449 }
450
451 // If the block was a keyhole, remove nearby door blocks
452 // TODO: Abstract this code into a generalised way to do block updates?
453 if let Some(kind_to_destroy) = match block.get_sprite() {
454 Some(SpriteKind::Keyhole) => Some(SpriteKind::KeyDoor),
455 Some(SpriteKind::BoneKeyhole) => Some(SpriteKind::BoneKeyDoor),
456 Some(SpriteKind::HaniwaKeyhole) => Some(SpriteKind::HaniwaKeyDoor),
457 Some(SpriteKind::SahaginKeyhole) => {
458 Some(SpriteKind::SahaginKeyDoor)
459 },
460 Some(SpriteKind::GlassKeyhole) => Some(SpriteKind::GlassBarrier),
461 Some(SpriteKind::KeyholeBars) => Some(SpriteKind::DoorBars),
462 Some(SpriteKind::TerracottaKeyhole) => {
463 Some(SpriteKind::TerracottaKeyDoor)
464 },
465 Some(SpriteKind::VampireKeyhole) => {
466 Some(SpriteKind::VampireKeyDoor)
467 },
468 Some(SpriteKind::MyrmidonKeyhole | SpriteKind::MinotaurKeyhole) => {
469 Some(SpriteKind::MyrmidonKeyDoor)
470 },
471 _ => None,
472 } {
473 let dirs = [
474 Vec3::unit_x(),
475 -Vec3::unit_x(),
476 Vec3::unit_y(),
477 -Vec3::unit_y(),
478 Vec3::unit_z(),
479 -Vec3::unit_z(),
480 ];
481 let mut destroyed = HashSet::<Vec3<i32>>::default();
482 let mut pending = dirs
483 .into_iter()
484 .map(|dir| sprite_pos + dir)
485 .collect::<HashSet<_>>();
486 // TODO: Replace with `entry` eventually
487 while destroyed.len() < 450 {
488 if let Some(pos) = pending.iter().next().copied() {
489 pending.remove(&pos);
490
491 if !destroyed.contains(&pos)
492 && data
493 .terrain
494 .get(pos)
495 .ok()
496 .and_then(|b| b.get_sprite())
497 == Some(kind_to_destroy)
498 {
499 data.block_change.try_set(pos, Block::empty());
500 destroyed.insert(pos);
501 pending.extend(dirs.into_iter().map(|dir| pos + dir));
502 }
503 } else {
504 break;
505 }
506 }
507 }
508 } else {
509 debug!(
510 "Can't reclaim item from block at pos={}: block is not \
511 collectable or was already set this tick.",
512 sprite_pos
513 );
514 }
515 }
516 if !drop_items.is_empty() {
517 inventory_update.push(InventoryUpdateEvent::BlockCollectFailed {
518 pos: sprite_pos,
519 reason: CollectFailedReason::InventoryFull,
520 })
521 }
522
523 for item in drop_items {
524 emitters.emit(CreateItemDropEvent {
525 pos: comp::Pos(
526 Vec3::new(
527 sprite_pos.x as f32,
528 sprite_pos.y as f32,
529 sprite_pos.z as f32,
530 ) + Vec3::one().with_z(0.0) * 0.5,
531 ),
532 vel: comp::Vel(Vec3::zero()),
533 ori: data.orientations.get(entity).copied().unwrap_or_default(),
534 item: PickupItem::new(item, *data.program_time, true),
535 loot_owner: Some(LootOwner::new(LootOwnerKind::Player(*uid), false)),
536 });
537 }
538 },
539 comp::InventoryManip::Use(slot) => {
540 let mut maybe_effect = None;
541
542 let event = match slot {
543 Slot::Inventory(slot) => {
544 use item::ItemKind;
545
546 let (is_equippable, lantern_info) =
547 inventory.get(slot).map_or((false, None), |i| {
548 let kind = i.kind();
549 let is_equippable = kind.is_equippable();
550 let lantern_info = match_some!(&*kind, ItemKind::Lantern(l) => (l.color(), l.strength()));
551 (is_equippable, lantern_info)
552 });
553 if is_equippable {
554 if let Some(lantern_info) = lantern_info {
555 swap_lantern(&mut data.light_emitters, entity, lantern_info);
556 }
557 if let Some(pos) = data.positions.get(entity)
558 && let Ok(Some(unloaded_items)) = inventory.equip(
559 slot,
560 *data.time,
561 &data.ability_map,
562 &data.msm,
563 )
564 {
565 dropped_items.extend(unloaded_items.into_iter().map(|item| {
566 (
567 *pos,
568 data.orientations
569 .get(entity)
570 .copied()
571 .unwrap_or_default(),
572 PickupItem::new(item, *data.program_time, true),
573 *uid,
574 )
575 }));
576 }
577 Some(InventoryUpdateEvent::Used)
578 } else if let Some(item) =
579 inventory.take(slot, &data.ability_map, &data.msm)
580 {
581 match &*item.kind() {
582 ItemKind::Consumable {
583 effects, container, ..
584 } => {
585 maybe_effect = Some(effects.clone());
586
587 if let Some(container) = container
588 && let Ok(container_item) =
589 comp::Item::new_from_item_definition_id(
590 container.as_ref(),
591 &data.ability_map,
592 &data.msm,
593 )
594 {
595 let result = inventory.push(container_item);
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 data.inventory_updates
797 .insert(entity, comp::InventoryUpdate::new(event))
798 .expect("We know entity exists since we got its inventory.");
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.get(slot).and_then(|i| {
810 match_some!(&*i.kind(), ItemKind::Lantern(l) => (l.color(), l.strength()))
811 })
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 data.inventory_updates
843 .insert(
844 entity,
845 comp::InventoryUpdate::new(InventoryUpdateEvent::Swapped),
846 )
847 .expect("We know entity exists since we got its inventory.");
848 },
849 comp::InventoryManip::SplitSwap(slot, target) => {
850 // If both slots have items and we're attempting to split from one stack
851 // into another, ensure that they are the same type of item. If they are
852 // the same type do nothing, as you don't want to overwrite the existing item.
853
854 if let (
855 Slot::Inventory(source_inv_slot_id),
856 Slot::Inventory(target_inv_slot_id),
857 ) = (slot, target)
858 {
859 if let Some(source_item) = inventory.get(source_inv_slot_id) {
860 if let Some(target_item) = inventory.get(target_inv_slot_id) {
861 if source_item != target_item {
862 continue;
863 }
864 }
865 }
866 }
867
868 let item = match slot {
869 Slot::Inventory(slot) => {
870 inventory.take_half(slot, &data.ability_map, &data.msm)
871 },
872 Slot::Equip(_) => None,
873 Slot::Overflow(_) => None,
874 };
875
876 if let Some(item) = item {
877 if let Slot::Inventory(target) = target {
878 inventory.insert_or_stack_at(target, item).ok();
879 }
880 }
881
882 data.inventory_updates
883 .insert(
884 entity,
885 comp::InventoryUpdate::new(InventoryUpdateEvent::Swapped),
886 )
887 .expect("We know entity exists since we got its inventory.");
888 },
889 comp::InventoryManip::Drop(slot) => {
890 let item = match slot {
891 Slot::Inventory(slot) => inventory.remove(slot),
892 Slot::Equip(slot) => inventory.replace_loadout_item(slot, None, *data.time),
893 Slot::Overflow(slot) => inventory.overflow_remove(slot),
894 };
895
896 // FIXME: We should really require the drop and write to be atomic!
897 if let (Some(mut item), Some(pos)) = (item, data.positions.get(entity)) {
898 item.put_in_world();
899 dropped_items.push((
900 *pos,
901 data.orientations.get(entity).copied().unwrap_or_default(),
902 PickupItem::new(item, *data.program_time, true),
903 *uid,
904 ));
905 }
906 data.inventory_updates
907 .insert(
908 entity,
909 comp::InventoryUpdate::new(InventoryUpdateEvent::Dropped),
910 )
911 .expect("We know entity exists since we got its inventory.");
912 },
913 comp::InventoryManip::SplitDrop(slot) => {
914 let item = match slot {
915 Slot::Inventory(slot) => {
916 inventory.take_half(slot, &data.ability_map, &data.msm)
917 },
918 Slot::Equip(_) => None,
919 Slot::Overflow(o) => {
920 inventory.overflow_take_half(o, &data.ability_map, &data.msm)
921 },
922 };
923
924 // FIXME: We should really require the drop and write to be atomic!
925 if let (Some(mut item), Some(pos)) = (item, data.positions.get(entity)) {
926 item.put_in_world();
927 dropped_items.push((
928 *pos,
929 data.orientations.get(entity).copied().unwrap_or_default(),
930 PickupItem::new(item, *data.program_time, true),
931 *uid,
932 ));
933 }
934 data.inventory_updates
935 .insert(
936 entity,
937 comp::InventoryUpdate::new(InventoryUpdateEvent::Dropped),
938 )
939 .expect("We know entity exists since we got its inventory.");
940 },
941 comp::InventoryManip::CraftRecipe {
942 craft_event,
943 craft_sprite,
944 } => {
945 use comp::controller::CraftEvent;
946 use recipe::ComponentKey;
947
948 let get_craft_sprite = |sprite_pos: Option<VolumePos>| {
949 sprite_pos
950 .filter(|pos| {
951 let entity_cylinder = get_cylinder(entity);
952 let in_range = within_pickup_range(entity_cylinder, || {
953 pos.get_block_and_transform(
954 &data.terrain,
955 &data.id_maps,
956 |e| {
957 data.positions
958 .get(e)
959 .copied()
960 .zip(data.orientations.get(e).copied())
961 },
962 &data.colliders,
963 )
964 .map(|(mat, _)| mat.mul_point(Vec3::broadcast(0.5)))
965 });
966 if !in_range {
967 debug!(
968 ?entity_cylinder,
969 "Failed to craft recipe as not within range of required \
970 sprite, sprite pos: {:?}",
971 pos
972 );
973 }
974 in_range
975 })
976 .and_then(|pos| {
977 pos.get_block(&data.terrain, &data.id_maps, &data.colliders)
978 })
979 .and_then(|block| block.get_sprite())
980 };
981
982 let crafted_items = match craft_event {
983 CraftEvent::Simple {
984 recipe,
985 slots,
986 amount,
987 } => {
988 let filtered_recipe = inventory
989 .get_recipe(&recipe, &data.rbm)
990 .cloned()
991 .filter(|r| {
992 if let Some(needed_sprite) = r.craft_sprite {
993 let sprite = get_craft_sprite(craft_sprite);
994 Some(needed_sprite) == sprite
995 } else {
996 true
997 }
998 });
999 if let Some(recipe) = filtered_recipe {
1000 let items = (0..amount)
1001 .filter_map(|_| {
1002 recipe
1003 .craft_simple(
1004 &mut inventory,
1005 slots.clone(),
1006 &data.ability_map,
1007 &data.msm,
1008 )
1009 .ok()
1010 })
1011 .flatten()
1012 .collect::<Vec<_>>();
1013
1014 if items.is_empty() { None } else { Some(items) }
1015 } else {
1016 None
1017 }
1018 },
1019 CraftEvent::Salvage(slot) => {
1020 let sprite = get_craft_sprite(craft_sprite);
1021 if matches!(sprite, Some(SpriteKind::DismantlingBench)) {
1022 recipe::try_salvage(
1023 &mut inventory,
1024 slot,
1025 &data.ability_map,
1026 &data.msm,
1027 )
1028 .ok()
1029 } else {
1030 None
1031 }
1032 },
1033 CraftEvent::ModularWeapon {
1034 primary_component,
1035 secondary_component,
1036 } => {
1037 let sprite = get_craft_sprite(craft_sprite);
1038 if matches!(sprite, Some(SpriteKind::CraftingBench)) {
1039 recipe::modular_weapon(
1040 &mut inventory,
1041 primary_component,
1042 secondary_component,
1043 &data.ability_map,
1044 &data.msm,
1045 )
1046 .ok()
1047 .map(|item| vec![item])
1048 } else {
1049 None
1050 }
1051 },
1052 CraftEvent::ModularWeaponPrimaryComponent {
1053 toolkind,
1054 material,
1055 modifier,
1056 slots,
1057 } => {
1058 let component_recipes = default_component_recipe_book().read();
1059 let item_id = |slot| {
1060 inventory.get(slot).and_then(|item| {
1061 item.item_definition_id().itemdef_id().map(String::from)
1062 })
1063 };
1064 if let Some(material_item_id) = item_id(material) {
1065 component_recipes
1066 .get(&ComponentKey {
1067 toolkind,
1068 material: material_item_id,
1069 modifier: modifier.and_then(item_id),
1070 })
1071 .filter(|r| {
1072 let sprite = if let Some(needed_sprite) = r.craft_sprite {
1073 let sprite = get_craft_sprite(craft_sprite);
1074 Some(needed_sprite) == sprite
1075 } else {
1076 true
1077 };
1078 let known = inventory.recipe_is_known(&r.recipe_book_key);
1079 sprite && known
1080 })
1081 .and_then(|r| {
1082 r.craft_component(
1083 &mut inventory,
1084 material,
1085 modifier,
1086 slots,
1087 &data.ability_map,
1088 &data.msm,
1089 )
1090 .ok()
1091 })
1092 } else {
1093 None
1094 }
1095 },
1096 CraftEvent::Repair { item, slots } => {
1097 let repair_recipes = default_repair_recipe_book().read();
1098 let sprite = get_craft_sprite(craft_sprite);
1099 if matches!(sprite, Some(SpriteKind::RepairBench)) {
1100 let _ = repair_recipes.repair_item(
1101 &mut inventory,
1102 item,
1103 slots,
1104 &data.ability_map,
1105 &data.msm,
1106 );
1107 }
1108 None
1109 },
1110 };
1111
1112 // Attempt to insert items into inventory, dropping them if there is not enough
1113 // space
1114 let items_were_crafted = if let Some(crafted_items) = crafted_items {
1115 let mut dropped: Vec<PickupItem> = Vec::new();
1116 for item in crafted_items {
1117 if let Err((item, _inserted)) = inventory.push(item) {
1118 let item = PickupItem::new(item, *data.program_time, true);
1119 if let Some(can_merge) =
1120 dropped.iter_mut().find(|other| other.can_merge(&item))
1121 {
1122 can_merge
1123 .try_merge(item)
1124 .expect("We know these items can be merged");
1125 } else {
1126 dropped.push(item);
1127 }
1128 }
1129 }
1130
1131 if !dropped.is_empty()
1132 && let Some(pos) = data.positions.get(entity)
1133 {
1134 for item in dropped {
1135 dropped_items.push((
1136 *pos,
1137 data.orientations.get(entity).copied().unwrap_or_default(),
1138 item,
1139 *uid,
1140 ));
1141 }
1142 }
1143
1144 true
1145 } else {
1146 false
1147 };
1148
1149 // FIXME: We should really require the drop and write to be atomic!
1150 if items_were_crafted {
1151 let _ = data.inventory_updates.insert(
1152 entity,
1153 comp::InventoryUpdate::new(InventoryUpdateEvent::Craft),
1154 );
1155 }
1156 },
1157 comp::InventoryManip::Sort => {
1158 inventory.sort();
1159 },
1160 comp::InventoryManip::SwapEquippedWeapons => {
1161 inventory.swap_equipped_weapons(*data.time);
1162 },
1163 }
1164 if data.trades.in_mutable_trade(uid) {
1165 // manipulating the inventory mutated the trade, so reset the accept flags
1166 data.trades.implicit_mutation_occurred(uid);
1167 }
1168 }
1169
1170 // Drop items, Debug items should simply disappear when dropped
1171 for (pos, ori, mut item, owner) in dropped_items
1172 .into_iter()
1173 .filter(|(_, _, i, _)| !matches!(i.item().quality(), item::Quality::Debug))
1174 {
1175 item.remove_debug_items();
1176
1177 emitters.emit(CreateItemDropEvent {
1178 pos,
1179 vel: comp::Vel::default(),
1180 ori,
1181 item,
1182 loot_owner: Some(LootOwner::new(LootOwnerKind::Player(owner), true)),
1183 })
1184 }
1185 }
1186}
1187
1188fn within_pickup_range<S: FindDist<find_dist::Cylinder>>(
1189 entity_cylinder: Option<find_dist::Cylinder>,
1190 shape_fn: impl FnOnce() -> Option<S>,
1191) -> bool {
1192 entity_cylinder
1193 .and_then(|entity_cylinder| {
1194 shape_fn().map(|shape| shape.min_distance(entity_cylinder) < MAX_PICKUP_RANGE)
1195 })
1196 .unwrap_or(false)
1197}
1198
1199fn announce_loot_to_group(
1200 group_id: &Group,
1201 entity: EcsEntity,
1202 item: comp::FrontendItem,
1203 clients: &ReadStorage<Client>,
1204 uids: &ReadStorage<Uid>,
1205 groups: &ReadStorage<comp::Group>,
1206 alignments: &ReadStorage<comp::Alignment>,
1207 entities: &Entities,
1208 ability_map: &AbilityMap,
1209 msm: &MaterialStatManifest,
1210) {
1211 if let Some(uid) = uids.get(entity) {
1212 members(*group_id, groups, entities, alignments, uids)
1213 .filter(|(member_e, _)| member_e != &entity)
1214 .for_each(|(e, _)| {
1215 clients.get(e).map(|c| {
1216 c.send_fallible(ServerGeneral::GroupInventoryUpdate(
1217 item.duplicate(ability_map, msm),
1218 *uid,
1219 ));
1220 });
1221 });
1222 }
1223}
1224
1225#[cfg(test)]
1226mod tests {
1227 use vek::Vec3;
1228
1229 use common::comp::Pos;
1230 use find_dist::*;
1231
1232 use super::*;
1233
1234 // Helper function
1235 fn test_cylinder(pos: Pos) -> Option<Cylinder> {
1236 Some(Cylinder::from_components(pos.0, None, None, None))
1237 }
1238
1239 #[test]
1240 fn pickup_distance_within_range() {
1241 let position = Pos(Vec3::zero());
1242 let item_position = Pos(Vec3::one());
1243
1244 assert!(within_pickup_range(test_cylinder(position), || {
1245 test_cylinder(item_position)
1246 },),);
1247 }
1248
1249 #[test]
1250 fn pickup_distance_not_within_range() {
1251 let position = Pos(Vec3::zero());
1252 let item_position = Pos(Vec3::one() * 500.0);
1253
1254 assert!(!within_pickup_range(test_cylinder(position), || {
1255 test_cylinder(item_position)
1256 },),);
1257 }
1258}