Skip to main content

veloren_server/sys/agent/
mod.rs

1pub mod behavior_tree;
2use server_agent::data::AgentEvents;
3pub use server_agent::{action_nodes, attack, consts, data, util};
4
5use crate::sys::agent::{
6    behavior_tree::{BehaviorData, BehaviorTree},
7    data::{AgentData, ReadData},
8};
9use common::{
10    comp::{
11        self, Agent, Alignment, Body, CharacterState, Controller, Health, Scale,
12        inventory::slot::EquipSlot, item::ItemDesc,
13    },
14    mounting::Volume,
15    path::TraversalConfig,
16};
17use common_base::prof_span;
18use common_ecs::{Job, Origin, ParMode, Phase, System};
19use rand::rng;
20use rayon::iter::ParallelIterator;
21use specs::{LendJoin, ParJoin, WriteStorage};
22
23/// This system will allow NPCs to modify their controller
24#[derive(Default)]
25pub struct Sys;
26impl<'a> System<'a> for Sys {
27    type SystemData = (
28        ReadData<'a>,
29        AgentEvents<'a>,
30        WriteStorage<'a, Agent>,
31        WriteStorage<'a, Controller>,
32    );
33
34    const NAME: &'static str = "agent";
35    const ORIGIN: Origin = Origin::Server;
36    const PHASE: Phase = Phase::Create;
37
38    fn run(
39        job: &mut Job<Self>,
40        (read_data, events, mut agents, mut controllers): Self::SystemData,
41    ) {
42        job.cpu_stats.measure(ParMode::Rayon);
43
44        (
45            &read_data.entities,
46            (
47                &read_data.energies,
48                read_data.healths.maybe(),
49                read_data.combos.maybe(),
50            ),
51            (
52                &read_data.positions,
53                &read_data.velocities,
54                &read_data.orientations,
55            ),
56            read_data.bodies.maybe(),
57            &read_data.inventories,
58            (
59                &read_data.char_states,
60                &read_data.skill_set,
61                &read_data.active_abilities,
62            ),
63            &read_data.physics_states,
64            &read_data.uids,
65            &mut agents,
66            &mut controllers,
67            read_data.light_emitter.maybe(),
68            read_data.groups.maybe(),
69            read_data.rtsim_actors.maybe(),
70            (
71                read_data.is_mounts.maybe(),
72                read_data.is_riders.maybe(),
73                read_data.is_volume_riders.maybe(),
74            ),
75        )
76            .par_join()
77            .for_each_init(
78                || {
79                    prof_span!(guard, "agent rayon job");
80                    guard
81                },
82                |_guard,
83                 (
84                    entity,
85                    (energy, health, combo),
86                    (pos, vel, ori),
87                    body,
88                    inventory,
89                    (char_state, skill_set, active_abilities),
90                    physics_state,
91                    uid,
92                    agent,
93                    controller,
94                    light_emitter,
95                    group,
96                    rtsim_actor,
97                    (is_mount, is_rider, is_volume_rider),
98                )| {
99                    let mut emitters = events.get_emitters();
100                    let mut rng = rng();
101
102                    if is_mount.is_some() && body.is_none_or(|body| !body.has_free_will()) {
103                        return;
104                    }
105
106                    // The entity that is moving, if riding it's the mount, otherwise it's itself
107                    let moving_entity = is_rider
108                        .and_then(|is_rider| {
109                            let mut mount = is_rider.mount;
110                            // Find the root mount, i.e the one that's doing the moving.
111                            loop {
112                                let e = read_data.id_maps.uid_entity(mount)?;
113
114                                if let Some(is_rider) = read_data.is_riders.get(e) {
115                                    mount = is_rider.mount;
116                                } else {
117                                    return Some(e);
118                                }
119                            }
120                        })
121                        .or_else(|| {
122                            is_volume_rider.and_then(|is_volume_rider| {
123                                match is_volume_rider.pos.kind {
124                                    Volume::Terrain => None,
125                                    Volume::Entity(uid) => read_data.id_maps.uid_entity(uid),
126                                }
127                            })
128                        })
129                        .unwrap_or(entity);
130
131                    let pos = read_data.positions.get(moving_entity).unwrap_or(pos);
132                    let vel = read_data.velocities.get(moving_entity).unwrap_or(vel);
133                    let moving_body = read_data.bodies.get(moving_entity);
134                    let physics_state = read_data
135                        .physics_states
136                        .get(moving_entity)
137                        .unwrap_or(physics_state);
138
139                    // Hack, replace with better system when groups are more sophisticated
140                    // Override alignment if in a group unless entity is owned already
141                    let alignment = if matches!(
142                        &read_data.alignments.get(entity),
143                        &Some(Alignment::Owned(_))
144                    ) {
145                        read_data.alignments.get(entity).copied()
146                    } else {
147                        group
148                            .and_then(|g| read_data.group_manager.group_info(*g))
149                            .and_then(|info| read_data.uids.get(info.leader))
150                            .copied()
151                            .map_or_else(
152                                || read_data.alignments.get(entity).copied(),
153                                |uid| Some(Alignment::Owned(uid)),
154                            )
155                    };
156
157                    if !matches!(
158                        char_state,
159                        CharacterState::LeapMelee(_) | CharacterState::Glide(_)
160                    ) {
161                        // Default to looking in orientation direction
162                        // (can be overridden below)
163                        //
164                        // This definitely breaks LeapMelee, Glide and
165                        // probably not only that, do we really need this at all?
166                        controller.reset();
167                        controller.inputs.look_dir = ori.look_dir();
168                    }
169
170                    let scale = read_data
171                        .scales
172                        .get(moving_entity)
173                        .map_or(1.0, |Scale(s)| *s);
174
175                    let glider_equipped = inventory
176                        .equipped(EquipSlot::Glider)
177                        .as_ref()
178                        .is_some_and(|item| matches!(&*item.kind(), comp::item::ItemKind::Glider));
179
180                    let is_gliding = matches!(
181                        read_data.char_states.get(entity),
182                        Some(CharacterState::GlideWield(_) | CharacterState::Glide(_))
183                    ) && physics_state.on_ground.is_none();
184
185                    // This controls how picky NPCs are about their pathfinding.
186                    // Giants are larger and so can afford to be less precise
187                    // when trying to move around the world
188                    // (especially since they would otherwise get stuck on
189                    // obstacles that smaller entities would not).
190                    let node_tolerance = scale * 1.5;
191                    let slow_factor =
192                        moving_body.map_or(0.0, |b| 1.0 - 1.0 / (1.0 + b.base_accel() * 0.01));
193                    let traversal_config = TraversalConfig {
194                        node_tolerance,
195                        slow_factor,
196                        on_ground: physics_state.on_ground.is_some(),
197                        in_liquid: physics_state.in_liquid().is_some(),
198                        min_tgt_dist: scale * moving_body.map_or(1.0, |body| body.max_radius()),
199                        can_climb: moving_body.is_some_and(Body::can_climb),
200                        can_fly: moving_body.is_some_and(|b| b.fly_thrust().is_some()),
201                        vectored_propulsion: moving_body.is_some_and(|b| b.vectored_propulsion()),
202                        is_target_loaded: true,
203                    };
204                    let health_fraction = health.map_or(1.0, Health::fraction);
205
206                    // Package all this agent's data into a convenient struct
207                    let data = AgentData {
208                        entity: &entity,
209                        rtsim_actor,
210                        uid,
211                        pos,
212                        vel,
213                        ori,
214                        energy,
215                        body,
216                        inventory,
217                        skill_set,
218                        physics_state,
219                        alignment: alignment.as_ref(),
220                        traversal_config,
221                        scale,
222                        damage: health_fraction,
223                        light_emitter,
224                        glider_equipped,
225                        is_gliding,
226                        health: read_data.healths.get(entity),
227                        heads: read_data.heads.get(entity),
228                        char_state,
229                        active_abilities,
230                        combo,
231                        buffs: read_data.buffs.get(entity),
232                        stats: read_data.stats.get(entity),
233                        cached_spatial_grid: &read_data.cached_spatial_grid,
234                        msm: &read_data.msm,
235                        poise: read_data.poises.get(entity),
236                        stance: read_data.stances.get(entity),
237                    };
238
239                    ///////////////////////////////////////////////////////////
240                    // Behavior tree
241                    ///////////////////////////////////////////////////////////
242                    // The behavior tree is meant to make decisions for agents
243                    // *but should not* mutate any data (only action nodes
244                    // should do that). Each path should lead to one (and only
245                    // one) action node. This makes bugfinding much easier and
246                    // debugging way easier. If you don't think so, try
247                    // debugging the agent code before this MR
248                    // (https://gitlab.com/veloren/veloren/-/merge_requests/1801).
249                    // Each tick should arrive at one (1) action node which
250                    // then determines what the agent does. If this makes you
251                    // uncomfortable, consider dt the response time of the
252                    // NPC. To make the tree easier to read, subtrees can be
253                    // created as methods on `AgentData`. Action nodes are
254                    // also methods on the `AgentData` struct. Action nodes
255                    // are the only parts of this tree that should provide
256                    // inputs.
257                    let mut behavior_data = BehaviorData {
258                        agent,
259                        agent_data: data,
260                        read_data: &read_data,
261                        emitters: &mut emitters,
262                        controller,
263                        rng: &mut rng,
264                    };
265
266                    BehaviorTree::root().run(&mut behavior_data);
267
268                    debug_assert!(controller.inputs.move_dir.map(|e| !e.is_nan()).reduce_and());
269                    debug_assert!(controller.inputs.look_dir.map(|e| !e.is_nan()).reduce_and());
270                },
271            );
272    }
273}