mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-12 18:35:16 -10:00
Credit the mob that swung for a guest's effect and sting hits
Only AttackPlayer passed the recorded id on, so a cave spider poisoning a guest could name another mob at the same X and Z: the search by position ignores height. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b82a70f352
commit
3ab80dc9d0
4 files changed
+94
-152
No files matched your search
@@ -807,3 +807,82 @@ mod stuck_tests {
|
||||
assert_eq!((s.arrows, s.stingers), (0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod guest_hit_tests {
|
||||
use super::*;
|
||||
use crate::game::{Audio, Options, PlayerMotion, Progress, Settings, load_resources};
|
||||
use horizon_client::mobs::{Mob, MobEvent};
|
||||
use horizon_core::geometry::Vec3;
|
||||
|
||||
/// A bystander stands exactly under the mob that bites, listed first: the
|
||||
/// search by position ignores height and would have named the bystander.
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn an_effect_hit_on_a_guest_credits_the_mob_that_swung() {
|
||||
let root = std::path::PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
|
||||
let settings = Settings::default();
|
||||
let loaded = load_resources(
|
||||
root.clone(),
|
||||
None,
|
||||
&root,
|
||||
&settings,
|
||||
std::time::Instant::now(),
|
||||
false,
|
||||
true,
|
||||
&Progress::new(),
|
||||
&mut |_, _| unreachable!(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut options = Options::parse(std::iter::empty()).unwrap().unwrap();
|
||||
options.resources = root.clone();
|
||||
options.execution_role = crate::game::execution_role::ExecutionRole::IntegratedServer;
|
||||
let mut game = Game::new(
|
||||
&loaded.resources,
|
||||
loaded.pack,
|
||||
loaded.gui.unwrap(),
|
||||
options,
|
||||
settings,
|
||||
Audio::silent(&root),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
game.player = Some(PlayerMotion::new(game.motion, Vec3::new(0.5, 100.0, 0.5)).unwrap());
|
||||
for (id, kind, y) in [
|
||||
(4, "minecraft:zombie", 90.0),
|
||||
(5, "minecraft:cave_spider", 100.0),
|
||||
] {
|
||||
let kind = game.res.mob_types().get(kind).cloned().unwrap();
|
||||
game.simulation
|
||||
.mobs
|
||||
.push(Mob::new(id, kind, Vec3::new(1.5, y, 0.5), 0.0));
|
||||
}
|
||||
let from = [1.5, 100.0, 0.5];
|
||||
let hits = [
|
||||
MobEvent::AttackPlayerWithEffect {
|
||||
amount: 2.0,
|
||||
from,
|
||||
effect: "minecraft:poison",
|
||||
ticks: 140,
|
||||
amplifier: 0,
|
||||
},
|
||||
MobEvent::StingPlayer {
|
||||
amount: 2.0,
|
||||
from,
|
||||
poison_ticks: 200,
|
||||
},
|
||||
];
|
||||
for hit in hits {
|
||||
game.combat.pending.clear();
|
||||
game.guest_mob_event(5, hit);
|
||||
let noted: Vec<u32> = game
|
||||
.combat
|
||||
.pending
|
||||
.iter()
|
||||
.map(|(_, (id, _), _)| *id)
|
||||
.collect();
|
||||
assert_eq!(noted, [5]);
|
||||
assert_eq!(game.combat.melee_mob, None, "the hit used it up");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,17 +134,25 @@ impl Game<'_> {
|
||||
return;
|
||||
};
|
||||
for (conn, mob, e) in events {
|
||||
self.guest_hit_by(&mut host, conn, |g| {
|
||||
// The mob swung: no guessing which one from where it stood.
|
||||
if matches!(e, MobEvent::AttackPlayer(..)) {
|
||||
g.combat.melee_mob = Some(mob);
|
||||
}
|
||||
let _ = g.handle_mob_events(vec![e]);
|
||||
});
|
||||
self.guest_hit_by(&mut host, conn, |g| g.guest_mob_event(mob, e));
|
||||
}
|
||||
self.lan = Some(host);
|
||||
}
|
||||
|
||||
/// One mob event, with the guest swapped in. A hit knows the mob that swung,
|
||||
/// so nothing guesses it from where the hit came from.
|
||||
pub(super) fn guest_mob_event(&mut self, mob: u32, e: MobEvent) {
|
||||
if matches!(
|
||||
e,
|
||||
MobEvent::AttackPlayer(..)
|
||||
| MobEvent::AttackPlayerWithEffect { .. }
|
||||
| MobEvent::StingPlayer { .. }
|
||||
) {
|
||||
self.combat.melee_mob = Some(mob);
|
||||
}
|
||||
let _ = self.handle_mob_events(vec![e]);
|
||||
}
|
||||
|
||||
/// The guests' boxes for the blocks entities trigger (pressure plates,
|
||||
/// tripwires, detector rails): `Entity.checkInsideBlocks` and
|
||||
/// `Level.getEntities` see every player in the level, but not spectators
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
//! Benchmark actual Mob::seen projection; this does not measure a mob/server tick.
|
||||
//! Build after horizon-client-lib (same features/profile), for example:
|
||||
//! rustc --edition=2024 -O tools/bench_mob_snapshot.rs -L dependency=.local/parity-target/fast/deps \
|
||||
//! --extern horizon_client_lib=<path-to-libhorizon_client_lib.rlib> -o /tmp/mob-snapshot-bench
|
||||
//! Run: /tmp/mob-snapshot-bench baseline|serial|parallel <prepared-resource-root>
|
||||
use horizon_client_lib::{
|
||||
mobs::{Mob, MobTypes, Seen},
|
||||
server_regions::{Regions, Workers},
|
||||
};
|
||||
use std::{hint::black_box, path::Path, sync::Arc, time::Instant};
|
||||
|
||||
fn main() {
|
||||
let mode = std::env::args()
|
||||
.nth(1)
|
||||
.expect("baseline, serial, or parallel");
|
||||
let workers = match mode.as_str() {
|
||||
"baseline" | "serial" => 1,
|
||||
"parallel" => 8,
|
||||
_ => panic!("baseline, serial, or parallel"),
|
||||
};
|
||||
let root = std::env::args().nth(2).expect("prepared resource root");
|
||||
let types = MobTypes::load_for_server(Path::new(&root)).expect("mob types");
|
||||
let kinds: Vec<_> = ["minecraft:zombie", "minecraft:cow", "minecraft:villager"]
|
||||
.map(|name| Arc::new(types.types.get(name).expect(name).clone()))
|
||||
.into_iter()
|
||||
.collect();
|
||||
let mobs: Vec<_> = (0..7000)
|
||||
.map(|i| {
|
||||
let mut mob = Mob::new(
|
||||
i + 1,
|
||||
kinds[i as usize % kinds.len()].clone(),
|
||||
Default::default(),
|
||||
0.0,
|
||||
);
|
||||
mob.position.x = f64::from(i / 70) * 1600.0 + 0.5;
|
||||
mob.position.y = 64.0;
|
||||
mob.position.z = f64::from(i % 70) + 0.5;
|
||||
mob.previous = mob.position;
|
||||
mob
|
||||
})
|
||||
.collect();
|
||||
let chunks =
|
||||
(0..100).flat_map(|p| (-8..=8).flat_map(move |x| (-8..=8).map(move |z| (p * 100 + x, z))));
|
||||
let mut regions = Regions::default();
|
||||
regions.rebuild(1, chunks);
|
||||
let players: Vec<_> = (0..100).map(|p| (p * 100, 0)).collect();
|
||||
let expected: Vec<_> = mobs.iter().map(|m| m.seen(None)).collect();
|
||||
let mut times = Vec::new();
|
||||
let mut last = Vec::new();
|
||||
for iteration in 0..120 {
|
||||
let start = Instant::now();
|
||||
let out: Vec<Seen> = if mode == "baseline" {
|
||||
mobs.iter().map(|m| black_box(m).seen(None)).collect()
|
||||
} else {
|
||||
regions.index(
|
||||
mobs.iter().map(|m| {
|
||||
(
|
||||
m.position.x.floor() as i32 >> 4,
|
||||
m.position.z.floor() as i32 >> 4,
|
||||
)
|
||||
}),
|
||||
players.iter().copied(),
|
||||
);
|
||||
regions.project(&mobs, Workers::new(workers), |m| black_box(m).seen(None))
|
||||
};
|
||||
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
|
||||
assert_eq!(out.len(), mobs.len());
|
||||
black_box(&out);
|
||||
if iteration >= 20 {
|
||||
times.push(elapsed);
|
||||
}
|
||||
last = out;
|
||||
}
|
||||
// Compare every observable snapshot field with the original serial method,
|
||||
// outside the timing. Seen deliberately has no implementation-pinning Eq.
|
||||
for (serial, projected) in expected.iter().zip(&last) {
|
||||
assert_eq!(format!("{serial:?}"), format!("{projected:?}"));
|
||||
}
|
||||
times.sort_by(f64::total_cmp);
|
||||
let percentile = |p: f64| times[((times.len() - 1) as f64 * p).ceil() as usize];
|
||||
println!(
|
||||
"mode={mode} samples={} mobs={} regions={} median_ms={:.3} p95_ms={:.3} p99_ms={:.3}",
|
||||
times.len(),
|
||||
mobs.len(),
|
||||
regions.regions().len(),
|
||||
percentile(0.5),
|
||||
percentile(0.95),
|
||||
percentile(0.99)
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
//! Standalone ownership/projection microbenchmark (not AI or a server tick).
|
||||
//! Build: rustc --edition=2024 -O tools/bench_server_regions.rs -o /tmp/region-bench
|
||||
//! Run each mode under /usr/bin/time -v for CPU and maximum resident memory.
|
||||
#[path = "../crates/horizon-client/src/server_regions.rs"]
|
||||
#[allow(dead_code)]
|
||||
mod server_regions;
|
||||
use server_regions::{Regions, Workers};
|
||||
use std::{hint::black_box, time::Instant};
|
||||
|
||||
fn main() {
|
||||
let mode = std::env::args().nth(1).unwrap_or_else(|| "baseline".into());
|
||||
let workers = match mode.as_str() {
|
||||
"baseline" | "serial" => 1,
|
||||
"parallel" => 8,
|
||||
_ => panic!("mode must be baseline, serial, or parallel"),
|
||||
};
|
||||
// 100 disconnected 17x17 loaded areas, 70 entities at each player.
|
||||
// The projection copies 384 bytes/entity, approximating snapshot traffic;
|
||||
// this deliberately does not pretend to model AI, navigation or combat.
|
||||
let entities: Vec<[u64; 48]> = (0..7000).map(|i| [i; 48]).collect();
|
||||
let chunks: Vec<_> = (0..100)
|
||||
.flat_map(|p| (-8..=8).flat_map(move |x| (-8..=8).map(move |z| (p * 100 + x, z))))
|
||||
.collect();
|
||||
let positions: Vec<_> = (0..7000).map(|i| ((i / 70) * 100, 0)).collect();
|
||||
let players: Vec<_> = (0..100).map(|p| (p * 100, 0)).collect();
|
||||
let mut regions = Regions::default();
|
||||
let started = Instant::now();
|
||||
regions.rebuild(1, chunks.into_iter());
|
||||
let topology_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||
let mut times = Vec::new();
|
||||
for tick in 0..350 {
|
||||
let started = Instant::now();
|
||||
let out: Vec<[u64; 48]> = if mode == "baseline" {
|
||||
entities.iter().map(|e| *black_box(e)).collect()
|
||||
} else {
|
||||
regions.index(positions.iter().copied(), players.iter().copied());
|
||||
regions.project(&entities, Workers::new(workers), |e| *black_box(e))
|
||||
};
|
||||
assert_eq!(black_box(&out), &entities);
|
||||
if tick >= 50 {
|
||||
times.push(started.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
}
|
||||
times.sort_by(f64::total_cmp);
|
||||
let percentile = |p: f64| times[((times.len() - 1) as f64 * p).ceil() as usize];
|
||||
println!(
|
||||
"mode={mode} samples={} entities={} regions={} topology_ms={topology_ms:.3} median_ms={:.3} p95_ms={:.3} p99_ms={:.3}",
|
||||
times.len(),
|
||||
entities.len(),
|
||||
regions.regions().len(),
|
||||
percentile(0.5),
|
||||
percentile(0.95),
|
||||
percentile(0.99)
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user