veloren_server/rtsim/
tick.rs

1use super::*;
2use crate::sys::terrain::SpawnEntityData;
3use common::{
4    LoadoutBuilder,
5    calendar::Calendar,
6    comp::{self, Body, Presence, PresenceKind},
7    event::{CreateNpcEvent, CreateShipEvent, DeleteEvent, EventBus, NpcBuilder},
8    generation::{BodyBuilder, EntityConfig, EntityInfo},
9    resources::{DeltaTime, Time, TimeOfDay},
10    rtsim::{Actor, NpcId, RtSimEntity},
11    slowjob::SlowJobPool,
12    terrain::CoordinateConversions,
13    trade::{Good, SiteInformation},
14    util::Dir,
15};
16use common_ecs::{Job, Origin, Phase, System};
17use rtsim::data::{
18    Npc, Sites,
19    npc::{Profession, SimulationMode},
20};
21use specs::{Entities, Join, LendJoin, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage};
22use std::{sync::Arc, time::Duration};
23use tracing::error;
24use world::site::settlement::trader_loadout;
25
26fn humanoid_config(profession: &Profession) -> &'static str {
27    match profession {
28        Profession::Farmer => "common.entity.village.farmer",
29        Profession::Hunter => "common.entity.village.hunter",
30        Profession::Herbalist => "common.entity.village.herbalist",
31        Profession::Captain => "common.entity.village.captain",
32        Profession::Merchant => "common.entity.village.merchant",
33        Profession::Guard => "common.entity.village.guard",
34        Profession::Adventurer(rank) => match rank {
35            0 => "common.entity.world.traveler0",
36            1 => "common.entity.world.traveler1",
37            2 => "common.entity.world.traveler2",
38            3 => "common.entity.world.traveler3",
39            _ => {
40                error!(
41                    "Tried to get configuration for invalid adventurer rank {}",
42                    rank
43                );
44                "common.entity.world.traveler3"
45            },
46        },
47        Profession::Blacksmith => "common.entity.village.blacksmith",
48        Profession::Chef => "common.entity.village.chef",
49        Profession::Alchemist => "common.entity.village.alchemist",
50        Profession::Pirate => "common.entity.spot.pirate",
51        Profession::Cultist => "common.entity.dungeon.cultist.cultist",
52    }
53}
54
55fn loadout_default(
56    loadout: LoadoutBuilder,
57    _economy: Option<&SiteInformation>,
58    _time: Option<&(TimeOfDay, Calendar)>,
59) -> LoadoutBuilder {
60    loadout
61}
62
63fn merchant_loadout(
64    loadout_builder: LoadoutBuilder,
65    economy: Option<&SiteInformation>,
66    _time: Option<&(TimeOfDay, Calendar)>,
67) -> LoadoutBuilder {
68    trader_loadout(loadout_builder, economy, |_| true)
69}
70
71fn farmer_loadout(
72    loadout_builder: LoadoutBuilder,
73    economy: Option<&SiteInformation>,
74    _time: Option<&(TimeOfDay, Calendar)>,
75) -> LoadoutBuilder {
76    trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food))
77}
78
79fn herbalist_loadout(
80    loadout_builder: LoadoutBuilder,
81    economy: Option<&SiteInformation>,
82    _time: Option<&(TimeOfDay, Calendar)>,
83) -> LoadoutBuilder {
84    trader_loadout(loadout_builder, economy, |good| {
85        matches!(good, Good::Ingredients)
86    })
87}
88
89fn chef_loadout(
90    loadout_builder: LoadoutBuilder,
91    economy: Option<&SiteInformation>,
92    _time: Option<&(TimeOfDay, Calendar)>,
93) -> LoadoutBuilder {
94    trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food))
95}
96
97fn blacksmith_loadout(
98    loadout_builder: LoadoutBuilder,
99    economy: Option<&SiteInformation>,
100    _time: Option<&(TimeOfDay, Calendar)>,
101) -> LoadoutBuilder {
102    trader_loadout(loadout_builder, economy, |good| {
103        matches!(good, Good::Tools | Good::Armor)
104    })
105}
106
107fn alchemist_loadout(
108    loadout_builder: LoadoutBuilder,
109    economy: Option<&SiteInformation>,
110    _time: Option<&(TimeOfDay, Calendar)>,
111) -> LoadoutBuilder {
112    trader_loadout(loadout_builder, economy, |good| {
113        matches!(good, Good::Potions)
114    })
115}
116
117fn profession_extra_loadout(
118    profession: Option<&Profession>,
119) -> fn(
120    LoadoutBuilder,
121    Option<&SiteInformation>,
122    time: Option<&(TimeOfDay, Calendar)>,
123) -> LoadoutBuilder {
124    match profession {
125        Some(Profession::Merchant) => merchant_loadout,
126        Some(Profession::Farmer) => farmer_loadout,
127        Some(Profession::Herbalist) => herbalist_loadout,
128        Some(Profession::Chef) => chef_loadout,
129        Some(Profession::Blacksmith) => blacksmith_loadout,
130        Some(Profession::Alchemist) => alchemist_loadout,
131        _ => loadout_default,
132    }
133}
134
135fn profession_agent_mark(profession: Option<&Profession>) -> Option<comp::agent::Mark> {
136    match profession {
137        Some(
138            Profession::Merchant
139            | Profession::Farmer
140            | Profession::Herbalist
141            | Profession::Chef
142            | Profession::Blacksmith
143            | Profession::Alchemist,
144        ) => Some(comp::agent::Mark::Merchant),
145        Some(Profession::Guard) => Some(comp::agent::Mark::Guard),
146        _ => None,
147    }
148}
149
150fn get_npc_entity_info(
151    npc: &Npc,
152    sites: &Sites,
153    index: IndexRef,
154    time: Option<&(TimeOfDay, Calendar)>,
155) -> EntityInfo {
156    let pos = comp::Pos(npc.wpos);
157
158    let mut rng = npc.rng(Npc::PERM_ENTITY_CONFIG);
159    if let Some(profession) = npc.profession() {
160        let economy = npc.home.and_then(|home| {
161            let site = sites.get(home)?.world_site?;
162            index.sites.get(site).trade_information(site.id())
163        });
164
165        let config_asset = humanoid_config(&profession);
166
167        let entity_config = EntityConfig::from_asset_expect_owned(config_asset)
168            .with_body(BodyBuilder::Exact(npc.body));
169        EntityInfo::at(pos.0)
170            .with_entity_config(entity_config, Some(config_asset), &mut rng, time)
171            .with_alignment(if matches!(profession, Profession::Cultist) {
172                comp::Alignment::Enemy
173            } else {
174                comp::Alignment::Npc
175            })
176            .with_economy(economy.as_ref())
177            .with_lazy_loadout(profession_extra_loadout(Some(&profession)))
178            .with_alias(npc.get_name())
179            .with_agent_mark(profession_agent_mark(Some(&profession)))
180    } else {
181        let config_asset = match npc.body {
182            Body::BirdLarge(body) => match body.species {
183                comp::bird_large::Species::Phoenix => "common.entity.wild.aggressive.phoenix",
184                comp::bird_large::Species::Cockatrice => "common.entity.wild.aggressive.cockatrice",
185                comp::bird_large::Species::Roc => "common.entity.wild.aggressive.roc",
186                comp::bird_large::Species::CloudWyvern => {
187                    "common.entity.wild.aggressive.cloudwyvern"
188                },
189                comp::bird_large::Species::FlameWyvern => {
190                    "common.entity.wild.aggressive.flamewyvern"
191                },
192                comp::bird_large::Species::FrostWyvern => {
193                    "common.entity.wild.aggressive.frostwyvern"
194                },
195                comp::bird_large::Species::SeaWyvern => "common.entity.wild.aggressive.seawyvern",
196                comp::bird_large::Species::WealdWyvern => {
197                    "common.entity.wild.aggressive.wealdwyvern"
198                },
199            },
200            Body::BipedLarge(body) => match body.species {
201                comp::biped_large::Species::Ogre => "common.entity.wild.aggressive.ogre",
202                comp::biped_large::Species::Cyclops => "common.entity.wild.aggressive.cyclops",
203                comp::biped_large::Species::Wendigo => "common.entity.wild.aggressive.wendigo",
204                comp::biped_large::Species::Werewolf => "common.entity.wild.aggressive.werewolf",
205                comp::biped_large::Species::Cavetroll => "common.entity.wild.aggressive.cave_troll",
206                comp::biped_large::Species::Mountaintroll => {
207                    "common.entity.wild.aggressive.mountain_troll"
208                },
209                comp::biped_large::Species::Swamptroll => {
210                    "common.entity.wild.aggressive.swamp_troll"
211                },
212                comp::biped_large::Species::Blueoni => "common.entity.wild.aggressive.blue_oni",
213                comp::biped_large::Species::Redoni => "common.entity.wild.aggressive.red_oni",
214                comp::biped_large::Species::Tursus => "common.entity.wild.aggressive.tursus",
215                comp::biped_large::Species::Gigasfrost => {
216                    "common.entity.world.world_bosses.gigas_frost"
217                },
218                species => unimplemented!("rtsim spawning for {:?}", species),
219            },
220            body => unimplemented!("rtsim spawning for {:?}", body),
221        };
222        let entity_config = EntityConfig::from_asset_expect_owned(config_asset)
223            .with_body(BodyBuilder::Exact(npc.body));
224
225        EntityInfo::at(pos.0).with_entity_config(entity_config, Some(config_asset), &mut rng, time)
226    }
227}
228
229#[derive(Default)]
230pub struct Sys;
231impl<'a> System<'a> for Sys {
232    type SystemData = (
233        Entities<'a>,
234        Read<'a, DeltaTime>,
235        Read<'a, Time>,
236        Read<'a, TimeOfDay>,
237        Read<'a, EventBus<CreateShipEvent>>,
238        Read<'a, EventBus<CreateNpcEvent>>,
239        Read<'a, EventBus<DeleteEvent>>,
240        WriteExpect<'a, RtSim>,
241        ReadExpect<'a, Arc<world::World>>,
242        ReadExpect<'a, world::IndexOwned>,
243        ReadExpect<'a, SlowJobPool>,
244        ReadStorage<'a, comp::Pos>,
245        ReadStorage<'a, RtSimEntity>,
246        WriteStorage<'a, comp::Agent>,
247        ReadStorage<'a, Presence>,
248        ReadExpect<'a, Calendar>,
249        <rtsim::OnTick as rtsim::Event>::SystemData<'a>,
250    );
251
252    const NAME: &'static str = "rtsim::tick";
253    const ORIGIN: Origin = Origin::Server;
254    const PHASE: Phase = Phase::Create;
255
256    fn run(
257        _job: &mut Job<Self>,
258        (
259            entities,
260            dt,
261            time,
262            time_of_day,
263            create_ship_events,
264            create_npc_events,
265            delete_events,
266            mut rtsim,
267            world,
268            index,
269            slow_jobs,
270            positions,
271            rtsim_entities,
272            mut agents,
273            presences,
274            calendar,
275            mut tick_data,
276        ): Self::SystemData,
277    ) {
278        let mut create_ship_emitter = create_ship_events.emitter();
279        let mut create_npc_emitter = create_npc_events.emitter();
280        let mut delete_emitter = delete_events.emitter();
281        let rtsim = &mut *rtsim;
282        let calendar_data = (*time_of_day, (*calendar).clone());
283
284        // Set up rtsim inputs
285        {
286            let mut data = rtsim.state.data_mut();
287
288            // Update time of day
289            data.time_of_day = *time_of_day;
290
291            // Update character map (i.e: so that rtsim knows where players are)
292            // TODO: Other entities too like animals? Or do we now care about that?
293            data.npcs.character_map.clear();
294            for (presence, wpos) in (&presences, &positions).join() {
295                if let PresenceKind::Character(character) = &presence.kind {
296                    let chunk_pos = wpos.0.xy().as_().wpos_to_cpos();
297                    data.npcs
298                        .character_map
299                        .entry(chunk_pos)
300                        .or_default()
301                        .push((*character, wpos.0));
302                }
303            }
304        }
305
306        // Tick rtsim
307        rtsim.state.tick(
308            &mut tick_data,
309            &world,
310            index.as_index_ref(),
311            *time_of_day,
312            *time,
313            dt.0,
314        );
315
316        // Perform a save if required
317        if rtsim
318            .last_saved
319            .is_none_or(|ls| ls.elapsed() > Duration::from_secs(60))
320        {
321            // TODO: Use slow jobs
322            let _ = slow_jobs;
323            rtsim.save(/* &slow_jobs, */ false);
324        }
325
326        let chunk_states = rtsim.state.resource::<ChunkStates>();
327        let data = &mut *rtsim.state.data_mut();
328
329        let mut create_event = |id: NpcId, npc: &Npc, steering: Option<NpcBuilder>| match npc.body {
330            Body::Ship(body) => {
331                create_ship_emitter.emit(CreateShipEvent {
332                    pos: comp::Pos(npc.wpos),
333                    ori: comp::Ori::from(Dir::new(npc.dir.with_z(0.0))),
334                    ship: body,
335                    rtsim_entity: Some(RtSimEntity(id)),
336                    driver: steering,
337                });
338            },
339            _ => {
340                let entity_info = get_npc_entity_info(
341                    npc,
342                    &data.sites,
343                    index.as_index_ref(),
344                    Some(&calendar_data),
345                );
346
347                let (mut npc_builder, pos) = SpawnEntityData::from_entity_info(entity_info)
348                    .into_npc_data_inner()
349                    .expect("Entity loaded from assets cannot be special")
350                    .to_npc_builder();
351
352                if let Some(agent) = &mut npc_builder.agent {
353                    agent.rtsim_outbox = Some(Default::default());
354                }
355
356                if let Some(health) = &mut npc_builder.health {
357                    health.set_fraction(npc.health_fraction);
358                }
359
360                create_npc_emitter.emit(CreateNpcEvent {
361                    pos,
362                    ori: comp::Ori::from(Dir::new(npc.dir.with_z(0.0))),
363                    npc: npc_builder.with_rtsim(RtSimEntity(id)),
364                    rider: steering,
365                });
366            },
367        };
368
369        // Load in mounted npcs and their riders
370        for mount in data.npcs.mounts.iter_mounts() {
371            let mount_npc = data.npcs.npcs.get_mut(mount).expect("This should exist");
372            let chunk = mount_npc.wpos.xy().as_::<i32>().wpos_to_cpos();
373
374            if matches!(mount_npc.mode, SimulationMode::Simulated)
375                && chunk_states.0.get(chunk).is_some_and(|c| c.is_some())
376            {
377                mount_npc.mode = SimulationMode::Loaded;
378
379                let mut actor_info = |actor: Actor| {
380                    let npc_id = actor.npc()?;
381                    let npc = data.npcs.npcs.get_mut(npc_id)?;
382                    if matches!(npc.mode, SimulationMode::Simulated) {
383                        npc.mode = SimulationMode::Loaded;
384                        let entity_info = get_npc_entity_info(
385                            npc,
386                            &data.sites,
387                            index.as_index_ref(),
388                            Some(&calendar_data),
389                        );
390
391                        let mut npc_builder = SpawnEntityData::from_entity_info(entity_info)
392                            .into_npc_data_inner()
393                            // EntityConfig can't represent Waypoints at all
394                            // as of now, and if someone will try to spawn
395                            // rtsim waypoint it is definitely error.
396                            .expect("Entity loaded from assets cannot be special")
397                            .to_npc_builder()
398                            .0
399                            .with_rtsim(RtSimEntity(npc_id));
400
401                        if let Some(agent) = &mut npc_builder.agent {
402                            agent.rtsim_outbox = Some(Default::default());
403                        }
404
405                        Some(npc_builder)
406                    } else {
407                        error!("Npc is loaded but vehicle is unloaded");
408                        None
409                    }
410                };
411
412                let steerer = data
413                    .npcs
414                    .mounts
415                    .get_steerer_link(mount)
416                    .and_then(|link| actor_info(link.rider));
417
418                let mount_npc = data.npcs.npcs.get(mount).expect("This should exist");
419                create_event(mount, mount_npc, steerer);
420            }
421        }
422
423        // Load in NPCs
424        for (npc_id, npc) in data.npcs.npcs.iter_mut() {
425            let chunk = npc.wpos.xy().as_::<i32>().wpos_to_cpos();
426
427            // Load the NPC into the world if it's in a loaded chunk and is not already
428            // loaded
429            if matches!(npc.mode, SimulationMode::Simulated)
430                && chunk_states.0.get(chunk).is_some_and(|c| c.is_some())
431                // Riding npcs will be spawned by the vehicle.
432                && data.npcs.mounts.get_mount_link(npc_id).is_none()
433            {
434                npc.mode = SimulationMode::Loaded;
435                create_event(npc_id, npc, None);
436            }
437        }
438
439        // Synchronise rtsim NPC with entity data
440        for (entity, pos, rtsim_entity, agent) in (
441            &entities,
442            &positions,
443            &rtsim_entities,
444            (&mut agents).maybe(),
445        )
446            .join()
447        {
448            if let Some(npc) = data.npcs.get_mut(rtsim_entity.0) {
449                match npc.mode {
450                    SimulationMode::Loaded => {
451                        // Update rtsim NPC state
452                        npc.wpos = pos.0;
453
454                        // Update entity state
455                        if let Some(agent) = agent {
456                            agent.rtsim_controller.personality = npc.personality;
457                            agent.rtsim_controller.look_dir = npc.controller.look_dir;
458                            agent.rtsim_controller.activity = npc.controller.activity;
459                            agent
460                                .rtsim_controller
461                                .actions
462                                .extend(std::mem::take(&mut npc.controller.actions));
463                            if let Some(rtsim_outbox) = &mut agent.rtsim_outbox {
464                                npc.inbox.append(rtsim_outbox);
465                            }
466                        }
467                    },
468                    SimulationMode::Simulated => {
469                        delete_emitter.emit(DeleteEvent(entity));
470                    },
471                }
472            }
473        }
474    }
475}