mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #10031 from TheMrZZ/fix/h326-boat-live-leash
🤖 Agent: data get on a leashed boat now reads its leash from the same save path the world writes (write_boat_leash); merged main twice, boat_leash_view harness passes and the u/3 union builds with clippy -D warnings.
This commit is contained in:
7 files changed
+245
-3
No files matched your search
@@ -506,6 +506,69 @@ impl Game<'_> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn command_view_uses_live_and_pending_boat_leashes_not_raw_tags() {
|
||||
use crate::game::{Audio, Options, Progress, Settings, commands::Target, load_resources};
|
||||
let root = std::path::PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
|
||||
let settings = Settings::default();
|
||||
let loaded = load_resources(
|
||||
root.clone(),
|
||||
None,
|
||||
&root,
|
||||
&settings,
|
||||
std::time::Instant::now(),
|
||||
false,
|
||||
true,
|
||||
&Progress::new(),
|
||||
&mut |_, _| unreachable!(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut options = Options::parse(std::iter::empty()).unwrap().unwrap();
|
||||
options.resources = root.clone();
|
||||
options.execution_role = crate::game::execution_role::ExecutionRole::IntegratedServer;
|
||||
let mut game = Game::new(
|
||||
&loaded.resources,
|
||||
loaded.pack,
|
||||
loaded.gui.unwrap(),
|
||||
options,
|
||||
settings,
|
||||
Audio::silent(&root),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
game.build_registries();
|
||||
let (id, uuid, actual_holder, unknown) =
|
||||
(1, [326, 1, 0, 1], [326, 2, 0, 1], [111, 222, 333, 444]);
|
||||
game.boats
|
||||
.push(Boat::new(id, "oak".into(), Vec3::new(6.5, 100.0, 4.5), 0.0));
|
||||
game.persist.uuids.insert(id, uuid);
|
||||
game.level.player_uuid = Some([326, 3, 0, 1]);
|
||||
let read = |game: &mut Game<'_>| game.entity_nbt(Target::Boat(id)).unwrap();
|
||||
assert!(!game.persist.raw.contains_key(&id));
|
||||
assert_eq!(
|
||||
read(&mut game).get("UUID"),
|
||||
Some(&Tag::IntArray(uuid.to_vec()))
|
||||
);
|
||||
assert!(read(&mut game).get("leash").is_none());
|
||||
game.boats[0].leash = Some(Holder::Player);
|
||||
game.boats[0].leash_player = Some(actual_holder);
|
||||
let expected = Tag::compound([("UUID", Tag::IntArray(actual_holder.to_vec()))]);
|
||||
assert_eq!(read(&mut game).get("leash"), Some(&expected));
|
||||
assert_eq!(read(&mut game).get("leash"), Some(&expected));
|
||||
let delayed = Tag::compound([("UUID", Tag::IntArray(unknown.to_vec()))]);
|
||||
game.persist
|
||||
.raw
|
||||
.insert(id, Tag::compound([("leash", delayed.clone())]));
|
||||
assert_eq!(read(&mut game).get("leash"), Some(&expected));
|
||||
game.boats[0].leash = None;
|
||||
game.boats[0].leash_player = None;
|
||||
game.boats[0].leash_pending = Some(Pending::Uuid(unknown));
|
||||
assert_eq!(read(&mut game).get("leash"), Some(&delayed));
|
||||
game.boats[0].leash_pending = None;
|
||||
assert!(read(&mut game).get("leash").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_boat_is_centred_half_its_height_up() {
|
||||
let b = Boat::new(1, "oak".into(), Vec3::new(1.0, 64.0, 2.0), 0.0);
|
||||
|
||||
@@ -599,6 +599,47 @@ impl Game<'_> {
|
||||
}
|
||||
// Java produced the fixture file before this world was opened; map_state loads it.
|
||||
"crafter_max_map_fixture" => {}
|
||||
"boat_interact" => {
|
||||
let uuid = u128::from_str_radix(
|
||||
&op["uuid"]
|
||||
.as_str()
|
||||
.ok_or("boat click: missing UUID")?
|
||||
.replace('-', ""),
|
||||
16,
|
||||
)?;
|
||||
self.in_owner_level(|g| -> Result<()> {
|
||||
let id = g
|
||||
.boat_by_uuid(horizon_client::lan::playerdata::uuid_ints(uuid))
|
||||
.ok_or("boat click: missing live boat")?;
|
||||
let owner = g.serving.as_ref().ok_or("boat click: no server")?.owner;
|
||||
let conn = g
|
||||
.lan
|
||||
.as_ref()
|
||||
.and_then(|h| {
|
||||
h.hub
|
||||
.guests
|
||||
.iter()
|
||||
.find(|p| p.uuid == owner && p.placed)
|
||||
.map(|p| p.conn)
|
||||
})
|
||||
.ok_or("boat click: owner not connected")?;
|
||||
let mut host = g.lan.take().ok_or("boat click: no hub")?;
|
||||
g.lan_guest_packet(
|
||||
&mut host,
|
||||
conn,
|
||||
horizon_protocol::packets::play::Serverbound::Interact(
|
||||
horizon_protocol::packets::play::Interact {
|
||||
id: id as i32,
|
||||
hand: 0,
|
||||
location: [0.0; 3],
|
||||
secondary: false,
|
||||
},
|
||||
),
|
||||
);
|
||||
g.lan = Some(host);
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
"use_item" => {
|
||||
let owner = self
|
||||
.serving
|
||||
|
||||
@@ -136,8 +136,9 @@ impl Game<'_> {
|
||||
}
|
||||
Target::Boat(id) => {
|
||||
let uuid = self.uuid_for(id);
|
||||
let b = self.boats.iter().find(|b| b.id == id)?;
|
||||
entity_nbt::boat_to_nbt(
|
||||
let i = self.boats.iter().position(|b| b.id == id)?;
|
||||
let b = &self.boats[i];
|
||||
let mut tag = entity_nbt::boat_to_nbt(
|
||||
&b.entity_type(),
|
||||
b.position,
|
||||
b.velocity,
|
||||
@@ -145,7 +146,10 @@ impl Game<'_> {
|
||||
uuid,
|
||||
self.persist.raw.get(&id),
|
||||
b.chest.then_some(&b.items[..]),
|
||||
)
|
||||
);
|
||||
// AbstractBoat.addAdditionalSaveData: the live or pending leash.
|
||||
self.write_boat_leash(&mut tag, i);
|
||||
tag
|
||||
}
|
||||
Target::Cart(id) => {
|
||||
let uuid = self.uuid_for(id);
|
||||
|
||||
@@ -836,6 +836,14 @@ public final class VanillaCommandExecOracle {
|
||||
}
|
||||
}
|
||||
}
|
||||
case "boat_interact" -> {
|
||||
var boat = level.getEntity(UUID.fromString(op.get("uuid").getAsString()));
|
||||
if (!(boat instanceof net.minecraft.world.entity.vehicle.boat.AbstractBoat))
|
||||
throw new IllegalStateException("boat click: missing live boat");
|
||||
player.connection.handleInteract(new net.minecraft.network.protocol.game.ServerboundInteractPacket(
|
||||
boat.getId(), net.minecraft.world.InteractionHand.MAIN_HAND,
|
||||
net.minecraft.world.phys.Vec3.ZERO, false));
|
||||
}
|
||||
case "use_item" -> {
|
||||
if (!op.has("sequence")) {
|
||||
player.gameMode.useItem(player, player.level(), player.getMainHandItem(),
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Live boat leash command reads after real connected-owner interactions."""
|
||||
|
||||
|
||||
def cases(cmd, setup):
|
||||
boat = '00000146-0000-0001-0000-000000000001'
|
||||
pending = '00000146-0000-0001-0000-000000000002'
|
||||
unknown = '[I;111,222,333,444]'
|
||||
ops = [cmd('/tick freeze'), cmd('/forceload add 0 0 31 31'),
|
||||
{'op': 'wait_chunks', 'chunks': [[0, 0], [1, 0], [0, 1], [1, 1]]}]
|
||||
for x, z in [(0, 0), (16, 0), (0, 16), (16, 16)]:
|
||||
ops.append(cmd(f'/execute if loaded {x} 100 {z}', expect_result=1))
|
||||
ops += [cmd('/fill 0 99 0 31 99 31 stone', expect_result=1024),
|
||||
cmd('/fill 0 100 0 31 104 31 air'),
|
||||
cmd('/gamemode survival @s'), cmd('/tp @s 4.5 100 4.5 0 0'),
|
||||
cmd('/clear @s'), cmd('/scoreboard objectives add boat_view dummy'),
|
||||
cmd('/item replace entity @s hotbar.0 with lead 2', expect_result=1)]
|
||||
ops += setup('/execute store result score #start boat_view run time query gametime',
|
||||
'/data modify storage boat_view:guard owner set from entity @s UUID')
|
||||
|
||||
def owner():
|
||||
return [cmd('/execute if entity @s[type=player,gamemode=survival,nbt={Pos:[4.5d,100d,4.5d],Health:20f}]', expect_result=1),
|
||||
cmd('/data get entity @s UUID'), cmd('/data get entity @s Inventory')]
|
||||
|
||||
def actor(target, x, z, tag):
|
||||
return [
|
||||
cmd(f'/execute if entity @e[type=oak_boat,tag={tag},nbt={{UUID:[I;326,1,0,{int(target[-8:], 16)}],Pos:[{x}d,100d,{z}d],NoGravity:1b}}]', expect_result=1),
|
||||
cmd(f'/data get entity {target} UUID', expect_result=4),
|
||||
cmd(f'/data get entity {target} Pos', expect_result=3),
|
||||
cmd(f'/execute if block {int(x)} 99 {int(z)} stone', expect_result=1)]
|
||||
|
||||
def leash(target, present):
|
||||
rows = [cmd(f'/data get entity {target} leash'),
|
||||
cmd(f'/data get entity {target} leash.UUID', expect_result=4 if present else None)]
|
||||
rows += setup('/scoreboard players set #r boat_view 31',
|
||||
'/scoreboard players set #s boat_view 17')
|
||||
rows += [cmd('/scoreboard players get #r boat_view', expect_result=31),
|
||||
cmd('/scoreboard players get #s boat_view', expect_result=17),
|
||||
cmd('/execute store result score #r boat_view store success score #s boat_view '
|
||||
f'run data get entity {target} leash.UUID'),
|
||||
cmd('/scoreboard players get #r boat_view', expect_result=4 if present else None),
|
||||
cmd('/scoreboard players get #s boat_view', expect_result=int(present))]
|
||||
return rows
|
||||
|
||||
ops += owner()
|
||||
ops += [cmd('/summon oak_boat 6.5 100 4.5 {UUID:[I;326,1,0,1],Tags:["boat_view"],NoGravity:1b,Motion:[0d,0d,0d]}')]
|
||||
ops += actor(boat, 6.5, 4.5, 'boat_view') + leash(boat, False)
|
||||
# Attach, detach and attach again. Each click traverses the real owner packet path.
|
||||
for attached, lead_count in [(True, 1), (False, 1), (True, 0)]:
|
||||
ops.append({'op': 'boat_interact', 'uuid': boat})
|
||||
ops += owner() + actor(boat, 6.5, 4.5, 'boat_view') + leash(boat, attached)
|
||||
ops.append(cmd('/clear @s lead 0', expect_result=lead_count if lead_count else None))
|
||||
if not lead_count:
|
||||
ops.append(cmd('/execute unless items entity @s hotbar.0 lead', expect_result=1))
|
||||
if attached:
|
||||
ops += setup('/data modify storage boat_view:guard owner set from entity @s UUID')
|
||||
ops.append(cmd('/execute store success score #different boat_view run data modify '
|
||||
f'storage boat_view:guard owner set from entity {boat} leash.UUID'))
|
||||
ops.append(cmd('/scoreboard players get #different boat_view', expect_result=0))
|
||||
ops += leash(boat, True) # Reads themselves do not change the live association.
|
||||
ops += [cmd(f'/summon oak_boat 10.5 100 4.5 {{UUID:[I;326,1,0,2],Tags:["boat_pending"],NoGravity:1b,leash:{{UUID:{unknown}}}}}')]
|
||||
ops += actor(pending, 10.5, 4.5, 'boat_pending') + leash(pending, True)
|
||||
ops.append(cmd('/execute unless entity 0000006f-0000-00de-0000-014d000001bc', expect_result=1))
|
||||
ops += [cmd(f'/execute if entity {pending}'),
|
||||
cmd('/data get storage boat_view:guard owner')]
|
||||
ops += setup('/execute store result score #elapsed boat_view run time query gametime',
|
||||
'/scoreboard players operation #elapsed boat_view -= #start boat_view')
|
||||
ops += [cmd('/scoreboard players get #elapsed boat_view', expect_result=0)] + owner()
|
||||
# Independently measured loaded-link control: after resolution and a real
|
||||
# detach, stale input NBT must not override the now-absent live association.
|
||||
# This is the pinned harness owner's UUID, positively checked before use.
|
||||
holder = '[I;-1608632253,-450807400,-1786055591,-2146546415]'
|
||||
loaded = '00000146-0000-0001-0000-000000000003'
|
||||
ops += [cmd(f'/execute if entity @s[nbt={{UUID:{holder}}}]', expect_result=1),
|
||||
cmd('/tp @s 4.5 100 8.5 0 0'),
|
||||
cmd('/execute if entity @s[type=player,gamemode=survival,nbt={Pos:[4.5d,100d,8.5d],Health:20f}]', expect_result=1),
|
||||
cmd(f'/summon oak_boat 6.5 100 8.5 {{UUID:[I;326,1,0,3],Tags:["boat_loaded"],NoGravity:1b,Motion:[0d,0d,0d],leash:{{UUID:{holder}}}}}')]
|
||||
ops += actor(loaded, 6.5, 8.5, 'boat_loaded') + leash(loaded, True)
|
||||
ops.append(cmd(f'/execute as {loaded} on leasher if entity @s[type=player]'))
|
||||
ops += setup('/execute store result score #loaded_start boat_view run time query gametime')
|
||||
ops += [cmd('/tick step 1', expect_result=1), {'op': 'tick', 'n': 1}]
|
||||
ops += actor(loaded, 6.5, 8.5, 'boat_loaded') + leash(loaded, True)
|
||||
ops += [cmd(f'/execute as {loaded} on leasher if entity @s[nbt={{UUID:{holder}}}]', expect_result=1),
|
||||
cmd('/item replace entity @s hotbar.0 with lead', expect_result=1),
|
||||
{'op': 'boat_interact', 'uuid': loaded}]
|
||||
ops += actor(loaded, 6.5, 8.5, 'boat_loaded') + leash(loaded, False)
|
||||
ops += [cmd(f'/execute as {loaded} on leasher if entity @s[type=player]'),
|
||||
cmd('/clear @s lead 0', expect_result=1),
|
||||
cmd('/execute if entity @s[type=player,gamemode=survival,nbt={Pos:[4.5d,100d,8.5d],Health:20f}]', expect_result=1)]
|
||||
ops += setup('/execute store result score #loaded_elapsed boat_view run time query gametime',
|
||||
'/scoreboard players operation #loaded_elapsed boat_view -= #loaded_start boat_view')
|
||||
ops += [cmd('/scoreboard players get #loaded_elapsed boat_view', expect_result=1)]
|
||||
return ops
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Boat command-visible live leash data after real owner clicks."""
|
||||
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('--reference-only', action='store_true')
|
||||
parser.add_argument('--prepare', action='store_true')
|
||||
parser.add_argument('--work-dir', type=Path)
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
command = [sys.executable, str(root / 'tools/test_command_exec_parity.py'),
|
||||
'--boat-leash-view-fixture', '--work-dir',
|
||||
str(args.work_dir.resolve() if args.work_dir else root / '.local/boat-leash-view-parity')]
|
||||
if args.binary:
|
||||
command += ['--binary', str(args.binary.resolve())]
|
||||
for name in ['reference_only', 'prepare']:
|
||||
if getattr(args, name):
|
||||
command.append('--' + name.replace('_', '-'))
|
||||
return subprocess.run(command, cwd=root).returncode
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -121,6 +121,7 @@ def main():
|
||||
parser.add_argument('--item-contents-provider-empty-control', action='store_true')
|
||||
parser.add_argument("--boat-item-hit-fixture", action="store_true")
|
||||
parser.add_argument("--reference-only", action="store_true")
|
||||
parser.add_argument("--boat-leash-view-fixture", action="store_true")
|
||||
parser.add_argument("--boat-motion-diagnostic", action="store_true")
|
||||
parser.add_argument('--reinforcement-original-diagnostic', action='store_true', help='replay the original unisolated174/108 fixture, retaining its terrain prerequisite failure')
|
||||
parser.add_argument('--reinforcement-target-diagnostic', action='store_true', help='retain the separate creative-callee target relation record (full209)')
|
||||
@@ -318,6 +319,10 @@ def main():
|
||||
if args.selector_bounds_fixture:
|
||||
from selector_bounds_corpus import selector_bounds
|
||||
groups = [("setup", corpus_module.group_setup()), ("selector entity bounds", selector_bounds())]
|
||||
if args.boat_leash_view_fixture:
|
||||
from boat_leash_view_corpus import cases as boat_leash_views
|
||||
groups = [('setup', corpus_module.group_setup()),
|
||||
('boat live leash view', boat_leash_views(corpus_module.cmd, corpus_module.setup))]
|
||||
if args.merchant_crafted_fixture:
|
||||
from merchant_crafted_corpus import cases
|
||||
groups = [('merchant crafted statistics', cases())]
|
||||
|
||||
Reference in new issue
Block a user