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::plot::PlotKindMeta;
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.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                            let ally_faction = match (
87                                npc.faction.and_then(|f| data.factions.get(f)),
88                                site.faction.and_then(|f| data.factions.get(f)),
89                            ) {
90                                (None, None) => true,
91                                (None, Some(_)) => true,
92                                (Some(_), None) => true,
93                                (Some(npc_faction), Some(site_faction)) => {
94                                    npc_faction.good_or_evil == site_faction.good_or_evil
95                                },
96                            };
97
98                            // See if there is at least one house in this site.
99                            let has_house = site.world_site.is_some_and(|ws| {
100                                ctx.index.sites.get(ws).any_plot(|p| {
101                                    matches!(p.meta(), Some(PlotKindMeta::House { .. }))
102                                })
103                            });
104
105                            ally_faction && has_house
106                        })
107                        .min_by_key(|(_, site)| {
108                            site.wpos.as_().distance_squared(npc.wpos.xy()) as i32
109                        })
110                        .map(|(site_id, _)| site_id);
111                }
112            }
113
114            /*
115               When rtsim is first generated, the airship captain NPCs will have been assigned a route at this point.
116               When loading existing rtsim data, the captain NPCs will have no route assigned at this point.
117               If the world has changed, the routes may have changed.
118
119               First, get all the location where airships can spawn. All available spawning points for airships must be used.
120               It does not matter that site ids may be moved around. A captain may be assigned to any site, and
121               it does not have to be the site that was previously assigned to the captain.
122
123               First, use all existing captains:
124               For each captain
125                   If captain is not assigned to a route
126                       if a spawning point is available
127                           Register the captain for the route that uses the spawning point.
128                           Remove the spawning point from the list of available spawning points.
129                       else
130                           Delete the captain (& airship) pair
131                       end
132                   End
133               End
134
135               Then use all remaining spawning points:
136               while there are available spawning points
137                   spawn a new captain/airship pair (there won't be existing captains for these)
138                   Register the captain for the route that uses the spawning point.
139                   Remove the spawning point from the list of available spawning points
140               End
141            */
142
143            // get all the places to spawn an airship
144            let mut spawning_locations = data.airship_spawning_locations(ctx.world, ctx.index);
145
146            // The captains can't be registered inline with this code because it requires
147            // mutable access to data.
148            let mut captains_to_register = Vec::new();
149            for captain_id in airship_captains.iter() {
150                if let Some(mount_link) = data.npcs.mounts.get_mount_link(*captain_id) {
151                    let airship_id = mount_link.mount;
152                    if data.airship_sim.assigned_routes.get(captain_id).is_none() {
153                        if let Some(spawning_location) = spawning_locations.pop() {
154                            captains_to_register.push((*captain_id, airship_id, spawning_location));
155                        } else {
156                            // delete the captain (& airship) pair
157                            data.npcs.remove(*captain_id);
158                            data.npcs.remove(airship_id);
159                        }
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                // The airship position returned by register_airship_captain is the approach
175                // final position. From there, the airship will fly a transition
176                // phase to directly above the docking position.
177                if let Some(airship_pos) = data.airship_sim.register_airship_captain(
178                    spawning_location.docking_pos.map(|i| i as f32),
179                    *captain_id,
180                    *airship_id,
181                    ctx.index.index,
182                    &ctx.world.civs().airships,
183                ) {
184                    // move the airship (the captain is the rider) into position
185                    let airship = data.npcs.get_mut(*airship_id).unwrap();
186                    airship.wpos = airship_pos;
187                }
188            }
189
190            // Group the airship captains by route
191            data.airship_sim
192                .configure_route_pilots(&ctx.world.civs().airships);
193        });
194
195        Ok(Self)
196    }
197}