Restore NoAI Dolphin air after its superclass tick

Use the existing species maximum after shared Mob tick work, preserving the normal underwater countdown. Cover the real full-tick path and actual Game commands against fresh Java.

Co-Authored-By: Codex <[email protected]>
This commit is contained in:
Florian ERNSTandCodex committed 2026-11-06 07:16:46 +01:00
1 parent 29825eb774
commit 3aaff8da93
3 files changed
+369

No files matched your search

+75
View File
@@ -766,6 +766,10 @@ pub fn tick(
super::panda::tick(m, w, events);
}
aquatic::school_tick(m, w, level_random);
// Dolphin.tick: after super.tick, NoAI restores the species maximum.
if m.no_ai && m.species() == Species::Dolphin {
m.air = m.max_air_supply();
}
true
}
/// `LivingEntity.aiStep` -> `Entity.applyEffectsFromBlocks` for the blocks
@@ -5053,3 +5057,74 @@ mod flight_effect_fall_tests {
assert!(ran, "prepared resources must run the integration fixture");
}
}
#[cfg(test)]
mod dolphin_noai_air_tests {
use super::*;
use std::sync::Arc;
fn check(water: bool, no_ai: bool, initial: i32, expected: [i32; 2]) {
let build = |world: &mut crate::world::World,
registry: &horizon_world::registry::BlockRegistry| {
world
.chunks
.insert((0, 0), Arc::new(crate::chunk::Chunk::empty(0, 0)));
if water {
let water = registry.default_state("minecraft:water").unwrap();
for x in 0..=4 {
for z in 0..=4 {
for y in 0..=8 {
world.set(x, y, z, water).unwrap();
}
}
}
}
};
let ran = crate::mobs::world::with_built_test_world(&[], build, |w, types| {
let kind = Arc::new(types.types["minecraft:dolphin"].clone());
let mut mob = Mob::new(1, kind, Vec3::new(2.5, 3.0, 2.5), 0.0);
mob.no_ai = no_ai;
mob.no_gravity = true;
mob.persistent = true;
mob.attrs.speed = 0.0;
mob.air = initial;
let mut random = LegacyRandom::new(0);
let mut events = Vec::new();
assert_eq!(mob.health, 10.0);
assert_eq!(mob.max_air_supply(), 4800);
for (index, expected) in expected.into_iter().enumerate() {
assert!(tick(&mut mob, w, &mut random, &mut events));
assert_eq!(mob.age, (index + 1) as u32);
assert_eq!(mob.fluids.eye_in_water, water);
assert_eq!(mob.health, 10.0);
assert_eq!(mob.no_ai, no_ai);
assert_eq!(
mob.air,
expected,
"water={water}, NoAI={no_ai}, initial={initial}, tick={}",
index + 1
);
}
});
if !ran {
eprintln!("Prepared mob data unavailable; Dolphin full-tick coverage did not run");
}
}
/// `Dolphin.tick` restores max air after super.tick for a NoAI dolphin,
/// even when that superclass tick refilled or consumed loaded air.
#[test]
fn noai_dolphins_restore_air_after_the_full_mob_tick() {
for water in [false, true] {
for initial in [4800, 100] {
check(water, true, initial, [4800, 4800]);
}
}
}
/// The NoAI exception must not suppress normal underwater consumption.
#[test]
fn normal_dolphins_keep_their_underwater_countdown() {
check(true, false, 4800, [4799, 4798]);
}
}
+157
View File
@@ -0,0 +1,157 @@
# NoAI Dolphin post-superclass air reset
Pinned 26.3 `Dolphin.tick` calls the superclass tick, then checks NoAI. A NoAI
Dolphin sets air to `getMaxAirSupply()` and returns; that accessor returns 4800.
The fix adds exactly that reset after the shared full-Mob tick work, using the
existing `Mob::max_air_supply`. It does not call species AI or change normal
Dolphin countdown, other species, generic breathing hooks or damage policy.
The port ledger and pinned bytecode were read independently. Closed PR9459
contained a proposed NoAI reset, but merged successor PR9658 omitted it while
fixing other Dolphin air/tag behavior. No active duplicate owner was found.
## Frozen actual Game/Java fixture
`tools/test_noai_dolphin_air_parity.py` uses the existing command harness, a
real Game and a fresh pinned Java server. The source base is
`a589a3dd9be04fa7f17667c0175711aaa92e1147`.
The unchanged executable is our preserved H180 baseline on exactly that same
current base, verified against its SHA256/provenance and original build log. Its
H180 unit-only draft was compiled out of the Game executable; no H180 production
change is imported. H072 supplies its own new fixture and fresh Java reference.
The fixed executable is built from this H072 worktree on the same frozen base.
Five actual UUID-bound, persistent, Health 10 Dolphins cover:
| Subject | NoAI | Medium | Air at 0/1/2 ticks |
|---|---|---|---|
| air_full | true | air | 4800/4800/4800 |
| air_low | true | air | 100/4800/4800 |
| water_full | true | source water | 4800/4800/4800 |
| water_low | true | source water | 100/4800/4800 |
| normal_water | false | source water | 4800/4799/4798 |
Fresh Java independently validates these expectations before the pair. The
loaded floor, glass pool, source-water/air samples at body and eye levels,
actual creative owner, actor UUID/type/tag/Health/NoAI/NoGravity/Pos, fresh
result/success sentinels and exactly two actual world ticks are guarded.
NoGravity and a real zero movement-speed attribute isolate stationary actors;
normal AI still runs. No observer or fabricated air state is introduced.
The predeclared 182-record canonical program compares complete raw feedback/styles,
callbacks, saved Short air values, result/success stores, typed NBT predicates,
positions and relative clocks. All 15 full actor bodies and two absolute startup
clock replies remain trace diagnostics, also executable in the 199-record full
program. These categories were declared before any fresh captures. No tolerance,
row mutation or comparison mask is used.
Program SHA256:
- canonical: `24630b6345aa82e2dd9c138d9e46bc002b4ef9cfc0f2785a40384f02aedf5e00`
- full: `0dd0ffca384f7dbe56a89d5730c2fe890c12849cd4f6ae5d8369ebb8af2bd571`
All raw replies, complete bodies, references, saved worlds, scripts, executable
hashes and manifests remain locally under `.local/h072`; no coordinator payload
or game resource is committed. Unknown phase times stay null.
## Actual full-Mob tick regression
`cargo test -p horizon-client-lib --lib dolphin_noai_air_tests` uses the existing
prepared-resource world factory with actual water or air and calls the real full
`mobs::ai::tick`. It checks both media, loaded 4800/100, actual tick age, eye-water
state, unchanged Health and NoAI, plus the normal water countdown. Before the fix,
the NoAI test fails at 104 versus 4800 and the normal-AI control passes. This is
not a copied air model or a direct call to the reset. Prepared resources are
required for this test to execute; absence is reported, not counted as runtime
coverage. The own local old failure proves the real fixture ran.
The initial unit draft had a u64 tick-age assertion, corrected to the actual u32
model type before compilation reached it; that draft is retained, with no runtime
claim. The gameplay change does not touch saved Air conversion, normal refill,
Nautilus, water-breathing effects, goals, particles, sounds, RNG or lifecycle.
```sh
python3 tools/test_noai_dolphin_air_parity.py --binary target/fast/horizon-client --work-dir .local/noai-dolphin-air
python3 tools/test_noai_dolphin_air_parity.py --binary target/fast/horizon-client --work-dir .local/noai-dolphin-air-full --full-diagnostic
```
## Paired results and complete diagnostic residuals
- Canonical: unchanged 164/182 → fixed 182/182. All 18 original differences are six NoAI air values independently checked by raw Short reply, stored result and typed predicate.
- Full diagnostic: unchanged 165/199 → fixed 183/199. No 199/199 claim.
Every remaining full-program difference against the immutable common reference:
| Label | Expected Java | Actual Game |
|---|---|---|
| clock-start | `[[True, 313]]` | `[[True, 0]]` |
| air_full-0-body | `NoAI before GotFish` | `GotFish before NoAI` |
| air_low-0-body | `NoAI before GotFish` | `GotFish before NoAI` |
| water_full-0-body | `NoAI before GotFish` | `GotFish before NoAI` |
| water_low-0-body | `NoAI before GotFish` | `GotFish before NoAI` |
| air_full-1-body | `NoAI before GotFish` | `GotFish before NoAI` |
| air_low-1-body | `NoAI before GotFish` | `GotFish before NoAI` |
| water_full-1-body | `NoAI before GotFish; oxygen_bonus base0.0d` | `GotFish before NoAI; oxygen_bonus absent` |
| water_low-1-body | `NoAI before GotFish; oxygen_bonus base0.0d` | `GotFish before NoAI; oxygen_bonus absent` |
| normal_water-1-body | `Rotation: [350.0f, 5.0f]; oxygen_bonus base0.0d` | `Rotation: [0.0f, 0.0f]; oxygen_bonus absent` |
| air_full-2-body | `NoAI before GotFish` | `GotFish before NoAI` |
| air_low-2-body | `NoAI before GotFish` | `GotFish before NoAI` |
| water_full-2-body | `NoAI before GotFish; oxygen_bonus base0.0d` | `GotFish before NoAI; oxygen_bonus absent` |
| water_low-2-body | `NoAI before GotFish; oxygen_bonus base0.0d` | `GotFish before NoAI; oxygen_bonus absent` |
| normal_water-2-body | `Rotation: [340.0f, 10.0f]; oxygen_bonus base0.0d` | `Rotation: [0.0f, 0.0f]; oxygen_bonus absent` |
| clock-end | `[[True, 315]]` | `[[True, 2]]` |
All twelve NoAI-body rows retain existing compound presentation ordering
(H191). Four of those water rows also retain the pre-existing zero-valued
`oxygen_bonus` attribute-materialization difference. The two normal-body rows
retain that attribute difference and the existing rotation differences. No
attribute materialization, formatter or normal Dolphin logic is changed. Every body
callback remains `[true,1]`; full text, styles, coordinates and callbacks remain
raw and executable. Raw absolute clocks are not replaced or normalized, while
both observed relative deltas are exactly two ticks.
## Required checks and limits
`tools/pr_ready.py --quick --base a589a3dd9be04fa7f17667c0175711aaa92e1147`
ran once on the final Rust and harness files:1962 Rust passes,63 ignored and one
pre-existing `entities::drop_motion_tests::drops_match_vanillas_motion_bit_for_bit`
failure. At yaw37.5/pitch-21.25, Z bits are4597448471913462162 versus expected
4597448471913462163. Our own untouched-production a171 executable already
reproduced that exact failure; entities.rs and jmath are unchanged from that
baseline. Its original log/provenance is retained, not a new H072 baseline run.
Python reports696 tests, four failures and one skip: the unchanged installer
signature fixture lacks `horizon-macos-arm64.zip`, and three optional-archive
cases fail under Bash3.2 with `musl[@]`/`arm64[@]: unbound variable`. The prior
same-base narrow installer diagnostic is retained. No installer or drop-motion
repair is included, and the overall self-check is not green.
Formatting, Clippy with warnings denied, doctests and public-tree checks pass;
Cargo.lock is unchanged. The ledger reports no new gaps, with a long-uncited
advisory on the test fixture helper (it is not itself a vanilla port). The assigned
actual Game/Java proof was run separately from quick mode's broad-harness skip.
Only evidence notes were added after checks; Rust and harness files are unchanged.
The latest source checkpoint has no relevant delta from the frozen base.
Disk stayed above the5GiB hard guard with planned combined-link headroom,
jobs2/incremental0 and one heavy build. Only verified inactive OWN artifacts were
archived; proofs and other owners' artifacts remain preserved. This does not
establish drowning, all air policies, effect behavior, particles, packets,
rendering, lifecycle or all-species parity.
## Publication source refresh
Main advanced after the paired proof and required checks to
`29825eb7747028cad8363b72d023fe8bf69b672a`. The own branch was refreshed without
an upstream merge. The conflict was only the shared append location of the new
flight-effect test module and this Dolphin test module; both complete modules
are preserved. The four-line Dolphin patch is unchanged.
Source review retains the new fall-distance reset (no active effects in this
fixture), merged age-aware HappyGhast voices, conversion tags, model-layer and
isolated-orb-fixture changes. None changes Dolphin air or this fixture's selected
commands/inputs. The original Game pair and full required-check report remain
qualified to a589. Affected formatting, real full-Mob tick tests and scoped Clippy
are checked again on29825; this is not a new latest-base Game pair or a claim that
all latest-base tests were rerun.
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""NoAI Dolphin air after actual superclass ticks, in air and water.
Complete scalar/action records stay strict. Full bodies and absolute clocks are
predeclared executable diagnostics; the actual two-tick delta remains strict.
"""
import hashlib
import json
from pathlib import Path
import sys
import test_command_exec_parity as driver
def subjects():
return [
(1, 'air_full', False, True, 4800, [3.5, 201.0, 3.5]),
(2, 'air_low', False, True, 100, [6.5, 201.0, 3.5]),
(3, 'water_full', True, True, 4800, [10.5, 201.0, 10.5]),
(4, 'water_low', True, True, 100, [12.5, 201.0, 10.5]),
(5, 'normal_water', True, False, 4800, [10.5, 201.0, 13.5]),
]
def program(full=False):
ops = []
def cmd(text, label, expected=None, admission=True, diagnostic=False):
op = dict(op='cmd', cmd=text, label=label, admission=admission)
if expected is not None:
op['expect_result'] = expected
if diagnostic and not full:
op.update(record=False, trace=True)
ops.append(op)
def tick(n, label):
cmd(f'/tick step {n}', label + '-request')
ops.append(dict(op='tick', n=n))
cmd('/tick freeze', 'freeze')
for key in ['spawn_mobs', 'advance_time', 'mob_griefing']:
cmd('/gamerule ' + key + ' false', key)
cmd('/gamerule random_tick_speed 0', 'random-ticks')
cmd('/forceload add 0 0 15 15', 'force', 1)
ops.append(dict(op='settle', required_chunks=[[0, 0]]))
cmd('/fill 0 199 0 15 199 15 stone', 'floor', 256)
cmd('/fill 0 200 0 15 205 15 air', 'clear')
cmd('/fill 8 200 8 15 204 15 glass', 'pool-shell', 320)
cmd('/fill 9 200 9 14 204 14 water', 'pool-water', 180)
cmd('/tp @s 1.5 200 1.5 0 0', 'owner-position', 1)
cmd('/gamemode creative @s', 'owner-mode')
cmd('/execute if entity @s[type=player,name=Player]', 'owner-type', 1)
for key, value in [('UUID', 4), ('Health', 20), ('playerGameType', 1), ('Pos', 3)]:
cmd('/data get entity @s ' + key, 'owner-' + key, value)
cmd('/scoreboard objectives add h072 dummy', 'objective')
cmd('/execute store result score start h072 run time query gametime', 'clock-start', diagnostic=True)
for i, label, water, no_ai, initial, pos in subjects():
xyz = ' '.join(map(str, pos))
body = '{UUID:[I;72,0,0,%d],Tags:["h072_%s"],Health:10f,NoAI:%db,NoGravity:1b,PersistenceRequired:1b,Air:%ds,attributes:[{id:"minecraft:movement_speed",base:0.0d}]}' % (i, label, no_ai, initial)
cmd(f'/summon dolphin {xyz} {body}', label + '-summon')
cmd(f'/execute if loaded {int(pos[0])} 201 {int(pos[2])}', label + '-loaded', 1)
cmd(f'/execute if block {int(pos[0])} 199 {int(pos[2])} stone', label + '-floor', 1)
for y in [201, 202]:
block = 'water[level=0]' if water else 'air'
cmd(f'/execute if block {int(pos[0])} {y} {int(pos[2])} {block}', f'{label}-medium-{y}', 1)
for elapsed in [0, 1, 2]:
if elapsed:
tick(1, f'tick{elapsed}')
for i, label, water, no_ai, initial, pos in subjects():
uid = f'00000048-0000-0000-0000-{i:012x}'
phase = f'{label}-{elapsed}'
body = '{UUID:[I;72,0,0,%d],Tags:["h072_%s"],Health:10f,NoGravity:1b,PersistenceRequired:1b}' % (i, label)
cmd(f'/execute if entity @e[type=dolphin,nbt={body}]', phase + '-identity', 1)
cmd(f'/execute {"if" if no_ai else "unless"} entity @e[type=dolphin,nbt={{UUID:[I;72,0,0,{i}],NoAI:1b}}]', phase + '-ai', 1)
cmd(f'/data get entity {uid} Pos', phase + '-position')
for field in ['result', 'success']:
cmd(f'/scoreboard players set {field} h072 -99', phase + '-reset-' + field, -99)
value = initial if elapsed == 0 else (4800 if no_ai else initial - elapsed)
cmd(f'/execute store result score result h072 store success score success h072 run data get entity {uid} Air', phase + '-air-read', admission=False)
cmd('/scoreboard players get result h072', phase + '-air-result', admission=False)
cmd('/scoreboard players get success h072', phase + '-air-success', 1)
cmd(f'/execute if entity @e[type=dolphin,nbt={{UUID:[I;72,0,0,{i}],Air:{value}s}}]', phase + '-air-typed', admission=False)
cmd(f'/data get entity {uid}', phase + '-body', diagnostic=True)
cmd('/execute store result score end h072 run time query gametime', 'clock-end', diagnostic=True)
cmd('/scoreboard players operation end h072 -= start h072', 'clock-delta-operation', 2)
cmd('/scoreboard players get end h072', 'clock-delta', 2)
return ops
def main():
full = '--full-diagnostic' in sys.argv
if full:
sys.argv.remove('--full-diagnostic')
reference = None
if '--reference' in sys.argv:
i = sys.argv.index('--reference')
reference = Path(sys.argv[i + 1]).resolve().read_bytes()
del sys.argv[i:i + 2]
if '--binary' not in sys.argv:
raise SystemExit('--binary is required (also for a reference-only run)')
build = Path(sys.argv[sys.argv.index('--work-dir') + 1]).resolve()
ops = program(full)
driver.corpus_module.groups = lambda: [('NoAI dolphin air', ops)]
sys.argv.append('--allow-gaps')
driver.main()
recorded = [o for o in ops if o['op'] == 'cmd' and o.get('record', True)]
java_bytes = (build / 'vanilla.jsonl').read_bytes()
java = [json.loads(s) for s in java_bytes.splitlines()]
rows = {o['label']: r for o, r in zip(recorded, java)}
assert len(recorded) == len(java)
for elapsed in [0, 1, 2]:
for i, label, water, no_ai, initial, pos in subjects():
value = initial if elapsed == 0 else (4800 if no_ai else initial - elapsed)
assert rows[f'{label}-{elapsed}-air-read']['results'] == [[True, value]]
assert rows[f'{label}-{elapsed}-air-typed']['results'] == [[True, 1]]
if '--reference-only' in sys.argv:
return
if reference and not full:
assert reference == java_bytes, 'fresh reference changed'
comparison = reference or java_bytes
(build / 'comparison-reference.jsonl').write_bytes(comparison)
java = [json.loads(s) for s in comparison.splitlines()]
native = [json.loads(s) for s in (build / 'rust.jsonl').read_bytes().splitlines()]
assert len(recorded) == len(java) == len(native)
differences = [dict(label=o['label'], expected=a, actual=b) for o, a, b in zip(recorded, java, native) if a != b]
report = dict(records=len(native), passed=len(native)-len(differences), differences=differences,
reference_sha256=hashlib.sha256(comparison).hexdigest(), comparison='complete records; no masks or tolerances')
(build / 'strict-report.json').write_text(json.dumps(report, indent=2)+'\n')
for o, a, b in zip(recorded, java, native):
if o['admission'] and not o['label'].endswith('-body') and o['label'] not in ['clock-start', 'clock-end']:
assert a == b, ('admission', o, a, b)
print(f'Strict raw records: {report["passed"]}/{report["records"]}')
if differences:
raise SystemExit(1)
if __name__ == '__main__':
main()