veloren_server/events/
inventory_manip.rs

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