veloren_server/rtsim/
tick.rs

1use super::*;
2use crate::{ServerConstants, sys::terrain::SpawnEntityData};
3use common::{
4    LoadoutBuilder,
5    calendar::Calendar,
6    comp::{
7        self, Body, Item, Presence, PresenceKind, inventory::trade_pricing::TradePricing,
8        slot::ArmorSlot,
9    },
10    event::{CreateNpcEvent, CreateShipEvent, DeleteEvent, EventBus, NpcBuilder},
11    generation::{BodyBuilder, EntityConfig, EntityInfo},
12    resources::{DeltaTime, Time, TimeOfDay},
13    rtsim::{Actor, NpcId, RtSimEntity},
14    slowjob::SlowJobPool,
15    terrain::CoordinateConversions,
16    trade::{Good, SiteInformation},
17    uid::IdMaps,
18    util::Dir,
19    weather::WeatherGrid,
20};
21use common_ecs::{Job, Origin, Phase, System};
22use rand::Rng;
23use rtsim::{
24    ai::NpcSystemData,
25    data::{
26        Npc, Sites,
27        npc::{Profession, SimulationMode},
28    },
29};
30use specs::{Entities, Join, LendJoin, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage};
31use std::{
32    ops::Range,
33    sync::{Arc, Mutex},
34    time::Duration,
35};
36use tracing::error;
37
38pub fn trader_loadout(
39    loadout_builder: LoadoutBuilder,
40    economy: Option<&SiteInformation>,
41    mut permitted: impl FnMut(Good) -> bool,
42    coin_range: Range<f32>,
43) -> LoadoutBuilder {
44    let rng = &mut rand::rng();
45    let mut backpack = Item::new_from_asset_expect("common.items.armor.misc.back.backpack");
46    let mut bag1 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
47    let mut bag2 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
48    let mut bag3 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
49    let mut bag4 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack");
50    let slots = backpack.slots().len() + 4 * bag1.slots().len();
51    let mut stockmap: hashbrown::HashMap<Good, f32> = economy
52        .map(|e| {
53            e.unconsumed_stock
54                .clone()
55                .into_iter()
56                .filter(|(good, _)| permitted(*good))
57                .collect()
58        })
59        .unwrap_or_default();
60    // modify stock for better gameplay
61
62    // TODO: currently econsim spends all its food on population, resulting in none
63    // for the players to buy; the `.max` is temporary to ensure that there's some
64    // food for sale at every site, to be used until we have some solution like NPC
65    // houses as a limit on econsim population growth
66    if permitted(Good::Food) {
67        stockmap
68            .entry(Good::Food)
69            .and_modify(|e| *e = e.max(10_000.0))
70            .or_insert(10_000.0);
71    }
72    // Reduce amount of potions so merchants do not oversupply potions.
73    // TODO: Maybe remove when merchants and their inventories are rtsim?
74    // Note: Likely without effect now that potions are counted as food
75    if permitted(Good::Potions) {
76        stockmap
77            .entry(Good::Potions)
78            .and_modify(|e| *e = e.powf(0.25));
79    }
80    stockmap
81        .entry(Good::Coin)
82        .and_modify(|e| *e = e.min(rng.random_range(coin_range)));
83    // assume roughly 10 merchants sharing a town's stock (other logic for coins)
84    stockmap
85        .iter_mut()
86        .filter(|(good, _amount)| **good != Good::Coin)
87        .for_each(|(_good, amount)| *amount *= 0.1);
88    // Fill bags with stuff according to unclaimed stock
89    let ability_map = &comp::tool::AbilityMap::load().read();
90    let msm = &comp::item::MaterialStatManifest::load().read();
91    let mut wares: Vec<Item> =
92        TradePricing::random_items(&mut stockmap, slots as u32, true, true, 16)
93            .iter()
94            .filter_map(|(n, a)| {
95                let i = Item::new_from_item_definition_id(n.as_ref(), ability_map, msm).ok();
96                i.map(|mut i| {
97                    i.set_amount(*a)
98                        .map_err(|_| tracing::error!("merchant loadout amount failure"))
99                        .ok();
100                    i
101                })
102            })
103            .collect();
104    sort_wares(&mut wares);
105    transfer(&mut wares, &mut backpack);
106    transfer(&mut wares, &mut bag1);
107    transfer(&mut wares, &mut bag2);
108    transfer(&mut wares, &mut bag3);
109    transfer(&mut wares, &mut bag4);
110
111    loadout_builder
112        .back(Some(backpack))
113        .bag(ArmorSlot::Bag1, Some(bag1))
114        .bag(ArmorSlot::Bag2, Some(bag2))
115        .bag(ArmorSlot::Bag3, Some(bag3))
116        .bag(ArmorSlot::Bag4, Some(bag4))
117}
118
119fn sort_wares(bag: &mut [Item]) {
120    use common::comp::item::TagExampleInfo;
121
122    bag.sort_by(|a, b| {
123        a.quality()
124            .cmp(&b.quality())
125        // sort by kind
126        .then(
127            Ord::cmp(
128                a.tags().first().map_or("", |tag| tag.name()),
129                b.tags().first().map_or("", |tag| tag.name()),
130            )
131        )
132        // sort by name
133        // TODO: figure out the better way here
134        .then(#[expect(deprecated)] Ord::cmp(&a.legacy_name(), &b.legacy_name()))
135    });
136}
137
138fn transfer(wares: &mut Vec<Item>, bag: &mut Item) {
139    let capacity = bag.slots().len();
140    for (s, w) in bag
141        .slots_mut()
142        .iter_mut()
143        .zip(wares.drain(0..wares.len().min(capacity)))
144    {
145        *s = Some(w);
146    }
147}
148
149fn humanoid_config(profession: &Profession) -> &'static str {
150    match profession {
151        Profession::Farmer => "common.entity.village.farmer",
152        Profession::Hunter => "common.entity.village.hunter",
153        Profession::Herbalist => "common.entity.village.herbalist",
154        Profession::Captain => "common.entity.village.captain",
155        Profession::Merchant => "common.entity.village.merchant",
156        Profession::Guard => "common.entity.village.guard",
157        Profession::Adventurer(rank) => match rank {
158            0 => "common.entity.world.traveler0",
159            1 => "common.entity.world.traveler1",
160            2 => "common.entity.world.traveler2",
161            3 => "common.entity.world.traveler3",
162            _ => {
163                error!(
164                    "Tried to get configuration for invalid adventurer rank {}",
165                    rank
166                );
167                "common.entity.world.traveler3"
168            },
169        },
170        Profession::Blacksmith => "common.entity.village.blacksmith",
171        Profession::Chef => "common.entity.village.chef",
172        Profession::Alchemist => "common.entity.village.alchemist",
173        Profession::Pirate(leader) => match leader {
174            false => "common.entity.spot.pirate",
175            true => "common.entity.spot.buccaneer",
176        },
177        Profession::Cultist => "common.entity.dungeon.cultist.cultist",
178    }
179}
180
181fn loadout_default(
182    loadout: LoadoutBuilder,
183    _economy: Option<&SiteInformation>,
184    _time: Option<&(TimeOfDay, Calendar)>,
185) -> LoadoutBuilder {
186    loadout
187}
188
189fn merchant_loadout(
190    loadout_builder: LoadoutBuilder,
191    economy: Option<&SiteInformation>,
192    _time: Option<&(TimeOfDay, Calendar)>,
193) -> LoadoutBuilder {
194    trader_loadout(loadout_builder, economy, |_| true, 1000.0..3000.0)
195}
196
197fn farmer_loadout(
198    loadout_builder: LoadoutBuilder,
199    economy: Option<&SiteInformation>,
200    _time: Option<&(TimeOfDay, Calendar)>,
201) -> LoadoutBuilder {
202    trader_loadout(
203        loadout_builder,
204        economy,
205        |good| matches!(good, Good::Food | Good::Coin),
206        200.0..300.0,
207    )
208}
209
210fn herbalist_loadout(
211    loadout_builder: LoadoutBuilder,
212    economy: Option<&SiteInformation>,
213    _time: Option<&(TimeOfDay, Calendar)>,
214) -> LoadoutBuilder {
215    trader_loadout(
216        loadout_builder,
217        economy,
218        |good| matches!(good, Good::Ingredients),
219        200.0..300.0,
220    )
221}
222
223fn chef_loadout(
224    loadout_builder: LoadoutBuilder,
225    economy: Option<&SiteInformation>,
226    _time: Option<&(TimeOfDay, Calendar)>,
227) -> LoadoutBuilder {
228    trader_loadout(
229        loadout_builder,
230        economy,
231        |good| matches!(good, Good::Food | Good::Coin),
232        200.0..300.0,
233    )
234}
235
236fn blacksmith_loadout(
237    loadout_builder: LoadoutBuilder,
238    economy: Option<&SiteInformation>,
239    _time: Option<&(TimeOfDay, Calendar)>,
240) -> LoadoutBuilder {
241    trader_loadout(
242        loadout_builder,
243        economy,
244        |good| matches!(good, Good::Tools | Good::Armor | Good::Coin),
245        200.0..300.0,
246    )
247}
248
249fn alchemist_loadout(
250    loadout_builder: LoadoutBuilder,
251    economy: Option<&SiteInformation>,
252    _time: Option<&(TimeOfDay, Calendar)>,
253) -> LoadoutBuilder {
254    trader_loadout(
255        loadout_builder,
256        economy,
257        |good| matches!(good, Good::Potions | Good::Coin),
258        200.0..300.0,
259    )
260}
261
262fn profession_extra_loadout(
263    profession: Option<&Profession>,
264) -> fn(
265    LoadoutBuilder,
266    Option<&SiteInformation>,
267    time: Option<&(TimeOfDay, Calendar)>,
268) -> LoadoutBuilder {
269    match profession {
270        Some(Profession::Merchant) => merchant_loadout,
271        Some(Profession::Farmer) => farmer_loadout,
272        Some(Profession::Herbalist) => herbalist_loadout,
273        Some(Profession::Chef) => chef_loadout,
274        Some(Profession::Blacksmith) => blacksmith_loadout,
275        Some(Profession::Alchemist) => alchemist_loadout,
276        _ => loadout_default,
277    }
278}
279
280fn profession_agent_mark(profession: Option<&Profession>) -> Option<comp::agent::Mark> {
281    match profession {
282        Some(
283            Profession::Merchant
284            | Profession::Farmer
285            | Profession::Herbalist
286            | Profession::Chef
287            | Profession::Blacksmith
288            | Profession::Alchemist,
289        ) => Some(comp::agent::Mark::Merchant),
290        Some(Profession::Guard) => Some(comp::agent::Mark::Guard),
291        _ => None,
292    }
293}
294
295fn get_npc_entity_info(
296    npc: &Npc,
297    sites: &Sites,
298    index: IndexRef,
299    time: Option<&(TimeOfDay, Calendar)>,
300) -> EntityInfo {
301    let pos = comp::Pos(npc.wpos);
302
303    let mut rng = npc.rng(Npc::PERM_ENTITY_CONFIG);
304    if let Some(profession) = npc.profession() {
305        let economy = npc.home.and_then(|home| {
306            let site = sites.get(home)?.world_site?;
307            index.sites.get(site).trade_information(site)
308        });
309
310        let config_asset = humanoid_config(&profession);
311
312        let entity_config = EntityConfig::from_asset_expect_owned(config_asset)
313            .with_body(BodyBuilder::Exact(npc.body));
314        EntityInfo::at(pos.0)
315            .with_entity_config(entity_config, Some(config_asset), &mut rng, time)
316            .with_alignment(
317                if matches!(profession, Profession::Cultist | Profession::Pirate(_)) {
318                    comp::Alignment::Enemy
319                } else {
320                    comp::Alignment::Npc
321                },
322            )
323            .with_economy(economy.as_ref())
324            .with_lazy_loadout(profession_extra_loadout(Some(&profession)))
325            .with_alias(npc.get_name())
326            .with_agent_mark(profession_agent_mark(Some(&profession)))
327    } else {
328        let config_asset = match npc.body {
329            Body::BirdLarge(body) => match body.species {
330                comp::bird_large::Species::Phoenix => "common.entity.wild.aggressive.phoenix",
331                comp::bird_large::Species::Cockatrice => "common.entity.wild.aggressive.cockatrice",
332                comp::bird_large::Species::Roc => "common.entity.wild.aggressive.roc",
333                comp::bird_large::Species::CloudWyvern => {
334                    "common.entity.wild.aggressive.cloudwyvern"
335                },
336                comp::bird_large::Species::FlameWyvern => {
337                    "common.entity.wild.aggressive.flamewyvern"
338                },
339                comp::bird_large::Species::FrostWyvern => {
340                    "common.entity.wild.aggressive.frostwyvern"
341                },
342                comp::bird_large::Species::SeaWyvern => "common.entity.wild.aggressive.seawyvern",
343                comp::bird_large::Species::WealdWyvern => {
344                    "common.entity.wild.aggressive.wealdwyvern"
345                },
346            },
347            Body::BipedLarge(body) => match body.species {
348                comp::biped_large::Species::Ogre => "common.entity.wild.aggressive.ogre",
349                comp::biped_large::Species::Cyclops => "common.entity.wild.aggressive.cyclops",
350                comp::biped_large::Species::Wendigo => "common.entity.wild.aggressive.wendigo",
351                comp::biped_large::Species::Werewolf => "common.entity.wild.aggressive.werewolf",
352                comp::biped_large::Species::Cavetroll => "common.entity.wild.aggressive.cave_troll",
353                comp::biped_large::Species::Mountaintroll => {
354                    "common.entity.wild.aggressive.mountain_troll"
355                },
356                comp::biped_large::Species::Swamptroll => {
357                    "common.entity.wild.aggressive.swamp_troll"
358                },
359                comp::biped_large::Species::Blueoni => "common.entity.wild.aggressive.blue_oni",
360                comp::biped_large::Species::Redoni => "common.entity.wild.aggressive.red_oni",
361                comp::biped_large::Species::Tursus => "common.entity.wild.aggressive.tursus",
362                comp::biped_large::Species::Gigasfrost => {
363                    "common.entity.world.world_bosses.gigas_frost"
364                },
365                comp::biped_large::Species::Gigasfire => {
366                    "common.entity.world.world_bosses.gigas_fire"
367                },
368                species => unimplemented!("rtsim spawning for {:?}", species),
369            },
370            body => unimplemented!("rtsim spawning for {:?}", body),
371        };
372        let entity_config = EntityConfig::from_asset_expect_owned(config_asset)
373            .with_body(BodyBuilder::Exact(npc.body));
374
375        EntityInfo::at(pos.0).with_entity_config(entity_config, Some(config_asset), &mut rng, time)
376    }
377}
378
379#[derive(Default)]
380pub struct Sys;
381impl<'a> System<'a> for Sys {
382    type SystemData = (
383        Entities<'a>,
384        Read<'a, DeltaTime>,
385        Read<'a, Time>,
386        Read<'a, TimeOfDay>,
387        Read<'a, EventBus<CreateShipEvent>>,
388        Read<'a, EventBus<CreateNpcEvent>>,
389        Read<'a, EventBus<DeleteEvent>>,
390        WriteExpect<'a, RtSim>,
391        ReadExpect<'a, Arc<world::World>>,
392        ReadExpect<'a, world::IndexOwned>,
393        ReadExpect<'a, SlowJobPool>,
394        ReadStorage<'a, comp::Pos>,
395        ReadStorage<'a, RtSimEntity>,
396        WriteStorage<'a, comp::Agent>,
397        ReadStorage<'a, Presence>,
398        ReadExpect<'a, Calendar>,
399        Read<'a, IdMaps>,
400        ReadExpect<'a, ServerConstants>,
401        ReadExpect<'a, WeatherGrid>,
402        WriteStorage<'a, comp::Inventory>,
403        WriteExpect<'a, comp::gizmos::RtsimGizmos>,
404        ReadExpect<'a, comp::tool::AbilityMap>,
405        ReadExpect<'a, comp::item::MaterialStatManifest>,
406    );
407
408    const NAME: &'static str = "rtsim::tick";
409    const ORIGIN: Origin = Origin::Server;
410    const PHASE: Phase = Phase::Create;
411
412    fn run(
413        _job: &mut Job<Self>,
414        (
415            entities,
416            dt,
417            time,
418            time_of_day,
419            create_ship_events,
420            create_npc_events,
421            delete_events,
422            mut rtsim,
423            world,
424            index,
425            slow_jobs,
426            positions,
427            rtsim_entities,
428            mut agents,
429            presences,
430            calendar,
431            id_maps,
432            server_constants,
433            weather_grid,
434            inventories,
435            rtsim_gizmos,
436            ability_map,
437            msm,
438        ): Self::SystemData,
439    ) {
440        let mut create_ship_emitter = create_ship_events.emitter();
441        let mut create_npc_emitter = create_npc_events.emitter();
442        let mut delete_emitter = delete_events.emitter();
443        let rtsim = &mut *rtsim;
444        let calendar_data = (*time_of_day, (*calendar).clone());
445
446        // Set up rtsim inputs
447        {
448            let mut data = rtsim.state.data_mut();
449
450            // Update time of day
451            data.time_of_day = *time_of_day;
452
453            // Update character map (i.e: so that rtsim knows where players are)
454            // TODO: Other entities too like animals? Or do we now care about that?
455            data.npcs.character_map.clear();
456            for (presence, wpos) in (&presences, &positions).join() {
457                if let PresenceKind::Character(character) = &presence.kind {
458                    let chunk_pos = wpos.0.xy().as_().wpos_to_cpos();
459                    data.npcs
460                        .character_map
461                        .entry(chunk_pos)
462                        .or_default()
463                        .push((*character, wpos.0));
464                }
465            }
466        }
467
468        // Tick rtsim
469        rtsim.state.tick(
470            &mut NpcSystemData {
471                positions: positions.clone(),
472                id_maps,
473                server_constants,
474                weather_grid,
475                inventories: Mutex::new(inventories),
476                rtsim_gizmos,
477                ability_map,
478                msm,
479            },
480            &world,
481            index.as_index_ref(),
482            *time_of_day,
483            *time,
484            dt.0,
485        );
486
487        // Perform a save if required
488        if rtsim
489            .last_saved
490            .is_none_or(|ls| ls.elapsed() > Duration::from_secs(60))
491        {
492            // TODO: Use slow jobs
493            let _ = slow_jobs;
494            rtsim.save(/* &slow_jobs, */ false);
495        }
496
497        let chunk_states = rtsim.state.resource::<ChunkStates>();
498        let data = &mut *rtsim.state.data_mut();
499
500        let mut create_event = |id: NpcId, npc: &Npc, steering: Option<NpcBuilder>| match npc.body {
501            Body::Ship(body) => {
502                create_ship_emitter.emit(CreateShipEvent {
503                    pos: comp::Pos(npc.wpos),
504                    ori: comp::Ori::from(Dir::new(npc.dir.with_z(0.0))),
505                    ship: body,
506                    rtsim_entity: Some(id),
507                    driver: steering,
508                });
509            },
510            _ => {
511                let entity_info = get_npc_entity_info(
512                    npc,
513                    &data.sites,
514                    index.as_index_ref(),
515                    Some(&calendar_data),
516                );
517
518                let (mut npc_builder, pos) = SpawnEntityData::from_entity_info(entity_info)
519                    .into_npc_data_inner()
520                    .expect("Entity loaded from assets cannot be special")
521                    .to_npc_builder();
522
523                if let Some(agent) = &mut npc_builder.agent {
524                    agent.rtsim_outbox = Some(Default::default());
525                }
526
527                if let Some(health) = &mut npc_builder.health {
528                    health.set_fraction(npc.health_fraction);
529                }
530
531                create_npc_emitter.emit(CreateNpcEvent {
532                    pos,
533                    ori: comp::Ori::from(Dir::new(npc.dir.with_z(0.0))),
534                    npc: npc_builder.with_rtsim(id).with_rider(steering),
535                });
536            },
537        };
538
539        // Load in mounted npcs and their riders
540        for mount in data.npcs.mounts.iter_mounts() {
541            let mount_npc = data.npcs.npcs.get_mut(mount).expect("This should exist");
542            let chunk = mount_npc.wpos.xy().as_::<i32>().wpos_to_cpos();
543
544            if matches!(mount_npc.mode, SimulationMode::Simulated)
545                && chunk_states.0.get(chunk).is_some_and(|c| c.is_some())
546            {
547                mount_npc.mode = SimulationMode::Loaded;
548
549                let mut actor_info = |actor: Actor| {
550                    let npc_id = actor.npc()?;
551                    let npc = data.npcs.npcs.get_mut(npc_id)?;
552                    if matches!(npc.mode, SimulationMode::Simulated) {
553                        npc.mode = SimulationMode::Loaded;
554                        let entity_info = get_npc_entity_info(
555                            npc,
556                            &data.sites,
557                            index.as_index_ref(),
558                            Some(&calendar_data),
559                        );
560
561                        let mut npc_builder = SpawnEntityData::from_entity_info(entity_info)
562                            .into_npc_data_inner()
563                            // EntityConfig can't represent Waypoints at all
564                            // as of now, and if someone will try to spawn
565                            // rtsim waypoint it is definitely error.
566                            .expect("Entity loaded from assets cannot be special")
567                            .to_npc_builder()
568                            .0
569                            .with_rtsim(npc_id);
570
571                        if let Some(agent) = &mut npc_builder.agent {
572                            agent.rtsim_outbox = Some(Default::default());
573                        }
574
575                        Some(npc_builder)
576                    } else {
577                        error!("Npc is loaded but vehicle is unloaded");
578                        None
579                    }
580                };
581
582                let steerer = data
583                    .npcs
584                    .mounts
585                    .get_steerer_link(mount)
586                    .and_then(|link| actor_info(link.rider));
587
588                let mount_npc = data.npcs.npcs.get(mount).expect("This should exist");
589                create_event(mount, mount_npc, steerer);
590            }
591        }
592
593        // Load in NPCs
594        for (npc_id, npc) in data.npcs.npcs.iter_mut() {
595            let chunk = npc.wpos.xy().as_::<i32>().wpos_to_cpos();
596
597            // Load the NPC into the world if it's in a loaded chunk and is not already
598            // loaded
599            if matches!(npc.mode, SimulationMode::Simulated)
600                && chunk_states.0.get(chunk).is_some_and(|c| c.is_some())
601                // Riding npcs will be spawned by the vehicle.
602                && data.npcs.mounts.get_mount_link(npc_id).is_none()
603            {
604                npc.mode = SimulationMode::Loaded;
605                create_event(npc_id, npc, None);
606            }
607        }
608
609        // Synchronise rtsim NPC with entity data
610        for (entity, pos, rtsim_entity, agent) in (
611            &entities,
612            &positions,
613            &rtsim_entities,
614            (&mut agents).maybe(),
615        )
616            .join()
617        {
618            if let Some(npc) = data.npcs.get_mut(*rtsim_entity) {
619                match npc.mode {
620                    SimulationMode::Loaded => {
621                        // Update rtsim NPC state
622                        npc.wpos = pos.0;
623
624                        // Update entity state
625                        if let Some(agent) = agent {
626                            agent.rtsim_controller.personality = npc.personality;
627                            agent.rtsim_controller.look_dir = npc.controller.look_dir;
628                            agent.rtsim_controller.activity = npc.controller.activity;
629                            agent
630                                .rtsim_controller
631                                .actions
632                                .extend(std::mem::take(&mut npc.controller.actions));
633                            if let Some(rtsim_outbox) = &mut agent.rtsim_outbox {
634                                npc.inbox.append(rtsim_outbox);
635                            }
636                        }
637                    },
638                    SimulationMode::Simulated => {
639                        delete_emitter.emit(DeleteEvent(entity));
640                    },
641                }
642            }
643        }
644    }
645}