veloren_rtsim/data/
npc.rs

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