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