mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Widen float constants in experience-orb underwater movement
Preserve the double operations around vanilla float drag, buoyancy and cap constants. Pin six executed Java method vectors and guard the real Game water and air tick paths. Co-Authored-By: Codex <[email protected]>
This commit is contained in:
1 parent
83ad384f48
commit
c0f778ae98
7 files changed
+228
-12
No files matched your search
@@ -1187,6 +1187,15 @@ impl ExperienceOrb {
|
||||
let sky = light >> 4;
|
||||
(u32::try_from(sky).unwrap_or(0) * 16) | ((u32::try_from(block).unwrap_or(15) * 16) << 8)
|
||||
}
|
||||
/// `ExperienceOrb.setUnderwaterMovement`: buoyancy and drag before moving.
|
||||
fn set_underwater_movement(&mut self) {
|
||||
// Vanilla widens float constants before applying double arithmetic.
|
||||
self.velocity = Vec3::new(
|
||||
self.velocity.x * f64::from(0.99_f32),
|
||||
(self.velocity.y + f64::from(5.0e-4_f32)).min(f64::from(0.06_f32)),
|
||||
self.velocity.z * f64::from(0.99_f32),
|
||||
);
|
||||
}
|
||||
pub fn tick(
|
||||
&mut self,
|
||||
world: &World,
|
||||
@@ -1216,11 +1225,7 @@ impl ExperienceOrb {
|
||||
// inside blocks takes no gravity until it is pushed out below.
|
||||
let colliding = !crate::collisions::no_collision_empty(&mut scene, bounds);
|
||||
if facts.get(block).fluid == Fluid::Water {
|
||||
self.velocity = Vec3::new(
|
||||
self.velocity.x * 0.99,
|
||||
(self.velocity.y + 5.0e-4).min(0.06),
|
||||
self.velocity.z * 0.99,
|
||||
);
|
||||
self.set_underwater_movement();
|
||||
} else if !colliding {
|
||||
self.velocity.y -= self.get_gravity();
|
||||
}
|
||||
@@ -3832,6 +3837,72 @@ mod item_destroyed_tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod orb_underwater_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn matches_the_executed_java_private_method() {
|
||||
// Actual 26.3 setUnderwaterMovement calls on server-level orbs,
|
||||
// captured by test_orb_water_parity.py (orb-water-reference.json).
|
||||
// Cover both horizontal signs, falling/rising, and the vertical cap.
|
||||
let cases: [([f64; 3], [u64; 3]); 6] = [
|
||||
(
|
||||
[0.0, 0.0, 0.0],
|
||||
[0x0000000000000000, 0x3f40624de0000000, 0x0000000000000000],
|
||||
),
|
||||
(
|
||||
[0.1, 0.01, -0.2],
|
||||
[0x3fb9581066666667, 0x3f85810625ae147b, 0xbfc9581066666667],
|
||||
),
|
||||
(
|
||||
[-0.25, -0.03, 0.125],
|
||||
[0xbfcfae1480000000, 0xbf9e353f7c851eb8, 0x3fbfae1480000000],
|
||||
),
|
||||
(
|
||||
[0.0, 0.059, 0.0],
|
||||
[0x0000000000000000, 0x3fae76c8b46d9168, 0x0000000000000000],
|
||||
),
|
||||
(
|
||||
[0.5, 0.0595, -0.5],
|
||||
[0x3fdfae1480000000, 0x3faeb851e0000000, 0xbfdfae1480000000],
|
||||
),
|
||||
(
|
||||
[-1.0, 0.2, 1.0],
|
||||
[0xbfefae1480000000, 0x3faeb851e0000000, 0x3fefae1480000000],
|
||||
),
|
||||
];
|
||||
let actual: Vec<_> = cases
|
||||
.iter()
|
||||
.map(|(v, _)| {
|
||||
let mut orb = ExperienceOrb {
|
||||
rotation: [0.0, 0.0],
|
||||
join: Default::default(),
|
||||
no_gravity: false,
|
||||
id: 1,
|
||||
position: Vec3::ZERO,
|
||||
previous: Vec3::ZERO,
|
||||
velocity: Vec3::new(v[0], v[1], v[2]),
|
||||
value: 1,
|
||||
age: 0,
|
||||
on_ground: false,
|
||||
count: 1,
|
||||
ticks: 0,
|
||||
health: 5,
|
||||
};
|
||||
orb.set_underwater_movement();
|
||||
[
|
||||
orb.velocity.x.to_bits(),
|
||||
orb.velocity.y.to_bits(),
|
||||
orb.velocity.z.to_bits(),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
let expected: Vec<_> = cases.into_iter().map(|(_, bits)| bits).collect();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod render_light_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -443,10 +443,12 @@ mod tests {
|
||||
None,
|
||||
&fluids,
|
||||
));
|
||||
// Current pushes the orb, underwater drag damps before motion, then air
|
||||
// drag: `ExperienceOrb.tick`'s `0.98F`, a float widened to double.
|
||||
assert!((carried.position.x - at.x - 0.014 * 0.99).abs() < 1e-12);
|
||||
assert!((carried.velocity.x - 0.014 * 0.99 * f64::from(0.98_f32)).abs() < 1e-12);
|
||||
// Current pushes the orb, then underwater and air drag each widen
|
||||
// their float constants before multiplying the double motion.
|
||||
assert!((carried.position.x - at.x - 0.014 * f64::from(0.99_f32)).abs() < 1e-12);
|
||||
assert!(
|
||||
(carried.velocity.x - 0.014 * f64::from(0.99_f32) * f64::from(0.98_f32)).abs() < 1e-12
|
||||
);
|
||||
for _ in 0..20 {
|
||||
carried.tick(
|
||||
&world,
|
||||
|
||||
@@ -149,6 +149,34 @@ public final class VanillaCommandExecOracle {
|
||||
|
||||
ServerPlayer player;
|
||||
|
||||
// Execute the actual private arithmetic method on unregistered orbs in
|
||||
// this real server level. No world tick or live scene actor is added.
|
||||
void writeOrbWaterReference() throws IOException {
|
||||
String input = System.getProperty("horizon.orbWaterArithmetic");
|
||||
if (input == null) return;
|
||||
JsonArray result = new JsonArray();
|
||||
try {
|
||||
var method = net.minecraft.world.entity.ExperienceOrb.class.getDeclaredMethod("setUnderwaterMovement");
|
||||
method.setAccessible(true);
|
||||
for (var entry : JsonParser.parseString(Files.readString(Path.of(input))).getAsJsonArray()) {
|
||||
JsonArray p = entry.getAsJsonArray();
|
||||
var orb = new net.minecraft.world.entity.ExperienceOrb(net.minecraft.world.entity.EntityTypes.EXPERIENCE_ORB, player.level());
|
||||
orb.setDeltaMovement(p.get(0).getAsDouble(), p.get(1).getAsDouble(), p.get(2).getAsDouble());
|
||||
method.invoke(orb);
|
||||
var v = orb.getDeltaMovement();
|
||||
JsonObject row = new JsonObject(); row.add("input", p);
|
||||
JsonArray bits = new JsonArray(); JsonArray values = new JsonArray();
|
||||
for (double n : new double[] {v.x(), v.y(), v.z()}) {
|
||||
bits.add(Long.toHexString(Double.doubleToRawLongBits(n))); values.add(n);
|
||||
}
|
||||
row.add("bits", bits); row.add("motion", values); result.add(row);
|
||||
}
|
||||
} catch (ReflectiveOperationException error) {
|
||||
throw new IOException("orb underwater reference failed", error);
|
||||
}
|
||||
Files.writeString(output.resolveSibling("orb-water-reference.json"), result.toString());
|
||||
}
|
||||
|
||||
void runScript() throws IOException {
|
||||
ServerLevel level = this.overworld();
|
||||
GameProfile profile = new GameProfile(UUIDUtil.createOfflinePlayerUUID("Player"), "Player");
|
||||
@@ -167,6 +195,7 @@ public final class VanillaCommandExecOracle {
|
||||
new EmbeddedChannel(connection);
|
||||
this.getPlayerList().placeNewPlayer(connection, player, cookie);
|
||||
settle();
|
||||
writeOrbWaterReference();
|
||||
StringBuilder out = new StringBuilder();
|
||||
Gson gson = new Gson();
|
||||
for (String line : Files.readAllLines(script, StandardCharsets.UTF_8)) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Exact water/air orb fields; identity uses registration, never saved UUID NBT."""
|
||||
try:
|
||||
from .command_exec_corpus import cmd, setup
|
||||
except ImportError:
|
||||
from command_exec_corpus import cmd, setup
|
||||
|
||||
|
||||
def arithmetic_inputs():
|
||||
return [[0.0, 0.0, 0.0], [0.1, 0.01, -0.2], [-0.25, -0.03, 0.125],
|
||||
[0.0, 0.059, 0.0], [0.5, 0.0595, -0.5], [-1.0, 0.2, 1.0]]
|
||||
|
||||
|
||||
def orb_water(raw=False):
|
||||
out = setup('/tick freeze', '/gamerule minecraft:spawn_mobs false',
|
||||
'/gamerule minecraft:advance_time false', '/gamemode spectator',
|
||||
'/tp @s 24.5 210 24.5 0 0')
|
||||
out += [cmd('/forceload add 0 0 31 31', expect_result=4), {'op': 'settle'}]
|
||||
for x in [0, 16]:
|
||||
for z in [0, 16]:
|
||||
out.append(cmd(f'/execute if loaded {x} 200 {z}', expect_result=1))
|
||||
out += [cmd('/fill 0 200 0 31 207 31 stone', expect_result=8192),
|
||||
cmd('/fill 0 200 0 31 207 31 air', expect_result=8192),
|
||||
cmd('/fill 0 199 0 31 199 31 stone', expect_result=1024),
|
||||
cmd('/fill 2 200 2 4 206 4 glass', expect_result=63),
|
||||
cmd('/fill 3 200 3 3 206 3 water', expect_result=7)]
|
||||
for pos, block in [('3 200 3', 'water[level=0]'), ('3 201 3', 'water[level=0]'),
|
||||
('3 206 3', 'water[level=0]'), ('2 201 3', 'glass'),
|
||||
('4 201 3', 'glass'), ('3 201 2', 'glass'), ('3 201 4', 'glass'),
|
||||
('3 199 3', 'stone'), ('15 199 3', 'stone'), ('15 205 3', 'air')]:
|
||||
out.append(cmd(f'/execute if block {pos} minecraft:{block}', expect_result=1))
|
||||
out += setup('/scoreboard objectives add orb_audit dummy',
|
||||
'/data modify storage h303:observer uuid set from entity @s UUID')
|
||||
out.append(cmd('/data get entity @s UUID', expect_result=4))
|
||||
actors = [('water', '0000012f-0000-0000-0000-000000000001', '3.5 201.2 3.5', 1),
|
||||
('air', '0000012f-0000-0000-0000-000000000002', '15.5 205 3.5', 2)]
|
||||
for name, uuid, pos, n in actors:
|
||||
out.append(cmd(f'/summon minecraft:experience_orb {pos} '
|
||||
f'{{UUID:[I;303,0,0,{n}],Tags:["h303_{name}"],Health:5s,Age:0s,'
|
||||
'Value:1s,Count:1,NoGravity:0b,Motion:[0d,0d,0d]}', expect_result=1))
|
||||
out += setup('/execute store result score start orb_audit run time query gametime')
|
||||
for at, n in [(0, 0), (1, 1), (5, 4), (20, 15), (40, 20)]:
|
||||
if n:
|
||||
out += [cmd(f'/tick step {n}', expect_result=1), {'op': 'tick', 'n': n}]
|
||||
out += setup('/execute store result score elapsed orb_audit run time query gametime',
|
||||
'/scoreboard players operation elapsed orb_audit -= start orb_audit')
|
||||
out += [cmd('/scoreboard players get elapsed orb_audit', expect_result=at),
|
||||
cmd('/data get entity @s playerGameType', expect_result=3),
|
||||
cmd('/execute if entity @s[type=player,nbt={Health:20f,Pos:[24.5d,210d,24.5d]}]', expect_result=1)]
|
||||
for name, uuid, _, _ in actors:
|
||||
# Direct UUID lookup followed by type/tag/typed live fields. A saved
|
||||
# UUID read is intentionally absent: H011 is a separate getter bug.
|
||||
out += [cmd(f'/execute as {uuid} if entity @s[type=minecraft:experience_orb,tag=h303_{name},nbt={{Health:5s,Age:{at}s,Value:1s,Count:1}}]', expect_result=1),
|
||||
cmd(f'/execute if entity @e[type=minecraft:experience_orb,tag=h303_{name}]', expect_result=1),
|
||||
cmd(f'/execute as {uuid} at @s if block ~ ~ ~ minecraft:{name if name == "water" else "air"}', expect_result=1),
|
||||
cmd(f'/execute as {uuid} unless entity @s[nbt={{NoGravity:1b}}]', expect_result=1)]
|
||||
for field in ['Pos[1]', 'Motion[1]'] + (['Pos', 'Motion'] if raw else []):
|
||||
op = cmd(f'/data get entity {uuid} {field}')
|
||||
op['label'] = f'h303 {name}/{at}/{field}'
|
||||
out.append(op)
|
||||
out += setup('/execute store success score changed orb_audit run data modify storage h303:observer uuid set from entity @s UUID')
|
||||
out += [cmd('/scoreboard players get changed orb_audit', expect_result=0),
|
||||
cmd('/data get entity @s UUID', expect_result=4)]
|
||||
return out
|
||||
@@ -137,7 +137,7 @@ EquipmentTable.createForAllSlots skip:data drop chances come from the loot and
|
||||
ExperienceOrb.<init> port crates/horizon-client/src/game.rs::Game::add_orb random yaw and motion from the orb's own random; gap: minor: no rough direction offset and no unstuck when the orb spawns inside a block
|
||||
ExperienceOrb.unstuckIfPossible port crates/horizon-client/src/entities.rs::ExperienceOrb::unstuck an orb spawned with its box inside blocks moves to the nearest free position (find_free_position over the border), run by the spawn path
|
||||
ExperienceOrb.awardWithDirection port crates/horizon-client/src/game.rs::Game::spawn_orbs gap: minor: the rough direction is never given, so xp bottle orbs do not fly away from the surface they hit
|
||||
ExperienceOrb.setUnderwaterMovement port crates/horizon-client/src/entities.rs::ExperienceOrb::tick gap: minor: tests the block at the orb's feet, not its eyes
|
||||
ExperienceOrb.setUnderwaterMovement port crates/horizon-client/src/entities.rs::ExperienceOrb::set_underwater_movement widened float arithmetic; gap: minor: caller still tests the block at the orb's feet, not its eyes
|
||||
ExperienceOrb.hurtServer port crates/horizon-client/src/entities.rs::ExperienceOrb::hurt ticked in #3514; `ExperienceOrb.hurtServer` is cited at crates/horizon-client/src/entities.rs:538
|
||||
ExperienceOrb.addAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::orb_to_nbt gap: minor: Health is always written as 5
|
||||
ExperienceOrb.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::orbs_from_nbt gap: minor: Health is ignored
|
||||
|
||||
|
@@ -83,8 +83,11 @@ def main():
|
||||
parser.add_argument('--binary', type=Path, help='use an already built Horizon game instead of building it')
|
||||
parser.add_argument('--slot-codec-diagnostic', action='store_true',
|
||||
help='retain the separate invalid inline slot-range codec diagnostic')
|
||||
parser.add_argument('--work-dir', type=Path, help='isolated scratch output directory')
|
||||
parser.add_argument('--orb-water-fixture', action='store_true', help='guarded orb water/air scene')
|
||||
parser.add_argument('--orb-water-raw-diagnostic', action='store_true', help='add whole Pos/Motion records')
|
||||
args = parser.parse_args()
|
||||
build = lab.LOCAL / 'command-exec-parity'
|
||||
build = args.work_dir.resolve() if args.work_dir else lab.LOCAL / 'command-exec-parity'
|
||||
build.mkdir(parents=True, exist_ok=True)
|
||||
report_path = build / 'report.json'
|
||||
report_path.unlink(missing_ok=True)
|
||||
@@ -115,6 +118,12 @@ def main():
|
||||
if args.sulfur_power_load_diagnostic:
|
||||
groups = [('setup', corpus_module.group_setup()),
|
||||
('sulfur powered-load diagnostic', corpus_module.sulfur_cube_fuse_ticks(True))]
|
||||
if args.orb_water_fixture:
|
||||
try:
|
||||
from .orb_water_corpus import orb_water, arithmetic_inputs
|
||||
except ImportError:
|
||||
from orb_water_corpus import orb_water, arithmetic_inputs
|
||||
groups = [('setup', corpus_module.group_setup()), ('orb underwater arithmetic', orb_water(args.orb_water_raw_diagnostic))]
|
||||
if args.only:
|
||||
groups = [g for g in groups if args.only in g[0] or g[0] == 'setup']
|
||||
if args.slot_codec_diagnostic:
|
||||
@@ -135,9 +144,22 @@ def main():
|
||||
shutil.rmtree(vanilla_dir, ignore_errors=True)
|
||||
vanilla_dir.mkdir()
|
||||
install_datapack_fixtures(vanilla_dir / 'universe/harness', predicates, functions, jars['server'], advancements)
|
||||
run(['java', '-Xmx3G', '-Duser.timezone=UTC', '-cp', os.pathsep.join([str(classes), str(jars['server']), dependency_path]),
|
||||
java_options = []
|
||||
if args.orb_water_fixture:
|
||||
(build / 'orb-water-reference.json').unlink(missing_ok=True)
|
||||
direct_input = build / 'orb-water-inputs.json'
|
||||
direct_input.write_text(json.dumps(arithmetic_inputs()), encoding='utf-8')
|
||||
java_options = ['-Dhorizon.orbWaterArithmetic=' + str(direct_input)]
|
||||
run(['java', '-Xmx3G', '-Duser.timezone=UTC', *java_options, '-cp', os.pathsep.join([str(classes), str(jars['server']), dependency_path]),
|
||||
'dev.horizonlab.parity.VanillaCommandExecOracle', vanilla_dir / 'universe', script_path, build / 'vanilla.jsonl', SEED], 'java', cwd=vanilla_dir)
|
||||
|
||||
if args.orb_water_fixture:
|
||||
direct = json.loads((build / 'orb-water-reference.json').read_text())
|
||||
if ([r.get('input') for r in direct] != arithmetic_inputs() or
|
||||
any(len(r.get('bits', [])) != 3 or
|
||||
any(re.fullmatch(r'[0-9a-f]{1,16}', b) is None for b in r['bits']) for r in direct)):
|
||||
sys.exit('Java direct underwater reference prerequisite failed')
|
||||
|
||||
if args.binary:
|
||||
binary = args.binary.resolve(strict=True)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exact real Game/Java water-orb arithmetic, with an unchanged air control."""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--binary', type=Path)
|
||||
parser.add_argument('--prepare', action='store_true')
|
||||
parser.add_argument('--raw-diagnostic', action='store_true')
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
folder = 'orb-water-raw-diagnostic' if args.raw_diagnostic else 'orb-water-parity'
|
||||
command = [sys.executable, str(root / 'tools/test_command_exec_parity.py'),
|
||||
'--orb-water-fixture', '--work-dir', str(root / '.local' / folder)]
|
||||
if args.raw_diagnostic:
|
||||
command.append('--orb-water-raw-diagnostic')
|
||||
if args.binary:
|
||||
command += ['--binary', str(args.binary.resolve())]
|
||||
if args.prepare:
|
||||
command.append('--prepare')
|
||||
return subprocess.run(command, cwd=root).returncode
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user