mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
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:
1 parent
29825eb774
commit
3aaff8da93
3 files changed
+369
No files matched your search
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user