veloren_server/events/
inventory_manip.rs

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