veloren_rtsim/rule/migrate.rs
1use crate::{
2 RtState, Rule, RuleError,
3 data::{Site, npc::Profession},
4 event::OnSetup,
5};
6use rand::prelude::*;
7use rand_chacha::ChaChaRng;
8use tracing::warn;
9use world::site::SiteKind;
10
11/// This rule runs at rtsim startup and broadly acts to perform some primitive
12/// migration/sanitisation in order to ensure that the state of rtsim is mostly
13/// sensible.
14pub struct Migrate;
15
16impl Rule for Migrate {
17 fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
18 rtstate.bind::<Self, OnSetup>(|ctx| {
19 let data = &mut *ctx.state.data_mut();
20
21 let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>());
22
23 // Delete rtsim sites that don't correspond to a world site
24 data.sites.sites.retain(|site_id, site| {
25 if let Some((world_site_id, _)) = ctx
26 .index
27 .sites
28 .iter()
29 .find(|(_, world_site)| world_site.get_origin() == site.wpos)
30 {
31 site.world_site = Some(world_site_id);
32 data.sites.world_site_map.insert(world_site_id, site_id);
33 true
34 } else {
35 warn!(
36 "{:?} is no longer valid because the site it was derived from no longer \
37 exists. It will now be deleted.",
38 site_id
39 );
40 false
41 }
42 });
43
44 // Generate rtsim sites for world sites that don't have a corresponding rtsim
45 // site yet
46 for (world_site_id, _) in ctx.index.sites.iter() {
47 if !data.sites.values().any(|site| {
48 site.world_site
49 .expect("Rtsim site not assigned to world site")
50 == world_site_id
51 }) {
52 warn!(
53 "{:?} is new and does not have a corresponding rtsim site. One will now \
54 be generated afresh.",
55 world_site_id
56 );
57 data.sites.create(Site::generate(
58 world_site_id,
59 ctx.world,
60 ctx.index,
61 &[],
62 &data.factions,
63 &mut rng,
64 ));
65 }
66 }
67
68 // Reassign NPCs to sites if their old one was deleted. If they were already
69 // homeless, no need to do anything.
70 // Keep track of airship captains separately, as they need to be handled
71 // differently.
72 let mut airship_captains = Vec::new();
73 for (key, npc) in data.npcs.iter_mut() {
74 // For airships, just collect the captains for now
75 if matches!(npc.profession(), Some(Profession::Captain)) {
76 airship_captains.push(key);
77 } else if let Some(home) = npc.home
78 && !data.sites.contains_key(home)
79 {
80 // Choose the closest habitable site as the new home for the NPC
81 npc.home = data
82 .sites
83 .sites
84 .iter()
85 .filter(|(_, site)| {
86 // TODO: This is a bit silly, but needs to wait on the removal of site1
87 site.world_site.is_some_and(|ws| {
88 matches!(
89 &ctx.index.sites.get(ws).kind,
90 SiteKind::Refactor(_)
91 | SiteKind::CliffTown(_)
92 | SiteKind::SavannahTown(_)
93 | SiteKind::CoastalTown(_)
94 | SiteKind::DesertCity(_)
95 )
96 })
97 })
98 .min_by_key(|(_, site)| {
99 site.wpos.as_().distance_squared(npc.wpos.xy()) as i32
100 })
101 .map(|(site_id, _)| site_id);
102 }
103 }
104
105 /*
106 When rtsim is first generated, the airship captain NPCs will have been assigned a route at this point.
107 When loading existing rtsim data, the captain NPCs will have no route assigned at this point.
108 If the world has changed, the routes may have changed.
109
110 First, get all the location where airships can spawn. All available spawning points for airships must be used.
111 It does not matter that site ids may be moved around. A captain may be assigned to any site, and
112 it does not have to be the site that was previously assigned to the captain.
113
114 First, use all existing captains:
115 For each captain
116 If captain is not assigned to a route
117 if a spawning point is available
118 Register the captain for the route that uses the spawning point.
119 Remove the spawning point from the list of available spawning points.
120 else
121 Delete the captain (& airship) pair
122 end
123 End
124 End
125
126 Then use all remaining spawning points:
127 while there are available spawning points
128 spawn a new captain/airship pair (there won't be existing captains for these)
129 Register the captain for the route that uses the spawning point.
130 Remove the spawning point from the list of available spawning points
131 End
132 */
133
134 // get all the places to spawn an airship
135 let mut spawning_locations = data.airship_spawning_locations(ctx.world, ctx.index);
136
137 // The captains can't be registered inline with this code because it requires
138 // mutable access to data.
139 let mut captains_to_register = Vec::new();
140 for captain_id in airship_captains.iter() {
141 if let Some(mount_link) = data.npcs.mounts.get_mount_link(*captain_id) {
142 let airship_id = mount_link.mount;
143 if data.airship_sim.assigned_routes.get(captain_id).is_none() {
144 if let Some(spawning_location) = spawning_locations.pop() {
145 captains_to_register.push((*captain_id, airship_id, spawning_location));
146 } else {
147 // delete the captain (& airship) pair
148 data.npcs.remove(*captain_id);
149 data.npcs.remove(airship_id);
150 }
151 }
152 }
153 }
154 // All spawning points must be filled, so spawn new airships for any remaining
155 // points.
156 while let Some(spawning_location) = spawning_locations.pop() {
157 let (captain_id, airship_id) = data.spawn_airship(&spawning_location, &mut rng);
158 captains_to_register.push((captain_id, airship_id, spawning_location));
159 }
160
161 // Register all of the airship captains with airship operations. This can't be
162 // done inside the previous loop because this requires mutable
163 // access to this (data).
164 for (captain_id, airship_id, spawning_location) in captains_to_register.iter() {
165 // The airship position returned by register_airship_captain is the approach
166 // final position. From there, the airship will fly a transition
167 // phase to directly above the docking position.
168 if let Some(airship_pos) = data.airship_sim.register_airship_captain(
169 spawning_location.docking_pos.map(|i| i as f32),
170 *captain_id,
171 *airship_id,
172 ctx.index.index,
173 &ctx.world.civs().airships,
174 ) {
175 // move the airship (the captain is the rider) into position
176 let airship = data.npcs.get_mut(*airship_id).unwrap();
177 airship.wpos = airship_pos;
178 }
179 }
180
181 // Group the airship captains by route
182 data.airship_sim
183 .configure_route_pilots(&ctx.world.civs().airships);
184 });
185
186 Ok(Self)
187 }
188}