Skip to main content

azalea/pathfinder/
simulation.rs

1//! Simulate the Minecraft world, currently only used for tests.
2
3use std::sync::Arc;
4
5use azalea_client::{
6    ClientMovementState, interact::BlockStatePredictionHandler, local_player::PreviousGameMode,
7    mining::MineBundle,
8};
9use azalea_core::{
10    entity_id::MinecraftEntityId, game_type::GameMode, position::Vec3, tick::GameTick,
11};
12use azalea_entity::{
13    Attributes, LookDirection, Physics, Position, dimensions::EntityDimensions,
14    inventory::Inventory,
15};
16use azalea_registry::builtin::EntityKind;
17use azalea_world::{ChunkStorage, PartialWorld, World, WorldName, Worlds};
18use bevy_app::App;
19use bevy_ecs::{prelude::*, schedule::SingleThreadedExecutor};
20use parking_lot::RwLock;
21use uuid::Uuid;
22
23#[derive(Bundle, Clone)]
24pub struct SimulatedPlayerBundle {
25    pub position: Position,
26    pub physics: Physics,
27    pub physics_state: ClientMovementState,
28    pub look_direction: LookDirection,
29    pub attributes: Attributes,
30    pub inventory: Inventory,
31}
32
33impl SimulatedPlayerBundle {
34    pub fn new(position: Vec3) -> Self {
35        let dimensions = EntityDimensions::from(EntityKind::Player);
36
37        SimulatedPlayerBundle {
38            position: Position::new(position),
39            physics: Physics::new(&dimensions, position),
40            physics_state: ClientMovementState::default(),
41            look_direction: LookDirection::default(),
42            attributes: Attributes::new(EntityKind::Player),
43            inventory: Inventory::default(),
44        }
45    }
46}
47
48fn simulation_world_name() -> WorldName {
49    WorldName::new("azalea:simulation")
50}
51
52fn create_simulation_world(chunks: ChunkStorage) -> (App, Arc<RwLock<World>>) {
53    let world_name = simulation_world_name();
54
55    let world = Arc::new(RwLock::new(World {
56        chunks,
57        ..Default::default()
58    }));
59
60    let mut app = App::new();
61    // we don't use all the default azalea plugins because we don't need all of them
62    app.add_plugins((
63        azalea_physics::PhysicsPlugin,
64        azalea_entity::EntityPlugin,
65        azalea_client::movement::MovementPlugin,
66        super::PathfinderPlugin,
67        crate::bot::BotPlugin,
68        azalea_client::task_pool::TaskPoolPlugin::default(),
69        // for mining
70        azalea_client::inventory::InventoryPlugin,
71        azalea_client::mining::MiningPlugin,
72        azalea_client::interact::InteractPlugin,
73        azalea_client::loading::PlayerLoadedPlugin,
74    ))
75    .insert_resource(Worlds {
76        map: [(world_name.clone(), Arc::downgrade(&world.clone()))]
77            .iter()
78            .cloned()
79            .collect(),
80    });
81
82    app.edit_schedule(bevy_app::Main, |schedule| {
83        schedule.set_executor(SingleThreadedExecutor::new());
84    });
85
86    app.finish();
87
88    (app, world)
89}
90
91fn create_simulation_player_complete_bundle(
92    world: Arc<RwLock<World>>,
93    player: &SimulatedPlayerBundle,
94) -> impl Bundle {
95    let world_name = simulation_world_name();
96
97    (
98        MinecraftEntityId(0),
99        azalea_entity::LocalEntity,
100        azalea_entity::metadata::PlayerMetadataBundle::default(),
101        azalea_entity::EntityBundle::new(
102            Uuid::nil(),
103            *player.position,
104            EntityKind::Player,
105            world_name,
106        ),
107        azalea_client::local_player::WorldHolder {
108            // the partial world is never actually used by the pathfinder, so we can leave it empty
109            partial: Arc::new(RwLock::new(PartialWorld::default())),
110            shared: world.clone(),
111        },
112        Inventory::default(),
113        GameMode::Survival,
114        PreviousGameMode(None),
115        MineBundle::default(),
116        BlockStatePredictionHandler::default(),
117        azalea_client::local_player::PermissionLevel::default(),
118        azalea_entity::PlayerAbilities::default(),
119    )
120}
121
122pub fn create_simulation_player(
123    ecs: &mut bevy_ecs::world::World,
124    world: Arc<RwLock<World>>,
125    player: SimulatedPlayerBundle,
126) -> Entity {
127    let mut entity = ecs.spawn(create_simulation_player_complete_bundle(world, &player));
128    entity.insert(player);
129    entity.id()
130}
131
132/// Simulate the Minecraft world to see if certain movements would be possible.
133pub struct Simulation {
134    pub app: App,
135    pub entity: Entity,
136    pub world: Arc<RwLock<World>>,
137}
138
139impl Simulation {
140    pub fn new(chunks: ChunkStorage, player: SimulatedPlayerBundle) -> Self {
141        let (mut app, world) = create_simulation_world(chunks);
142        let entity = create_simulation_player(app.world_mut(), world.clone(), player);
143        Self { app, entity, world }
144    }
145
146    /// Despawn the old simulated player and create a new one.
147    ///
148    /// This is cheaper than creating a new [`Simulation`] from scratch.
149    pub fn reset(&mut self, player: SimulatedPlayerBundle) {
150        self.app.world_mut().despawn(self.entity);
151        let entity = create_simulation_player(self.app.world_mut(), self.world.clone(), player);
152        self.entity = entity;
153    }
154
155    pub fn tick(&mut self) {
156        self.run_update_schedule();
157        self.run_gametick_schedule();
158    }
159
160    pub fn run_update_schedule(&mut self) {
161        self.app.update();
162    }
163    pub fn run_gametick_schedule(&mut self) {
164        self.app.world_mut().run_schedule(GameTick);
165    }
166
167    pub fn component<T: Component + Clone>(&self) -> T {
168        self.app.world().get::<T>(self.entity).unwrap().clone()
169    }
170    pub fn get_component<T: Component + Clone>(&self) -> Option<T> {
171        self.app.world().get::<T>(self.entity).cloned()
172    }
173    pub fn position(&self) -> Vec3 {
174        *self.component::<Position>()
175    }
176    pub fn physics(&self) -> Physics {
177        self.component::<Physics>().clone()
178    }
179    pub fn is_mining(&self) -> bool {
180        // return true if the component is present and Some
181        self.get_component::<azalea_client::mining::MineBlockPos>()
182            .and_then(|c| *c)
183            .is_some()
184    }
185}
186
187/// A set of simulations, useful for efficiently doing multiple simulations.
188pub struct SimulationSet {
189    pub app: App,
190    world: Arc<RwLock<World>>,
191}
192impl SimulationSet {
193    pub fn new(chunks: ChunkStorage) -> Self {
194        let (app, world) = create_simulation_world(chunks);
195        Self { app, world }
196    }
197    pub fn tick(&mut self) {
198        self.app.update();
199        self.app.world_mut().run_schedule(GameTick);
200    }
201
202    pub fn spawn(&mut self, player: SimulatedPlayerBundle) -> Entity {
203        create_simulation_player(self.app.world_mut(), self.world.clone(), player)
204    }
205    pub fn despawn(&mut self, entity: Entity) {
206        self.app.world_mut().despawn(entity);
207    }
208
209    pub fn position(&self, entity: Entity) -> Vec3 {
210        **self.app.world().get::<Position>(entity).unwrap()
211    }
212}