mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 17:21:49 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8674aa784d | ||
|
|
f08a1016fd | ||
|
|
4aa1f8e14a |
No files matched your search
@@ -121,6 +121,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tick_frozen: false,
|
||||
tick_steps: (0, 0),
|
||||
collision: None,
|
||||
border: &horizon_client::worldborder::WorldBorder::default(),
|
||||
};
|
||||
for event in hub.tick(&mut ctx) {
|
||||
match event {
|
||||
|
||||
@@ -78,6 +78,7 @@ fn main() -> Result<()> {
|
||||
let mut selected = collisions::EntityCollisions {
|
||||
blocks: scene,
|
||||
entities: &entities,
|
||||
border: None,
|
||||
};
|
||||
let movement = world_movement::resolve_world_movement(
|
||||
&mut selected,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! Entity-dependent rules use the defaults for players and the supported mobs.
|
||||
use crate::{Result, world::World};
|
||||
use horizon_core::{
|
||||
geometry::{Aabb, Vec3, VoxelShape},
|
||||
geometry::{Aabb, Vec3, VoxelShape, border::BorderGeometry},
|
||||
position::{BlockPos, ChunkPos},
|
||||
};
|
||||
use horizon_world::{
|
||||
@@ -64,6 +64,9 @@ pub struct SceneCollisions<'w, 'r> {
|
||||
pub struct EntityCollisions<'a, W> {
|
||||
pub blocks: W,
|
||||
pub entities: &'a [Aabb],
|
||||
/// The world border and the moving entity's x and z
|
||||
/// (`CollisionGetter.borderCollision`); None: the blocks' answer.
|
||||
pub border: Option<(&'a BorderGeometry, [f64; 2])>,
|
||||
}
|
||||
impl<W: BlockCollisionWorld> BlockCollisionWorld for EntityCollisions<'_, W> {
|
||||
type Error = W::Error;
|
||||
@@ -114,12 +117,20 @@ impl<W: EntityMovementWorld<Error = io::Error>> EntityMovementWorld for EntityCo
|
||||
}
|
||||
Ok(shapes)
|
||||
}
|
||||
/// `WorldBorder.isInsideCloseToBorder`: an entity inside the border and
|
||||
/// near it collides with its walls.
|
||||
fn movement_border(
|
||||
&mut self,
|
||||
entity: EntityIdentity,
|
||||
bounds: Aabb,
|
||||
) -> io::Result<Option<VoxelShape>> {
|
||||
self.blocks.movement_border(entity, bounds)
|
||||
let Some((border, [x, z])) = self.border else {
|
||||
return self.blocks.movement_border(entity, bounds);
|
||||
};
|
||||
if !border.is_inside_close_to_border(x, z, bounds) {
|
||||
return Ok(None);
|
||||
}
|
||||
border.collision_shape().map(Some).map_err(io::Error::other)
|
||||
}
|
||||
}
|
||||
impl SceneCollisions<'_, '_> {
|
||||
|
||||
@@ -248,8 +248,32 @@ impl Game<'_> {
|
||||
|
||||
/// Every tick: the size change, the wall the player cannot pass and
|
||||
/// damage outside the border.
|
||||
/// `BorderChangeListener.DelegateBorderChangeListener`'s packets: what
|
||||
/// changed on the border since the guests were last told of it.
|
||||
pub(in crate::game) fn lan_sync_border(&mut self, host: &mut crate::game::lan::LanHost) {
|
||||
let now = horizon_client::lan::border::Sent::of(&self.border);
|
||||
if let Some(sent) = host.border_sent.replace(now) {
|
||||
for packet in sent.changes(&self.border) {
|
||||
host.hub.broadcast(&packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(in crate::game) fn tick_worldborder(&mut self) {
|
||||
self.border.tick();
|
||||
self.player_border();
|
||||
self.border_damage();
|
||||
}
|
||||
/// The player's movement collides with the border as it is now
|
||||
/// (`CollisionGetter.borderCollision`): from inside, its walls stop it.
|
||||
pub(in crate::game) fn player_border(&mut self) {
|
||||
if let Some(p) = self.player.as_mut() {
|
||||
p.border = Some(self.border.geometry().clone());
|
||||
}
|
||||
}
|
||||
/// `LivingEntity.baseTick`: a player outside the border, further than
|
||||
/// its safe zone, is hurt by the blocks beyond it. The game's player, or
|
||||
/// a guest's in their turn (`as_guest`).
|
||||
pub(in crate::game) fn border_damage(&mut self) {
|
||||
let Some((px, pz, half)) = self.player.as_ref().map(|p| {
|
||||
(
|
||||
p.position.x,
|
||||
@@ -269,20 +293,6 @@ impl Game<'_> {
|
||||
let amount = (-dist * self.border.damage_per_block).floor().max(1.0) as f32;
|
||||
self.hurt_player_by("minecraft:outside_border", amount);
|
||||
}
|
||||
// From inside the border the wall stops movement.
|
||||
if self.border.distance(px, pz) >= 0.0 {
|
||||
let [x, z] = self.border.clamp(px, pz, half);
|
||||
if let Some(p) = self.player.as_mut() {
|
||||
if x != px {
|
||||
p.velocity.x = 0.0;
|
||||
}
|
||||
if z != pz {
|
||||
p.velocity.z = 0.0;
|
||||
}
|
||||
p.position.x = x;
|
||||
p.position.z = z;
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(super) fn cmd_worldborder(&mut self, id: &str, ctx: &Context) -> R<i32> {
|
||||
let float2 = |v: f64| format!("{v:.2}");
|
||||
|
||||
@@ -979,6 +979,8 @@ impl Game<'_> {
|
||||
status["projectiles"] = json!(self.projectiles_status());
|
||||
// The particles made so far, by type.
|
||||
status["particles"] = json!(self.particles.made());
|
||||
// The world border this side knows of.
|
||||
status["border"] = json!({ "center": self.border.center(), "size": self.border.size() });
|
||||
// The scoreboard sidebar the HUD shows.
|
||||
status["sidebar"] = self.sidebar_status();
|
||||
status
|
||||
|
||||
@@ -66,6 +66,8 @@ pub(super) struct LanHost {
|
||||
/// whose respawn replaces their waypoint.
|
||||
pub waypoints: std::collections::HashMap<String, lan::waypoints::Waypoints>,
|
||||
pub waypoint_dead: std::collections::HashSet<u128>,
|
||||
/// The world border as the guests were last told of it (lan/border.rs).
|
||||
pub border_sent: Option<lan::border::Sent>,
|
||||
/// Guests' commands waiting for the hub to be back in `self.lan`, where
|
||||
/// a command can reach every player (game/lan_actions.rs).
|
||||
pub commands: Vec<super::lan_actions::GuestCommand>,
|
||||
@@ -561,6 +563,7 @@ impl Game<'_> {
|
||||
owner: None,
|
||||
waypoints: Default::default(),
|
||||
waypoint_dead: Default::default(),
|
||||
border_sent: None,
|
||||
commands: Vec::new(),
|
||||
states: Default::default(),
|
||||
});
|
||||
@@ -891,6 +894,7 @@ impl Game<'_> {
|
||||
tick_frozen: self.clock.frozen(),
|
||||
tick_steps: self.clock.step_state(),
|
||||
collision: Some(self.collision),
|
||||
border: &self.border,
|
||||
};
|
||||
host.hub.movement_checks = (
|
||||
self.ui.rules.bool("player_movement_check"),
|
||||
@@ -998,6 +1002,7 @@ impl Game<'_> {
|
||||
self.lan_update_models(&mut host);
|
||||
self.lan_update_waypoints(&mut host, state.as_ref());
|
||||
self.lan_sync_scoreboard(&mut host);
|
||||
self.lan_sync_border(&mut host);
|
||||
drop(holding);
|
||||
lan::flush_all(host.hub.guests.iter().map(|g| &g.sender));
|
||||
self.lan = Some(host);
|
||||
@@ -1074,6 +1079,7 @@ impl Game<'_> {
|
||||
tick_frozen: self.clock.frozen(),
|
||||
tick_steps: self.clock.step_state(),
|
||||
collision: Some(self.collision),
|
||||
border: &self.border,
|
||||
};
|
||||
host.hub.place_new_player(conn, &mut ctx);
|
||||
if elsewhere && let Err(e) = self.enter_level(Some(host), back) {
|
||||
|
||||
@@ -1581,6 +1581,7 @@ impl Game<'_> {
|
||||
tick_frozen: self.clock.frozen(),
|
||||
tick_steps: self.clock.step_state(),
|
||||
collision: Some(self.collision),
|
||||
border: &self.border,
|
||||
};
|
||||
host.hub.respawn(i, spawn, &ctx);
|
||||
if let Some(config) = elsewhere {
|
||||
@@ -1918,7 +1919,8 @@ impl Game<'_> {
|
||||
for d in travelled {
|
||||
game.movement_stats(Vec3::new(d[0], d[1], d[2]));
|
||||
}
|
||||
// LivingEntity.baseTick ends with tickEffects.
|
||||
// LivingEntity.baseTick: outside the world border, then tickEffects.
|
||||
game.border_damage();
|
||||
game.tick_mob_effects();
|
||||
let Some(player) = game.player.as_mut() else {
|
||||
return Vec::new();
|
||||
|
||||
@@ -3043,6 +3043,7 @@ impl Game<'_> {
|
||||
self.remote_handle_input();
|
||||
// ClientLevel.tick: the border's size change.
|
||||
self.border.tick();
|
||||
self.player_border();
|
||||
self.tick_remote_entities(running);
|
||||
// ClientLevel.tickBlockEntities: chest lids, shulker boxes, bells.
|
||||
self.tick_block_entity_anims();
|
||||
|
||||
@@ -455,6 +455,7 @@ impl Game<'_> {
|
||||
tick_frozen: self.clock.frozen(),
|
||||
tick_steps: self.clock.step_state(),
|
||||
collision: Some(self.collision),
|
||||
border: &self.border,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
//! The world border a server's players see: all of it when they enter a
|
||||
//! level (`PlayerList.sendLevelInfo`), then each change as
|
||||
//! `BorderChangeListener` broadcasts it: a new centre, a new size, a size
|
||||
//! change in progress, a new warning time or distance.
|
||||
use crate::worldborder::WorldBorder;
|
||||
use horizon_protocol::packets::play::{self, Clientbound as C};
|
||||
|
||||
/// `ClientboundInitializeBorderPacket`.
|
||||
pub fn initialize(b: &WorldBorder) -> play::InitializeBorder {
|
||||
let [center_x, center_z] = b.center();
|
||||
play::InitializeBorder {
|
||||
center_x,
|
||||
center_z,
|
||||
old_size: b.size(),
|
||||
new_size: b.target_size(),
|
||||
lerp_time: b.remaining_ticks(),
|
||||
absolute_max_size: b.absolute_max_size(),
|
||||
warning_blocks: b.warning_distance,
|
||||
warning_time: b.warning_time,
|
||||
}
|
||||
}
|
||||
|
||||
/// The border as the players were last told of it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Sent {
|
||||
center: [f64; 2],
|
||||
size: f64,
|
||||
target: f64,
|
||||
remaining: i64,
|
||||
warning_distance: i32,
|
||||
warning_time: i32,
|
||||
}
|
||||
|
||||
impl Sent {
|
||||
pub fn of(b: &WorldBorder) -> Self {
|
||||
Self {
|
||||
center: b.center(),
|
||||
size: b.size(),
|
||||
target: b.target_size(),
|
||||
remaining: b.remaining_ticks(),
|
||||
warning_distance: b.warning_distance,
|
||||
warning_time: b.warning_time,
|
||||
}
|
||||
}
|
||||
|
||||
/// The packets that tell the players what changed since `self`, other
|
||||
/// than a size change going on (which each client runs itself).
|
||||
pub fn changes(&self, b: &WorldBorder) -> Vec<C> {
|
||||
let now = Self::of(b);
|
||||
let mut out = Vec::new();
|
||||
if now.center != self.center {
|
||||
out.push(C::SetBorderCenter(play::SetBorderCenter {
|
||||
x: now.center[0],
|
||||
z: now.center[1],
|
||||
}));
|
||||
}
|
||||
// A change going on counts down a tick at a time (or not at all
|
||||
// while the game is frozen), towards the same size.
|
||||
let going_on = self.remaining > 0
|
||||
&& now.target == self.target
|
||||
&& (now.remaining == self.remaining || now.remaining == self.remaining - 1);
|
||||
if now.remaining > 0 {
|
||||
if !going_on {
|
||||
out.push(C::SetBorderLerpSize(play::SetBorderLerpSize {
|
||||
old_size: now.size,
|
||||
new_size: now.target,
|
||||
lerp_time: now.remaining,
|
||||
}));
|
||||
}
|
||||
} else if now.size != self.size && !(self.remaining > 0 && now.size == self.target) {
|
||||
out.push(C::SetBorderSize(play::SetBorderSize { size: now.size }));
|
||||
}
|
||||
if now.warning_time != self.warning_time {
|
||||
out.push(C::SetBorderWarningDelay(play::SetBorderWarningDelay {
|
||||
delay: now.warning_time,
|
||||
}));
|
||||
}
|
||||
if now.warning_distance != self.warning_distance {
|
||||
out.push(C::SetBorderWarningDistance(
|
||||
play::SetBorderWarningDistance {
|
||||
blocks: now.warning_distance,
|
||||
},
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn kinds(packets: &[C]) -> Vec<&'static str> {
|
||||
packets
|
||||
.iter()
|
||||
.map(|p| match p {
|
||||
C::SetBorderCenter(_) => "center",
|
||||
C::SetBorderSize(_) => "size",
|
||||
C::SetBorderLerpSize(_) => "lerp",
|
||||
C::SetBorderWarningDelay(_) => "delay",
|
||||
C::SetBorderWarningDistance(_) => "distance",
|
||||
_ => "other",
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_change_is_sent_once() {
|
||||
let mut b = WorldBorder::default();
|
||||
let mut sent = Sent::of(&b);
|
||||
assert!(sent.changes(&b).is_empty());
|
||||
b.set_center([4.0, -2.0]);
|
||||
b.set_size(10.0);
|
||||
b.warning_time = 30;
|
||||
assert_eq!(kinds(&sent.changes(&b)), ["center", "size", "delay"]);
|
||||
sent = Sent::of(&b);
|
||||
// A change over 3 ticks: sent once, then each client runs it.
|
||||
b.lerp_to(20.0, 3);
|
||||
assert_eq!(kinds(&sent.changes(&b)), ["lerp"]);
|
||||
for _ in 0..4 {
|
||||
sent = Sent::of(&b);
|
||||
b.tick();
|
||||
assert!(sent.changes(&b).is_empty(), "{:?}", sent.changes(&b));
|
||||
}
|
||||
assert_eq!(b.size(), 20.0);
|
||||
b.warning_distance = 1;
|
||||
assert_eq!(kinds(&Sent::of(&b).changes(&b)), [] as [&str; 0]);
|
||||
assert_eq!(kinds(&sent.changes(&b)), ["distance"]);
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,8 @@ pub struct TickCtx<'a> {
|
||||
/// Block collisions, for ServerGamePacketListenerImpl's moved-wrongly
|
||||
/// check (none: moves are not checked against blocks).
|
||||
pub collision: Option<&'a BlockCollisionRegistry<'a>>,
|
||||
/// The level's world border (PlayerList.sendLevelInfo sends it).
|
||||
pub border: &'a crate::worldborder::WorldBorder,
|
||||
}
|
||||
|
||||
/// What the gameplay code has to do for a guest.
|
||||
@@ -891,17 +893,7 @@ impl Hub {
|
||||
fn send_level_info(&mut self, i: usize, ctx: &TickCtx) {
|
||||
let spawn = ctx.spawn;
|
||||
let g = &self.guests[i];
|
||||
g.send(&C::InitializeBorder(play::InitializeBorder {
|
||||
center_x: 0.0,
|
||||
center_z: 0.0,
|
||||
old_size: 59_999_968.0,
|
||||
new_size: 59_999_968.0,
|
||||
lerp_time: 0,
|
||||
absolute_max_size: 29_999_984,
|
||||
warning_blocks: 5,
|
||||
// WorldBorder.DEFAULT_WARNING_TIME, in ticks.
|
||||
warning_time: 300,
|
||||
}));
|
||||
g.send(&C::InitializeBorder(super::border::initialize(ctx.border)));
|
||||
g.send(&self.time_packet(ctx.world, ctx.clock_rate));
|
||||
g.send(&self.default_spawn(spawn));
|
||||
let w = &ctx.world.weather;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//! world that is not open to the network pays nothing.
|
||||
mod advancements;
|
||||
pub mod attributes;
|
||||
pub mod border;
|
||||
pub mod chunks;
|
||||
mod command_order;
|
||||
pub mod command_suggestions;
|
||||
|
||||
@@ -307,6 +307,9 @@ pub struct PlayerMotion {
|
||||
pub hurt_log: Vec<crate::health::HurtRecord>,
|
||||
/// The last melee attacks made on mobs, newest last.
|
||||
pub attack_log: Vec<crate::health::AttackRecord>,
|
||||
/// The level's world border, whose walls stop the player's movement
|
||||
/// from inside (the game keeps it current; None: no border).
|
||||
pub border: Option<horizon_core::geometry::border::BorderGeometry>,
|
||||
}
|
||||
fn attribute_error(error: impl std::fmt::Debug) -> io::Error {
|
||||
io::Error::other(format!("movement attribute: {error:?}"))
|
||||
@@ -461,6 +464,7 @@ impl PlayerMotion {
|
||||
powder_snow_entered: Vec::new(),
|
||||
hurt_log: Vec::new(),
|
||||
attack_log: Vec::new(),
|
||||
border: None,
|
||||
})
|
||||
}
|
||||
/// `Entity.getTicksRequiredToFreeze`.
|
||||
@@ -678,9 +682,11 @@ impl MotionWorld<'_, '_, '_> {
|
||||
fn movement_collisions(
|
||||
&self,
|
||||
) -> crate::collisions::EntityCollisions<'_, SceneCollisions<'_, '_>> {
|
||||
let at = [self.player.position.x, self.player.position.z];
|
||||
crate::collisions::EntityCollisions {
|
||||
blocks: self.collisions(),
|
||||
entities: self.entities,
|
||||
border: self.player.border.as_ref().map(|b| (b, at)),
|
||||
}
|
||||
}
|
||||
/// `Level.noCollision(player, bounds)`: blocks and the host's entity boxes.
|
||||
|
||||
@@ -80,6 +80,10 @@ impl PartialEq for WorldBorder {
|
||||
}
|
||||
}
|
||||
impl WorldBorder {
|
||||
/// Its extent, for collisions.
|
||||
pub fn geometry(&self) -> &BorderGeometry {
|
||||
&self.geometry
|
||||
}
|
||||
pub fn center(&self) -> [f64; 2] {
|
||||
self.geometry.center()
|
||||
}
|
||||
|
||||
@@ -200,6 +200,7 @@ impl Server {
|
||||
tick_frozen: false,
|
||||
tick_steps: (0, 0),
|
||||
collision: None,
|
||||
border: &horizon_client::worldborder::WorldBorder::default(),
|
||||
};
|
||||
let events = self.hub.tick(&mut ctx);
|
||||
for event in events {
|
||||
|
||||
+74
-2
@@ -213,7 +213,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
GUEST_NAME = 'Guest'
|
||||
# The host plays single player on its server thread (--server-thread).
|
||||
SERVER_THREAD = False
|
||||
CHECKS = ['tab', 'self', 'armour', 'nametags', 'walls', 'chest', 'crafting', 'sign', 'damage', 'mobs', 'hazards', 'pvp', 'arrows', 'kills', 'hurt', 'looks', 'leads', 'respawn', 'containers', 'feedback', 'titles', 'chat', 'commands', 'recipes', 'machines', 'brewing', 'loom', 'stonecutter', 'grindstone', 'anvil', 'smithing', 'enchanting', 'beacon', 'crafter', 'lectern', 'cartography', 'trials', 'clicks', 'sleep', 'predict', 'push', 'eat', 'eating', 'xp', 'objects', 'projectiles', 'thrown', 'sparks', 'hud', 'sidebar', 'worn', 'blast', 'barter', 'deflect', 'fireball', 'light', 'time', 'swings', 'cracks', 'plates', 'effects', 'portals', 'distance', 'growth', 'flight', 'ladder', 'boats', 'minecarts', 'frames', 'lightning', 'riding', 'mounts', 'worldspawn', 'beds', 'shield', 'deathmining', 'contacts', 'absorption', 'breath', 'lava', 'levels', 'drops', 'underwater']
|
||||
CHECKS = ['tab', 'self', 'armour', 'nametags', 'walls', 'chest', 'crafting', 'sign', 'damage', 'mobs', 'hazards', 'pvp', 'arrows', 'kills', 'hurt', 'looks', 'leads', 'respawn', 'containers', 'feedback', 'titles', 'chat', 'commands', 'recipes', 'machines', 'brewing', 'loom', 'stonecutter', 'grindstone', 'anvil', 'smithing', 'enchanting', 'beacon', 'crafter', 'lectern', 'cartography', 'trials', 'clicks', 'sleep', 'predict', 'push', 'eat', 'eating', 'xp', 'objects', 'projectiles', 'thrown', 'sparks', 'hud', 'sidebar', 'border', 'worn', 'blast', 'barter', 'deflect', 'fireball', 'light', 'time', 'swings', 'cracks', 'plates', 'effects', 'portals', 'distance', 'growth', 'flight', 'ladder', 'boats', 'minecarts', 'frames', 'lightning', 'riding', 'mounts', 'worldspawn', 'beds', 'shield', 'deathmining', 'contacts', 'absorption', 'breath', 'lava', 'levels', 'drops', 'underwater']
|
||||
|
||||
|
||||
class Game:
|
||||
@@ -2794,6 +2794,77 @@ def check_sidebar(host, guest, out):
|
||||
return result
|
||||
|
||||
|
||||
def check_border(host, guest, out):
|
||||
spots = apart(host, guest)
|
||||
result = {}
|
||||
try:
|
||||
for label, player, other in (('host', host, guest), ('guest', guest, host)):
|
||||
x, y, z = spots[label]
|
||||
# A whole-block centre: the wall's collision box is the border's, rounded out to blocks.
|
||||
cx, cz = float(x), float(z)
|
||||
# The other player keeps out of harm outside the border meanwhile.
|
||||
other('command', command='gamemode creative')
|
||||
host('command', command=f'fill {x - 3} {y - 1} {z} {x + 4} {y - 1} {z + 10} minecraft:stone')
|
||||
host('command', command=f'fill {x - 3} {y} {z} {x + 4} {y + 3} {z + 10} minecraft:air')
|
||||
# A border 10 wide around the player, its wall 5 blocks ahead: both sides know it.
|
||||
host('command', command=f'worldborder center {cx} {cz}')
|
||||
host('command', command='worldborder set 10')
|
||||
seen = None
|
||||
for _ in range(10):
|
||||
player.wait(2)
|
||||
seen = [g('status').get('border') for g in (player, other)]
|
||||
if all(b and b['size'] == 10 and b['center'] == [cx, cz] for b in seen):
|
||||
break
|
||||
result[f'{label} border'] = seen
|
||||
# Sprint-jumping into the wall: it stops the player at the border.
|
||||
player('command', command=f'tp @s {cx} {y} {cz} 0 0')
|
||||
player.wait(10)
|
||||
player.raw('look-at', f'{cx:.2f}', f'{y + 1.6:.2f}', f'{cz + 20:.2f}')
|
||||
for k in ('w', 'ctrl', 'space'):
|
||||
player.raw('key', k, 'down')
|
||||
player.wait(40)
|
||||
for k in ('space', 'ctrl', 'w'):
|
||||
player.raw('key', k, 'up')
|
||||
player.wait(10)
|
||||
result[f'{label} reached'] = round(pos_of(player)[2] - cz, 2)
|
||||
player.shot(out, f'border-{label}')
|
||||
# Two blocks outside, with no safe zone: the border hurts.
|
||||
host('command', command='worldborder damage buffer 0')
|
||||
host('command', command='worldborder damage amount 1')
|
||||
player('command', command='effect give @s minecraft:instant_health 1 5')
|
||||
player.wait(20)
|
||||
player('command', command=f'tp @s {cx} {y} {cz + 7}')
|
||||
player.wait(5)
|
||||
before = player('status')['player']['health']
|
||||
player.wait(40)
|
||||
result[f'{label} hurt'] = [before, player('status')['player']['health']]
|
||||
player('command', command=f'tp @s {cx} {y} {cz}')
|
||||
player('command', command='effect give @s minecraft:instant_health 1 5')
|
||||
host('command', command='worldborder damage amount 0.2')
|
||||
host('command', command='worldborder damage buffer 5')
|
||||
other('command', command='gamemode survival')
|
||||
finally:
|
||||
guest('command', command='gamemode survival')
|
||||
host('command', command='gamemode survival')
|
||||
host('command', command='worldborder damage amount 0.2')
|
||||
host('command', command='worldborder damage buffer 5')
|
||||
host('command', command='worldborder center 0 0')
|
||||
host('command', command='worldborder set 59999968')
|
||||
wrong = {}
|
||||
for label in ('host', 'guest'):
|
||||
x, y, z = spots[label]
|
||||
known = result[f'{label} border']
|
||||
if not all(b and b['size'] == 10 and b['center'] == [x, z] for b in known):
|
||||
wrong[f'{label} border'] = known
|
||||
if not 4.0 < result[f'{label} reached'] <= 4.71:
|
||||
wrong[f'{label} reached'] = result[f'{label} reached']
|
||||
before, after = result[f'{label} hurt']
|
||||
if not after < before:
|
||||
wrong[f'{label} hurt'] = result[f'{label} hurt']
|
||||
assert not wrong, f'border: {wrong}\n{result}'
|
||||
return result
|
||||
|
||||
|
||||
def check_blast(host, guest, out):
|
||||
guest('command', command='gamemode survival')
|
||||
guest('command', command='clear @s')
|
||||
@@ -4131,7 +4202,8 @@ CHECK_FNS = {'tab': check_tab, 'self': check_self, 'armour': check_armour,
|
||||
'thrown': check_thrown,
|
||||
'sparks': check_sparks,
|
||||
'hud': check_hud,
|
||||
'sidebar': check_sidebar}
|
||||
'sidebar': check_sidebar,
|
||||
'border': check_border}
|
||||
|
||||
|
||||
def run(ctl, out, resources, checks):
|
||||
|
||||
Reference in new issue
Block a user