mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 17:21:49 -10:00
Thrown potions and eyes of ender fly on a server's clients
A splash or lingering potion, an experience bottle or an eye of ender a player threw was never an entity a server tracked: on single player's server thread, for LAN guests and on a dedicated server the item was used up and nothing flew. The server now numbers them like its other projectiles and tracks them as vanilla's entities: splash_potion, lingering_potion and experience_bottle with ThrowableItemProjectile.DATA_ITEM_STACK (the potion, for its colour), and eye_of_ender. A client draws them where the server puts them and drops them when the server removes them. lan_e2e's new `thrown` check: the host and the guest each throw a splash potion and an eye of ender, and both sides see them fly. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0d8249e124
commit
c5fe5d4bcf
5 files changed
+212
-10
No files matched your search
@@ -97,8 +97,58 @@ impl EndState {
|
||||
}
|
||||
}
|
||||
|
||||
impl EndState {
|
||||
/// The eyes' entity ids, for a server to number them.
|
||||
pub(super) fn eye_ids(&mut self) -> impl Iterator<Item = &mut u32> {
|
||||
self.eyes.iter_mut().map(|e| &mut e.id)
|
||||
}
|
||||
/// The numbered eyes as a server's clients track them: (id, position, motion).
|
||||
pub(super) fn eyes_tracked(&self) -> impl Iterator<Item = (u32, [f64; 3], [f64; 3])> {
|
||||
self.eyes
|
||||
.iter()
|
||||
.filter(|e| e.id != 0)
|
||||
.map(|e| (e.id, e.position.to_array(), e.velocity.to_array()))
|
||||
}
|
||||
/// `handleAddEntity` for an eye of ender on a server's client: it flies
|
||||
/// where the server says.
|
||||
pub(super) fn add_remote_eye(&mut self, id: u32, at: [f64; 3], velocity: [f64; 3]) {
|
||||
self.eyes.retain(|e| e.id != id);
|
||||
let at = glam::DVec3::from_array(at);
|
||||
self.eyes.push(EyeOfEnder {
|
||||
id,
|
||||
position: at,
|
||||
previous: at,
|
||||
velocity: glam::DVec3::from_array(velocity),
|
||||
target: at,
|
||||
life: 0,
|
||||
survive: false,
|
||||
});
|
||||
}
|
||||
/// One tick of a server's client: `step` moves each toward where the
|
||||
/// server put it.
|
||||
pub(super) fn step_remote_eyes(&mut self, mut step: impl FnMut(u32, [f64; 3]) -> [f64; 3]) {
|
||||
for e in &mut self.eyes {
|
||||
e.previous = e.position;
|
||||
e.life += 1;
|
||||
e.position = glam::DVec3::from_array(step(e.id, e.position.to_array()));
|
||||
}
|
||||
}
|
||||
pub(super) fn remove_remote_eye(&mut self, id: u32) {
|
||||
self.eyes.retain(|e| e.id != id);
|
||||
}
|
||||
pub(super) fn clear_remote_eyes(&mut self) {
|
||||
self.eyes.clear();
|
||||
}
|
||||
/// Where each eye is, for `status`.
|
||||
pub(super) fn eye_positions(&self) -> impl Iterator<Item = (u32, [f64; 3])> {
|
||||
self.eyes.iter().map(|e| (e.id, e.position.to_array()))
|
||||
}
|
||||
}
|
||||
|
||||
/// The EyeOfEnder entity.
|
||||
struct EyeOfEnder {
|
||||
/// Its entity id once a server's clients track it (0 until then).
|
||||
id: u32,
|
||||
position: glam::DVec3,
|
||||
previous: glam::DVec3,
|
||||
velocity: glam::DVec3,
|
||||
@@ -1023,6 +1073,7 @@ impl Game<'_> {
|
||||
};
|
||||
let survive = self.world.random.next_int_bound(5).unwrap_or(0) > 0;
|
||||
self.end.eyes.push(EyeOfEnder {
|
||||
id: 0,
|
||||
position: start,
|
||||
previous: start,
|
||||
velocity: glam::DVec3::ZERO,
|
||||
|
||||
@@ -277,6 +277,8 @@ impl Game<'_> {
|
||||
self.rockets.iter_mut().for_each(|r| give(&mut r.id));
|
||||
self.crystals.iter_mut().for_each(|c| give(&mut c.id));
|
||||
self.bolts.iter_mut().for_each(|b| give(&mut b.id));
|
||||
self.potions.thrown_ids().for_each(&mut give);
|
||||
self.end.eye_ids().for_each(&mut give);
|
||||
}
|
||||
|
||||
/// Primed TNT, falling blocks, projectiles, tridents, firework rockets and
|
||||
@@ -359,6 +361,26 @@ impl Game<'_> {
|
||||
}
|
||||
out.push(e);
|
||||
}
|
||||
// Thrown potions and bottles: ThrowableItemProjectile.DATA_ITEM_STACK
|
||||
// (the potion's colour), which a plain bottle's type already has.
|
||||
for (id, kind, stack, position, velocity) in self.potions.thrown_tracked() {
|
||||
let mut e = state(id, kind, at(position), 0.0, false);
|
||||
e.velocity = at(velocity);
|
||||
if stack.extra.is_some() || stack.count != 1 {
|
||||
e.meta.push((
|
||||
8,
|
||||
serializer::ITEM_STACK,
|
||||
Value::Item(hub.stack(&Some(stack.clone()))),
|
||||
));
|
||||
}
|
||||
out.push(e);
|
||||
}
|
||||
// EyeOfEnder: its item is the plain eye (DATA_ITEM_STACK's default).
|
||||
for (id, position, velocity) in self.end.eyes_tracked() {
|
||||
let mut e = state(id, "minecraft:eye_of_ender", position, 0.0, false);
|
||||
e.velocity = velocity;
|
||||
out.push(e);
|
||||
}
|
||||
for t in self.tridents.iter().filter(|t| t.id != 0) {
|
||||
let mut e = state(t.id, "minecraft:trident", at(t.position), t.yaw, false);
|
||||
e.pitch = t.pitch;
|
||||
|
||||
@@ -18,6 +18,8 @@ use horizon_core::{geometry::Vec3, random::RandomSource};
|
||||
/// A thrown splash or lingering potion in flight.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct ThrownPotion {
|
||||
/// Its entity id once a server's clients track it (0 until then).
|
||||
id: u32,
|
||||
stack: ItemStack,
|
||||
/// The position before the last tick, for drawing between ticks.
|
||||
previous: Vec3,
|
||||
@@ -47,6 +49,7 @@ impl Game<'_> {
|
||||
/// player's throws and dispensers both end up here).
|
||||
pub(super) fn launch_thrown(&mut self, stack: ItemStack, position: Vec3, velocity: Vec3) {
|
||||
self.potions.thrown.push(ThrownPotion {
|
||||
id: 0,
|
||||
stack,
|
||||
previous: position,
|
||||
position,
|
||||
@@ -90,19 +93,78 @@ impl PotionState {
|
||||
pub(super) fn thrown_entities(&self) -> Vec<(&'static str, Vec3, Vec3)> {
|
||||
self.thrown
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let kind = match &*t.stack.item {
|
||||
"minecraft:lingering_potion" => "minecraft:lingering_potion",
|
||||
"minecraft:experience_bottle" => "minecraft:experience_bottle",
|
||||
_ => "minecraft:splash_potion",
|
||||
};
|
||||
(kind, t.position, t.velocity)
|
||||
})
|
||||
.map(|t| (thrown_kind(&t.stack), t.position, t.velocity))
|
||||
.collect()
|
||||
}
|
||||
pub(super) fn clear_thrown(&mut self) {
|
||||
self.thrown.clear();
|
||||
}
|
||||
/// Their entity ids, for a server to number them.
|
||||
pub(super) fn thrown_ids(&mut self) -> impl Iterator<Item = &mut u32> {
|
||||
self.thrown.iter_mut().map(|t| &mut t.id)
|
||||
}
|
||||
/// The numbered ones as a server's clients track them: (id, entity
|
||||
/// type, item, position, motion).
|
||||
pub(super) fn thrown_tracked(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (u32, &'static str, &ItemStack, Vec3, Vec3)> {
|
||||
self.thrown.iter().filter(|t| t.id != 0).map(|t| {
|
||||
(
|
||||
t.id,
|
||||
thrown_kind(&t.stack),
|
||||
&t.stack,
|
||||
t.position,
|
||||
t.velocity,
|
||||
)
|
||||
})
|
||||
}
|
||||
/// `handleAddEntity` for a thrown potion or bottle on a server's client:
|
||||
/// drawn where the server says (its item comes in its entity data).
|
||||
pub(super) fn add_remote_thrown(&mut self, id: u32, kind: &str, at: Vec3, velocity: Vec3) {
|
||||
self.thrown.retain(|t| t.id != id);
|
||||
let item = match kind {
|
||||
"minecraft:lingering_potion" | "minecraft:experience_bottle" => kind,
|
||||
_ => "minecraft:splash_potion",
|
||||
};
|
||||
self.thrown.push(ThrownPotion {
|
||||
id,
|
||||
stack: ItemStack::new(item, 1),
|
||||
previous: at,
|
||||
position: at,
|
||||
velocity,
|
||||
age: 0,
|
||||
});
|
||||
}
|
||||
/// ThrowableItemProjectile.DATA_ITEM_STACK: the potion it shows.
|
||||
pub(super) fn remote_thrown_item(&mut self, id: u32, stack: ItemStack) {
|
||||
if let Some(t) = self.thrown.iter_mut().find(|t| t.id == id) {
|
||||
t.stack = stack;
|
||||
}
|
||||
}
|
||||
/// One tick of a server's client: `step` moves each toward where the
|
||||
/// server put it.
|
||||
pub(super) fn step_remote_thrown(&mut self, mut step: impl FnMut(u32, Vec3) -> Vec3) {
|
||||
for t in &mut self.thrown {
|
||||
t.previous = t.position;
|
||||
t.age += 1;
|
||||
t.position = step(t.id, t.position);
|
||||
}
|
||||
}
|
||||
pub(super) fn remove_remote_thrown(&mut self, id: u32) {
|
||||
self.thrown.retain(|t| t.id != id);
|
||||
}
|
||||
pub(super) fn has_thrown(&self, id: u32) -> bool {
|
||||
self.thrown.iter().any(|t| t.id == id)
|
||||
}
|
||||
}
|
||||
|
||||
/// The entity type of a thrown potion or bottle.
|
||||
fn thrown_kind(stack: &ItemStack) -> &'static str {
|
||||
match &*stack.item {
|
||||
"minecraft:lingering_potion" => "minecraft:lingering_potion",
|
||||
"minecraft:experience_bottle" => "minecraft:experience_bottle",
|
||||
_ => "minecraft:splash_potion",
|
||||
}
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
|
||||
@@ -79,6 +79,15 @@ impl Game<'_> {
|
||||
self.rockets
|
||||
.push(super::fireworks::Rocket::remote(id, at, velocity));
|
||||
}
|
||||
"minecraft:splash_potion"
|
||||
| "minecraft:lingering_potion"
|
||||
| "minecraft:experience_bottle" => {
|
||||
self.potions.add_remote_thrown(id, kind, at, velocity);
|
||||
}
|
||||
"minecraft:eye_of_ender" => {
|
||||
self.end
|
||||
.add_remote_eye(id, position, [velocity.x, velocity.y, velocity.z])
|
||||
}
|
||||
"minecraft:end_crystal" => self.crystals.push(super::dragon::Crystal {
|
||||
id,
|
||||
position: at,
|
||||
@@ -105,6 +114,17 @@ impl Game<'_> {
|
||||
/// `handleSetEntityData` for these types: what changes how they look.
|
||||
pub(super) fn remote_projectile_data(&mut self, id: i32, values: &[(u8, i32, Value)]) {
|
||||
let id = id as u32;
|
||||
if self.potions.has_thrown(id) {
|
||||
// ThrowableItemProjectile.DATA_ITEM_STACK: the potion it is.
|
||||
for (index, _, value) in values {
|
||||
if let (8, Value::Item(stack)) = (index, value)
|
||||
&& let Some(stack) = self.to_stack(stack)
|
||||
{
|
||||
self.potions.remote_thrown_item(id, stack);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (index, _, value) in values {
|
||||
if let Some(t) = self.tnts.iter_mut().find(|t| t.id == id) {
|
||||
// PrimedTnt.DATA_FUSE_ID.
|
||||
@@ -184,6 +204,11 @@ impl Game<'_> {
|
||||
c.time += 1;
|
||||
c.position = step(c.id, c.position);
|
||||
}
|
||||
self.potions.step_remote_thrown(&mut step);
|
||||
self.end.step_remote_eyes(|id, at| {
|
||||
let at = step(id, Vec3::new(at[0], at[1], at[2]));
|
||||
[at.x, at.y, at.z]
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn remove_remote_projectile(&mut self, id: u32) {
|
||||
@@ -193,6 +218,8 @@ impl Game<'_> {
|
||||
self.tridents.retain(|t| t.id != id);
|
||||
self.rockets.retain(|r| r.id != id);
|
||||
self.crystals.retain(|c| c.id != id);
|
||||
self.potions.remove_remote_thrown(id);
|
||||
self.end.remove_remote_eye(id);
|
||||
}
|
||||
|
||||
/// Every one a server sent (a new connection, level or dimension).
|
||||
@@ -203,6 +230,8 @@ impl Game<'_> {
|
||||
self.tridents.clear();
|
||||
self.rockets.clear();
|
||||
self.crystals.clear();
|
||||
self.potions.clear_thrown();
|
||||
self.end.clear_remote_eyes();
|
||||
}
|
||||
|
||||
/// Their kind and place, for `status`.
|
||||
@@ -225,6 +254,12 @@ impl Game<'_> {
|
||||
for p in &self.creatures.projectiles {
|
||||
out.push(json!({"id": p.id, "kind": format!("{:?}", p.kind).split([' ', '{']).next().unwrap_or(""), "position": at(p.position)}));
|
||||
}
|
||||
for (kind, p, _) in self.potions.thrown_entities() {
|
||||
out.push(json!({"kind": kind, "position": at(p)}));
|
||||
}
|
||||
for (id, p) in self.end.eye_positions() {
|
||||
out.push(json!({"id": id, "kind": "minecraft:eye_of_ender", "position": p}));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
+34
-2
@@ -107,6 +107,8 @@ address. The script then checks, from both sides, what a guest's game used to ge
|
||||
guest as the host has them (kind, place, rotation, what they hold or wear, the variant).
|
||||
- projectiles: primed TNT (one falling), an end crystal and an arrow that lands reach the guest where
|
||||
the host has them; a falling block lands on both sides.
|
||||
- thrown: a splash potion and an eye of ender the host and the guest each throw fly on both
|
||||
sides (#1363, #1391);
|
||||
- fireball: a fire charge a dispenser shoots onto stone lights a fire both sides see, and the fire
|
||||
goes out on its own within a minute (it ticks: FireBlock.tick on a floor that doesn't burn)
|
||||
|
||||
@@ -208,7 +210,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
GUEST_NAME = 'Guest'
|
||||
# The host plays single player on its server thread (--server-thread).
|
||||
SERVER_THREAD = False
|
||||
CHECKS = ['tab', 'self', 'armour', 'nametags', 'walls', 'chest', 'crafting', 'sign', 'damage', 'mobs', 'hazards', 'pvp', 'arrows', 'kills', 'hurt', 'looks', 'leads', 'respawn', 'containers', 'feedback', 'titles', 'chat', 'commands', 'recipes', 'machines', 'brewing', 'loom', 'stonecutter', 'grindstone', 'anvil', 'smithing', 'enchanting', 'beacon', 'crafter', 'lectern', 'cartography', 'trials', 'clicks', 'sleep', 'predict', 'push', 'eat', 'eating', 'xp', 'objects', 'projectiles', 'worn', 'blast', 'barter', 'deflect', 'fireball', 'light', 'time', 'swings', 'cracks', 'plates', 'effects', 'portals', 'distance', 'growth', 'flight', 'ladder', 'boats', 'minecarts', 'frames', 'lightning', 'riding', 'mounts', 'worldspawn', 'beds', 'shield', 'deathmining', 'contacts', 'absorption', 'breath', 'lava', 'levels', 'drops', 'underwater']
|
||||
CHECKS = ['tab', 'self', 'armour', 'nametags', 'walls', 'chest', 'crafting', 'sign', 'damage', 'mobs', 'hazards', 'pvp', 'arrows', 'kills', 'hurt', 'looks', 'leads', 'respawn', 'containers', 'feedback', 'titles', 'chat', 'commands', 'recipes', 'machines', 'brewing', 'loom', 'stonecutter', 'grindstone', 'anvil', 'smithing', 'enchanting', 'beacon', 'crafter', 'lectern', 'cartography', 'trials', 'clicks', 'sleep', 'predict', 'push', 'eat', 'eating', 'xp', 'objects', 'projectiles', 'thrown', 'worn', 'blast', 'barter', 'deflect', 'fireball', 'light', 'time', 'swings', 'cracks', 'plates', 'effects', 'portals', 'distance', 'growth', 'flight', 'ladder', 'boats', 'minecarts', 'frames', 'lightning', 'riding', 'mounts', 'worldspawn', 'beds', 'shield', 'deathmining', 'contacts', 'absorption', 'breath', 'lava', 'levels', 'drops', 'underwater']
|
||||
|
||||
|
||||
class Game:
|
||||
@@ -2612,6 +2614,35 @@ def check_projectiles(host, guest, out):
|
||||
return f'{len(theirs)} identical, fuses (host, guest) {fuses}'
|
||||
|
||||
|
||||
def check_thrown(host, guest, out):
|
||||
spots = apart(host, guest)
|
||||
result = {}
|
||||
for label, player, other in (('host', host, guest), ('guest', guest, host)):
|
||||
x, y, z = spots[label]
|
||||
player('command', command='gamemode survival')
|
||||
for item, kind in (('splash_potion', 'minecraft:splash_potion'), ('ender_eye', 'minecraft:eye_of_ender')):
|
||||
player('command', command=f'item replace entity @s weapon.mainhand with minecraft:{item}')
|
||||
player.wait(10)
|
||||
# Up at 45 degrees, into open sky.
|
||||
player.raw('look-at', f'{x + 0.5:.2f}', f'{y + 11.6:.2f}', f'{z + 10.5:.2f}')
|
||||
player.raw('use-item')
|
||||
seen = {'own': 0, 'other': 0}
|
||||
for _ in range(10):
|
||||
player.wait(2)
|
||||
for side, game in (('own', player), ('other', other)):
|
||||
if any(p['kind'] == kind for p in game('status')['projectiles']):
|
||||
seen[side] += 1
|
||||
if all(seen.values()):
|
||||
break
|
||||
player.shot(out, f'thrown-{label}-{item}')
|
||||
other.shot(out, f'thrown-{label}-{item}-seen')
|
||||
player.wait(100) # it lands or drops
|
||||
result[f'{label} {item}'] = seen
|
||||
for name, seen in result.items():
|
||||
assert all(seen.values()), f'{name}: seen in flight (samples) {result}'
|
||||
return result
|
||||
|
||||
|
||||
def check_blast(host, guest, out):
|
||||
guest('command', command='gamemode survival')
|
||||
guest('command', command='clear @s')
|
||||
@@ -3945,7 +3976,8 @@ CHECK_FNS = {'tab': check_tab, 'self': check_self, 'armour': check_armour,
|
||||
'lava': check_lava, 'levels': check_levels, 'drops': check_drops,
|
||||
'underwater': check_underwater,
|
||||
'looks': check_looks,
|
||||
'leads': check_leads}
|
||||
'leads': check_leads,
|
||||
'thrown': check_thrown}
|
||||
|
||||
|
||||
def run(ctl, out, resources, checks):
|
||||
|
||||
Reference in new issue
Block a user