Skip to main content

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