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        .then(#[expect(deprecated)] Ord::cmp(&a.name(), &b.name()))
134    });
135}
136
137fn transfer(wares: &mut Vec<Item>, bag: &mut Item) {
138    let capacity = bag.slots().len();
139    for (s, w) in bag
140        .slots_mut()
141        .iter_mut()
142        .zip(wares.drain(0..wares.len().min(capacity)))
143    {
144        *s = Some(w);
145    }
146}
147
148fn humanoid_config(profession: &Profession) -> &'static str {
149    match profession {
150        Profession::Farmer => "common.entity.village.farmer",
151        Profession::Hunter => "common.entity.village.hunter",
152        Profession::Herbalist => "common.entity.village.herbalist",
153        Profession::Captain => "common.entity.village.captain",
154        Profession::Merchant => "common.entity.village.merchant",
155        Profession::Guard => "common.entity.village.guard",
156        Profession::Adventurer(rank) => match rank {
157            0 => "common.entity.world.traveler0",
158            1 => "common.entity.world.traveler1",
159            2 => "common.entity.world.traveler2",
160            3 => "common.entity.world.traveler3",
161            _ => {
162                error!(
163                    "Tried to get configuration for invalid adventurer rank {}",
164                    rank
165                );
166                "common.entity.world.traveler3"
167            },
168        },
169        Profession::Blacksmith => "common.entity.village.blacksmith",
170        Profession::Chef => "common.entity.village.chef",
171        Profession::Alchemist => "common.entity.village.alchemist",
172        Profession::Pirate(leader) => match leader {
173            false => "common.entity.spot.pirate",
174            true => "common.entity.spot.buccaneer",
175        },
176        Profession::Cultist => "common.entity.dungeon.cultist.cultist",
177    }
178}
179
180fn loadout_default(
181    loadout: LoadoutBuilder,
182    _economy: Option<&SiteInformation>,
183    _time: Option<&(TimeOfDay, Calendar)>,
184) -> LoadoutBuilder {
185    loadout
186}
187
188fn merchant_loadout(
189    loadout_builder: LoadoutBuilder,
190    economy: Option<&SiteInformation>,
191    _time: Option<&(TimeOfDay, Calendar)>,
192) -> LoadoutBuilder {
193    trader_loadout(loadout_builder, economy, |_| true, 1000.0..3000.0)
194}
195
196fn farmer_loadout(
197    loadout_builder: LoadoutBuilder,
198    economy: Option<&SiteInformation>,
199    _time: Option<&(TimeOfDay, Calendar)>,
200) -> LoadoutBuilder {
201    trader_loadout(
202        loadout_builder,
203        economy,
204        |good| matches!(good, Good::Food | Good::Coin),
205        200.0..300.0,
206    )
207}
208
209fn herbalist_loadout(
210    loadout_builder: LoadoutBuilder,
211    economy: Option<&SiteInformation>,
212    _time: Option<&(TimeOfDay, Calendar)>,
213) -> LoadoutBuilder {
214    trader_loadout(
215        loadout_builder,
216        economy,
217        |good| matches!(good, Good::Ingredients),
218        200.0..300.0,
219    )
220}
221
222fn chef_loadout(
223    loadout_builder: LoadoutBuilder,
224    economy: Option<&SiteInformation>,
225    _time: Option<&(TimeOfDay, Calendar)>,
226) -> LoadoutBuilder {
227    trader_loadout(
228        loadout_builder,
229        economy,
230        |good| matches!(good, Good::Food | Good::Coin),
231        200.0..300.0,
232    )
233}
234
235fn blacksmith_loadout(
236    loadout_builder: LoadoutBuilder,
237    economy: Option<&SiteInformation>,
238    _time: Option<&(TimeOfDay, Calendar)>,
239) -> LoadoutBuilder {
240    trader_loadout(
241        loadout_builder,
242        economy,
243        |good| matches!(good, Good::Tools | Good::Armor | Good::Coin),
244        200.0..300.0,
245    )
246}
247
248fn alchemist_loadout(
249    loadout_builder: LoadoutBuilder,
250    economy: Option<&SiteInformation>,
251    _time: Option<&(TimeOfDay, Calendar)>,
252) -> LoadoutBuilder {
253    trader_loadout(
254        loadout_builder,
255        economy,
256        |good| matches!(good, Good::Potions | Good::Coin),
257        200.0..300.0,
258    )
259}
260
261fn profession_extra_loadout(
262    profession: Option<&Profession>,
263) -> fn(
264    LoadoutBuilder,
265    Option<&SiteInformation>,
266    time: Option<&(TimeOfDay, Calendar)>,
267) -> LoadoutBuilder {
268    match profession {
269        Some(Profession::Merchant) => merchant_loadout,
270        Some(Profession::Farmer) => farmer_loadout,
271        Some(Profession::Herbalist) => herbalist_loadout,
272        Some(Profession::Chef) => chef_loadout,
273        Some(Profession::Blacksmith) => blacksmith_loadout,
274        Some(Profession::Alchemist) => alchemist_loadout,
275        _ => loadout_default,
276    }
277}
278
279fn profession_agent_mark(profession: Option<&Profession>) -> Option<comp::agent::Mark> {
280    match profession {
281        Some(
282            Profession::Merchant
283            | Profession::Farmer
284            | Profession::Herbalist
285            | Profession::Chef
286            | Profession::Blacksmith
287            | Profession::Alchemist,
288        ) => Some(comp::agent::Mark::Merchant),
289        Some(Profession::Guard) => Some(comp::agent::Mark::Guard),
290        _ => None,
291    }
292}
293
294fn get_npc_entity_info(
295    npc: &Npc,
296    sites: &Sites,
297    index: IndexRef,
298    time: Option<&(TimeOfDay, Calendar)>,
299) -> EntityInfo {
300    let pos = comp::Pos(npc.wpos);
301
302    let mut rng = npc.rng(Npc::PERM_ENTITY_CONFIG);
303    if let Some(profession) = npc.profession() {
304        let economy = npc.home.and_then(|home| {
305            let site = sites.get(home)?.world_site?;
306            index.sites.get(site).trade_information(site)
307        });
308
309        let config_asset = humanoid_config(&profession);
310
311        let entity_config = EntityConfig::from_asset_expect_owned(config_asset)
312            .with_body(BodyBuilder::Exact(npc.body));
313        EntityInfo::at(pos.0)
314            .with_entity_config(entity_config, Some(config_asset), &mut rng, time)
315            .with_alignment(
316                if matches!(profession, Profession::Cultist | Profession::Pirate(_)) {
317                    comp::Alignment::Enemy
318                } else {
319                    comp::Alignment::Npc
320                },
321            )
322            .with_economy(economy.as_ref())
323            .with_lazy_loadout(profession_extra_loadout(Some(&profession)))
324            .with_alias(npc.get_name())
325            .with_agent_mark(profession_agent_mark(Some(&profession)))
326    } else {
327        let config_asset = match npc.body {
328            Body::BirdLarge(body) => match body.species {
329                comp::bird_large::Species::Phoenix => "common.entity.wild.aggressive.phoenix",
330                comp::bird_large::Species::Cockatrice => "common.entity.wild.aggressive.cockatrice",
331                comp::bird_large::Species::Roc => "common.entity.wild.aggressive.roc",
332                comp::bird_large::Species::CloudWyvern => {
333                    "common.entity.wild.aggressive.cloudwyvern"
334                },
335                comp::bird_large::Species::FlameWyvern => {
336                    "common.entity.wild.aggressive.flamewyvern"
337                },
338                comp::bird_large::Species::FrostWyvern => {
339                    "common.entity.wild.aggressive.frostwyvern"
340                },
341                comp::bird_large::Species::SeaWyvern => "common.entity.wild.aggressive.seawyvern",
342                comp::bird_large::Species::WealdWyvern => {
343                    "common.entity.wild.aggressive.wealdwyvern"
344                },
345            },
346            Body::BipedLarge(body) => match body.species {
347                comp::biped_large::Species::Ogre => "common.entity.wild.aggressive.ogre",
348                comp::biped_large::Species::Cyclops => "common.entity.wild.aggressive.cyclops",
349                comp::biped_large::Species::Wendigo => "common.entity.wild.aggressive.wendigo",
350                comp::biped_large::Species::Werewolf => "common.entity.wild.aggressive.werewolf",
351                comp::biped_large::Species::Cavetroll => "common.entity.wild.aggressive.cave_troll",
352                comp::biped_large::Species::Mountaintroll => {
353                    "common.entity.wild.aggressive.mountain_troll"
354                },
355                comp::biped_large::Species::Swamptroll => {
356                    "common.entity.wild.aggressive.swamp_troll"
357                },
358                comp::biped_large::Species::Blueoni => "common.entity.wild.aggressive.blue_oni",
359                comp::biped_large::Species::Redoni => "common.entity.wild.aggressive.red_oni",
360                comp::biped_large::Species::Tursus => "common.entity.wild.aggressive.tursus",
361                comp::biped_large::Species::Gigasfrost => {
362                    "common.entity.world.world_bosses.gigas_frost"
363                },
364                comp::biped_large::Species::Gigasfire => {
365                    "common.entity.world.world_bosses.gigas_fire"
366                },
367                species => unimplemented!("rtsim spawning for {:?}", species),
368            },
369            body => unimplemented!("rtsim spawning for {:?}", body),
370        };
371        let entity_config = EntityConfig::from_asset_expect_owned(config_asset)
372            .with_body(BodyBuilder::Exact(npc.body));
373
374        EntityInfo::at(pos.0).with_entity_config(entity_config, Some(config_asset), &mut rng, time)
375    }
376}
377
378#[derive(Default)]
379pub struct Sys;
380impl<'a> System<'a> for Sys {
381    type SystemData = (
382        Entities<'a>,
383        Read<'a, DeltaTime>,
384        Read<'a, Time>,
385        Read<'a, TimeOfDay>,
386        Read<'a, EventBus<CreateShipEvent>>,
387        Read<'a, EventBus<CreateNpcEvent>>,
388        Read<'a, EventBus<DeleteEvent>>,
389        WriteExpect<'a, RtSim>,
390        ReadExpect<'a, Arc<world::World>>,
391        ReadExpect<'a, world::IndexOwned>,
392        ReadExpect<'a, SlowJobPool>,
393        ReadStorage<'a, comp::Pos>,
394        ReadStorage<'a, RtSimEntity>,
395        WriteStorage<'a, comp::Agent>,
396        ReadStorage<'a, Presence>,
397        ReadExpect<'a, Calendar>,
398        Read<'a, IdMaps>,
399        ReadExpect<'a, ServerConstants>,
400        ReadExpect<'a, WeatherGrid>,
401        WriteStorage<'a, comp::Inventory>,
402        WriteExpect<'a, comp::gizmos::RtsimGizmos>,
403        ReadExpect<'a, comp::tool::AbilityMap>,
404        ReadExpect<'a, comp::item::MaterialStatManifest>,
405    );
406
407    const NAME: &'static str = "rtsim::tick";
408    const ORIGIN: Origin = Origin::Server;
409    const PHASE: Phase = Phase::Create;
410
411    fn run(
412        _job: &mut Job<Self>,
413        (
414            entities,
415            dt,
416            time,
417            time_of_day,
418            create_ship_events,
419            create_npc_events,
420            delete_events,
421            mut rtsim,
422            world,
423            index,
424            slow_jobs,
425            positions,
426            rtsim_entities,
427            mut agents,
428            presences,
429            calendar,
430            id_maps,
431            server_constants,
432            weather_grid,
433            inventories,
434            rtsim_gizmos,
435            ability_map,
436            msm,
437        ): Self::SystemData,
438    ) {
439        let mut create_ship_emitter = create_ship_events.emitter();
440        let mut create_npc_emitter = create_npc_events.emitter();
441        let mut delete_emitter = delete_events.emitter();
442        let rtsim = &mut *rtsim;
443        let calendar_data = (*time_of_day, (*calendar).clone());
444
445        // Set up rtsim inputs
446        {
447            let mut data = rtsim.state.data_mut();
448
449            // Update time of day
450            data.time_of_day = *time_of_day;
451
452            // Update character map (i.e: so that rtsim knows where players are)
453            // TODO: Other entities too like animals? Or do we now care about that?
454            data.npcs.character_map.clear();
455            for (presence, wpos) in (&presences, &positions).join() {
456                if let PresenceKind::Character(character) = &presence.kind {
457                    let chunk_pos = wpos.0.xy().as_().wpos_to_cpos();
458                    data.npcs
459                        .character_map
460                        .entry(chunk_pos)
461                        .or_default()
462                        .push((*character, wpos.0));
463                }
464            }
465        }
466
467        // Tick rtsim
468        rtsim.state.tick(
469            &mut NpcSystemData {
470                positions: positions.clone(),
471                id_maps,
472                server_constants,
473                weather_grid,
474                inventories: Mutex::new(inventories),
475                rtsim_gizmos,
476                ability_map,
477                msm,
478            },
479            &world,
480            index.as_index_ref(),
481            *time_of_day,
482            *time,
483            dt.0,
484        );
485
486        // Perform a save if required
487        if rtsim
488            .last_saved
489            .is_none_or(|ls| ls.elapsed() > Duration::from_secs(60))
490        {
491            // TODO: Use slow jobs
492            let _ = slow_jobs;
493            rtsim.save(/* &slow_jobs, */ false);
494        }
495
496        let chunk_states = rtsim.state.resource::<ChunkStates>();
497        let data = &mut *rtsim.state.data_mut();
498
499        let mut create_event = |id: NpcId, npc: &Npc, steering: Option<NpcBuilder>| match npc.body {
500            Body::Ship(body) => {
501                create_ship_emitter.emit(CreateShipEvent {
502                    pos: comp::Pos(npc.wpos),
503                    ori: comp::Ori::from(Dir::new(npc.dir.with_z(0.0))),
504                    ship: body,
505                    rtsim_entity: Some(id),
506                    driver: steering,
507                });
508            },
509            _ => {
510                let entity_info = get_npc_entity_info(
511                    npc,
512                    &data.sites,
513                    index.as_index_ref(),
514                    Some(&calendar_data),
515                );
516
517                let (mut npc_builder, pos) = SpawnEntityData::from_entity_info(entity_info)
518                    .into_npc_data_inner()
519                    .expect("Entity loaded from assets cannot be special")
520                    .to_npc_builder();
521
522                if let Some(agent) = &mut npc_builder.agent {
523                    agent.rtsim_outbox = Some(Default::default());
524                }
525
526                if let Some(health) = &mut npc_builder.health {
527                    health.set_fraction(npc.health_fraction);
528                }
529
530                create_npc_emitter.emit(CreateNpcEvent {
531                    pos,
532                    ori: comp::Ori::from(Dir::new(npc.dir.with_z(0.0))),
533                    npc: npc_builder.with_rtsim(id).with_rider(steering),
534                });
535            },
536        };
537
538        // Load in mounted npcs and their riders
539        for mount in data.npcs.mounts.iter_mounts() {
540            let mount_npc = data.npcs.npcs.get_mut(mount).expect("This should exist");
541            let chunk = mount_npc.wpos.xy().as_::<i32>().wpos_to_cpos();
542
543            if matches!(mount_npc.mode, SimulationMode::Simulated)
544                && chunk_states.0.get(chunk).is_some_and(|c| c.is_some())
545            {
546                mount_npc.mode = SimulationMode::Loaded;
547
548                let mut actor_info = |actor: Actor| {
549                    let npc_id = actor.npc()?;
550                    let npc = data.npcs.npcs.get_mut(npc_id)?;
551                    if matches!(npc.mode, SimulationMode::Simulated) {
552                        npc.mode = SimulationMode::Loaded;
553                        let entity_info = get_npc_entity_info(
554                            npc,
555                            &data.sites,
556                            index.as_index_ref(),
557                            Some(&calendar_data),
558                        );
559
560                        let mut npc_builder = SpawnEntityData::from_entity_info(entity_info)
561                            .into_npc_data_inner()
562                            // EntityConfig can't represent Waypoints at all
563                            // as of now, and if someone will try to spawn
564                            // rtsim waypoint it is definitely error.
565                            .expect("Entity loaded from assets cannot be special")
566                            .to_npc_builder()
567                            .0
568                            .with_rtsim(npc_id);
569
570                        if let Some(agent) = &mut npc_builder.agent {
571                            agent.rtsim_outbox = Some(Default::default());
572                        }
573
574                        Some(npc_builder)
575                    } else {
576                        error!("Npc is loaded but vehicle is unloaded");
577                        None
578                    }
579                };
580
581                let steerer = data
582                    .npcs
583                    .mounts
584                    .get_steerer_link(mount)
585                    .and_then(|link| actor_info(link.rider));
586
587                let mount_npc = data.npcs.npcs.get(mount).expect("This should exist");
588                create_event(mount, mount_npc, steerer);
589            }
590        }
591
592        // Load in NPCs
593        for (npc_id, npc) in data.npcs.npcs.iter_mut() {
594            let chunk = npc.wpos.xy().as_::<i32>().wpos_to_cpos();
595
596            // Load the NPC into the world if it's in a loaded chunk and is not already
597            // loaded
598            if matches!(npc.mode, SimulationMode::Simulated)
599                && chunk_states.0.get(chunk).is_some_and(|c| c.is_some())
600                // Riding npcs will be spawned by the vehicle.
601                && data.npcs.mounts.get_mount_link(npc_id).is_none()
602            {
603                npc.mode = SimulationMode::Loaded;
604                create_event(npc_id, npc, None);
605            }
606        }
607
608        // Synchronise rtsim NPC with entity data
609        for (entity, pos, rtsim_entity, agent) in (
610            &entities,
611            &positions,
612            &rtsim_entities,
613            (&mut agents).maybe(),
614        )
615            .join()
616        {
617            if let Some(npc) = data.npcs.get_mut(*rtsim_entity) {
618                match npc.mode {
619                    SimulationMode::Loaded => {
620                        // Update rtsim NPC state
621                        npc.wpos = pos.0;
622
623                        // Update entity state
624                        if let Some(agent) = agent {
625                            agent.rtsim_controller.personality = npc.personality;
626                            agent.rtsim_controller.look_dir = npc.controller.look_dir;
627                            agent.rtsim_controller.activity = npc.controller.activity;
628                            agent
629                                .rtsim_controller
630                                .actions
631                                .extend(std::mem::take(&mut npc.controller.actions));
632                            if let Some(rtsim_outbox) = &mut agent.rtsim_outbox {
633                                npc.inbox.append(rtsim_outbox);
634                            }
635                        }
636                    },
637                    SimulationMode::Simulated => {
638                        delete_emitter.emit(DeleteEvent(entity));
639                    },
640                }
641            }
642        }
643    }
644}