Skip to main content

veloren_server/events/
interaction.rs

1use std::{f32::consts::PI, ops::Mul};
2
3use common::{comp::loot_owner::ONWERSHIP_TIMEOUT_FAST, rtsim::DialogueKind};
4use common_state::{BlockChange, ScheduledBlockChange};
5use specs::{DispatcherBuilder, Join, ReadExpect, ReadStorage, WriteExpect, WriteStorage};
6use tracing::error;
7use vek::*;
8
9use common::{
10    assets::{AssetCombined, AssetHandle, Ron},
11    comp::{
12        self, InventoryUpdateEvent,
13        agent::{AgentEvent, Sound, SoundKind},
14        inventory::slot::EquipSlot,
15        item::{MaterialStatManifest, flatten_counted_items},
16        loot_owner::LootOwnerKind,
17        tool::AbilityMap,
18    },
19    consts::{MAX_INTERACT_RANGE, MAX_NPCINTERACT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME},
20    event::{
21        CreateItemDropEvent, CreateSpriteEvent, DialogueEvent, EventBus, MineBlockEvent,
22        NpcInteractEvent, SetLanternEvent, SetPetStayEvent, SoundEvent, TamePetEvent,
23        ToggleSpriteLightEvent,
24    },
25    link::Is,
26    mounting::Mount,
27    outcome::Outcome,
28    resources::ProgramTime,
29    terrain::{self, Block, SpriteKind, TerrainGrid},
30    uid::Uid,
31    util::Dir,
32    vol::ReadVol,
33};
34
35use crate::{Server, ServerGeneral, Time, client::Client};
36
37use crate::pet::tame_pet;
38use hashbrown::{HashMap, HashSet};
39use lazy_static::lazy_static;
40
41use super::{ServerEvent, event_dispatch, mounting::within_mounting_range};
42
43pub(super) fn register_event_systems(builder: &mut DispatcherBuilder) {
44    event_dispatch::<SetLanternEvent>(builder, &[]);
45    event_dispatch::<NpcInteractEvent>(builder, &[]);
46    event_dispatch::<DialogueEvent>(builder, &[]);
47    event_dispatch::<SetPetStayEvent>(builder, &[]);
48    event_dispatch::<MineBlockEvent>(builder, &[]);
49    event_dispatch::<SoundEvent>(builder, &[]);
50    event_dispatch::<CreateSpriteEvent>(builder, &[]);
51    event_dispatch::<ToggleSpriteLightEvent>(builder, &[]);
52}
53
54impl ServerEvent for SetLanternEvent {
55    type SystemData<'a> = (
56        WriteStorage<'a, comp::LightEmitter>,
57        ReadStorage<'a, comp::Inventory>,
58        ReadStorage<'a, comp::Health>,
59    );
60
61    fn handle(
62        events: impl ExactSizeIterator<Item = Self>,
63        (mut light_emitters, inventories, healths): Self::SystemData<'_>,
64    ) {
65        for SetLanternEvent(entity, enable) in events {
66            let lantern_exists = light_emitters
67                .get(entity)
68                .is_some_and(|light| light.strength > 0.0);
69
70            if lantern_exists != enable {
71                if !enable {
72                    light_emitters.remove(entity);
73                }
74                // Only enable lantern if entity is alive
75                else if healths.get(entity).is_none_or(|h| !h.is_dead) {
76                    inventories
77                        .get(entity)
78                        .and_then(|inventory| inventory.equipped(EquipSlot::Lantern))
79                        .map(|item| {
80                            if let comp::item::ItemKind::Lantern(l) = &*item.kind() {
81                                let _ = light_emitters.insert(entity, comp::LightEmitter {
82                                    col: l.color(),
83                                    strength: l.strength(),
84                                    flicker: l.flicker(),
85                                    animated: true,
86                                    dir: l.dir,
87                                });
88                            }
89                        });
90                }
91            }
92        }
93    }
94}
95
96impl ServerEvent for NpcInteractEvent {
97    type SystemData<'a> = (
98        WriteStorage<'a, comp::Agent>,
99        ReadStorage<'a, comp::Pos>,
100        ReadStorage<'a, Uid>,
101    );
102
103    fn handle(
104        events: impl ExactSizeIterator<Item = Self>,
105        (mut agents, positions, uids): Self::SystemData<'_>,
106    ) {
107        for NpcInteractEvent(interactor, npc_entity) in events {
108            let within_range = {
109                positions
110                    .get(interactor)
111                    .zip(positions.get(npc_entity))
112                    .is_some_and(|(interactor_pos, npc_pos)| {
113                        interactor_pos.0.distance_squared(npc_pos.0)
114                            <= MAX_NPCINTERACT_RANGE.powi(2)
115                    })
116            };
117
118            if within_range
119                && let Some(agent) = agents.get_mut(npc_entity)
120                && agent.target.is_none()
121                && let Some(interactor_uid) = uids.get(interactor)
122            {
123                agent.inbox.push_back(AgentEvent::Talk(*interactor_uid));
124            }
125        }
126    }
127}
128
129impl ServerEvent for DialogueEvent {
130    type SystemData<'a> = (
131        ReadStorage<'a, Uid>,
132        ReadStorage<'a, comp::Pos>,
133        ReadStorage<'a, Client>,
134        ReadStorage<'a, common::rtsim::ActorId>,
135        WriteStorage<'a, comp::Agent>,
136        WriteStorage<'a, comp::Inventory>,
137        ReadExpect<'a, AbilityMap>,
138        ReadExpect<'a, MaterialStatManifest>,
139        WriteStorage<'a, comp::InventoryUpdateBuffer>,
140    );
141
142    fn handle(
143        events: impl ExactSizeIterator<Item = Self>,
144        (
145            uids,
146            positions,
147            clients,
148            actors,
149            mut agents,
150            mut inventories,
151            ability_map,
152            msm,
153            mut inventory_update_buffers,
154        ): Self::SystemData<'_>,
155    ) {
156        for DialogueEvent(sender, target, dialogue) in events {
157            // Both have to be actors to perform dialogs.
158            if !(actors.get(sender).is_some() && actors.get(target).is_some()) {
159                continue;
160            }
161
162            // TODO: We check this because there's currently no way on the client to start
163            // a dialog, but a modified client could technically send a dialog message which
164            // comes with a bunch of issues and possible exploits.
165
166            // Both can't be players to perform dialogs.
167            if clients.get(sender).is_some() && clients.get(target).is_some() {
168                continue;
169            }
170
171            let within_range = positions
172                .get(sender)
173                .zip(positions.get(target))
174                .is_some_and(|(sender_pos, target_pos)| {
175                    sender_pos.0.distance_squared(target_pos.0) <= MAX_NPCINTERACT_RANGE.powi(2)
176                });
177
178            if within_range && let Some(sender_uid) = uids.get(sender) {
179                // Perform item transfer, if required
180                let given_item = match &dialogue.kind {
181                    DialogueKind::Start
182                    | DialogueKind::End
183                    | DialogueKind::Question { .. }
184                    | DialogueKind::Marker { .. }
185                    | DialogueKind::Ack { .. } => None,
186                    DialogueKind::Statement { given_item, .. } => given_item.as_ref(),
187                    DialogueKind::Response { response, .. } => response.given_item.as_ref(),
188                };
189                // If the response requires an item to be given, perform exchange (or exit)
190                if let Some((item_def, amount)) = given_item {
191                    // Check that the target's inventory has enough space for the item
192                    if let Some(target_inv) = inventories.get(target)
193                        && target_inv.has_space_for(item_def, *amount)
194                        // Check that the sender has enough of the item
195                        && let Some(mut sender_inv) = inventories.get_mut(sender)
196                        && sender_inv.item_count(item_def) >= *amount as u64
197                        // First, remove the item from the sender's inventory
198                        && let Some(items) = sender_inv.remove_item_amount(item_def, *amount, &ability_map, &msm)
199                        && let Some(mut target_inv) = inventories.get_mut(target)
200                    {
201                        for item in items {
202                            let item_event = InventoryUpdateEvent::Collected(
203                                item.frontend_item(&ability_map, &msm),
204                            );
205                            // Push the items to the target's inventory
206                            if target_inv.push(item).is_err() {
207                                error!(
208                                    "Failed to insert dialogue given item despite target \
209                                     inventory claiming to have space, dropping remaining items..."
210                                );
211                                break;
212                            } else if let Some(buf) = inventory_update_buffers.get_mut(target) {
213                                buf.push(item_event);
214                            }
215                        }
216                    } else {
217                        // TODO: Respond with error message on failure?
218                        continue;
219                    }
220                }
221
222                let dialogue = dialogue.into_validated_unchecked();
223
224                if let Some(agent) = agents.get_mut(target) {
225                    agent
226                        .inbox
227                        .push_back(AgentEvent::Dialogue(*sender_uid, dialogue.clone()));
228                }
229
230                if let Some(client) = clients.get(target) {
231                    client.send_fallible(ServerGeneral::Dialogue(*sender_uid, dialogue));
232                }
233            }
234        }
235    }
236}
237
238impl ServerEvent for SetPetStayEvent {
239    type SystemData<'a> = (
240        WriteStorage<'a, comp::Agent>,
241        WriteStorage<'a, comp::CharacterActivity>,
242        ReadStorage<'a, comp::Pos>,
243        ReadStorage<'a, comp::Alignment>,
244        ReadStorage<'a, Is<Mount>>,
245        ReadStorage<'a, Uid>,
246    );
247
248    fn handle(
249        events: impl ExactSizeIterator<Item = Self>,
250        (mut agents, mut character_activities, positions, alignments, is_mounts, uids): Self::SystemData<'_>,
251    ) {
252        for SetPetStayEvent(command_giver, pet, stay) in events {
253            let is_owner = uids.get(command_giver).is_some_and(|owner_uid| {
254                matches!(
255                    alignments.get(pet),
256                    Some(comp::Alignment::Owned(pet_owner)) if *pet_owner == *owner_uid,
257                )
258            });
259
260            let current_pet_position = positions.get(pet).copied();
261            let stay = stay && current_pet_position.is_some();
262            if is_owner
263                && within_mounting_range(positions.get(command_giver), positions.get(pet))
264                && is_mounts.get(pet).is_none()
265            {
266                character_activities
267                    .get_mut(pet)
268                    .map(|mut activity| activity.is_pet_staying = stay);
269                agents
270                    .get_mut(pet)
271                    .map(|s| s.stay_pos = current_pet_position.filter(|_| stay));
272            }
273        }
274    }
275}
276
277lazy_static! {
278    static ref RESOURCE_EXPERIENCE_MANIFEST: AssetHandle<Ron<HashMap<String, u32>>> =
279        Ron::load_expect_combined_static("server.manifests.resource_experience_manifest");
280}
281
282impl ServerEvent for MineBlockEvent {
283    type SystemData<'a> = (
284        WriteExpect<'a, BlockChange>,
285        ReadExpect<'a, TerrainGrid>,
286        ReadExpect<'a, MaterialStatManifest>,
287        ReadExpect<'a, AbilityMap>,
288        ReadExpect<'a, EventBus<CreateItemDropEvent>>,
289        ReadExpect<'a, EventBus<SoundEvent>>,
290        ReadExpect<'a, EventBus<Outcome>>,
291        ReadExpect<'a, ProgramTime>,
292        ReadExpect<'a, Time>,
293        WriteStorage<'a, comp::SkillSet>,
294        ReadStorage<'a, Uid>,
295    );
296
297    fn handle(
298        events: impl ExactSizeIterator<Item = Self>,
299        (
300            mut block_change,
301            terrain,
302            msm,
303            ability_map,
304            create_item_drop_events,
305            sound_events,
306            outcomes,
307            program_time,
308            time,
309            mut skill_sets,
310            uids,
311        ): Self::SystemData<'_>,
312    ) {
313        use rand::RngExt;
314        let mut rng = rand::rng();
315        let mut create_item_drop_emitter = create_item_drop_events.emitter();
316        let mut sound_event_emitter = sound_events.emitter();
317        let mut outcome_emitter = outcomes.emitter();
318        for ev in events {
319            if block_change.can_set_block(ev.pos) {
320                let block = terrain.get(ev.pos).ok().copied();
321                if let Some(mut block) =
322                    block.filter(|b| b.mine_tool().is_some_and(|t| Some(t) == ev.tool))
323                {
324                    // Attempt to increase the resource's damage
325                    let damage = if let Ok(damage) = block.get_attr::<terrain::sprite::Damage>() {
326                        let updated_damage = damage.0.saturating_add(1);
327                        block
328                            .set_attr(terrain::sprite::Damage(updated_damage))
329                            .expect(
330                                "We just read the Damage attribute from the block, writing should \
331                                 be possible too",
332                            );
333
334                        Some(updated_damage)
335                    } else {
336                        None
337                    };
338
339                    let sprite = block.get_sprite();
340
341                    // Maximum damage has reached, destroy the block
342                    let is_broken = damage
343                        .and_then(|damage| Some((sprite?.required_mine_damage(), damage)))
344                        .is_some_and(|(required_damage, damage)| {
345                            required_damage.is_none_or(|required| damage >= required)
346                        });
347
348                    // Stage changes happen in damage interval of `mine_drop_intevral`
349                    let stage_changed = damage
350                        .and_then(|damage| Some((sprite?.mine_drop_interval(), damage)))
351                        .is_some_and(|(interval, damage)| damage % interval == 0);
352
353                    let sprite_cfg = terrain.sprite_cfg_at(ev.pos);
354                    if (stage_changed || is_broken)
355                        && let Some(items) = comp::Item::try_reclaim_from_block(block, sprite_cfg)
356                    {
357                        let mut items: Vec<_> =
358                            flatten_counted_items(&items, &ability_map, &msm).collect();
359                        let maybe_uid = uids.get(ev.entity).copied();
360
361                        if let Some(mut skillset) = skill_sets.get_mut(ev.entity) {
362                            use common::comp::skills::{MiningSkill, SKILL_MODIFIERS, Skill};
363
364                            if is_broken
365                                && let (Some(tool), Some(uid), exp_reward @ 1..) = (
366                                    ev.tool,
367                                    maybe_uid,
368                                    items
369                                        .iter()
370                                        .filter_map(|item| {
371                                            item.item_definition_id().itemdef_id().and_then(|id| {
372                                                RESOURCE_EXPERIENCE_MANIFEST
373                                                    .read()
374                                                    .0
375                                                    .get(id)
376                                                    .copied()
377                                            })
378                                        })
379                                        .sum(),
380                                )
381                            {
382                                let skill_group = comp::SkillGroupKind::Weapon(tool);
383                                if let Some(level_outcome) =
384                                    skillset.add_experience(skill_group, exp_reward)
385                                {
386                                    outcome_emitter.emit(Outcome::SkillPointGain {
387                                        uid,
388                                        skill_tree: skill_group,
389                                        total_points: level_outcome,
390                                    });
391                                }
392                                outcome_emitter.emit(Outcome::ExpChange {
393                                    uid,
394                                    exp: exp_reward,
395                                    xp_pools: HashSet::from([skill_group]),
396                                });
397                            }
398
399                            let stage_ore_chance = || {
400                                let chance_mod = f64::from(SKILL_MODIFIERS.mining_tree.ore_gain);
401                                let skill_level = skillset
402                                    .skill_level(Skill::Pick(MiningSkill::OreGain))
403                                    .unwrap_or(0);
404
405                                chance_mod * f64::from(skill_level)
406                            };
407                            let stage_gem_chance = || {
408                                let chance_mod = f64::from(SKILL_MODIFIERS.mining_tree.gem_gain);
409                                let skill_level = skillset
410                                    .skill_level(Skill::Pick(MiningSkill::GemGain))
411                                    .unwrap_or(0);
412
413                                chance_mod * f64::from(skill_level)
414                            };
415
416                            // If the resource hasn't been fully broken, only drop certain resources
417                            // with a chance
418                            if !is_broken {
419                                items.retain(|item| {
420                                    rng.random_bool(
421                                        0.5 + item
422                                            .item_definition_id()
423                                            .itemdef_id()
424                                            .map(|id| {
425                                                if id.contains("mineral.ore.") {
426                                                    stage_ore_chance()
427                                                } else if id.contains("mineral.gem.") {
428                                                    stage_gem_chance()
429                                                } else {
430                                                    0.0
431                                                }
432                                            })
433                                            .unwrap_or(0.0),
434                                    )
435                                });
436                            }
437                        }
438                        for item in items {
439                            let loot_owner = maybe_uid.map(LootOwnerKind::Player).map(|owner| {
440                                comp::LootOwner::new(owner, false, ONWERSHIP_TIMEOUT_FAST)
441                            });
442                            create_item_drop_emitter.emit(CreateItemDropEvent {
443                                pos: comp::Pos(ev.pos.map(|e| e as f32) + Vec3::broadcast(0.5)),
444                                vel: comp::Vel(
445                                    Vec2::unit_x()
446                                        .rotated_z(rng.random::<f32>() * PI * 2.0)
447                                        .mul(4.0)
448                                        .with_z(rng.random_range(5.0..10.0)),
449                                ),
450                                ori: comp::Ori::from(Dir::random_2d(&mut rng)),
451                                item: comp::PickupItem::new(item, *program_time, false),
452                                loot_owner,
453                            });
454                        }
455                    }
456
457                    if damage.is_some() && !is_broken {
458                        block_change.set(ev.pos, block);
459                    } else {
460                        block_change.set(ev.pos, block.into_vacant());
461                    }
462                    outcome_emitter.emit(if is_broken {
463                        Outcome::BreakBlock {
464                            pos: ev.pos,
465                            tool: ev.tool,
466                            color: block.get_color(),
467                        }
468                    } else {
469                        Outcome::DamagedBlock {
470                            pos: ev.pos,
471                            stage_changed,
472                            tool: ev.tool,
473                        }
474                    });
475
476                    // Emit mining sound
477                    sound_event_emitter.emit(SoundEvent {
478                        sound: Sound::new(SoundKind::Mine, ev.pos.as_(), 20.0, time.0),
479                    });
480                }
481            }
482        }
483    }
484}
485
486impl ServerEvent for SoundEvent {
487    type SystemData<'a> = (
488        ReadExpect<'a, EventBus<Outcome>>,
489        WriteStorage<'a, comp::Agent>,
490        ReadStorage<'a, comp::Pos>,
491    );
492
493    fn handle(
494        events: impl ExactSizeIterator<Item = Self>,
495        (outcomes, mut agents, positions): Self::SystemData<'_>,
496    ) {
497        let mut outcome_emitter = outcomes.emitter();
498        for SoundEvent { sound } in events {
499            // TODO: Reduce the complexity of this problem by using spatial partitioning
500            // system
501            for (agent, agent_pos) in (&mut agents, &positions).join() {
502                // TODO: Use pathfinding for more dropoff around obstacles
503                let agent_dist_sqrd = agent_pos.0.distance_squared(sound.pos);
504                let sound_travel_dist_sqrd = (sound.vol * SOUND_TRAVEL_DIST_PER_VOLUME).powi(2);
505
506                let vol_dropoff = agent_dist_sqrd / sound_travel_dist_sqrd * sound.vol;
507                let propagated_sound = sound.with_new_vol(sound.vol - vol_dropoff);
508
509                let can_hear_sound = propagated_sound.vol > 0.00;
510                let should_hear_sound = agent_dist_sqrd < agent.psyche.listen_dist.powi(2);
511
512                if can_hear_sound && should_hear_sound {
513                    agent
514                        .inbox
515                        .push_back(AgentEvent::ServerSound(propagated_sound));
516                }
517            }
518
519            // Attempt to turn this sound into an outcome to be received by frontends.
520            if let Some(outcome) = match sound.kind {
521                SoundKind::Utterance(kind, body) => Some(Outcome::Utterance {
522                    kind,
523                    pos: sound.pos,
524                    body,
525                }),
526                _ => None,
527            } {
528                outcome_emitter.emit(outcome);
529            }
530        }
531    }
532}
533
534impl ServerEvent for CreateSpriteEvent {
535    type SystemData<'a> = (
536        WriteExpect<'a, BlockChange>,
537        WriteExpect<'a, ScheduledBlockChange>,
538        ReadExpect<'a, TerrainGrid>,
539        ReadExpect<'a, Time>,
540    );
541
542    fn handle(
543        events: impl ExactSizeIterator<Item = Self>,
544        (mut block_change, mut scheduled_block_change, terrain, time): Self::SystemData<'_>,
545    ) {
546        for ev in events {
547            if block_change.can_set_block(ev.pos) {
548                let block = terrain.get(ev.pos).ok().copied();
549                if block.is_some_and(|b| (*b).is_fluid()) {
550                    let old_block = block.unwrap_or_else(|| Block::air(SpriteKind::Empty));
551                    let new_block = old_block.with_sprite(ev.sprite);
552                    block_change.set(ev.pos, new_block);
553                    // Remove sprite after del_timeout and offset if specified
554                    if let Some((timeout, del_offset)) = ev.del_timeout {
555                        use rand::RngExt;
556                        let mut rng = rand::rng();
557                        let offset = rng.random_range(0.0..del_offset);
558                        let current_time: f64 = time.0;
559                        let replace_time = current_time + (timeout + offset) as f64;
560                        if old_block != new_block {
561                            scheduled_block_change.set(ev.pos, old_block, replace_time);
562                            scheduled_block_change.outcome_set(ev.pos, new_block, replace_time);
563                        }
564                    }
565                }
566            }
567        }
568    }
569}
570
571impl ServerEvent for ToggleSpriteLightEvent {
572    type SystemData<'a> = (
573        WriteExpect<'a, BlockChange>,
574        ReadExpect<'a, TerrainGrid>,
575        ReadStorage<'a, comp::Pos>,
576    );
577
578    fn handle(
579        events: impl ExactSizeIterator<Item = Self>,
580        (mut block_change, terrain, positions): Self::SystemData<'_>,
581    ) {
582        for ev in events.into_iter() {
583            if let Some(entity_pos) = positions.get(ev.entity)
584                && entity_pos.0.distance_squared(ev.pos.as_()) < MAX_INTERACT_RANGE.powi(2)
585                && block_change.can_set_block(ev.pos)
586                && let Some(new_block) = terrain
587                    .get(ev.pos)
588                    .ok()
589                    .and_then(|block| block.with_toggle_light(ev.enable))
590            {
591                block_change.set(ev.pos, new_block);
592                // TODO: Emit outcome
593            }
594        }
595    }
596}
597
598pub fn handle_tame_pet(server: &mut Server, ev: TamePetEvent) {
599    // TODO: Raise outcome to send to clients to play sound/render an indicator
600    // showing taming success?
601    tame_pet(server.state.ecs(), ev.pet_entity, ev.owner_entity);
602}