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}