veloren_server/events/
inventory_manip.rs

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