1
2
Fork 0
mirror of https://github.com/mat-1/azalea.git synced 2025-08-02 14:26:04 +00:00
azalea/azalea-physics/tests/physics.rs
2025-05-02 01:20:25 +00:00

560 lines
16 KiB
Rust

use std::sync::Arc;
use azalea_block::{
BlockState, block_state::BlockStateIntegerRepr, fluid_state::to_or_from_legacy_fluid_level,
properties::WaterLevel,
};
use azalea_core::{
position::{BlockPos, ChunkPos, Vec3},
registry_holder::RegistryHolder,
resource_location::ResourceLocation,
tick::GameTick,
};
use azalea_entity::{EntityBundle, EntityPlugin, LocalEntity, Physics, Position};
use azalea_physics::PhysicsPlugin;
use azalea_world::{Chunk, Instance, InstanceContainer, MinecraftEntityId, PartialInstance};
use bevy_app::App;
use parking_lot::RwLock;
use uuid::Uuid;
/// You need an app to spawn entities in the world and do updates.
fn make_test_app() -> App {
let mut app = App::new();
app.add_plugins((PhysicsPlugin, EntityPlugin))
.init_resource::<InstanceContainer>();
app
}
pub fn insert_overworld(app: &mut App) -> Arc<RwLock<Instance>> {
app.world_mut()
.resource_mut::<InstanceContainer>()
.get_or_insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
&RegistryHolder::default(),
)
}
#[test]
fn test_gravity() {
let mut app = make_test_app();
let world_lock = insert_overworld(&mut app);
let mut partial_world = PartialInstance::default();
// the entity has to be in a loaded chunk for physics to work
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 0.,
y: 70.,
z: 0.,
},
azalea_registry::EntityKind::Zombie,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
{
let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
// y should start at 70
assert_eq!(entity_pos.y, 70.);
}
app.update();
app.world_mut().run_schedule(GameTick);
app.update();
{
let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
// delta is applied before gravity, so the first tick only sets the delta
assert_eq!(entity_pos.y, 70.);
let entity_physics = app.world_mut().get::<Physics>(entity).unwrap();
assert!(entity_physics.velocity.y < 0.);
}
app.world_mut().run_schedule(GameTick);
app.update();
{
let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
// the second tick applies the delta to the position, so now it should go down
assert!(
entity_pos.y < 70.,
"Entity y ({}) didn't go down after physics steps",
entity_pos.y
);
}
}
#[test]
fn test_collision() {
let mut app = make_test_app();
let world_lock = insert_overworld(&mut app);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 0.5,
y: 70.,
z: 0.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let block_state = partial_world.chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 },
azalea_registry::Block::Stone.into(),
&world_lock.write().chunks,
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
app.update();
app.world_mut().run_schedule(GameTick);
app.update();
{
let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
// delta will change, but it won't move until next tick
assert_eq!(entity_pos.y, 70.);
let entity_physics = app.world_mut().get::<Physics>(entity).unwrap();
assert!(entity_physics.velocity.y < 0.);
}
app.world_mut().run_schedule(GameTick);
app.update();
{
let entity_pos = *app.world_mut().get::<Position>(entity).unwrap();
// the second tick applies the delta to the position, but it also does collision
assert_eq!(entity_pos.y, 70.);
}
}
#[test]
fn test_slab_collision() {
let mut app = make_test_app();
let world_lock = insert_overworld(&mut app);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 0.5,
y: 71.,
z: 0.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let block_state = partial_world.chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 },
azalea_block::blocks::StoneSlab {
kind: azalea_block::properties::Type::Bottom,
waterlogged: false,
}
.into(),
&world_lock.write().chunks,
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
// do a few steps so we fall on the slab
for _ in 0..20 {
app.world_mut().run_schedule(GameTick);
app.update();
}
let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 69.5);
}
#[test]
fn test_top_slab_collision() {
let mut app = make_test_app();
let world_lock = insert_overworld(&mut app);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 0.5,
y: 71.,
z: 0.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let block_state = world_lock.write().chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 },
azalea_block::blocks::StoneSlab {
kind: azalea_block::properties::Type::Top,
waterlogged: false,
}
.into(),
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
// do a few steps so we fall on the slab
for _ in 0..20 {
app.world_mut().run_schedule(GameTick);
app.update();
}
let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 70.);
}
#[test]
fn test_weird_wall_collision() {
let mut app = make_test_app();
let world_lock = app
.world_mut()
.resource_mut::<InstanceContainer>()
.get_or_insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
&RegistryHolder::default(),
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 0.5,
y: 73.,
z: 0.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let block_state = world_lock.write().chunks.set_block_state(
&BlockPos { x: 0, y: 69, z: 0 },
azalea_block::blocks::CobblestoneWall {
east: azalea_block::properties::WallEast::Low,
north: azalea_block::properties::WallNorth::Low,
south: azalea_block::properties::WallSouth::Low,
west: azalea_block::properties::WallWest::Low,
up: false,
waterlogged: false,
}
.into(),
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
// do a few steps so we fall on the wall
for _ in 0..20 {
app.world_mut().run_schedule(GameTick);
app.update();
}
let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 70.5);
}
#[test]
fn test_negative_coordinates_weird_wall_collision() {
let mut app = make_test_app();
let world_lock = app
.world_mut()
.resource_mut::<InstanceContainer>()
.get_or_insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
&RegistryHolder::default(),
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: -1, z: -1 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: -7.5,
y: 73.,
z: -7.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let block_state = world_lock.write().chunks.set_block_state(
&BlockPos {
x: -8,
y: 69,
z: -8,
},
azalea_block::blocks::CobblestoneWall {
east: azalea_block::properties::WallEast::Low,
north: azalea_block::properties::WallNorth::Low,
south: azalea_block::properties::WallSouth::Low,
west: azalea_block::properties::WallWest::Low,
up: false,
waterlogged: false,
}
.into(),
);
assert!(
block_state.is_some(),
"Block state should exist, if this fails that means the chunk wasn't loaded and the block didn't get placed"
);
// do a few steps so we fall on the wall
for _ in 0..20 {
app.world_mut().run_schedule(GameTick);
app.update();
}
let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
assert_eq!(entity_pos.y, 70.5);
}
#[test]
fn spawn_and_unload_world() {
let mut app = make_test_app();
let world_lock = app
.world_mut()
.resource_mut::<InstanceContainer>()
.get_or_insert(
ResourceLocation::new("minecraft:overworld"),
384,
-64,
&RegistryHolder::default(),
);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: -1, z: -1 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let _entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: -7.5,
y: 73.,
z: -7.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
// do a tick
app.world_mut().run_schedule(GameTick);
app.update();
// now unload the partial_world and world_lock
drop(partial_world);
drop(world_lock);
// do another tick
app.world_mut().run_schedule(GameTick);
app.update();
}
#[test]
fn test_afk_pool() {
let mut app = make_test_app();
let world_lock = insert_overworld(&mut app);
let mut partial_world = PartialInstance::default();
partial_world.chunks.set(
&ChunkPos { x: 0, z: 0 },
Some(Chunk::default()),
&mut world_lock.write().chunks,
);
let setblock = |x: i32, y: i32, z: i32, b: BlockState| {
world_lock
.write()
.chunks
.set_block_state(&BlockPos { x, y, z }, b);
};
let stone = azalea_block::blocks::Stone {}.into();
let sign = azalea_block::blocks::OakSign {
rotation: azalea_block::properties::OakSignRotation::_0,
waterlogged: false,
}
.into();
let water = |level: u8| {
BlockState::from(azalea_block::blocks::Water {
level: WaterLevel::from(to_or_from_legacy_fluid_level(level) as BlockStateIntegerRepr),
})
};
let mut y = 69;
// first layer
{
setblock(1, y, 1, stone);
setblock(2, y, 1, stone);
setblock(3, y, 1, stone);
setblock(3, y, 2, stone);
setblock(3, y, 3, stone);
setblock(2, y, 3, stone);
setblock(1, y, 3, stone);
setblock(1, y, 2, stone);
}
// second layer
y += 1;
{
setblock(1, y, 0, stone);
setblock(2, y, 0, stone);
setblock(3, y, 0, stone);
setblock(0, y, 1, stone);
setblock(0, y, 2, stone);
setblock(0, y, 3, stone);
setblock(1, y, 4, stone);
setblock(2, y, 4, stone);
setblock(3, y, 4, stone);
setblock(4, y, 1, stone);
setblock(4, y, 2, stone);
setblock(4, y, 3, stone);
// middle block
setblock(2, y, 2, stone);
// sign
setblock(1, y, 1, sign);
// water
setblock(1, y, 2, water(8));
setblock(1, y, 3, water(7));
setblock(2, y, 3, water(6));
setblock(3, y, 3, water(5));
setblock(3, y, 2, water(4));
setblock(3, y, 1, water(3));
setblock(2, y, 1, water(2));
}
// third layer
y += 1;
{
setblock(1, y, 1, water(8));
setblock(2, y, 1, sign);
}
let entity = app
.world_mut()
.spawn((
EntityBundle::new(
Uuid::nil(),
Vec3 {
x: 3.5,
y: 70.,
z: 1.5,
},
azalea_registry::EntityKind::Player,
ResourceLocation::new("minecraft:overworld"),
),
MinecraftEntityId(0),
LocalEntity,
))
.id();
let mut blocks_visited = Vec::new();
let mut loops_done = 0;
for _ in 0..300 {
app.world_mut().run_schedule(GameTick);
app.update();
let entity_pos = app.world_mut().get::<Position>(entity).unwrap();
let entity_block_pos = BlockPos::from(entity_pos);
if !blocks_visited.contains(&entity_block_pos) {
blocks_visited.push(entity_block_pos);
if blocks_visited.len() == 8 {
loops_done += 1;
blocks_visited.clear();
}
}
}
assert_eq!(
blocks_visited.into_iter().collect::<Vec<_>>(),
vec![
BlockPos::new(3, 70, 2),
BlockPos::new(3, 70, 1),
BlockPos::new(2, 70, 1),
BlockPos::new(1, 70, 1),
]
);
assert_eq!(loops_done, 1);
}