veloren_server/events/
inventory_manip.rs

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