mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Riders: a rider whose eyes are in water gets off a mount that can't swim with one
LivingEntity.baseTick stops a rider whose eyes are in water (not in a bubble column) from riding a vehicle of the dismounts_underwater tag: camel, chicken, donkey, happy ghast, horse, llama, mule, pig, ravager, spider, strider, trader llama and zombie horse. Horizon only did it for boats. The window's player and a server's player (through the same tick as the mount's input) and the mobs that ride a mob (a baby zombie on a chicken) now get off; a skeleton horse keeps its rider. tools/test_mount_submerged_parity.py compares 14 mounts with a vanilla server, passenger packet by passenger packet. Fixes #6152 Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
975c07d915
commit
8ecf339e5b
3 files changed
+176
-1
No files matched your search
@@ -858,7 +858,10 @@ impl Game<'_> {
|
||||
self.tick_village_siege();
|
||||
}
|
||||
self.tick_raids();
|
||||
horizon_client::spans::time("tick_creature_entities", || self.tick_creature_entities())
|
||||
let ticked =
|
||||
horizon_client::spans::time("tick_creature_entities", || self.tick_creature_entities());
|
||||
self.dismount_underwater_mob_riders();
|
||||
ticked
|
||||
}
|
||||
/// `DistanceManager.inEntityTickingRange` in an entity-ticking chunk
|
||||
/// (block coordinates), from every player's tickets (`update_chunk_tickets`).
|
||||
|
||||
@@ -54,6 +54,27 @@ impl MountState {
|
||||
self.jump_scale = 0.0;
|
||||
}
|
||||
}
|
||||
/// `EntityTypeTags.DISMOUNTS_UNDERWATER` (`Entity.dismountsUnderwater`): the mounts that throw their
|
||||
/// riders off when the rider's eyes are in water.
|
||||
pub(super) fn dismounts_underwater(species: Species) -> bool {
|
||||
matches!(
|
||||
species,
|
||||
Species::Camel
|
||||
| Species::Chicken
|
||||
| Species::Donkey
|
||||
| Species::HappyGhast
|
||||
| Species::Horse
|
||||
| Species::Llama
|
||||
| Species::Mule
|
||||
| Species::Pig
|
||||
| Species::Ravager
|
||||
| Species::Spider
|
||||
| Species::Strider
|
||||
| Species::TraderLlama
|
||||
| Species::ZombieHorse
|
||||
)
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
fn mount_index(&self) -> Option<usize> {
|
||||
let id = self.riding?;
|
||||
@@ -364,6 +385,19 @@ impl Game<'_> {
|
||||
}
|
||||
jump
|
||||
}
|
||||
/// Whether the rider's eyes are in water (`Entity.updateFluidOnEyes` then `isEyeInFluid(WATER)`),
|
||||
/// and not in a bubble column, as `LivingEntity.baseTick` asks before it lets a rider stay.
|
||||
fn rider_eyes_in_water(&self) -> bool {
|
||||
let Some([x, y, z]) = self.eye_position() else {
|
||||
return false;
|
||||
};
|
||||
let (bx, by, bz) = (x.floor() as i32, y.floor() as i32, z.floor() as i32);
|
||||
if &*self.res.facts().get(self.world.block(bx, by, bz)).name == "minecraft:bubble_column" {
|
||||
return false;
|
||||
}
|
||||
self.fluid_height_of(bx, by, bz, Some(Fluid::Water))
|
||||
.is_some_and(|(height, _)| f64::from(by) + f64::from(height) > y)
|
||||
}
|
||||
/// The rider's input for this tick; returns false when not on a mount.
|
||||
pub(super) fn tick_mount_riding(&mut self, keys: Keys) -> bool {
|
||||
let Some(i) = self.mount_index() else {
|
||||
@@ -373,6 +407,12 @@ impl Game<'_> {
|
||||
self.dismount_mount();
|
||||
return true;
|
||||
}
|
||||
// `LivingEntity.baseTick`: a rider whose eyes are in water gets off a mount of the
|
||||
// `dismounts_underwater` tag (a server's player too: `leave_guest` tells their client).
|
||||
if dismounts_underwater(self.mobs[i].species()) && self.rider_eyes_in_water() {
|
||||
self.dismount_mount();
|
||||
return true;
|
||||
}
|
||||
let strafe = f32::from(u8::from(keys.left)) - f32::from(u8::from(keys.right));
|
||||
let forward = f32::from(u8::from(keys.forward)) - f32::from(u8::from(keys.backward));
|
||||
// LocalPlayer.aiStep: the riding jump charges while space is held.
|
||||
@@ -422,6 +462,23 @@ impl Game<'_> {
|
||||
});
|
||||
true
|
||||
}
|
||||
/// `LivingEntity.baseTick` for the mobs that ride a mount of the `dismounts_underwater` tag (a baby
|
||||
/// zombie on a chicken): with their eyes in water they get off it.
|
||||
pub(super) fn dismount_underwater_mob_riders(&mut self) {
|
||||
let swimmers: Vec<u32> = self
|
||||
.mobs
|
||||
.iter()
|
||||
.filter(|m| m.alive() && m.fluids.eye_in_water)
|
||||
.filter_map(|m| {
|
||||
let mount = m.extra.mount?;
|
||||
let under = self.mobs.iter().find(|v| v.id == mount)?;
|
||||
dismounts_underwater(under.species()).then_some(m.id)
|
||||
})
|
||||
.collect();
|
||||
for rider in swimmers {
|
||||
self.dismount_mob(rider);
|
||||
}
|
||||
}
|
||||
/// `positionRider`: the player sits on the mount after it moves.
|
||||
pub(super) fn position_rider(&mut self) {
|
||||
let Some(i) = self.mount_index() else {
|
||||
@@ -847,6 +904,32 @@ mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn the_mounts_that_throw_a_swimmer_off_are_the_dismounts_underwater_tag() {
|
||||
// data/minecraft/tags/entity_type/dismounts_underwater.json of 26.3.
|
||||
for species in [
|
||||
Species::Camel,
|
||||
Species::Chicken,
|
||||
Species::Donkey,
|
||||
Species::HappyGhast,
|
||||
Species::Horse,
|
||||
Species::Llama,
|
||||
Species::Mule,
|
||||
Species::Pig,
|
||||
Species::Ravager,
|
||||
Species::Spider,
|
||||
Species::Strider,
|
||||
Species::TraderLlama,
|
||||
Species::ZombieHorse,
|
||||
] {
|
||||
assert!(dismounts_underwater(species), "{species:?}");
|
||||
}
|
||||
// Not in the tag: a skeleton horse swims with its rider, a camel husk is not the camel.
|
||||
for species in [Species::SkeletonHorse, Species::CamelHusk, Species::Cow] {
|
||||
assert!(!dismounts_underwater(species), "{species:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steering_accepts_the_correct_item_in_either_hand() {
|
||||
let carrot = "minecraft:carrot_on_a_stick";
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""A rider whose eyes are in water gets off a mount of the `dismounts_underwater` tag, on real vanilla and
|
||||
Horizon servers.
|
||||
|
||||
LivingEntity.baseTick: a rider whose eyes are in water (not in a bubble column) stops riding a vehicle whose
|
||||
type is in data/minecraft/tags/entity_type/dismounts_underwater.json (camel, chicken, donkey, horse, llama,
|
||||
mule, pig, ravager, spider, strider, trader llama, zombie horse, happy ghast). A vanilla-codec client rides
|
||||
each mount with `/ride`; water that stops below the rider's eyes keeps it on, deeper water throws it off, and
|
||||
a mount outside the tag (a skeleton horse, a cow) keeps its rider underwater.
|
||||
"""
|
||||
try:
|
||||
from . import test_locator_bar_parity as net
|
||||
except ImportError:
|
||||
import test_locator_bar_parity as net
|
||||
|
||||
SCRIPT = [('setup', ['cmd Alice gamerule spawn_mobs false'])]
|
||||
EXPECTED = []
|
||||
PROBES = []
|
||||
|
||||
# (entity type, the tag throws its rider off in deep water)
|
||||
MOUNTS = [
|
||||
('pig', True), ('horse', True), ('donkey', True), ('mule', True), ('llama', True),
|
||||
('trader_llama', True), ('camel', True), ('chicken', True), ('spider', True),
|
||||
('strider', True), ('ravager', True), ('zombie_horse', True),
|
||||
('skeleton_horse', False), ('cow', False),
|
||||
]
|
||||
|
||||
|
||||
def probe(label, mounted, player='Alice'):
|
||||
PROBES.append((label, player))
|
||||
EXPECTED.append(f'{label} mounted={str(mounted).lower()}')
|
||||
SCRIPT.append((label, []))
|
||||
|
||||
|
||||
for kind, ejects in MOUNTS:
|
||||
SCRIPT.append((kind + '-setup', [
|
||||
'cmd Alice kill @e[type=!player]',
|
||||
'cmd Alice fill -6 79 -6 6 95 6 minecraft:air',
|
||||
'cmd Alice fill -6 79 -6 6 95 6 minecraft:stone hollow',
|
||||
'cmd Alice fill -6 95 -6 6 95 6 minecraft:air',
|
||||
'cmd Alice tp @s 0.5 80 0.5',
|
||||
f'cmd Alice summon minecraft:{kind} 0.5 80 0.5 {{NoAI:1b,Silent:1b}}',
|
||||
f'cmd Alice ride @s mount @e[type=minecraft:{kind},limit=1]',
|
||||
]))
|
||||
for n in range(2):
|
||||
SCRIPT.append((f'{kind}-dry-wait-{n}', []))
|
||||
probe(kind + '-dry', True)
|
||||
# Water to the mount's back: the rider's eyes are above it.
|
||||
SCRIPT.append((kind + '-shallow-fill', ['cmd Alice fill -5 80 -5 5 80 5 minecraft:water']))
|
||||
for n in range(2):
|
||||
SCRIPT.append((f'{kind}-shallow-wait-{n}', []))
|
||||
probe(kind + '-shallow', True)
|
||||
# Water over the rider's head.
|
||||
SCRIPT.append((kind + '-deep-fill', ['cmd Alice fill -5 80 -5 5 92 5 minecraft:water']))
|
||||
for n in range(3):
|
||||
SCRIPT.append((f'{kind}-deep-wait-{n}', []))
|
||||
probe(kind + '-deep', not ejects)
|
||||
|
||||
|
||||
def shown(lines):
|
||||
# Each client's transcript is grouped under its name, so replay that
|
||||
# player's real passenger packets up to each observation step separately.
|
||||
order = {label: i for i, (label, _) in enumerate(SCRIPT)}
|
||||
observations = []
|
||||
for label, player in PROBES:
|
||||
mounted = None
|
||||
for line in lines:
|
||||
fields = line.split()
|
||||
if (len(fields) == 4 and fields[0] == player and fields[2] == 'passenger'
|
||||
and order.get(fields[1].removeprefix('step='), -1) <= order[label]):
|
||||
mounted = fields[3] == 'true'
|
||||
value = str(mounted).lower() if mounted is not None else 'missing mount'
|
||||
observations.append(f'{label} mounted={value}')
|
||||
return observations
|
||||
|
||||
|
||||
def check(native, all_lines):
|
||||
if native != EXPECTED:
|
||||
return f'vanilla did not exercise the expected immersion outcomes: {native!r}'
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
lines = net.one_player('mount-submerged-parity', SCRIPT, shown, check, __doc__, step_seconds=1.0)
|
||||
print(f'mount submersion: {len(lines)} passenger observations match vanilla')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user