mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 17:21:49 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66e41ed573 | ||
|
|
72542a89e2 | ||
|
|
0734daa911 | ||
|
|
f2c7a2d359 |
No files matched your search
@@ -5,7 +5,7 @@ use horizon_client::{
|
||||
Result,
|
||||
camera::{
|
||||
AvatarBob, BobState, Camera, CameraEntity, CameraFluid, CameraType, FovPlayer, HurtBob,
|
||||
LivingCamera, UseItem, bob_matrix, field_of_view_modifier,
|
||||
LivingCamera, UseItem, bob_matrix, field_of_view_modifier, perspective,
|
||||
},
|
||||
};
|
||||
use horizon_core::{
|
||||
@@ -258,6 +258,15 @@ fn camera(c: &Value, out: &mut impl Write) -> Result<()> {
|
||||
fb(fov),
|
||||
fb(hud)
|
||||
)?;
|
||||
let (w, h, far) = (fl(&op[4]), fl(&op[5]), fl(&op[6]));
|
||||
let matrix =
|
||||
|m: [f32; 16]| m.iter().map(|&x| format!(" {}", fb(x))).collect::<String>();
|
||||
writeln!(
|
||||
out,
|
||||
"projection{}{}",
|
||||
matrix(perspective(fov, w, h, far, 0.05, true)),
|
||||
matrix(perspective(hud, w, h, far, 0.05, true))
|
||||
)?;
|
||||
}
|
||||
other => panic!("{other}"),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
//! Probe for test_first_person_parity.py: drives horizon_client::first_person
|
||||
//! and attack_strength over the harness's scripted clicks, targets and use
|
||||
//! results, printing what VanillaFirstPersonOracle prints.
|
||||
use horizon_client::{
|
||||
Result,
|
||||
attack_strength::{AttackStrength, HeldStack},
|
||||
first_person::{
|
||||
Clicks, Hand, HandHeights, HandWorld, Hit, SwingAnimation, SwingState, UseResult,
|
||||
swing_duration,
|
||||
},
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
};
|
||||
|
||||
fn fb(v: f32) -> String {
|
||||
format!("{:08x}", v.to_bits())
|
||||
}
|
||||
fn fl(v: &Value) -> f32 {
|
||||
f32::from_bits(u32::from_str_radix(v.as_str().unwrap(), 16).unwrap())
|
||||
}
|
||||
fn dl(v: &Value) -> f64 {
|
||||
f64::from_bits(u64::from_str_radix(v.as_str().unwrap(), 16).unwrap())
|
||||
}
|
||||
|
||||
/// An `ItemStack` object: the hands hold indices into `Probe::stacks`, so
|
||||
/// shrinking keeps its identity as vanilla's in-place `shrink` does.
|
||||
#[derive(Clone)]
|
||||
struct Stack {
|
||||
item: String,
|
||||
count: i32,
|
||||
}
|
||||
impl Stack {
|
||||
fn empty(&self) -> bool {
|
||||
self.item == "minecraft:air" || self.count <= 0
|
||||
}
|
||||
/// `getItem` and `getCount` of an empty stack are air and 0.
|
||||
fn effective(&self) -> (&str, i32) {
|
||||
if self.empty() {
|
||||
("minecraft:air", 0)
|
||||
} else {
|
||||
(&self.item, self.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Probe<'a> {
|
||||
case: &'a Value,
|
||||
step: &'a Value,
|
||||
creative: bool,
|
||||
stacks: Vec<Stack>,
|
||||
/// The stacks in the main and off hand.
|
||||
hands: [usize; 2],
|
||||
blocks: HashMap<[i32; 3], String>,
|
||||
swing: SwingState,
|
||||
events: Vec<String>,
|
||||
heights: HandHeights,
|
||||
}
|
||||
fn hand_name(hand: Hand) -> &'static str {
|
||||
match hand {
|
||||
Hand::Main => "main",
|
||||
Hand::Off => "off",
|
||||
}
|
||||
}
|
||||
impl Probe<'_> {
|
||||
fn stack(&self, hand: Hand) -> &Stack {
|
||||
&self.stacks[self.hands[hand as usize]]
|
||||
}
|
||||
fn new_stack(&mut self, v: &Value) -> usize {
|
||||
// ItemStack.EMPTY is index 0.
|
||||
if v.is_null() {
|
||||
return 0;
|
||||
}
|
||||
self.stacks.push(Stack {
|
||||
item: v[0].as_str().unwrap().into(),
|
||||
count: v[1].as_i64().unwrap() as i32,
|
||||
});
|
||||
self.stacks.len() - 1
|
||||
}
|
||||
fn table<'b>(&'b self, name: &str) -> &'b Value {
|
||||
&self.case["rust"][name]
|
||||
}
|
||||
fn attack_speed(&self) -> f64 {
|
||||
let item = self.stack(Hand::Main).effective().0;
|
||||
self.table("attack_speed").get(item).map_or(4.0, dl)
|
||||
}
|
||||
fn scripted(&mut self, kind: &str, hand: Hand) -> UseResult {
|
||||
let name = self.step["uses"][kind].as_str().unwrap().to_string();
|
||||
self.events
|
||||
.push(format!("use {kind} {} {name}", hand_name(hand)));
|
||||
let result = match name.as_str() {
|
||||
"success" => UseResult::Success { swing: true },
|
||||
"server" | "consume" => UseResult::Success { swing: false },
|
||||
"fail" => UseResult::Fail,
|
||||
_ => UseResult::Pass,
|
||||
};
|
||||
if matches!(result, UseResult::Success { .. }) && self.step["shrink"].as_bool().unwrap() {
|
||||
let i = self.hands[hand as usize];
|
||||
if i != 0 {
|
||||
self.stacks[i].count -= 1;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
impl HandWorld for Probe<'_> {
|
||||
fn hit(&mut self) -> Hit {
|
||||
let h = &self.step["hit"];
|
||||
match h[0].as_str().unwrap() {
|
||||
"block" => Hit::Block {
|
||||
pos: [0, 1, 2].map(|i| h[i + 1].as_i64().unwrap() as i32),
|
||||
face: h[4].as_u64().unwrap() as u8,
|
||||
},
|
||||
"entity" => Hit::Entity,
|
||||
_ => Hit::Miss,
|
||||
}
|
||||
}
|
||||
fn is_air(&mut self, pos: [i32; 3]) -> bool {
|
||||
self.blocks.get(&pos).is_none_or(|b| b == "minecraft:air")
|
||||
}
|
||||
fn creative(&self) -> bool {
|
||||
self.creative
|
||||
}
|
||||
fn spectator(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn instabuild(&self) -> bool {
|
||||
self.creative
|
||||
}
|
||||
fn hands_busy(&self) -> bool {
|
||||
self.step["busy"].as_bool().unwrap()
|
||||
}
|
||||
fn using_item(&self) -> bool {
|
||||
self.step["using"].as_bool().unwrap()
|
||||
}
|
||||
fn item_enabled(&mut self, _: Hand) -> bool {
|
||||
true
|
||||
}
|
||||
fn held_empty(&mut self, hand: Hand) -> bool {
|
||||
self.stack(hand).empty()
|
||||
}
|
||||
fn held_count(&mut self, hand: Hand) -> i32 {
|
||||
self.stack(hand).effective().1
|
||||
}
|
||||
fn infinite_materials(&self) -> bool {
|
||||
self.creative
|
||||
}
|
||||
fn cannot_attack(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
fn piercing_weapon(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
fn attack_animation(&mut self) -> SwingAnimation {
|
||||
SwingAnimation::DEFAULT
|
||||
}
|
||||
fn interact_animation(&mut self, _: Hand) -> SwingAnimation {
|
||||
SwingAnimation::DEFAULT
|
||||
}
|
||||
fn piercing_attack(&mut self, _: SwingAnimation) {
|
||||
unreachable!()
|
||||
}
|
||||
fn attack_entity(&mut self) -> bool {
|
||||
self.events.push("attack".into());
|
||||
true
|
||||
}
|
||||
fn block_action_restricted(&mut self, _: [i32; 3]) -> bool {
|
||||
false
|
||||
}
|
||||
fn within_border(&mut self, _: [i32; 3]) -> bool {
|
||||
true
|
||||
}
|
||||
fn attack_block(&mut self, _: [i32; 3]) {}
|
||||
/// `BlockBehaviour.getDestroyProgress` with `Player.getDestroySpeed` for
|
||||
/// a player on the ground, out of water, with no mining efficiency.
|
||||
fn destroy_progress(&mut self, pos: [i32; 3]) -> f32 {
|
||||
let block = self.blocks.get(&pos).cloned().unwrap_or_default();
|
||||
let b = &self.table("blocks")[block.as_str()];
|
||||
let hardness = fl(&b[0]);
|
||||
if hardness == -1.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let item = self.stack(Hand::Main).effective().0.to_string();
|
||||
let tool = &self.table("tools")[format!("{item} {block}").as_str()];
|
||||
let (mut speed, correct) = if tool.is_null() {
|
||||
(1.0f32, false)
|
||||
} else {
|
||||
(fl(&tool[0]), tool[1].as_bool().unwrap())
|
||||
};
|
||||
if let Some(amp) = self.case["haste"].as_i64() {
|
||||
speed *= 1.0 + (amp as i32 + 1) as f32 * 0.2;
|
||||
}
|
||||
if let Some(amp) = self.case["fatigue"].as_i64() {
|
||||
speed *= horizon_core::jmath::pow(0.3, f64::from(amp as i32 + 1)) as f32;
|
||||
}
|
||||
let harvest = !b[1].as_bool().unwrap() || correct;
|
||||
speed / hardness / if harvest { 30.0 } else { 100.0 }
|
||||
}
|
||||
fn destroy_block(&mut self, pos: [i32; 3]) -> bool {
|
||||
let item = self.stack(Hand::Main).effective().0;
|
||||
// Item.canDestroyBlock: swords' tools cannot destroy in creative.
|
||||
let sword = item.ends_with("_sword");
|
||||
let ok = !(sword && self.creative) && !self.is_air(pos);
|
||||
if ok {
|
||||
self.blocks.insert(pos, "minecraft:air".into());
|
||||
}
|
||||
self.events
|
||||
.push(format!("destroy {} {} {} {ok}", pos[0], pos[1], pos[2]));
|
||||
ok
|
||||
}
|
||||
/// The stack object itself: `destroyingItem` is a reference.
|
||||
fn main_hand_key(&mut self) -> String {
|
||||
self.hands[0].to_string()
|
||||
}
|
||||
fn same_destroying_item(&mut self, key: &str) -> bool {
|
||||
// ItemStack.EMPTY (index 0) before any break.
|
||||
let then = &self.stacks[key.parse::<usize>().unwrap_or(0)];
|
||||
then.effective().0 == self.stack(Hand::Main).effective().0
|
||||
}
|
||||
fn destroy_stage(&mut self, pos: [i32; 3], stage: Option<i32>) {
|
||||
self.events.push(format!(
|
||||
"stage {} {} {} {}",
|
||||
pos[0],
|
||||
pos[1],
|
||||
pos[2],
|
||||
stage.unwrap_or(-1)
|
||||
));
|
||||
}
|
||||
fn swing(&mut self, hand: Hand, animation: SwingAnimation) {
|
||||
let duration = swing_duration(
|
||||
animation,
|
||||
self.case["haste"].as_i64().map(|a| a as i32),
|
||||
self.case["fatigue"].as_i64().map(|a| a as i32),
|
||||
);
|
||||
let started = self.swing.start_if_able(hand, animation, duration);
|
||||
self.events.push(format!(
|
||||
"swing {} {} {started}",
|
||||
hand_name(hand),
|
||||
animation.duration
|
||||
));
|
||||
}
|
||||
fn reset_attack_strength(&mut self) {
|
||||
unreachable!("ResetWorld resets the tickers")
|
||||
}
|
||||
fn entity_within_border(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
fn use_on_entity(&mut self, hand: Hand) -> Option<UseResult> {
|
||||
self.step["hit"][1]
|
||||
.as_bool()
|
||||
.unwrap()
|
||||
.then(|| self.scripted("entity", hand))
|
||||
}
|
||||
fn use_on_block(&mut self, hand: Hand) -> UseResult {
|
||||
self.scripted("block", hand)
|
||||
}
|
||||
fn use_item(&mut self, hand: Hand) -> UseResult {
|
||||
self.scripted("item", hand)
|
||||
}
|
||||
fn item_used(&mut self, hand: Hand) {
|
||||
self.events.push(format!("used {}", hand_name(hand)));
|
||||
self.heights.item_used(hand);
|
||||
}
|
||||
fn release_using(&mut self) {
|
||||
self.events.push("release".into());
|
||||
}
|
||||
}
|
||||
fn held(stack: &Stack) -> Option<HeldStack> {
|
||||
(!stack.empty()).then(|| HeldStack {
|
||||
item: stack.item.clone(),
|
||||
rest: stack.count.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn run(case: &Value, out: &mut impl Write) -> Result<()> {
|
||||
let empty = Value::Null;
|
||||
let mut probe = Probe {
|
||||
case,
|
||||
step: &empty,
|
||||
creative: case["creative"].as_bool().unwrap(),
|
||||
stacks: vec![Stack {
|
||||
item: "minecraft:air".into(),
|
||||
count: 0,
|
||||
}],
|
||||
hands: [0, 0],
|
||||
blocks: case["blocks"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|b| {
|
||||
(
|
||||
[0, 1, 2].map(|i| b[i].as_i64().unwrap() as i32),
|
||||
b[3].as_str().unwrap().to_string(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
swing: SwingState::default(),
|
||||
events: Vec::new(),
|
||||
heights: HandHeights::default(),
|
||||
};
|
||||
probe.hands[0] = probe.new_stack(&case["main"]);
|
||||
probe.hands[1] = probe.new_stack(&case["off"]);
|
||||
writeln!(out, "attack_speed {:x}", probe.attack_speed().to_bits())?;
|
||||
let mut clicks = Clicks::default();
|
||||
let mut strength = AttackStrength::default();
|
||||
// FirstPersonHandsAndItems' visible items start as ItemStack.EMPTY.
|
||||
let mut visible = [0usize, 0usize];
|
||||
for (s, step) in case["steps"].as_array().unwrap().iter().enumerate() {
|
||||
probe.step = step;
|
||||
probe.events.clear();
|
||||
if !step["set"].is_null() {
|
||||
let i = probe.new_stack(&step["set"]["stack"]);
|
||||
let hand = if step["set"]["hand"] == "main" { 0 } else { 1 };
|
||||
probe.hands[hand] = i;
|
||||
}
|
||||
clicks.tick(
|
||||
&mut ResetWorld {
|
||||
probe: &mut probe,
|
||||
strength: &mut strength,
|
||||
},
|
||||
step["ac"].as_u64().unwrap() as u32,
|
||||
step["uc"].as_u64().unwrap() as u32,
|
||||
step["ad"].as_bool().unwrap(),
|
||||
step["ud"].as_bool().unwrap(),
|
||||
);
|
||||
probe.swing.tick();
|
||||
let main = probe.stack(Hand::Main).clone();
|
||||
strength.tick(held(&main));
|
||||
// FirstPersonHandsAndItems.tick: shouldInstantlyReplaceVisibleItem
|
||||
// (ItemStack.matchesIgnoringComponents; every item plays the swap
|
||||
// animation), then the heights.
|
||||
let mut changed = [false; 2];
|
||||
for h in 0..2 {
|
||||
let (a, b) = (visible[h], probe.hands[h]);
|
||||
let same = a == b || probe.stacks[a].effective() == probe.stacks[b].effective();
|
||||
if same {
|
||||
visible[h] = b;
|
||||
}
|
||||
changed[h] = visible[h] != b;
|
||||
}
|
||||
let speed = probe.attack_speed();
|
||||
let replace = probe.heights.tick(
|
||||
probe.step["busy"].as_bool().unwrap(),
|
||||
strength.swap_scale(1.0, speed),
|
||||
changed,
|
||||
);
|
||||
for h in 0..2 {
|
||||
if replace[h] {
|
||||
visible[h] = probe.hands[h];
|
||||
}
|
||||
}
|
||||
for e in &probe.events {
|
||||
writeln!(out, "{e}")?;
|
||||
}
|
||||
let sw = &probe.swing;
|
||||
let current = sw.current.map_or("- 0".to_string(), |c| {
|
||||
format!("{} {}", hand_name(c.hand), c.duration_ticks)
|
||||
});
|
||||
let h = &probe.heights;
|
||||
let d = &clicks.destroying;
|
||||
writeln!(
|
||||
out,
|
||||
"tick {s} {current} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {}",
|
||||
sw.ticks,
|
||||
fb(sw.animation),
|
||||
fb(sw.old_animation),
|
||||
fb(sw.animation(0.5)),
|
||||
strength.ticker,
|
||||
strength.swap_ticker,
|
||||
fb(strength.swap_scale(1.0, speed)),
|
||||
fb(h.main),
|
||||
fb(h.old_main),
|
||||
fb(h.off),
|
||||
fb(h.old_off),
|
||||
clicks.miss_time,
|
||||
clicks.right_click_delay,
|
||||
d.delay,
|
||||
d.active,
|
||||
fb(d.progress)
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The probe with the attack-strength tickers it resets.
|
||||
struct ResetWorld<'p, 'c> {
|
||||
probe: &'p mut Probe<'c>,
|
||||
strength: &'p mut AttackStrength,
|
||||
}
|
||||
macro_rules! forward {
|
||||
($($name:ident($($arg:ident: $t:ty),*) -> $r:ty;)*) => {
|
||||
$(fn $name(&mut self, $($arg: $t),*) -> $r { self.probe.$name($($arg),*) })*
|
||||
};
|
||||
}
|
||||
impl HandWorld for ResetWorld<'_, '_> {
|
||||
forward! {
|
||||
hit() -> Hit;
|
||||
is_air(pos: [i32; 3]) -> bool;
|
||||
item_enabled(hand: Hand) -> bool;
|
||||
held_empty(hand: Hand) -> bool;
|
||||
held_count(hand: Hand) -> i32;
|
||||
cannot_attack() -> bool;
|
||||
piercing_weapon() -> bool;
|
||||
attack_animation() -> SwingAnimation;
|
||||
interact_animation(hand: Hand) -> SwingAnimation;
|
||||
piercing_attack(a: SwingAnimation) -> ();
|
||||
attack_entity() -> bool;
|
||||
block_action_restricted(pos: [i32; 3]) -> bool;
|
||||
within_border(pos: [i32; 3]) -> bool;
|
||||
attack_block(pos: [i32; 3]) -> ();
|
||||
destroy_progress(pos: [i32; 3]) -> f32;
|
||||
destroy_block(pos: [i32; 3]) -> bool;
|
||||
main_hand_key() -> String;
|
||||
same_destroying_item(key: &str) -> bool;
|
||||
destroy_stage(pos: [i32; 3], stage: Option<i32>) -> ();
|
||||
swing(hand: Hand, a: SwingAnimation) -> ();
|
||||
entity_within_border() -> bool;
|
||||
use_on_entity(hand: Hand) -> Option<UseResult>;
|
||||
use_on_block(hand: Hand) -> UseResult;
|
||||
use_item(hand: Hand) -> UseResult;
|
||||
item_used(hand: Hand) -> ();
|
||||
release_using() -> ();
|
||||
}
|
||||
fn creative(&self) -> bool {
|
||||
self.probe.creative()
|
||||
}
|
||||
fn spectator(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn instabuild(&self) -> bool {
|
||||
self.probe.instabuild()
|
||||
}
|
||||
fn hands_busy(&self) -> bool {
|
||||
self.probe.hands_busy()
|
||||
}
|
||||
fn using_item(&self) -> bool {
|
||||
self.probe.using_item()
|
||||
}
|
||||
fn infinite_materials(&self) -> bool {
|
||||
self.probe.infinite_materials()
|
||||
}
|
||||
fn reset_attack_strength(&mut self) {
|
||||
self.strength.reset();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let input = BufReader::new(File::open(&args[1])?);
|
||||
let mut out = BufWriter::new(File::create(&args[2])?);
|
||||
for (n, line) in input.lines().enumerate() {
|
||||
let c: Value = serde_json::from_str(&line?)?;
|
||||
writeln!(out, "== {n}")?;
|
||||
run(&c, &mut out)?;
|
||||
}
|
||||
out.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -583,6 +583,44 @@ impl Camera {
|
||||
modify_fov_based_on_death_or_fluid(partial, 70.0, effect_scale, dead, fluid)
|
||||
}
|
||||
}
|
||||
/// `Projection.getMatrix` for a perspective set up by `setupPerspective`:
|
||||
/// JOML's `Matrix4f.setPerspective(fov * DEG_TO_RAD, width / height, near,
|
||||
/// far, zZeroToOne)`, column-major. Vanilla passes its planes reversed
|
||||
/// (`near` is `zFar`); the hand pass here keeps them in order.
|
||||
pub fn perspective(
|
||||
fov_degrees: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
near: f32,
|
||||
far: f32,
|
||||
zero_to_one: bool,
|
||||
) -> [f32; 16] {
|
||||
let h = horizon_core::jmath::tan(f64::from(fov_degrees * DEG_TO_RAD * 0.5)) as f32;
|
||||
let aspect = width / height;
|
||||
let mut m = [0.0f32; 16];
|
||||
m[0] = 1.0 / (h * aspect);
|
||||
m[5] = 1.0 / h;
|
||||
let e = 1.0e-6f32;
|
||||
if far > 0.0 && far.is_infinite() {
|
||||
m[10] = e - 1.0;
|
||||
m[14] = (e - if zero_to_one { 1.0 } else { 2.0 }) * near;
|
||||
} else if near > 0.0 && near.is_infinite() {
|
||||
m[10] = (if zero_to_one { 0.0 } else { 1.0 }) - e;
|
||||
m[14] = ((if zero_to_one { 1.0 } else { 2.0 }) - e) * far;
|
||||
} else {
|
||||
m[10] = (if zero_to_one { far } else { far + near }) / (near - far);
|
||||
m[14] = (if zero_to_one { far } else { far + far }) * near / (near - far);
|
||||
}
|
||||
m[11] = -1.0;
|
||||
m
|
||||
}
|
||||
/// `GameRenderer.render3dHud`: the first-person hand and held items are
|
||||
/// drawn with their own projection at `hudFov` (70 degrees, changed only by
|
||||
/// death and fluids), never the FOV option or the sprint, speed and bow
|
||||
/// modifier.
|
||||
pub fn hand_projection(hud_fov: f32, width: f32, height: f32, far: f32) -> [f32; 16] {
|
||||
perspective(hud_fov, width, height, 0.05, far, true)
|
||||
}
|
||||
/// `Camera.modifyFovBasedOnDeathOrFluid`. `dead` is the camera entity's
|
||||
/// `deathTime` while it is a living entity that is dead or dying.
|
||||
pub fn modify_fov_based_on_death_or_fluid(
|
||||
|
||||
@@ -365,12 +365,7 @@ impl PlayerEffects {
|
||||
f *= 1.0 + (a + 1) as f32 * 0.2;
|
||||
}
|
||||
if let Some(a) = self.amplifier("mining_fatigue") {
|
||||
f *= match a {
|
||||
0 => 0.3,
|
||||
1 => 0.09,
|
||||
2 => 0.0027,
|
||||
_ => 8.1e-4,
|
||||
};
|
||||
f *= horizon_core::jmath::pow(0.3, f64::from(a + 1)) as f32;
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
@@ -0,0 +1,601 @@
|
||||
//! The local player's hand: the arm swing (`LivingEntity.SwingState`,
|
||||
//! `getModifiedSwingDuration`), the first-person equip heights
|
||||
//! (`FirstPersonHandsAndItems.tick` and `itemUsed`), and the click handling
|
||||
//! that starts swings: `Minecraft.startAttack`, `continueAttack`,
|
||||
//! `startUseItem` and `handleKeybinds`' attack and use part, with
|
||||
//! `MultiPlayerGameMode`'s block breaking (`startDestroyBlock`,
|
||||
//! `continueDestroyBlock`, `stopDestroyBlock`).
|
||||
//!
|
||||
//! What those read and do in the world is behind [`HandWorld`].
|
||||
|
||||
/// `InteractionHand`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Hand {
|
||||
Main,
|
||||
Off,
|
||||
}
|
||||
impl Hand {
|
||||
pub const BOTH: [Hand; 2] = [Hand::Main, Hand::Off];
|
||||
}
|
||||
|
||||
/// `SwingAnimation`: its type (`SwingAnimationType` ordinal) and duration.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct SwingAnimation {
|
||||
pub kind: u8,
|
||||
pub duration: i32,
|
||||
}
|
||||
impl SwingAnimation {
|
||||
/// `SwingAnimation.DEFAULT`: WHACK for 6 ticks.
|
||||
pub const DEFAULT: SwingAnimation = SwingAnimation {
|
||||
kind: 1,
|
||||
duration: 6,
|
||||
};
|
||||
}
|
||||
|
||||
/// `LivingEntity.SwingDescription`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct SwingDescription {
|
||||
pub hand: Hand,
|
||||
pub animation: SwingAnimation,
|
||||
pub duration_ticks: i32,
|
||||
}
|
||||
|
||||
/// `LivingEntity.SwingState`.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct SwingState {
|
||||
pub current: Option<SwingDescription>,
|
||||
pub ticks: i32,
|
||||
pub old_animation: f32,
|
||||
pub animation: f32,
|
||||
}
|
||||
impl SwingState {
|
||||
/// `startIfAble`: a swing restarts unless the current one is in its
|
||||
/// first half (past its first tick).
|
||||
pub fn start_if_able(&mut self, hand: Hand, animation: SwingAnimation, duration: i32) -> bool {
|
||||
if let Some(c) = self.current
|
||||
&& self.ticks <= c.duration_ticks / 2
|
||||
&& self.ticks > 0
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let swing = SwingDescription {
|
||||
hand,
|
||||
animation,
|
||||
duration_ticks: duration,
|
||||
};
|
||||
if self.current != Some(swing) {
|
||||
self.old_animation = 0.0;
|
||||
self.animation = 0.0;
|
||||
}
|
||||
self.current = Some(swing);
|
||||
self.ticks = 0;
|
||||
true
|
||||
}
|
||||
/// `tick` (from `LivingEntity.baseTick`).
|
||||
pub fn tick(&mut self) {
|
||||
self.old_animation = self.animation;
|
||||
if let Some(c) = self.current {
|
||||
if c.duration_ticks > 0 {
|
||||
self.animation = (self.ticks as f32 / c.duration_ticks as f32).min(1.0);
|
||||
}
|
||||
let ticks = self.ticks;
|
||||
self.ticks += 1;
|
||||
if ticks > c.duration_ticks {
|
||||
self.current = None;
|
||||
self.animation = 0.0;
|
||||
}
|
||||
} else {
|
||||
self.animation = 0.0;
|
||||
}
|
||||
}
|
||||
/// `getAnimation(partialTicks)`: a restarted swing keeps moving forward.
|
||||
pub fn animation(&self, partial: f32) -> f32 {
|
||||
if self.animation >= self.old_animation {
|
||||
crate::camera::lerp(partial, self.old_animation, self.animation)
|
||||
} else {
|
||||
crate::camera::lerp(partial, self.old_animation, self.animation + 1.0)
|
||||
}
|
||||
}
|
||||
pub fn swinging(&self) -> bool {
|
||||
self.current.is_some()
|
||||
}
|
||||
/// The hand of the current swing (`state.attackHand`), main by default.
|
||||
pub fn attack_hand(&self) -> Hand {
|
||||
self.current.map_or(Hand::Main, |c| c.hand)
|
||||
}
|
||||
}
|
||||
|
||||
/// `LivingEntity.getModifiedSwingDuration`: `dig_speed` is
|
||||
/// `MobEffectUtil.getDigSpeedAmplification` while haste or conduit power is
|
||||
/// active, `fatigue` the mining fatigue amplifier.
|
||||
pub fn swing_duration(
|
||||
animation: SwingAnimation,
|
||||
dig_speed: Option<i32>,
|
||||
fatigue: Option<i32>,
|
||||
) -> i32 {
|
||||
let duration = animation.duration;
|
||||
if let Some(amplification) = dig_speed {
|
||||
duration - (1 + amplification)
|
||||
} else if let Some(amplifier) = fatigue {
|
||||
duration + (1 + amplifier) * 2
|
||||
} else {
|
||||
duration
|
||||
}
|
||||
}
|
||||
|
||||
/// `FirstPersonHandsAndItems`' equip heights.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct HandHeights {
|
||||
pub main: f32,
|
||||
pub old_main: f32,
|
||||
pub off: f32,
|
||||
pub old_off: f32,
|
||||
}
|
||||
impl Default for HandHeights {
|
||||
fn default() -> Self {
|
||||
HandHeights {
|
||||
main: 0.0,
|
||||
old_main: 0.0,
|
||||
off: 0.0,
|
||||
old_off: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// What `FirstPersonHandsAndItems.tick` decides about a hand's visible item.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct HandChange {
|
||||
/// The visible item is not the hand's item after the instant replacement
|
||||
/// (`mainHandItem != nextMainHand`): the hand lowers.
|
||||
pub changed: bool,
|
||||
/// The visible item becomes the hand's item this tick.
|
||||
pub replace: bool,
|
||||
}
|
||||
impl HandHeights {
|
||||
/// `FirstPersonHandsAndItems.tick`'s heights. `changed` says per hand
|
||||
/// whether the visible item differs from the held one once
|
||||
/// `shouldInstantlyReplaceVisibleItem` has run; `swap_scale` is
|
||||
/// `getItemSwapScale(1.0F)`. Returns whether each hand's visible item
|
||||
/// is replaced at the end.
|
||||
pub fn tick(&mut self, hands_busy: bool, swap_scale: f32, changed: [bool; 2]) -> [bool; 2] {
|
||||
self.old_main = self.main;
|
||||
self.old_off = self.off;
|
||||
if hands_busy {
|
||||
self.main = (self.main - 0.4).clamp(0.0, 1.0);
|
||||
self.off = (self.off - 0.4).clamp(0.0, 1.0);
|
||||
} else {
|
||||
let attack = swap_scale;
|
||||
let main_target = if changed[0] {
|
||||
0.0
|
||||
} else {
|
||||
attack * attack * attack
|
||||
};
|
||||
let off_target = if changed[1] { 0.0 } else { 1.0 };
|
||||
self.main += (main_target - self.main).clamp(-0.4, 0.4);
|
||||
self.off += (off_target - self.off).clamp(-0.4, 0.4);
|
||||
}
|
||||
[self.main < 0.1, self.off < 0.1]
|
||||
}
|
||||
/// `itemUsed`: the hand drops to the bottom and rises again.
|
||||
pub fn item_used(&mut self, hand: Hand) {
|
||||
match hand {
|
||||
Hand::Main => self.main = 0.0,
|
||||
Hand::Off => self.off = 0.0,
|
||||
}
|
||||
}
|
||||
/// `Mth.lerp(partialTicks, oMainHandHeight, mainHandHeight)` and the
|
||||
/// off hand's.
|
||||
pub fn interpolated(&self, partial: f32) -> [f32; 2] {
|
||||
[
|
||||
crate::camera::lerp(partial, self.old_main, self.main),
|
||||
crate::camera::lerp(partial, self.old_off, self.off),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// `HitResult` at the crosshair (`Minecraft.hitResult`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Hit {
|
||||
Miss,
|
||||
Block { pos: [i32; 3], face: u8 },
|
||||
Entity,
|
||||
}
|
||||
|
||||
/// `InteractionResult` as `startUseItem` reads it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum UseResult {
|
||||
/// `Success` with its `SwingSource`: whether the client swings.
|
||||
Success {
|
||||
swing: bool,
|
||||
},
|
||||
Pass,
|
||||
Fail,
|
||||
}
|
||||
|
||||
/// What the click handling reads and does in the world.
|
||||
pub trait HandWorld {
|
||||
fn hit(&mut self) -> Hit;
|
||||
fn is_air(&mut self, pos: [i32; 3]) -> bool;
|
||||
/// `MultiPlayerGameMode.localPlayerMode`.
|
||||
fn creative(&self) -> bool;
|
||||
fn spectator(&self) -> bool;
|
||||
/// `Abilities.instabuild`.
|
||||
fn instabuild(&self) -> bool;
|
||||
fn hands_busy(&self) -> bool;
|
||||
fn using_item(&self) -> bool;
|
||||
/// `ItemStack.isItemEnabled` for the item in a hand.
|
||||
fn item_enabled(&mut self, hand: Hand) -> bool;
|
||||
fn held_empty(&mut self, hand: Hand) -> bool;
|
||||
fn held_count(&mut self, hand: Hand) -> i32;
|
||||
/// `Player.hasInfiniteMaterials`.
|
||||
fn infinite_materials(&self) -> bool;
|
||||
/// `Player.cannotAttackWithItem(heldItem, 0)`.
|
||||
fn cannot_attack(&mut self) -> bool;
|
||||
/// The main-hand item is a `PIERCING_WEAPON`.
|
||||
fn piercing_weapon(&mut self) -> bool;
|
||||
fn attack_animation(&mut self) -> SwingAnimation;
|
||||
fn interact_animation(&mut self, hand: Hand) -> SwingAnimation;
|
||||
/// `MultiPlayerGameMode.piercingAttack`.
|
||||
fn piercing_attack(&mut self, animation: SwingAnimation);
|
||||
/// `Player.attack` on the targeted entity when the item's attack range
|
||||
/// allows it; false when out of range.
|
||||
fn attack_entity(&mut self) -> bool;
|
||||
/// `Player.blockActionRestricted` for the game mode.
|
||||
fn block_action_restricted(&mut self, pos: [i32; 3]) -> bool;
|
||||
/// `WorldBorder.isWithinBounds`.
|
||||
fn within_border(&mut self, pos: [i32; 3]) -> bool;
|
||||
/// `BlockState.attack` on the first hit of a break.
|
||||
fn attack_block(&mut self, pos: [i32; 3]);
|
||||
/// `BlockState.getDestroyProgress` for the player.
|
||||
fn destroy_progress(&mut self, pos: [i32; 3]) -> f32;
|
||||
/// `MultiPlayerGameMode.destroyBlock`.
|
||||
fn destroy_block(&mut self, pos: [i32; 3]) -> bool;
|
||||
/// A key for the main-hand stack, kept as `destroyingItem`.
|
||||
fn main_hand_key(&mut self) -> String;
|
||||
/// `ItemStack.isSameItemSameComponents(selected, destroyingItem)` for
|
||||
/// the stack `main_hand_key` gave when the break started. Vanilla keeps
|
||||
/// a reference to that stack, so it compares what it holds now.
|
||||
fn same_destroying_item(&mut self, key: &str) -> bool {
|
||||
self.main_hand_key() == key
|
||||
}
|
||||
/// Breaking progress shown (`destroyBlockProgress`), or cleared with `None`.
|
||||
fn destroy_stage(&mut self, pos: [i32; 3], stage: Option<i32>);
|
||||
/// `player.swing(hand, animation, false)`.
|
||||
fn swing(&mut self, hand: Hand, animation: SwingAnimation);
|
||||
/// `Player.resetAttackStrengthTicker`.
|
||||
fn reset_attack_strength(&mut self);
|
||||
/// The targeted entity's block position is inside the world border.
|
||||
fn entity_within_border(&mut self) -> bool;
|
||||
/// The use attempts of `startUseItem`: on the targeted entity (`None`
|
||||
/// when it is out of interaction range), the targeted block, and the
|
||||
/// item alone.
|
||||
fn use_on_entity(&mut self, hand: Hand) -> Option<UseResult>;
|
||||
fn use_on_block(&mut self, hand: Hand) -> UseResult;
|
||||
fn use_item(&mut self, hand: Hand) -> UseResult;
|
||||
/// `LocalPlayer.itemUsed`.
|
||||
fn item_used(&mut self, hand: Hand);
|
||||
/// `MultiPlayerGameMode.releaseUsingItem`.
|
||||
fn release_using(&mut self);
|
||||
}
|
||||
|
||||
/// `MultiPlayerGameMode`'s block breaking.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Destroying {
|
||||
pub delay: i32,
|
||||
pub active: bool,
|
||||
pub pos: [i32; 3],
|
||||
pub face: u8,
|
||||
pub item: String,
|
||||
pub progress: f32,
|
||||
pub ticks: f32,
|
||||
}
|
||||
impl Default for Destroying {
|
||||
/// `destroyBlockPos` starts at (-1, -1, -1) and `destroyingItem` empty
|
||||
/// (an empty key).
|
||||
fn default() -> Self {
|
||||
Destroying {
|
||||
delay: 0,
|
||||
active: false,
|
||||
pos: [-1; 3],
|
||||
face: 0,
|
||||
item: String::new(),
|
||||
progress: 0.0,
|
||||
ticks: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Destroying {
|
||||
/// `getDestroyStage`.
|
||||
pub fn stage(&self) -> i32 {
|
||||
if self.progress > 0.0 {
|
||||
(self.progress * 10.0) as i32
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
fn same_target(&self, w: &mut impl HandWorld, pos: [i32; 3]) -> bool {
|
||||
pos == self.pos && w.same_destroying_item(&self.item)
|
||||
}
|
||||
/// `startDestroyBlock`.
|
||||
pub fn start(&mut self, w: &mut impl HandWorld, pos: [i32; 3], face: u8) -> bool {
|
||||
if w.block_action_restricted(pos) || !w.within_border(pos) {
|
||||
return false;
|
||||
}
|
||||
if w.instabuild() {
|
||||
w.destroy_block(pos);
|
||||
self.delay = 5;
|
||||
} else if !self.active || !self.same_target(w, pos) {
|
||||
let not_air = !w.is_air(pos);
|
||||
if not_air && self.progress == 0.0 {
|
||||
w.attack_block(pos);
|
||||
}
|
||||
if not_air && w.destroy_progress(pos) >= 1.0 {
|
||||
w.destroy_block(pos);
|
||||
} else {
|
||||
self.active = true;
|
||||
self.pos = pos;
|
||||
self.face = face;
|
||||
self.item = w.main_hand_key();
|
||||
self.progress = 0.0;
|
||||
self.ticks = 0.0;
|
||||
w.destroy_stage(pos, Some(self.stage()));
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
/// `stopDestroyBlock`.
|
||||
pub fn stop(&mut self, w: &mut impl HandWorld) {
|
||||
if self.active {
|
||||
self.active = false;
|
||||
self.progress = 0.0;
|
||||
w.destroy_stage(self.pos, None);
|
||||
w.reset_attack_strength();
|
||||
}
|
||||
}
|
||||
/// `continueDestroyBlock`: true while the hand keeps swinging.
|
||||
pub fn continue_destroy(&mut self, w: &mut impl HandWorld, pos: [i32; 3], face: u8) -> bool {
|
||||
if self.delay > 0 {
|
||||
self.delay -= 1;
|
||||
return true;
|
||||
}
|
||||
if w.instabuild() && w.within_border(pos) {
|
||||
self.delay = 5;
|
||||
w.destroy_block(pos);
|
||||
return true;
|
||||
}
|
||||
if !self.same_target(w, pos) {
|
||||
return self.start(w, pos, face);
|
||||
}
|
||||
if w.is_air(pos) {
|
||||
self.active = false;
|
||||
return false;
|
||||
}
|
||||
self.progress += w.destroy_progress(pos);
|
||||
self.ticks += 1.0;
|
||||
if self.progress >= 1.0 {
|
||||
self.active = false;
|
||||
w.destroy_block(pos);
|
||||
self.progress = 0.0;
|
||||
self.ticks = 0.0;
|
||||
self.delay = 5;
|
||||
} else if self.face != face {
|
||||
self.face = face;
|
||||
}
|
||||
w.destroy_stage(self.pos, Some(self.stage()));
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// `Minecraft`'s attack and use handling.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Clicks {
|
||||
pub miss_time: i32,
|
||||
pub right_click_delay: i32,
|
||||
pub destroying: Destroying,
|
||||
}
|
||||
impl Clicks {
|
||||
/// `Minecraft.startAttack`: whether the click ended the attack (broke
|
||||
/// the block outright).
|
||||
pub fn start_attack(&mut self, w: &mut impl HandWorld) -> bool {
|
||||
if self.miss_time > 0 || w.hands_busy() {
|
||||
return false;
|
||||
}
|
||||
if w.spectator() {
|
||||
return true;
|
||||
}
|
||||
if !w.item_enabled(Hand::Main) || w.cannot_attack() {
|
||||
return false;
|
||||
}
|
||||
let animation = w.attack_animation();
|
||||
if w.piercing_weapon() {
|
||||
w.piercing_attack(animation);
|
||||
return true;
|
||||
}
|
||||
let mut end = false;
|
||||
let mut miss = false;
|
||||
match w.hit() {
|
||||
// MultiPlayerGameMode.attack.
|
||||
Hit::Entity => {
|
||||
if w.attack_entity() {
|
||||
w.reset_attack_strength();
|
||||
if w.instabuild() {
|
||||
self.destroying.delay = 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
Hit::Block { pos, face } if !w.is_air(pos) => {
|
||||
self.destroying.start(w, pos, face);
|
||||
end = w.is_air(pos);
|
||||
}
|
||||
Hit::Block { .. } | Hit::Miss => miss = true,
|
||||
}
|
||||
if miss {
|
||||
if !w.creative() {
|
||||
self.miss_time = 10;
|
||||
}
|
||||
w.reset_attack_strength();
|
||||
}
|
||||
w.swing(Hand::Main, animation);
|
||||
end
|
||||
}
|
||||
/// `Minecraft.continueAttack`.
|
||||
pub fn continue_attack(&mut self, w: &mut impl HandWorld, down: bool) {
|
||||
if !down {
|
||||
self.miss_time = 0;
|
||||
}
|
||||
if self.miss_time > 0 || w.using_item() || w.piercing_weapon() {
|
||||
return;
|
||||
}
|
||||
let animation = w.attack_animation();
|
||||
match w.hit() {
|
||||
Hit::Block { pos, face } if down => {
|
||||
if !w.is_air(pos) && self.destroying.continue_destroy(w, pos, face) {
|
||||
w.swing(Hand::Main, animation);
|
||||
}
|
||||
}
|
||||
_ => self.destroying.stop(w),
|
||||
}
|
||||
}
|
||||
/// `Minecraft.startUseItem`.
|
||||
pub fn start_use_item(&mut self, w: &mut impl HandWorld) {
|
||||
if self.destroying.active {
|
||||
return;
|
||||
}
|
||||
self.right_click_delay = 4;
|
||||
if w.hands_busy() {
|
||||
return;
|
||||
}
|
||||
for hand in Hand::BOTH {
|
||||
if !w.item_enabled(hand) {
|
||||
return;
|
||||
}
|
||||
let animation = w.interact_animation(hand);
|
||||
match w.hit() {
|
||||
Hit::Entity => {
|
||||
if !w.entity_within_border() {
|
||||
return;
|
||||
}
|
||||
if let Some(UseResult::Success { swing }) = w.use_on_entity(hand) {
|
||||
if swing {
|
||||
w.swing(hand, animation);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
Hit::Block { .. } => {
|
||||
let count = w.held_count(hand);
|
||||
match w.use_on_block(hand) {
|
||||
UseResult::Success { swing } => {
|
||||
if swing {
|
||||
w.swing(hand, animation);
|
||||
if !w.held_empty(hand)
|
||||
&& (w.held_count(hand) != count || w.infinite_materials())
|
||||
{
|
||||
w.item_used(hand);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
UseResult::Fail => return,
|
||||
UseResult::Pass => {}
|
||||
}
|
||||
}
|
||||
Hit::Miss => {}
|
||||
}
|
||||
if !w.held_empty(hand)
|
||||
&& let UseResult::Success { swing } = w.use_item(hand)
|
||||
{
|
||||
if swing {
|
||||
w.swing(hand, animation);
|
||||
}
|
||||
w.item_used(hand);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The attack and use part of `Minecraft.tick`: `rightClickDelay`, the
|
||||
/// clicks `handleKeybinds` consumes, held use and attack, then
|
||||
/// `missTime`. `attack_down` is `keyAttack.isDown()` with no screen and
|
||||
/// the mouse grabbed.
|
||||
pub fn tick(
|
||||
&mut self,
|
||||
w: &mut impl HandWorld,
|
||||
attack_clicks: u32,
|
||||
use_clicks: u32,
|
||||
attack_down: bool,
|
||||
use_down: bool,
|
||||
) {
|
||||
if self.right_click_delay > 0 {
|
||||
self.right_click_delay -= 1;
|
||||
}
|
||||
let mut instant = false;
|
||||
if w.using_item() {
|
||||
if !use_down {
|
||||
w.release_using();
|
||||
}
|
||||
} else {
|
||||
for _ in 0..attack_clicks {
|
||||
instant |= self.start_attack(w);
|
||||
}
|
||||
for _ in 0..use_clicks {
|
||||
self.start_use_item(w);
|
||||
}
|
||||
}
|
||||
if use_down && self.right_click_delay == 0 && !w.using_item() {
|
||||
self.start_use_item(w);
|
||||
}
|
||||
self.continue_attack(w, !instant && attack_down);
|
||||
if self.miss_time > 0 {
|
||||
self.miss_time -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn a_swing_lasts_its_duration_and_restarts_after_half() {
|
||||
let mut s = SwingState::default();
|
||||
assert!(s.start_if_able(Hand::Main, SwingAnimation::DEFAULT, 6));
|
||||
let mut seen = Vec::new();
|
||||
for _ in 0..9 {
|
||||
s.tick();
|
||||
seen.push(s.animation);
|
||||
}
|
||||
assert_eq!(
|
||||
seen,
|
||||
[
|
||||
0.0,
|
||||
1.0 / 6.0,
|
||||
2.0 / 6.0,
|
||||
0.5,
|
||||
4.0 / 6.0,
|
||||
5.0 / 6.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0
|
||||
]
|
||||
);
|
||||
assert!(!s.swinging());
|
||||
s.start_if_able(Hand::Main, SwingAnimation::DEFAULT, 6);
|
||||
s.tick();
|
||||
s.tick();
|
||||
assert!(!s.start_if_able(Hand::Main, SwingAnimation::DEFAULT, 6));
|
||||
s.tick();
|
||||
s.tick();
|
||||
assert!(s.start_if_able(Hand::Main, SwingAnimation::DEFAULT, 6));
|
||||
s.tick();
|
||||
// Restarting the same swing wraps forward rather than snapping back.
|
||||
assert_eq!(
|
||||
s.animation(0.5),
|
||||
(s.old_animation + s.animation + 1.0) / 2.0
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn haste_shortens_and_fatigue_lengthens_swings() {
|
||||
let d = SwingAnimation::DEFAULT;
|
||||
assert_eq!(swing_duration(d, None, None), 6);
|
||||
assert_eq!(swing_duration(d, Some(1), Some(0)), 4);
|
||||
assert_eq!(swing_duration(d, None, Some(2)), 12);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ mod game_mode;
|
||||
mod ghasts;
|
||||
mod golems;
|
||||
mod growth;
|
||||
mod hand;
|
||||
mod item_draw;
|
||||
mod jukebox;
|
||||
mod lan;
|
||||
@@ -685,12 +686,18 @@ struct Game<'a> {
|
||||
orbs: Vec<ExperienceOrb>,
|
||||
xp_delay: u32,
|
||||
last_level_up: u64,
|
||||
/// Arm swing: ticks into the 6-tick swing (None when idle), previous/current progress.
|
||||
swing: Option<u32>,
|
||||
swing_anim: (f32, f32),
|
||||
/// Main-hand equip height (previous, current) and the item it shows.
|
||||
hand_height: (f32, f32),
|
||||
hand_item: Option<ItemStack>,
|
||||
/// The arm swing (`LivingEntity.SwingState`) and how many swings started.
|
||||
swing: horizon_client::first_person::SwingState,
|
||||
swings_started: u32,
|
||||
/// The first-person equip heights and the items the hands show (with
|
||||
/// the slot each came from).
|
||||
heights: horizon_client::first_person::HandHeights,
|
||||
visible_hands: [(usize, Option<ItemStack>); 2],
|
||||
/// `Minecraft`'s attack handling, attack clicks since the last tick and
|
||||
/// the attack strength tickers the hand reads.
|
||||
clicks: horizon_client::first_person::Clicks,
|
||||
attack_clicks: u32,
|
||||
hand_strength: horizon_client::attack_strength::AttackStrength,
|
||||
settings: Settings,
|
||||
/// Where the options screen returns: true = title, false = pause menu.
|
||||
options_from_title: bool,
|
||||
@@ -2789,7 +2796,7 @@ impl Game<'_> {
|
||||
// ticks; ServerLevel.tick then runs the scheduled and random block
|
||||
// ticks before any entity, so what they drop or spawn (dispensed
|
||||
// items, popped crops) moves on the tick it appears, as in vanilla.
|
||||
self.tick_mining()?;
|
||||
self.tick_local_hand()?;
|
||||
self.postprocess_chunks()?;
|
||||
if running {
|
||||
self.tick_blocks()?;
|
||||
@@ -2841,7 +2848,7 @@ impl Game<'_> {
|
||||
self.tick_particles();
|
||||
}
|
||||
if self.right && self.use_delay == 0 && self.screen == Screen::Playing {
|
||||
self.use_item()?;
|
||||
self.use_with_hand()?;
|
||||
self.use_delay = 4;
|
||||
}
|
||||
Ok(())
|
||||
@@ -6374,38 +6381,6 @@ impl Game<'_> {
|
||||
}
|
||||
self.damage_held(1);
|
||||
}
|
||||
/// LivingEntity.swing: restart the 6-tick arm swing.
|
||||
fn swing_arm(&mut self) {
|
||||
if self.swing.is_none_or(|t| t >= 3) {
|
||||
self.swing = Some(0);
|
||||
}
|
||||
}
|
||||
/// LivingEntity.updateSwingTime and ItemInHandRenderer.tick (equip height).
|
||||
fn tick_hand(&mut self) {
|
||||
self.swing_anim.0 = self.swing_anim.1;
|
||||
if let Some(t) = self.swing {
|
||||
let t = t + 1;
|
||||
if t >= 6 {
|
||||
self.swing = None;
|
||||
self.swing_anim.1 = 0.0;
|
||||
} else {
|
||||
self.swing = Some(t);
|
||||
self.swing_anim.1 = t as f32 / 6.0;
|
||||
}
|
||||
}
|
||||
self.hand_height.0 = self.hand_height.1;
|
||||
let held = self.inventory[self.selected].clone();
|
||||
let same = match (&held, &self.hand_item) {
|
||||
(Some(a), Some(b)) => a.item == b.item,
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
};
|
||||
let target = if same { 1.0 } else { 0.0 };
|
||||
self.hand_height.1 += (target - self.hand_height.1).clamp(-0.4, 0.4);
|
||||
if self.hand_height.1 < 0.1 {
|
||||
self.hand_item = held;
|
||||
}
|
||||
}
|
||||
/// FirstPersonHandsAndItemsRenderer: the right arm or held item in view
|
||||
/// space, converted to camera-relative world space.
|
||||
fn hand_geometry(&mut self, alpha: f64) -> Vec<Vertex> {
|
||||
@@ -6423,14 +6398,15 @@ impl Game<'_> {
|
||||
return out;
|
||||
}
|
||||
let a = alpha as f32;
|
||||
let attack = self.swing_anim.0 + (self.swing_anim.1 - self.swing_anim.0) * a;
|
||||
let attack = if self.swing.is_none() && self.swing_anim.1 == 0.0 {
|
||||
0.0
|
||||
// FirstPersonHandsAndItemsRenderer: the swing of the hand that
|
||||
// swings, and its inverse arm height (the swap scale is 1 for every
|
||||
// item but spears).
|
||||
let attack = if self.swing.attack_hand() == horizon_client::first_person::Hand::Main {
|
||||
self.swing.animation(a)
|
||||
} else {
|
||||
attack
|
||||
0.0
|
||||
};
|
||||
let height = self.hand_height.0 + (self.hand_height.1 - self.hand_height.0) * a;
|
||||
let inverse_height = 1.0 - height;
|
||||
let inverse_height = 1.0 - self.heights.interpolated(a)[0];
|
||||
let (yaw, pitch) = (self.yaw.to_radians(), self.pitch.to_radians());
|
||||
let forward = glam::Vec3::new(
|
||||
-yaw.sin() * pitch.cos(),
|
||||
@@ -6451,7 +6427,8 @@ impl Game<'_> {
|
||||
if self.held_map_geometry(to_world, attack, inverse_height, packed, &mut out) {
|
||||
return out;
|
||||
}
|
||||
match self.hand_item.clone() {
|
||||
let [main_item, _] = self.visible_hand_items();
|
||||
match main_item {
|
||||
None => {
|
||||
let Some(arm) = self.res.player_arm.clone() else {
|
||||
return out;
|
||||
@@ -7319,6 +7296,7 @@ impl Game<'_> {
|
||||
additive: &additive,
|
||||
celestial: &celestial,
|
||||
hand: &hand,
|
||||
hand_fov_degrees: self.hud_fov_degrees(alpha as f32, camera.to_array()),
|
||||
screen: &screen,
|
||||
border,
|
||||
game_time: self.world.game_time as f32,
|
||||
@@ -7491,22 +7469,20 @@ impl Game<'_> {
|
||||
}
|
||||
match button {
|
||||
MouseButton::Left => {
|
||||
if pressed && self.attack() {
|
||||
return Ok(());
|
||||
// KeyMapping.click: Minecraft.tick starts the attack.
|
||||
if pressed {
|
||||
self.attack_clicks += 1;
|
||||
}
|
||||
self.toggles
|
||||
.button(2, pressed, false, self.settings.toggle_attack);
|
||||
self.left = self.toggles.is_down(2);
|
||||
if pressed {
|
||||
self.destroy_delay = 0;
|
||||
}
|
||||
}
|
||||
MouseButton::Right => {
|
||||
self.toggles
|
||||
.button(3, pressed, false, self.settings.toggle_use);
|
||||
self.right = self.toggles.is_down(3);
|
||||
if pressed {
|
||||
self.use_item()?;
|
||||
self.use_with_hand()?;
|
||||
self.use_delay = 4;
|
||||
}
|
||||
}
|
||||
@@ -8357,10 +8333,13 @@ pub fn run() -> Result<()> {
|
||||
orbs: Vec::new(),
|
||||
xp_delay: 0,
|
||||
last_level_up: 0,
|
||||
swing: None,
|
||||
swing_anim: (0.0, 0.0),
|
||||
hand_height: (1.0, 1.0),
|
||||
hand_item: None,
|
||||
swing: Default::default(),
|
||||
swings_started: 0,
|
||||
heights: Default::default(),
|
||||
visible_hands: [(0, None), (0, None)],
|
||||
clicks: Default::default(),
|
||||
attack_clicks: 0,
|
||||
hand_strength: Default::default(),
|
||||
settings,
|
||||
options_from_title: false,
|
||||
dragging: false,
|
||||
|
||||
@@ -183,7 +183,7 @@ impl Game<'_> {
|
||||
true
|
||||
}
|
||||
/// The armor stand under the crosshair, its distance and the hit point.
|
||||
fn armor_stand_hit(&self) -> Option<(usize, f64, [f64; 3])> {
|
||||
pub(super) fn armor_stand_hit(&self) -> Option<(usize, f64, [f64; 3])> {
|
||||
let eye = self.eye_position()?;
|
||||
let dir = view_vector(self.yaw, self.pitch);
|
||||
let reach: f64 = if self.creative { 5.0 } else { 3.0 };
|
||||
|
||||
@@ -597,7 +597,7 @@ impl Game<'_> {
|
||||
self.swing_arm();
|
||||
true
|
||||
}
|
||||
fn boat_hit(&self) -> Option<(usize, f64)> {
|
||||
pub(super) fn boat_hit(&self) -> Option<(usize, f64)> {
|
||||
let eye = self.eye_position()?;
|
||||
let dir = view_vector(self.yaw, self.pitch);
|
||||
let reach: f64 = if self.creative { 5.0 } else { 3.0 };
|
||||
|
||||
@@ -722,10 +722,9 @@ impl Game<'_> {
|
||||
.play("minecraft:entity.ender_dragon.hurt", Some(at), 5.0, pitch);
|
||||
}
|
||||
/// Player melee on the dragon's parts or a crystal under the crosshair.
|
||||
pub(super) fn attack_dragon(&mut self) -> bool {
|
||||
let Some(eye) = self.eye_position() else {
|
||||
return false;
|
||||
};
|
||||
/// The dragon part under the crosshair within reach: its id and distance.
|
||||
pub(super) fn dragon_hit(&self) -> Option<(u32, f64)> {
|
||||
let eye = self.eye_position()?;
|
||||
let dir = super::view_vector(self.yaw, self.pitch);
|
||||
let reach: f64 = if self.creative { 5.0 } else { 3.0 };
|
||||
let mut best: Option<(u32, f64)> = None;
|
||||
@@ -750,7 +749,12 @@ impl Game<'_> {
|
||||
best = Some((id, t0));
|
||||
}
|
||||
}
|
||||
let Some((id, d)) = best else { return false };
|
||||
best
|
||||
}
|
||||
pub(super) fn attack_dragon(&mut self) -> bool {
|
||||
let Some((id, d)) = self.dragon_hit() else {
|
||||
return false;
|
||||
};
|
||||
if !self.before_block(d) || self.mob_hit().is_some_and(|m| m.1 < d) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,15 @@ impl Game<'_> {
|
||||
false,
|
||||
)
|
||||
}
|
||||
/// `Camera.calculateHudFov`: the first-person hand's field of view.
|
||||
pub(super) fn hud_fov_degrees(&self, partial: f32, camera: [f64; 3]) -> f32 {
|
||||
self.view.calculate_hud_fov(
|
||||
partial,
|
||||
self.fov_effect_scale(),
|
||||
self.death_time(),
|
||||
self.camera_fluid(camera),
|
||||
)
|
||||
}
|
||||
/// The pose `GameRenderer.renderLevel` multiplies into the projection:
|
||||
/// `bobHurt`, then `bobView` with the View Bobbing option.
|
||||
pub(super) fn bob_matrix(&self, partial: f32) -> glam::Mat4 {
|
||||
|
||||
@@ -0,0 +1,372 @@
|
||||
//! The local player's hand: clicks and held attack through
|
||||
//! `horizon_client::first_person` (`Minecraft.startAttack` /
|
||||
//! `continueAttack`, `MultiPlayerGameMode` block breaking), the arm swing
|
||||
//! (`LivingEntity.SwingState`) and the first-person equip heights
|
||||
//! (`FirstPersonHandsAndItems`) with the item swap ticker they read.
|
||||
use super::{Game, Mining, Screen};
|
||||
use horizon_client::{
|
||||
Result,
|
||||
attack_strength::HeldStack,
|
||||
first_person::{Hand, HandWorld, Hit, SwingAnimation, UseResult, swing_duration},
|
||||
items::ItemStack,
|
||||
};
|
||||
|
||||
/// The off hand's place in `visible_hands` (the main hand uses its hotbar slot).
|
||||
const OFFHAND: usize = usize::MAX;
|
||||
|
||||
impl Game<'_> {
|
||||
/// `LivingEntity.swing` with `getModifiedSwingDuration`.
|
||||
pub(super) fn swing_hand(&mut self, hand: Hand, animation: SwingAnimation) {
|
||||
let effects = &self.potions.effects;
|
||||
let dig_speed = effects
|
||||
.amplifier("haste")
|
||||
.max(effects.amplifier("conduit_power"));
|
||||
let fatigue = effects.amplifier("mining_fatigue");
|
||||
let duration = swing_duration(animation, dig_speed, fatigue);
|
||||
if self.swing.start_if_able(hand, animation, duration) {
|
||||
self.swings_started += 1;
|
||||
}
|
||||
}
|
||||
/// `swing(InteractionHand.MAIN_HAND)` with the default animation.
|
||||
pub(super) fn swing_arm(&mut self) {
|
||||
self.swing_hand(Hand::Main, SwingAnimation::DEFAULT);
|
||||
}
|
||||
/// `Player.resetAttackStrengthTicker`.
|
||||
pub(super) fn reset_attack_strength(&mut self) {
|
||||
self.attack_ticks = 0;
|
||||
self.hand_strength.reset();
|
||||
}
|
||||
/// The attack and use part of `Minecraft.tick` for the local player:
|
||||
/// attack clicks since the last tick and the held attack button.
|
||||
pub(super) fn tick_local_hand(&mut self) -> Result<()> {
|
||||
let attack_clicks = std::mem::take(&mut self.attack_clicks);
|
||||
if self.screen == Screen::Playing && self.title.is_none() {
|
||||
let mut clicks = std::mem::take(&mut self.clicks);
|
||||
// keyAttack.isDown(); playing implies the grabbed mouse here
|
||||
// (scripts under --control hold buttons without grabbing it).
|
||||
let down = self.left;
|
||||
let ticks = self.clicks.destroying.ticks;
|
||||
let mut world = GameHands {
|
||||
game: self,
|
||||
error: None,
|
||||
};
|
||||
clicks.tick(&mut world, attack_clicks, 0, down, false);
|
||||
let error = world.error.take();
|
||||
self.clicks = clicks;
|
||||
if let Some(e) = error {
|
||||
return Err(e);
|
||||
}
|
||||
// A tick of breaking: the hit sound every fourth tick and the
|
||||
// cracking particles.
|
||||
let d = &self.clicks.destroying;
|
||||
if d.active && d.ticks > ticks {
|
||||
let (pos, first) = (d.pos, (d.ticks as u32 - 1).is_multiple_of(4));
|
||||
let state = self.world.block(pos[0], pos[1], pos[2]);
|
||||
if first {
|
||||
let sounds = self.res.facts.get(state).sounds.clone();
|
||||
self.audio.play(
|
||||
&sounds.hit,
|
||||
Some(pos.map(|v| f64::from(v) + 0.5)),
|
||||
(sounds.volume + 1.0) / 8.0,
|
||||
sounds.pitch * 0.5,
|
||||
);
|
||||
}
|
||||
if let Some(hit) = self.pick() {
|
||||
self.crack_particle(pos, hit.face, state);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Minecraft.tick: a screen keeps the attack from starting.
|
||||
self.clicks.miss_time = 10000;
|
||||
}
|
||||
let d = &self.clicks.destroying;
|
||||
self.mining = d.active.then_some(Mining {
|
||||
pos: d.pos,
|
||||
progress: d.progress,
|
||||
ticks: d.ticks as u32,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
/// `BlockBehaviour.getDestroyProgress` with `Player.getDestroySpeed`.
|
||||
pub(super) fn destroy_progress(&self, pos: [i32; 3]) -> f32 {
|
||||
let state = self.world.block(pos[0], pos[1], pos[2]);
|
||||
let facts = self.res.facts.get(state);
|
||||
if facts.hardness == -1.0 {
|
||||
return 0.0;
|
||||
}
|
||||
let held = self.held();
|
||||
let mut speed = self.res.items.mining_speed(held, &facts.name);
|
||||
speed = self.modify_destroy_speed(speed);
|
||||
if let Some(eye) = self.eye_position()
|
||||
&& self
|
||||
.res
|
||||
.facts
|
||||
.get(self.world.block(
|
||||
eye[0].floor() as i32,
|
||||
eye[1].floor() as i32,
|
||||
eye[2].floor() as i32,
|
||||
))
|
||||
.fluid
|
||||
== horizon_client::blocks::Fluid::Water
|
||||
{
|
||||
speed *= self.submerged_mining_speed();
|
||||
}
|
||||
if self.player.as_ref().is_some_and(|p| !p.ground.on_ground) {
|
||||
speed /= 5.0;
|
||||
}
|
||||
let harvest = !facts.requires_tool || self.res.items.correct_for_drops(held, &facts.name);
|
||||
speed / facts.hardness / if harvest { 30.0 } else { 100.0 }
|
||||
}
|
||||
/// Whether an attack would hit an entity rather than the block behind
|
||||
/// it: the crosshair's `EntityHitResult`.
|
||||
fn entity_targeted(&self) -> bool {
|
||||
let before = |d: Option<f64>| d.is_some_and(|d| self.before_block(d));
|
||||
before(self.painting_hit().map(|h| h.1))
|
||||
|| before(self.frame_hit().map(|h| h.1))
|
||||
|| before(self.armor_stand_hit().map(|h| h.1))
|
||||
|| before(self.boat_hit().map(|h| h.1))
|
||||
|| before(self.cart_hit().map(|h| h.1))
|
||||
|| before(self.dragon_hit().map(|h| h.1))
|
||||
|| before(self.mob_hit().map(|h| h.1))
|
||||
}
|
||||
/// `LocalPlayer.isHandsBusy`: paddling a boat.
|
||||
fn hands_busy(&self) -> bool {
|
||||
self.riding
|
||||
.is_some_and(|id| self.boats.iter().any(|b| b.id == id))
|
||||
&& [
|
||||
winit::keyboard::KeyCode::KeyW,
|
||||
winit::keyboard::KeyCode::KeyS,
|
||||
winit::keyboard::KeyCode::KeyA,
|
||||
winit::keyboard::KeyCode::KeyD,
|
||||
]
|
||||
.iter()
|
||||
.any(|k| self.keys.contains(k))
|
||||
}
|
||||
/// `MultiPlayerGameMode.startUseItem`'s bookkeeping around the game's
|
||||
/// use: nothing starts while a block is being broken or the hands are
|
||||
/// busy, and `LocalPlayer.itemUsed` drops the hand after a use that
|
||||
/// swung and spent (or, with infinite materials, kept) the item, or that
|
||||
/// started using it.
|
||||
pub(super) fn use_with_hand(&mut self) -> Result<()> {
|
||||
if self.clicks.destroying.active || self.hands_busy() {
|
||||
return Ok(());
|
||||
}
|
||||
let before = self.held().cloned();
|
||||
let swings = self.swings_started;
|
||||
let using = self.using.is_some();
|
||||
self.use_item()?;
|
||||
let swung = self.swings_started != swings;
|
||||
let after = self.held().cloned();
|
||||
let spent = match (&before, &after) {
|
||||
(Some(a), Some(b)) => a.count != b.count || a.item != b.item || self.creative,
|
||||
_ => false,
|
||||
};
|
||||
if (swung && spent) || (!using && self.using.is_some() && after.is_some()) {
|
||||
self.heights.item_used(Hand::Main);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// `LocalPlayer.tick`: the swing (`LivingEntity.baseTick`), the attack
|
||||
/// strength and item swap tickers (`Player.tick`), then
|
||||
/// `FirstPersonHandsAndItems.tick`.
|
||||
pub(super) fn tick_hand(&mut self) {
|
||||
self.swing.tick();
|
||||
let main = self.inventory[self.selected].clone();
|
||||
self.hand_strength.tick(main.as_ref().map(|s| HeldStack {
|
||||
item: s.item.to_string(),
|
||||
rest: format!("{} {} {:?}", s.count, s.damage, s.extra),
|
||||
}));
|
||||
let next = [(self.selected, main), (OFFHAND, self.offhand.clone())];
|
||||
// shouldInstantlyReplaceVisibleItem: the same stack, or an equal one
|
||||
// (matchesIgnoringComponents; every item plays the swap animation).
|
||||
// A stack that stays in its slot with the same item is the same
|
||||
// stack, whatever its count (vanilla shrinks and grows it in place).
|
||||
let mut changed = [false; 2];
|
||||
for h in 0..2 {
|
||||
let same = match (&self.visible_hands[h], &next[h]) {
|
||||
((_, None), (_, None)) => true,
|
||||
((slot, Some(a)), (next_slot, Some(b))) => {
|
||||
a.item == b.item
|
||||
&& (slot == next_slot || (a.count == b.count && a.extra == b.extra))
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if same {
|
||||
self.visible_hands[h] = next[h].clone();
|
||||
}
|
||||
changed[h] = !same;
|
||||
}
|
||||
let speed = self.main_hand_attack_speed();
|
||||
let replace = self.heights.tick(
|
||||
self.hands_busy(),
|
||||
self.hand_strength.swap_scale(1.0, speed),
|
||||
changed,
|
||||
);
|
||||
for h in 0..2 {
|
||||
if replace[h] {
|
||||
self.visible_hands[h] = next[h].clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `Attributes.ATTACK_SPEED` with the main-hand item's modifier.
|
||||
fn main_hand_attack_speed(&self) -> f64 {
|
||||
horizon_client::attack_strength::BASE_ATTACK_SPEED
|
||||
+ self
|
||||
.held()
|
||||
.and_then(|h| self.res.items.get(&h.item))
|
||||
.map_or(0.0, |i| f64::from(i.attack_speed))
|
||||
}
|
||||
/// The visible main- and off-hand items.
|
||||
pub(super) fn visible_hand_items(&self) -> [Option<ItemStack>; 2] {
|
||||
[
|
||||
self.visible_hands[0].1.clone(),
|
||||
self.visible_hands[1].1.clone(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// The game as `first_person`'s world: the crosshair, breaking and attacking.
|
||||
struct GameHands<'g, 'a> {
|
||||
game: &'g mut Game<'a>,
|
||||
error: Option<Box<dyn std::error::Error>>,
|
||||
}
|
||||
impl GameHands<'_, '_> {
|
||||
fn keep(&mut self, r: Result<()>) {
|
||||
if let Err(e) = r
|
||||
&& self.error.is_none()
|
||||
{
|
||||
self.error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl HandWorld for GameHands<'_, '_> {
|
||||
fn hit(&mut self) -> Hit {
|
||||
if self.game.entity_targeted() {
|
||||
return Hit::Entity;
|
||||
}
|
||||
match self.game.pick() {
|
||||
Some(h) => Hit::Block {
|
||||
pos: h.pos,
|
||||
face: match h.face {
|
||||
[0, -1, 0] => 0,
|
||||
[0, 1, 0] => 1,
|
||||
[0, 0, -1] => 2,
|
||||
[0, 0, 1] => 3,
|
||||
[-1, 0, 0] => 4,
|
||||
_ => 5,
|
||||
},
|
||||
},
|
||||
None => Hit::Miss,
|
||||
}
|
||||
}
|
||||
fn is_air(&mut self, pos: [i32; 3]) -> bool {
|
||||
let state = self.game.world.block(pos[0], pos[1], pos[2]);
|
||||
self.game.res.facts.get(state).name.ends_with("air")
|
||||
}
|
||||
fn creative(&self) -> bool {
|
||||
self.game.creative
|
||||
}
|
||||
fn spectator(&self) -> bool {
|
||||
self.game.spectator()
|
||||
}
|
||||
fn instabuild(&self) -> bool {
|
||||
self.game.creative
|
||||
}
|
||||
fn hands_busy(&self) -> bool {
|
||||
self.game.hands_busy()
|
||||
}
|
||||
fn using_item(&self) -> bool {
|
||||
self.game.using.is_some()
|
||||
}
|
||||
fn item_enabled(&mut self, _: Hand) -> bool {
|
||||
true
|
||||
}
|
||||
fn held_empty(&mut self, hand: Hand) -> bool {
|
||||
match hand {
|
||||
Hand::Main => self.game.held().is_none(),
|
||||
Hand::Off => self.game.offhand.is_none(),
|
||||
}
|
||||
}
|
||||
fn held_count(&mut self, hand: Hand) -> i32 {
|
||||
let stack = match hand {
|
||||
Hand::Main => self.game.held(),
|
||||
Hand::Off => self.game.offhand.as_ref(),
|
||||
};
|
||||
stack.map_or(0, |s| s.count as i32)
|
||||
}
|
||||
fn infinite_materials(&self) -> bool {
|
||||
self.game.creative
|
||||
}
|
||||
fn cannot_attack(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
fn piercing_weapon(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
fn attack_animation(&mut self) -> SwingAnimation {
|
||||
SwingAnimation::DEFAULT
|
||||
}
|
||||
fn interact_animation(&mut self, _: Hand) -> SwingAnimation {
|
||||
SwingAnimation::DEFAULT
|
||||
}
|
||||
fn piercing_attack(&mut self, _: SwingAnimation) {}
|
||||
fn attack_entity(&mut self) -> bool {
|
||||
self.game.attack()
|
||||
}
|
||||
fn block_action_restricted(&mut self, _: [i32; 3]) -> bool {
|
||||
!self.game.may_build()
|
||||
}
|
||||
fn within_border(&mut self, _: [i32; 3]) -> bool {
|
||||
true
|
||||
}
|
||||
fn attack_block(&mut self, _: [i32; 3]) {}
|
||||
fn destroy_progress(&mut self, pos: [i32; 3]) -> f32 {
|
||||
self.game.destroy_progress(pos)
|
||||
}
|
||||
fn destroy_block(&mut self, pos: [i32; 3]) -> bool {
|
||||
if self.is_air(pos) {
|
||||
return false;
|
||||
}
|
||||
// Item.canDestroyBlock: swords, tridents and maces cannot break
|
||||
// blocks in creative.
|
||||
let held = self.game.held().map(|s| s.item.to_string());
|
||||
if self.game.creative
|
||||
&& held.is_some_and(|i| {
|
||||
i.ends_with("_sword") || i == "minecraft:trident" || i == "minecraft:mace"
|
||||
})
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let r = self.game.break_block(pos);
|
||||
self.keep(r);
|
||||
true
|
||||
}
|
||||
fn main_hand_key(&mut self) -> String {
|
||||
self.game
|
||||
.held()
|
||||
.map_or(String::new(), |s| format!("{} {:?}", s.item, s.extra))
|
||||
}
|
||||
fn destroy_stage(&mut self, _: [i32; 3], _: Option<i32>) {}
|
||||
fn swing(&mut self, hand: Hand, animation: SwingAnimation) {
|
||||
self.game.swing_hand(hand, animation);
|
||||
}
|
||||
fn reset_attack_strength(&mut self) {
|
||||
self.game.reset_attack_strength();
|
||||
}
|
||||
fn entity_within_border(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
fn use_on_entity(&mut self, _: Hand) -> Option<UseResult> {
|
||||
None
|
||||
}
|
||||
fn use_on_block(&mut self, _: Hand) -> UseResult {
|
||||
UseResult::Pass
|
||||
}
|
||||
fn use_item(&mut self, _: Hand) -> UseResult {
|
||||
UseResult::Pass
|
||||
}
|
||||
fn item_used(&mut self, hand: Hand) {
|
||||
self.game.heights.item_used(hand);
|
||||
}
|
||||
fn release_using(&mut self) {}
|
||||
}
|
||||
@@ -29,8 +29,7 @@ pub(super) struct GuestPlay {
|
||||
using: Option<(usize, u32)>,
|
||||
use_delay: u32,
|
||||
attack_ticks: u32,
|
||||
swing: Option<u32>,
|
||||
swing_anim: (f32, f32),
|
||||
swing: horizon_client::first_person::SwingState,
|
||||
/// The block being dug and the face the client aimed at.
|
||||
digging: Option<([i32; 3], [i32; 3])>,
|
||||
last_progress: i32,
|
||||
@@ -131,8 +130,7 @@ impl Game<'_> {
|
||||
let using = std::mem::replace(&mut self.using, play.using.take());
|
||||
let use_delay = std::mem::replace(&mut self.use_delay, play.use_delay);
|
||||
let attack_ticks = std::mem::replace(&mut self.attack_ticks, play.attack_ticks);
|
||||
let swing = std::mem::replace(&mut self.swing, play.swing.take());
|
||||
let swing_anim = std::mem::replace(&mut self.swing_anim, play.swing_anim);
|
||||
let swing = std::mem::replace(&mut self.swing, play.swing);
|
||||
let (left, right) = (self.left, self.right);
|
||||
self.left = play.digging.is_some();
|
||||
self.right = play.using.is_some() || self.using.is_some();
|
||||
@@ -153,7 +151,6 @@ impl Game<'_> {
|
||||
play.use_delay = std::mem::replace(&mut self.use_delay, use_delay);
|
||||
play.attack_ticks = std::mem::replace(&mut self.attack_ticks, attack_ticks);
|
||||
play.swing = std::mem::replace(&mut self.swing, swing);
|
||||
play.swing_anim = std::mem::replace(&mut self.swing_anim, swing_anim);
|
||||
self.container = container;
|
||||
self.screen = screen;
|
||||
if sneak {
|
||||
@@ -689,7 +686,7 @@ impl Game<'_> {
|
||||
.mining
|
||||
.as_ref()
|
||||
.map_or(-1, |m| (m.progress.clamp(0.0, 1.0) * 10.0) as i32 - 1);
|
||||
let swinging = play.swing.is_some();
|
||||
let swinging = play.swing.swinging();
|
||||
if progress != play.last_progress && progress >= 0 {
|
||||
play.last_progress = progress;
|
||||
host.hub.broadcast_near(
|
||||
@@ -779,7 +776,7 @@ impl Game<'_> {
|
||||
};
|
||||
let (id, at) = (g.entity_id, g.position);
|
||||
let play = host.play.get_mut(&conn).expect("play");
|
||||
let swinging = play.swing == Some(0);
|
||||
let swinging = play.swing.swinging() && play.swing.ticks == 0;
|
||||
if swinging && !play.swung {
|
||||
host.hub.swing(id, 0, at, Some(conn));
|
||||
}
|
||||
|
||||
@@ -890,7 +890,7 @@ impl Game<'_> {
|
||||
self.swing_arm();
|
||||
true
|
||||
}
|
||||
fn cart_hit(&self) -> Option<(usize, f64)> {
|
||||
pub(super) fn cart_hit(&self) -> Option<(usize, f64)> {
|
||||
let eye = self.eye_position()?;
|
||||
let dir = view_vector(self.yaw, self.pitch);
|
||||
let reach: f64 = if self.creative { 5.0 } else { 3.0 };
|
||||
|
||||
@@ -297,8 +297,8 @@ impl Game<'_> {
|
||||
}
|
||||
let crouching = p.pose == Pose::Crouching;
|
||||
let (position, flying) = (p.position, p.flying || p.fall_flying);
|
||||
let attack = self.swing_anim;
|
||||
let swinging = self.swing.is_some();
|
||||
let attack = (self.swing.old_animation, self.swing.animation);
|
||||
let swinging = self.swing.swinging();
|
||||
let (yaw, pitch) = (self.yaw, self.pitch);
|
||||
let main = self.inventory[self.selected]
|
||||
.as_ref()
|
||||
|
||||
@@ -78,6 +78,9 @@ pub struct Frame<'a> {
|
||||
pub celestial: &'a [Vertex],
|
||||
/// First-person hand and held item, drawn after clearing depth.
|
||||
pub hand: &'a [Vertex],
|
||||
/// `CameraRenderState.hudFov`: the hand's own field of view
|
||||
/// (`GameRenderer.render3dHud`).
|
||||
pub hand_fov_degrees: f32,
|
||||
/// Full-screen camera effects drawn over the world and hand, under the HUD.
|
||||
pub screen: &'a [crate::screen_effects::ScreenQuad],
|
||||
/// The world border wall, when near enough to be drawn.
|
||||
@@ -139,6 +142,9 @@ pub struct Renderer {
|
||||
screen_buffer: wgpu::Buffer,
|
||||
screen_capacity: u64,
|
||||
entity_group: wgpu::BindGroup,
|
||||
/// The entity resources with the hand's projection (`hand_globals`).
|
||||
hand_group: wgpu::BindGroup,
|
||||
hand_globals: wgpu::Buffer,
|
||||
entity_buffer: wgpu::Buffer,
|
||||
entity_capacity: u64,
|
||||
group: wgpu::BindGroup,
|
||||
@@ -470,6 +476,12 @@ impl Renderer {
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let hand_globals = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("First-person hand globals"),
|
||||
size: std::mem::size_of::<Globals>() as u64,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let (mips, mip_levels) = mip_chain(sprite_pixels, sprite_size, layers);
|
||||
let sprite_texture = device.create_texture_with_data(
|
||||
&queue,
|
||||
@@ -665,6 +677,32 @@ impl Renderer {
|
||||
},
|
||||
],
|
||||
});
|
||||
let hand_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("First-person hand resources"),
|
||||
layout: &entity_bindings,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: hand_globals.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(&atlas_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: wgpu::BindingResource::Sampler(&atlas_sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: wgpu::BindingResource::TextureView(&lightmap_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 4,
|
||||
resource: wgpu::BindingResource::Sampler(&lightmap_sampler),
|
||||
},
|
||||
],
|
||||
});
|
||||
let entity_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Entity layout"),
|
||||
bind_group_layouts: &[Some(&entity_bindings)],
|
||||
@@ -1065,6 +1103,8 @@ impl Renderer {
|
||||
screen_buffer,
|
||||
screen_capacity,
|
||||
entity_group,
|
||||
hand_group,
|
||||
hand_globals,
|
||||
entity_buffer,
|
||||
entity_capacity,
|
||||
group,
|
||||
@@ -1492,6 +1532,20 @@ impl Renderer {
|
||||
};
|
||||
self.queue
|
||||
.write_buffer(&self.globals, 0, bytemuck::bytes_of(&globals));
|
||||
// GameRenderer.render3dHud: the hand's projection at hudFov, with the
|
||||
// bob in its pose rather than the projection (same product).
|
||||
let hand_projection = Mat4::from_cols_array(&crate::camera::hand_projection(
|
||||
frame.hand_fov_degrees,
|
||||
self.config.width as f32,
|
||||
self.config.height.max(1) as f32,
|
||||
(frame.distance * 4.0).max(1024.0),
|
||||
));
|
||||
let hand_globals = Globals {
|
||||
view_proj: (hand_projection * frame.bob * view).to_cols_array_2d(),
|
||||
..globals
|
||||
};
|
||||
self.queue
|
||||
.write_buffer(&self.hand_globals, 0, bytemuck::bytes_of(&hand_globals));
|
||||
// Frustum culling and camera-relative origins.
|
||||
let planes = frustum(view_proj);
|
||||
let camera = frame.camera;
|
||||
@@ -1803,7 +1857,7 @@ impl Renderer {
|
||||
..Default::default()
|
||||
});
|
||||
pass.set_pipeline(&self.entity_pipeline);
|
||||
pass.set_bind_group(0, &self.entity_group, &[]);
|
||||
pass.set_bind_group(0, &self.hand_group, &[]);
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
|
||||
pass.draw_indexed(
|
||||
|
||||
@@ -41,6 +41,7 @@ pub mod entities;
|
||||
pub mod entity_nbt;
|
||||
pub mod environment;
|
||||
pub mod fastutil_order;
|
||||
pub mod first_person;
|
||||
pub mod flatgen;
|
||||
pub mod fog;
|
||||
pub mod font;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
@@ -299,6 +299,11 @@ public final class VanillaCameraOracle {
|
||||
+ " " + fb(up.x()) + " " + fb(up.y()) + " " + fb(up.z())
|
||||
+ " " + fb(left.x()) + " " + fb(left.y()) + " " + fb(left.z())
|
||||
+ " " + camera.isDetached() + " " + fb(fv) + " " + fb(hv));
|
||||
// Camera.setupPerspective and GameRenderer.render3dHud's hudProjection:
|
||||
// Projection.getMatrix's setPerspective (planes reversed, zero-to-one depth).
|
||||
float w = f(op.get(4)), h = f(op.get(5)), far = f(op.get(6));
|
||||
out.println("projection" + matrix(new Matrix4f().setPerspective(fv * (float) (Math.PI / 180.0), w / h, far, 0.05F, true))
|
||||
+ matrix(new Matrix4f().setPerspective(hv * (float) (Math.PI / 180.0), w / h, far, 0.05F, true)));
|
||||
}
|
||||
default -> throw new IllegalArgumentException(op.toString());
|
||||
}
|
||||
@@ -388,6 +393,16 @@ public final class VanillaCameraOracle {
|
||||
out.println(sb);
|
||||
}
|
||||
|
||||
static String matrix(Matrix4f m) {
|
||||
var sb = new StringBuilder();
|
||||
for (int col = 0; col < 4; col++) {
|
||||
for (int row = 0; row < 4; row++) {
|
||||
sb.append(' ').append(fb(m.get(col, row)));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static void avatar(JsonObject c, PrintWriter out) throws Exception {
|
||||
StubPlayer p = make(StubPlayer.class);
|
||||
var state = new ClientAvatarState();
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
package dev.horizonlab.parity;
|
||||
|
||||
import com.google.gson.*;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.multiplayer.ClientPacketListener;
|
||||
import net.minecraft.client.multiplayer.MultiPlayerGameMode;
|
||||
import net.minecraft.client.multiplayer.prediction.BlockStatePredictionHandler;
|
||||
import net.minecraft.client.player.FirstPersonHandsAndItems;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.client.renderer.item.ItemModelResolver;
|
||||
import net.minecraft.client.tutorial.Tutorial;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.effect.*;
|
||||
import net.minecraft.world.entity.*;
|
||||
import net.minecraft.world.entity.ai.attributes.*;
|
||||
import net.minecraft.world.entity.player.*;
|
||||
import net.minecraft.world.flag.FeatureFlagSet;
|
||||
import net.minecraft.world.flag.FeatureFlags;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.component.SwingAnimation;
|
||||
import net.minecraft.world.level.GameType;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.border.WorldBorder;
|
||||
import net.minecraft.world.level.gameevent.GameEvent;
|
||||
import net.minecraft.world.level.material.*;
|
||||
import net.minecraft.world.phys.*;
|
||||
|
||||
/**
|
||||
* Drives the real Minecraft.startAttack, continueAttack and startUseItem,
|
||||
* MultiPlayerGameMode's startDestroyBlock, continueDestroyBlock,
|
||||
* stopDestroyBlock, destroyBlock and attack, LivingEntity.swing with its
|
||||
* SwingState and getModifiedSwingDuration, Player's attack strength and item
|
||||
* swap tickers and getItemSwapScale, and FirstPersonHandsAndItems.tick and
|
||||
* itemUsed, tick by tick over scripted clicks and crosshair targets. The
|
||||
* player is a LocalPlayer shell with a real Inventory and attributes; the
|
||||
* level, connection and item model resolver are shells answering what these
|
||||
* read, and the results of using items on blocks, entities and the air are
|
||||
* scripted. The attack and use part of Minecraft.handleKeybinds and the
|
||||
* ticker lines of Player.tick are mirrored here in vanilla's order.
|
||||
*/
|
||||
public final class VanillaFirstPersonOracle {
|
||||
static Object unsafe;
|
||||
static Method allocate;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> T make(Class<T> type) throws Exception {
|
||||
return (T) allocate.invoke(unsafe, type);
|
||||
}
|
||||
|
||||
static Field field(Class<?> type, String name) throws Exception {
|
||||
for (Class<?> c = type; c != null; c = c.getSuperclass()) {
|
||||
try {
|
||||
Field f = c.getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f;
|
||||
} catch (NoSuchFieldException e) {
|
||||
// try the superclass
|
||||
}
|
||||
}
|
||||
throw new NoSuchFieldException(type + "." + name);
|
||||
}
|
||||
|
||||
static Method method(Class<?> type, String name, Class<?>... args) throws Exception {
|
||||
Method m = type.getDeclaredMethod(name, args);
|
||||
m.setAccessible(true);
|
||||
return m;
|
||||
}
|
||||
|
||||
static String fb(float v) {
|
||||
return String.format("%08x", Float.floatToRawIntBits(v));
|
||||
}
|
||||
|
||||
static List<String> events = new ArrayList<>();
|
||||
static Map<BlockPos, BlockState> blocks = new HashMap<>();
|
||||
static JsonObject step;
|
||||
|
||||
static String hand(InteractionHand h) {
|
||||
return h == InteractionHand.MAIN_HAND ? "main" : "off";
|
||||
}
|
||||
|
||||
static InteractionResult result(String name) {
|
||||
return switch (name) {
|
||||
case "success" -> InteractionResult.SUCCESS;
|
||||
case "server" -> InteractionResult.SUCCESS_SERVER;
|
||||
case "consume" -> InteractionResult.CONSUME;
|
||||
case "fail" -> InteractionResult.FAIL;
|
||||
default -> InteractionResult.PASS;
|
||||
};
|
||||
}
|
||||
|
||||
static final class Level0 extends ClientLevel {
|
||||
Level0() {
|
||||
super(null, null, null, null, 0, 0, null, false, 0L, 0);
|
||||
}
|
||||
|
||||
@Override public BlockState getBlockState(BlockPos pos) { return blocks.getOrDefault(pos, Blocks.AIR.defaultBlockState()); }
|
||||
@Override public FluidState getFluidState(BlockPos pos) { return Fluids.EMPTY.defaultFluidState(); }
|
||||
@Override public boolean setBlock(BlockPos pos, BlockState state, int flags, int limit) {
|
||||
blocks.put(pos.immutable(), state);
|
||||
return true;
|
||||
}
|
||||
@Override public void levelEvent(Entity source, int type, BlockPos pos, int data) { }
|
||||
@Override public void gameEvent(Holder<GameEvent> event, Vec3 pos, GameEvent.Context context) { }
|
||||
@Override public void destroyBlockProgress(int id, BlockPos pos, int progress) {
|
||||
events.add("stage " + pos.getX() + " " + pos.getY() + " " + pos.getZ() + " " + progress);
|
||||
}
|
||||
@Override public void addBreakingBlockEffects(BlockPos pos, Direction direction, boolean sound) { }
|
||||
@Override public FeatureFlagSet enabledFeatures() { return FeatureFlags.REGISTRY.allFlags(); }
|
||||
}
|
||||
|
||||
static final class Connection0 extends ClientPacketListener {
|
||||
Connection0() {
|
||||
super(null, null, null);
|
||||
}
|
||||
|
||||
@Override public void send(Packet<?> packet) { }
|
||||
}
|
||||
|
||||
static final class Resolver extends ItemModelResolver {
|
||||
Resolver() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
// No vanilla item model turns hand_animation_on_swap off, and only
|
||||
// spears (not scripted here) scale the swap animation.
|
||||
@Override public boolean shouldPlaySwapAnimation(ItemStack stack) { return true; }
|
||||
@Override public float swapAnimationScale(ItemStack stack) { return 1.0F; }
|
||||
}
|
||||
|
||||
static final class Player0 extends LocalPlayer {
|
||||
AttributeMap attributes;
|
||||
boolean inRange;
|
||||
|
||||
Player0() {
|
||||
super(null, null, null, null, null, Input.EMPTY, false, null, null);
|
||||
}
|
||||
|
||||
@Override public AttributeMap getAttributes() { return attributes; }
|
||||
@Override public boolean isEyeInFluid(TagKey<Fluid> type) { return false; }
|
||||
@Override public boolean onGround() { return true; }
|
||||
@Override public boolean isWithinEntityInteractionRange(Entity entity, double buffer) { return inRange; }
|
||||
@Override public void attack(Entity entity) { events.add("attack"); }
|
||||
@Override public boolean swing(InteractionHand hand, SwingAnimation animation, boolean send) {
|
||||
boolean started = super.swing(hand, animation, send);
|
||||
events.add("swing " + hand(hand) + " " + animation.duration() + " " + started);
|
||||
return started;
|
||||
}
|
||||
@Override public void itemUsed(InteractionHand hand) {
|
||||
events.add("used " + hand(hand));
|
||||
super.itemUsed(hand);
|
||||
}
|
||||
}
|
||||
|
||||
static final class GameMode0 extends MultiPlayerGameMode {
|
||||
GameMode0(Minecraft minecraft, ClientPacketListener connection) {
|
||||
super(minecraft, connection);
|
||||
}
|
||||
|
||||
@Override public boolean destroyBlock(BlockPos pos) {
|
||||
boolean changed = super.destroyBlock(pos);
|
||||
events.add("destroy " + pos.getX() + " " + pos.getY() + " " + pos.getZ() + " " + changed);
|
||||
return changed;
|
||||
}
|
||||
@Override public void releaseUsingItem(Player player) { events.add("release"); }
|
||||
|
||||
InteractionResult scripted(String kind, Player player, InteractionHand hand) {
|
||||
String name = step.getAsJsonObject("uses").get(kind).getAsString();
|
||||
events.add("use " + kind + " " + hand(hand) + " " + name);
|
||||
InteractionResult r = result(name);
|
||||
if (r instanceof InteractionResult.Success && step.get("shrink").getAsBoolean()) {
|
||||
player.getItemInHand(hand).shrink(1);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
@Override public InteractionResult useItemOn(LocalPlayer player, InteractionHand hand, BlockHitResult hit) { return scripted("block", player, hand); }
|
||||
@Override public InteractionResult useItem(Player player, InteractionHand hand) { return scripted("item", player, hand); }
|
||||
@Override public InteractionResult interact(Player player, Entity entity, EntityHitResult hit, InteractionHand hand) {
|
||||
return scripted("entity", player, hand);
|
||||
}
|
||||
}
|
||||
|
||||
static ItemStack stack(JsonElement e) {
|
||||
if (e.isJsonNull()) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
var a = e.getAsJsonArray();
|
||||
return new ItemStack(BuiltInRegistries.ITEM.getValue(Identifier.parse(a.get(0).getAsString())), a.get(1).getAsInt());
|
||||
}
|
||||
|
||||
/** LivingEntity.collectEquipmentChanges for the main hand: the item's attribute modifiers. */
|
||||
static AttributeMap attributes(ItemStack main) {
|
||||
var map = new AttributeMap(Player.createAttributes().build());
|
||||
main.forEachModifier(EquipmentSlot.MAINHAND, (attribute, modifier) -> {
|
||||
var instance = map.getInstance(attribute);
|
||||
if (instance != null) {
|
||||
instance.addTransientModifier(modifier);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
// The vanilla data pack: item components (tools, attribute modifiers) and block tags.
|
||||
ParityData.init();
|
||||
var u = Class.forName("sun.misc.Unsafe");
|
||||
unsafe = field(u, "theUnsafe").get(null);
|
||||
allocate = u.getMethod("allocateInstance", Class.class);
|
||||
Method startAttack = method(Minecraft.class, "startAttack");
|
||||
Method continueAttack = method(Minecraft.class, "continueAttack", boolean.class);
|
||||
Method startUseItem = method(Minecraft.class, "startUseItem");
|
||||
Field missTime = field(Minecraft.class, "missTime");
|
||||
Field rightClickDelay = field(Minecraft.class, "rightClickDelay");
|
||||
Field swingState = field(LivingEntity.class, "swingState");
|
||||
Field attackTicker = field(LivingEntity.class, "attackStrengthTicker");
|
||||
Field swapTicker = field(LivingEntity.class, "itemSwapTicker");
|
||||
Field lastItem = field(Player.class, "lastItemInMainHand");
|
||||
Field hands = field(LocalPlayer.class, "firstPersonHandsAndItems");
|
||||
Field handsBusy = field(LocalPlayer.class, "handsBusy");
|
||||
Field using = field(LocalPlayer.class, "startedUsingItem");
|
||||
Field destroyDelay = field(MultiPlayerGameMode.class, "destroyDelay");
|
||||
Field destroying = field(MultiPlayerGameMode.class, "isDestroying");
|
||||
Field progress = field(MultiPlayerGameMode.class, "destroyProgress");
|
||||
Field mainHeight = field(FirstPersonHandsAndItems.class, "mainHandHeight");
|
||||
Field oMainHeight = field(FirstPersonHandsAndItems.class, "oMainHandHeight");
|
||||
Field offHeight = field(FirstPersonHandsAndItems.class, "offHandHeight");
|
||||
Field oOffHeight = field(FirstPersonHandsAndItems.class, "oOffHandHeight");
|
||||
Field ticks = field(LivingEntity.SwingState.class, "ticks");
|
||||
Field animation = field(LivingEntity.SwingState.class, "animation");
|
||||
Field oldAnimation = field(LivingEntity.SwingState.class, "oldAnimation");
|
||||
try (var out = new PrintWriter(Files.newBufferedWriter(Path.of(args[1]), StandardCharsets.UTF_8));
|
||||
var lines = Files.lines(Path.of(args[0]))) {
|
||||
int n = 0;
|
||||
for (var line : (Iterable<String>) lines::iterator) {
|
||||
var c = JsonParser.parseString(line).getAsJsonObject();
|
||||
out.println("== " + n++);
|
||||
blocks.clear();
|
||||
for (var e : c.getAsJsonArray("blocks")) {
|
||||
var b = e.getAsJsonArray();
|
||||
blocks.put(new BlockPos(b.get(0).getAsInt(), b.get(1).getAsInt(), b.get(2).getAsInt()),
|
||||
BuiltInRegistries.BLOCK.getValue(Identifier.parse(b.get(3).getAsString())).defaultBlockState());
|
||||
}
|
||||
Minecraft mc = make(Minecraft.class);
|
||||
Level0 level = make(Level0.class);
|
||||
field(ClientLevel.class, "worldBorder").set(level, new WorldBorder());
|
||||
field(ClientLevel.class, "blockStatePredictionHandler").set(level, new BlockStatePredictionHandler());
|
||||
Connection0 connection = make(Connection0.class);
|
||||
Player0 player = make(Player0.class);
|
||||
field(LocalPlayer.class, "minecraft").set(player, mc);
|
||||
field(LocalPlayer.class, "connection").set(player, connection);
|
||||
field(Entity.class, "level").set(player, level);
|
||||
field(Entity.class, "id").setInt(player, 1);
|
||||
var equipment = new PlayerEquipment(player);
|
||||
field(LivingEntity.class, "equipment").set(player, equipment);
|
||||
field(Player.class, "inventory").set(player, new Inventory(player, equipment));
|
||||
var abilities = new Abilities();
|
||||
boolean creative = c.get("creative").getAsBoolean();
|
||||
abilities.instabuild = creative;
|
||||
field(Player.class, "abilities").set(player, abilities);
|
||||
swingState.set(player, new LivingEntity.SwingState());
|
||||
lastItem.set(player, ItemStack.EMPTY);
|
||||
hands.set(player, new FirstPersonHandsAndItems());
|
||||
var effects = new HashMap<Holder<MobEffect>, MobEffectInstance>();
|
||||
if (!c.get("haste").isJsonNull()) {
|
||||
effects.put(MobEffects.HASTE, new MobEffectInstance(MobEffects.HASTE, 100000, c.get("haste").getAsInt()));
|
||||
}
|
||||
if (!c.get("fatigue").isJsonNull()) {
|
||||
effects.put(MobEffects.MINING_FATIGUE, new MobEffectInstance(MobEffects.MINING_FATIGUE, 100000, c.get("fatigue").getAsInt()));
|
||||
}
|
||||
field(LivingEntity.class, "activeEffects").set(player, effects);
|
||||
player.getInventory().setItem(0, stack(c.get("main")));
|
||||
equipment.set(EquipmentSlot.OFFHAND, stack(c.get("off")));
|
||||
player.attributes = attributes(player.getMainHandItem());
|
||||
field(Minecraft.class, "player").set(mc, player);
|
||||
field(Minecraft.class, "level").set(mc, level);
|
||||
field(Minecraft.class, "tutorial").set(mc, make(Tutorial.class));
|
||||
field(Minecraft.class, "itemModelResolver").set(mc, make(Resolver.class));
|
||||
var gameMode = new GameMode0(mc, connection);
|
||||
field(MultiPlayerGameMode.class, "localPlayerMode").set(gameMode, creative ? GameType.CREATIVE : GameType.SURVIVAL);
|
||||
field(Minecraft.class, "gameMode").set(mc, gameMode);
|
||||
out.println("attack_speed " + Long.toHexString(Double.doubleToRawLongBits(player.getAttributeValue(Attributes.ATTACK_SPEED))));
|
||||
var entity = make(net.minecraft.world.entity.item.ItemEntity.class);
|
||||
field(Entity.class, "blockPosition").set(entity, BlockPos.ZERO);
|
||||
field(Entity.class, "id").setInt(entity, 2);
|
||||
int s = 0;
|
||||
for (var e : c.getAsJsonArray("steps")) {
|
||||
step = e.getAsJsonObject();
|
||||
events.clear();
|
||||
if (!step.get("set").isJsonNull()) {
|
||||
var set = step.getAsJsonObject("set");
|
||||
if (set.get("hand").getAsString().equals("main")) {
|
||||
player.getInventory().setItem(0, stack(set.get("stack")));
|
||||
} else {
|
||||
equipment.set(EquipmentSlot.OFFHAND, stack(set.get("stack")));
|
||||
}
|
||||
}
|
||||
var hit = step.getAsJsonArray("hit");
|
||||
HitResult hitResult = switch (hit.get(0).getAsString()) {
|
||||
case "block" -> new BlockHitResult(Vec3.ZERO, Direction.from3DDataValue(hit.get(4).getAsInt()),
|
||||
new BlockPos(hit.get(1).getAsInt(), hit.get(2).getAsInt(), hit.get(3).getAsInt()), false);
|
||||
case "entity" -> {
|
||||
player.inRange = hit.get(1).getAsBoolean();
|
||||
yield new EntityHitResult(entity, Vec3.ZERO);
|
||||
}
|
||||
default -> BlockHitResult.miss(Vec3.ZERO, Direction.UP, BlockPos.ZERO);
|
||||
};
|
||||
field(Minecraft.class, "hitResult").set(mc, hitResult);
|
||||
handsBusy.setBoolean(player, step.get("busy").getAsBoolean());
|
||||
using.setBoolean(player, step.get("using").getAsBoolean());
|
||||
boolean attackDown = step.get("ad").getAsBoolean();
|
||||
boolean useDown = step.get("ud").getAsBoolean();
|
||||
// Minecraft.tick: rightClickDelay, then handleKeybinds' attack and use part.
|
||||
if (rightClickDelay.getInt(mc) > 0) {
|
||||
rightClickDelay.setInt(mc, rightClickDelay.getInt(mc) - 1);
|
||||
}
|
||||
boolean instant = false;
|
||||
if (player.isUsingItem()) {
|
||||
if (!useDown) {
|
||||
gameMode.releaseUsingItem(player);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < step.get("ac").getAsInt(); i++) {
|
||||
instant |= (boolean) startAttack.invoke(mc);
|
||||
}
|
||||
for (int i = 0; i < step.get("uc").getAsInt(); i++) {
|
||||
startUseItem.invoke(mc);
|
||||
}
|
||||
}
|
||||
if (useDown && rightClickDelay.getInt(mc) == 0 && !player.isUsingItem()) {
|
||||
startUseItem.invoke(mc);
|
||||
}
|
||||
continueAttack.invoke(mc, !instant && attackDown);
|
||||
if (missTime.getInt(mc) > 0) {
|
||||
missTime.setInt(mc, missTime.getInt(mc) - 1);
|
||||
}
|
||||
// LocalPlayer.tick: LivingEntity.baseTick's swing, the equipment's
|
||||
// attribute modifiers, Player.tick's tickers, then
|
||||
// FirstPersonHandsAndItems.tick.
|
||||
var swing = (LivingEntity.SwingState) swingState.get(player);
|
||||
swing.tick();
|
||||
// LivingEntity.tick's detectEquipmentUpdates: the main hand's modifiers.
|
||||
player.attributes = attributes(player.getMainHandItem());
|
||||
attackTicker.setInt(player, attackTicker.getInt(player) + 1);
|
||||
swapTicker.setInt(player, swapTicker.getInt(player) + 1);
|
||||
ItemStack main = player.getMainHandItem();
|
||||
if (!ItemStack.matches((ItemStack) lastItem.get(player), main)) {
|
||||
if (!ItemStack.isSameItem((ItemStack) lastItem.get(player), main)) {
|
||||
player.resetAttackStrengthTicker();
|
||||
}
|
||||
lastItem.set(player, main.copy());
|
||||
}
|
||||
var fp = (FirstPersonHandsAndItems) hands.get(player);
|
||||
fp.tick(player);
|
||||
for (var ev : events) {
|
||||
out.println(ev);
|
||||
}
|
||||
var current = player.getCurrentSwing();
|
||||
out.println("tick " + s++ + " " + (current == null ? "- 0" : hand(current.hand()) + " " + current.durationTicks())
|
||||
+ " " + ticks.getInt(swing) + " " + fb(animation.getFloat(swing)) + " " + fb(oldAnimation.getFloat(swing))
|
||||
+ " " + fb(player.getSwingAnimation(0.5F))
|
||||
+ " " + attackTicker.getInt(player) + " " + swapTicker.getInt(player) + " " + fb(player.getItemSwapScale(1.0F))
|
||||
+ " " + fb(mainHeight.getFloat(fp)) + " " + fb(oMainHeight.getFloat(fp))
|
||||
+ " " + fb(offHeight.getFloat(fp)) + " " + fb(oOffHeight.getFloat(fp))
|
||||
+ " " + missTime.getInt(mc) + " " + rightClickDelay.getInt(mc) + " " + destroyDelay.getInt(gameMode)
|
||||
+ " " + destroying.getBoolean(gameMode) + " " + fb(progress.getFloat(gameMode)));
|
||||
}
|
||||
}
|
||||
}
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,10 @@ field-of-view easing), alignWithEntity (view rotation quaternion and basis,
|
||||
interpolated eye, third person back and front, camera distance and scale
|
||||
attributes with mounts, getMaxZoom clipping against visual shapes, sleeping),
|
||||
calculateFov/calculateHudFov (option, modifier, death and fluid effects,
|
||||
panoramic mode), AbstractClientPlayer.getFieldOfViewModifier (flight, speed
|
||||
attribute against walking speed, bow draw, spyglass), the walk and bob
|
||||
panoramic mode) with the world projection and the first-person hand's
|
||||
(GameRenderer.render3dHud at hudFov, whatever the FOV option),
|
||||
AbstractClientPlayer.getFieldOfViewModifier (flight, speed attribute against
|
||||
walking speed, bow draw, spyglass), the walk and bob
|
||||
history (ClientAvatarState, updateBob) and GameRenderer.bobHurt/bobView,
|
||||
driven tick by tick and frame by frame with identical scripted states."""
|
||||
import json
|
||||
@@ -91,6 +93,12 @@ def entity_state(rng, center):
|
||||
return s
|
||||
|
||||
|
||||
def window(rng):
|
||||
"""Window size and Camera.depthFar for the world and hand projections."""
|
||||
w, h = rng.choice([(1600, 1000), (854, 480), (1920, 1080), (800, 1200), (1, 1), (rng.randint(1, 4000), rng.randint(1, 3000))])
|
||||
return [fbits(w), fbits(h), fbits(rng.choice([768.0, 128.0, 2048.0, 4096.0, rng.uniform(32, 4096)]))]
|
||||
|
||||
|
||||
def camera_cases(rng):
|
||||
for n in range(2500):
|
||||
center = rng.choice([(0, 64, 0), (-10, -60, 7), (1000, 120, -3000), (0, 0, 0)])
|
||||
@@ -105,12 +113,27 @@ def camera_cases(rng):
|
||||
ops.append(['tick'])
|
||||
elif kind == 'frame':
|
||||
pt = rng.choice([0.0, 1.0, 0.5, 0.25, 0.999, rng.random()])
|
||||
ops.append(['frame', fbits(pt), rng.choice([0, 0, 0, 1, 2, 3]), rng.random() < 0.05])
|
||||
ops.append(['frame', fbits(pt), rng.choice([0, 0, 0, 1, 2, 3]), rng.random() < 0.05] + window(rng))
|
||||
else:
|
||||
ops.append(['type', rng.randrange(3)])
|
||||
yield dict(t='camera', fov=rng.choice([70, 30, 110, 90, rng.randint(30, 110)]),
|
||||
effect=dbits(rng.choice([1.0, 0.0, 0.5, rng.random()])), type=rng.choice([0, 1, 1, 2]),
|
||||
cells=world(rng, center), ops=ops)
|
||||
# The hand's projection at FOV 30, 70 and 110 while walking, sprinting, under
|
||||
# speed, drawing a bow, dying and in fluids: GameRenderer.render3dHud uses
|
||||
# hudFov, never the option or the modifier.
|
||||
for fov in (30, 70, 110):
|
||||
for speed in (f32(0.1), f32(0.1) * 1.3, 0.2, 0.05):
|
||||
for use in (None, ['bow', 10]):
|
||||
for dead in (None, 5):
|
||||
for fluid in (0, 1, 2):
|
||||
s = dict(kind='player', pos=[dbits(0.5), dbits(64.0), dbits(0.5)], prev=[dbits(0.5), dbits(64.0), dbits(0.5)],
|
||||
xr=fbits(0.0), yr=fbits(0.0), xro=fbits(0.0), yro=fbits(0.0), eye=fbits(1.62), flying=False,
|
||||
ws=fbits(0.1), speed=dbits(speed), use=use, scale_attr=dbits(1.0), dist=dbits(4.0),
|
||||
sleeping=None, dead=dead, mount=None)
|
||||
ops = [['entity', s]] + [['tick']] * 8 + [['frame', fbits(pt), fluid, False, fbits(1600.0), fbits(1000.0), fbits(768.0)]
|
||||
for pt in (0.0, 0.5, 1.0)]
|
||||
yield dict(t='camera', fov=fov, effect=dbits(1.0), type=0, cells={}, ops=ops)
|
||||
# A walled-in third-person camera stepping through every distance bound.
|
||||
for dist in [0.0, 0.5, 1.0, 2.0, 4.0, 8.0, 32.0]:
|
||||
for wall in range(0, 6):
|
||||
@@ -124,7 +147,7 @@ def camera_cases(rng):
|
||||
xr=fbits(15.0), yr=fbits(yaw), xro=fbits(15.0), yro=fbits(yaw), eye=fbits(1.62), flying=False,
|
||||
ws=fbits(0.1), speed=dbits(f32(0.1)), use=None, scale_attr=dbits(1.0), dist=dbits(dist),
|
||||
sleeping=None, dead=None, mount=None)
|
||||
ops = [['entity', s]] + [['tick']] * 6 + [['frame', fbits(pt), 0, False] for pt in (0.0, 0.5, 1.0)]
|
||||
ops = [['entity', s]] + [['tick']] * 6 + [['frame', fbits(pt), 0, False] + window(rng) for pt in (0.0, 0.5, 1.0)]
|
||||
yield dict(t='camera', fov=70, effect=dbits(1.0), type=1 + (wall % 2), cells=cells, ops=ops)
|
||||
|
||||
|
||||
@@ -184,7 +207,9 @@ def main():
|
||||
'interpolated eye with eye-height easing, first person, third person back and mirrored front, scale and '
|
||||
'camera_distance attributes including a living mount, getMaxZoom over the actual Level.clip with '
|
||||
'ClipContext.Block.VISUAL against controlled visual shapes, sleeping bed orientation), calculateFov and '
|
||||
'calculateHudFov (option, eased modifier, death, water/lava with the effect scale, panoramic), '
|
||||
'calculateHudFov (option, eased modifier, death, water/lava with the effect scale, panoramic), the world projection '
|
||||
'(Camera.setupPerspective) and the hand projection GameRenderer.render3dHud sets up at hudFov, as JOML setPerspective '
|
||||
'matrices, at FOV options 30, 70 and 110 among others, '
|
||||
'AbstractClientPlayer.getFieldOfViewModifier with real Abilities, attribute maps and item stacks, '
|
||||
'ClientAvatarState walk/bob history with AbstractClientPlayer.updateBob, and GameRenderer.bobHurt/bobView '
|
||||
'as the full JOML pose matrix with its property flags. The camera entity is a shell with controlled '
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare the local player's hand with the pinned client, tick by tick over
|
||||
scripted clicks: Minecraft.startAttack, continueAttack and startUseItem with
|
||||
handleKeybinds' attack and use part (missTime, rightClickDelay, held
|
||||
buttons), MultiPlayerGameMode's block breaking (survival progress, creative
|
||||
instant breaking and its destroy delay, swords that cannot break in
|
||||
creative, stopping), LivingEntity.swing with SwingState and haste or mining
|
||||
fatigue durations, Player's attack strength and item swap tickers, and
|
||||
FirstPersonHandsAndItems' equip heights (swap animation, itemUsed after
|
||||
placing or using an item, busy hands)."""
|
||||
import json
|
||||
import random
|
||||
import struct
|
||||
|
||||
try:
|
||||
from . import lab, oracle_harness as harness
|
||||
except ImportError:
|
||||
import lab, oracle_harness as harness
|
||||
|
||||
|
||||
def fbits(v):
|
||||
return struct.pack('>f', v).hex()
|
||||
|
||||
|
||||
def dbits(v):
|
||||
return struct.pack('>d', v).hex()
|
||||
|
||||
|
||||
def f32(v):
|
||||
return struct.unpack('>f', struct.pack('>f', v))[0]
|
||||
|
||||
|
||||
# What the probe needs about the scripted blocks and items, checked against
|
||||
# the client through the progress and attack speed both print:
|
||||
# Block.destroyTime and requiresCorrectToolForDrops,
|
||||
BLOCKS = {'minecraft:dirt': (0.5, False), 'minecraft:stone': (1.5, True), 'minecraft:glass': (0.3, False),
|
||||
'minecraft:bedrock': (-1.0, False), 'minecraft:short_grass': (0.0, False), 'minecraft:oak_planks': (2.0, False)}
|
||||
# the Tool component's speed and correct-for-drops per item and block (1.0
|
||||
# and incorrect otherwise),
|
||||
TOOLS = {'minecraft:diamond_pickaxe minecraft:stone': (8.0, True), 'minecraft:diamond_shovel minecraft:dirt': (8.0, True),
|
||||
'minecraft:diamond_pickaxe minecraft:glass': (1.0, False)}
|
||||
# and ATTACK_SPEED with the main-hand item's modifier (4.0 without one).
|
||||
SPEEDS = {'minecraft:diamond_sword': 4.0 + f32(-2.4), 'minecraft:diamond_pickaxe': 4.0 + f32(-2.8),
|
||||
'minecraft:diamond_shovel': 4.0 + f32(-3.0)}
|
||||
ITEMS = [None, ['minecraft:diamond_sword', 1], ['minecraft:diamond_pickaxe', 1], ['minecraft:diamond_shovel', 1],
|
||||
['minecraft:dirt', 64], ['minecraft:dirt', 2], ['minecraft:bread', 3], ['minecraft:bow', 1], ['minecraft:shield', 1]]
|
||||
TARGETS = [(0, 64, 0), (1, 64, 0), (0, 65, 2)]
|
||||
|
||||
|
||||
def step(hit=('miss',), ac=0, uc=0, ad=False, ud=False, busy=False, using=False, uses=None, shrink=False, set=None):
|
||||
return dict(hit=list(hit), ac=ac, uc=uc, ad=ad, ud=ud, busy=busy, using=using,
|
||||
uses=uses or dict(entity='pass', block='pass', item='pass'), shrink=shrink, set=set)
|
||||
|
||||
|
||||
def case(steps, creative=False, main=None, off=None, blocks=None, haste=None, fatigue=None):
|
||||
return dict(creative=creative, main=main, off=off, haste=haste, fatigue=fatigue,
|
||||
blocks=[[*p, b] for p, b in (blocks or {}).items()], steps=steps,
|
||||
rust=dict(blocks={k: [fbits(v[0]), v[1]] for k, v in BLOCKS.items()},
|
||||
tools={k: [fbits(v[0]), v[1]] for k, v in TOOLS.items()},
|
||||
attack_speed={k: dbits(v) for k, v in SPEEDS.items()}))
|
||||
|
||||
|
||||
def block_hit(pos, face=1):
|
||||
return ('block', *pos, face)
|
||||
|
||||
|
||||
def scenarios():
|
||||
stone = {TARGETS[0]: 'minecraft:stone', TARGETS[1]: 'minecraft:dirt', TARGETS[2]: 'minecraft:glass'}
|
||||
for creative in (False, True):
|
||||
for main in ITEMS:
|
||||
# Hold attack on blocks until they break, then on the air behind.
|
||||
for target, block in [(TARGETS[1], 'minecraft:dirt'), (TARGETS[0], 'minecraft:stone'), (TARGETS[0], 'minecraft:bedrock'),
|
||||
(TARGETS[2], 'minecraft:short_grass'), (TARGETS[2], 'minecraft:glass')]:
|
||||
steps = [step(block_hit(target), ac=1, ad=True)] + [step(block_hit(target), ad=True)] * 40 + [step()] * 8
|
||||
yield case(steps, creative, main, blocks={target: block})
|
||||
# Sweep across three blocks while holding, as when mining a tunnel.
|
||||
steps = [step(block_hit(TARGETS[0]), ac=1, ad=True)]
|
||||
for i in range(60):
|
||||
steps.append(step(block_hit(TARGETS[(i // 7) % 3]), ad=True))
|
||||
yield case(steps + [step()] * 6, creative, main, blocks=stone)
|
||||
# Clicking the air, quickly and slowly; and clicking an entity.
|
||||
for gap in (0, 1, 2, 3, 5, 9, 10, 11, 15):
|
||||
steps = []
|
||||
for _ in range(5):
|
||||
steps += [step(ac=1)] + [step()] * gap
|
||||
yield case(steps + [step()] * 8, creative, main)
|
||||
steps = []
|
||||
for _ in range(5):
|
||||
steps += [step(('entity', True), ac=1)] + [step(('entity', True))] * gap
|
||||
yield case(steps + [step(('entity', True), ac=1, ad=True)] + [step(('entity', True), ad=True)] * 8, creative, main)
|
||||
# Placing and using: held right button on a block and in the air.
|
||||
for result in ('success', 'server', 'consume', 'pass', 'fail'):
|
||||
for shrink in (False, True):
|
||||
uses = dict(entity=result, block=result, item=result)
|
||||
steps = [step(block_hit(TARGETS[0]), uc=1, ud=True, uses=uses, shrink=shrink)]
|
||||
steps += [step(block_hit(TARGETS[0]), ud=True, uses=uses, shrink=shrink)] * 14 + [step(uses=uses)] * 8
|
||||
yield case(steps, creative, main, off=['minecraft:shield', 1], blocks=stone)
|
||||
steps = [step(uc=1, ud=True, uses=uses, shrink=shrink)] + [step(ud=True, uses=uses, shrink=shrink)] * 10
|
||||
yield case(steps + [step(uses=uses)] * 8, creative, main, blocks=stone)
|
||||
steps = [step(('entity', inr), uc=1, ud=True, uses=uses, shrink=shrink) for inr in (True, False, True)]
|
||||
yield case(steps + [step(('entity', True), uses=uses)] * 8, creative, main)
|
||||
# Eating or drawing a bow: using the item, then releasing.
|
||||
uses = dict(entity='pass', block='pass', item='consume')
|
||||
steps = [step(uc=1, ud=True, uses=uses)] + [step(ud=True, using=True, ac=k % 2, ad=True, uses=uses) for k in range(12)]
|
||||
steps += [step(using=True, uses=uses), step(uses=uses)] + [step()] * 8
|
||||
yield case(steps, creative, main, off=['minecraft:bread', 3])
|
||||
# Changing the held item: other items, the same item with another count,
|
||||
# emptying the hand, while the attack recharges or the hands are busy.
|
||||
for a in ITEMS:
|
||||
for b in ITEMS:
|
||||
for busy in (False, True):
|
||||
steps = [step()] * 25 + [step(set=dict(hand='main', stack=b), busy=busy)] + [step(busy=busy)] * 25
|
||||
steps += [step(set=dict(hand='off', stack=a))] + [step()] * 12 + [step(ac=1)] + [step()] * 30
|
||||
yield case(steps, False, a)
|
||||
# Haste and mining fatigue change the swing's length.
|
||||
for haste in (None, 0, 1, 2, 4):
|
||||
for fatigue in (None, 0, 1, 2):
|
||||
steps = [step(block_hit(TARGETS[1]), ac=1, ad=True)] + [step(block_hit(TARGETS[1]), ad=True)] * 60 + [step(ac=1)] + [step()] * 20
|
||||
yield case(steps, False, ['minecraft:diamond_shovel', 1], blocks={TARGETS[1]: 'minecraft:dirt'}, haste=haste, fatigue=fatigue)
|
||||
yield case(steps, True, None, blocks={TARGETS[1]: 'minecraft:dirt'}, haste=haste, fatigue=fatigue)
|
||||
|
||||
|
||||
def random_cases(rng):
|
||||
results = ['success', 'server', 'consume', 'pass', 'fail']
|
||||
for _ in range(3000):
|
||||
blocks = {p: rng.choice(list(BLOCKS) + ['minecraft:air']) for p in TARGETS}
|
||||
steps = []
|
||||
for _ in range(rng.randrange(5, 80)):
|
||||
hit = rng.choice([('miss',), ('entity', rng.random() < 0.7)] + [block_hit(p, rng.randrange(6)) for p in TARGETS] * 2)
|
||||
setting = None
|
||||
if rng.random() < 0.04:
|
||||
setting = dict(hand=rng.choice(['main', 'off']), stack=rng.choice(ITEMS))
|
||||
steps.append(step(hit, ac=rng.choice([0] * 6 + [1, 1, 2]), uc=rng.choice([0] * 8 + [1, 2]), ad=rng.random() < 0.5,
|
||||
ud=rng.random() < 0.3, busy=rng.random() < 0.05, using=rng.random() < 0.08,
|
||||
uses=dict(entity=rng.choice(results), block=rng.choice(results), item=rng.choice(results)),
|
||||
shrink=rng.random() < 0.3, set=setting))
|
||||
yield case(steps, rng.random() < 0.4, rng.choice(ITEMS), rng.choice(ITEMS), blocks,
|
||||
rng.choice([None] * 6 + [0, 1, 3]), rng.choice([None] * 6 + [0, 2]))
|
||||
|
||||
|
||||
def make_cases(build):
|
||||
rng = random.Random(0x48414e44)
|
||||
path = build / 'cases.jsonl'
|
||||
cases = build / 'cases'
|
||||
cases.mkdir(exist_ok=True)
|
||||
count = 0
|
||||
with path.open('w') as out:
|
||||
for generator in (scenarios(), random_cases(rng)):
|
||||
for c in generator:
|
||||
text = json.dumps(c, separators=(',', ':'))
|
||||
out.write(text + '\n')
|
||||
if count < 20000:
|
||||
(cases / str(count)).write_text(text)
|
||||
count += 1
|
||||
return path, count
|
||||
|
||||
|
||||
def main():
|
||||
scope = ('Real Minecraft.startAttack, continueAttack and startUseItem (with the attack and use part of handleKeybinds and '
|
||||
'missTime mirrored in order), MultiPlayerGameMode.startDestroyBlock, continueDestroyBlock, stopDestroyBlock, '
|
||||
'destroyBlock and attack, LivingEntity.swing with SwingState and getModifiedSwingDuration under haste and mining '
|
||||
'fatigue, Player.getItemSwapScale and resetAttackStrengthTicker with the ticker lines of Player.tick, and '
|
||||
'FirstPersonHandsAndItems.tick and itemUsed, on a LocalPlayer shell with a real Inventory, effects and attribute '
|
||||
'modifiers, over scripted clicks, held buttons, crosshair targets, item changes and use results. Level effects '
|
||||
'(particles, game events), packets and the outcome of using items on blocks, entities and the air are scripted '
|
||||
'or absorbed; every vanilla item plays the swap animation unscaled except spears, which are not scripted.')
|
||||
harness.differential(
|
||||
'rust-first-person-parity', 'VanillaFirstPersonOracle', 'first_person_probe', make_cases, scope,
|
||||
sources=[lab.ROOT / 'crates/horizon-client/src/first_person.rs', lab.ROOT / 'crates/horizon-client/src/attack_strength.rs',
|
||||
lab.ROOT / 'crates/horizon-client/src/camera.rs',
|
||||
lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/ParityData.java'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -334,6 +334,31 @@ def sprint_corpus():
|
||||
for gap in range(12):
|
||||
seq=[0,1,0]+[0]*gap+[1]*12+[0]*3+[65]*20+[1]*12+[0]
|
||||
yield dict(position=vec((12,4,0)),ops=[op('controlled_tick',k) for k in seq])
|
||||
# Sprint-jumping: Player.jumpFromGround's forward impulse while sprinting,
|
||||
# with sprint held (65) or started by double-tapping forward, and jump
|
||||
# pressed with it, tapped, held through landings (bunny hops) or held
|
||||
# before sprinting; at several headings, with too little food to sprint,
|
||||
# under a two-block ceiling and up onto a slab.
|
||||
runs={
|
||||
'held':[65]*12+[81]+[65]*14+[81]*2+[65]*14+[0]*10,
|
||||
'together':[0,81]+[81]*30+[0]*10,
|
||||
'hop':[65]*8+[81]*40+[0]*10,
|
||||
'jump_first':[16]*3+[17]*3+[81]*30+[0]*10,
|
||||
'double_tap':[1,0,1]+[1]*12+[17]+[1]*14+[17]*25+[0]*10,
|
||||
'release_sprint':[65]*10+[81]*6+[17]*20+[1]*10+[0]*5,
|
||||
}
|
||||
for name,seq in runs.items():
|
||||
for yaw in [0,45,90,180,300]:
|
||||
for food in [20,6]:
|
||||
for scene in ['open','ceiling','slab']:
|
||||
ops=[op('food',food=food,saturation=single(5),exhaustion=single(0),timer=0),op('yaw',single(yaw))]
|
||||
blocks=[]
|
||||
if scene=='ceiling':blocks=[([8,6,8+i],'stone',{}) for i in range(14)]
|
||||
elif scene=='slab':blocks=[([8,4,12],'stone_slab',{'type':'bottom'}),([8,4,13],'stone',{})]
|
||||
ops += [op('block',name='minecraft:'+b,position=pos,properties=props) for pos,b,props in blocks]
|
||||
ops += [op('controlled_tick',k) for k in seq]
|
||||
ops += [op('block',name='minecraft:air',position=pos) for pos,_,_ in blocks]
|
||||
yield dict(position=vec((8.5,4,8.5)),ops=ops)
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--sprint-input',action='store_true');args=parser.parse_args()
|
||||
|
||||
Reference in new issue
Block a user