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