mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Compare commits
61
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57fe750035 | ||
|
|
3849e37393 | ||
|
|
9b091ed657 | ||
|
|
777e0e2eb5 | ||
|
|
04ecdee7a2 | ||
|
|
6ddc3226d4 | ||
|
|
1903865cd5 | ||
|
|
d5d54a8775 | ||
|
|
c27e2d2c2d | ||
|
|
d463d793a9 | ||
|
|
f10425675c | ||
|
|
aa8ab4253d | ||
|
|
fa56070097 | ||
|
|
dd5863edc2 | ||
|
|
37cabdcb02 | ||
|
|
a40ef4eee0 | ||
|
|
914a6956e9 | ||
|
|
698b49e3a5 | ||
|
|
ec067f1e39 | ||
|
|
6081a66629 | ||
|
|
ee69413a68 | ||
|
|
fc4e7af854 | ||
|
|
0e069c31d7 | ||
|
|
fbbff39e07 | ||
|
|
49c2000de3 | ||
|
|
f561d4845b | ||
|
|
d6c4e5a027 | ||
|
|
ad271604af | ||
|
|
73a9345c43 | ||
|
|
d511139408 | ||
|
|
3093f8c594 | ||
|
|
905fd0d193 | ||
|
|
f92cf816b1 | ||
|
|
0c9bddecda | ||
|
|
e872d3ae54 | ||
|
|
aed4e26f59 | ||
|
|
b5adad15eb | ||
|
|
d17df3ca05 | ||
|
|
9ae3882c1a | ||
|
|
89c195fa6e | ||
|
|
8a309c393d | ||
|
|
58a99a2ce9 | ||
|
|
c439b806b8 | ||
|
|
2886b8f9ba | ||
|
|
dc36089156 | ||
|
|
08c959807d | ||
|
|
b8d0590d6d | ||
|
|
bc2dd35cc3 | ||
|
|
849b5b663b | ||
|
|
b65b0a6a7d | ||
|
|
605e8a1488 | ||
|
|
6823e34451 | ||
|
|
f25fbb6f2e | ||
|
|
872409ea57 | ||
|
|
5657080884 | ||
|
|
fe167781c4 | ||
|
|
0d02a4c722 | ||
|
|
f2424043f3 | ||
|
|
45996d8d78 | ||
|
|
94592a35ac | ||
|
|
1b4fe0f067 |
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> {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user