mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
An anvil landing on turtle eggs drops its item on top of them, not inside
FallingBlockEntity.tick spawns the item of a block it cannot place with spawnAtLocation, at the entity's own position. Horizon popped it like a mined block, somewhere inside the eggs' cell, where it was pushed out sideways and ended up as far as 2.5 blocks away. The anvil's break sound (Fallable.onBrokenAfterFall) now plays only when the item drops, and the item's motion comes from its own random, not the level's. Part of #5858. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d928d1cd61
commit
3cbd09e448
5 files changed
+161
-33
No files matched your search
@@ -5696,12 +5696,9 @@ impl Game<'_> {
|
||||
// Entity.spawnAtLocation: an item entity where the block was.
|
||||
let item = self.res.facts().get(state).item.clone();
|
||||
if &*item != "minecraft:air" {
|
||||
let r = &mut self.world.random;
|
||||
let v = [
|
||||
r.next_double() * 0.2 - 0.1,
|
||||
0.2,
|
||||
r.next_double() * 0.2 - 0.1,
|
||||
];
|
||||
// new ItemEntity(level, x, y, z, stack): the motion is from the
|
||||
// item's own random, not the level's.
|
||||
let v = dispenser::new_item_motion();
|
||||
let stack = ItemStack {
|
||||
item,
|
||||
count: 1,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Landing impacts for falling blocks, before their placement/drop decision.
|
||||
use super::{FallingBlock, Game, Landing};
|
||||
use horizon_client::{falling_block, items::ItemStack};
|
||||
use horizon_client::falling_block;
|
||||
use horizon_core::{geometry::Vec3, position::BlockPos, random::RandomSource};
|
||||
use horizon_world::level::LevelEvent;
|
||||
|
||||
@@ -81,9 +81,12 @@ impl Game<'_> {
|
||||
pub(super) fn finish_falling_block(&mut self, block: FallingBlock, pos: [i32; 3]) {
|
||||
let name = self.res.facts().get(block.state).name.clone();
|
||||
let anvil = name.ends_with("anvil");
|
||||
// Fallable.onBrokenAfterFall: the anvil's, the pointed dripstone's and
|
||||
// the sulfur spike's level event.
|
||||
let sound_event = match &*name {
|
||||
"minecraft:pointed_dripstone" => Some(1045),
|
||||
"minecraft:sulfur_spike" => Some(1052),
|
||||
_ if anvil => Some(1029),
|
||||
_ => None,
|
||||
};
|
||||
if block.damage.cancel_drop {
|
||||
@@ -93,12 +96,6 @@ impl Game<'_> {
|
||||
pos: BlockPos::new(pos[0], pos[1], pos[2]),
|
||||
data: 0,
|
||||
});
|
||||
} else if anvil {
|
||||
self.level_events.push(LevelEvent::LevelEvent {
|
||||
id: 1029,
|
||||
pos: BlockPos::new(pos[0], pos[1], pos[2]),
|
||||
data: 0,
|
||||
});
|
||||
} else if self
|
||||
.res
|
||||
.facts()
|
||||
@@ -157,18 +154,9 @@ impl Game<'_> {
|
||||
data: 0,
|
||||
});
|
||||
}
|
||||
let item = self.res.facts().get(block.state).item.clone();
|
||||
if &*item != "minecraft:air" {
|
||||
self.pop_resource(
|
||||
pos,
|
||||
ItemStack {
|
||||
item,
|
||||
count: 1,
|
||||
damage: 0,
|
||||
extra: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
// FallingBlockEntity.tick: spawnAtLocation, from where the entity
|
||||
// came to rest (on top of a turtle egg, not inside its cell).
|
||||
self.drop_falling_block(block.state, block.position);
|
||||
}
|
||||
}
|
||||
fn falling_hurt_player(&mut self, position: Vec3, amount: f32, kind: &str) {
|
||||
|
||||
@@ -12,7 +12,9 @@ pub enum Landing {
|
||||
/// solidified); discard the entity.
|
||||
Placed(BlockStateId),
|
||||
/// The block could not be placed: discard the entity and, if the entity
|
||||
/// drops items, spawn the block's item (vanilla `spawnAtLocation`).
|
||||
/// drops items, run `Fallable.onBrokenAfterFall` (the anvil's level event
|
||||
/// 1029) and spawn the block's item from the entity's position (vanilla
|
||||
/// `spawnAtLocation`).
|
||||
Broken,
|
||||
/// The landing position holds a moving piston: keep falling.
|
||||
Continue,
|
||||
@@ -109,9 +111,6 @@ impl Behaviours {
|
||||
let would_continue = sim.is_free(sim.below_state(pos)) && (!concrete || !stuck_in_water);
|
||||
let would_survive = sim.can_survive(state, pos) && !would_continue;
|
||||
if !(may_replace && would_survive) {
|
||||
if self.instance_of(state, Class::Anvil) {
|
||||
sim.level_event(1029, pos, 0);
|
||||
}
|
||||
return Landing::Broken;
|
||||
}
|
||||
let mut placed = state;
|
||||
@@ -121,9 +120,6 @@ impl Behaviours {
|
||||
placed = self.table.set_bool(placed, self.props.waterlogged, true);
|
||||
}
|
||||
if !sim.set_block_and_update(pos, placed) {
|
||||
if self.instance_of(state, Class::Anvil) {
|
||||
sim.level_event(1029, pos, 0);
|
||||
}
|
||||
return Landing::Broken;
|
||||
}
|
||||
// Fallable.onLand
|
||||
|
||||
@@ -102,7 +102,10 @@ import net.minecraft.world.phys.Vec3;
|
||||
* motion, inventory slots, effects and death) and {@code end}, which writes each
|
||||
* player's lines to OUT. {@code boats on|off} records boat paddle metadata;
|
||||
* {@code hooks on|off} records the entity a fishing bobber hooks, by type;
|
||||
* {@code paddle NAME LEFT RIGHT} sends the two boolean paddle states.
|
||||
* {@code paddle NAME LEFT RIGHT} sends the two boolean paddle states;
|
||||
* {@code events on|off} records each level event: its id, block and data;
|
||||
* {@code spawns on|off} records each entity other than a player added to the
|
||||
* level, by type and position.
|
||||
*/
|
||||
public final class LocatorBarClient {
|
||||
private static volatile boolean tickSteps;
|
||||
@@ -112,6 +115,8 @@ public final class LocatorBarClient {
|
||||
private static volatile boolean boats;
|
||||
private static volatile boolean hooks;
|
||||
private static volatile boolean state;
|
||||
private static volatile boolean events;
|
||||
private static volatile boolean spawns;
|
||||
private static final Map<UUID, String> NAMES = new LinkedHashMap<>();
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
@@ -176,6 +181,8 @@ public final class LocatorBarClient {
|
||||
Boolean.parseBoolean(sides[0]), Boolean.parseBoolean(sides[1])));
|
||||
}
|
||||
case "state" -> state = w[1].equals("on");
|
||||
case "events" -> events = w[1].equals("on");
|
||||
case "spawns" -> spawns = w[1].equals("on");
|
||||
case "end" -> {
|
||||
try (PrintWriter out = new PrintWriter(Files.newBufferedWriter(output, StandardCharsets.UTF_8))) {
|
||||
for (Player p : players.values()) {
|
||||
@@ -484,6 +491,11 @@ public final class LocatorBarClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (spawns && p instanceof net.minecraft.network.protocol.game.ClientboundAddEntityPacket a
|
||||
&& a.getType() != net.minecraft.world.entity.EntityTypes.PLAYER) {
|
||||
record(String.format(java.util.Locale.ROOT, "spawn %s %.4f %.4f %.4f",
|
||||
net.minecraft.world.entity.EntityType.getKey(a.getType()), a.getX(), a.getY(), a.getZ()));
|
||||
}
|
||||
if (p instanceof net.minecraft.network.protocol.game.ClientboundAddEntityPacket a
|
||||
&& a.getType() == net.minecraft.world.entity.EntityTypes.PLAYER) {
|
||||
String who = NAMES.getOrDefault(a.getUUID(), "uuid?");
|
||||
@@ -524,6 +536,12 @@ public final class LocatorBarClient {
|
||||
Vec3 v = m.movement();
|
||||
record(String.format(java.util.Locale.ROOT, "motion %.4f %.4f %.4f", v.x, v.y, v.z));
|
||||
}
|
||||
} else if (p instanceof net.minecraft.network.protocol.game.ClientboundLevelEventPacket e) {
|
||||
if (events) {
|
||||
net.minecraft.core.BlockPos at = e.getPos();
|
||||
record("event " + e.getType() + " " + at.getX() + " " + at.getY() + " " + at.getZ()
|
||||
+ " " + e.getData());
|
||||
}
|
||||
} else if (p instanceof ClientboundKeepAlivePacket k) {
|
||||
send(new ServerboundKeepAlivePacket(k.getId()));
|
||||
} else if (p instanceof ClientboundPingPacket k) {
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Anvils landing on turtle eggs through real vanilla and Horizon servers.
|
||||
|
||||
An anvil cannot replace turtle eggs, so `FallingBlockEntity.tick` discards it
|
||||
where it came to rest, on top of the eggs: `Fallable.onBrokenAfterFall`
|
||||
(AnvilBlock: level event 1029) and `spawnAtLocation`, an item at the entity's
|
||||
own position with `new ItemEntity`'s motion (at most 0.1 sideways, 0.2 up).
|
||||
The item is added on top of the eggs, right under the anvil's centre, and
|
||||
about half the time falls off them. Horizon dropped it like a mined block,
|
||||
somewhere inside the eggs' cell, where it was pushed out sideways tick after
|
||||
tick and ended up to 2.5 blocks away (median 1.5, vanilla's 0.6). Each
|
||||
item's position when it is added is compared; its motion is the item's own
|
||||
random. Horizon tells clients about an entity added after `tick_lan` (falling
|
||||
blocks tick later) a tick late, after its first move: 0.2 up less 0.04 of
|
||||
gravity, and at most 0.1 sideways. An item added at the anvil's centre on
|
||||
top of the eggs, either then or one tick on, shows as `at-anvil`.
|
||||
The eggs stay: a falling block is no LivingEntity
|
||||
(`TurtleEggBlock.canDestroyEgg`). Controls: an anvil that drops no item
|
||||
breaks silently; one landing on stone is placed (level event 1031).
|
||||
"""
|
||||
import json
|
||||
|
||||
try:
|
||||
from . import test_locator_bar_parity as net
|
||||
except ImportError:
|
||||
import test_locator_bar_parity as net
|
||||
|
||||
STEP_SECONDS = 2.0
|
||||
EGGS = [(0, 1), (4, 2), (8, 3), (12, 4), (16, 1), (20, 2), (24, 3), (28, 4)]
|
||||
NO_DROP = 32
|
||||
STONE = 36
|
||||
|
||||
|
||||
def cmd(command):
|
||||
return 'cmd Alice ' + command
|
||||
|
||||
|
||||
def anvil(x, extra=''):
|
||||
return cmd(f'summon minecraft:falling_block {x}.5 86 0.5 {{BlockState:"minecraft:anvil",Time:1{extra}}}')
|
||||
|
||||
|
||||
SCRIPT = [('setup', ['chat on', cmd('gamerule spawn_mobs false'),
|
||||
cmd('gamerule random_tick_speed 0'), cmd('gamerule mob_griefing true'),
|
||||
cmd('fill -2 79 -4 42 79 2 minecraft:stone'),
|
||||
cmd('fill -2 80 -4 42 90 2 minecraft:air'),
|
||||
cmd('tp @s 18.5 80 -3.5')])]
|
||||
SCRIPT.append(('eggs', [cmd(f'setblock {x} 80 0 minecraft:turtle_egg[eggs={n}]')
|
||||
for x, n in EGGS + [(NO_DROP, 4)]]))
|
||||
SCRIPT.append(('drop', ['events on', 'spawns on'] + [anvil(x) for x, _ in EGGS]
|
||||
+ [anvil(NO_DROP, ',DropItem:0b'), anvil(STONE)]))
|
||||
SCRIPT.append(('fall', []))
|
||||
SCRIPT.append(('settle', ['events off', 'spawns off']))
|
||||
SCRIPT.append(('settle-2', []))
|
||||
EXPECTED = []
|
||||
|
||||
|
||||
def check(name, condition, result):
|
||||
SCRIPT.append((f'{name}-check', [cmd(f'execute {condition}')]))
|
||||
EXPECTED.append(f'{name}-check {result}')
|
||||
|
||||
|
||||
for x, n in EGGS:
|
||||
check(f'eggs-{x}', f'if block {x} 80 0 minecraft:turtle_egg[eggs={n}]', 'pass')
|
||||
check('no-drop-eggs', f'if block {NO_DROP} 80 0 minecraft:turtle_egg[eggs=4]', 'pass')
|
||||
check('no-drop-item', f'if entity @e[type=minecraft:item,x={NO_DROP}.5,y=80.2,z=0.5,distance=..1.5]', 'fail')
|
||||
check('stone-anvil', f'if block {STONE} 80 0 minecraft:anvil', 'pass')
|
||||
|
||||
|
||||
EGG_TOP = 80 + 7 / 16
|
||||
|
||||
|
||||
def at_anvil(spawn):
|
||||
"""`minecraft:item X Y Z` as `minecraft:item CELL at-anvil` when the item
|
||||
was added at its cell's centre on the eggs, now or a tick ago."""
|
||||
kind, *xyz = spawn.split()
|
||||
if kind != 'minecraft:item':
|
||||
return spawn
|
||||
x, y, z = map(float, xyz)
|
||||
cell = int(x // 1)
|
||||
moved = max(abs(x - cell - 0.5), abs(z - 0.5))
|
||||
if (moved < 1e-4 and abs(y - EGG_TOP) < 1e-4) or (moved <= 0.1 + 1e-4 and abs(y - EGG_TOP - 0.16) < 1e-4):
|
||||
return f'{kind} {cell} at-anvil'
|
||||
return spawn
|
||||
|
||||
|
||||
def shown(lines):
|
||||
out, events = [], []
|
||||
for line in lines:
|
||||
parts = line.split(' ', 3)
|
||||
if len(parts) >= 3 and parts[2] in ('event', 'spawn'):
|
||||
# One tick's events and entities may come in any order.
|
||||
what = at_anvil(parts[3]) if parts[2] == 'spawn' else parts[3]
|
||||
events.append(f'{parts[1].removeprefix("step=")} {parts[2]} {what}')
|
||||
continue
|
||||
if len(parts) < 4 or not parts[1].endswith('-check') or parts[2] != 'chat':
|
||||
continue
|
||||
value = json.loads(parts[3])
|
||||
# A failed test comes wrapped in the command error's text component.
|
||||
if isinstance(value, dict) and 'extra' in value:
|
||||
value = next((v for v in value['extra'] if isinstance(v, dict)
|
||||
and v.get('translate', '').startswith('commands.execute.conditional.')), value)
|
||||
key = value.get('translate', '') if isinstance(value, dict) else ''
|
||||
if key.startswith('commands.execute.conditional.'):
|
||||
out.append(f'{parts[1].removeprefix("step=")} {key.rsplit(".", 1)[1]}')
|
||||
return sorted(events) + out
|
||||
|
||||
|
||||
def check_vanilla(native, all_lines):
|
||||
wanted = sorted([f'drop event 1029 {x} 80 0 0' for x, _ in EGGS] + [f'drop event 1031 {STONE} 80 0 0']
|
||||
+ [f'drop spawn minecraft:item {x} at-anvil' for x, _ in EGGS]
|
||||
+ [f'drop spawn minecraft:falling_block {x}.5000 86.0000 0.5000'
|
||||
for x in [x for x, _ in EGGS] + [NO_DROP, STONE]])
|
||||
if native != wanted + EXPECTED:
|
||||
return f'vanilla did not exercise the intended cases: {native!r}'
|
||||
# Vanilla's own items are added before their first tick, exactly there.
|
||||
exact = {f'spawn minecraft:item {x}.5000 80.4375 0.5000' for x, _ in EGGS}
|
||||
if not exact <= {line.split(' ', 2)[2] for line in all_lines if ' spawn ' in line}:
|
||||
return 'vanilla added an item away from its anvil'
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
lines = net.one_player('anvil-turtle-egg-parity', SCRIPT, shown, check_vanilla, __doc__,
|
||||
step_seconds=STEP_SECONDS)
|
||||
print(f'anvil turtle eggs: {len(lines)} results match vanilla')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user