mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Both extend the boat authority fixture and harness: keep the paddle and attack cases, both Case fields and both docstring paragraphs. The harness keeps main's fast-profile build with #10434's --resources option. Queued after #10434, so either landing first leaves the other mergeable. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
c9b0011952
5 files changed
+171
-15
No files matched your search
@@ -1,5 +1,5 @@
|
||||
//! The native parity harness runs the actual client boat ticker without a
|
||||
//! renderer, on an empty world; movement ownership changes between ticks.
|
||||
//! The native parity harness runs the client boat ticker and authoritative
|
||||
//! boat attacks without a renderer, on an empty world.
|
||||
use super::{Game, boats::Boat};
|
||||
use horizon_client::Result;
|
||||
use horizon_core::geometry::Vec3;
|
||||
@@ -16,6 +16,20 @@ struct Case {
|
||||
owned: Vec<bool>,
|
||||
#[serde(default)]
|
||||
paddles: Option<PaddleCase>,
|
||||
#[serde(default)]
|
||||
attack: Option<Attack>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Attack {
|
||||
yaw: f32,
|
||||
sprint: bool,
|
||||
ticks: u32,
|
||||
player_velocity: [f64; 3],
|
||||
#[serde(default)]
|
||||
knockback: u8,
|
||||
#[serde(default)]
|
||||
creative: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -49,6 +63,38 @@ impl Game<'_> {
|
||||
boat.velocity = Vec3::new(x, y, z);
|
||||
boat.delta_rotation = case.spin;
|
||||
self.boats.push(boat);
|
||||
if let Some(attack) = case.attack {
|
||||
let mut player =
|
||||
horizon_client::motion::PlayerMotion::new(self.motion, Vec3::ZERO)?;
|
||||
player.velocity = Vec3::new(
|
||||
attack.player_velocity[0],
|
||||
attack.player_velocity[1],
|
||||
attack.player_velocity[2],
|
||||
);
|
||||
player.set_sprinting(attack.sprint)?;
|
||||
self.player = Some(player);
|
||||
self.inventory.fill(None);
|
||||
self.selected = 0;
|
||||
if attack.knockback > 0 {
|
||||
self.inventory[0] = horizon_client::item_data::parse_stack(&format!(
|
||||
r#"{{id:"minecraft:stick",count:1,components:{{"minecraft:enchantments":{{"minecraft:knockback":{}}}}}}}"#,
|
||||
attack.knockback
|
||||
))?;
|
||||
}
|
||||
self.yaw = attack.yaw;
|
||||
self.attack_ticks = attack.ticks;
|
||||
self.creative = attack.creative;
|
||||
self.strike_boat(0);
|
||||
let player = self.player.as_ref().unwrap();
|
||||
output.push(json!({
|
||||
"name": case.name,
|
||||
"velocity": self.boats.first().map(|boat| boat.velocity.components().map(|v| format!("{:016x}", v.to_bits()))),
|
||||
"destroyed": self.boats.is_empty(),
|
||||
"player_velocity": player.velocity.components().map(|v| format!("{:016x}", v.to_bits())),
|
||||
"sprint": player.sprinting,
|
||||
}));
|
||||
continue;
|
||||
}
|
||||
let mut steps = Vec::new();
|
||||
if let Some(paddles) = case.paddles {
|
||||
self.riding = None;
|
||||
|
||||
@@ -1439,16 +1439,40 @@ impl Game<'_> {
|
||||
if !self.before_block(d) || self.mob_hit().is_some_and(|m| m.1 < d) {
|
||||
return false;
|
||||
}
|
||||
self.strike_boat(i);
|
||||
self.swing_arm();
|
||||
true
|
||||
}
|
||||
/// The accepted boat target, shared by aimed attacks and headless parity.
|
||||
pub(super) fn strike_boat(&mut self, i: usize) {
|
||||
let area = self.boats[i].bounds();
|
||||
// Player.attack's full damage against the boat's entity type.
|
||||
let (kind, id) = (self.boats[i].entity_type(), self.boats[i].id as i32);
|
||||
let (damage, _, _, magic) = self.melee_damage(&kind, false);
|
||||
self.hurt_boat(i, damage, self.creative);
|
||||
let (damage, scale, _, magic) = self.melee_damage(&kind, false);
|
||||
let hurt = self.hurt_boat_result(i, damage, self.creative);
|
||||
self.vehicle_attack_sounds(area, magic.then_some(id));
|
||||
if damage > 0.0
|
||||
&& let Some(destroyed) = hurt
|
||||
{
|
||||
let (knockback, _) = self.extra_knockback(&kind, scale);
|
||||
if knockback > 0.0 {
|
||||
// Player.causeExtraKnockback uses Entity.push for a boat,
|
||||
// not LivingEntity.knockback (no resistance or velocity halving).
|
||||
if !destroyed {
|
||||
let (x, z) = self.extra_knockback_direction();
|
||||
self.boats[i].velocity.x += f64::from(-(x as f32) * knockback);
|
||||
self.boats[i].velocity.y += 0.1;
|
||||
self.boats[i].velocity.z += f64::from(-(z as f32) * knockback);
|
||||
}
|
||||
if let Some(player) = self.player.as_mut() {
|
||||
player.velocity.x *= horizon_client::attack_strength::ATTACKER_SLOWDOWN;
|
||||
player.velocity.z *= horizon_client::attack_strength::ATTACKER_SLOWDOWN;
|
||||
}
|
||||
self.stop_sprint_after_extra_knockback();
|
||||
}
|
||||
}
|
||||
// Player.onAttack: the hit starts the attack strength again.
|
||||
self.attack_ticks = 0;
|
||||
self.swing_arm();
|
||||
true
|
||||
}
|
||||
/// `VehicleEntity.hurtServer`: wobble, add the damage and break past 40
|
||||
/// (`creative`: a creative player's hit discards it straight away).
|
||||
@@ -1456,11 +1480,15 @@ impl Game<'_> {
|
||||
/// reason that destroys the boat (DISCARDED included), while the boat item
|
||||
/// itself only drops on the death path (`VehicleEntity.destroy`), not when
|
||||
/// a creative hit discards it. True when the boat was removed.
|
||||
pub(super) fn hurt_boat(&mut self, i: usize, mut damage: f32, creative: bool) -> bool {
|
||||
pub(super) fn hurt_boat(&mut self, i: usize, damage: f32, creative: bool) -> bool {
|
||||
self.hurt_boat_result(i, damage, creative).unwrap_or(false)
|
||||
}
|
||||
/// None means a canceled hit; Some records whether an accepted hit destroyed it.
|
||||
fn hurt_boat_result(&mut self, i: usize, mut damage: f32, creative: bool) -> Option<bool> {
|
||||
let mut cancel = false;
|
||||
self.native_vehicle_damage("boat", &mut damage, &mut cancel);
|
||||
if cancel {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
let b = &mut self.boats[i];
|
||||
b.hurt_dir = -b.hurt_dir;
|
||||
@@ -1499,7 +1527,7 @@ impl Game<'_> {
|
||||
);
|
||||
}
|
||||
}
|
||||
destroyed
|
||||
Some(destroyed)
|
||||
}
|
||||
/// AbstractBoatRenderer.
|
||||
pub(super) fn boat_geometry(&self, camera: glam::DVec3, alpha: f64, out: &mut Vec<Vertex>) {
|
||||
|
||||
@@ -3333,7 +3333,10 @@ impl Game<'_> {
|
||||
return;
|
||||
}
|
||||
let (old_yaw, old_pitch) = (host.hub.guests[gi].yaw, host.hub.guests[gi].pitch);
|
||||
if self.look_at_entity(host, gi, id, None).is_none() {
|
||||
let boat = self.boats.iter().position(|boat| boat.id as i32 == id);
|
||||
// A packet names its target already. Keep the attacker's actual yaw
|
||||
// for the boat's knockback instead of aiming a ray at its center.
|
||||
if boat.is_none() && self.look_at_entity(host, gi, id, None).is_none() {
|
||||
return;
|
||||
}
|
||||
// A mace's smash sets the attacker's velocity and sends it at once, before the
|
||||
@@ -3344,7 +3347,12 @@ impl Game<'_> {
|
||||
.player
|
||||
.as_mut()
|
||||
.is_some_and(|p| std::mem::take(&mut p.health.sync_velocity));
|
||||
g.attack();
|
||||
if let Some(index) = boat {
|
||||
g.strike_boat(index);
|
||||
g.swing_arm();
|
||||
} else {
|
||||
g.attack();
|
||||
}
|
||||
let p = g.player.as_mut()?;
|
||||
let sent = std::mem::replace(&mut p.health.sync_velocity, marked);
|
||||
sent.then_some(p.velocity)
|
||||
|
||||
@@ -36,6 +36,7 @@ public final class VanillaBoatAuthorityOracle {
|
||||
Lvl() { super(null, null, null, null, null, null, false, 0L, List.of(), false); }
|
||||
@Override public boolean isClientSide() { return true; }
|
||||
@Override public int getNextEntityId() { return next++; }
|
||||
@Override public void gameEvent(net.minecraft.core.Holder<net.minecraft.world.level.gameevent.GameEvent> event, Vec3 position, net.minecraft.world.level.gameevent.GameEvent.Context context) {}
|
||||
@Override public void sendPacketToServer(net.minecraft.network.protocol.Packet<?> packet) {}
|
||||
@Override public RegistryAccess registryAccess() { return ParityData.access; }
|
||||
@Override public BlockState getBlockState(BlockPos pos) { return Blocks.AIR.defaultBlockState(); }
|
||||
@@ -58,6 +59,14 @@ public final class VanillaBoatAuthorityOracle {
|
||||
@Override public net.minecraft.world.scores.PlayerTeam getTeam() { return null; }
|
||||
}
|
||||
|
||||
static JsonArray bits(Vec3 velocity) {
|
||||
JsonArray result = new JsonArray();
|
||||
for (double value : new double[]{velocity.x, velocity.y, velocity.z}) {
|
||||
result.add(String.format("%016x", Double.doubleToRawLongBits(value)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
ParityData.init();
|
||||
VanillaVehicleContainerOracle.reflect();
|
||||
@@ -76,6 +85,49 @@ public final class VanillaBoatAuthorityOracle {
|
||||
JsonArray velocity = c.getAsJsonArray("velocity");
|
||||
boat.setDeltaMovement(new Vec3(velocity.get(0).getAsDouble(), velocity.get(1).getAsDouble(), velocity.get(2).getAsDouble()));
|
||||
spin.setFloat(boat, c.get("spin").getAsFloat());
|
||||
if (c.has("attack")) {
|
||||
JsonObject attack = c.getAsJsonObject("attack");
|
||||
var u = Class.forName("sun.misc.Unsafe");
|
||||
VanillaRemoteAttackOracle.unsafe = VanillaRemoteAttackOracle.field(u, "theUnsafe").get(null);
|
||||
VanillaRemoteAttackOracle.allocate = u.getMethod("allocateInstance", Class.class);
|
||||
var client = VanillaRemoteAttackOracle.make(VanillaRemoteAttackOracle.Client.class);
|
||||
client.registries = ParityData.access;
|
||||
client.sources = new net.minecraft.world.damagesource.DamageSources(ParityData.access);
|
||||
var player = VanillaRemoteAttackOracle.entity(VanillaRemoteAttackOracle.Attacker.class, EntityTypes.PLAYER, client);
|
||||
VanillaRemoteAttackOracle.field(net.minecraft.world.entity.player.Player.class, "abilities")
|
||||
.set(player, new net.minecraft.world.entity.player.Abilities());
|
||||
VanillaRemoteAttackOracle.field(net.minecraft.world.entity.LivingEntity.class, "activeEffects")
|
||||
.set(player, new java.util.HashMap<>());
|
||||
player.map = new net.minecraft.world.entity.ai.attributes.AttributeMap(
|
||||
net.minecraft.world.entity.player.Player.createAttributes().build());
|
||||
player.hand = net.minecraft.world.item.ItemStack.EMPTY;
|
||||
VanillaRemoteAttackOracle.field(net.minecraft.world.entity.LivingEntity.class, "attackStrengthTicker")
|
||||
.setInt(player, attack.get("ticks").getAsInt());
|
||||
player.sprint = attack.get("sprint").getAsBoolean();
|
||||
player.setYRot(attack.get("yaw").getAsFloat());
|
||||
var pv = attack.getAsJsonArray("player_velocity");
|
||||
player.setDeltaMovement(new Vec3(pv.get(0).getAsDouble(), pv.get(1).getAsDouble(), pv.get(2).getAsDouble()));
|
||||
// Player.attack adds 0.5 only to a charged sprint hit.
|
||||
float strength = Math.min(1.0f, (attack.get("ticks").getAsInt() + 0.5f) / 5.0f);
|
||||
float amount = (player.sprint && strength > 0.9f ? 0.5f : 0.0f)
|
||||
+ attack.get("knockback").getAsFloat() / 2.0f;
|
||||
if (attack.get("knockback").getAsInt() == 0 && !attack.get("creative").getAsBoolean()) {
|
||||
player.attack(boat);
|
||||
} else {
|
||||
// Enchanted/creative controls isolate causeExtraKnockback;
|
||||
// server enchantment lookup and hurt/removal are not simulated here.
|
||||
player.causeExtraKnockback(boat, amount, boat.getDeltaMovement(), null, 1.0f, true);
|
||||
}
|
||||
JsonObject result = new JsonObject();
|
||||
result.add("name", c.get("name"));
|
||||
boolean destroyed = attack.get("creative").getAsBoolean();
|
||||
result.add("velocity", destroyed ? com.google.gson.JsonNull.INSTANCE : bits(boat.getDeltaMovement()));
|
||||
result.addProperty("destroyed", destroyed);
|
||||
result.add("player_velocity", bits(player.getDeltaMovement()));
|
||||
result.addProperty("sprint", player.sprint);
|
||||
output.add(result);
|
||||
continue;
|
||||
}
|
||||
JsonArray steps = new JsonArray();
|
||||
if (c.has("paddles")) {
|
||||
JsonObject paddles = c.getAsJsonObject("paddles");
|
||||
|
||||
@@ -13,6 +13,13 @@ Horizon's controlled ticker and remote-copy ticker. Synced flag values and the
|
||||
first passenger are controlled inputs, including stale true flags after a
|
||||
player leaves and a non-player first passenger. These checks compare animation
|
||||
phase and interpolation, not rendered screenshots or network packet delivery.
|
||||
|
||||
Bare-hand and enchanted-stick attacks additionally run Horizon's real boat strike and vanilla's
|
||||
Player.attack for bare-hand hits and Player.causeExtraKnockback for the
|
||||
enchanted/creative controls, checking retained boat motion, upward impulse, the
|
||||
attacker's slowdown and sprint cancellation. The enchanted/creative Java
|
||||
knockback amount and removal flag are controlled; the vanilla server attack
|
||||
and enchantment lookup are not run.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
@@ -75,6 +82,22 @@ def cases():
|
||||
dict(occupied=True, first_player=True, flags=[True, True]),
|
||||
dict(occupied=False, flags=[True, True]),
|
||||
])))
|
||||
# Player.causeExtraKnockback's nonliving branch, through the actual boat
|
||||
# attack in Horizon: uncharged/non-sprinting controls, axis and oblique yaw,
|
||||
# retained target motion and the attacker's slowdown/sprint cancellation.
|
||||
for yaw in (0.0, 90.0, 180.0, -90.0, 37.0):
|
||||
for sprint in (False, True):
|
||||
for ticks in (0, 4, 5, 100):
|
||||
for velocity in ([0.0, 0.0, 0.0], [0.125, -0.0625, -0.25]):
|
||||
for knockback in (0, 1, 2):
|
||||
out.append(dict(name=f'attack-{len(out)}', spin=0, velocity=velocity, owned=[],
|
||||
attack=dict(yaw=yaw, sprint=sprint, ticks=ticks, knockback=knockback, creative=False,
|
||||
player_velocity=[0.25, 0.42, -0.1])))
|
||||
for sprint in (False, True):
|
||||
for knockback in (0, 1, 2):
|
||||
out.append(dict(name=f'attack-destroyed-{len(out)}', spin=0, velocity=[0.0, 0.0, 0.0], owned=[],
|
||||
attack=dict(yaw=0.0, sprint=sprint, ticks=100, knockback=knockback,
|
||||
creative=True, player_velocity=[0.25, 0.42, -0.1])))
|
||||
return out
|
||||
|
||||
|
||||
@@ -114,11 +137,10 @@ def main():
|
||||
'compile-java')
|
||||
run(['java', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.VanillaBoatAuthorityOracle',
|
||||
work / 'inputs.json', work / 'vanilla.json'], 'vanilla')
|
||||
run(['cargo', 'build', '--locked', '-p', 'horizon-client', '--bin', 'horizon-client'], 'compile-rust')
|
||||
binary = lab.cargo_target() / 'debug' / ('horizon-client.exe' if os.name == 'nt' else 'horizon-client')
|
||||
run(['cargo', 'build', '--profile', 'fast', '-p', 'horizon-client', '--bin', 'horizon-client'], 'compile-rust')
|
||||
binary = lab.cargo_target() / 'fast' / ('horizon-client.exe' if os.name == 'nt' else 'horizon-client')
|
||||
with (work / 'rust.log').open('w') as log, tempfile.TemporaryDirectory(prefix='boat-authority-') as game_dir:
|
||||
run_game([str(binary), '--headless', '--resources', str(args.resources.resolve()),
|
||||
'--game-dir', game_dir], cwd=game_dir,
|
||||
run_game([str(binary), '--headless', '--resources', str(args.resources.resolve()), '--game-dir', game_dir], cwd=game_dir,
|
||||
env=lab.game_env(HORIZON_BOAT_AUTHORITY_FIXTURE=work), stdout=log,
|
||||
stderr=subprocess.STDOUT, check=True, timeout=180)
|
||||
native = json.loads((work / 'vanilla.json').read_text())
|
||||
|
||||
Reference in new issue
Block a user