mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Reset mob fall distance before travel
This commit is contained in:
1 parent
69fab597a6
commit
8181ce0222
4 files changed
+243
-5
No files matched your search
@@ -681,6 +681,7 @@ pub fn tick(
|
||||
super::villager_brain::woken(m, w.game_time);
|
||||
}
|
||||
if m.no_ai {
|
||||
reset_effect_fall_distance(m);
|
||||
// Mob.aiStep still runs without AI (only serverAiStep is skipped),
|
||||
// so undead still burn in daylight and the endermite's life keeps
|
||||
// counting.
|
||||
@@ -1541,6 +1542,14 @@ fn ageable_step(
|
||||
}
|
||||
}
|
||||
|
||||
/// `LivingEntity.aiStep`: both flight effects discard accumulated fall before
|
||||
/// travel, even when NoAI skips the server AI step.
|
||||
fn reset_effect_fall_distance(m: &mut Mob) {
|
||||
if m.has_effect("minecraft:slow_falling") || m.has_effect("minecraft:levitation") {
|
||||
m.fall_distance = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// LivingEntity.aiStep (and Mob/AgeableMob/Animal additions).
|
||||
fn ai_step(m: &mut Mob, w: &MobWorld, level_random: &mut LegacyRandom, events: &mut Vec<MobEvent>) {
|
||||
if m.species() == Species::Parrot {
|
||||
@@ -1602,6 +1611,7 @@ fn ai_step(m: &mut Mob, w: &MobWorld, level_random: &mut LegacyRandom, events: &
|
||||
f64::from(m.ai.yya),
|
||||
f64::from(m.ai.zza),
|
||||
);
|
||||
reset_effect_fall_distance(m);
|
||||
if super::equine::moved_by_client(m) {
|
||||
// LivingEntity.travelRidden: the rider's client moves it
|
||||
// (handleMoveVehicle), so the server only stops it.
|
||||
@@ -4969,3 +4979,75 @@ mod air_tests {
|
||||
assert_eq!(air, 300);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod flight_effect_fall_tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn flight_effects_reset_accumulated_fall_distance_even_without_ai() {
|
||||
let root = std::path::PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
|
||||
let ran = crate::mobs::world::with_resource_test_world(
|
||||
&root,
|
||||
&[],
|
||||
true,
|
||||
true,
|
||||
|world, _| {
|
||||
world
|
||||
.chunks
|
||||
.insert((0, 0), Arc::new(crate::chunk::Chunk::empty(0, 0)));
|
||||
},
|
||||
|world, types| {
|
||||
let mut failures = Vec::new();
|
||||
let mut rows = Vec::new();
|
||||
for effect in [
|
||||
None,
|
||||
Some("minecraft:levitation"),
|
||||
Some("minecraft:slow_falling"),
|
||||
] {
|
||||
for no_ai in [false, true] {
|
||||
for velocity_y in [0.0, 0.2, -0.2] {
|
||||
let kind = Arc::new(types.types["minecraft:cow"].clone());
|
||||
let mut mob = Mob::new(1, kind, Vec3::new(2.5, 64.0, 2.5), 0.0);
|
||||
mob.no_ai = no_ai;
|
||||
mob.no_gravity = true;
|
||||
mob.persistent = true;
|
||||
mob.fall_distance = 12.0;
|
||||
mob.velocity.y = velocity_y;
|
||||
if let Some(effect) = effect {
|
||||
mob.add_effect(effect, 100, 0);
|
||||
}
|
||||
let mut random = LegacyRandom::new(0);
|
||||
assert!(tick(&mut mob, world, &mut random, &mut Vec::new()));
|
||||
// The pinned Java aiStep resets before travel. A
|
||||
// downward move then adds 0.2F of NEW fall distance;
|
||||
// NoAI skips movement but must still erase the old fall.
|
||||
let expected = match (effect.is_some(), no_ai, velocity_y < 0.0) {
|
||||
(false, false, true) => 12.200000002980232,
|
||||
(true, false, true) => 0.20000000298023224,
|
||||
(false, _, _) => 12.0,
|
||||
(true, _, _) => 0.0,
|
||||
};
|
||||
let name = effect.unwrap_or("none").trim_start_matches("minecraft:");
|
||||
rows.push(format!(
|
||||
"{name} {no_ai} {:016x} {:016x}",
|
||||
velocity_y.to_bits(),
|
||||
mob.fall_distance.to_bits()
|
||||
));
|
||||
if mob.fall_distance != expected {
|
||||
failures.push((effect, no_ai, velocity_y, mob.fall_distance));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(path) = std::env::var_os("HORIZON_MOB_FALL_RESET_OUTPUT") {
|
||||
std::fs::write(path, rows.join("\n") + "\n").unwrap();
|
||||
}
|
||||
assert!(failures.is_empty(), "fall distance differs: {failures:?}");
|
||||
},
|
||||
);
|
||||
assert!(ran, "prepared resources must run the integration fixture");
|
||||
}
|
||||
}
|
||||
@@ -1382,11 +1382,6 @@ impl Mob {
|
||||
{
|
||||
y += (0.05 * f64::from(amp + 1) - movement.y) * 0.2;
|
||||
} else {
|
||||
// LivingEntity.travelInAir: a Slow Falling mob on its way down lands without
|
||||
// having fallen.
|
||||
if self.velocity.y <= 0.0 && self.has_effect("minecraft:slow_falling") {
|
||||
self.fall_distance = 0.0;
|
||||
}
|
||||
y -= self.effective_gravity();
|
||||
}
|
||||
// LivingEntity.shouldDiscardFriction (a breeze's, a frog's or a goat's
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package dev.horizonlab.parity;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.world.effect.MobEffect;
|
||||
import net.minecraft.world.effect.MobEffectInstance;
|
||||
import net.minecraft.world.effect.MobEffects;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.entity.animal.cow.Cow;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Real Cow.aiStep through travel and movement, stopped before block contacts.
|
||||
* The scene is dry air and gravity is disabled to isolate accumulated fall distance.
|
||||
* Active-effect lookups are controlled; the NoAI flag and movement are vanilla's.
|
||||
*/
|
||||
public final class VanillaMobFallResetOracle {
|
||||
static final class Finished extends RuntimeException {}
|
||||
|
||||
static final class Actor extends Cow {
|
||||
String effect;
|
||||
boolean reachedTravelEnd;
|
||||
|
||||
Actor(Level level) { super(EntityTypes.COW, level); }
|
||||
|
||||
@Override public boolean hasEffect(Holder<MobEffect> query) {
|
||||
return "levitation".equals(effect) && query == MobEffects.LEVITATION
|
||||
|| "slow_falling".equals(effect) && query == MobEffects.SLOW_FALLING;
|
||||
}
|
||||
|
||||
@Override public MobEffectInstance getEffect(Holder<MobEffect> query) {
|
||||
return hasEffect(query) ? new MobEffectInstance(query, 100, 0) : null;
|
||||
}
|
||||
|
||||
// The fixture contains only air; no fluid interaction is possible.
|
||||
@Override protected boolean updateFluidInteraction() { return false; }
|
||||
|
||||
@Override protected void applyEffectsFromBlocks() {
|
||||
reachedTravelEnd = true;
|
||||
throw new Finished();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
if (args.length != 1) throw new IllegalArgumentException("expected output path");
|
||||
ParityData.init();
|
||||
VanillaBlockUpdateOracle.AIR = Blocks.AIR.defaultBlockState();
|
||||
VanillaBlockUpdateOracle.OVERWORLD_TYPE = VanillaBlockUpdateOracle.loadOverworld();
|
||||
var allocator = Class.forName("sun.misc.Unsafe");
|
||||
var field = allocator.getDeclaredField("theUnsafe");
|
||||
field.setAccessible(true);
|
||||
VanillaBlockUpdateOracle.unsafe = field.get(null);
|
||||
VanillaBlockUpdateOracle.allocate = allocator.getMethod("allocateInstance", Class.class);
|
||||
try (var out = new PrintWriter(Files.newBufferedWriter(Path.of(args[0])))) {
|
||||
for (String effect : List.of("none", "levitation", "slow_falling")) {
|
||||
for (boolean noAI : List.of(false, true)) {
|
||||
for (double vy : new double[]{0.0, 0.2, -0.2}) {
|
||||
var world = VanillaBlockUpdateOracle.build(
|
||||
JsonParser.parseString("{\"cells\":[],\"seed\":0}").getAsJsonObject());
|
||||
world.entities = new ArrayList<>();
|
||||
world.registries = ParityData.access;
|
||||
var actor = new Actor(world);
|
||||
actor.effect = effect;
|
||||
actor.setNoAi(noAI);
|
||||
actor.fallDistance = 12.0;
|
||||
actor.setNoGravity(true);
|
||||
actor.setPos(2.5, 64.0, 2.5);
|
||||
actor.setDeltaMovement(0, vy, 0);
|
||||
try { actor.aiStep(); } catch (Finished done) { }
|
||||
if (!actor.reachedTravelEnd) {
|
||||
throw new AssertionError("did not reach the post-travel boundary");
|
||||
}
|
||||
out.printf(Locale.ROOT, "%s %s %016x %016x%n", effect, noAI,
|
||||
Double.doubleToLongBits(vy), Double.doubleToLongBits(actor.fallDistance));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare mob flight-effect fall distance with the pinned server's actual aiStep.
|
||||
|
||||
The Java cow runs through real travel/movement, stopping before inside-block effects.
|
||||
Its fluid query is fixed to dry air; active effects are controlled and NoAI is real.
|
||||
Horizon runs its complete production mob tick in an empty resource-backed world.
|
||||
Gravity is disabled. No effect, Levitation and Slow Falling are compared for both
|
||||
NoAI states and zero/upward/downward movement. Positions, damage and landing are
|
||||
outside this scoped comparison.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
try:
|
||||
from . import lab, oracle_harness as harness, test_rust_parity as base, integration_env
|
||||
except ImportError:
|
||||
import lab, oracle_harness as harness, test_rust_parity as base, integration_env
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--resources', type=Path,
|
||||
default=Path(os.environ.get('HORIZON_TEST_RESOURCES', lab.RESOURCES)))
|
||||
args = parser.parse_args()
|
||||
resources = args.resources.resolve()
|
||||
if not (resources / 'entity-types.json').is_file():
|
||||
raise SystemExit('prepared resources required; pass --resources PATH')
|
||||
lock, jars, libraries = base.verify_artifacts()
|
||||
build = lab.LOCAL / 'rust-mob-fall-reset-parity'
|
||||
build.mkdir(parents=True, exist_ok=True)
|
||||
report = build / 'report.json'
|
||||
report.unlink(missing_ok=True)
|
||||
java_root = lab.ROOT / 'mods/horizon/src/test/java'
|
||||
oracle = java_root / 'dev/horizonlab/parity/VanillaMobFallResetOracle.java'
|
||||
sources = [Path(__file__), oracle, lab.ROOT / 'tools/lab.py',
|
||||
lab.ROOT / 'tools/oracle_harness.py', lab.ROOT / 'tools/test_rust_parity.py',
|
||||
lab.ROOT / 'tools/integration_env.py', lab.ROOT / 'Cargo.toml', lab.ROOT / 'Cargo.lock',
|
||||
lab.ROOT / 'crates/horizon-client/src/mobs.rs',
|
||||
lab.ROOT / 'crates/horizon-client/src/mobs/ai.rs',
|
||||
lab.ROOT / 'crates/horizon-client/src/mobs/living.rs',
|
||||
lab.ROOT / 'crates/horizon-client/src/mobs/world.rs',
|
||||
java_root / 'dev/horizonlab/parity/ParityData.java',
|
||||
java_root / 'dev/horizonlab/parity/VanillaBlockUpdateOracle.java']
|
||||
hashes = base.source_hashes(sources)
|
||||
classes = build / 'classes'
|
||||
classes.mkdir(exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
harness.run_logged(['javac', '--release', lock['minecraft']['java_major'], '-proc:none',
|
||||
'-sourcepath', java_root, '-cp', dependencies, '-d', classes, oracle],
|
||||
build, 'compile-java')
|
||||
harness.run_logged(['java', '-Xmx512M', '-Djava.awt.headless=true', '-cp',
|
||||
str(classes) + os.pathsep + dependencies,
|
||||
'dev.horizonlab.parity.VanillaMobFallResetOracle', build / 'server.txt'],
|
||||
build, 'server')
|
||||
with tempfile.TemporaryDirectory(prefix='horizon-mob-fall-reset-') as scratch:
|
||||
env = integration_env.game_environment(os.environ, scratch)
|
||||
env.update(HORIZON_TEST_RESOURCES=str(resources),
|
||||
HORIZON_MOB_FALL_RESET_OUTPUT=str(build / 'rust.txt'))
|
||||
env.setdefault('CARGO_BUILD_JOBS', '1')
|
||||
env.setdefault('RUST_TEST_THREADS', '2')
|
||||
harness.run_logged(['cargo', 'test', '--locked', '-p', 'horizon-client-lib', '--lib',
|
||||
'mobs::ai::flight_effect_fall_tests::flight_effects_reset_accumulated_fall_distance_even_without_ai',
|
||||
'--', '--exact', '--ignored'], build, 'rust', env=env)
|
||||
matched = harness.compare(build / 'server.txt', build / 'rust.txt', 18)
|
||||
base.ensure_sources_unchanged(sources, hashes)
|
||||
lab.write_json(report, dict(schema=1, minecraft=lock['minecraft']['version'],
|
||||
sources_sha256=hashes, server_jar_sha256=lab.digest(jars['server'], 'sha256'),
|
||||
matched_output_lines=matched, output_sha256=lab.digest(build / 'rust.txt', 'sha256'),
|
||||
scope=__doc__))
|
||||
print(f'{matched} mob fall-distance cases match the pinned server: {report}', flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user