mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Evaluate named entity_scores predicates for bound holders
Co-Authored-By: Codex <[email protected]>
This commit is contained in:
1 parent
41f822b659
commit
5201202a30
5 files changed
+360
-3
No files matched your search
@@ -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)),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
@@ -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()
|
||||
Reference in new issue
Block a user