mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Use stored entity bounds for supported box selector targets
Keep point distance and sorting, and retain the unresolved special-kind fallbacks. Co-Authored-By: Codex <[email protected]>
This commit is contained in:
1 parent
f319701e8c
commit
2d11bd857a
5 files changed
+313
-6
No files matched your search
@@ -14,6 +14,7 @@ mod players;
|
||||
pub(super) mod pregen;
|
||||
mod profiles;
|
||||
mod raid;
|
||||
mod selector_bounds;
|
||||
mod terrain_cmds;
|
||||
mod test_locate;
|
||||
mod test_runs;
|
||||
@@ -880,14 +881,21 @@ impl Game<'_> {
|
||||
}
|
||||
if s.delta.iter().any(Option::is_some) {
|
||||
let d = s.delta.map(|v| v.unwrap_or(0.0));
|
||||
for i in 0..3 {
|
||||
let (a, b) = (
|
||||
origin[i].min(origin[i] + d[i]),
|
||||
origin[i].max(origin[i] + d[i]) + 1.0,
|
||||
);
|
||||
if p[i] < a || p[i] >= b {
|
||||
if let Some(bounds) = self.selector_target_bounds(t) {
|
||||
if !selector_bounds::selector_box(origin, d).intersects(bounds) {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
// Unmodeled boxes retain their previous point fallback.
|
||||
for i in 0..3 {
|
||||
let (a, b) = (
|
||||
origin[i].min(origin[i] + d[i]),
|
||||
origin[i].max(origin[i] + d[i]) + 1.0,
|
||||
);
|
||||
if p[i] < a || p[i] >= b {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some((t, d2))
|
||||
@@ -4400,3 +4408,91 @@ mod teleport_rider_tests {
|
||||
assert_eq!(rider.position, Vec3::new(0.0, 64.0, 0.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod selector_bounds_tests {
|
||||
use super::*;
|
||||
use crate::game::{Audio, Options, Progress, Settings, load_resources};
|
||||
use horizon_client::{entities::ArmorStand, mobs::Mob};
|
||||
use horizon_core::geometry::Vec3;
|
||||
|
||||
#[test]
|
||||
fn actual_selector_uses_body_overlap_but_distance_stays_at_the_origin() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../.local/horizon-client")
|
||||
.join(horizon_core::VANILLA_VERSION);
|
||||
if !root.join("entity-types.json").exists() {
|
||||
eprintln!("Prepared resources unavailable; Game selector coverage did not run");
|
||||
return;
|
||||
}
|
||||
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.build_registries();
|
||||
let kind = game.res.mob_types().get("minecraft:pig").unwrap().clone();
|
||||
game.mobs
|
||||
.push(Mob::new(30, kind, Vec3::new(10.5, 220.0, 10.5), 0.0));
|
||||
let target = Target::Mob(30);
|
||||
let mut selector = Selector {
|
||||
kind: 'e',
|
||||
include_entities: true,
|
||||
limit: usize::MAX,
|
||||
delta: [Some(0.0); 3],
|
||||
..Default::default()
|
||||
};
|
||||
let origin = [9.5, 220.0, 10.5];
|
||||
assert_eq!(
|
||||
game.select_in_level(vec![target], &selector, origin),
|
||||
vec![(target, 1.0)]
|
||||
);
|
||||
selector.distance = Some((None, Some(0.9)));
|
||||
assert!(
|
||||
game.select_in_level(vec![target], &selector, origin)
|
||||
.is_empty()
|
||||
);
|
||||
selector.distance = None;
|
||||
assert!(
|
||||
game.select_in_level(vec![target], &selector, [11.0, 220.0, 10.5])
|
||||
.is_empty()
|
||||
);
|
||||
selector.delta = [Some(-1.0), None, None];
|
||||
assert_eq!(
|
||||
game.select_in_level(vec![target], &selector, [11.5, 220.0, 10.5])
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
game.armor_stands
|
||||
.push(ArmorStand::new(31, Vec3::new(30.5, 220.0, 10.5), 0.0));
|
||||
selector.delta = [Some(0.0); 3];
|
||||
for (x, count) in [(29.5, 1), (29.25, 0), (30.75, 0), (29.250000000000004, 1)] {
|
||||
assert_eq!(
|
||||
game.select_in_level(vec![Target::Stand(31)], &selector, [x, 220.0, 10.5])
|
||||
.len(),
|
||||
count
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
//! The physical bounds already kept for selector targets. This is not a
|
||||
//! rendered box or a new source of entity dimensions.
|
||||
use super::{Game, Target};
|
||||
use crate::game::entity::Entity;
|
||||
use horizon_core::geometry::{Aabb, Vec3};
|
||||
|
||||
fn extents((lo, hi): ([f64; 3], [f64; 3])) -> Aabb {
|
||||
Aabb::new(
|
||||
Vec3::new(lo[0], lo[1], lo[2]),
|
||||
Vec3::new(hi[0], hi[1], hi[2]),
|
||||
)
|
||||
}
|
||||
|
||||
/// `EntitySelectorParser.createAabb`, moved by the selector origin.
|
||||
pub(super) fn selector_box(origin: [f64; 3], delta: [f64; 3]) -> Aabb {
|
||||
let lo = std::array::from_fn(|i| origin[i] + delta[i].min(0.0));
|
||||
let hi = std::array::from_fn(|i| origin[i] + (delta[i].max(0.0) + 1.0));
|
||||
extents((lo, hi))
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
/// `Entity.getBoundingBox`: use each simulated kind's existing physical
|
||||
/// box. None means no authoritative box is modeled here yet; the selector
|
||||
/// retains its existing point fallback for those kinds.
|
||||
pub(super) fn selector_target_bounds(&self, target: Target) -> Option<Aabb> {
|
||||
match target {
|
||||
Target::Player => self.player.as_ref().map(|p| p.bounds()),
|
||||
Target::Guest(conn) => self.command_guest(conn).map(|p| p.bounds()),
|
||||
Target::Mob(id) => self.mobs.iter().find(|e| e.id == id).map(Entity::bounds),
|
||||
Target::Item(id) => self.dropped.iter().find(|e| e.id == id).map(Entity::bounds),
|
||||
Target::Orb(id) => self.orbs.iter().find(|e| e.id == id).map(Entity::bounds),
|
||||
Target::Projectile(id) => self
|
||||
.creatures
|
||||
.projectiles
|
||||
.iter()
|
||||
.find(|e| e.id == id)
|
||||
.map(Entity::bounds),
|
||||
Target::Stand(id) => self
|
||||
.armor_stands
|
||||
.iter()
|
||||
.find(|e| e.id == id)
|
||||
.map(Entity::bounds),
|
||||
Target::Boat(id) => self.boats.iter().find(|e| e.id == id).map(Entity::bounds),
|
||||
Target::Cart(id) => self.carts.iter().find(|e| e.id == id).map(Entity::bounds),
|
||||
Target::Tnt(id) => self.tnts.iter().find(|e| e.id == id).map(Entity::bounds),
|
||||
Target::Falling(id) => self.falling.iter().find(|e| e.id == id).map(Entity::bounds),
|
||||
Target::Rocket(id) => self.rockets.iter().find(|e| e.id == id).map(Entity::bounds),
|
||||
Target::Trident(id) => self
|
||||
.tridents
|
||||
.iter()
|
||||
.find(|e| e.id == id)
|
||||
.map(Entity::bounds),
|
||||
Target::Frame(id) => self
|
||||
.item_frames
|
||||
.iter()
|
||||
.find(|e| e.id == id)
|
||||
.map(|e| extents(e.bounds())),
|
||||
Target::Painting(id) => self
|
||||
.paintings
|
||||
.iter()
|
||||
.find(|e| e.id == id)
|
||||
.map(|e| extents(e.bounds())),
|
||||
Target::Crystal(id) => self
|
||||
.crystals
|
||||
.iter()
|
||||
.find(|e| e.id == id)
|
||||
.map(|e| e.bounds()),
|
||||
Target::Other(_)
|
||||
| Target::Fangs(_)
|
||||
| Target::Dragon(_)
|
||||
| Target::Spawner(_)
|
||||
| Target::Cloud(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Strict zero-tick box selection versus independent point distance/sorting."""
|
||||
from command_exec_corpus import cmd
|
||||
|
||||
|
||||
def selector_bounds():
|
||||
ops = []
|
||||
|
||||
def run(text, expected=None, label=None, record=True):
|
||||
op = cmd(text, record=record, expect_result=expected)
|
||||
if label:
|
||||
op['label'] = label
|
||||
ops.append(op)
|
||||
|
||||
run('/tick freeze', 1)
|
||||
run('/gamerule spawn_mobs false')
|
||||
run('/gamerule advance_time false')
|
||||
run('/forceload add 0 0 63 31', 8)
|
||||
ops.append({'op': 'settle', 'required_chunks': [[x, z] for x in range(4) for z in range(2)]})
|
||||
for x in range(4):
|
||||
for z in range(2):
|
||||
run(f'/execute if loaded {x*16} 220 {z*16}', 1)
|
||||
run('/fill 0 219 0 63 219 31 minecraft:stone', 2048)
|
||||
run('/fill 0 220 0 63 224 31 minecraft:air')
|
||||
for x in [10, 20, 30, 40, 50]:
|
||||
run(f'/execute if block {x} 219 10 minecraft:stone', 1)
|
||||
run('/execute if block 10 220 10 minecraft:air', 1)
|
||||
run('/tp @s 0.5 220 0.5')
|
||||
run('/gamemode creative')
|
||||
run('/execute if entity @s[type=minecraft:player,gamemode=creative,nbt={Health:20f,Pos:[0.5d,220d,0.5d]}]', 1)
|
||||
run('/data get entity @s UUID', 4)
|
||||
run('/kill @e[type=!player]', record=False)
|
||||
run('/scoreboard objectives add h030 dummy', 1)
|
||||
run('/execute store result score start h030 run time query gametime', record=False)
|
||||
actors = [
|
||||
('pig', 'pig', 10.5, 'Health:10f,NoAI:1b,', 'Health:10f,NoAI:1b,'),
|
||||
('golem', 'iron_golem', 20.5, 'Health:100f,NoAI:1b,', 'Health:100f,NoAI:1b,'),
|
||||
('stand', 'armor_stand', 30.5, 'Health:20f,', 'Health:20f,'),
|
||||
('item', 'item', 40.5, 'Health:5s,PickupDelay:32767s,Item:{id:"minecraft:stone",count:3},', 'Health:5s,Item:{id:"minecraft:stone",count:3},'),
|
||||
('boat', 'oak_boat', 50.5, '', ''),
|
||||
('far_pig', 'pig', 12.5, 'Health:10f,NoAI:1b,', 'Health:10f,NoAI:1b,'),
|
||||
]
|
||||
for n, (name, kind, x, extra, guard) in enumerate(actors, 1):
|
||||
tags = f'"h030_{name}"' + (',"h030_pigs"' if kind == 'pig' else '')
|
||||
run(f'/summon minecraft:{kind} {x} 220 10.5 {{UUID:[I;30,0,0,{n}],Tags:[{tags}],{extra}NoGravity:1b}}', 1)
|
||||
run(f'/execute if entity @e[type=minecraft:{kind},tag=h030_{name},nbt={{UUID:[I;30,0,0,{n}],{guard}NoGravity:1b,Pos:[{x}d,220d,10.5d]}}]', 1)
|
||||
run(f'/data get entity 0000001e-0000-0000-0000-{n:012x} UUID', 4)
|
||||
run(f'/data get entity 0000001e-0000-0000-0000-{n:012x} Pos')
|
||||
checks = [
|
||||
('pig-overlap-x', '9.5 220 10.5', 'tag=h030_pig,dx=0,dy=0,dz=0', 1),
|
||||
('pig-separated-x', '11 220 10.5', 'tag=h030_pig,dx=0,dy=0,dz=0', 0),
|
||||
('pig-point-distance', '9.5 220 10.5', 'tag=h030_pig,dx=0,dy=0,dz=0,distance=..0.9', 0),
|
||||
('pig-distance-one', '9.5 220 10.5', 'tag=h030_pig,dx=0,dy=0,dz=0,distance=..1', 1),
|
||||
('pig-overlap-z', '10.5 220 9.5', 'tag=h030_pig,dz=0', 1),
|
||||
('pig-positive-x', '8.5 220 10.5', 'tag=h030_pig,dx=1', 1),
|
||||
('pig-negative-x', '11.5 220 10.5', 'tag=h030_pig,dx=-1', 1),
|
||||
('pig-positive-z', '10.5 220 8.5', 'tag=h030_pig,dz=1', 1),
|
||||
('pig-negative-z', '10.5 220 11.5', 'tag=h030_pig,dz=-1', 1),
|
||||
('pig-positive-y', '10.5 218.5 10.5', 'tag=h030_pig,dy=1', 1),
|
||||
('pig-negative-y', '10.5 221.5 10.5', 'tag=h030_pig,dy=-1', 1),
|
||||
('pig-bottom-touch', '10.5 219 10.5', 'tag=h030_pig,dy=0', 0),
|
||||
('pig-top-touch', '10.5 220.89999997615814 10.5', 'tag=h030_pig,dy=0', 0),
|
||||
('pig-type-positive', '9.5 220 10.5', 'tag=h030_pig,type=minecraft:pig,dx=0', 1),
|
||||
('pig-type-negative', '9.5 220 10.5', 'tag=h030_pig,type=!minecraft:pig,dx=0', 0),
|
||||
('pig-type-other-negative', '9.5 220 10.5', 'tag=h030_pig,type=!minecraft:cow,dx=0,limit=1', 1),
|
||||
('golem-wide', '19 220 10.5', 'tag=h030_golem,dx=0', 1),
|
||||
('golem-tall', '20.5 222 10.5', 'tag=h030_golem,dy=0', 1),
|
||||
('golem-separated', '22 220 10.5', 'tag=h030_golem,dx=0', 0),
|
||||
('stand-overlap', '29.5 220 10.5', 'tag=h030_stand,dx=0', 1),
|
||||
('stand-left-touch', '29.25 220 10.5', 'tag=h030_stand,dx=0', 0),
|
||||
('stand-right-touch', '30.75 220 10.5', 'tag=h030_stand,dx=0', 0),
|
||||
('stand-left-inside', '29.250000000000004 220 10.5', 'tag=h030_stand,dx=0', 1),
|
||||
('stand-tall', '30.5 221.5 10.5', 'tag=h030_stand,dy=0', 1),
|
||||
('item-overlap', '39.5 220 10.5', 'tag=h030_item,dx=0', 1),
|
||||
('item-left-touch', '39.375 220 10.5', 'tag=h030_item,dx=0', 0),
|
||||
('item-right-touch', '40.625 220 10.5', 'tag=h030_item,dx=0', 0),
|
||||
('item-front-touch', '40.5 220 9.375', 'tag=h030_item,dz=0', 0),
|
||||
('item-back-touch', '40.5 220 10.625', 'tag=h030_item,dz=0', 0),
|
||||
('item-top-touch', '40.5 220.25 10.5', 'tag=h030_item,dy=0', 0),
|
||||
('boat-overlap', '49 220 10.5', 'tag=h030_boat,dx=0', 1),
|
||||
('boat-left-touch', '48.8125 220 10.5', 'tag=h030_boat,dx=0', 0),
|
||||
('boat-right-touch', '51.1875 220 10.5', 'tag=h030_boat,dx=0', 0),
|
||||
('pigs-both', '9.5 220 10.5', 'tag=h030_pigs,dx=3', 2),
|
||||
('pigs-limit', '9.5 220 10.5', 'tag=h030_pigs,dx=3,limit=1', 1),
|
||||
]
|
||||
for label, origin, options, expected in checks:
|
||||
run('/scoreboard players set result h030 -99', -99)
|
||||
run('/scoreboard players set success h030 -99', -99)
|
||||
run('/scoreboard players get result h030', -99)
|
||||
run('/scoreboard players get success h030', -99)
|
||||
run(f'/execute positioned {origin} store result score result h030 store success score success h030 run execute if entity @e[{options}]', expected, label)
|
||||
if expected == 0:
|
||||
ops[-1].pop('expect_result')
|
||||
ops[-1]['expect_callback'] = [False, 0]
|
||||
run('/scoreboard players get result h030', expected)
|
||||
run('/scoreboard players get success h030', int(expected > 0))
|
||||
for sort, tag in [('nearest', 'pig'), ('furthest', 'far_pig')]:
|
||||
run(f'/execute positioned 9.5 220 10.5 as @e[tag=h030_pigs,dx=3,sort={sort},limit=1] if entity @s[tag=h030_{tag}]', 1, 'point-sort-' + sort)
|
||||
run(f'/execute positioned 9.5 220 10.5 as @e[tag=h030_pigs,distance=..4,sort={sort},limit=1] if entity @s[tag=h030_{tag}]', 1, 'unboxed-point-sort-' + sort)
|
||||
for n, (name, kind, x, _, guard) in enumerate(actors, 1):
|
||||
run(f'/execute if entity @e[type=minecraft:{kind},tag=h030_{name},nbt={{UUID:[I;30,0,0,{n}],{guard}NoGravity:1b,Pos:[{x}d,220d,10.5d]}}]', 1)
|
||||
run(f'/data get entity 0000001e-0000-0000-0000-{n:012x} Pos')
|
||||
run('/execute store result score end h030 run time query gametime', record=False)
|
||||
run('/scoreboard players operation end h030 -= start h030', record=False)
|
||||
run('/scoreboard players get end h030', 0, 'zero-world-ticks')
|
||||
run('/execute if entity @s[type=minecraft:player,gamemode=creative,nbt={Health:20f,Pos:[0.5d,220d,0.5d]}]', 1)
|
||||
return ops
|
||||
@@ -128,6 +128,7 @@ def main():
|
||||
parser.add_argument("--pufferfish-mob-fixture", action="store_true")
|
||||
parser.add_argument("--pufferfish-mob-duration-diagnostic", action="store_true")
|
||||
parser.add_argument("--pufferfish-mob-overlap-control", action="store_true")
|
||||
parser.add_argument("--selector-bounds-fixture", action="store_true")
|
||||
args = parser.parse_args()
|
||||
build = args.work_dir.resolve() if args.work_dir else lab.LOCAL / 'command-exec-parity'
|
||||
build.mkdir(parents=True, exist_ok=True)
|
||||
@@ -250,6 +251,9 @@ def main():
|
||||
parser.error('reinforcement fixtures require --only with one exact group')
|
||||
if any(name in isolated_recipes for name, _ in groups) and sum(name != 'setup' for name, _ in groups) != 1:
|
||||
parser.error('crafting recipe fixtures require --only with one exact group')
|
||||
if args.selector_bounds_fixture:
|
||||
from selector_bounds_corpus import selector_bounds
|
||||
groups = [("setup", corpus_module.group_setup()), ("selector entity bounds", selector_bounds())]
|
||||
script = []
|
||||
for _, lines in groups:
|
||||
script.extend(lines)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Box selectors through actual Game entity bounds; point distance and sort controls."""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--binary', type=Path)
|
||||
parser.add_argument('--reference-only', action='store_true')
|
||||
parser.add_argument('--work-dir', type=Path)
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
command = [sys.executable, str(root / 'tools/test_command_exec_parity.py'), '--selector-bounds-fixture',
|
||||
'--work-dir', str(args.work_dir.resolve() if args.work_dir else root / '.local/selector-bounds-parity')]
|
||||
if args.binary:
|
||||
command += ['--binary', str(args.binary.resolve())]
|
||||
if args.reference_only:
|
||||
command.append('--reference-only')
|
||||
return subprocess.run(command, cwd=root).returncode
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user