mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Keep glow squid dark countdown running with NoAI
Co-Authored-By: Codex <[email protected]>
This commit is contained in:
1 parent
3d33b0299d
commit
0e05b77d95
6 files changed
+336
-4
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user