Keep glow squid dark countdown running with NoAI

Co-Authored-By: Codex <[email protected]>
This commit is contained in:
Florian ERNSTandCodex committed 2026-11-05 22:00:30 +01:00
1 parent 3d33b0299d
commit 0e05b77d95
6 files changed
+336 -4

No files matched your search

+3
View File
@@ -719,6 +719,9 @@ pub fn tick(
// Pufferfish.aiStep's mob contacts run even when serverAiStep is skipped.
aquatic::sting_touching_mobs(m, w, events);
}
if m.species() == Species::GlowSquid {
aquatic::glow_squid_dark_tick(m);
}
} else {
ai_step(m, w, level_random, events);
}
+13 -4
View File
@@ -463,16 +463,22 @@ pub fn hurt_server(m: &mut Mob) {
set_dark_ticks(m, 100);
}
/// `GlowSquid.aiStep`: its positive dark timer runs after the parent step,
/// including when NoAI skips server AI. This upkeep consumes no random draws.
pub(super) fn glow_squid_dark_tick(m: &mut Mob) {
let ticks = dark_ticks(m);
if ticks > 0 {
set_dark_ticks(m, ticks - 1);
}
}
/// `GlowSquid.aiStep` after `super.aiStep()`: the dark timer counts down, and
/// a `glow` particle appears at `getRandomX(0.6)`, `getRandomY()`,
/// `getRandomZ(0.6)` (drawn in that order). A window that simulates the world
/// and a client of a server both draw it, so the server's own `ai_step` pushes
/// the event as well (as the nautilus bubbles do).
fn glow_squid_ai_step(m: &mut Mob, events: &mut Vec<MobEvent>) {
let ticks = dark_ticks(m);
if ticks > 0 {
set_dark_ticks(m, ticks - 1);
}
glow_squid_dark_tick(m);
let (w, h) = (f64::from(m.width()), f64::from(m.height()));
let r = &mut m.random;
let x = m.position.x + w * ((2.0 * r.next_double() - 1.0) * 0.6);
@@ -1605,3 +1611,6 @@ mod pufferfish_sting_tests {
});
}
}
#[cfg(test)]
mod countdown_tests;
@@ -0,0 +1,69 @@
use super::*;
use horizon_core::random::LegacyRandom;
use std::sync::Arc;
#[test]
#[ignore = "requires prepared vanilla resources in .local/horizon-client"]
fn mob_tick_keeps_glow_squid_countdowns_with_no_ai() {
let ran = crate::mobs::world::with_built_test_world(
&[],
|world, registry| {
world
.chunks
.insert((0, 0), Arc::new(crate::chunk::Chunk::empty(0, 0)));
let water = registry.default_state("minecraft:water").unwrap();
for x in 0..16 {
for y in 60..69 {
for z in 0..16 {
world.set(x, y, z, water).unwrap();
}
}
}
},
|world, types| {
// Normal and negative controls run before the old NoAI positive failure.
for no_ai in [false, true] {
for initial in [-2, 40] {
let kind = Arc::new(types.types["minecraft:glow_squid"].clone());
let mut squid = Mob::new(1, kind, Vec3::new(8.5, 64.0, 8.5), 0.0);
squid.no_ai = no_ai;
squid.no_gravity = true;
squid.persistent = true;
set_dark_ticks(&mut squid, initial);
let mut random = LegacyRandom::new(0);
let mut events = Vec::new();
for elapsed in 1..=10 {
assert!(squid.tick(world, &mut random, &mut events));
assert_eq!(squid.health, 10.0);
let expected = if initial > 0 {
initial - elapsed
} else {
initial
};
assert_eq!(
dark_ticks(&squid),
expected,
"NoAI={no_ai}, initial={initial}, elapsed={elapsed}"
);
}
let glows = events
.iter()
.filter(|event| {
matches!(event,
MobEvent::ParticleAt { kind, .. } if *kind == ParticleType::Glow.name())
})
.count();
assert_eq!(
glows,
if no_ai { 0 } else { 10 },
"the timer-only NoAI path must preserve existing particle dispatch"
);
}
}
},
);
assert!(
ran,
"prepared vanilla resources are required; do not silently skip this regression"
);
}
+122
View File
@@ -0,0 +1,122 @@
# Glow squid NoAI dark-tick upkeep (H176)
A glow squid's positive DarkTicksRemaining counts down after the parent aiStep even
with NoAI. Extract the existing positive-only countdown from glow_squid_ai_step and
call it from Mob.tick's NoAI branch. Normal AI and the existing remote animation helper
keep their one countdown call at the same position before the unchanged glow-particle
RNG draws. The new NoAI call performs no movement, particle or RNG work.
Pinned Java26.3 GlowSquid.aiStep bytecode calls Squid.aiStep at offset1, reads the
counter at5 and decrements only when positive at10–17. Own port ledger/bytecode and
full current/closed duplicate searches were reviewed. Merged3944 introduced the helper;
8864 concerns damage dimming and is not this route. H177 is excluded: there is no damage
in this program and no damage/ink policy change.
## Frozen actual Game/Java program
Base `3d33b0299d5c80769300d8b33dafb389ac9e7656`. The existing actual Game command
harness and fresh pinned Java run four loaded glow squid, NoAI0/1 crossed with loaded
DarkTicksRemaining40/-2, at cumulative actor ticks0/1/5/10 (steps1,4,5).
The UUID words are [176,0,0,1] through [176,0,0,4], with independently observed unique
tags, type, Health10, NoAI/default absence, NoGravity1 and PersistenceRequired1.
They start at [4.5,203,4.5], [10.5,203,4.5], [4.5,203,10.5], [10.5,203,10.5] in a
positively loaded glass pool with stone foundation and full source water. Actual feet
and head water, floor/shell, actual owner UUID/Health/mode/Pos, and exact elapsed game-time
deltas are guarded at every snapshot. No unavailable entity or field can stand in for
a countdown observation. Counter input comes only from summon NBT, never damage.
The strict224 program was declared before fresh Java and the paired native runs.
It compares every recorded JSON record (feedback, styles and callbacks) exactly, without value
masks or tolerances. NoAI positions and all initial positions are strict. Full raw
bodies/Motion, later ordinary-AI positions and absolute clock reads still execute and
are traced; the full267 program records all43 diagnostics too. Those positions are
independently guarded as remaining in loaded water, not claimed as general trajectory
parity. The complete old/fixed programs are identical within each mode.
| Actor | Loaded counter | t0 | t1 | t5 | t10 |
|---|---:|---:|---:|---:|---:|
| NoAI1 positive | 40 | 40 | 39 | 35 | 30 |
| NoAI1 negative | -2 | -2 | -2 | -2 | -2 |
| NoAI0 positive | 40 | 40 | 39 | 35 | 30 |
| NoAI0 negative | -2 | -2 | -2 | -2 | -2 |
The actual Mob.tick regression builds a real resource-backed water world and uses
actual glow-squid types. Normal-AI and negative controls run before the old intended
NoAI positive assertion (40 versus39 at tick1). It checks health and existing particle
dispatch counts as preservation controls, not new official particle parity: NoAI's
existing zero events stay zero, ordinary AI keeps one per tick. The resource requirement
is explicit and the test asserts that the world builder ran, preventing a silent skip.
Replay (resource tests need isolated fresh homes/cwd as in AGENTS.md):
```sh
python3 tools/test_glow_squid_noai_parity.py --binary /path/to/game --work-dir .local/h176/replay
python3 tools/test_glow_squid_noai_parity.py --full-diagnostic --binary /path/to/game --work-dir .local/h176/full-replay
cargo test -p horizon-client-lib --lib mob_tick_keeps_glow_squid_countdowns_with_no_ai -- --ignored --nocapture
```
Raw commands/JSON/bodies/styles, full expected/actual reports, Java logs, bytecode,
source checkpoints, hashes and immutable binaries remain locally under `.local/h176/`.
No prepared data or Mojang files are committed. No new observer or renderer is used.
No damage/ink, movement, metadata, packet, particle/RNG parity or reopen claim is made.
## Declared hashes
| Artifact | SHA-256 |
|---|---|
| canonical program | `d5d4e95b3213478c8707bfe4e156a883a048530bbf8b0822eea65bd3247ccada` |
| Fresh canonical Java records | `36f9303bfdb2fbb953ad32e085d390745613d93aeb81c8bc2499e6c9c219bd4c` |
| full program | `df4e2845f7532c99994b27aa6e4604f0d13a6dd89bf690b7f69a3705d3547af7` |
| Fresh full Java records | `9e3fa1e6e7c88920c8c8270aaf6d7512c83a203781aa62b7fb316f86a5635a07` |
## Paired results and exact residuals
| Program | Untouched Game | Fixed Game |
|---|---:|---:|
| Strict complete records | 221/224 | **224/224** |
| Full diagnostic | 252/267 | **258/267** |
Strict old/fixed programs and independently regenerated Java reference records are
byte-identical. The only three old strict differences are the NoAI positive counter
remaining40 instead of39/35/30. The corresponding three full-body differences are fixed
too. Normal-AI and negative controls pass both builds, without duplicate countdowns.
All nine remaining full records are unchanged by the fix. Exact field/text differences
and callbacks follow; this readable table is not a comparison projection. Complete styled
JSON expected/actual records remain in strict-report.json. There is no full267-green or
raw absolute-clock equality claim.
| Label | Expected Java | Actual native | Separate diagnostic |
|---|---|---|---|
| `clock-start` | `The game time is 309 tick(s)`; `[true,309]` | `The game time is 0 tick(s)`; `[true,0]` | Unequal startup clocks; relative0/1/5/10 remains strict |
| `0-clock-raw` | `The game time is 309 tick(s)`; `[true,309]` | `The game time is 0 tick(s)`; `[true,0]` | Unequal startup clocks; relative0/1/5/10 remains strict |
| `0-noai_positive-body` | `attributes: []`; `[true,1]` | `attributes: [{id:"minecraft:movement_speed",base:0.7d}]`; `[true,1]` | Extra initial default-attribute serialization; not a formatter or H176 repair |
| `0-noai_negative-body` | `attributes: []`; `[true,1]` | `attributes: [{id:"minecraft:movement_speed",base:0.7d}]`; `[true,1]` | Extra initial default-attribute serialization; not a formatter or H176 repair |
| `0-ai_positive-body` | `attributes: []`; `[true,1]` | `attributes: [{id:"minecraft:movement_speed",base:0.7d}]`; `[true,1]` | Extra initial default-attribute serialization; not a formatter or H176 repair |
| `0-ai_negative-body` | `attributes: []`; `[true,1]` | `attributes: [{id:"minecraft:movement_speed",base:0.7d}]`; `[true,1]` | Extra initial default-attribute serialization; not a formatter or H176 repair |
| `1-clock-raw` | `The game time is 310 tick(s)`; `[true,310]` | `The game time is 1 tick(s)`; `[true,1]` | Unequal startup clocks; relative0/1/5/10 remains strict |
| `5-clock-raw` | `The game time is 314 tick(s)`; `[true,314]` | `The game time is 5 tick(s)`; `[true,5]` | Unequal startup clocks; relative0/1/5/10 remains strict |
| `10-clock-raw` | `The game time is 319 tick(s)`; `[true,319]` | `The game time is 10 tick(s)`; `[true,10]` | Unequal startup clocks; relative0/1/5/10 remains strict |
## Local readiness and source checkpoints
The unchanged resource-backed actual Mob.tick regression passes fixed (one executed test),
after the intended old failure. Required pr_ready --quick ran once on the final gameplay
patch: formatting, workspace/headless clippy, doc tests and public tree pass. The library
suite reports1880 passed/1 failed/63 ignored. Its failure is the pre-existing drop-motion
bit test at yaw37.5/pitch-21.25: actual Z bits4597448471913462162 versus expected
4597448471913462163. The archived untouched binary reproduces the identical failure;
entities.rs is unchanged. No movement repair is included.
Python ran686 tests, four failed and one skipped. These are unchanged macOS packaging
failures: the signature mock lacks the ARM64 archive, and optional-archive masks0/1/2
encounter unset Bash3.2 arrays. The focused Game/Java programs and ignored resource-backed
regression were explicitly run; --quick does not claim broad parity-suite coverage.
Only evidence documentation was finalized afterward, followed by public-tree/whitespace.
Actual subsequent main pulls reached86a9d9e66 (launcher screenshots) and finally
`5849fa33b306beae3f218d03694f4e17497d59c4` (mod-browser/UI plus launcher changes).
Relevant mob tick/aquatic/codec/creature/command-driver sources are unchanged. The new
browser polling is idle without an explicit browser/install job; this fixture never
opens it. These source changes were reviewed, not imported or called a combined latest-main
runtime proof. The paired source base remains3d33b0299.
+76
View File
@@ -0,0 +1,76 @@
"""Loaded positive/negative glow squid counters across actual world ticks."""
ACTORS = [
('noai_positive', 1, 1, 40, 4.5, 4.5),
('noai_negative', 2, 1, -2, 10.5, 4.5),
('ai_positive', 3, 0, 40, 4.5, 10.5),
('ai_negative', 4, 0, -2, 10.5, 10.5),
]
def squid_countdown(full=False):
ops = []
def cmd(text, label, result=None, callback=None, admission=True, diagnostic=False):
op = {'op': 'cmd', 'cmd': text, 'label': label, 'admission': admission}
if result is not None:
op['expect_result'] = result
if callback is not None:
op['expect_callback'] = callback
if diagnostic and not full:
op.update(record=False, trace=True)
ops.append(op)
cmd('/tick freeze', 'freeze')
for rule, value in [('spawn_mobs', 'false'), ('advance_time', 'false'), ('random_tick_speed', '0')]:
cmd(f'/gamerule {rule} {value}', 'rule-' + rule)
cmd('/forceload add 0 0', 'force', 1)
ops.append({'op': 'settle', 'required_chunks': [[0, 0]]})
cmd('/fill 0 199 0 15 199 15 stone', 'floor', 256)
cmd('/fill 0 200 0 15 207 15 glass', 'pool-shell', 2048)
cmd('/fill 1 201 1 14 206 14 water', 'pool-water', 1176)
cmd('/tp @s 8.5 210 8.5 0 0', 'owner-position', 1)
cmd('/gamemode creative @s', 'owner-mode')
cmd('/scoreboard objectives add h176_clock dummy', 'clock-objective')
for name, n, noai, dark, x, z in ACTORS:
cmd(f'/summon glow_squid {x} 203 {z} {{UUID:[I;176,0,0,{n}],Tags:["h176_{name}"],'
f'NoAI:{noai}b,NoGravity:1b,PersistenceRequired:1b,DarkTicksRemaining:{dark}}}',
name + '-summon', 1)
cmd('/execute store result score start h176_clock run time query gametime',
'clock-start', diagnostic=True)
elapsed = 0
for at in [0, 1, 5, 10]:
if at:
n = at - elapsed
cmd(f'/tick step {n}', f'{at}-step')
ops.append({'op': 'tick', 'n': n})
elapsed = at
key = str(at)
cmd('/execute if entity @s[type=player,name=Player]', key + '-owner', 1)
for field, value in [('UUID', 4), ('Health', 20), ('Pos', 3), ('playerGameType', 1)]:
cmd(f'/data get entity @s {field}', key + '-owner-' + field, value)
for pos, block in [('4 199 4', 'stone'), ('0 203 4', 'glass'), ('4 200 4', 'glass'),
('4 207 4', 'glass'), ('4 203 4', 'water[level=0]')]:
cmd(f'/execute if block {pos} {block}', key + '-geometry-' + pos, 1)
cmd('/execute if loaded 4 203 4', key + '-loaded', 1)
cmd('/execute store result score now h176_clock run time query gametime',
key + '-clock-raw', diagnostic=True)
cmd('/scoreboard players operation now h176_clock -= start h176_clock', key + '-clock-delta-op', at)
cmd('/scoreboard players get now h176_clock', key + '-clock-delta', at)
for name, n, noai, dark, x, z in ACTORS:
uuid = f'000000b0-0000-0000-0000-{n:012}'
label = key + '-' + name
cmd(f'/execute as {uuid} if entity @s[type=glow_squid,tag=h176_{name}]', label + '-type-tag', 1)
for field, value in [('UUID', 4), ('Health', 10), ('NoGravity', 1), ('PersistenceRequired', 1)]:
cmd(f'/data get entity {uuid} {field}', label + '-' + field, value)
cmd(f'/data get entity {uuid} NoAI', label + '-NoAI',
result=1 if noai else None, callback=None if noai else [False, 0])
for dy in ['', '1']:
cmd(f'/execute as {uuid} at @s if block ~ ~{dy} ~ water[level=0]', label + '-water-' + dy, 1)
cmd(f'/data get entity {uuid} Pos', label + '-Pos', 3, diagnostic=not noai and at != 0)
cmd(f'/data get entity {uuid} Motion', label + '-Motion', 3, diagnostic=True)
cmd(f'/data get entity {uuid} DarkTicksRemaining', label + '-dark',
max(0, dark - at) if dark > 0 else dark,
admission=not (noai and dark > 0 and at > 0))
cmd(f'/data get entity {uuid}', label + '-body', diagnostic=True)
return ops
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Strict actual Game/Java glow squid server countdown proof."""
import hashlib
import json
from pathlib import Path
import sys
import test_command_exec_parity as driver
from glow_squid_noai_corpus import squid_countdown
if __name__ == '__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()
del sys.argv[i:i + 2]
if '--work-dir' not in sys.argv:
sys.argv += ['--work-dir', str(driver.lab.LOCAL / 'glow-squid-noai-parity')]
build = Path(sys.argv[sys.argv.index('--work-dir') + 1]).resolve()
ops = squid_countdown(full)
driver.corpus_module.groups = lambda: [('loaded glow squid counters', ops)]
sys.argv.append('--allow-gaps')
driver.main()
recorded = [op for op in ops if op['op'] == 'cmd' and op.get('record', True)]
java = [json.loads(s) for s in (build / 'vanilla.jsonl').read_text().splitlines()]
assert len(java) == len(recorded)
for op, row in zip(recorded, java):
if 'expect_results' in op:
assert row['results'] == op['expect_results'], ('Java action/callback admission', op, row)
if '--reference-only' not in sys.argv:
source = reference or build / 'vanilla.jsonl'
if reference and not full:
assert source.read_bytes() == (build / 'vanilla.jsonl').read_bytes(), 'fresh Java bytes changed'
expected = [json.loads(s) for s in source.read_text().splitlines()]
native = [json.loads(s) for s in (build / 'rust.jsonl').read_text().splitlines()]
assert len(expected) == len(native) == len(recorded)
for op, a, b in zip(recorded, expected, native):
if op['admission'] and not full:
assert a == b, ('actual actor/state/control admission', op, a, b)
differences = [{'label': op['label'], 'expected': a, 'actual': b}
for op, a, b in zip(recorded, expected, native) if a != b]
report = {'records': len(native), 'passed': len(native) - len(differences),
'comparison': 'complete raw records, no masks/tolerances',
'reference_sha256': hashlib.sha256(source.read_bytes()).hexdigest(),
'fresh_java_sha256': hashlib.sha256((build / 'vanilla.jsonl').read_bytes()).hexdigest(),
'differences': differences}
(build / 'strict-report.json').write_text(json.dumps(report, indent=2) + '\n')
print(f'Strict raw records: {report["passed"]}/{report["records"]}')
if differences:
raise SystemExit(1)