veloren_rtsim/data/
npc.rs

1use crate::{
2    ai::Action,
3    data::{Reports, Sentiments, quest::Quest},
4    generate::name,
5};
6pub use common::rtsim::{NpcId, Profession};
7use common::{
8    character::CharacterId,
9    comp::{self, agent::FlightMode, item::ItemDef},
10    grid::Grid,
11    map::Marker,
12    resources::Time,
13    rtsim::{
14        Actor, Dialogue, DialogueId, DialogueKind, FactionId, NpcAction, NpcActivity, NpcInput,
15        NpcMsg, Personality, QuestId, ReportId, Response, Role, SiteId, TerrainResource,
16    },
17    store::Id,
18    terrain::CoordinateConversions,
19    util::Dir,
20};
21use hashbrown::{HashMap, HashSet};
22use rand::prelude::*;
23use serde::{Deserialize, Serialize};
24use slotmap::HopSlotMap;
25use std::{
26    collections::VecDeque,
27    ops::{Deref, DerefMut},
28    sync::{
29        Arc,
30        atomic::{AtomicU32, Ordering},
31    },
32};
33use tracing::error;
34use vek::*;
35use world::{
36    civ::Track,
37    site::Site as WorldSite,
38    util::{LOCALITY, RandomPerm},
39};
40
41#[derive(Copy, Clone, Debug, Default)]
42pub enum SimulationMode {
43    /// The NPC is unloaded and is being simulated via rtsim.
44    #[default]
45    Simulated,
46    /// The NPC has been loaded into the game world as an ECS entity.
47    Loaded,
48}
49
50#[derive(Clone)]
51pub struct PathData<P, N> {
52    pub end: N,
53    pub path: VecDeque<P>,
54    pub repoll: bool,
55}
56
57#[derive(Clone, Default)]
58pub struct PathingMemory {
59    pub intrasite_path: Option<(PathData<Vec2<i32>, Vec2<i32>>, Id<WorldSite>)>,
60    pub intersite_path: Option<(PathData<(Id<Track>, bool), SiteId>, usize)>,
61}
62
63#[derive(Default)]
64pub struct Controller {
65    pub actions: Vec<NpcAction>,
66    pub activity: Option<NpcActivity>,
67    pub new_home: Option<Option<SiteId>>,
68    pub look_dir: Option<Dir>,
69    pub job: Option<Job>,
70    pub quests_to_create: Vec<(QuestId, Quest)>,
71}
72
73impl Controller {
74    /// Reset the controller to a neutral state before the start of the next
75    /// brain tick.
76    pub fn reset(&mut self, npc: &Npc) {
77        self.activity = None;
78        self.look_dir = None;
79        self.job = npc.job.clone();
80    }
81
82    pub fn do_idle(&mut self) { self.activity = None; }
83
84    pub fn do_talk(&mut self, tgt: Actor) { self.activity = Some(NpcActivity::Talk(tgt)); }
85
86    pub fn do_goto(&mut self, wpos: Vec3<f32>, speed_factor: f32) {
87        self.activity = Some(NpcActivity::Goto(wpos, speed_factor));
88    }
89
90    /// go to with height above terrain and direction
91    pub fn do_goto_with_height_and_dir(
92        &mut self,
93        wpos: Vec3<f32>,
94        speed_factor: f32,
95        height: Option<f32>,
96        dir: Option<Dir>,
97        flight_mode: FlightMode,
98    ) {
99        self.activity = Some(NpcActivity::GotoFlying(
100            wpos,
101            speed_factor,
102            height,
103            dir,
104            flight_mode,
105        ));
106    }
107
108    pub fn do_gather(&mut self, resources: &'static [TerrainResource]) {
109        self.activity = Some(NpcActivity::Gather(resources));
110    }
111
112    pub fn do_hunt_animals(&mut self) { self.activity = Some(NpcActivity::HuntAnimals); }
113
114    pub fn do_dance(&mut self, dir: Option<Dir>) { self.activity = Some(NpcActivity::Dance(dir)); }
115
116    pub fn do_cheer(&mut self, dir: Option<Dir>) { self.activity = Some(NpcActivity::Cheer(dir)); }
117
118    pub fn do_sit(&mut self, dir: Option<Dir>, pos: Option<Vec3<i32>>) {
119        self.activity = Some(NpcActivity::Sit(dir, pos));
120    }
121
122    pub fn say(&mut self, target: impl Into<Option<Actor>>, content: comp::Content) {
123        self.actions.push(NpcAction::Say(target.into(), content));
124    }
125
126    pub fn attack(&mut self, target: impl Into<Actor>) {
127        self.actions.push(NpcAction::Attack(target.into()));
128    }
129
130    pub fn set_new_home(&mut self, new_home: impl Into<Option<SiteId>>) {
131        self.new_home = Some(new_home.into());
132    }
133
134    pub fn set_newly_hired(&mut self, actor: Actor, expires: Time) {
135        self.job = Some(Job::Hired(actor, expires));
136    }
137
138    pub fn end_hiring(&mut self) {
139        if matches!(self.job, Some(Job::Hired(..))) {
140            self.job = None;
141        }
142    }
143
144    pub fn end_quest(&mut self) {
145        if matches!(self.job, Some(Job::Quest(..))) {
146            self.job = None;
147        }
148    }
149
150    pub fn send_msg(&mut self, to: impl Into<Actor>, msg: NpcMsg) {
151        self.actions.push(NpcAction::Msg { to: to.into(), msg });
152    }
153
154    /// Start a new dialogue.
155    pub fn dialogue_start(&mut self, target: impl Into<Actor>) -> DialogueSession {
156        let target = target.into();
157
158        let session = DialogueSession {
159            target,
160            id: DialogueId(rand::rng().random()),
161        };
162
163        self.actions.push(NpcAction::Dialogue(target, Dialogue {
164            id: session.id,
165            kind: DialogueKind::Start,
166        }));
167
168        session
169    }
170
171    /// End an existing dialogue.
172    pub fn dialogue_end(&mut self, session: DialogueSession) {
173        self.actions
174            .push(NpcAction::Dialogue(session.target, Dialogue {
175                id: session.id,
176                kind: DialogueKind::End,
177            }));
178    }
179
180    pub fn dialogue_response(
181        &mut self,
182        session: DialogueSession,
183        tag: u32,
184        response: &(u16, Response),
185    ) {
186        self.actions
187            .push(NpcAction::Dialogue(session.target, Dialogue {
188                id: session.id,
189                kind: DialogueKind::Response {
190                    tag,
191                    response: response.1.clone(),
192                    response_id: response.0,
193                },
194            }));
195    }
196
197    fn new_dialogue_tag(&self) -> u32 {
198        static TAG_COUNTER: AtomicU32 = AtomicU32::new(0);
199        TAG_COUNTER.fetch_add(1, Ordering::Relaxed)
200    }
201
202    /// Ask a question, with various possible answers. Returns the dialogue tag,
203    /// used for identifying the answer.
204    pub fn dialogue_question(
205        &mut self,
206        session: DialogueSession,
207        msg: comp::Content,
208        responses: impl IntoIterator<Item = (u16, Response)>,
209    ) -> u32 {
210        let tag = self.new_dialogue_tag();
211
212        self.actions
213            .push(NpcAction::Dialogue(session.target, Dialogue {
214                id: session.id,
215                kind: DialogueKind::Question {
216                    tag,
217                    msg,
218                    responses: responses.into_iter().collect(),
219                },
220            }));
221
222        tag
223    }
224
225    /// Provide a statement as part of a dialogue. Returns the dialogue tag,
226    /// used for identifying acknowledgements.
227    pub fn dialogue_statement(
228        &mut self,
229        session: DialogueSession,
230        msg: comp::Content,
231        given_item: Option<(Arc<ItemDef>, u32)>,
232    ) -> u32 {
233        let tag = self.new_dialogue_tag();
234
235        self.actions
236            .push(NpcAction::Dialogue(session.target, Dialogue {
237                id: session.id,
238                kind: DialogueKind::Statement {
239                    msg,
240                    given_item,
241                    tag,
242                },
243            }));
244
245        tag
246    }
247
248    /// Provide a location marker as part of a dialogue.
249    pub fn dialogue_marker(&mut self, session: DialogueSession, marker: Marker) {
250        self.actions
251            .push(NpcAction::Dialogue(session.target, Dialogue {
252                id: session.id,
253                kind: DialogueKind::Marker(marker),
254            }));
255    }
256}
257
258// Represents an ongoing dialogue with another actor.
259#[derive(Copy, Clone)]
260pub struct DialogueSession {
261    pub target: Actor,
262    pub id: DialogueId,
263}
264
265pub struct Brain {
266    pub action: Box<dyn Action<(), !>>,
267}
268
269#[derive(Serialize, Deserialize)]
270pub struct Npc {
271    pub uid: u64,
272    // Persisted state
273    pub seed: u32,
274    /// Represents the location of the NPC.
275    pub wpos: Vec3<f32>,
276    pub dir: Vec2<f32>,
277
278    pub body: comp::Body,
279    pub role: Role,
280    pub home: Option<SiteId>,
281    pub faction: Option<FactionId>,
282    /// The current health of the NPC, < 0.0 is dead and 1.0 is max.
283    pub health_fraction: f32,
284
285    /// The [`crate::data::Report`]s that the NPC is aware of.
286    pub known_reports: HashSet<ReportId>,
287
288    #[serde(default)]
289    pub personality: Personality,
290    #[serde(default)]
291    pub sentiments: Sentiments,
292
293    #[serde(default)]
294    pub job: Option<Job>,
295
296    // Unpersisted state
297    #[serde(skip)]
298    pub chunk_pos: Option<Vec2<i32>>,
299    #[serde(skip)]
300    pub current_site: Option<SiteId>,
301
302    #[serde(skip)]
303    pub controller: Controller,
304    #[serde(skip)]
305    pub inbox: VecDeque<NpcInput>,
306
307    /// Whether the NPC is in simulated or loaded mode (when rtsim is run on the
308    /// server, loaded corresponds to being within a loaded chunk). When in
309    /// loaded mode, the interactions of the NPC should not be simulated but
310    /// should instead be derived from the game.
311    #[serde(skip)]
312    pub mode: SimulationMode,
313
314    #[serde(skip)]
315    pub brain: Option<Brain>,
316}
317
318/// A job is a long-running, persistent, non-stackable occupation that an NPC
319/// must persistently attend to, but may be temporarily interrupted from. NPCs
320/// will recurrently attempt to perform tasks that relate to their job.
321#[derive(Clone, Serialize, Deserialize, PartialEq)]
322pub enum Job {
323    /// An NPC can temporarily become a hired hand (`(hiring_actor,
324    /// termination_time)`).
325    Hired(Actor, Time),
326    /// NPC is helping to perform a quest
327    Quest(QuestId),
328}
329
330impl Clone for Npc {
331    fn clone(&self) -> Self {
332        Self {
333            uid: self.uid,
334            seed: self.seed,
335            wpos: self.wpos,
336            dir: self.dir,
337            role: self.role.clone(),
338            home: self.home,
339            faction: self.faction,
340            health_fraction: self.health_fraction,
341            known_reports: self.known_reports.clone(),
342            body: self.body,
343            personality: self.personality,
344            sentiments: self.sentiments.clone(),
345            job: self.job.clone(),
346            // Not persisted
347            chunk_pos: None,
348            current_site: Default::default(),
349            controller: Default::default(),
350            inbox: Default::default(),
351            mode: Default::default(),
352            brain: Default::default(),
353        }
354    }
355}
356
357impl Npc {
358    pub const PERM_ENTITY_CONFIG: u32 = 1;
359    const PERM_NAME: u32 = 0;
360
361    pub fn new(seed: u32, wpos: Vec3<f32>, body: comp::Body, role: Role) -> Self {
362        Self {
363            // To be assigned later
364            uid: 0,
365            seed,
366            wpos,
367            dir: Vec2::unit_x(),
368            body,
369            personality: Default::default(),
370            sentiments: Default::default(),
371            job: None,
372            role,
373            home: None,
374            faction: None,
375            health_fraction: 1.0,
376            known_reports: Default::default(),
377            chunk_pos: None,
378            current_site: None,
379            controller: Default::default(),
380            inbox: Default::default(),
381            mode: SimulationMode::Simulated,
382            brain: None,
383        }
384    }
385
386    pub fn is_dead(&self) -> bool { self.health_fraction <= 0.0 }
387
388    // TODO: have a dedicated `NpcBuilder` type for this.
389    pub fn with_personality(mut self, personality: Personality) -> Self {
390        self.personality = personality;
391        self
392    }
393
394    // // TODO: have a dedicated `NpcBuilder` type for this.
395    // pub fn with_profession(mut self, profession: impl Into<Option<Profession>>)
396    // -> Self {     if let Role::Humanoid(p) = &mut self.role {
397    //         *p = profession.into();
398    //     } else {
399    //         panic!("Tried to assign profession {:?} to NPC, but has role {:?},
400    // which cannot have a profession", profession.into(), self.role);     }
401    //     self
402    // }
403
404    // TODO: have a dedicated `NpcBuilder` type for this.
405    pub fn with_home(mut self, home: impl Into<Option<SiteId>>) -> Self {
406        self.home = home.into();
407        self
408    }
409
410    // TODO: have a dedicated `NpcBuilder` type for this.
411    pub fn with_faction(mut self, faction: impl Into<Option<FactionId>>) -> Self {
412        self.faction = faction.into();
413        self
414    }
415
416    pub fn rng(&self, perm: u32) -> impl Rng + use<> {
417        RandomPerm::new(self.seed.wrapping_add(perm))
418    }
419
420    // TODO: Don't make this depend on deterministic RNG, actually persist names
421    // once we've decided that we want to
422    pub fn get_name(&self) -> Option<String> {
423        if let comp::Body::Humanoid(_) = &self.body {
424            Some(name::generate_npc(&mut self.rng(Self::PERM_NAME)))
425        } else {
426            None
427        }
428    }
429
430    pub fn profession(&self) -> Option<Profession> {
431        match &self.role {
432            Role::Civilised(profession) => *profession,
433            Role::Monster | Role::Wild | Role::Vehicle => None,
434        }
435    }
436
437    pub fn hired(&self) -> Option<(Actor, Time)> {
438        if let Some(Job::Hired(actor, time)) = self.job {
439            Some((actor, time))
440        } else {
441            None
442        }
443    }
444
445    pub fn cleanup(&mut self, reports: &Reports) {
446        // Clear old or superfluous sentiments
447        // TODO: It might be worth giving more important NPCs a higher sentiment
448        // 'budget' than less important ones.
449        self.sentiments
450            .cleanup(crate::data::sentiment::NPC_MAX_SENTIMENTS);
451        // Clear reports that have been forgotten
452        self.known_reports
453            .retain(|report| reports.contains_key(*report));
454        // TODO: Limit number of reports
455        // TODO: Clear old inbox items
456    }
457}
458
459#[derive(Default, Clone, Serialize, Deserialize)]
460pub struct GridCell {
461    pub npcs: Vec<NpcId>,
462}
463
464#[derive(Clone, Serialize, Deserialize, Debug)]
465pub struct NpcLink {
466    pub mount: NpcId,
467    pub rider: Actor,
468    pub is_steering: bool,
469}
470
471#[derive(Clone, Default, Serialize, Deserialize)]
472struct Riders {
473    steerer: Option<MountId>,
474    riders: Vec<MountId>,
475}
476
477#[derive(Clone, Default, Serialize, Deserialize)]
478#[serde(
479    from = "HopSlotMap<MountId, NpcLink>",
480    into = "HopSlotMap<MountId, NpcLink>"
481)]
482pub struct NpcLinks {
483    links: HopSlotMap<MountId, NpcLink>,
484    mount_map: slotmap::SecondaryMap<NpcId, Riders>,
485    rider_map: HashMap<Actor, MountId>,
486}
487
488impl NpcLinks {
489    pub fn remove_mount(&mut self, mount: NpcId) {
490        if let Some(riders) = self.mount_map.remove(mount) {
491            for link in riders
492                .riders
493                .into_iter()
494                .chain(riders.steerer)
495                .filter_map(|link_id| self.links.get(link_id))
496            {
497                self.rider_map.remove(&link.rider);
498            }
499        }
500    }
501
502    /// Internal function, only removes from `mount_map`.
503    fn remove_rider(&mut self, id: MountId, link: &NpcLink) {
504        if let Some(riders) = self.mount_map.get_mut(link.mount) {
505            if link.is_steering && riders.steerer == Some(id) {
506                riders.steerer = None;
507            } else if let Some((i, _)) = riders.riders.iter().enumerate().find(|(_, i)| **i == id) {
508                riders.riders.remove(i);
509            }
510
511            if riders.steerer.is_none() && riders.riders.is_empty() {
512                self.mount_map.remove(link.mount);
513            }
514        }
515    }
516
517    pub fn remove_link(&mut self, link_id: MountId) {
518        if let Some(link) = self.links.remove(link_id) {
519            self.rider_map.remove(&link.rider);
520            self.remove_rider(link_id, &link);
521        }
522    }
523
524    pub fn dismount(&mut self, rider: impl Into<Actor>) {
525        if let Some(id) = self.rider_map.remove(&rider.into())
526            && let Some(link) = self.links.remove(id)
527        {
528            self.remove_rider(id, &link);
529        }
530    }
531
532    // This is the only function to actually add a mount link.
533    // And it ensures that there isn't link chaining
534    pub fn add_mounting(
535        &mut self,
536        mount: NpcId,
537        rider: impl Into<Actor>,
538        steering: bool,
539    ) -> Result<MountId, MountingError> {
540        let rider = rider.into();
541        if Actor::Npc(mount) == rider {
542            return Err(MountingError::MountSelf);
543        }
544        if let Actor::Npc(rider) = rider
545            && self.mount_map.contains_key(rider)
546        {
547            return Err(MountingError::RiderIsMounted);
548        }
549        if self.rider_map.contains_key(&Actor::Npc(mount)) {
550            return Err(MountingError::MountIsRiding);
551        }
552        if let Some(mount_entry) = self.mount_map.entry(mount) {
553            if let hashbrown::hash_map::Entry::Vacant(rider_entry) = self.rider_map.entry(rider) {
554                let riders = mount_entry.or_insert(Riders::default());
555
556                if steering {
557                    if riders.steerer.is_none() {
558                        let id = self.links.insert(NpcLink {
559                            mount,
560                            rider,
561                            is_steering: true,
562                        });
563                        riders.steerer = Some(id);
564                        rider_entry.insert(id);
565                        Ok(id)
566                    } else {
567                        Err(MountingError::HasSteerer)
568                    }
569                } else {
570                    // TODO: Maybe have some limit on the number of riders depending on the mount?
571                    let id = self.links.insert(NpcLink {
572                        mount,
573                        rider,
574                        is_steering: false,
575                    });
576                    riders.riders.push(id);
577                    rider_entry.insert(id);
578                    Ok(id)
579                }
580            } else {
581                Err(MountingError::AlreadyRiding)
582            }
583        } else {
584            Err(MountingError::MountDead)
585        }
586    }
587
588    pub fn steer(
589        &mut self,
590        mount: NpcId,
591        rider: impl Into<Actor>,
592    ) -> Result<MountId, MountingError> {
593        self.add_mounting(mount, rider, true)
594    }
595
596    pub fn ride(
597        &mut self,
598        mount: NpcId,
599        rider: impl Into<Actor>,
600    ) -> Result<MountId, MountingError> {
601        self.add_mounting(mount, rider, false)
602    }
603
604    pub fn get_mount_link(&self, rider: impl Into<Actor>) -> Option<&NpcLink> {
605        self.rider_map
606            .get(&rider.into())
607            .and_then(|link| self.links.get(*link))
608    }
609
610    pub fn get_steerer_link(&self, mount: NpcId) -> Option<&NpcLink> {
611        self.mount_map
612            .get(mount)
613            .and_then(|mount| self.links.get(mount.steerer?))
614    }
615
616    pub fn get(&self, id: MountId) -> Option<&NpcLink> { self.links.get(id) }
617
618    pub fn ids(&self) -> impl Iterator<Item = MountId> + '_ { self.links.keys() }
619
620    pub fn iter(&self) -> impl Iterator<Item = &NpcLink> + '_ { self.links.values() }
621
622    pub fn iter_mounts(&self) -> impl Iterator<Item = NpcId> + '_ { self.mount_map.keys() }
623}
624
625impl From<HopSlotMap<MountId, NpcLink>> for NpcLinks {
626    fn from(mut value: HopSlotMap<MountId, NpcLink>) -> Self {
627        let mut from_map = slotmap::SecondaryMap::new();
628        let mut to_map = HashMap::with_capacity(value.len());
629        let mut delete = Vec::new();
630        for (id, link) in value.iter() {
631            if let Some(entry) = from_map.entry(link.mount) {
632                let riders = entry.or_insert(Riders::default());
633                if link.is_steering {
634                    if let Some(old) = riders.steerer.replace(id) {
635                        error!("Replaced steerer {old:?} with {id:?}");
636                    }
637                } else {
638                    riders.riders.push(id);
639                }
640            } else {
641                delete.push(id);
642            }
643            to_map.insert(link.rider, id);
644        }
645        for id in delete {
646            value.remove(id);
647        }
648        Self {
649            links: value,
650            mount_map: from_map,
651            rider_map: to_map,
652        }
653    }
654}
655
656impl From<NpcLinks> for HopSlotMap<MountId, NpcLink> {
657    fn from(other: NpcLinks) -> Self { other.links }
658}
659slotmap::new_key_type! {
660    pub struct MountId;
661}
662
663#[derive(Clone, Serialize, Deserialize)]
664pub struct MountData {
665    is_steering: bool,
666}
667
668#[derive(Clone, Serialize, Deserialize)]
669pub struct Npcs {
670    pub uid_counter: u64,
671    pub npcs: HopSlotMap<NpcId, Npc>,
672    pub mounts: NpcLinks,
673    // TODO: This feels like it should be its own rtsim resource
674    // TODO: Consider switching to `common::util::SpatialGrid` instead
675    #[serde(skip, default = "construct_npc_grid")]
676    pub npc_grid: Grid<GridCell>,
677    #[serde(skip)]
678    pub character_map: HashMap<Vec2<i32>, Vec<(CharacterId, Vec3<f32>)>>,
679}
680
681impl Default for Npcs {
682    fn default() -> Self {
683        Self {
684            uid_counter: 0,
685            npcs: Default::default(),
686            mounts: Default::default(),
687            npc_grid: construct_npc_grid(),
688            character_map: Default::default(),
689        }
690    }
691}
692
693fn construct_npc_grid() -> Grid<GridCell> { Grid::new(Vec2::zero(), Default::default()) }
694
695#[derive(Debug)]
696pub enum MountingError {
697    MountDead,
698    RiderDead,
699    HasSteerer,
700    AlreadyRiding,
701    MountIsRiding,
702    RiderIsMounted,
703    MountSelf,
704}
705
706impl Npcs {
707    pub fn create_npc(&mut self, mut npc: Npc) -> NpcId {
708        npc.uid = self.uid_counter;
709        self.uid_counter += 1;
710        self.npcs.insert(npc)
711    }
712
713    /// Queries nearby npcs, not garantueed to work if radius > 32.0
714    // TODO: Find a more efficient way to implement this, it's currently
715    // (theoretically) O(n^2).
716    pub fn nearby(
717        &self,
718        this_npc: Option<NpcId>,
719        wpos: Vec3<f32>,
720        radius: f32,
721    ) -> impl Iterator<Item = Actor> + '_ {
722        let chunk_pos = wpos.xy().as_().wpos_to_cpos();
723        let r_sqr = radius * radius;
724        LOCALITY
725            .into_iter()
726            .flat_map(move |neighbor| {
727                self.npc_grid.get(chunk_pos + neighbor).map(move |cell| {
728                    cell.npcs
729                        .iter()
730                        .copied()
731                        .filter(move |npc| {
732                            self.npcs
733                                .get(*npc)
734                                .is_some_and(|npc| npc.wpos.distance_squared(wpos) < r_sqr)
735                                && Some(*npc) != this_npc
736                        })
737                        .map(Actor::Npc)
738                })
739            })
740            .flatten()
741            .chain(
742                self.character_map
743                    .get(&chunk_pos)
744                    .map(|characters| {
745                        characters.iter().filter_map(move |(character, c_wpos)| {
746                            if c_wpos.distance_squared(wpos) < r_sqr {
747                                Some(Actor::Character(*character))
748                            } else {
749                                None
750                            }
751                        })
752                    })
753                    .into_iter()
754                    .flatten(),
755            )
756    }
757}
758
759impl Deref for Npcs {
760    type Target = HopSlotMap<NpcId, Npc>;
761
762    fn deref(&self) -> &Self::Target { &self.npcs }
763}
764
765impl DerefMut for Npcs {
766    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.npcs }
767}