veloren_rtsim/rule/migrate.rs
1use crate::{
2 RtState, Rule, RuleError,
3 data::{
4 Site,
5 architect::{Population, TrackedPopulation},
6 npc::Profession,
7 },
8 event::OnSetup,
9 gen::wanted_population,
10};
11use rand::prelude::*;
12use rand_chacha::ChaChaRng;
13use tracing::warn;
14use world::site::plot::PlotKindMeta;
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(thread_rng().gen::<[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.npcs.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 ctx.index.sites.get(ws).any_plot(|p| {
106 matches!(p.meta(), Some(PlotKindMeta::House { .. }))
107 })
108 });
109
110 ally_faction && has_house
111 })
112 .min_by_key(|(_, site)| {
113 site.wpos.as_().distance_squared(npc.wpos.xy()) as i32
114 })
115 .map(|(site_id, _)| site_id);
116 }
117 }
118
119 /*
120 First, get all the location where airships can spawn. All available spawning points for airships must be used.
121 It does not matter that site ids may be moved around. A captain may be assigned to any site, and
122 it does not have to be the site that was previously assigned to the captain.
123
124 First, use all existing captains:
125 For each captain
126 If captain is not assigned to a route
127 if a spawning point is available
128 Register the captain for the route that uses the spawning point.
129 Remove the spawning point from the list of available spawning points.
130 else
131 Delete the captain (& airship) pair
132 end
133 End
134 End
135
136 Then use all remaining spawning points:
137 while there are available spawning points
138 spawn a new captain/airship pair (there won't be existing captains for these)
139 Register the captain for the route that uses the spawning point.
140 Remove the spawning point from the list of available spawning points
141 End
142 */
143
144 // get all the places to spawn an airship
145 let mut spawning_locations = data.airship_spawning_locations(ctx.world);
146
147 // The captains can't be registered inline with this code because it requires
148 // mutable access to data.
149 let mut captains_to_register = Vec::new();
150 for captain_id in airship_captains.iter() {
151 if let Some(mount_link) = data.npcs.mounts.get_mount_link(*captain_id) {
152 let airship_id = mount_link.mount;
153 assert!(data.airship_sim.assigned_routes.get(captain_id).is_none());
154 if let Some(spawning_location) = spawning_locations.pop() {
155 captains_to_register.push((*captain_id, airship_id, spawning_location));
156 } else {
157 // delete the captain (& airship) pair
158 data.npcs.remove(*captain_id);
159 data.npcs.remove(airship_id);
160 }
161 }
162 }
163 // All spawning points must be filled, so spawn new airships for any remaining
164 // points.
165 while let Some(spawning_location) = spawning_locations.pop() {
166 let (captain_id, airship_id) = data.spawn_airship(&spawning_location, &mut rng);
167 captains_to_register.push((captain_id, airship_id, spawning_location));
168 }
169
170 // Register all of the airship captains with airship operations. This can't be
171 // done inside the previous loop because this requires mutable
172 // access to this (data).
173 for (captain_id, airship_id, spawning_location) in captains_to_register.iter() {
174 data.airship_sim.register_airship_captain(
175 spawning_location,
176 *captain_id,
177 *airship_id,
178 ctx.world,
179 &mut data.npcs,
180 );
181 }
182
183 // Group the airship captains by route
184 data.airship_sim
185 .configure_route_pilots(&ctx.world.civs().airships, &data.npcs);
186
187 // Calculate architect populations
188 data.architect.wanted_population = wanted_population(ctx.world, ctx.index);
189
190 data.architect.population = Population::default();
191
192 for npc in data.npcs.values() {
193 let pop = TrackedPopulation::from_body_and_role(&npc.body, &npc.role);
194 data.architect.population.add(pop, 1);
195 }
196
197 for death in data.architect.deaths.iter() {
198 data.architect.population.on_spawn(death);
199 }
200 });
201
202 Ok(Self)
203 }
204}