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