Compare commits

...
Author SHA1 Message Date
Ludovic Marie 57fe750035 Merge pull request #2642 from kacpherek/fix/pick-item-server
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:59:54 +01:00
Ludovic Marie 3849e37393 Merge pull request #2640 from kacpherek/fix/hopper-entity-containers
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:59:48 +01:00
Ludovic Marie 9b091ed657 Merge pull request #2418 from kacpherek/fix/announce-player-achievements
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:59:43 +01:00
Ludovic Marie 777e0e2eb5 Merge pull request #2416 from kacpherek/fix/riptide-lift-collision
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:56:37 +01:00
Ludovic Marie 04ecdee7a2 Merge pull request #2415 from kacpherek/fix/op-permission-level
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:56:33 +01:00
Ludovic Marie 6ddc3226d4 Merge pull request #2410 from kacpherek/fix/enforce-whitelist
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:56:29 +01:00
Ludovic Marie 1903865cd5 Merge pull request #2409 from kacpherek/fix/flying-edge-backoff
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:56:25 +01:00
Ludovic Marie d5d54a8775 Merge pull request #2405 from kacpherek/fix/lan-join-leave-host-chat
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:56:21 +01:00
Ludovic Marie c27e2d2c2d Merge pull request #2404 from kacpherek/fix/sulfur-cube-archetype-physics
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:52:58 +01:00
Ludovic Marie d463d793a9 Merge pull request #2398 from kacpherek/fix/lodestone-compass-use
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:52:54 +01:00
Ludovic Marie f10425675c Merge pull request #2395 from kacpherek/fix/horse-armor-protects
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:52:50 +01:00
Ludovic Marie aa8ab4253d Merge pull request #2392 from kacpherek/fix/loot-set-name
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:52:46 +01:00
Ludovic Marie fa56070097 Merge pull request #2390 from kacpherek/fix/block-loot-copy-components
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:52:41 +01:00
Ludovic Marie dd5863edc2 Merge pull request #2385 from kacpherek/fix/lightning-sky-flash
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:48:05 +01:00
Ludovic Marie 37cabdcb02 Merge pull request #2381 from kacpherek/fix/hud-text-above-bars
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:48:00 +01:00
Ludovic Marie a40ef4eee0 Merge pull request #2379 from kacpherek/fix/sculk-catalyst-nonplayer-kills
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:47:56 +01:00
Ludovic Marie 914a6956e9 Merge pull request #2378 from kacpherek/fix/sky-body-rain-brightness
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:47:51 +01:00
Ludovic Marie 698b49e3a5 Merge pull request #2362 from kacpherek/fix/shulker-peaceful
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:47:46 +01:00
Ludovic Marie ec067f1e39 Merge pull request #2353 from kacpherek/fix/piglin-hurt-drops-admired-item
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:42:07 +01:00
Ludovic Marie 6081a66629 Merge pull request #2352 from kacpherek/fix/portal-pigmen-cooldown
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:42:03 +01:00
Ludovic Marie ee69413a68 Merge pull request #2351 from kacpherek/fix/ice-melts-when-mined
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:41:59 +01:00
Ludovic Marie fc4e7af854 Merge pull request #2344 from kacpherek/fix/creeper-fall-swell
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:41:54 +01:00
Ludovic Marie 0e069c31d7 Merge pull request #2243 from kacpherek/fix/bee-flower-effects
🤖 Agent: Read against the vanilla 26.3 sources (Bees get an effect from a wither rose or open eyeblossom instead of breeding) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread; main merged in since, clippy clean. Thanks @kacpherek!
2026-10-27 14:41:50 +01:00
Ludovic Marie fbbff39e07 Merge pull request #2341 from kacpherek/fix/fishing-rain-biome
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:36:42 +01:00
Ludovic Marie 49c2000de3 Merge pull request #2339 from kacpherek/fix/fishing-pickup-delay
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:36:37 +01:00
Ludovic Marie f561d4845b Merge pull request #2336 from kacpherek/fix/zombie-drowned-conversion-stops
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:36:31 +01:00
Ludovic Marie d6c4e5a027 Merge pull request #2333 from kacpherek/fix/farmer-requested-items
🤖 Agent: Proven in union R1 (branch union/cg19-r1, main + 83 of kacpherek's PRs): the 98 harnesses it can affect, clippy -D warnings, tests, lan_e2e in-frame and on the server thread.
2026-10-27 14:36:26 +01:00
Ludovic Marie ad271604af Merge pull request #2262 from kacpherek/fix/2209-flat-world-horizon
🤖 Agent: Read against the vanilla 26.3 sources (Flat worlds: no black horizon, the dark disc and void fog use the flat horizon) and proven in contrib-gate-21's union of 72 kacpherek PRs: 95 parity harnesses on playhorizon and the Mac mini, clippy, 1534 tests, lan_e2e 23 checks in-frame and with --server-thread. Thanks @kacpherek!
2026-10-27 14:36:22 +01:00
Ludovic MarieandClaude Opus 5.5 73a9345c43 Merge origin/main into fix/bee-flower-effects
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:57:00 +01:00
Ludovic MarieandClaude Opus 5.5 d511139408 Merge origin/main into fix/bee-flower-effects
Resolved as in contrib-gate-21's union of the kacpherek PRs: both sides kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:48:49 +01:00
Ludovic MarieandClaude Opus 5.5 3093f8c594 Merge origin/main into fix/2209-flat-world-horizon (picks up main's rabbit-arm fix, #2716)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:37:28 +01:00
Ludovic MarieandClaude Opus 5.5 905fd0d193 Merge origin/main into fix/2209-flat-world-horizon
Remote keeps main's use_item field and the PR's public dimension field.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 11:26:21 +01:00
Kacpherek f92cf816b1 Pick blocks and entities on the server like vanilla 2026-10-27 06:16:32 +01:00
Kacpherek 0c9bddecda Let block hoppers move items into and out of minecart containers 2026-10-27 06:11:01 +01:00
Kacpherek e872d3ae54 Send isFlat in every dimension and use the dimension's min y for the horizon 2026-10-26 20:26:12 +01:00
Kacpherek aed4e26f59 Apply the legacy announce-player-achievements key
The key was parsed and dropped. It now sets the show_advancement_messages game rule when the dedicated server starts, like DedicatedServer.initServer.
2026-10-26 19:54:23 +01:00
Kacpherek b5adad15eb Stop the riptide ground lift at ceilings 2026-10-26 19:53:42 +01:00
Kacpherek d17df3ca05 Give /op the level from op-permission-level
The console's /op always granted level 4. It now grants the permission level set in server.properties, like PlayerList.op.
2026-10-26 19:53:41 +01:00
Kacpherek 9ae3882c1a Kick unlisted players when enforce-whitelist is on
Whitelist remove, reload and on, and deop, now disconnect online players who are not on the whitelist, like MinecraftServer.kickUnlistedPlayers.
2026-10-26 19:52:46 +01:00
Kacpherek 89c195fa6e Don't back off from edges while flying 2026-10-26 19:51:46 +01:00
Kacpherek 8a309c393d Show join and leave messages in a LAN host's own chat 2026-10-26 19:50:50 +01:00
Kacpherek 58a99a2ce9 Sulfur cubes bounce, slide and keep speed by their block's archetype 2026-10-26 19:49:41 +01:00
Kacpherek c439b806b8 Let a compass lock onto a lodestone
Using a compass on a lodestone now sets its lodestone target (in place for a single compass in survival, otherwise one is split off), and the target clears when the lodestone is gone. The target also travels in item packets on the server thread.
2026-10-26 19:40:59 +01:00
Kacpherek 2886b8f9ba Horse armor reduces the damage a horse takes 2026-10-26 19:39:31 +01:00
Kacpherek dc36089156 Apply the set_name loot function to generated items 2026-10-26 19:37:51 +01:00
Kacpherek 08c959807d Broken heads keep their skin and silk-touched hives keep their bees and honey 2026-10-26 19:37:01 +01:00
Kacpherek b8d0590d6d Lightning flash: mix the sky colour toward the flash tint instead of replacing it 2026-10-26 19:34:47 +01:00
Kacpherek bc2dd35cc3 Raise the held item name and action bar above extra heart, armor and mount rows 2026-10-26 19:33:57 +01:00
Kacpherek 849b5b663b Let a sculk catalyst take the experience of mobs killed without a player 2026-10-26 19:33:09 +01:00
Kacpherek b65b0a6a7d Dim the sun and moon in rain and turn the moon texture the right way up 2026-10-26 19:32:59 +01:00
Kacpherek 605e8a1488 Stop shulkers from targeting and shooting in Peaceful 2026-10-26 19:18:08 +01:00
Kacpherek 6823e34451 Let a piglin let go of the gold it admires when a player hits it 2026-10-26 19:12:31 +01:00
Kacpherek f25fbb6f2e Give zombified piglins that spawn in a nether portal a portal cooldown 2026-10-26 19:11:24 +01:00
Kacpherek 872409ea57 Mined ice melts into water unless the tool has silk touch 2026-10-26 19:08:19 +01:00
Kacpherek 5657080884 Swell a creeper's fuse when it falls 2026-10-26 18:51:45 +01:00
Kacpherek fe167781c4 Give the fishing rain bonus only where rain falls 2026-10-26 18:50:36 +01:00
Kacpherek 0d02a4c722 Let a caught fish be picked up right away 2026-10-26 18:50:04 +01:00
Kacpherek f2424043f3 Stop zombie drowning conversion when it leaves the water 2026-10-26 18:49:12 +01:00
Kacpherek 45996d8d78 Match the farmer's requested items to vanilla
The farmer's list had food, torchflower seeds and pitcher pods and lacked bone meal. Vanilla requests wheat, wheat seeds, beetroot seeds and bone meal. Food is already picked up and traded through its villager food value.
2026-10-26 18:48:10 +01:00
Kacpherek 94592a35ac Use the flat world's horizon height for the dark disc and void fog 2026-10-26 15:16:33 +01:00
Kacpherek 1b4fe0f067 Wither rose and open eyeblossom give bees an effect instead of breeding them 2026-10-26 15:15:28 +01:00
43 changed files with 1132 additions and 135 deletions

No files matched your search

@@ -24,6 +24,7 @@ fn stack(spec: &str) -> LootStack {
exploration: None,
ominous_amplifier: None,
stew_effects: Vec::new(),
names: Vec::new(),
};
for e in parts {
let (name, level) = e.rsplit_once(':').unwrap_or((e, "1"));
@@ -223,6 +223,7 @@ fn decorations(c: &Value, atlas: &HudAtlas) -> Vec<GuiDraw> {
display_health: int("displayHealth", 0),
last_health_time: millis - i64::from(int("lastHealthAgo", 0)),
health_blink_time: i64::from(int("healthBlinkTime", 0)),
..HealthHud::default()
};
let kind = if has("minecraft:poison") {
HeartKind::Poisoned
@@ -101,7 +101,7 @@ fn title(c: &Value, w: i32, h: i32, atlas: &HudAtlas) -> Vec<GuiDraw> {
}
let partial = float(c, "partial", 0.0);
let bg = background(c);
let mut out = t.overlay_message(w, h, partial, &atlas.font, bg);
let mut out = t.overlay_message(w, h, 39, partial, &atlas.font, bg);
out.extend(t.title(w, h, partial, &atlas.font, bg));
out
}
@@ -129,7 +129,7 @@ fn selected(c: &Value, w: i32, h: i32, atlas: &HudAtlas) -> Vec<GuiDraw> {
}
let mode = c["gameMode"].as_str().unwrap_or("survival");
let can_hurt = mode == "survival" || mode == "adventure";
highlight.draw(w, h, can_hurt, &atlas.font, background(c))
highlight.draw(w, h, 39, can_hurt, &atlas.font, background(c))
}
fn boss(c: &Value, w: i32, h: i32, atlas: &HudAtlas) -> Vec<GuiDraw> {
+67 -17
View File
@@ -134,6 +134,7 @@ mod pandas;
mod passengers;
mod persist;
mod perspective;
mod pick_entity;
mod pickups;
mod piglins;
mod piston_push;
@@ -1546,6 +1547,8 @@ struct Game<'a> {
sleep_timer: u32,
dim: Dim,
world_root: PathBuf,
/// The world is superflat (`ServerLevel.isFlat`, for every dimension).
world_flat: bool,
/// A player carried through a portal, re-placed once the destination loads.
travelling: Option<(PlayerMotion, Vec3)>,
/// A server's players on their way to another level, each waiting for
@@ -3894,6 +3897,20 @@ impl Game<'_> {
);
}
}
// IceBlock.playerDestroy: the ice melts back into water (the
// #prevents_ice_melting tool tag holds only silk touch).
let prevents_melting = held
.as_ref()
.is_some_and(|h| h.enchantment_level("minecraft:silk_touch") > 0);
self.with_level(|b, level, ticks| {
b.ice_destroyed_by_player(
level,
ticks,
BlockPos::new(pos[0], pos[1], pos[2]),
broken,
prevents_melting,
);
});
}
self.pending.clear(pos);
self.process_level_events()?;
@@ -4174,6 +4191,7 @@ impl Game<'_> {
|| self.place_minecart(hit, &stack)
|| self.place_painting(hit, &stack)
|| self.place_item_frame(hit, &stack)
|| self.use_lodestone_compass(hit, &stack)
|| self.place_armor_stand(hit, &stack)
|| self.place_cushion(hit, &stack)
|| self.place_end_crystal(hit, &stack))
@@ -4224,6 +4242,7 @@ impl Game<'_> {
return Ok(true);
};
let target = [pos.x, pos.y, pos.z];
let state = self.apply_item_block_state(target, state, &stack)?;
self.mod_block_event(target, state, true);
if let Some(entity) = BlockEntity::for_block(&self.res.facts().get(state).name) {
let key = (target[0], target[1], target[2]);
@@ -5144,6 +5163,7 @@ impl Game<'_> {
horizon_client::spans::time("tick_thunder", || self.tick_thunder());
horizon_client::spans::time("tick_lecterns", || self.tick_lecterns());
horizon_client::spans::time("tick_maps", || self.tick_maps());
self.tick_lodestone_compasses();
horizon_client::spans::time("tick_bolts", || self.tick_bolts());
horizon_client::spans::time("tick_fishing", || self.tick_fishing());
horizon_client::spans::time("tick_tridents", || self.tick_tridents());
@@ -5623,6 +5643,9 @@ impl Game<'_> {
let spectator = self.spectator();
let (wi, hi) = (w as i32, h as i32);
let show_hud = !self.ui.hide_gui;
// max(Hud.leftHeight, Hud.rightHeight) after the bars: the item name and
// the action bar sit above it.
let mut bar_height = 39;
if show_hud {
self.draw_spyglass(&mut batch);
// Hud.extractCrosshair / extractItemHotbar / extractPlayerHealth /
@@ -5696,6 +5719,8 @@ impl Game<'_> {
contextual == ContextualBar::JumpableVehicle,
)
};
// Without the survival bars only the mount's hearts raise it.
bar_height = 39 + 10 * ((vehicle_hearts + 9) / 10);
if contextual == ContextualBar::Locator {
let (sprite, [x, y, w, h]) = horizon_client::locator_bar::background(wi, hi);
horizon_client::ui::nine_slice(
@@ -5769,20 +5794,24 @@ impl Game<'_> {
};
let draws = self.health_hud.layout(&inputs, wi, hi);
hud_layout::paint(&mut batch, atlas, &draws);
bar_height = self
.health_hud
.left_height
.max(self.health_hud.right_height);
}
if contextual == ContextualBar::Locator {
let draws = self.locator_bar_draws(wi, hi);
hud_layout::paint(&mut batch, &self.gui.atlas, &draws);
}
if !spectator {
self.draw_selected_name(&mut batch, !self.creative);
self.draw_selected_name(&mut batch, bar_height, !self.creative);
}
}
// Hud.extractSleepOverlay: over all health/food bars, under titles and chat.
let draws = hud_overlays::sleep_overlay(wi, hi, self.sleep_timer as i32);
hud_layout::paint(&mut batch, &self.gui.atlas, &draws);
if show_hud {
self.draw_titles(&mut batch);
self.draw_titles(&mut batch, bar_height);
self.draw_scoreboard(&mut batch);
self.draw_subtitles(&mut batch);
}
@@ -8475,24 +8504,34 @@ impl Game<'_> {
}
/// SkyRenderer.renderSunMoonAndStars: bodies on a frame rotated Y -90 then X by angle.
/// Returns the sun and moon quads, and the stars' frame and brightness
/// (the renderer keeps their geometry, built once).
/// (the renderer keeps their geometry, built once). The bodies fade with
/// the rain (`rainBrightness = 1 - rainLevel`, their colour's alpha).
fn celestial_geometry(
&self,
sky: &horizon_client::environment::Sky,
rain: f32,
) -> (Vec<Vertex>, Option<(glam::Mat4, f32)>) {
let body_color =
0x00FF_FFFF | (((1.0 - rain).clamp(0.0, 1.0) * 255.0).round() as u32) << 24;
let mut out = Vec::new();
let atlas = &self.gui.atlas;
let frame = |angle: f32| {
glam::Mat4::from_rotation_y((-90.0f32).to_radians())
* glam::Mat4::from_rotation_x(angle.to_radians())
};
let mut body = |name: &str, angle: f32, size: f32, color: u32| {
let mut body = |name: &str, angle: f32, size: f32, moon: bool| {
let Some(r) = atlas.sprites.get(name).copied() else {
return;
};
let m = frame(angle);
let corners = [(-size, -size), (size, -size), (size, size), (-size, size)];
let uv = [[r.u0, r.v0], [r.u1, r.v0], [r.u1, r.v1], [r.u0, r.v1]];
// buildCelestialQuad (sun) vs buildMoonPhases: the moon's UVs run
// the other way round.
let uv = if moon {
[[r.u1, r.v1], [r.u0, r.v1], [r.u0, r.v0], [r.u1, r.v0]]
} else {
[[r.u0, r.v0], [r.u1, r.v0], [r.u1, r.v1], [r.u0, r.v1]]
};
for ((x, z), uv) in corners.iter().zip(uv) {
let p = m.transform_point3(glam::Vec3::new(*x, 100.0, *z));
out.push(Vertex {
@@ -8500,19 +8539,14 @@ impl Game<'_> {
pos: p.to_array(),
uv,
layer: 2,
color,
color: body_color,
light: 0,
glint_uv: [0.0, 0.0],
});
}
};
body(
"environment/celestial/sun",
sky.sun_angle,
30.0,
0xFFFF_FFFF,
);
body("environment/celestial/sun", sky.sun_angle, 30.0, false);
let phases = [
"full_moon",
"waning_gibbous",
@@ -8528,7 +8562,7 @@ impl Game<'_> {
&format!("environment/celestial/moon/{phase}"),
sky.moon_angle,
20.0,
0xFFFF_FFFF,
true,
);
// SkyRenderer.renderStars: the static star buffer on the same frame.
let stars =
@@ -8685,6 +8719,7 @@ impl Game<'_> {
book.to_be_displayed.retain(|r| index.contains(r));
self.dim = dim;
self.world_root = save_root.clone();
self.world_flat = horizon_client::save::WorldGen::read(&save_root).is_some_and(|w| w.flat);
self.block_ticks = Ticks::new(self.level.seed as i32);
self.tick_chunks.clear();
self.clear_entities();
@@ -11227,8 +11262,11 @@ impl Game<'_> {
horizon_client::environment::apply_weather(&mut sky, rain, thunder);
self.end_flash_sky(&mut sky, alpha);
if self.flash > 0 && !self.settings.file.bool("hideLightningFlashes") {
// Lightning flash brightens the sky (LevelRenderer skyFlashTime).
sky.sky_color = [0.9, 0.9, 1.0];
// ClientLevel's flash layer while skyFlashTime > 0: the sky colour
// moves 22% of the way to (0.8, 0.8, 1.0).
let flash = [0.8, 0.8, 1.0];
sky.sky_color =
std::array::from_fn(|i| sky.sky_color[i] + (flash[i] - sky.sky_color[i]) * 0.22);
sky.sky_light_factor = 1.0;
}
// FishingHookRenderer's line, then DebugRenderer: chunk borders and
@@ -11367,7 +11405,7 @@ impl Game<'_> {
&& self.potions.effects.get("blindness").is_none()
{
// Camera.doesMobEffectBlockSky: no sun, moon or stars.
self.celestial_geometry(&sky)
self.celestial_geometry(&sky, rain)
} else if self.dim == Dim::End && self.settings.horizon.sky {
(self.end_flash_geometry(), None)
} else {
@@ -11846,7 +11884,18 @@ impl Game<'_> {
// Minecraft.pickBlockOrEntity -> the server's handlePickItemFromBlock:
// the block's clone item, with its block entity's data on Ctrl in
// creative, picked from the inventory (`pick_item`).
if let Some(hit) = self.pick() {
if let Some(entity) = self.pick_entity_hit() {
// handlePickItemFromEntity: the entity's pick result.
if let Some(stack) = entity.and_then(|id| self.entity_pick_result(id)) {
let infinite = self.creative;
horizon_client::pick_item::pick(
&mut self.inventory[..36],
&mut self.selected,
&stack,
infinite,
);
}
} else if let Some(hit) = self.pick() {
let include_data = self.creative && self.has_control_down();
if let Some(stack) = self.picked_stack(hit.pos, include_data) {
let infinite = self.creative;
@@ -13361,6 +13410,7 @@ impl<'a> Game<'a> {
sleep_timer: 0,
dim: Dim::Overworld,
world_root: PathBuf::new(),
world_flat: false,
travelling: None,
server_trips: Vec::new(),
level_teleports: Vec::new(),
+42 -2
View File
@@ -4,9 +4,13 @@
//! placed item gives its block entity.
use super::Game;
use horizon_client::{
block_entity_item, block_entity_nbt, containers::BlockEntity, item_data, items::ItemStack, save,
Result, block_entity_item, block_entity_nbt, containers::BlockEntity, item_data,
items::ItemStack, save,
};
use horizon_world::{
nbt::{Compound, Tag},
registry::BlockStateId,
};
use horizon_world::nbt::{Compound, Tag};
/// The component patch of `stack`.
fn components(stack: &ItemStack) -> Compound {
@@ -77,6 +81,42 @@ impl Game<'_> {
Some(stack)
}
/// `BlockItem.updateBlockStateFromTag`: the properties of the item's
/// `block_state` component (a hive's honey level, a statue's pose) set on
/// the block just placed at `pos`. Returns the state it has now.
pub(super) fn apply_item_block_state(
&mut self,
pos: [i32; 3],
state: BlockStateId,
stack: &ItemStack,
) -> Result<BlockStateId> {
let Some(Tag::Compound(saved)) =
stack.data().and_then(|d| d.other("minecraft:block_state"))
else {
return Ok(state);
};
let Some(current) = self.pack.assets.registry.state(state) else {
return Ok(state);
};
// Only the properties the block has.
let props: Vec<(String, String)> = saved
.iter()
.filter_map(|(k, v)| match v {
Tag::String(v) => Some((k.to_string_lossy(), v.to_string_lossy())),
_ => None,
})
.filter(|(k, v)| current.properties.get(k).is_some_and(|old| old != v))
.collect();
let props: Vec<(&str, &str)> = props.iter().map(|(k, v)| (&**k, &**v)).collect();
match self.with_props(state, &props) {
Some(new) if !props.is_empty() && new != state => {
self.level_set(pos, new)?;
Ok(new)
}
_ => Ok(state),
}
}
/// `BlockItem.updateCustomBlockEntityTag` and `updateBlockEntityComponents`
/// for the block entity just placed at `pos` from `stack`; `game_master`:
/// the placer can use game master blocks (a dispenser can't).
+27 -3
View File
@@ -772,11 +772,12 @@ impl Game<'_> {
let by_player = killer.is_some() || m.ai.last_hurt_by_player > 0;
let mut dying = m.clone();
// LivingEntity.die: the death is heard, and a sculk catalyst nearby
// takes the experience (before the loot is dropped).
// takes the experience (before the loot is dropped). It takes what
// would drop, whoever killed the mob.
self.sculk_mob_died(m);
let consumed =
self.catalyst_takes_experience([m.position.x, m.position.y, m.position.z], |_| {
if by_player {
if horizon_client::mobs::drops::drops_experience(&dying) {
horizon_client::mobs::drops::base_experience(&mut dying).max(0) as u32
} else {
0
@@ -3458,6 +3459,22 @@ impl Game<'_> {
horizon_client::particles::Burst::HORSE_EATING,
);
}
// Bee.mobInteract: a wither rose or an open eyeblossom is eaten for
// its `getBeeInteractionEffect` instead of breeding the bee.
Species::Bee
if is_food
&& matches!(
&*item,
"minecraft:wither_rose" | "minecraft:open_eyeblossom"
) =>
{
if &*item == "minecraft:wither_rose" {
m.add_effect("minecraft:wither", 40, 0);
} else {
m.add_effect("minecraft:poison", 25, 0);
}
consume = true;
}
// HappyGhast.canFallInLove: never, so only a ghastling takes food.
_ if is_food
&& m.kind.flags.animal
@@ -4172,6 +4189,7 @@ fn to_loot(stack: &ItemStack) -> horizon_world::loot::LootStack {
exploration: None,
ominous_amplifier: None,
stew_effects: Vec::new(),
names: Vec::new(),
}
}
/// A generated loot stack as a Horizon item stack.
@@ -4191,6 +4209,9 @@ fn from_loot(stack: &horizon_world::loot::LootStack) -> ItemStack {
if let Some(amplifier) = stack.ominous_amplifier.filter(|a| *a > 0) {
item.data_mut().ominous_amplifier = Some(amplifier as u8);
}
for (id, text) in &stack.names {
item.data_mut().set_loot_name(id, text);
}
item
}
impl Game<'_> {
@@ -4234,6 +4255,9 @@ impl Game<'_> {
f64::from(y + 1),
f64::from(pos[2]) + 0.5,
];
let _ = self.spawn_mob("minecraft:zombified_piglin", at, false);
// setPortalCooldown: the new piglin does not travel at once.
if let Some(id) = self.spawn_mob("minecraft:zombified_piglin", at, false) {
self.set_portal_cooldown(id);
}
}
}
+59 -1
View File
@@ -412,6 +412,8 @@ struct ServerState {
paused: bool,
/// `broadcast-console-to-ops`.
broadcast_console: bool,
/// `op-permission-level`: what `/op` grants (operatorUserPermissions).
op_level: i32,
}
/// The dedicated-only commands' replies vanilla sends with `broadcast`
@@ -573,6 +575,14 @@ impl Game<'_> {
// DedicatedServer.initServer: worldData.setGameType; forceDifficulty.
self.set_default_game_mode(game_mode);
self.ui.difficulty = props.difficulty as u8;
// DedicatedServer.initServer: a legacy `announce-player-achievements`
// sets the show_advancement_messages rule once, as the file is read.
if let Some(announce) = props.announce_player_achievements {
self.ui.rules.set(
"show_advancement_messages",
horizon_client::gamerules::Value::Bool(announce),
);
}
self.settings.render_distance = props.view_distance.clamp(2, 32);
self.settings.simulation_distance = props.simulation_distance.clamp(2, 32);
self.streaming.radius = self.settings.render_distance;
@@ -685,6 +695,7 @@ impl Game<'_> {
ticks: Default::default(),
paused: false,
broadcast_console: props.broadcast_console_to_ops,
op_level: props.op_permission_level,
};
let pause_after = props.pause_when_empty_seconds * 20;
let idle_timeout = props.player_idle_timeout;
@@ -946,7 +957,7 @@ impl Game<'_> {
let Some(user) = self.resolve(rest[0]) else {
return;
};
let level = 4;
let level = state.op_level;
let changed = {
let mut l = state.lists.lock().unwrap();
if command == "op" {
@@ -972,6 +983,10 @@ impl Game<'_> {
self.console_reply(Text::translate(key, &[lit(&user.name)]));
let lists = state.lists.clone();
self.sync_ops(&lists);
// DeOpCommands.deopPlayers.
if command == "deop" && changed {
self.kick_unlisted_players(&lists);
}
}
"whitelist" => self.console_whitelist(state, &rest),
"ban" if !rest.is_empty() => {
@@ -1183,6 +1198,7 @@ impl Game<'_> {
["on"] | ["off"] => {
let on = rest[0] == "on";
let mut l = state.lists.lock().unwrap();
let turned_on = on && !l.use_whitelist;
let key = match (on, l.use_whitelist) {
(true, true) => "commands.whitelist.alreadyOn",
(true, false) => "commands.whitelist.enabled",
@@ -1193,6 +1209,10 @@ impl Game<'_> {
drop(l);
self.store_property("white-list", on.to_string());
self.console_reply(Text::translate(key, &[]));
if turned_on {
let lists = state.lists.clone();
self.kick_unlisted_players(&lists);
}
}
["list"] => {
let names: Vec<String> = state
@@ -1216,6 +1236,8 @@ impl Game<'_> {
["reload"] => {
let _ = state.lists.lock().unwrap().white.load();
self.console_reply(Text::translate("commands.whitelist.reloaded", &[]));
let lists = state.lists.clone();
self.kick_unlisted_players(&lists);
}
["add", name] | ["remove", name] => {
let Some(user) = self.resolve(name) else {
@@ -1241,6 +1263,10 @@ impl Game<'_> {
Vec::new()
};
self.console_reply(Text::translate(key, &args));
if ok && rest[0] == "remove" {
let lists = state.lists.clone();
self.kick_unlisted_players(&lists);
}
}
_ => self.console_reply(Text::translate("command.unknown.argument", &[])),
}
@@ -1256,6 +1282,38 @@ impl Game<'_> {
}
}
/// MinecraftServer.kickUnlistedPlayers: with `enforce-whitelist` and the
/// whitelist on, whoever is online and not on the whitelist file is
/// disconnected (operators are not spared here).
fn kick_unlisted_players(&mut self, lists: &Arc<Mutex<Lists>>) {
let unlisted: Vec<u128> = {
let l = lists.lock().unwrap();
let Some(host) = self.lan.as_ref() else {
return;
};
if !(l.enforce_whitelist && l.use_whitelist) {
return;
}
host.hub
.guests
.iter()
.filter(|g| {
!l.white.contains(&NameAndId {
id: g.uuid,
name: g.name.clone(),
})
})
.map(|g| g.uuid)
.collect()
};
for uuid in unlisted {
self.kick_uuid(
uuid,
translatable("multiplayer.disconnect.not_whitelisted", &[]),
);
}
}
fn kick_uuid(&mut self, uuid: u128, reason: Tag) {
if let Some(h) = self.lan.as_mut() {
let conns: Vec<u32> = h
@@ -1548,6 +1548,7 @@ impl Game<'_> {
return Ok((Some(stack), FAIL));
};
let target = (placed.x, placed.y, placed.z);
let state = self.apply_item_block_state([target.0, target.1, target.2], state, &stack)?;
if let Some(entity) = BlockEntity::for_block(&self.res.facts().get(state).name) {
self.world.block_entities.insert(target, entity);
self.apply_placed_item([target.0, target.1, target.2], &stack, false);
+8 -4
View File
@@ -15,6 +15,7 @@ use horizon_client::{
};
use horizon_core::{
geometry::{Aabb, Vec3, trig},
position::BlockPos,
random::RandomSource,
};
use horizon_protocol::entity_data::{Value, serializer};
@@ -248,7 +249,7 @@ impl Game<'_> {
item,
[hook.position.x, hook.position.y, hook.position.z],
v,
10,
0,
);
let xp = self.world.random.next_int_bound(6).unwrap_or(0) as u32 + 1;
self.spawn_orbs([owner.x, owner.y + 0.5, owner.z + 0.5], xp);
@@ -339,9 +340,12 @@ impl Game<'_> {
let mut speed = 1;
let above = [b[0], b[1] + 1, b[2]];
let sky = self.world.light(above[0], above[1], above[2]) >> 4 == 15;
let raining = self.world.weather.raining;
let r = &mut self.world.random;
if r.next_float() < 0.25 && raining && sky {
// Level.isRainingAt: rain (not snow) reaches the position.
if self.world.random.next_float() < 0.25
&& self.with_level(|b, level, _| {
b.is_raining_at(level, BlockPos::new(above[0], above[1], above[2]))
})
{
speed += 1;
}
let r = &mut self.world.random;
+19 -1
View File
@@ -1014,7 +1014,8 @@ impl Game<'_> {
let host_uuid = host.unwrap_or(0);
self.next_entity += 1;
let host_entity = self.next_entity as i32;
let hub = Hub::new(lan, sync, encoder, config);
let mut hub = Hub::new(lan, sync, encoder, config);
hub.flat_world = self.world_flat;
let port = hub.lan.port;
self.world.block_log = Some(Vec::new());
self.world.light_log = Some(Default::default());
@@ -1477,6 +1478,16 @@ impl Game<'_> {
"INFO",
&format!("System chat: {} joined the game", g.name),
);
} else if self.player.is_some()
&& let Some(g) = host.hub.guest(conn)
&& g.placed
{
// The host's chat hears of the newcomer as the guests do.
let who = self.player_text(&g.name, g.uuid);
self.system_message(
Text::translate("multiplayer.player.joined", &[who])
.with_color(horizon_client::text::YELLOW),
);
}
}
HostEvent::Left(mut guest) => {
@@ -1511,6 +1522,13 @@ impl Game<'_> {
"INFO",
&format!("System chat: {} left the game", guest.name),
);
} else if self.player.is_some() && guest.placed {
// ServerGamePacketListenerImpl.removePlayerFromWorld.
let who = self.player_text(&guest.name, guest.uuid);
self.system_message(
Text::translate("multiplayer.player.left", &[who])
.with_color(horizon_client::text::YELLOW),
);
}
}
HostEvent::Packet(conn, packet) => {
+57 -7
View File
@@ -1434,19 +1434,68 @@ impl Game<'_> {
.unwrap_or_default();
host.hub.send_stats(conn, &stats);
}
// handlePickItemFromBlock: a block within the block reach plus 1, its
// data only with infinite materials.
S::PickItemFromBlock(p) => {
let state = self.world.block(p.pos[0], p.pos[1], p.pos[2]);
let item = self.res.facts().get(state).item.clone();
let g = &mut host.hub.guests[gi];
if g.creative() && &*item != "minecraft:air" {
let slot = g.selected;
g.inventory[slot] = Some(ItemStack::new(&item, 1));
}
self.guest_pick_item(host, conn, |g| {
let eye = g.eye_position()?;
let d2: f64 = (0..3)
.map(|a| {
let lo = f64::from(p.pos[a]);
(eye[a] - eye[a].clamp(lo, lo + 1.0)).powi(2)
})
.sum();
let range = g.reach() + 1.0;
if d2 >= range * range {
return None;
}
g.picked_stack(p.pos, g.creative && p.include_data)
});
}
// handlePickItemFromEntity.
S::PickItemFromEntity(p) => {
self.guest_pick_item(host, conn, |g| {
g.within_entity_range(p.id)
.then(|| g.entity_pick_result(p.id))
.flatten()
});
}
_ => {}
}
}
/// `ServerGamePacketListenerImpl.tryPickItem` for the guest: a matching stack
/// is selected, else a creative player gets the item; the held slot is sent
/// back and the changed slots follow with the tick's inventory changes.
fn guest_pick_item(
&mut self,
host: &mut LanHost,
conn: u32,
stack: impl FnOnce(&mut Self) -> Option<ItemStack>,
) {
let picked = self.as_guest(host, conn, |g| {
let stack = stack(g)?;
let infinite = g.creative;
horizon_client::pick_item::pick(
&mut g.inventory[..36],
&mut g.selected,
&stack,
infinite,
);
Some(())
});
if picked.flatten().is_some()
&& let Some(g) = host.hub.guests.iter().find(|g| g.conn == conn)
{
host.hub.send_to(
conn,
&C::SetHeldSlot(play::SetHeldSlot {
slot: g.selected as i32,
}),
);
}
}
fn item_from_network(
&self,
host: &LanHost,
@@ -2054,6 +2103,7 @@ impl Game<'_> {
self.as_guest(host, conn, |g| {
g.tick_fishing();
g.tick_maps();
g.tick_lodestone_compasses();
g.count_tick_stats(sleeping);
g.tick_advancements();
g.update_player_attributes();
+153 -44
View File
@@ -648,27 +648,41 @@ impl Game<'_> {
self.container_changed(pos);
}
}
/// `HopperBlockEntity.ejectItems`.
/// `HopperBlockEntity.ejectItems`: into the block container in front, else
/// a container entity there (`getContainerAt`).
fn hopper_eject(&mut self, pos: Pos, hopper: &View) -> bool {
let state = self.world.block(pos.0, pos.1, pos.2);
let c = self.containers();
let Some(facing) = c.prop(state, "facing").and_then(dir_from_name) else {
return false;
};
let Some(target) = c.view(offset(pos, facing), false) else {
let front = offset(pos, facing);
let face = facing.opposite();
if let Some(target) = c.view(front, false) {
return self.eject_into(pos, hopper, &target, face);
}
let Some(id) = self.entity_container_at([
f64::from(front.0) + 0.5,
f64::from(front.1) + 0.5,
f64::from(front.2) + 0.5,
]) else {
return false;
};
let face = facing.opposite();
if c.is_full(&target, face) {
self.with_entity_container(id, |g, target| g.eject_into(pos, hopper, target, face))
.unwrap_or(false)
}
/// The rest of `HopperBlockEntity.ejectItems`: one item into `target`.
fn eject_into(&mut self, pos: Pos, hopper: &View, target: &View, face: Direction) -> bool {
if self.containers().is_full(target, face) {
return false;
}
for slot in 0..hopper.len() {
let Some(one) = self.take_one(hopper, slot) else {
continue;
};
match self.add_item(Some(pos), &target, one, Some(face)) {
match self.add_item(Some(pos), target, one, Some(face)) {
None => {
self.view_changed(&target);
self.view_changed(target);
return true;
}
Some(rest) => self.put_back(hopper, slot, rest),
@@ -679,7 +693,12 @@ impl Game<'_> {
/// `HopperBlockEntity.suckInItems`: from the container above, or item entities.
fn hopper_suck(&mut self, pos: Pos, hopper: &View) -> bool {
let above = (pos.0, pos.1 + 1, pos.2);
if let Some(took) = self.suck_from_container(above, hopper) {
let at = [
f64::from(pos.0) + 0.5,
f64::from(pos.1) + 1.5,
f64::from(pos.2) + 0.5,
];
if let Some(took) = self.suck_from_container(above, at, hopper) {
return took;
}
let above_state = self.world.block(above.0, above.1, above.2);
@@ -713,11 +732,16 @@ impl Game<'_> {
self.suck_item_entities(hopper, min, max)
}
/// The container half of `HopperBlockEntity.suckInItems`: one item from the
/// container at `above`, through its bottom face. `None` when there is no
/// container there.
fn suck_from_container(&mut self, above: Pos, hopper: &View) -> Option<bool> {
/// container at `above`, through its bottom face; with no block container
/// there, from a container entity whose box meets the one around `at`
/// (`getSourceContainerOrHandler`). `None` when there is no container.
fn suck_from_container(&mut self, above: Pos, at: [f64; 3], hopper: &View) -> Option<bool> {
let c = self.containers();
let source = c.view(above, false)?;
let Some(source) = c.view(above, false) else {
let id = self.entity_container_at(at)?;
return self
.with_entity_container(id, |g, source| g.suck_from_slots(None, source, hopper));
};
if let Kind::Composter(level) = source.kind {
if level != 8 {
return Some(false);
@@ -736,26 +760,133 @@ impl Game<'_> {
return Some(moved);
}
let source_hopper = (source.kind == Kind::Hopper).then_some(above);
Some(self.suck_from_slots(source_hopper, &source, hopper))
}
/// `HopperBlockEntity.tryTakeInItemFromSlot` over the `DOWN` face of `source`.
fn suck_from_slots(
&mut self,
source_hopper: Option<Pos>,
source: &View,
hopper: &View,
) -> bool {
for slot in source.slots_for_face(Some(Direction::Down)) {
let c = self.containers();
let Some(stack) = c.get(&source, slot) else {
let Some(stack) = c.get(source, slot) else {
continue;
};
if !c.can_take(&source, slot, stack, Direction::Down) {
if !c.can_take(source, slot, stack, Direction::Down) {
continue;
}
let Some(one) = self.take_one(&source, slot) else {
let Some(one) = self.take_one(source, slot) else {
continue;
};
match self.add_item(source_hopper, hopper, one, None) {
None => {
self.view_changed(&source);
return Some(true);
self.view_changed(source);
return true;
}
Some(rest) => self.put_back(&source, slot, rest),
Some(rest) => self.put_back(source, slot, rest),
}
}
Some(false)
false
}
/// `HopperBlockEntity.getEntityContainer`: one of the alive container
/// entities (`EntitySelector.CONTAINER_ENTITY_SELECTOR`: chest and hopper
/// minecarts, chest boats) whose box meets the 1x1x1 box around `at`,
/// picked at random.
fn entity_container_at(&mut self, at: [f64; 3]) -> Option<u32> {
let meets = |b: horizon_core::geometry::Aabb| {
let (lo, hi) = (b.min_position(), b.max_position());
lo.x < at[0] + 0.5
&& hi.x > at[0] - 0.5
&& lo.y < at[1] + 0.5
&& hi.y > at[1] - 0.5
&& lo.z < at[2] + 0.5
&& hi.z > at[2] - 0.5
};
let mut found: Vec<u32> = self
.carts
.iter()
.filter(|c| {
matches!(
c.kind,
super::minecarts::CartKind::Chest | super::minecarts::CartKind::Hopper
) && meets(c.bounds())
})
.map(|c| c.id)
.collect();
found.extend(
self.boats
.iter()
.filter(|b| b.chest && meets(b.bounds()))
.map(|b| b.id),
);
if found.is_empty() {
return None;
}
let pick = self
.world
.random
.next_int_bound(found.len() as i32)
.unwrap_or(0);
found.get(pick as usize).copied()
}
/// Runs `f` on a container entity's items as a container, under the
/// placeholder key its menu uses (the open menu's copy when it is open),
/// then hands the items back to the entity.
fn with_entity_container<R>(
&mut self,
id: u32,
f: impl FnOnce(&mut Self, &View) -> R,
) -> Option<R> {
let mut items = if let Some(c) = self.carts.iter_mut().find(|c| c.id == id) {
std::mem::take(&mut c.items)
} else {
std::mem::take(&mut self.boats.iter_mut().find(|b| b.id == id)?.items)
};
let key = (i32::MAX - 15, super::minecarts::CART_KEY_Y, id as i32);
let (view, open) = self.lend_items(key, &mut items);
let result = f(self, &view);
self.reclaim_items(key, open, &mut items);
if let Some(c) = self.carts.iter_mut().find(|c| c.id == id) {
c.items = items;
} else if let Some(b) = self.boats.iter_mut().find(|b| b.id == id) {
b.items = items;
}
Some(result)
}
/// A vehicle's slots as a container under its placeholder `key`. When its
/// menu is open the placeholder is the live copy and stays as it is.
/// Returns the view and whether the menu was open.
fn lend_items(&mut self, key: Pos, items: &mut Vec<Option<ItemStack>>) -> (View, bool) {
let open = self.world.block_entities.contains_key(&key);
if !open {
let slots = BlockEntity::Chest(std::mem::take(items));
self.world.block_entities.insert(key, slots);
}
let size = self
.world
.block_entities
.get(&key)
.map_or(0, |e| e.slots().len());
let view = View {
parts: vec![(key, size)],
kind: Kind::Generic,
};
(view, open)
}
/// Gives `lend_items`' slots back to the vehicle, so its own items stay
/// the one source of truth: a menu copy that is still around (open, or
/// left by a guest's close) gets the changed items copied to `items`.
fn reclaim_items(&mut self, key: Pos, open: bool, items: &mut Vec<Option<ItemStack>>) {
let slots = if open {
self.world.block_entities.get(&key).cloned()
} else {
self.world.block_entities.remove(&key)
};
if let Some(BlockEntity::Chest(slots)) = slots {
*items = slots;
}
}
/// The item half of `HopperBlockEntity.suckInItems` (`getItemsAtAndAbove`):
/// adds the item entities that meet the box, until one is taken whole.
@@ -791,20 +922,7 @@ impl Game<'_> {
// The cart's slots are edited as a container under the placeholder key
// its menu uses; when that menu is open the placeholder is the live copy.
let key = (i32::MAX - 15, super::minecarts::CART_KEY_Y, cart.id as i32);
let open = self.world.block_entities.contains_key(&key);
if !open {
let items = BlockEntity::Chest(std::mem::take(&mut cart.items));
self.world.block_entities.insert(key, items);
}
let size = self
.world
.block_entities
.get(&key)
.map_or(0, |e| e.slots().len());
let view = View {
parts: vec![(key, size)],
kind: Kind::Generic,
};
let (view, open) = self.lend_items(key, &mut cart.items);
let p = cart.position;
// getLevelY is the cart's y + 0.5, so the container is the block at
// y + 1.5, and Hopper.SUCK_AABB (moved by getLevel - 0.5) starts 11/16
@@ -814,7 +932,8 @@ impl Game<'_> {
(p.y + 1.5).floor() as i32,
p.z.floor() as i32,
);
let took = match self.suck_from_container(above, &view) {
let at = [p.x, p.y + 1.5, p.z];
let took = match self.suck_from_container(above, at, &view) {
Some(took) => took,
None => self.suck_item_entities(
&view,
@@ -832,17 +951,7 @@ impl Game<'_> {
[hi.x + 0.25, hi.y, hi.z + 0.25],
);
}
// The cart's own items stay the one source of truth: a menu copy that
// is still around (open, or left by a guest's close) gets the pulled
// items back into `cart.items`, so reopening never restores a stale copy.
let slots = if open {
self.world.block_entities.get(&key).cloned()
} else {
self.world.block_entities.remove(&key)
};
if let Some(BlockEntity::Chest(items)) = slots {
cart.items = items;
}
self.reclaim_items(key, open, &mut cart.items);
}
// ---- dispensers and droppers ----
+87
View File
@@ -721,6 +721,93 @@ impl Game<'_> {
self.swing_arm();
true
}
/// `CompassItem.useOn`: a compass used on a lodestone locks onto it (a
/// single one in survival changes in place, else one is split off).
pub(super) fn use_lodestone_compass(&mut self, hit: &super::Hit, stack: &ItemStack) -> bool {
if &*stack.item != "minecraft:compass" {
return false;
}
let [x, y, z] = hit.pos;
if self
.res
.facts()
.get(self.world.block(x, y, z))
.name
.as_ref()
!= "minecraft:lodestone"
{
return false;
}
let at = [x, y, z].map(|v| f64::from(v) + 0.5);
self.audio
.play("minecraft:item.lodestone_compass.lock", Some(at), 1.0, 1.0);
let target = Tag::compound([(
"target",
Tag::compound([
("dimension", Tag::String(self.dimension_id().into())),
("pos", Tag::IntArray(vec![x, y, z])),
]),
)]);
if !self.creative && stack.count == 1 {
if let Some(held) = self.held_mut() {
held.data_mut()
.set_other("minecraft:lodestone_tracker", Some(target));
}
} else {
let mut locked = stack.clone();
locked.count = 1;
locked
.data_mut()
.set_other("minecraft:lodestone_tracker", Some(target));
self.consume_held();
self.give_or_drop_stack(locked);
}
self.stats.add("used", &stack.item, 1);
self.swing_arm();
true
}
/// `CompassItem.inventoryTick` / `LodestoneTracker.tick`: a compass whose
/// lodestone is gone loses its target. A target in an unloaded chunk waits.
pub(super) fn tick_lodestone_compasses(&mut self) {
let dim = self.dimension_id();
let gone = |game: &Self, stack: &ItemStack| -> bool {
let Some(tracker) = stack
.data()
.and_then(|d| d.other("minecraft:lodestone_tracker"))
else {
return false;
};
let (Some(target), true) = (
tracker.get("target"),
horizon_client::save::number(tracker.get("tracked")).is_none_or(|v| v != 0),
) else {
return false;
};
let Some(Tag::IntArray(pos)) = target.get("pos") else {
return false;
};
let [x, y, z] = [pos[0], pos[1], pos[2]];
horizon_client::save::text(target.get("dimension")).as_deref() == Some(dim)
&& game.world.chunks.contains_key(&(x >> 4, z >> 4))
&& game
.res
.facts()
.get(game.world.block(x, y, z))
.name
.as_ref()
!= "minecraft:lodestone"
};
for slot in (0..self.inventory.len()).chain([super::shield::OFFHAND_SLOT]) {
if self.stack_in(slot).is_some_and(|s| gone(self, s))
&& let Some(s) = self.stack_in_mut(slot)
{
s.data_mut().set_other(
"minecraft:lodestone_tracker",
Some(Tag::Compound(Default::default())),
);
}
}
}
fn give_or_drop_stack(&mut self, stack: ItemStack) {
if let Some(rest) = self.give(stack)
&& let Some(eye) = self.eye_position()
@@ -662,6 +662,17 @@ impl Game<'_> {
);
}
}
/// `Entity.setPortalCooldown` on an entity that has just spawned in a
/// portal: it stands in the portal for 300 ticks without going through.
pub(super) fn set_portal_cooldown(&mut self, id: u32) {
self.portals.entities.insert(
id,
PortalState {
cooldown: ENTITY_DELAY,
process: None,
},
);
}
/// `canUsePortal(false)` of a mob: alive, riding nothing, not asleep,
/// not carrying the player, and not a boss that never travels.
fn mob_can_use_portal(&self, m: &horizon_client::mobs::Mob) -> bool {
@@ -0,0 +1,98 @@
//! Pick Block on an entity (`Minecraft.pickBlockOrEntity`'s entity case and
//! `ServerGamePacketListenerImpl.handlePickItemFromEntity`): the client aims,
//! the server answers with `Entity.getPickResult`.
use super::Game;
use horizon_client::items::ItemStack;
impl Game<'_> {
/// The entity the crosshair hits before any block, as `Entity.getId`; an
/// entity with nothing to pick (a projectile, a player, the dragon) is
/// `Some(None)`. `None` when the view hits a block or nothing.
pub(super) fn pick_entity_hit(&self) -> Option<Option<i32>> {
let mut best: Option<(f64, Option<i32>)> = None;
let mut offer = |distance: f64, id: Option<i32>| {
if self.before_block(distance) && best.is_none_or(|b| distance < b.0) {
best = Some((distance, id));
}
};
if let Some(hit) = self.remote_object_hit(true) {
offer(hit.distance, Some(hit.id));
}
if let Some((i, d)) = self.mob_hit() {
offer(d, Some(self.mobs[i].id as i32));
}
if let Some((_, d)) = self.guest_hit() {
offer(d, None);
}
if let Some((_, d)) = self.redirectable_hit() {
offer(d, None);
}
if let Some((_, d)) = self.dragon_hit() {
offer(d, None);
}
best.map(|b| b.1)
}
/// `Entity.getPickResult` of the entity `id`: a mob's spawn egg, the item
/// of a boat, minecart, armor stand, cushion, painting, item frame (the
/// framed item when it holds one) or end crystal.
pub(super) fn entity_pick_result(&self, id: i32) -> Option<ItemStack> {
let id = u32::try_from(id).ok()?;
let item = if let Some(m) = self.mobs.iter().find(|m| m.id == id) {
// Mob.getPickResult: SpawnEggItem.byId.
let egg = format!("{}_spawn_egg", m.kind.id);
return self.res.items().get(&egg).map(|_| ItemStack::new(&egg, 1));
} else if let Some(b) = self.boats.iter().find(|b| b.id == id) {
b.entity_type()
} else if let Some(c) = self.carts.iter().find(|c| c.id == id) {
c.kind.id().to_owned()
} else if self.armor_stands.iter().any(|a| a.id == id) {
"minecraft:armor_stand".to_owned()
} else if let Some(c) = self.cushions.iter().find(|c| c.id == id) {
c.item()
} else if let Some(f) = self.item_frames.iter().find(|f| f.id == id) {
// ItemFrame.getPickResult: what it frames, else the frame itself.
return Some(
f.item
.clone()
.unwrap_or_else(|| ItemStack::new(f.kind(), 1)),
);
} else if self.paintings.iter().any(|p| p.id == id) {
"minecraft:painting".to_owned()
} else if self.crystals.iter().any(|c| c.id == id) {
"minecraft:end_crystal".to_owned()
} else {
return None;
};
Some(ItemStack::new(&item, 1))
}
/// `Player.isWithinEntityInteractionRange(entity, 3.0)`: the entity's box
/// within the interaction range plus 3 of the eyes.
pub(super) fn within_entity_range(&self, id: i32) -> bool {
let Some(eye) = self.eye_position() else {
return false;
};
let mob = u32::try_from(id)
.ok()
.and_then(|id| self.mobs.iter().find(|m| m.id == id))
.map(|m| m.bounds());
let Some(b) = self
.object_target(id)
.map(|(lo, hi, _)| (lo, hi))
.or_else(|| {
mob.or_else(|| self.tracked_special_box(id)).map(|b| {
let (lo, hi) = (b.min_position(), b.max_position());
([lo.x, lo.y, lo.z], [hi.x, hi.y, hi.z])
})
})
else {
return false;
};
let d2: f64 = (0..3)
.map(|a| (eye[a] - eye[a].clamp(b.0[a], b.1[a])).powi(2))
.sum();
let range = if self.creative { 5.0 } else { 3.0 } + 3.0;
d2 < range * range
}
}
+15 -2
View File
@@ -119,7 +119,7 @@ pub(super) struct Remote {
pub(super) entity_id: i32,
/// The item the player's use began with (`LivingEntity.useItem`).
pub(super) use_item: Option<Arc<str>>,
dimension: DimensionType,
pub(super) dimension: DimensionType,
/// What chunk packets decode with (the dimension's height and sky, the
/// biome registry); replaced whenever one of them changes.
pub(super) chunk_format: Arc<ChunkFormat>,
@@ -202,6 +202,8 @@ pub(super) struct Remote {
pub(super) pending_keys: Vec<KeyCode>,
stop_mining: bool,
placing: Option<([i32; 3], u64)>,
/// `CommonPlayerSpawnInfo.isFlat`: the level is a superflat one.
pub(super) flat: bool,
death_message: Option<String>,
/// `LocalPlayer.showDeathScreen`: off with `immediate_respawn`.
show_death_screen: bool,
@@ -497,6 +499,7 @@ impl Game<'_> {
pending_keys: Vec::new(),
stop_mining: false,
placing: None,
flat: false,
death_message: None,
show_death_screen: true,
player_type,
@@ -1018,6 +1021,7 @@ impl Game<'_> {
r.chunk_format = Arc::new(r.chunk_format.with_dimension(&dimension));
r.dimension = dimension;
r.game_mode = spawn.game_type;
r.flat = spawn.flat;
r.loaded_sent = false;
// A replacement LocalPlayer has a fresh sendPosition baseline.
r.position_sender = Default::default();
@@ -5990,7 +5994,16 @@ impl horizon_client::interaction_input::InteractionWorld for RemoteInputWorld<'_
self.0.remote_use();
}
fn pick(&mut self) {
if let Some(hit) = self.0.pick() {
// Minecraft.pickBlockOrEntity: an entity before the block is asked
// for (`handlePickItemFromEntity`), else the block.
if let Some(entity) = self.0.pick_entity_hit() {
if let Some(id) = entity {
self.0.send(S::PickItemFromEntity(play::PickItemFromEntity {
id,
include_data: self.0.has_control_down(),
}));
}
} else if let Some(hit) = self.0.pick() {
self.0.send(S::PickItemFromBlock(play::PickItemFromBlock {
pos: hit.pos,
include_data: self.0.has_control_down(),
+25 -5
View File
@@ -92,9 +92,8 @@ impl Game<'_> {
distant_terrain: self.lod.is_some(),
cloud_range_chunks: self.settings.cloud_range,
camera_y: camera.y,
// level.getMinY(): the Nether and the End start at y 0.
min_y: if self.dim == Dim::Overworld { MIN_Y } else { 0 },
flat: false,
min_y: self.level_min_y(),
flat: self.level_flat(),
forward: forward.map(|c| c as f32),
rain,
thunder,
@@ -108,9 +107,30 @@ impl Game<'_> {
},
)
}
/// `SkyRenderer.shouldRenderDarkDisc`: eye below the horizon (y 63) and not under water.
/// `ClientLevelData.isFlat`: on the server thread, the world-wide flag.
fn level_flat(&self) -> bool {
match self.remote.as_ref() {
Some(r) => r.flat,
None => self.world_flat,
}
}
/// `Level.getMinY`: the current dimension type's minimum y.
fn level_min_y(&self) -> i32 {
match self.remote.as_ref() {
Some(r) => r.dimension.min_y,
None => self.dim.min_y(),
}
}
/// `SkyRenderer.shouldRenderDarkDisc`: eye below the horizon and not under
/// water. `ClientLevelData.getHorizonHeight`: the bottom of a flat world,
/// otherwise y 63.
pub(super) fn dark_disc(&self, camera: glam::DVec3) -> bool {
camera.y < 63.0
let horizon = if self.level_flat() {
f64::from(self.level_min_y())
} else {
63.0
};
camera.y < horizon
&& fog_type(
&self.world,
self.res.facts(),
+45 -1
View File
@@ -5,9 +5,11 @@
//! world as soon as the behaviour returns, within the same tick.
use super::{Game, GameLevel, Screen, dispenser::new_item_motion, storage::is_shulker_box};
use horizon_client::{
block_entity_item, block_entity_nbt,
containers::{Banner, BlockEntity},
item_data,
items::ItemStack,
save,
};
use horizon_core::position::BlockPos;
use horizon_core::random::RandomSource;
@@ -16,7 +18,7 @@ use horizon_world::{
LevelEvent,
removal::{self, PreRemove},
},
nbt::{JavaString, Tag},
nbt::{Compound, JavaString, Tag},
registry::BlockStateId,
};
use std::collections::HashMap;
@@ -32,6 +34,9 @@ pub(super) struct Pending {
pub(super) names: HashMap<[i32; 3], (String, String)>,
/// A decorated pot's sherds (back, left, right, front).
pub(super) pot: HashMap<[i32; 3], [Option<ItemStack>; 4]>,
/// A head's profile, note block sound and name, or a hive's bees: the
/// dropped item and the component patch it takes.
pub(super) copied: HashMap<[i32; 3], (String, Compound)>,
}
impl Pending {
/// Remember what the block entity at `pos` copies into the drops of `block`.
@@ -49,6 +54,31 @@ impl Pending {
if let Some(name) = name {
self.names.insert(pos, (block.to_owned(), name));
}
// `copy_components` from the block entity: the head tables copy the
// profile (player heads), note block sound and name, the hive tables
// the bees.
let include: &[&str] = match entity {
BlockEntity::Skull(_) if block.starts_with("minecraft:player_") => &[
"minecraft:profile",
"minecraft:note_block_sound",
"minecraft:custom_name",
],
BlockEntity::Skull(_) => &["minecraft:custom_name"],
BlockEntity::Beehive(_) => &["minecraft:bees"],
_ => &[],
};
if !include.is_empty() {
let saved = save::encode_block_entity((pos[0], pos[1], pos[2]), entity, block);
let mut patch = block_entity_nbt::canonical(&saved)
.map(|saved| block_entity_item::pick(&saved))
.unwrap_or_default();
patch.retain(|k, _| include.contains(&k.to_string_lossy().as_str()));
if !patch.is_empty() {
// `Blocks.wallVariant`: a wall head drops the standing item.
let item = block.replacen("_wall_", "_", 1);
self.copied.insert(pos, (item, patch));
}
}
if is_shulker_box(block)
&& let BlockEntity::Chest(items) = entity
{
@@ -76,6 +106,19 @@ impl Pending {
{
stack.data_mut().custom_name = Some(name);
}
if self
.copied
.get(&pos)
.is_some_and(|(item, _)| **item == *stack.item)
&& let Some((_, copied)) = self.copied.remove(&pos)
{
let mut patch = match item_data::encode_components(stack) {
Some(Tag::Compound(c)) => c,
_ => Default::default(),
};
patch.extend(copied);
(stack.damage, stack.extra) = item_data::decode_components(Some(&Tag::Compound(patch)));
}
if &*stack.item == "minecraft:decorated_pot" {
// The pot's block entity always copies its decorations, empty or not.
let sides = self.pot.remove(&pos).unwrap_or_default();
@@ -155,6 +198,7 @@ impl Pending {
self.shulker.remove(&pos);
self.banner.remove(&pos);
self.names.remove(&pos);
self.copied.remove(&pos);
}
}
+6 -3
View File
@@ -12,12 +12,14 @@ use horizon_client::{
impl Game<'_> {
/// `Hud.extractOverlayMessage` and `extractTitle`.
pub(super) fn draw_titles(&self, batch: &mut HudBatch) {
pub(super) fn draw_titles(&self, batch: &mut HudBatch, bar_height: i32) {
let (w, h) = (batch.width as i32, batch.height as i32);
let partial = self.clock.fraction() as f32;
let background = self.settings.text_background(0.0);
let font = &self.gui.atlas.font;
let mut draws = self.titles.overlay_message(w, h, partial, font, background);
let mut draws = self
.titles
.overlay_message(w, h, bar_height, partial, font, background);
draws.extend(self.titles.title(w, h, partial, font, background));
hud_layout::paint(batch, &self.gui.atlas, &draws);
}
@@ -71,10 +73,11 @@ impl Game<'_> {
.tick(held, f64::from(self.settings.notification_display_time));
}
/// `Hud.extractSelectedItemName`.
pub(super) fn draw_selected_name(&self, batch: &mut HudBatch, can_hurt: bool) {
pub(super) fn draw_selected_name(&self, batch: &mut HudBatch, bar_height: i32, can_hurt: bool) {
let draws = self.tool_highlight.draw(
batch.width as i32,
batch.height as i32,
bar_height,
can_hurt,
&self.gui.atlas.font,
self.settings.text_background(0.0),
+18 -2
View File
@@ -257,8 +257,24 @@ impl Game<'_> {
// Player.startAutoSpinAttack(20, 8, stack).
p.auto_spin_attack_ticks = 20;
p.auto_spin_attack_dmg = 8.0;
if p.ground.on_ground {
p.position.y += 1.2;
}
// `player.move(SELF, (0, 1.1999999, 0))` when on the ground: the lift
// stops at a ceiling.
let lift = self
.player
.as_ref()
.filter(|p| p.ground.on_ground)
.map(|p| p.bounds());
if let Some(bounds) = lift {
let mut scene = SceneCollisions {
world: &self.world,
binding: self.collision,
subject: None,
};
let up = Vec3::new(0.0, f64::from(1.1999999f32), 0.0);
let moved = scene.resolve(bounds, up, 0.0, true).unwrap_or(Vec3::ZERO);
if let Some(p) = self.player.as_mut() {
p.position.y += moved.y;
}
}
let sound = format!("minecraft:item.trident.riptide_{}", riptide.min(3));
+10
View File
@@ -485,6 +485,11 @@ pub struct HealthHud {
/// `Util.getMillis()` of the last health change.
pub last_health_time: i64,
pub health_blink_time: i64,
/// `Hud.leftHeight` and `rightHeight` once the health, armor, food, mount
/// and air layers have run; the selected item name and the action bar sit
/// above them.
pub left_height: i32,
pub right_height: i32,
}
pub struct HealthInputs {
@@ -645,6 +650,9 @@ impl HealthHud {
let rows = ceil((max_health + absorption as f32) / 2.0 / 10.0);
let row_height = (10 - (rows - 2)).max(3);
let mut y_air = y_base - 10;
self.left_height = 39 + (rows - 1) * row_height + 10 + if i.armor > 0 { 10 } else { 0 };
// The mount's heart rows (`extractVehicleHealth`); food adds its row below.
self.right_height = 39 + 10 * ((i.vehicle_hearts + 9) / 10);
let offset_index = if i.regeneration {
i.tick % ceil(max_health + 5.0)
} else {
@@ -739,11 +747,13 @@ impl HealthHud {
}
}
y_air -= 10;
self.right_height += 10;
}
// Hud.extractAirBubbles.
let max_air = i.max_air.max(1);
let air = i.air.clamp(0, max_air);
if i.under_water || air < max_air {
self.right_height += 10;
let rows = (f64::from(i.vehicle_hearts) / 10.0).ceil() as i32;
let y = y_air - (rows - 1) * 10;
let bubble = |offset: i32| ceil(((air + offset) * 10) as f32 / max_air as f32);
+11 -4
View File
@@ -323,11 +323,13 @@ impl Titles {
}
}
}
/// `Hud.extractOverlayMessage`.
/// `Hud.extractOverlayMessage`; `bar_height` is
/// `max(leftHeight, rightHeight)`.
pub fn overlay_message(
&self,
w: i32,
h: i32,
bar_height: i32,
partial: f32,
font: &Font,
background: u32,
@@ -343,9 +345,12 @@ impl Titles {
let alpha = ((t * 255.0 / 20.0) as i32).min(255);
if alpha > 0 {
let message = message.resolve_legacy();
// The bars' height plus the gap between the item name (59) and the
// overlay (68), at least the default level.
let y_shift = bar_height + (68 - 59);
out.push(GuiDraw::Push(vec![PoseOp::Translate(
(w / 2) as f32,
(h - 68) as f32,
(h - y_shift.max(68)) as f32,
)]));
let color = if self.overlay_animated {
hsv_to_argb(t / 50.0, 0.7, 0.6, alpha as u32)
@@ -463,11 +468,13 @@ impl ToolHighlight {
self.last = selected;
}
/// `Hud.extractSelectedItemName`; `can_hurt` is
/// `gameMode.canHurtPlayer()` (survival and adventure).
/// `gameMode.canHurtPlayer()` (survival and adventure) and `bar_height`
/// is `max(leftHeight, rightHeight)`.
pub fn draw(
&self,
w: i32,
h: i32,
bar_height: i32,
can_hurt: bool,
font: &Font,
background: u32,
@@ -488,7 +495,7 @@ impl ToolHighlight {
}
let str_width = width(font, &name);
let x = (w - str_width) / 2;
let mut y = h - 59;
let mut y = h - bar_height.max(59);
if !can_hurt {
y += 14;
}
+10
View File
@@ -106,6 +106,16 @@ impl StackData {
pub fn other(&self, id: &str) -> Option<&Tag> {
self.other.iter().find(|(k, _)| k == id).map(|(_, v)| v)
}
/// `SetNameFunction.run`: a loot text component stored as `custom_name`
/// (plain text, as modelled) or as the unmodelled `item_name`.
pub fn set_loot_name(&mut self, id: &str, text: &serde_json::Value) {
let tag = crate::scoreboard::json_to_tag(text);
if id == "minecraft:custom_name" {
self.custom_name = read_text(&tag);
} else {
self.set_other(id, Some(tag));
}
}
/// Set (or with None remove) an unmodelled component.
pub fn set_other(&mut self, id: &str, value: Option<Tag>) {
self.other.retain(|(k, _)| k != id);
+16
View File
@@ -95,6 +95,22 @@ fn components_tag(s: &Stack, builtin: &Builtin, synced: &Dynamic) -> Option<Tag>
),
]),
Component::FireworkExplosion(e) => fireworks_tag(e),
Component::LodestoneTracker { target, tracked } => {
let mut entries = Vec::new();
if let Some((dimension, pos)) = target {
entries.push((
"target",
Tag::compound([
("dimension", Tag::String(dimension.as_str().into())),
("pos", Tag::IntArray(pos.to_vec())),
]),
));
}
if !tracked {
entries.push(("tracked", Tag::Byte(0)));
}
Tag::compound(entries)
}
Component::Enchantments(list) => {
// Enchantments are a synced (data-driven) registry: the
// ids index the server's RegistryData, not the builtin
+26
View File
@@ -1154,6 +1154,8 @@ impl Items {
ctx.evaluate(table, random)
}
}
/// `DataComponents.BLOCK_STATE`.
const BLOCK_STATE: &str = "minecraft:block_state";
impl LootContext<'_> {
fn evaluate<R: RandomSource>(&self, table: &Value, random: &mut R) -> Vec<ItemStack> {
let ctx = self;
@@ -1561,6 +1563,30 @@ impl LootContext<'_> {
}
}
}
// CopyBlockState: the listed properties of the broken block
// become the item's `block_state` component.
"minecraft:copy_state" => {
use horizon_world::nbt::Tag;
let mut copied = match stack.data().and_then(|d| d.other(BLOCK_STATE)) {
Some(Tag::Compound(c)) => c.clone(),
_ => Default::default(),
};
for name in f["properties"]
.as_array()
.into_iter()
.flatten()
.filter_map(Value::as_str)
{
if let Some(value) = self.properties.get(name) {
copied.insert(name.into(), Tag::String(value.as_str().into()));
}
}
if !copied.is_empty() {
stack
.data_mut()
.set_other(BLOCK_STATE, Some(Tag::Compound(copied)));
}
}
// ApplyExplosionDecay: each item survives with 1 / radius.
"minecraft:explosion_decay" => {
if let Some(radius) = self.explosion_radius {
+15
View File
@@ -614,6 +614,21 @@ fn network_stack(
));
}
stack.added.extend(firework_components(d));
// The compass's lodestone target.
if let Some(t) = d.other("minecraft:lodestone_tracker") {
let target = t.get("target").and_then(|g| {
let Some(horizon_world::nbt::Tag::IntArray(pos)) = g.get("pos") else {
return None;
};
let dimension = crate::save::text(g.get("dimension"))?;
Some((dimension, [*pos.first()?, *pos.get(1)?, *pos.get(2)?]))
});
let tracked = crate::save::number(t.get("tracked")).is_none_or(|v| v != 0);
stack.added.push((
horizon_protocol::item::component::LODESTONE_TRACKER,
horizon_protocol::item::Component::LodestoneTracker { target, tracked },
));
}
// PotionContents: the potion by its `minecraft:potion` id.
if let Some(id) = (d.potion.as_deref()).and_then(|potion| {
synced_id(
+5 -1
View File
@@ -178,6 +178,9 @@ pub struct Hub {
/// An integrated server's owner (`MinecraftServer.isSingleplayerOwner`),
/// whose moves are never checked.
pub singleplayer_owner: Option<u128>,
/// The world is superflat (`ServerLevel.isFlat`, a world-wide flag, sent as
/// `CommonPlayerSpawnInfo.isFlat` in every dimension).
pub flat_world: bool,
/// Who rides what, set by the game before each tick.
pub riding: super::entity::Riding,
/// The scoreboard's teams, set by the game each tick, for players'
@@ -358,6 +361,7 @@ impl Hub {
immediate_respawn: false,
movement_checks: (true, true),
singleplayer_owner: None,
flat_world: false,
riding: Default::default(),
last_clock: i64::MIN,
last_rate: 0.0,
@@ -610,7 +614,7 @@ impl Hub {
game_type: game_mode,
previous_game_type: None,
debug: false,
flat: false,
flat: self.flat_world,
last_death: None,
portal_cooldown: 0,
sea_level: 63,
+7 -1
View File
@@ -1888,8 +1888,14 @@ impl Mob {
/// the last-hurt-by bookkeeping goals read.
/// The `armor` and `armor_toughness` modifiers of worn humanoid armour.
pub fn worn_armor(&self) -> (f32, f32) {
// An equine keeps its horse armor in `extra.head`: the item's `armor`
// and `armor_toughness` modifiers of the body slot.
if equine::is_equine(self.species()) {
return (0.0, 0.0);
return self
.extra
.head
.as_deref()
.map_or((0.0, 0.0), spawning::horse_armor_stats);
}
[
&self.extra.head,
+10 -1
View File
@@ -1560,7 +1560,16 @@ pub fn after_hurt(m: &mut Mob, source: &Damage) {
// PiglinAi.wasHurtBy: drop what is being admired, no admiring for
// a while after a player's hit, and anger.
Species::Piglin if source.attacker == Some(Who::Player) => {
m.extra.using_item = 0;
// stopHoldingOffHandItem(.., false): a gold ingot is not bartered
// and is gone; anything else ends its admiring next tick, as if
// the time ran out, and the game side puts it away.
if m.extra.off_hand.as_deref() == Some("minecraft:gold_ingot") {
m.extra.off_hand = None;
m.extra.using_item = 0;
} else {
m.extra.using_item =
i32::from(m.extra.off_hand.is_some() && m.extra.using_item > 0);
}
m.extra.egg_time = 400;
m.extra.anger = 600;
}
+12 -7
View File
@@ -311,6 +311,7 @@ fn loot_stack(item: &str, count: i32) -> LootStack {
exploration: None,
ominous_amplifier: None,
stew_effects: Vec::new(),
names: Vec::new(),
}
}
/// `Mob.dropChances.byEquipment(slot)`: the recorded chance, or the one the
@@ -510,13 +511,7 @@ pub fn die(m: &mut Mob, d: &Death, sequences: &mut RandomSequences) -> (Vec<Drop
}
}
// dropExperience.
let drops_experience = match s {
Species::Tadpole => false,
Species::Hoglin => true,
s if is_monster(s) => true,
_ => !m.is_baby(),
};
if killed_by_player && drops_experience && d.mob_drops {
if killed_by_player && drops_experience(m) && d.mob_drops {
let mut amount = base_experience(m);
while amount > 0 {
let value = orb_value(amount);
@@ -540,6 +535,16 @@ fn roll(
None => Vec::new(),
}
}
/// `LivingEntity.shouldDropExperience`: babies drop none, except monsters and
/// hoglins; tadpoles never do.
pub fn drops_experience(m: &Mob) -> bool {
match m.species() {
Species::Tadpole => false,
Species::Hoglin => true,
s if is_monster(s) => true,
_ => !m.is_baby(),
}
}
/// `getBaseExperienceReward` (no vanilla enchantment changes mob experience).
pub fn base_experience(m: &mut Mob) -> i32 {
use Species as S;
+64 -8
View File
@@ -546,19 +546,45 @@ impl Mob {
} else if !matches!(self.kind.species, super::Species::Bat | super::Species::Bee) {
self.check_fall_damage(w, moved, events);
}
// restituteMovementAfterCollisions with no bounciness: the colliding
// components become (signed) zero.
// Entity.restituteMovementAfterCollisions: the colliding components
// bounce back by the entity's bounciness (zero for most mobs, so they
// become signed zero). Block bounciness is not modelled for mobs.
if moved_vertically && vertical || self.horizontal_collision {
let v = self.velocity;
let restitution = self.entity_bounciness();
let mut after = v;
if x_hit {
self.velocity.x = -v.x * 0.0;
after.x = -v.x * restitution;
}
if z_hit {
self.velocity.z = -v.z * 0.0;
after.z = -v.z * restitution;
}
if vertical {
self.velocity.y = (0.0 - v.y) * 1.0 * 0.0;
let mut restitution = restitution;
if self.vertical_collision_below {
// Only a hard enough landing bounces, and never on honey.
let on = (
self.position.x.floor() as i32,
(self.position.y - f64::from(0.2_f32)).floor() as i32,
self.position.z.floor() as i32,
);
if -v.y <= self.gravity()
|| restitution > 0.0
&& w.block_name(on.0, on.1, on.2) == "minecraft:honey_block"
{
restitution = 0.0;
}
}
let (compensation, drag) = if restitution > 0.0 {
let portion = moved.y / v.y;
let air_drag = f64::from(self.air_drag());
(portion * self.gravity(), 1.0 + portion * (air_drag - 1.0))
} else {
(0.0, 1.0)
};
after.y = (compensation - v.y) * drag * restitution;
}
self.velocity = after;
}
// Entity.checkSupportingBlock
self.supporting = self.supporting_block(w, moved);
@@ -814,6 +840,11 @@ impl Mob {
let at = [self.position.x, self.position.y, self.position.z];
events.push(MobEvent::Sound("entity.breeze.land", at, 1.0, 1.0));
}
// Creeper.causeFallDamage: the fall swells the fuse, up to five ticks
// short of exploding.
if self.species() == super::Species::Creeper {
self.swell = (self.swell + (self.fall_distance * 1.5) as i32).min(30 - 5);
}
if self.kind.flags.fall_immune {
return;
}
@@ -909,9 +940,33 @@ impl Mob {
let y = (self.position.y - 0.500_001).floor() as i32;
w.facts.get(w.world.block(x, y, z)).friction
}
/// `getAttributeValue(BOUNCINESS)`.
fn entity_bounciness(&self) -> f64 {
super::sulfur_cube::physics(self).map_or(0.0, |a| f64::from(a.bounciness.clamp(0.0, 1.0)))
}
/// `LivingEntity.computeModifiedFriction`.
fn modified_friction(friction: f32, modifier: f32) -> f32 {
(1.0 - (1.0 - friction) * modifier).clamp(0.0, 1.0)
}
/// `LivingEntity.getAirDrag`: what vertical air drag a bounce applies.
fn air_drag(&self) -> f32 {
let base = if self.kind.flags.omnidirectional {
0.91
} else {
0.98
};
Self::modified_friction(base, self.air_drag_modifier())
}
/// `getAttributeValue(AIR_DRAG_MODIFIER)`.
fn air_drag_modifier(&self) -> f32 {
super::sulfur_cube::physics(self).map_or(1.0, |a| a.air_drag_modifier.clamp(0.0, 2048.0))
}
fn travel_in_air(&mut self, w: &MobWorld, input: Vec3, events: &mut Vec<MobEvent>) {
let friction = if self.on_ground {
self.block_friction(w)
// FRICTION_MODIFIER scales the block's slipperiness.
let modifier = super::sulfur_cube::physics(self)
.map_or(1.0, |a| a.friction_modifier.clamp(0.0, 2048.0));
Self::modified_friction(self.block_friction(w), modifier)
} else {
1.0
};
@@ -945,12 +1000,13 @@ impl Mob {
self.velocity = Vec3::new(movement.x, y, movement.z);
return;
}
let drag = 0.91f32;
let air_drag_modifier = self.air_drag_modifier();
let drag = Self::modified_friction(0.91, air_drag_modifier);
let f = friction * drag;
let vertical = if self.kind.flags.omnidirectional {
drag
} else {
0.98
Self::modified_friction(0.98, air_drag_modifier)
};
self.velocity = Vec3::new(
movement.x * f64::from(f),
+10 -2
View File
@@ -49,7 +49,7 @@ pub fn register(m: &mut Mob) {
// attach axis.
m.ai.targets.add(
2,
NearestTarget::player(true).when(|m, w| {
NearestTarget::player(true).gate(peaceful_off).when(|m, w| {
let Some(p) = &w.player else { return false };
let d = p.position.subtract(m.position);
let axis = match m.extra.variant3 {
@@ -426,6 +426,11 @@ impl ProjectileWorld for Probe<'_, '_> {
.full_collision
}
}
/// `level().getDifficulty() != Difficulty.PEACEFUL`, which the shulker's
/// attack goal and target goal check.
fn peaceful_off(_m: &Mob, w: &MobWorld) -> bool {
w.difficulty.effective != 0.0
}
/// `ShulkerAttackGoal`: open up and fire a bullet every one to five and a
/// half seconds at a target within 20 blocks.
#[derive(Clone, Debug)]
@@ -441,7 +446,7 @@ impl Goal for ShulkerAttack {
true
}
fn can_use(&mut self, m: &mut Mob, cx: &mut Cx) -> bool {
m.target_info(cx.w).is_some_and(|t| t.alive)
m.target_info(cx.w).is_some_and(|t| t.alive) && peaceful_off(m, cx.w)
}
fn start(&mut self, m: &mut Mob, cx: &mut Cx) {
self.attack_time = 20;
@@ -451,6 +456,9 @@ impl Goal for ShulkerAttack {
set_peek(m, 0, cx.events);
}
fn tick(&mut self, m: &mut Mob, cx: &mut Cx) {
if !peaceful_off(m, cx.w) {
return;
}
self.attack_time -= 1;
let (Some(who), Some(t)) = (m.ai.target, m.target_info(cx.w)) else {
return;
@@ -109,6 +109,19 @@ pub fn armor_stats(item: &str) -> (f32, f32) {
};
(points[slot], toughness)
}
/// The armour and toughness a horse armor item adds in the body slot
/// (`ArmorMaterial.createAttributes(ArmorType.BODY)`).
pub fn horse_armor_stats(item: &str) -> (f32, f32) {
match item.strip_prefix("minecraft:") {
Some("leather_horse_armor") => (3.0, 0.0),
Some("copper_horse_armor") => (4.0, 0.0),
Some("iron_horse_armor") => (5.0, 0.0),
Some("golden_horse_armor") => (7.0, 0.0),
Some("diamond_horse_armor") => (11.0, 2.0),
Some("netherite_horse_armor") => (19.0, 3.0),
_ => (0.0, 0.0),
}
}
/// `Mob.populateDefaultEquipmentSlots`: sometimes a set of armour, from the
/// head down, of one random material.
fn populate_armor(m: &mut Mob, d: &LocalDifficulty) {
+86 -6
View File
@@ -32,6 +32,12 @@ pub struct Archetype {
pub contact_damage: Option<f32>,
/// The knockback resistance modifier (the attribute clamps to 0..1).
pub knockback_resistance: f64,
/// `Attributes.BOUNCINESS` (the modifier added to a base of 0).
pub bounciness: f32,
/// `Attributes.FRICTION_MODIFIER` and `AIR_DRAG_MODIFIER` (base 1 times
/// the archetype's factor).
pub friction_modifier: f32,
pub air_drag_modifier: f32,
pub horizontal_power: f32,
pub vertical_power: f32,
pub hit_sound: &'static str,
@@ -40,13 +46,16 @@ pub struct Archetype {
pub push_threshold: f32,
}
macro_rules! arch {
($n:literal, $b:expr, $e:expr, $c:expr, $k:expr, $h:expr, $v:expr, $cd:expr, $t:expr) => {
($n:literal, $b:expr, $e:expr, $c:expr, $k:expr, $phys:expr, $h:expr, $v:expr, $cd:expr, $t:expr) => {
Archetype {
name: $n,
buoyant: $b,
explosion: $e,
contact_damage: $c,
knockback_resistance: $k,
bounciness: $phys.0,
friction_modifier: $phys.1,
air_drag_modifier: $phys.2,
horizontal_power: $h,
vertical_power: $v,
hit_sound: concat!("entity.sulfur_cube.", $n, ".hit"),
@@ -58,13 +67,25 @@ macro_rules! arch {
}
/// `data/minecraft/sulfur_cube_archetype/*.json`.
pub const ARCHETYPES: [Archetype; 12] = [
arch!("bouncy", true, None, None, -2.0, 0.4125, 0.105, 0.7, 0.3),
arch!(
"bouncy",
true,
None,
None,
-2.0,
(0.9, 0.3, 0.01),
0.4125,
0.105,
0.7,
0.3
),
arch!(
"explosive",
true,
Some((120, 3.0, false)),
None,
-1.0,
(0.5, 0.3, 0.3),
0.4125,
0.09,
0.7,
@@ -76,6 +97,7 @@ pub const ARCHETYPES: [Archetype; 12] = [
None,
None,
-1.0,
(0.5, 0.2, 0.01),
0.9125,
0.09,
0.9,
@@ -87,6 +109,7 @@ pub const ARCHETYPES: [Archetype; 12] = [
None,
None,
0.5,
(0.1, 0.05, 0.01),
0.6625,
0.09,
1.0,
@@ -98,20 +121,55 @@ pub const ARCHETYPES: [Archetype; 12] = [
None,
None,
0.7,
(0.2, 1.0, 0.01),
0.4125,
0.09,
0.7,
0.03
),
arch!("hot", true, None, Some(1.0), -1.0, 0.4125, 0.09, 0.7, 0.2),
arch!("light", true, None, None, -1.0, 0.4125, 0.18, 0.7, 0.2),
arch!("regular", true, None, None, -1.0, 0.4125, 0.09, 0.5, 0.2),
arch!(
"hot",
true,
None,
Some(1.0),
-1.0,
(0.5, 0.3, 0.1),
0.4125,
0.09,
0.7,
0.2
),
arch!(
"light",
true,
None,
None,
-1.0,
(1.0, 0.3, 1.8),
0.4125,
0.18,
0.7,
0.2
),
arch!(
"regular",
true,
None,
None,
-1.0,
(0.5, 0.3, 0.1),
0.4125,
0.09,
0.5,
0.2
),
arch!(
"slow_bouncy",
false,
None,
None,
0.4,
(0.6, 0.3, 0.05),
0.4125,
0.24,
0.5,
@@ -123,6 +181,7 @@ pub const ARCHETYPES: [Archetype; 12] = [
None,
None,
0.5,
(0.4, 0.4, 0.1),
0.4125,
0.105,
0.9,
@@ -134,12 +193,24 @@ pub const ARCHETYPES: [Archetype; 12] = [
None,
None,
0.8,
(0.1, 0.05, 0.01),
0.4125,
0.09,
1.0,
0.02
),
arch!("sticky", false, None, None, -2.0, 0.4125, 0.09, 0.5, 0.05),
arch!(
"sticky",
false,
None,
None,
-2.0,
(0.0, 2.0, 0.01),
0.4125,
0.09,
0.5,
0.05
),
];
/// `SulfurCubeArchetype.DEFAULT_*`: no block swallowed.
const DEFAULT_PUSH: (&str, f32, f32) = ("entity.sulfur_cube.regular.push", 0.5, 0.2);
@@ -178,6 +249,15 @@ pub fn has_body(m: &Mob) -> bool {
pub fn archetype(m: &Mob) -> Option<&'static Archetype> {
m.extra.head.as_deref().and_then(archetype_of)
}
/// The archetype whose bounciness, friction and air drag attributes a
/// sulfur cube's physics use (no other mob has those modifiers).
pub fn physics(m: &Mob) -> Option<&'static Archetype> {
if m.species() == super::Species::SulfurCube {
archetype(m)
} else {
None
}
}
pub fn fuse(m: &Mob) -> i32 {
m.extra.counter2 - 1
}
+6 -1
View File
@@ -563,7 +563,12 @@ pub fn custom_server_ai_step(m: &mut Mob, cx: &mut Cx) {
return zombified_piglin_step(m, cx);
}
if matches!(m.species(), Species::Zombie | Species::Husk) && m.alive() {
if m.extra.counter2 > 0 {
if m.extra.counter2 > 0 && !m.fluids.eye_in_water {
// ConversionTracker.tick: leaving the water ends a started
// conversion too.
m.extra.counter = -1;
m.extra.counter2 = 0;
} else if m.extra.counter2 > 0 {
m.extra.counter2 -= 1;
if m.extra.counter2 == 0 {
let (kind, sound) = if m.species() == Species::Husk {
+3 -7
View File
@@ -353,18 +353,14 @@ pub fn food_points(item: &str) -> i32 {
_ => 0,
}
}
/// The farmer's `requestedItems`.
/// The farmer's `requestedItems` (`VillagerProfession.FARMER`). Food is picked
/// up and traded separately, through its villager food value.
pub fn farmer_items() -> &'static [&'static str] {
&[
"minecraft:bread",
"minecraft:potato",
"minecraft:carrot",
"minecraft:wheat",
"minecraft:wheat_seeds",
"minecraft:beetroot",
"minecraft:beetroot_seeds",
"minecraft:torchflower_seeds",
"minecraft:pitcher_pod",
"minecraft:bone_meal",
]
}
/// A villager's `requestedItems` by profession.
+2 -1
View File
@@ -1937,7 +1937,8 @@ impl EntityMoveWorld for MotionWorld<'_, '_, '_> {
let input = EdgeBackoffInput {
movement,
bounds: self.player.bounds(),
flying: false,
// Player.maybeBackOffFromEdge: not while flying.
flying: self.player.flying,
self_or_player: matches!(mover, MoverType::SelfMovement | MoverType::Player),
fall_distance: self.player.fall_distance,
query_budget: 1_000_000,
@@ -69,6 +69,9 @@ pub fn loot_item_with_components(s: &horizon_world::loot::LootStack) -> ItemStac
.stew_effects
.push((Arc::from(effect.as_str()), *duration));
}
for (id, text) in &s.names {
item.data_mut().set_loot_name(id, text);
}
item
}
@@ -287,6 +290,7 @@ mod tests {
exploration: None,
ominous_amplifier: None,
stew_effects: Vec::new(),
names: Vec::new(),
};
// An End city chest's enchant_with_levels roll.
let boots = loot_item_with_components(&stack(
@@ -349,6 +353,7 @@ mod tests {
})))),
ominous_amplifier: None,
stew_effects: Vec::new(),
names: Vec::new(),
};
let mut item = loot_item(&stack);
assert!(has_pending_map(&item));
+27 -1
View File
@@ -3,7 +3,7 @@
//! client sends back in container clicks (HashedStack).
use crate::{
Result,
buf::{Codec, Reader, Writer},
buf::{Codec, Reader, Writer, pack_block_pos, unpack_block_pos},
};
use horizon_world::nbt::Tag;
@@ -40,6 +40,7 @@ pub mod component {
pub const WRITABLE_BOOK_CONTENT: i32 = 56;
pub const WRITTEN_BOOK_CONTENT: i32 = 57;
pub const TRIM: i32 = 58;
pub const LODESTONE_TRACKER: i32 = 69;
pub const FIREWORK_EXPLOSION: i32 = 70;
pub const FIREWORKS: i32 = 71;
}
@@ -99,6 +100,12 @@ pub enum Component {
},
/// `FireworkExplosion` (a firework star).
FireworkExplosion(FireworkExplosion),
/// `LodestoneTracker`: the target's dimension and block, and whether it
/// follows the lodestone.
LodestoneTracker {
target: Option<(String, [i32; 3])>,
tracked: bool,
},
/// `ArmorTrim` naming its material and pattern by registry id (`trim_material`,
/// `trim_pattern`); a trim with a direct holder stays [`Component::Raw`].
Trim {
@@ -316,6 +323,17 @@ impl Component {
}
}
FIREWORK_EXPLOSION => Self::FireworkExplosion(FireworkExplosion::read(r)?),
LODESTONE_TRACKER => {
let target = if r.bool()? {
Some((r.string(32767)?, unpack_block_pos(r.i64()?)))
} else {
None
};
Self::LodestoneTracker {
target,
tracked: r.bool()?,
}
}
// ArmorTrim.STREAM_CODEC: two holders, each a reference's id plus
// one. A direct material or pattern has no registry name: read past.
TRIM => {
@@ -437,6 +455,14 @@ impl Component {
explosions.iter().for_each(|e| e.write(w));
}
Self::FireworkExplosion(e) => e.write(w),
Self::LodestoneTracker { target, tracked } => {
w.bool(target.is_some());
if let Some((dimension, pos)) = target {
w.string(dimension);
w.i64(pack_block_pos(*pos));
}
w.bool(*tracked);
}
Self::WritableBook(pages) => {
w.varint(pages.len() as i32);
for page in pages {
@@ -2243,6 +2243,7 @@ packet!(
PaddleBoat { left: bool => Bool, right: bool => Bool }
);
packet!(PickItemFromBlock { pos: [i32; 3] => Pos, include_data: bool => Bool });
packet!(PickItemFromEntity { id: i32 => VarInt, include_data: bool => Bool });
packet!(
/// ServerboundPlayerActionPacket.Action: 0 start destroy, 1 change destroy
/// direction, 2 abort, 3 stop destroy, 4 drop all, 5 drop item, 6 release
@@ -2321,6 +2322,7 @@ packet_enum!(Serverbound, crate::ids::play::serverbound {
MoveVehicle(MoveVehicleRequest) = MOVE_VEHICLE,
PaddleBoat(PaddleBoat) = PADDLE_BOAT,
PickItemFromBlock(PickItemFromBlock) = PICK_ITEM_FROM_BLOCK,
PickItemFromEntity(PickItemFromEntity) = PICK_ITEM_FROM_ENTITY,
PingRequest(crate::packets::status::Ping) = PING_REQUEST,
PlayerAbilities(PlayerAbilitiesRequest) = PLAYER_ABILITIES,
PlayerAction(PlayerAction) = PLAYER_ACTION,
+23
View File
@@ -433,6 +433,29 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
self.game_event(pos, "minecraft:block_destroy");
self.remove_block(pos, false)
}
/// `IceBlock.playerDestroy` (after the drops): mined ice turns into water
/// when it sits over a solid block or a liquid, unless the tool prevents
/// ice melting (silk touch). In the nether, where water evaporates, the
/// block stays air.
pub(crate) fn ice_destroyed_by_player(
&mut self,
pos: BlockPos,
destroyed: BlockStateId,
prevents_melting: bool,
) {
let b = self.b;
if !b.data(destroyed).is(Class::Ice)
|| prevents_melting
|| self.level.dimension() == super::Dimension::Nether
{
return;
}
let below = self.below_state(pos);
if b.in_set(below, &b.tags.blocks_motion) || b.liquid(below) {
let water = b.default_of(b.named.water);
self.set_block_and_update(pos, water);
}
}
/// `entityInside` for entity-sensing blocks.
pub(crate) fn entity_inside(&mut self, pos: BlockPos) {
let b = self.b;
+14
View File
@@ -653,6 +653,20 @@ impl Behaviours {
) -> bool {
self.sim(level, ticks).player_destroy(pos, creative)
}
/// `IceBlock.playerDestroy` after the drops: mined ice melts into water
/// unless the tool prevents ice melting (silk touch). `destroyed` is the
/// state that was broken.
pub fn ice_destroyed_by_player<L: LevelAccess + ?Sized>(
&self,
level: &mut L,
ticks: &mut Ticks,
pos: BlockPos,
destroyed: BlockStateId,
prevents_melting: bool,
) {
self.sim(level, ticks)
.ice_destroyed_by_player(pos, destroyed, prevents_melting);
}
/// `BaseFireBlock.getState(level, pos)`: soul fire on its base blocks,
/// otherwise fire shaped by its neighbours.
pub fn fire_state_for<L: LevelAccess + ?Sized>(
+13 -1
View File
@@ -34,6 +34,8 @@ pub struct LootStack {
/// Added by set_stew_effect (the `suspicious_stew_effects` component):
/// (effect id, duration in ticks).
pub stew_effects: Vec<(String, i32)>,
/// Set by set_name: (`custom_name` or `item_name`, the text component).
pub names: Vec<(String, Value)>,
}
/// `ExplorationMapFunction`'s settings.
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -78,6 +80,7 @@ impl LootStack {
exploration: None,
ominous_amplifier: None,
stew_effects: Vec::new(),
names: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
@@ -1224,7 +1227,16 @@ impl<R: RandomSource> Context<'_, R> {
stack = smelted;
}
}
// Names, lore, components and other data without random draws.
// SetNameFunction: the component the target names (custom_name by
// default). The entity-resolved selectors and scores are not
// modelled; no table uses `entity`.
"minecraft:set_name" if !f["name"].is_null() => {
let target = f["target"].as_str().unwrap_or("custom_name");
let id = format!("minecraft:{target}");
stack.names.retain(|(k, _)| *k != id);
stack.names.push((id, f["name"].clone()));
}
// Lore, components and other data without random draws.
_ => {}
}
stack