Evaluate named entity_scores predicates for bound holders

Co-Authored-By: Codex <[email protected]>
This commit is contained in:
Florian ERNSTandCodex committed 2026-11-06 15:59:55 +01:00
1 parent 41f822b659
commit 5201202a30
5 files changed
+360 -3

No files matched your search

+28
View File
@@ -662,6 +662,10 @@ pub struct EntityView {
/// `Entity.getTeam().getName()` (a tamed pet answers its owner's team);
/// `None` without a team or where the identity is not bound.
pub team: Option<String>,
/// Existing objective scores for the bound command source holder. `None`
/// means this context did not bind a scoreboard holder; an empty map is a
/// real holder without scores. Named predicates snapshot current values.
pub scores: Option<BTreeMap<String, i32>>,
}
/// What `PlayerPredicate` reads of a player beyond its entity. Each part is
/// filled only when a criterion may ask for it (`Tree::player_facts_asked`);
@@ -1343,6 +1347,30 @@ pub fn conditions(v: &Value, ctx: &Entities, w: &dyn World) -> bool {
.and_then(Value::as_str)
.unwrap_or("");
match &*full(ty) {
// EntityHasScoreCondition.test/hasScore: missing objective or
// score fails; this bounded path supports constant integer ranges.
"minecraft:entity_scores" if v["entity"].as_str() == Some("this") => {
let scores = ctx.this.and_then(|e| e.scores.as_ref());
v["scores"].as_object().is_some_and(|tests| {
scores.is_some_and(|scores| {
tests.iter().all(|(objective, range)| {
// Do not treat unsupported number providers as
// absent min/max bounds in the shared range helper.
let numeric = range.as_i64().is_some()
|| range.as_object().is_some_and(|bounds| {
bounds.iter().all(|(key, value)| {
matches!(key.as_str(), "min" | "max")
&& value.as_i64().is_some()
})
});
numeric
&& scores
.get(objective)
.is_some_and(|score| range_i(range, i64::from(*score)))
})
})
})
}
"minecraft:entity_properties" => ctx
.this
.is_some_and(|e| entity(&v["predicate"], e, w, ctx.origin)),
+119 -1
View File
@@ -1009,7 +1009,19 @@ impl Game<'_> {
.ok_or_else(|| err("argument.predicate.unknown", &[&id]))?
};
let source = self.command_source.unwrap_or_else(|| self.player_source());
let view = source.entity.map(|t| self.entity_view_asked(t, &json));
let view = source.entity.map(|t| {
let scores = self.holder_name(t).map(|holder| {
self.score
.board
.holder_scores(&holder)
.into_iter()
.map(|(objective, value)| (objective.name.clone(), value))
.collect()
});
let mut view = self.entity_view_asked(t, &json);
view.scores = scores;
view
});
Ok(self.conditions_hold(&json, view.as_ref(), source.pos))
}
}
@@ -1139,3 +1151,109 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
}
#[cfg(test)]
mod entity_score_tests {
use super::*;
use crate::game::{Audio, Options, Progress, Settings, commands::Target, load_resources};
use horizon_world::nbt::Tag;
#[test]
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
fn named_scores_follow_the_registered_target_holder_and_mutations() {
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.build_registries();
game.set_allow_commands(true);
game.ensure_datapacks();
game.fns.store.predicates.insert(
"h270:min2".into(),
serde_json::json!({
"type": "minecraft:entity_scores", "entity": "this", "scores": {"pts": {"min": 2}}
}),
);
assert!(game.run_command("scoreboard objectives add pts dummy"));
let mut actors = Vec::new();
for (index, score) in [Some(0), Some(2), None].into_iter().enumerate() {
let uuid = [270, 1, 0, index as i32 + 1];
let tag = Tag::compound([
("id", Tag::String("minecraft:pig".into())),
("UUID", Tag::IntArray(uuid.to_vec())),
(
"Pos",
Tag::List(vec![
Tag::Double((3.5 + index as f64).to_bits()),
Tag::Double(200.0_f64.to_bits()),
Tag::Double(4.5_f64.to_bits()),
]),
),
("Health", Tag::Float(10.0_f32.to_bits())),
("NoAI", Tag::Byte(1)),
("NoGravity", Tag::Byte(1)),
]);
let id = game.load_mob(tag, None).unwrap();
assert_eq!(game.persist.uuids.get(&id), Some(&uuid));
let holder = game.holder_name(Target::Mob(id)).unwrap();
assert_eq!(holder, horizon_client::save::uuid_text(uuid));
if let Some(score) = score {
assert!(game.run_command(&format!("scoreboard players set {holder} pts {score}")));
}
assert_eq!(
game.score.board.score(&holder, "pts").map(|s| s.value),
score
);
actors.push((id, holder));
}
let time = game.world.game_time;
let matches = |game: &mut Game<'_>, id| {
let mut source = game.player_source();
source.entity = Some(Target::Mob(id));
source.pos = game.target_position(Target::Mob(id)).unwrap();
game.command_source = Some(source);
let result = game.check_predicate("h270:min2").unwrap();
game.command_source = None;
result
};
assert!(!matches(&mut game, actors[0].0));
assert!(matches(&mut game, actors[1].0));
assert!(!matches(&mut game, actors[2].0));
for (index, score, expected) in [(0, 3, true), (1, 1, false)] {
let (id, holder) = &actors[index];
assert!(game.run_command(&format!("scoreboard players set {holder} pts {score}")));
assert_eq!(
game.score.board.score(holder, "pts").map(|s| s.value),
Some(score)
);
assert_eq!(matches(&mut game, *id), expected);
}
assert!(!matches(&mut game, actors[2].0));
assert!(game.score.board.score(&actors[2].1, "pts").is_none());
assert_eq!(game.world.game_time, time);
}
}
+95
View File
@@ -0,0 +1,95 @@
# Named entity_scores predicates (H270)
Frozen source `41f822b659789780f58d32c63ddfb6887ea1b8bf`. Own actual Game/fresh pinned Java26.3, `tools/test_entity_scores_parity.py`. The declared complete-record contract has **212 records** with no masks/tolerances. Seven separately declared raw traces retain the five full pig bodies and exact absolute startup clocks.
The real fixture pack contains one `h270:min2` entity_scores condition with `entity:this` and `pts:{min:2}`, plus always/type admission predicates. Five actual UUID-bound pigs have scores0/1/2/3/missing. Both if/unless named action paths reset fresh result/success/branch sentinels before each call. Two scores mutate (0→2,3→1), and every score and condition is read again. UUID/type/Health/Pos/NoAI/NoGravity/load/floor/pack, direct scores selectors and relative-clock0 remain strict. The named target proof is not replaced by a direct selector.
Untouched: **180/212**; fixed: **212/212**, with the same program and byte-identical fresh Java reference. All admissions pass. The32 residuals below are four action/store records for both signs at each positive state; negative/missing-score states, actual score reads and direct selectors already match. The actual registered Game named-evaluator unit first proves the holder UUID and score state, then fails old at score2.
## Exact untouched residuals
| Label | Java expected | Untouched Game actual | Callback expected / actual |
|---|---|---|---|
| initial-2-if-named | `Set [h270] for branch to 1` | `` | `[[True, 1]]` / `[]` |
| initial-2-if-result | `result has 1 [h270]` | `result has -99 [h270]` | `[[True, 1]]` / `[[True, -99]]` |
| initial-2-if-success | `success has 1 [h270]` | `success has -99 [h270]` | `[[True, 1]]` / `[[True, -99]]` |
| initial-2-if-branch | `branch has 1 [h270]` | `branch has 0 [h270]` | `[[True, 1]]` / `[[True, 0]]` |
| initial-2-unless-named | `` | `Set [h270] for branch to 1` | `[]` / `[[True, 1]]` |
| initial-2-unless-result | `result has -99 [h270]` | `result has 1 [h270]` | `[[True, -99]]` / `[[True, 1]]` |
| initial-2-unless-success | `success has -99 [h270]` | `success has 1 [h270]` | `[[True, -99]]` / `[[True, 1]]` |
| initial-2-unless-branch | `branch has 0 [h270]` | `branch has 1 [h270]` | `[[True, 0]]` / `[[True, 1]]` |
| initial-3-if-named | `Set [h270] for branch to 1` | `` | `[[True, 1]]` / `[]` |
| initial-3-if-result | `result has 1 [h270]` | `result has -99 [h270]` | `[[True, 1]]` / `[[True, -99]]` |
| initial-3-if-success | `success has 1 [h270]` | `success has -99 [h270]` | `[[True, 1]]` / `[[True, -99]]` |
| initial-3-if-branch | `branch has 1 [h270]` | `branch has 0 [h270]` | `[[True, 1]]` / `[[True, 0]]` |
| initial-3-unless-named | `` | `Set [h270] for branch to 1` | `[]` / `[[True, 1]]` |
| initial-3-unless-result | `result has -99 [h270]` | `result has 1 [h270]` | `[[True, -99]]` / `[[True, 1]]` |
| initial-3-unless-success | `success has -99 [h270]` | `success has 1 [h270]` | `[[True, -99]]` / `[[True, 1]]` |
| initial-3-unless-branch | `branch has 0 [h270]` | `branch has 1 [h270]` | `[[True, 0]]` / `[[True, 1]]` |
| mutated-0-if-named | `Set [h270] for branch to 1` | `` | `[[True, 1]]` / `[]` |
| mutated-0-if-result | `result has 1 [h270]` | `result has -99 [h270]` | `[[True, 1]]` / `[[True, -99]]` |
| mutated-0-if-success | `success has 1 [h270]` | `success has -99 [h270]` | `[[True, 1]]` / `[[True, -99]]` |
| mutated-0-if-branch | `branch has 1 [h270]` | `branch has 0 [h270]` | `[[True, 1]]` / `[[True, 0]]` |
| mutated-0-unless-named | `` | `Set [h270] for branch to 1` | `[]` / `[[True, 1]]` |
| mutated-0-unless-result | `result has -99 [h270]` | `result has 1 [h270]` | `[[True, -99]]` / `[[True, 1]]` |
| mutated-0-unless-success | `success has -99 [h270]` | `success has 1 [h270]` | `[[True, -99]]` / `[[True, 1]]` |
| mutated-0-unless-branch | `branch has 0 [h270]` | `branch has 1 [h270]` | `[[True, 0]]` / `[[True, 1]]` |
| mutated-2-if-named | `Set [h270] for branch to 1` | `` | `[[True, 1]]` / `[]` |
| mutated-2-if-result | `result has 1 [h270]` | `result has -99 [h270]` | `[[True, 1]]` / `[[True, -99]]` |
| mutated-2-if-success | `success has 1 [h270]` | `success has -99 [h270]` | `[[True, 1]]` / `[[True, -99]]` |
| mutated-2-if-branch | `branch has 1 [h270]` | `branch has 0 [h270]` | `[[True, 1]]` / `[[True, 0]]` |
| mutated-2-unless-named | `` | `Set [h270] for branch to 1` | `[]` / `[[True, 1]]` |
| mutated-2-unless-result | `result has -99 [h270]` | `result has 1 [h270]` | `[[True, -99]]` / `[[True, 1]]` |
| mutated-2-unless-success | `success has -99 [h270]` | `success has 1 [h270]` | `[[True, -99]]` / `[[True, 1]]` |
| mutated-2-unless-branch | `branch has 0 [h270]` | `branch has 1 [h270]` | `[[True, 0]]` / `[[True, 1]]` |
## Source and bounded support
Pinned `EntityHasScoreCondition.test` obtains the selected entity and level scoreboard; `hasScore` rejects missing objectives/scores before applying its integer range. The Game named command context uses existing `holder_name` and `Scoreboard::holder_scores` to snapshot actual current objective values. A bound empty map means a real holder without scores; an unbound context remains None. The condition reuses the existing numeric range helper and fails closed for unsupported score providers or entity targets.
Only `entity:this` and constant integer ranges are supported here. The new optional entity-view field is filled by the existing named/inline command predicate adapter; other contexts remain unbound. No global loot engine or automatic-context projection is added. Pig runtime proof does not establish Player/Guest attribution or every target class. Existing holder lookup policy is reused without new identity fallbacks. Its Orb/Projectile branches return None and remain unbound; they require a separate measured holder-binding follow-up. Other supported holder variants have source review only, no runtime claim. Non-score predicate behavior and direct selectors remain unchanged.
## Reproduce and retain
```sh
python3 tools/test_entity_scores_parity.py --reference-only --work-dir /tmp/scores-java/run
python3 tools/test_entity_scores_parity.py --binary /absolute/path/to/horizon-client --reference /tmp/scores-java/run/vanilla.jsonl --work-dir /tmp/scores-game/run
```
Fresh directories are required. Complete scripts, JSONL feedback/styles/callbacks, references, declarations, raw bodies, immutable binaries and manifests are retained under `.local/h270/`. Draftv1 had an invalid legacy pig-admission key (Java registry rejection). Draftv2 omitted actual datapack-list initialization before native registry parsing. Both executable scripts/raws are retained rejected prerequisites, with no qualified H270 failure count. The canonical v3 adds the real pack admission; no parser fix.
- Program SHA256: `70ce1ca3a6577b3922e6913dd1a2e851792adc46336a7dde1601e11b57a2af6a`
- Fresh Java reference SHA256: `4c6f5e6cd1267747ef7863d8297f109c8ddd6b66c157ed0266c567592404745c`
- Untouched Game SHA256: `e45a7e8023e21869d10c1b010f3c3617bb092f62e3b941a94bbd986619d1e131`
## Full raw diagnostics
0/7 separately declared full-body/absolute-clock traces match. Exact unmatched text follows; all complete styles/callbacks stay retained. These are not canonical row masks and no full-body/absolute-clock equality is claimed.
```text
/execute store result score start h270 run time query gametime
JAVA: The game time is 313 tick(s)
GAME: The game time is 0 tick(s)
/data get entity 0000010e-0000-0001-0000-000000000001
JAVA: Pig has the following entity data: {NoGravity: 1b, AgeLocked: 0b, Brain: {memories: {}}, Invulnerable: 0b, FallFlying: 0b, ForcedAge: 0, sound_variant: "minecraft:classic", PortalCooldown: 0, AbsorptionAmount: 0.0f, InLove: 0, DeathTime: 0s, variant: "minecraft:temperate", PersistenceRequired: 0b, UUID: [I; 270, 1, 0, 1], Tags: ["h270_0"], Age: 0, Motion: [0.0d, 0.0d, 0.0d], Health: 10.0f, LeftHanded: 0b, fall_distance: 0.0d, Air: 300s, OnGround: 0b, NoAI: 1b, Rotation: [0.0f, 0.0f], current_impulse_context_reset_grace_time: 0, Pos: [3.5d, 200.0d, 4.5d], Fire: 0s, CanPickUpLoot: 0b, attributes: [], HurtTime: 0s}
GAME: Pig has the following entity data: {NoGravity: 1b, AgeLocked: 0b, Brain: {memories: {}}, Invulnerable: 0b, FallFlying: 0b, ForcedAge: 0, sound_variant: "minecraft:classic", PortalCooldown: 0, AbsorptionAmount: 0.0f, InLove: 0, DeathTime: 0s, variant: "minecraft:temperate", PersistenceRequired: 0b, UUID: [I; 270, 1, 0, 1], Tags: ["h270_0"], Age: 0, Motion: [0.0d, 0.0d, 0.0d], Health: 10.0f, LeftHanded: 0b, fall_distance: 0.0d, Air: 300s, OnGround: 0b, NoAI: 1b, Rotation: [0.0f, 0.0f], current_impulse_context_reset_grace_time: 0, Pos: [3.5d, 200.0d, 4.5d], Fire: 0s, CanPickUpLoot: 0b, attributes: [{id: "minecraft:movement_speed", base: 0.25d}], HurtTime: 0s}
/data get entity 0000010e-0000-0001-0000-000000000002
JAVA: Pig has the following entity data: {NoGravity: 1b, AgeLocked: 0b, Brain: {memories: {}}, Invulnerable: 0b, FallFlying: 0b, ForcedAge: 0, sound_variant: "minecraft:classic", PortalCooldown: 0, AbsorptionAmount: 0.0f, InLove: 0, DeathTime: 0s, variant: "minecraft:temperate", PersistenceRequired: 0b, UUID: [I; 270, 1, 0, 2], Tags: ["h270_1"], Age: 0, Motion: [0.0d, 0.0d, 0.0d], Health: 10.0f, LeftHanded: 0b, fall_distance: 0.0d, Air: 300s, OnGround: 0b, NoAI: 1b, Rotation: [0.0f, 0.0f], current_impulse_context_reset_grace_time: 0, Pos: [4.5d, 200.0d, 4.5d], Fire: 0s, CanPickUpLoot: 0b, attributes: [], HurtTime: 0s}
GAME: Pig has the following entity data: {NoGravity: 1b, AgeLocked: 0b, Brain: {memories: {}}, Invulnerable: 0b, FallFlying: 0b, ForcedAge: 0, sound_variant: "minecraft:classic", PortalCooldown: 0, AbsorptionAmount: 0.0f, InLove: 0, DeathTime: 0s, variant: "minecraft:temperate", PersistenceRequired: 0b, UUID: [I; 270, 1, 0, 2], Tags: ["h270_1"], Age: 0, Motion: [0.0d, 0.0d, 0.0d], Health: 10.0f, LeftHanded: 0b, fall_distance: 0.0d, Air: 300s, OnGround: 0b, NoAI: 1b, Rotation: [0.0f, 0.0f], current_impulse_context_reset_grace_time: 0, Pos: [4.5d, 200.0d, 4.5d], Fire: 0s, CanPickUpLoot: 0b, attributes: [{id: "minecraft:movement_speed", base: 0.25d}], HurtTime: 0s}
/data get entity 0000010e-0000-0001-0000-000000000003
JAVA: Pig has the following entity data: {NoGravity: 1b, AgeLocked: 0b, Brain: {memories: {}}, Invulnerable: 0b, FallFlying: 0b, ForcedAge: 0, sound_variant: "minecraft:classic", PortalCooldown: 0, AbsorptionAmount: 0.0f, InLove: 0, DeathTime: 0s, variant: "minecraft:temperate", PersistenceRequired: 0b, UUID: [I; 270, 1, 0, 3], Tags: ["h270_2"], Age: 0, Motion: [0.0d, 0.0d, 0.0d], Health: 10.0f, LeftHanded: 0b, fall_distance: 0.0d, Air: 300s, OnGround: 0b, NoAI: 1b, Rotation: [0.0f, 0.0f], current_impulse_context_reset_grace_time: 0, Pos: [5.5d, 200.0d, 4.5d], Fire: 0s, CanPickUpLoot: 0b, attributes: [], HurtTime: 0s}
GAME: Pig has the following entity data: {NoGravity: 1b, AgeLocked: 0b, Brain: {memories: {}}, Invulnerable: 0b, FallFlying: 0b, ForcedAge: 0, sound_variant: "minecraft:classic", PortalCooldown: 0, AbsorptionAmount: 0.0f, InLove: 0, DeathTime: 0s, variant: "minecraft:temperate", PersistenceRequired: 0b, UUID: [I; 270, 1, 0, 3], Tags: ["h270_2"], Age: 0, Motion: [0.0d, 0.0d, 0.0d], Health: 10.0f, LeftHanded: 0b, fall_distance: 0.0d, Air: 300s, OnGround: 0b, NoAI: 1b, Rotation: [0.0f, 0.0f], current_impulse_context_reset_grace_time: 0, Pos: [5.5d, 200.0d, 4.5d], Fire: 0s, CanPickUpLoot: 0b, attributes: [{id: "minecraft:movement_speed", base: 0.25d}], HurtTime: 0s}
/data get entity 0000010e-0000-0001-0000-000000000004
JAVA: Pig has the following entity data: {NoGravity: 1b, AgeLocked: 0b, Brain: {memories: {}}, Invulnerable: 0b, FallFlying: 0b, ForcedAge: 0, sound_variant: "minecraft:classic", PortalCooldown: 0, AbsorptionAmount: 0.0f, InLove: 0, DeathTime: 0s, variant: "minecraft:temperate", PersistenceRequired: 0b, UUID: [I; 270, 1, 0, 4], Tags: ["h270_3"], Age: 0, Motion: [0.0d, 0.0d, 0.0d], Health: 10.0f, LeftHanded: 0b, fall_distance: 0.0d, Air: 300s, OnGround: 0b, NoAI: 1b, Rotation: [0.0f, 0.0f], current_impulse_context_reset_grace_time: 0, Pos: [6.5d, 200.0d, 4.5d], Fire: 0s, CanPickUpLoot: 0b, attributes: [], HurtTime: 0s}
GAME: Pig has the following entity data: {NoGravity: 1b, AgeLocked: 0b, Brain: {memories: {}}, Invulnerable: 0b, FallFlying: 0b, ForcedAge: 0, sound_variant: "minecraft:classic", PortalCooldown: 0, AbsorptionAmount: 0.0f, InLove: 0, DeathTime: 0s, variant: "minecraft:temperate", PersistenceRequired: 0b, UUID: [I; 270, 1, 0, 4], Tags: ["h270_3"], Age: 0, Motion: [0.0d, 0.0d, 0.0d], Health: 10.0f, LeftHanded: 0b, fall_distance: 0.0d, Air: 300s, OnGround: 0b, NoAI: 1b, Rotation: [0.0f, 0.0f], current_impulse_context_reset_grace_time: 0, Pos: [6.5d, 200.0d, 4.5d], Fire: 0s, CanPickUpLoot: 0b, attributes: [{id: "minecraft:movement_speed", base: 0.25d}], HurtTime: 0s}
/data get entity 0000010e-0000-0001-0000-000000000005
JAVA: Pig has the following entity data: {NoGravity: 1b, AgeLocked: 0b, Brain: {memories: {}}, Invulnerable: 0b, FallFlying: 0b, ForcedAge: 0, sound_variant: "minecraft:classic", PortalCooldown: 0, AbsorptionAmount: 0.0f, InLove: 0, DeathTime: 0s, variant: "minecraft:temperate", PersistenceRequired: 0b, UUID: [I; 270, 1, 0, 5], Tags: ["h270_4"], Age: 0, Motion: [0.0d, 0.0d, 0.0d], Health: 10.0f, LeftHanded: 0b, fall_distance: 0.0d, Air: 300s, OnGround: 0b, NoAI: 1b, Rotation: [0.0f, 0.0f], current_impulse_context_reset_grace_time: 0, Pos: [7.5d, 200.0d, 4.5d], Fire: 0s, CanPickUpLoot: 0b, attributes: [], HurtTime: 0s}
GAME: Pig has the following entity data: {NoGravity: 1b, AgeLocked: 0b, Brain: {memories: {}}, Invulnerable: 0b, FallFlying: 0b, ForcedAge: 0, sound_variant: "minecraft:classic", PortalCooldown: 0, AbsorptionAmount: 0.0f, InLove: 0, DeathTime: 0s, variant: "minecraft:temperate", PersistenceRequired: 0b, UUID: [I; 270, 1, 0, 5], Tags: ["h270_4"], Age: 0, Motion: [0.0d, 0.0d, 0.0d], Health: 10.0f, LeftHanded: 0b, fall_distance: 0.0d, Air: 300s, OnGround: 0b, NoAI: 1b, Rotation: [0.0f, 0.0f], current_impulse_context_reset_grace_time: 0, Pos: [7.5d, 200.0d, 4.5d], Fire: 0s, CanPickUpLoot: 0b, attributes: [{id: "minecraft:movement_speed", base: 0.25d}], HurtTime: 0s}
/execute store result score end h270 run time query gametime
JAVA: The game time is 313 tick(s)
GAME: The game time is 0 tick(s)
```
## Local validation
The identical actual registered Game named-predicate test fails old at score2 after independent UUID/holder/score admission, then passes fixed1/1 including mutations and missing score. Required `pr_ready --quick --verbose --base 41f822b659789780f58d32c63ddfb6887ea1b8bf` ran once final: **3158 Rust tests passed**, plus rustfmt, clippy (including server features), doc tests and public-tree checks. Overall exit1 is the same three pre-existing macOS optional-release-archive Python subtests (masks0/1 `musl[@]`, mask2 `arm64[@]` unbound Bash arrays); 708 ran, one skipped. The382 broad affected harnesses were listed for CI by `--quick`; the focused actual Game pair was executed locally.
+2 -2
View File
@@ -17,8 +17,8 @@ CompositeLootItemCondition.Builder skip:java builder (addTerm)
ConditionUserBuilder skip:java builder interface for loot conditions
DamageSourceCondition.test port crates/horizon-world/src/loot.rs::Context::condition damage_source_properties arm (mob death loot)
EnchantmentActiveCheck.test inline crates/horizon-client/src/game/enchant_effects.rs::Game::tick_soul_speed the soul speed modifiers switch on and off here; the ENCHANTMENT_ACTIVE flag itself is not read as a condition
EntityHasScoreCondition.hasScore missing `loot.rs::condition` has no `entity_scores` arm, and no vanilla loot table asks for one
EntityHasScoreCondition.test missing same as `hasScore`: the condition dispatch (`inverted`, `all_of`, `any_of`, `entity_properties`, `killed_by_player`, the chance and location checks) has no scoreboard arm
EntityHasScoreCondition.hasScore missing named Game predicates support this/constant ranges in advancements::conditions; the separate global loot.rs::condition engine and score providers remain unsupported
EntityHasScoreCondition.test missing same global-loot gap as hasScore; named Game this/constant ranges use the actual holder snapshot via Game::check_predicate
EntityHasScoreCondition.validate skip:data datapack validation
EnvironmentAttributeCheck.createCodec skip:java codec
EnvironmentAttributeCheck.test missing no `environment_attribute_check` arm in `loot.rs::condition`; no vanilla data uses it
1 SetStewEffectFunction.Builder skip:java builder for the stew effect list; Horizon keeps the effects in loot.rs Context::run (set_stew_effect)
17 ConditionUserBuilder skip:java builder interface for loot conditions
18 DamageSourceCondition.test port crates/horizon-world/src/loot.rs::Context::condition damage_source_properties arm (mob death loot)
19 EnchantmentActiveCheck.test inline crates/horizon-client/src/game/enchant_effects.rs::Game::tick_soul_speed the soul speed modifiers switch on and off here; the ENCHANTMENT_ACTIVE flag itself is not read as a condition
20 EntityHasScoreCondition.hasScore missing `loot.rs::condition` has no `entity_scores` arm, and no vanilla loot table asks for one named Game predicates support this/constant ranges in advancements::conditions; the separate global loot.rs::condition engine and score providers remain unsupported
21 EntityHasScoreCondition.test missing same as `hasScore`: the condition dispatch (`inverted`, `all_of`, `any_of`, `entity_properties`, `killed_by_player`, the chance and location checks) has no scoreboard arm same global-loot gap as hasScore; named Game this/constant ranges use the actual holder snapshot via Game::check_predicate
22 EntityHasScoreCondition.validate skip:data datapack validation
23 EnvironmentAttributeCheck.createCodec skip:java codec
24 EnvironmentAttributeCheck.test missing no `environment_attribute_check` arm in `loot.rs::condition`; no vanilla data uses it
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Named entity_scores predicates against actual target holders."""
import hashlib
import json
from pathlib import Path
import sys
import tempfile
import test_command_exec_parity as driver
CASES = [0, 1, 2, 3, None]
PREDICATES = {
'h270:min2': {'type': 'minecraft:entity_scores', 'entity': 'this', 'scores': {'pts': {'min': 2}}},
'h270:always': {'type': 'minecraft:all_of', 'terms': []},
'h270:pig': {'type': 'minecraft:entity_properties', 'entity': 'this', 'predicate': {'minecraft:entity_type': 'minecraft:pig'}},
}
def uuid_string(words):
import uuid
return str(uuid.UUID(hex=''.join(f'{int(n)&0xffffffff:08x}' for n in words.split(','))))
def program():
ops = []
def cmd(text, label, result=None, admission=True, diagnostic=False):
op = dict(op='cmd', cmd=text, label=label, admission=admission)
if result is not None: op['expect_result'] = result
if diagnostic: op.update(record=False, trace=True)
ops.append(op)
cmd('/tick freeze', 'freeze')
cmd('/gamerule spawn_mobs false', 'no-spawn')
cmd('/gamerule advance_time false', 'no-daylight')
cmd('/forceload add 0 0', 'force', 1)
ops.append(dict(op='settle', required_chunks=[[0, 0]]))
cmd('/fill 0 199 0 15 199 15 stone', 'floor', 256)
cmd('/tp @s 14.5 200 14.5 0 0', 'player-position', 1)
cmd('/gamemode creative @s', 'player-mode')
cmd('/execute if loaded 4 200 4', 'loaded', 1)
cmd('/execute if block 4 199 4 stone', 'support', 1)
cmd('/execute if entity @s[type=player,name=Player,nbt={Health:20f,playerGameType:1,Pos:[14.5d,200d,14.5d]}]', 'player', 1)
cmd('/scoreboard objectives add h270 dummy', 'objective')
cmd('/scoreboard objectives add pts dummy', 'points-objective')
cmd('/execute store result score start h270 run time query gametime', 'clock-start', diagnostic=True)
cmd('/datapack list enabled', 'pack-list')
cmd('/execute if predicate h270:always', 'named-pack-always', 1)
for index,score in enumerate(CASES):
words=f'270,1,0,{index+1}';uuid='[I;'+words+']';target=uuid_string(words);x=3.5+index
cmd(f'/summon pig {x} 200 4.5 {{UUID:{uuid},Tags:["h270_{index}"],Health:10f,NoAI:1b,NoGravity:1b}}', f'actor{index}-summon', 1)
cmd(f'/execute if entity @e[type=pig,nbt={{UUID:{uuid},Health:10f,NoAI:1b,NoGravity:1b,Pos:[{x}d,200d,4.5d]}}]', f'actor{index}-identity', 1)
cmd(f'/data get entity {target} UUID', f'actor{index}-uuid')
cmd(f'/data get entity {target}', f'actor{index}-body', diagnostic=True)
cmd(f'/execute as {target} if predicate h270:pig run scoreboard players set pack_probe h270 1', f'actor{index}-named-pig', 1)
if score is not None:
cmd(f'/scoreboard players set {target} pts {score}', f'actor{index}-score-set', score)
for phase in ['initial', 'mutated']:
if phase=='mutated':
for index,score in [(0,2),(3,1)]:
cmd(f'/scoreboard players set {uuid_string(f"270,1,0,{index+1}")} pts {score}', f'mutate{index}', score)
for index in range(5):
words=f'270,1,0,{index+1}';target=uuid_string(words)
prefix=f'{phase}-{index}'
cmd(f'/scoreboard players get {target} pts', prefix+'-score')
for sign in ['if','unless']:
cmd(f'/execute {sign} entity @e[type=pig,nbt={{UUID:[I;{words}]}},scores={{pts=2..}}]', prefix+'-direct-'+sign)
cmd('/scoreboard players set result h270 -99', prefix+'-'+sign+'-reset-result', -99)
cmd('/scoreboard players set success h270 -99', prefix+'-'+sign+'-reset-success', -99)
cmd('/scoreboard players set branch h270 0', prefix+'-'+sign+'-reset-branch', 0)
cmd(f'/execute store result score result h270 store success score success h270 as {target} {sign} predicate h270:min2 run scoreboard players set branch h270 1', prefix+'-'+sign+'-named', admission=False)
for key in ['result','success','branch']:
cmd(f'/scoreboard players get {key} h270', prefix+'-'+sign+'-'+key, admission=False)
cmd('/execute store result score end h270 run time query gametime', 'clock-end', diagnostic=True)
cmd('/scoreboard players operation end h270 -= start h270', 'clock-delta-operation', 0)
cmd('/scoreboard players get end h270', 'clock-delta', 0)
return ops
def main():
reference = None
if '--reference' in sys.argv:
i = sys.argv.index('--reference'); reference = Path(sys.argv[i+1]).resolve()
del sys.argv[i:i+2]
if '--work-dir' not in sys.argv:
parent = Path(tempfile.mkdtemp(prefix='entity-scores-', dir=driver.lab.LOCAL))
sys.argv += ['--work-dir', str(parent / 'run')]
build = Path(sys.argv[sys.argv.index('--work-dir')+1]).resolve()
assert not build.exists(), 'preserve attempts; choose a fresh directory'
build.mkdir(parents=True)
ops = program(); recorded = [op for op in ops if op['op']=='cmd' and op.get('record', True)]
(build/'declaration.json').write_text(json.dumps(dict(
script_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), records=len(recorded),
cases=CASES, labels=[op['label'] for op in recorded],
raw_diagnostics=[op['label'] for op in ops if op.get('trace')],
comparison='complete records, no masks/tolerances; bodies and absolute clocks are separate raw traces'), indent=2)+'\n')
expected_bytes = reference.read_bytes() if reference else None
driver.corpus_module.predicate_selector_fixtures = lambda names: PREDICATES
driver.corpus_module.groups = lambda: [('Named entity scores', ops)]
sys.argv.append('--allow-gaps'); driver.main()
java_bytes = (build/'vanilla.jsonl').read_bytes()
if expected_bytes is not None:
assert java_bytes==expected_bytes and reference.read_bytes()==expected_bytes, 'fresh Java reference changed'
java = [json.loads(s) for s in java_bytes.splitlines()]
if '--reference-only' in sys.argv: return
native = [json.loads(s) for s in (build/'rust.jsonl').read_bytes().splitlines()]
assert len(recorded)==len(java)==len(native)
residuals = [dict(label=op['label'], expected=a, actual=b) for op,a,b in zip(recorded,java,native) if a!=b]
report = dict(records=len(recorded), passed=len(recorded)-len(residuals), residuals=residuals,
reference_sha256=hashlib.sha256(java_bytes).hexdigest())
(build/'strict-report.json').write_text(json.dumps(report, indent=2)+'\n')
for op,a,b in zip(recorded,java,native):
if op['admission']: assert a==b, ('admission',op,a,b)
print(f'Strict records: {report["passed"]}/{report["records"]}')
if residuals: raise SystemExit(1)
if __name__ == '__main__':
main()