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 {
286 let mut data = rtsim.state.data_mut();
287
288 data.time_of_day = *time_of_day;
290
291 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 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 if rtsim
318 .last_saved
319 .is_none_or(|ls| ls.elapsed() > Duration::from_secs(60))
320 {
321 let _ = slow_jobs;
323 rtsim.save(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 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 .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 for (npc_id, npc) in data.npcs.npcs.iter_mut() {
425 let chunk = npc.wpos.xy().as_::<i32>().wpos_to_cpos();
426
427 if matches!(npc.mode, SimulationMode::Simulated)
430 && chunk_states.0.get(chunk).is_some_and(|c| c.is_some())
431 && 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 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 npc.wpos = pos.0;
453
454 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}