mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge remote-tracking branch 'origin/main' into refactor/entity-walk-fluids
This commit is contained in:
commit
b07a0ccaa4
17 files changed
+534
-24
No files matched your search
@@ -5536,9 +5536,11 @@ impl Renderer {
|
||||
.map(pack_timing::PassTiming::medians)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
/// The macros of the GL extensions this device gives shader packs: subgroups, when it
|
||||
/// has them.
|
||||
/// The macros this renderer gives shader packs: the GL extensions this device has
|
||||
/// (subgroups, when it has them), and the vendor, renderer and mipmap levels
|
||||
/// (`Macros::define_renderer`).
|
||||
pub fn define_pack_macros(&self, macros: &mut horizon_shaders::Macros) {
|
||||
macros.define_renderer(self.mipmaps);
|
||||
if self.device.features().contains(wgpu::Features::SUBGROUP) {
|
||||
for ext in [
|
||||
"basic",
|
||||
|
||||
@@ -1171,6 +1171,7 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
player: player.clone(),
|
||||
nearest_player: player.as_ref().filter(|p| !p.spectator).map(|p| p.position),
|
||||
others: $snapshot,
|
||||
dragon: None,
|
||||
leash_boats: &[],
|
||||
others_grid: $grid,
|
||||
others_index: None,
|
||||
|
||||
@@ -210,6 +210,12 @@ impl Game<'_> {
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
"mobs" => {
|
||||
// Observe authoritative AI state without a control-socket race.
|
||||
let record = self.harness_owner(|g| Ok(g.control_mobs(128.0)))?;
|
||||
out.push_str(&record.to_string());
|
||||
out.push('\n');
|
||||
}
|
||||
"tick" => {
|
||||
self.chat_capture = Some(Vec::new());
|
||||
for _ in 0..op["n"].as_u64().unwrap_or(1) {
|
||||
|
||||
@@ -753,6 +753,7 @@ impl Game<'_> {
|
||||
nearest_player: player.as_ref().filter(|p| !p.spectator).map(|p| p.position),
|
||||
player,
|
||||
others,
|
||||
dragon: self.dragon_target(),
|
||||
leash_boats: &self.leash_boats,
|
||||
others_grid: None,
|
||||
others_index: None,
|
||||
@@ -7233,6 +7234,7 @@ mod pearl_tests {
|
||||
player: None,
|
||||
nearest_player: None,
|
||||
others: &[],
|
||||
dragon: None,
|
||||
leash_boats: &[],
|
||||
others_grid: None,
|
||||
others_index: None,
|
||||
|
||||
@@ -1129,6 +1129,11 @@ impl Game<'_> {
|
||||
for b in &self.bolts {
|
||||
g.hitbox(b.at, (0.0, 0.0), 0.0, false, (0.0, 0.0));
|
||||
}
|
||||
// AreaEffectCloud.getDimensions: diameter follows synced radius,
|
||||
// height is 0.5; EntityDimensions.scalable gives eyes at 85% height.
|
||||
for c in &self.potions.clouds {
|
||||
g.hitbox(c.at, (c.radius * 2.0, 0.5), 0.425, false, (0.0, 0.0));
|
||||
}
|
||||
// The dragon: 16 by 8, a living entity; its parts follow.
|
||||
if let Some(d) = &self.dragon {
|
||||
g.hitbox(
|
||||
|
||||
@@ -2188,6 +2188,27 @@ impl Game<'_> {
|
||||
set(d, 3 + i, x, y, z);
|
||||
}
|
||||
}
|
||||
/// `Level.getEntity`: revenge goals must resolve the dragon too.
|
||||
pub(super) fn dragon_target(&self) -> Option<(u32, horizon_client::mobs::TargetInfo)> {
|
||||
let d = self.dragon.as_ref().filter(|d| d.alive())?;
|
||||
let p = d.position;
|
||||
Some((
|
||||
d.id,
|
||||
horizon_client::mobs::TargetInfo {
|
||||
position: p,
|
||||
eye_y: p.y + 6.8,
|
||||
bounds: Aabb::new(
|
||||
Vec3::new(p.x - 8.0, p.y, p.z - 8.0),
|
||||
Vec3::new(p.x + 8.0, p.y + 8.0, p.z + 8.0),
|
||||
),
|
||||
alive: true,
|
||||
can_be_seen_as_enemy: true,
|
||||
is_player: false,
|
||||
creative: false,
|
||||
passenger: false,
|
||||
},
|
||||
))
|
||||
}
|
||||
/// Wing knockback, head and neck damage, and breaking blocks in the way.
|
||||
fn dragon_contacts(&mut self) {
|
||||
let Some(d) = self.dragon.as_ref() else {
|
||||
@@ -2232,6 +2253,35 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// EnderDragon.aiStep queries living entities, not only players.
|
||||
if let Some(d) = self.dragon.as_ref() {
|
||||
let frame = horizon_client::mobs::dragon_contacts::Frame {
|
||||
dragon_id: d.id,
|
||||
origin: d.position,
|
||||
center: (bc, bz),
|
||||
wings: wings.map(|w| {
|
||||
w.inflate(Vec3::new(4.0, 2.0, 4.0))
|
||||
.move_by(Vec3::new(0.0, -2.0, 0.0))
|
||||
}),
|
||||
heads: heads.map(|h| h.inflate(Vec3::new(1.0, 1.0, 1.0))),
|
||||
sitting,
|
||||
recently_hurt,
|
||||
};
|
||||
// Keep team lookup in Game (including tamed pets' owner teams).
|
||||
let allowed: Vec<bool> = self
|
||||
.mobs
|
||||
.iter()
|
||||
.map(|m| {
|
||||
horizon_client::scoreboard::teams_allow_damage(
|
||||
self.mob_team(d.id),
|
||||
self.mob_team_of(m),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
for (mob, allowed) in self.mobs.iter_mut().zip(allowed) {
|
||||
horizon_client::mobs::dragon_contacts::contact(mob, &frame, allowed);
|
||||
}
|
||||
}
|
||||
// checkWalls over head, neck and body.
|
||||
let griefing = self.ui.rules.bool("mob_griefing");
|
||||
let mut in_wall = false;
|
||||
|
||||
@@ -16,8 +16,8 @@ use horizon_world::registry::BlockStateId;
|
||||
/// An `AreaEffectCloud` a server tracks: what its client tick reads.
|
||||
pub(super) struct RemoteCloud {
|
||||
id: u32,
|
||||
at: [f64; 3],
|
||||
radius: f32,
|
||||
pub(super) at: [f64; 3],
|
||||
pub(super) radius: f32,
|
||||
waiting: bool,
|
||||
particle: ParticleOptions,
|
||||
}
|
||||
|
||||
@@ -99,7 +99,8 @@ pub mod warden_brain;
|
||||
pub mod witch;
|
||||
pub mod wither;
|
||||
mod world;
|
||||
pub use world::visual_clip;
|
||||
pub use world::{TargetInfo, visual_clip};
|
||||
pub mod dragon_contacts;
|
||||
pub mod zombie_villager;
|
||||
|
||||
use crate::{blocks::BlockFacts, mesher::Vertex, world::World};
|
||||
@@ -2438,6 +2439,8 @@ pub struct MobWorld<'a> {
|
||||
/// player in the level stands, alive or not (the despawn distance).
|
||||
pub nearest_player: Option<Vec3>,
|
||||
pub others: &'a [Seen],
|
||||
/// The separately simulated dragon, visible to revenge and melee goals.
|
||||
pub dragon: Option<(u32, TargetInfo)>,
|
||||
/// The boats and rafts a lead can hang on (`Holder::Boat`): id and state.
|
||||
pub leash_boats: &'a [(u32, leash::HolderState)],
|
||||
/// `others` by position, when the caller keeps one (the mob ticks).
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
//! `EnderDragon.knockBack` and `hurt` for living mobs.
|
||||
//! The dragon lives outside the normal mob array; its contacts must still reach them.
|
||||
// Contact rules compared with the installed vanilla Java 26.3 EnderDragon.
|
||||
use crate::mobs::{Damage, DamageKind, Mob, Who};
|
||||
use horizon_core::geometry::{Aabb, Vec3};
|
||||
|
||||
pub struct Frame {
|
||||
pub dragon_id: u32,
|
||||
pub origin: Vec3,
|
||||
pub center: (f64, f64),
|
||||
pub wings: [Aabb; 2],
|
||||
pub heads: [Aabb; 2],
|
||||
pub sitting: bool,
|
||||
pub recently_hurt: bool,
|
||||
}
|
||||
|
||||
/// Wings push even while perched and even when teams forbid damage.
|
||||
/// The normal mob damage handler owns armor, invulnerability frames and last-attacker bookkeeping.
|
||||
pub fn contact(m: &mut Mob, f: &Frame, teams_allow: bool) {
|
||||
if f.recently_hurt || m.removed || m.id == f.dragon_id {
|
||||
return;
|
||||
}
|
||||
let bounds = m.bounds();
|
||||
let source = Damage {
|
||||
kind: DamageKind::MobAttack,
|
||||
attacker: Some(Who::Mob(f.dragon_id)),
|
||||
from: Some([f.origin.x, f.origin.y, f.origin.z]),
|
||||
};
|
||||
for wing in &f.wings {
|
||||
if wing.intersects(bounds) {
|
||||
let dx = m.position.x - f.center.0;
|
||||
let dz = m.position.z - f.center.1;
|
||||
let dd = (dx * dx + dz * dz).max(0.1);
|
||||
m.velocity.x += dx / dd * 4.0;
|
||||
m.velocity.y += f64::from(0.2_f32);
|
||||
m.velocity.z += dz / dd * 4.0;
|
||||
if !f.sitting
|
||||
&& teams_allow
|
||||
&& m.ai.last_hurt_by_time < (m.age as i32).saturating_sub(2)
|
||||
{
|
||||
m.damage(5.0, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
if teams_allow {
|
||||
for head in &f.heads {
|
||||
if head.intersects(bounds) {
|
||||
m.damage(10.0, source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn box_at(x: f64) -> Aabb {
|
||||
Aabb::new(Vec3::new(x, -1.0, -1.0), Vec3::new(x + 2.0, 3.0, 1.0))
|
||||
}
|
||||
fn setup() -> (Mob, Frame) {
|
||||
let mut m = Mob::new(
|
||||
22,
|
||||
crate::mobs::sensor_scan_tests::kind(),
|
||||
Vec3::new(1.0, 0.0, 0.0),
|
||||
0.0,
|
||||
);
|
||||
m.health = 40.0;
|
||||
m.age = 100;
|
||||
let f = Frame {
|
||||
dragon_id: 99,
|
||||
origin: Vec3::ZERO,
|
||||
center: (0.0, 0.0),
|
||||
wings: [box_at(0.0), box_at(100.0)],
|
||||
heads: [box_at(100.0), box_at(100.0)],
|
||||
sitting: false,
|
||||
recently_hurt: false,
|
||||
};
|
||||
(m, f)
|
||||
}
|
||||
#[test]
|
||||
fn flying_wing_hurts_and_names_the_dragon() {
|
||||
let (mut m, f) = setup();
|
||||
contact(&mut m, &f, true);
|
||||
assert_eq!(m.health, 35.0);
|
||||
assert_eq!(m.ai.last_hurt_by, Some(Who::Mob(99)));
|
||||
}
|
||||
#[test]
|
||||
fn perched_wing_pushes_without_damage() {
|
||||
let (mut m, mut f) = setup();
|
||||
f.sitting = true;
|
||||
contact(&mut m, &f, true);
|
||||
assert_eq!(m.health, 40.0);
|
||||
assert_eq!(m.velocity, Vec3::new(4.0, f64::from(0.2_f32), 0.0));
|
||||
}
|
||||
#[test]
|
||||
fn team_protection_keeps_push_but_blocks_damage() {
|
||||
let (mut m, f) = setup();
|
||||
contact(&mut m, &f, false);
|
||||
assert_eq!(m.health, 40.0);
|
||||
assert_eq!(m.velocity, Vec3::new(4.0, f64::from(0.2_f32), 0.0));
|
||||
}
|
||||
#[test]
|
||||
fn hurt_dragon_does_not_contact_mobs() {
|
||||
let (mut m, mut f) = setup();
|
||||
f.recently_hurt = true;
|
||||
contact(&mut m, &f, true);
|
||||
assert_eq!(m.health, 40.0);
|
||||
assert_eq!(m.velocity, Vec3::ZERO);
|
||||
}
|
||||
#[test]
|
||||
fn head_hurts_even_when_perched() {
|
||||
let (mut m, mut f) = setup();
|
||||
f.sitting = true;
|
||||
f.wings = [box_at(100.0), box_at(100.0)];
|
||||
f.heads[0] = box_at(0.0);
|
||||
contact(&mut m, &f, true);
|
||||
assert_eq!(m.health, 30.0);
|
||||
}
|
||||
#[test]
|
||||
fn recent_mob_attack_suppresses_wing_damage() {
|
||||
let (mut m, f) = setup();
|
||||
m.ai.last_hurt_by_time = 99;
|
||||
contact(&mut m, &f, true);
|
||||
assert_eq!(m.health, 40.0);
|
||||
assert_eq!(m.velocity, Vec3::new(4.0, f64::from(0.2_f32), 0.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod parity {
|
||||
use super::*;
|
||||
#[test]
|
||||
#[ignore = "writes the vanilla parity transcript"]
|
||||
fn contact_transcript() {
|
||||
let mut rows = Vec::new();
|
||||
for name in [
|
||||
"flying",
|
||||
"sitting",
|
||||
"two_wings",
|
||||
"team",
|
||||
"head",
|
||||
"hurt_dragon",
|
||||
"recent_victim",
|
||||
] {
|
||||
let mut kind = (*crate::mobs::sensor_scan_tests::kind()).clone();
|
||||
kind.id = "minecraft:enderman".into();
|
||||
kind.species = crate::mobs::Species::Enderman;
|
||||
kind.max_health = 40.0;
|
||||
kind.width = 0.6;
|
||||
kind.height = 2.9;
|
||||
let mut m = Mob::new(22, std::sync::Arc::new(kind), Vec3::new(1.0, 0.0, 0.0), 0.0);
|
||||
m.age = 100;
|
||||
let near = Aabb::new(Vec3::new(-10.0, -10.0, -10.0), Vec3::new(10.0, 10.0, 10.0));
|
||||
let far = near.move_by(Vec3::new(100.0, 0.0, 0.0));
|
||||
let mut f = Frame {
|
||||
dragon_id: 99,
|
||||
origin: Vec3::ZERO,
|
||||
center: (0.0, 0.0),
|
||||
wings: [near, far],
|
||||
heads: [far, far],
|
||||
sitting: name == "sitting",
|
||||
recently_hurt: name == "hurt_dragon",
|
||||
};
|
||||
if name == "two_wings" {
|
||||
f.wings = [near, near];
|
||||
}
|
||||
if name == "head" {
|
||||
f.wings = [far, far];
|
||||
f.heads = [near, far];
|
||||
}
|
||||
if name == "recent_victim" {
|
||||
m.ai.last_hurt_by = Some(Who::Mob(99));
|
||||
m.ai.last_hurt_by_time = 99;
|
||||
}
|
||||
contact(&mut m, &f, name != "team");
|
||||
rows.push(serde_json::json!({"case":name,"health":m.health,"velocity":[m.velocity.x,m.velocity.y,m.velocity.z],"revenge":m.ai.last_hurt_by==Some(Who::Mob(99))}));
|
||||
}
|
||||
let path = std::env::var_os("HORIZON_DRAGON_CONTACT_OUTPUT").expect("parity output path");
|
||||
std::fs::write(path, serde_json::to_vec_pretty(&rows).unwrap()).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -1734,6 +1734,7 @@ mod tests {
|
||||
player: None,
|
||||
nearest_player: None,
|
||||
others: &[],
|
||||
dragon: None,
|
||||
leash_boats: &[],
|
||||
others_grid: None,
|
||||
others_index: None,
|
||||
@@ -1844,6 +1845,7 @@ mod tests {
|
||||
player: None,
|
||||
nearest_player: None,
|
||||
others: &[],
|
||||
dragon: None,
|
||||
leash_boats: &[],
|
||||
others_grid: None,
|
||||
others_index: None,
|
||||
|
||||
@@ -267,6 +267,11 @@ impl MobWorld<'_> {
|
||||
if self.daylight { 0 } else { 11 }
|
||||
}
|
||||
pub fn target_info(&self, who: Who) -> Option<TargetInfo> {
|
||||
if let Some((id, target)) = self.dragon
|
||||
&& who == Who::Mob(id)
|
||||
{
|
||||
return Some(target);
|
||||
}
|
||||
match who {
|
||||
Who::Player => self.player.as_ref().map(|p| TargetInfo {
|
||||
position: p.position,
|
||||
@@ -462,6 +467,7 @@ pub(crate) fn with_resource_test_world(
|
||||
player: None,
|
||||
nearest_player: None,
|
||||
others,
|
||||
dragon: None,
|
||||
leash_boats: &[],
|
||||
others_grid: None,
|
||||
others_index: None,
|
||||
|
||||
@@ -204,6 +204,7 @@ mod tests {
|
||||
player: None,
|
||||
nearest_player: None,
|
||||
others: &[],
|
||||
dragon: None,
|
||||
leash_boats: &[],
|
||||
others_grid: None,
|
||||
others_index: None,
|
||||
|
||||
@@ -78,8 +78,9 @@ pub fn render_stage(name: &str) -> u32 {
|
||||
|
||||
impl Macros {
|
||||
/// The standard macros that need no renderer (spec 1.5.1): `MC_VERSION`, this system's
|
||||
/// `MC_OS_*`, the GL and GLSL versions Horizon's translation accepts, the render stages and
|
||||
/// the host settings at their defaults. The GPU vendor and family arrive with the renderer.
|
||||
/// `MC_OS_*`, the GL and GLSL versions Iris reports, the render stages and the host
|
||||
/// settings at their defaults. The GPU vendor and family arrive with the renderer
|
||||
/// (`define_renderer`).
|
||||
pub fn standard() -> Self {
|
||||
let mut m = Self::default();
|
||||
m.define("MC_VERSION", MC_VERSION.to_string());
|
||||
@@ -90,8 +91,11 @@ impl Macros {
|
||||
_ => "MC_OS_OTHER",
|
||||
};
|
||||
m.define(os, "");
|
||||
m.define("MC_GL_VERSION", "460");
|
||||
m.define("MC_GLSL_VERSION", "460");
|
||||
// What Iris 1.11.7 reports on 26.3, whatever the pack's `#version` (observed with a probe
|
||||
// pack): its GL context's 3.3. Packs then take the paths they take there; the
|
||||
// translation itself reads GLSL up to 4.60.
|
||||
m.define("MC_GL_VERSION", "330");
|
||||
m.define("MC_GLSL_VERSION", "330");
|
||||
for (i, stage) in RENDER_STAGES.iter().enumerate() {
|
||||
m.define(format!("MC_RENDER_STAGE_{stage}"), i.to_string());
|
||||
}
|
||||
@@ -106,9 +110,25 @@ impl Macros {
|
||||
// Packs take Iris's extensions (compute programs, images, custom textures) where this
|
||||
// is defined.
|
||||
m.define("IS_IRIS", "");
|
||||
// Iris 1.11.7's own, as it defines them on 26.3 (observed with a probe pack): its version
|
||||
// (1.11.7 as 11107), and translucent terrain sorted back to front, as Horizon sorts it
|
||||
// (vanilla's `VertexSorting.byDistance`, `gfx/translucency.rs`).
|
||||
m.define("IRIS_VERSION", "11107");
|
||||
m.define("IRIS_HAS_TRANSLUCENCY_SORTING", "");
|
||||
m
|
||||
}
|
||||
|
||||
/// The macros the renderer adds: the mipmap levels terrain is drawn with (`MC_MIPMAP_LEVEL`,
|
||||
/// Iris's: it follows the Mipmap Levels option), and `OTHER` as the GL vendor and renderer
|
||||
/// (OptiFine's shaders.txt, E and F: one of each is defined). Horizon draws through wgpu, not
|
||||
/// a GL driver, so a pack's paths for one driver (an NVIDIA extension it takes behind
|
||||
/// `MC_GL_VENDOR_NVIDIA`) are not there to take.
|
||||
pub fn define_renderer(&mut self, mipmap_levels: u32) {
|
||||
self.define("MC_GL_VENDOR_OTHER", "");
|
||||
self.define("MC_GL_RENDERER_OTHER", "");
|
||||
self.define("MC_MIPMAP_LEVEL", mipmap_levels.to_string());
|
||||
}
|
||||
|
||||
/// `iris.features.optional`'s features Horizon has, as `IRIS_FEATURE_<name>`; a note for
|
||||
/// each feature `iris.features.required` names that it lacks.
|
||||
pub fn define_features(&mut self, properties: &crate::Properties) -> Vec<String> {
|
||||
@@ -333,14 +353,9 @@ pub(crate) fn is_ident(c: char) -> bool {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Packs take their normal- and specular-map branches, as they do with the maps on: a
|
||||
/// declaration made only there is seen.
|
||||
#[test]
|
||||
fn programs_take_the_normal_and_specular_map_branches() {
|
||||
let fsh = "#version 330\nout vec4 c;\n\
|
||||
#ifdef MC_NORMAL_MAP\nconst float bump = 1.0;\n#endif\n\
|
||||
#ifdef MC_SPECULAR_MAP\nconst float shine = 2.0;\n#endif\n\
|
||||
void main() { c = vec4(bump, shine, 0.0, 1.0); }\n";
|
||||
/// Whether `fsh` translates with `m`: a program that declares what it reads only under
|
||||
/// a macro's condition translates when the condition holds.
|
||||
fn translates(fsh: &str, m: &Macros) -> Result<(), String> {
|
||||
let vsh = "#version 330\nvoid main() { gl_Position = vec4(0.0); }\n";
|
||||
let pack = crate::Pack::from_zip_bytes(
|
||||
"P.zip",
|
||||
@@ -350,13 +365,49 @@ mod tests {
|
||||
]),
|
||||
)
|
||||
.unwrap();
|
||||
let m = Macros::standard();
|
||||
let program = crate::translate_composite(
|
||||
&crate::preprocess(&pack, "composite.vsh", &m),
|
||||
&crate::preprocess(&pack, "composite.fsh", &m),
|
||||
crate::translate_composite(
|
||||
&crate::preprocess(&pack, "composite.vsh", m),
|
||||
&crate::preprocess(&pack, "composite.fsh", m),
|
||||
crate::Target::Buffers,
|
||||
);
|
||||
assert!(program.is_ok(), "{:?}", program.err());
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|e| format!("{e:?}"))
|
||||
}
|
||||
|
||||
/// Packs see what Iris 1.11.7 defines on 26.3: GL and GLSL 3.3, its version and its sorted
|
||||
/// translucent terrain, so they take the paths they take there.
|
||||
#[test]
|
||||
fn programs_see_the_versions_iris_reports() {
|
||||
let fsh = "#version 330\nout vec4 c;\n\
|
||||
#if MC_GL_VERSION == 330 && MC_GLSL_VERSION == 330 && IRIS_VERSION >= 11107 \\\n\
|
||||
&& defined IRIS_HAS_TRANSLUCENCY_SORTING\nconst float v = 1.0;\n#endif\n\
|
||||
void main() { c = vec4(v); }\n";
|
||||
translates(fsh, &Macros::standard()).unwrap();
|
||||
}
|
||||
|
||||
/// With the renderer's macros a pack sees one GL vendor and renderer, `OTHER`, and the
|
||||
/// mipmap levels terrain is drawn with.
|
||||
#[test]
|
||||
fn programs_see_the_renderers_vendor_and_mipmap_levels() {
|
||||
let fsh = "#version 330\nout vec4 c;\n\
|
||||
#if defined MC_GL_VENDOR_OTHER && defined MC_GL_RENDERER_OTHER && MC_MIPMAP_LEVEL == 2\n\
|
||||
const float v = 1.0;\n#endif\n\
|
||||
#if defined MC_GL_VENDOR_NVIDIA || defined MC_GL_RENDERER_GEFORCE\nconst float v = 2.0;\n#endif\n\
|
||||
void main() { c = vec4(v); }\n";
|
||||
let mut m = Macros::standard();
|
||||
m.define_renderer(2);
|
||||
translates(fsh, &m).unwrap();
|
||||
}
|
||||
|
||||
/// Packs take their normal- and specular-map branches, as they do with the maps on: a
|
||||
/// declaration made only there is seen.
|
||||
#[test]
|
||||
fn programs_take_the_normal_and_specular_map_branches() {
|
||||
let fsh = "#version 330\nout vec4 c;\n\
|
||||
#ifdef MC_NORMAL_MAP\nconst float bump = 1.0;\n#endif\n\
|
||||
#ifdef MC_SPECULAR_MAP\nconst float shine = 2.0;\n#endif\n\
|
||||
void main() { c = vec4(bump, shine, 0.0, 1.0); }\n";
|
||||
translates(fsh, &Macros::standard()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -222,6 +222,16 @@ public final class VanillaCommandExecOracle {
|
||||
out.append(gson.toJson(record)).append('\n');
|
||||
}
|
||||
}
|
||||
case "dragon_contacts" -> {
|
||||
// Optional fixture: the shared runner still compiles on its own.
|
||||
try {
|
||||
Class<?> fixture = Class.forName("dev.horizonlab.parity.VanillaDragonContactsOracle");
|
||||
var method = fixture.getDeclaredMethod("run", ServerLevel.class);
|
||||
out.append(gson.toJson(method.invoke(null, level))).append('\n');
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
case "settle" -> settle();
|
||||
case "break" -> {
|
||||
JsonArray pos = op.getAsJsonArray("pos");
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package dev.horizonlab.parity;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
|
||||
import net.minecraft.world.entity.boss.enderdragon.EnderDragonPart;
|
||||
import net.minecraft.world.entity.boss.enderdragon.phases.EnderDragonPhase;
|
||||
import net.minecraft.world.entity.monster.Enderman;
|
||||
|
||||
/** Calls the real 26.3 dragon contact methods; no entity or physics ticks intervene. */
|
||||
public final class VanillaDragonContactsOracle {
|
||||
static JsonArray run(ServerLevel level) {
|
||||
try {
|
||||
Method wing = EnderDragon.class.getDeclaredMethod("knockBack", ServerLevel.class, List.class);
|
||||
Method head = EnderDragon.class.getDeclaredMethod("hurt", ServerLevel.class, List.class);
|
||||
Field body = EnderDragon.class.getDeclaredField("body");
|
||||
wing.setAccessible(true);
|
||||
head.setAccessible(true);
|
||||
body.setAccessible(true);
|
||||
JsonArray rows = new JsonArray();
|
||||
String[] names = {"flying", "sitting", "two_wings", "team", "head", "hurt_dragon", "recent_victim"};
|
||||
for (String name : names) {
|
||||
EnderDragon dragon = (EnderDragon) BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse("minecraft:ender_dragon")).create(level, EntitySpawnReason.COMMAND);
|
||||
dragon.setPos(0, 0, 0);
|
||||
((EnderDragonPart) body.get(dragon)).setPos(0, 0, 0);
|
||||
dragon.getPhaseManager().setPhase(name.equals("sitting") ? EnderDragonPhase.HOVERING : EnderDragonPhase.HOLDING_PATTERN);
|
||||
Enderman mob = (Enderman) BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse("minecraft:enderman")).create(level, EntitySpawnReason.COMMAND);
|
||||
mob.setPos(1, 0, 0);
|
||||
mob.setNoAi(true);
|
||||
mob.tickCount = 100;
|
||||
if (name.equals("recent_victim")) {
|
||||
mob.tickCount = 99;
|
||||
mob.setLastHurtByMob(dragon);
|
||||
mob.tickCount = 100;
|
||||
}
|
||||
if (name.equals("team")) {
|
||||
var board = level.getScoreboard();
|
||||
var team = board.addPlayerTeam("contact");
|
||||
team.setAllowFriendlyFire(false);
|
||||
board.addPlayerToTeam(dragon.getScoreboardName(), team);
|
||||
board.addPlayerToTeam(mob.getScoreboardName(), team);
|
||||
}
|
||||
if (!name.equals("hurt_dragon")) {
|
||||
if (name.equals("head")) head.invoke(dragon, level, List.of(mob));
|
||||
else {
|
||||
wing.invoke(dragon, level, List.of(mob));
|
||||
if (name.equals("two_wings")) wing.invoke(dragon, level, List.of(mob));
|
||||
}
|
||||
}
|
||||
JsonObject row = new JsonObject();
|
||||
row.addProperty("case", name);
|
||||
row.addProperty("health", mob.getHealth());
|
||||
JsonArray velocity = new JsonArray();
|
||||
velocity.add(mob.getDeltaMovement().x);
|
||||
velocity.add(mob.getDeltaMovement().y);
|
||||
velocity.add(mob.getDeltaMovement().z);
|
||||
row.add("velocity", velocity);
|
||||
row.addProperty("revenge", mob.getLastHurtByMob() == dragon);
|
||||
rows.add(row);
|
||||
if (name.equals("team")) level.getScoreboard().removePlayerTeam(level.getScoreboard().getPlayerTeam("contact"));
|
||||
mob.discard();
|
||||
dragon.discard();
|
||||
}
|
||||
return rows;
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ EnderDragon.onFlap port crates/horizon-client/src/game/dragon.rs::Game::tick_dra
|
||||
EnderDragon.defineSynchedData port crates/horizon-client/src/game/dragon.rs::Game::lan_dragon_state DATA_PHASE defined as hovering on the wire
|
||||
EnderDragon.tickPart port crates/horizon-client/src/game/dragon.rs::Game::place_dragon_parts
|
||||
EnderDragon.getHeadYOffset inline crates/horizon-client/src/game/dragon.rs::Game::place_dragon_parts sitting gives -1, else flight sample 5 minus sample 0
|
||||
EnderDragon.knockBack port crates/horizon-client/src/game/dragon.rs::Game::dragon_contacts wing zones push players
|
||||
EnderDragon.knockBack port crates/horizon-client/src/game/dragon.rs::Game::dragon_contacts wing zones push players and living mobs; head/neck contacts hurt living mobs
|
||||
EnderDragon.handleKillingBlow inline crates/horizon-client/src/game/dragon.rs::Dragon::hurt health set to 1 and dying phase unless sitting
|
||||
EnderDragon.knockback missing minor: a hit does not push the dragon back outside the sitting phase
|
||||
EnderDragon.findClosestNode port crates/horizon-client/src/game/dragon.rs::Game::closest_node
|
||||
|
||||
|
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare living-mob dragon contacts with the real Java 26.3 methods.
|
||||
|
||||
Checks health, resulting velocity and revenge attribution for seven contact
|
||||
scenarios. Does not claim parity of the whole fight, AI pathfinding, or gear
|
||||
with post-attack enchantments. The game integration check is separate.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
try:
|
||||
from . import lab, test_rust_parity as base
|
||||
from .integration_env import game_environment
|
||||
except ImportError:
|
||||
import lab
|
||||
import test_rust_parity as base
|
||||
from integration_env import game_environment
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--classpath', help='already verified vanilla server and libraries')
|
||||
parser.add_argument('--resources', type=Path, help='also check the integrated-server game path')
|
||||
parser.add_argument('--binary', type=Path, help='already built game for --resources')
|
||||
args = parser.parse_args()
|
||||
build = lab.LOCAL / 'dragon-contacts-parity'
|
||||
build.mkdir(parents=True, exist_ok=True)
|
||||
report = build / 'report.json'
|
||||
report.unlink(missing_ok=True)
|
||||
cp = args.classpath
|
||||
major = '25'
|
||||
if not cp:
|
||||
lock, jars, libraries = base.verify_artifacts()
|
||||
cp = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
major = str(lock['minecraft']['java_major'])
|
||||
classes = build / 'classes'
|
||||
classes.mkdir(exist_ok=True)
|
||||
source = lab.ROOT / 'mods/horizon/src/test/java'
|
||||
oracle = source / 'dev/horizonlab/parity'
|
||||
def run(command, name, **kwargs):
|
||||
with (build / (name + '.log')).open('w') as log:
|
||||
subprocess.run(list(map(str, command)), stdout=log,
|
||||
stderr=subprocess.STDOUT, check=True, timeout=240, **kwargs)
|
||||
run(['javac', '--release', major, '-proc:none', '-cp', cp, '-d', classes,
|
||||
oracle / 'VanillaCommandExecOracle.java',
|
||||
oracle / 'VanillaDragonContactsOracle.java'], 'compile-java')
|
||||
script = build / 'script.jsonl'
|
||||
script.write_text('{"op":"dragon_contacts"}\n')
|
||||
expected_path = build / 'java.json'
|
||||
actual_path = build / 'rust.json'
|
||||
for p in [expected_path, actual_path]:
|
||||
p.unlink(missing_ok=True)
|
||||
with tempfile.TemporaryDirectory(prefix='hz-dragon-contacts-') as scratch:
|
||||
env = game_environment(os.environ, scratch)
|
||||
run(['java', '-Xmx2G', '-cp', str(classes) + os.pathsep + cp,
|
||||
'dev.horizonlab.parity.VanillaCommandExecOracle',
|
||||
Path(scratch) / 'universe', script, expected_path, '1'], 'java', env=env)
|
||||
run(['cargo', 'test', '--locked', '-p', 'horizon-client-lib', '--lib',
|
||||
'mobs::dragon_contacts::', '--', '--include-ignored'], 'rust',
|
||||
env=dict(os.environ, HORIZON_DRAGON_CONTACT_OUTPUT=str(actual_path)))
|
||||
expected = json.loads(expected_path.read_text())
|
||||
actual = json.loads(actual_path.read_text())
|
||||
assert len(actual) == len(expected) == 7
|
||||
for want, got in zip(expected, actual):
|
||||
for key in ['case', 'health', 'revenge']:
|
||||
assert want[key] == got[key], (key, want, got)
|
||||
for a, b in zip(want['velocity'], got['velocity']):
|
||||
assert math.isclose(a, b, rel_tol=0, abs_tol=1e-12), (want, got)
|
||||
integration = False
|
||||
if args.resources:
|
||||
if not args.binary:
|
||||
parser.error('--resources requires --binary')
|
||||
ops = []
|
||||
def cmd(text):
|
||||
ops.append({'op': 'cmd', 'cmd': text, 'record': False})
|
||||
for c in ['/gamemode creative', '/gamerule spawn_mobs false',
|
||||
'/gamerule mob_griefing false',
|
||||
'/execute in minecraft:the_end run tp @s 8.5 205 8.5']:
|
||||
cmd(c)
|
||||
ops += [{'op': 'settle'}, {'op': 'tick', 'n': 260}]
|
||||
cmd('/kill @e[type=minecraft:ender_dragon]')
|
||||
cmd('/summon minecraft:ender_dragon 8 200 8 {DragonPhase:10}')
|
||||
cmd('/summon minecraft:enderman 8 200 1.5 {NoGravity:1b,Tags:["contact_test"]}')
|
||||
ops.append({'op': 'tick', 'n': 1})
|
||||
cmd('/data merge entity @e[tag=contact_test,limit=1] {Invulnerable:1b}')
|
||||
ops += [{'op': 'tick', 'n': 24}, {'op': 'mobs'}]
|
||||
game_script = build / 'game-script.jsonl'
|
||||
game_script.write_text(''.join(json.dumps(op) + '\n' for op in ops))
|
||||
game_output = build / 'game.jsonl'
|
||||
with tempfile.TemporaryDirectory(prefix='hz-dragon-integration-') as scratch:
|
||||
env = game_environment(os.environ, scratch)
|
||||
env.update(HORIZON_COMMAND_SCRIPT=str(game_script),
|
||||
HORIZON_COMMAND_OUTPUT=str(game_output), HORIZON_HARNESS_TIMEOUT='120')
|
||||
run([args.binary.resolve(), '--resources', args.resources.resolve(),
|
||||
'--world', 'contacts', '--seed', '1', '--radius', '4'], 'game', env=env, cwd=scratch)
|
||||
observations = [json.loads(line) for line in game_output.read_text().splitlines()]
|
||||
mobs = observations[-1]['near']
|
||||
endermen = [m for m in mobs if m['kind'] == 'minecraft:enderman']
|
||||
assert len(endermen) == 1, endermen
|
||||
mob = endermen[0]
|
||||
assert mob['health'] < 40 and str(mob['target']).startswith('Mob('), mob
|
||||
integration = True
|
||||
report.write_text(json.dumps({'passed': True, 'cases': len(actual),
|
||||
'integrated_server': integration}, indent=2) + '\n')
|
||||
print('dragon contacts: 7 Java/Rust cases match')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user