mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-12 18:35:16 -10:00
A player's attributes stay through a respawn and a change of level (#3684)
The server made a new body for a player from the defaults when they died and respawned or went to another level, so /attribute @s minecraft:scale base set 0.1 was 1.0 again (box, eye height and reach as well), and so was every other attribute a command or a datapack had set. Vanilla keeps the same ServerPlayer through teleport, and ServerPlayer.restoreFrom copies the base values to the body after a death (assignBaseValues). The old body's attributes now go to the next one (with their permanent modifiers for a level change, base values only after a death), and a player saved between two bodies writes them to the file. restoreFrom then gives the respawned player its max health (setHealth(getMaxHealth())): a max health base of 40 comes back with 40, not with the fixed 20 the respawn set. Also: a glide the client announced before the body's own tick used a scale-1 box for that tick (Guest::dimensions); it takes the body's scale. The port map's LivingEntity.getScale row still said there was no scale attribute. Tests: the respawn health, the glide box, the carried attributes, a save between bodies; lan_e2e check scale-body (host and guest: the Nether and back, a death and respawn, the health after it). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01RodDbPWCByh96WKcbiEQpB
This commit is contained in:
1 parent
8d93d68f1d
commit
3c0a4db876
8 files changed
+316
-15
No files matched your search
@@ -383,6 +383,15 @@ impl Game<'_> {
|
||||
let _ =
|
||||
m.apply_saved_attributes(g.saved.as_ref().and_then(|t| t.get("attributes")));
|
||||
}
|
||||
// ServerPlayer.restoreFrom: a body that follows another takes its
|
||||
// attributes (a scale of 0.1 stays 0.1 after a respawn or a trip
|
||||
// to another level).
|
||||
let g = &mut host.hub.guests[i];
|
||||
if let Some(carried) = g.carried_attributes.take()
|
||||
&& let Some(m) = g.motion.as_mut()
|
||||
{
|
||||
let _ = m.apply_saved_attributes(Some(&carried));
|
||||
}
|
||||
// The saved golden hearts go on the first body of a player who
|
||||
// has been here before.
|
||||
let saved = host.hub.guests[i].saved_absorption.take();
|
||||
@@ -2508,8 +2517,9 @@ impl Game<'_> {
|
||||
}
|
||||
let g = &mut host.hub.guests[gi];
|
||||
(g.yaw, g.pitch) = (yaw, pitch);
|
||||
// A new body there: no fall distance or motion carried over.
|
||||
g.motion = None;
|
||||
// A new body there: no fall distance or motion carried over, but the
|
||||
// player's attributes (ServerPlayer.teleport keeps the entity).
|
||||
g.new_body(true);
|
||||
host.hub.teleport(gi, Some(at));
|
||||
}
|
||||
fn guest_tick_death(&mut self, host: &mut LanHost, gi: usize) {
|
||||
@@ -3233,14 +3243,16 @@ impl Game<'_> {
|
||||
g.load_timeout = horizon_client::lan::guest::CLIENT_LOADED_TIMEOUT;
|
||||
g.loading = true;
|
||||
g.death_time = 0;
|
||||
g.health = 20.0;
|
||||
g.food = 20;
|
||||
g.saturation = 5.0;
|
||||
g.sent_health = None;
|
||||
if let Some(m) = g.motion.take() {
|
||||
// ServerPlayer.restoreFrom(old, false): the base values come back,
|
||||
// and the new player has the max health they give.
|
||||
if let Some(m) = g.new_body(false) {
|
||||
let m = *m;
|
||||
g.logs_kept = Some((m.hurt_log, m.attack_log));
|
||||
}
|
||||
g.health = g.respawn_health(self.motion);
|
||||
let mut xp = Experience {
|
||||
level: g.xp_level,
|
||||
progress: g.xp_progress,
|
||||
|
||||
@@ -670,8 +670,9 @@ impl Game<'_> {
|
||||
host.play.entry(trip.conn).or_default().portal.cooldown = 10;
|
||||
let g = &mut host.hub.guests[i];
|
||||
g.yaw = yaw;
|
||||
// A new body at the exit: no fall distance or motion carried over.
|
||||
g.motion = None;
|
||||
// A new body at the exit: no fall distance or motion carried over,
|
||||
// but the player's attributes (ServerPlayer.teleport keeps the entity).
|
||||
g.new_body(true);
|
||||
let at = [position.x, position.y, position.z];
|
||||
if portal {
|
||||
host.hub.land(i, at);
|
||||
@@ -727,8 +728,9 @@ impl Game<'_> {
|
||||
let g = &mut host.hub.guests[i];
|
||||
let [yaw, pitch] = t.rotation.unwrap_or([g.yaw, g.pitch]);
|
||||
let origin = g.position;
|
||||
// A new body there: no fall distance or motion carried over.
|
||||
g.motion = None;
|
||||
// A new body there: no fall distance or motion carried over, but
|
||||
// the player's attributes (ServerPlayer.teleport keeps the entity).
|
||||
g.new_body(true);
|
||||
let cooldown = host.play.get(&t.conn).map_or(0, |p| p.portal.cooldown);
|
||||
let ctx = self.server_ctx();
|
||||
host.hub
|
||||
|
||||
@@ -214,6 +214,11 @@ pub struct Guest {
|
||||
/// What the player's file says of the body (`Entity.load`: the fire
|
||||
/// and air ticks and the fall distance), for the first body it gets.
|
||||
pub loaded_body: Option<SavedBody>,
|
||||
/// The attribute list the player's last body hands to its next one
|
||||
/// (`ServerPlayer.restoreFrom`, `PlayerMotion::carried_attributes`),
|
||||
/// until that body exists: the scale a player has set stays through a
|
||||
/// respawn and a change of level (#3684).
|
||||
pub carried_attributes: Option<horizon_world::nbt::Tag>,
|
||||
/// The vehicle the player's file names (`ServerPlayer.saveParentVehicle`'s
|
||||
/// "RootVehicle"), for the ride to come back on the next login.
|
||||
pub saved_vehicle: Option<[i32; 4]>,
|
||||
@@ -408,6 +413,7 @@ impl Guest {
|
||||
tracker: Tracker::default(),
|
||||
motion: None,
|
||||
loaded_body: None,
|
||||
carried_attributes: None,
|
||||
saved_vehicle: None,
|
||||
logs_kept: None,
|
||||
inventory: vec![None; 36],
|
||||
@@ -597,14 +603,39 @@ impl Guest {
|
||||
Pose::Standing
|
||||
}
|
||||
}
|
||||
/// `Avatar.POSES` for the guest's pose, with its scale attribute.
|
||||
/// `Avatar.POSES` for the guest's pose, with its scale attribute: also for
|
||||
/// a pose the client has announced (gliding) before the body's own tick
|
||||
/// has taken it.
|
||||
pub fn dimensions(&self) -> Dimensions {
|
||||
let pose = self.pose();
|
||||
match &self.motion {
|
||||
Some(m) if m.pose == pose => m.dimensions,
|
||||
_ => pose.dimensions(1.0),
|
||||
Some(m) => pose.dimensions(m.scale),
|
||||
None => pose.dimensions(1.0),
|
||||
}
|
||||
}
|
||||
/// The player's body is replaced (`ServerPlayer.restoreFrom`): the next
|
||||
/// one starts from the old one's attributes, all of them when it is the
|
||||
/// same player in another level (`restore_all`), only their base values
|
||||
/// after a death. The old body, for what else it hands over.
|
||||
pub fn new_body(&mut self, restore_all: bool) -> Option<Box<PlayerMotion>> {
|
||||
let m = self.motion.take()?;
|
||||
self.carried_attributes = Some(m.carried_attributes(restore_all));
|
||||
Some(m)
|
||||
}
|
||||
/// `ServerPlayer.restoreFrom(old, false)`'s `setHealth(getMaxHealth())`:
|
||||
/// the health a player comes back from a death with, the max health of
|
||||
/// the base values their next body takes over, before its equipment
|
||||
/// modifies it (40 after `/attribute @s minecraft:max_health base set
|
||||
/// 40`, not 20).
|
||||
pub fn respawn_health(&self, resources: &crate::motion::MotionResources) -> f32 {
|
||||
let Ok(mut body) = PlayerMotion::new(resources, Vec3::ZERO) else {
|
||||
return 20.0;
|
||||
};
|
||||
let _ = body.apply_saved_attributes(self.carried_attributes.as_ref());
|
||||
body.attribute("minecraft:max_health")
|
||||
.map_or(20.0, |max| max as f32)
|
||||
}
|
||||
/// `Entity.getBoundingBox`.
|
||||
pub fn bounds(&self) -> Aabb {
|
||||
let [x, y, z] = self.position;
|
||||
@@ -1381,4 +1412,102 @@ mod tests {
|
||||
Some(Component::Text(_))
|
||||
));
|
||||
}
|
||||
|
||||
/// The prepared game data a body needs, or `None` without it.
|
||||
fn resources() -> Option<(crate::assets::Assets, crate::motion::MotionResources)> {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../.local/horizon-client")
|
||||
.join(horizon_core::VANILLA_VERSION);
|
||||
if !root.join("movement-properties.nbt").exists() {
|
||||
eprintln!("skipped: prepared resources unavailable");
|
||||
return None;
|
||||
}
|
||||
let assets = crate::assets::Assets::load(&root).ok()?;
|
||||
let resources = crate::motion::MotionResources::load(&root, &assets).ok()?;
|
||||
Some((assets, resources))
|
||||
}
|
||||
|
||||
/// `/attribute @s <attribute> base set`, as `AttributeCommand` runs it,
|
||||
/// and the next tick's `refreshDirtyAttributes` (the box of a new scale).
|
||||
fn set_base(m: &mut PlayerMotion, attribute: &str, value: f64) {
|
||||
let key = m.attributes.key(attribute).unwrap();
|
||||
let handle = m.attributes.get_instance(Some(key)).unwrap().unwrap();
|
||||
m.attributes
|
||||
.edit(&handle, |attribute, observer| {
|
||||
attribute.set_base_value(value, observer)
|
||||
})
|
||||
.unwrap();
|
||||
m.refresh_attributes().unwrap();
|
||||
}
|
||||
|
||||
fn assert_dims(got: Dimensions, want: (f32, f32, f32), what: &str) {
|
||||
let close = |a: f32, b: f32| (a - b).abs() <= 1e-5 * b.abs().max(1.0);
|
||||
assert!(
|
||||
close(got.width, want.0) && close(got.height, want.1) && close(got.eye_height, want.2),
|
||||
"{what}: {got:?}, want {want:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// `ServerPlayer.restoreFrom(old, false)` takes the base values
|
||||
/// (`assignBaseValues`), then `setHealth(getMaxHealth())`: a player whose
|
||||
/// max health base was set comes back with that much, not 20, and a
|
||||
/// permanent modifier (`/attribute ... modifier add`) does not count, as
|
||||
/// it stays with the dead body. The attribute's range is 1..1024.
|
||||
#[test]
|
||||
fn a_respawned_player_has_the_max_health_of_their_base_values() {
|
||||
use horizon_world::attributes::{Modifier, Operation};
|
||||
let Some((_assets, resources)) = resources() else {
|
||||
return;
|
||||
};
|
||||
for (base, bonus, want) in [
|
||||
(40.0, 0.0, 40.0),
|
||||
(10.0, 0.0, 10.0),
|
||||
(20.0, 20.0, 20.0),
|
||||
(0.0, 0.0, 1.0),
|
||||
(2048.0, 0.0, 1024.0),
|
||||
] {
|
||||
let mut g = guest_at([0.5, 64.0, 0.5]);
|
||||
let mut m = PlayerMotion::new(&resources, Vec3::new(0.5, 64.0, 0.5)).unwrap();
|
||||
set_base(&mut m, "minecraft:max_health", base);
|
||||
let key = m.attributes.key("minecraft:max_health").unwrap();
|
||||
let handle = m.attributes.get_instance(Some(key)).unwrap().unwrap();
|
||||
let bonus = Modifier::new("test:bonus", bonus, Operation::AddValue).unwrap();
|
||||
m.attributes
|
||||
.edit(&handle, |a, o| a.add_permanent(bonus, o))
|
||||
.unwrap();
|
||||
g.motion = Some(Box::new(m));
|
||||
assert!(g.new_body(false).is_some());
|
||||
assert_eq!(g.respawn_health(&resources), want, "base {base}");
|
||||
}
|
||||
// No body to take anything from: the default.
|
||||
let g = guest_at([0.5, 64.0, 0.5]);
|
||||
assert_eq!(g.respawn_health(&resources), 20.0);
|
||||
}
|
||||
|
||||
/// #3684: a glide the client announced (`START_FALL_FLYING`) is the
|
||||
/// player's pose before its body's own tick has taken it: the box in
|
||||
/// between is the gliding one at the player's scale, not at scale 1 (a
|
||||
/// 0.6 box on a player 0.1 or 2 times as big sent them back from a wall).
|
||||
#[test]
|
||||
fn a_glide_announced_before_the_body_ticks_has_the_scaled_box() {
|
||||
let Some((_assets, resources)) = resources() else {
|
||||
return;
|
||||
};
|
||||
for (scale, side) in [(0.1_f64, 0.06_f32), (2.0, 1.2)] {
|
||||
let mut g = guest_at([0.5, 64.0, 0.5]);
|
||||
let mut m = PlayerMotion::new(&resources, Vec3::new(0.5, 64.0, 0.5)).unwrap();
|
||||
set_base(&mut m, "minecraft:scale", scale);
|
||||
m.set_pose(Pose::Standing).unwrap();
|
||||
g.motion = Some(Box::new(m));
|
||||
g.fall_flying = true;
|
||||
assert_eq!(g.pose(), Pose::FallFlying);
|
||||
assert_ne!(g.motion.as_ref().unwrap().pose, Pose::FallFlying);
|
||||
let d = g.dimensions();
|
||||
assert_dims(
|
||||
d,
|
||||
(side, side, side * (0.4 / 0.6)),
|
||||
&format!("scale {scale}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,12 +250,19 @@ pub fn store(guest: &Guest, level: &mut Level, vehicle: Option<Tag>) {
|
||||
}
|
||||
// LivingEntity.addAdditionalSaveData: the live body's AttributeMap.pack,
|
||||
// or the list the file had while the player has no body yet.
|
||||
if let Some(m) = guest.motion.as_deref() {
|
||||
// A player between two bodies (a respawn, a change of level) saves what
|
||||
// the next one takes over.
|
||||
let attributes = guest
|
||||
.motion
|
||||
.as_deref()
|
||||
.map(crate::motion::PlayerMotion::saved_attributes)
|
||||
.or_else(|| guest.carried_attributes.clone());
|
||||
if let Some(attributes) = attributes {
|
||||
let extra = level
|
||||
.player_extra
|
||||
.get_or_insert_with(|| horizon_world::nbt::Tag::Compound(Default::default()));
|
||||
if let horizon_world::nbt::Tag::Compound(map) = extra {
|
||||
map.insert("attributes".into(), m.saved_attributes());
|
||||
map.insert("attributes".into(), attributes);
|
||||
}
|
||||
}
|
||||
level.recipe_book = guest.recipe_book.clone();
|
||||
@@ -490,6 +497,89 @@ mod tests {
|
||||
Some((assets, resources))
|
||||
}
|
||||
|
||||
/// A body at scale 0.1 with a permanent +0.4 on its step height, such as
|
||||
/// a command or a datapack gives.
|
||||
fn small_body(resources: &crate::motion::MotionResources) -> crate::motion::PlayerMotion {
|
||||
use horizon_world::attributes::{Modifier, Operation};
|
||||
let mut m = crate::motion::PlayerMotion::new(resources, horizon_core::geometry::Vec3::ZERO)
|
||||
.expect("a body");
|
||||
let key = m.attributes.key("minecraft:scale").unwrap();
|
||||
let handle = m.attributes.get_instance(Some(key)).unwrap().unwrap();
|
||||
m.attributes
|
||||
.edit(&handle, |a, o| a.set_base_value(0.1, o))
|
||||
.unwrap();
|
||||
let key = m.attributes.key("minecraft:step_height").unwrap();
|
||||
let handle = m.attributes.get_instance(Some(key)).unwrap().unwrap();
|
||||
let step = Modifier::new("test:step", 0.4, Operation::AddValue).unwrap();
|
||||
m.attributes
|
||||
.edit(&handle, |a, o| a.add_permanent(step, o))
|
||||
.unwrap();
|
||||
m
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_body_takes_the_scale_of_the_old_one() {
|
||||
// #3684: ServerPlayer.restoreFrom copies the base values to the body
|
||||
// that follows a death (assignBaseValues) and, for the same player in
|
||||
// another level, the permanent modifiers too (assignPermanentModifiers).
|
||||
// The body Horizon made on a respawn or a level change started from
|
||||
// the defaults: a player at /attribute scale 0.1 was 1.0 again.
|
||||
let Some((_assets, resources)) = resources() else {
|
||||
eprintln!("skipped: prepared resources unavailable");
|
||||
return;
|
||||
};
|
||||
let next = |g: &Guest| {
|
||||
let mut m =
|
||||
crate::motion::PlayerMotion::new(&resources, horizon_core::geometry::Vec3::ZERO)
|
||||
.expect("a body");
|
||||
m.apply_saved_attributes(g.carried_attributes.as_ref())
|
||||
.unwrap();
|
||||
m
|
||||
};
|
||||
for (restore_all, step) in [(false, 0.6), (true, 1.0)] {
|
||||
let mut g = guest();
|
||||
g.motion = Some(Box::new(small_body(&resources)));
|
||||
g.new_body(restore_all);
|
||||
assert!(g.motion.is_none());
|
||||
let mut m = next(&g);
|
||||
assert_eq!(m.attribute("minecraft:scale").unwrap(), 0.1);
|
||||
assert!((m.attribute("minecraft:step_height").unwrap() - step).abs() < 1e-9);
|
||||
// The tick that follows rebuilds the box at that scale.
|
||||
m.refresh_attributes().unwrap();
|
||||
assert_eq!(m.dimensions.width, 0.6 * 0.1);
|
||||
assert_eq!(m.dimensions.height, 1.8 * 0.1);
|
||||
}
|
||||
// No body before: nothing to hand over.
|
||||
let mut g = guest();
|
||||
g.new_body(true);
|
||||
assert!(g.carried_attributes.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_player_between_two_bodies_saves_the_attributes_the_next_one_takes() {
|
||||
// #3684: logging out after a respawn, before the new body has ticked,
|
||||
// must not write the file's older list over the scale.
|
||||
let Some((_assets, resources)) = resources() else {
|
||||
eprintln!("skipped: prepared resources unavailable");
|
||||
return;
|
||||
};
|
||||
let mut g = guest();
|
||||
g.motion = Some(Box::new(small_body(&resources)));
|
||||
g.new_body(false);
|
||||
let mut level = Level::default();
|
||||
store(&g, &mut level, None);
|
||||
let tag = level.player_tag();
|
||||
let mut read = Level::default();
|
||||
read.read_player(&tag);
|
||||
read.player_extra = Some(tag);
|
||||
let mut back =
|
||||
crate::motion::PlayerMotion::new(&resources, horizon_core::geometry::Vec3::ZERO)
|
||||
.expect("a body");
|
||||
back.apply_saved_attributes(read.player_extra.as_ref().and_then(|t| t.get("attributes")))
|
||||
.unwrap();
|
||||
assert_eq!(back.attribute("minecraft:scale").unwrap(), 0.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_saved_air_comes_back() {
|
||||
// A player saved underwater (#1321): the file keeps the remaining air
|
||||
|
||||
@@ -344,6 +344,10 @@ pub struct PlayerMotion {
|
||||
pub local_crouching: bool,
|
||||
pub pose: crate::pose::Pose,
|
||||
pub dimensions: crate::pose::Dimensions,
|
||||
/// The `minecraft:scale` the dimensions were last built with
|
||||
/// (`Entity.refreshDimensions`), for the poses the body takes before it
|
||||
/// next refreshes.
|
||||
pub scale: f32,
|
||||
pub speed: f32,
|
||||
/// `LocalPlayer.itemUseSpeedMultiplier` while using an item (not riding).
|
||||
pub item_use_speed: Option<f32>,
|
||||
@@ -559,6 +563,7 @@ impl PlayerMotion {
|
||||
local_crouching: false,
|
||||
pose: crate::pose::Pose::Standing,
|
||||
dimensions: crate::pose::Pose::Standing.dimensions(1.0),
|
||||
scale: 1.0,
|
||||
swimming: false,
|
||||
is_passenger: false,
|
||||
fire_ticks: -20,
|
||||
@@ -717,6 +722,18 @@ impl PlayerMotion {
|
||||
/// player's `attributes` list (LivingEntity.addAdditionalSaveData), each
|
||||
/// instance's id, base and permanent modifiers.
|
||||
pub fn saved_attributes(&self) -> horizon_world::nbt::Tag {
|
||||
self.packed_attributes(true)
|
||||
}
|
||||
/// `ServerPlayer.restoreFrom`: the attributes a player's new body takes
|
||||
/// over from the old one. A body that comes back from a death takes the
|
||||
/// base values only (`assignBaseValues`); the same player in another
|
||||
/// level (`ServerPlayer.teleport`) is the same entity, with its permanent
|
||||
/// modifiers too (`restoreAll`, `assignPermanentModifiers`). A scale of
|
||||
/// 0.1 is still 0.1 after a respawn or a trip through a portal (#3684).
|
||||
pub fn carried_attributes(&self, restore_all: bool) -> horizon_world::nbt::Tag {
|
||||
self.packed_attributes(restore_all)
|
||||
}
|
||||
fn packed_attributes(&self, modifiers: bool) -> horizon_world::nbt::Tag {
|
||||
use horizon_client_core::nbt_value::string;
|
||||
use horizon_world::nbt::Tag;
|
||||
let list = self
|
||||
@@ -729,7 +746,7 @@ impl PlayerMotion {
|
||||
("base", Tag::Double(packed.base.to_bits())),
|
||||
];
|
||||
// `modifiers` is an optional field whose default is the empty list.
|
||||
if !packed.modifiers.is_empty() {
|
||||
if modifiers && !packed.modifiers.is_empty() {
|
||||
let modifiers = packed
|
||||
.modifiers
|
||||
.iter()
|
||||
|
||||
@@ -94,7 +94,8 @@ impl PlayerMotion {
|
||||
Ok(pose.dimensions(self.attribute("minecraft:scale")? as f32))
|
||||
}
|
||||
pub fn refresh_dimensions(&mut self) -> Result<()> {
|
||||
self.dimensions = self.dimensions_for_pose(self.pose)?;
|
||||
self.scale = self.attribute("minecraft:scale")? as f32;
|
||||
self.dimensions = self.pose.dimensions(self.scale);
|
||||
Ok(())
|
||||
}
|
||||
pub fn set_pose(&mut self, pose: Pose) -> Result<()> {
|
||||
|
||||
@@ -11747,5 +11747,55 @@ CHECKS += ['mining-drop-latency']
|
||||
CHECK_FNS['mining-drop-latency'] = check_mining_drop_latency
|
||||
|
||||
|
||||
def check_scale_body(host, guest, out):
|
||||
"""#3684: a player's `minecraft:scale` is the same entity's attribute through a change of level and
|
||||
a death. ServerPlayer.teleport keeps the player; ServerPlayer.restoreFrom copies the base values to
|
||||
the body after a death (assignBaseValues), then sets its health to the max they give. The eye the
|
||||
client sees stays at 0.162 (1.62 * 0.1), not back at 1.62, and a max health base of 40 respawns the
|
||||
player with 40."""
|
||||
seen = {}
|
||||
|
||||
def scale_of(player):
|
||||
# The eye height the player's client has: the server's scale reaches it as an attribute update.
|
||||
state = player('state')['player']
|
||||
return round(state['eye'][1] - state['position'][1], 3)
|
||||
|
||||
for label, player in (('host', host), ('guest', guest)):
|
||||
s = seen[label] = {}
|
||||
player('command', command='gamemode creative')
|
||||
player('command', command='attribute @s minecraft:scale base set 0.1')
|
||||
player('command', command='attribute @s minecraft:max_health base set 40')
|
||||
player.wait(20)
|
||||
s['set'] = scale_of(player)
|
||||
x, z = (int(v // 1) for v in pos_of(player)[::2])
|
||||
into_level(player, NETHER, x, 100, z)
|
||||
s['nether'] = scale_of(player)
|
||||
player('command', command=f'execute in {OVERWORLD} run tp @s {x + 0.5} 200 {z + 0.5}')
|
||||
wait_dimension(player, OVERWORLD, 400)
|
||||
player.wait(40)
|
||||
s['overworld'] = scale_of(player)
|
||||
player('command', command='kill @s')
|
||||
player.wait(30)
|
||||
player.raw('respawn')
|
||||
player.wait(60)
|
||||
back_to_game(player)
|
||||
s['respawned'] = scale_of(player)
|
||||
s['health'] = player('state')['player']['health']
|
||||
player('command', command='attribute @s minecraft:scale base set 1')
|
||||
player('command', command='attribute @s minecraft:max_health base set 20')
|
||||
player.wait(20)
|
||||
s['reset'] = scale_of(player)
|
||||
for label, s in seen.items():
|
||||
for step in ('set', 'nether', 'overworld', 'respawned'):
|
||||
assert s[step] == 0.162, f'{label} after {step}: {s}'
|
||||
assert s['reset'] == 1.62, f'{label} reset: {s}'
|
||||
assert s['health'] == 40, f'{label} respawned health: {s}'
|
||||
return seen
|
||||
|
||||
|
||||
CHECKS += ['scale-body']
|
||||
CHECK_FNS['scale-body'] = check_scale_body
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -32,7 +32,7 @@ LinearInterpolationHandler.doInterpolate inline crates/horizon-client/src/game/r
|
||||
LivingEntity.<init> port crates/horizon-client/src/mobs.rs::Mob::new box, health, random yaw
|
||||
LivingEntity.defineSynchedData port crates/horizon-client/src/game/lan.rs::mob_meta gap: player: arrow count (12) and stinger count (13) are never kept or sent, so arrows and bee stingers do not stay stuck in a body
|
||||
LivingEntity.hasLandedInLiquid port crates/horizon-world/src/living_fall.rs::ImpulseContext::landed_in_liquid water or lava with getDeltaMovement().y below 9.9999997E-6, from the guest move loop; the grace time still defers it
|
||||
LivingEntity.getScale missing minor: no scale attribute, so /attribute scale and datapack scaling do nothing; baby and slime sizes come from tables in Mob::dims
|
||||
LivingEntity.getScale port crates/horizon-client/src/mobs/riders.rs::scale the minecraft:scale a client has for any living entity; the server's player reads its own in PlayerMotion::refresh_dimensions (PlayerMotion.scale keeps it for Guest::dimensions)
|
||||
LivingEntity.shouldDropLoot inline crates/horizon-client/src/mobs/drops.rs::die mob_drops rule and not a baby (monsters excepted)
|
||||
LivingEntity.decreaseAirSupply port crates/horizon-client/src/motion/environment.rs::MotionWorld::tick_environment_damage gap: minor: only the player reads oxygen_bonus (Respiration); a mob loses one air per tick in ai.rs::base_tick
|
||||
LivingEntity.setLastHurtByPlayer inline crates/horizon-client/src/mobs.rs::Mob::damage_with ai.last_hurt_by_player = 100 on a player's hit; Game::last_hurt_by_player records which guest
|
||||
|
||||
|
Reference in new issue
Block a user