veloren_server/events/
inventory_manip.rs

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