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