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