mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 17:21:49 -10:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca0637da80 | ||
|
|
47459b0b19 | ||
|
|
9b16d37e80 | ||
|
|
221545e92b | ||
|
|
88b27070bd | ||
|
|
499f8bddfc | ||
|
|
5904922865 | ||
|
|
49e706e8be | ||
|
|
5e63b0512d | ||
|
|
22b2711cf9 | ||
|
|
c924894624 | ||
|
|
4daa91cf52 | ||
|
|
5595db9077 | ||
|
|
fe70ff90b4 | ||
|
|
60611ed5a3 | ||
|
|
ad76f911bd | ||
|
|
5ed8181adc | ||
|
|
5617d702cc | ||
|
|
52d8ed6721 | ||
|
|
de358cd721 | ||
|
|
0f3a92ac2b | ||
|
|
a79077b30e | ||
|
|
ac811d4708 | ||
|
|
6a58fc20da |
No files matched your search
@@ -87,3 +87,7 @@ rng-trace = ["horizon-core/rng-trace"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
# Every WGSL shader translated as wgpu's DirectX 12 backend does (gfx.rs tests).
|
||||
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out", "msl-out", "spv-out"] }
|
||||
@@ -1817,6 +1817,16 @@ impl Game<'_> {
|
||||
self.stack_in_mut(self.held_slot())
|
||||
}
|
||||
/// The slot of the hand that acts: the selected one, or `OFFHAND_SLOT`.
|
||||
/// `LivingEntity.canGlideUsing`: a glider in the chest slot that will
|
||||
/// not break with its next point of wear.
|
||||
fn glider_worn(&self) -> bool {
|
||||
self.armor[1].as_ref().is_some_and(|s| {
|
||||
self.res
|
||||
.items()
|
||||
.get(&s.item)
|
||||
.is_some_and(|i| i.glider && (i.max_damage == 0 || s.damage + 1 < i.max_damage))
|
||||
})
|
||||
}
|
||||
fn held_slot(&self) -> usize {
|
||||
match self.hand {
|
||||
Hand::Main => self.selected,
|
||||
@@ -1994,6 +2004,28 @@ impl Game<'_> {
|
||||
particle.scale(0.6);
|
||||
self.particles.push(particle);
|
||||
}
|
||||
/// `Player.dropEquipment` for the player who died: unless keep_inventory
|
||||
/// is on, `destroyVanishingCursedItems`, then `Inventory.dropAll` from
|
||||
/// `at` (the eyes less 0.3): slots 0..36, armor feet to head (36..39),
|
||||
/// the off hand (40), each thrown in a random direction.
|
||||
fn drop_inventory(&mut self, at: [f64; 3]) {
|
||||
if self.ui.rules.bool("keep_inventory") {
|
||||
return;
|
||||
}
|
||||
let mut stacks: Vec<ItemStack> =
|
||||
self.inventory.iter_mut().filter_map(Option::take).collect();
|
||||
stacks.extend(self.armor.iter_mut().rev().filter_map(Option::take));
|
||||
stacks.extend(self.offhand.take());
|
||||
stacks.retain(|s| s.enchantment_level("minecraft:vanishing_curse") == 0);
|
||||
self.apply_equipment();
|
||||
for stack in stacks {
|
||||
let r = &mut self.world.random;
|
||||
let strength = f64::from(r.next_float()) * 0.5;
|
||||
let angle = f64::from(r.next_float()) * std::f64::consts::TAU;
|
||||
let v = [-angle.sin() * strength, 0.2, angle.cos() * strength];
|
||||
self.spawn_item(stack, at, v, 40);
|
||||
}
|
||||
}
|
||||
fn spawn_item(
|
||||
&mut self,
|
||||
stack: ItemStack,
|
||||
@@ -3649,16 +3681,8 @@ impl Game<'_> {
|
||||
// Switching hotbar slots only ends a main-hand use.
|
||||
let hand_changed = slot != self.selected && slot != shield::OFFHAND_SLOT;
|
||||
if !self.right || hand_changed || self.screen != Screen::Playing {
|
||||
if &*stack.item == "minecraft:bow" && !hand_changed {
|
||||
self.release_bow(ticks);
|
||||
}
|
||||
if &*stack.item == "minecraft:spyglass" {
|
||||
self.stop_spyglass();
|
||||
}
|
||||
if &*stack.item == "minecraft:trident" && !hand_changed {
|
||||
self.release_trident(ticks);
|
||||
}
|
||||
self.using = None;
|
||||
self.release_used_item(&stack, ticks, hand_changed);
|
||||
return;
|
||||
}
|
||||
let Some(info) = self.res.items().get(&stack.item).cloned() else {
|
||||
@@ -3714,6 +3738,17 @@ impl Game<'_> {
|
||||
self.using = Some((slot, ticks));
|
||||
}
|
||||
}
|
||||
/// `LivingEntity.releaseUsingItem` (`Item.releaseUsing`) for `stack`,
|
||||
/// used for `ticks`, its hand acting: a bow or a trident lets go (not
|
||||
/// when its hand changed), a spyglass stops scoping.
|
||||
pub(super) fn release_used_item(&mut self, stack: &ItemStack, ticks: u32, hand_changed: bool) {
|
||||
match &*stack.item {
|
||||
"minecraft:bow" if !hand_changed => self.release_bow(ticks),
|
||||
"minecraft:trident" if !hand_changed => self.release_trident(ticks),
|
||||
"minecraft:spyglass" => self.stop_spyglass(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
fn tick_mining(&mut self) -> Result<()> {
|
||||
if self.destroy_delay > 0 {
|
||||
self.destroy_delay -= 1;
|
||||
@@ -3937,13 +3972,7 @@ impl Game<'_> {
|
||||
.as_ref()
|
||||
.map_or(0, |s| s.enchantment_level("minecraft:respiration") as i32);
|
||||
player.pitch = self.pitch;
|
||||
// LivingEntity.canGlideUsing: a glider in its slot that will not break next.
|
||||
player.glider = self.armor[1].as_ref().is_some_and(|s| {
|
||||
self.res
|
||||
.items()
|
||||
.get(&s.item)
|
||||
.is_some_and(|i| i.glider && (i.max_damage == 0 || s.damage + 1 < i.max_damage))
|
||||
});
|
||||
player.glider = self.glider_worn();
|
||||
// A saved death already dropped inventory/XP and counted the death.
|
||||
if player.health.dead_or_dying() {
|
||||
self.screen = Screen::Dead;
|
||||
@@ -4021,6 +4050,7 @@ impl Game<'_> {
|
||||
let riding = self.riding.is_some();
|
||||
let ghast_collisions = self.happy_ghast_player_collisions();
|
||||
let spin_targets = self.spin_targets();
|
||||
let glider = self.glider_worn();
|
||||
let Some(player) = self.player.as_mut() else {
|
||||
// A dedicated server has no local player: the world still runs.
|
||||
if self.options.dedicated.is_some() {
|
||||
@@ -4063,13 +4093,7 @@ impl Game<'_> {
|
||||
.as_ref()
|
||||
.map_or(0, |s| s.enchantment_level("minecraft:respiration") as i32);
|
||||
player.pitch = self.pitch;
|
||||
// LivingEntity.canGlideUsing: a glider in its slot that will not break next.
|
||||
player.glider = self.armor[1].as_ref().is_some_and(|s| {
|
||||
self.res
|
||||
.items()
|
||||
.get(&s.item)
|
||||
.is_some_and(|i| i.glider && (i.max_damage == 0 || s.damage + 1 < i.max_damage))
|
||||
});
|
||||
player.glider = glider;
|
||||
// LocalPlayer.modifyInput: walking while using an item.
|
||||
player.item_use_speed = self
|
||||
.using
|
||||
@@ -4306,20 +4330,8 @@ impl Game<'_> {
|
||||
.experience
|
||||
.death_reward(self.ui.rules.bool("keep_inventory") || self.spectator());
|
||||
self.spawn_orbs(feet, reward);
|
||||
if !self.creative && !self.ui.rules.bool("keep_inventory") {
|
||||
// Inventory.dropAll: slots 0..36, armor feet to head (36..39), off hand (40).
|
||||
let mut stacks: Vec<ItemStack> =
|
||||
self.inventory.iter_mut().filter_map(Option::take).collect();
|
||||
stacks.extend(self.armor.iter_mut().rev().filter_map(Option::take));
|
||||
stacks.extend(self.offhand.take());
|
||||
self.apply_equipment();
|
||||
for stack in stacks {
|
||||
let r = &mut self.world.random;
|
||||
let strength = f64::from(r.next_float()) * 0.5;
|
||||
let angle = f64::from(r.next_float()) * std::f64::consts::TAU;
|
||||
let v = [-angle.sin() * strength, 0.2, angle.cos() * strength];
|
||||
self.spawn_item(stack, at, v, 40);
|
||||
}
|
||||
if !self.spectator() {
|
||||
self.drop_inventory(at);
|
||||
}
|
||||
self.audio
|
||||
.play("minecraft:entity.player.death", Some(at), 1.0, 1.0);
|
||||
|
||||
@@ -335,6 +335,12 @@ impl Game<'_> {
|
||||
Some(Ticker::Shulker { .. }) => v["lid"] = json!(self.block_anim(pos, 1.0).shulker.0),
|
||||
_ => {}
|
||||
}
|
||||
// The lines of a sign, as this client has them.
|
||||
if let Some(horizon_client::containers::BlockEntity::Sign(sign)) =
|
||||
self.world.block_entities.get(&(pos[0], pos[1], pos[2]))
|
||||
{
|
||||
v["sign"] = json!({ "front": sign.front.lines, "back": sign.back.lines });
|
||||
}
|
||||
v
|
||||
}
|
||||
pub(super) fn stack_value(&self, s: &ItemStack) -> Value {
|
||||
@@ -457,6 +463,7 @@ impl Game<'_> {
|
||||
"velocity": [p.velocity.x, p.velocity.y, p.velocity.z],
|
||||
"on_ground": p.ground.on_ground,
|
||||
"flying": p.flying,
|
||||
"fall_flying": p.fall_flying,
|
||||
"sneaking": self.keys.contains(&KeyCode::ShiftLeft),
|
||||
"sprinting": self.keys.contains(&KeyCode::ControlLeft),
|
||||
"health": p.health.health(),
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
//! pixels, 1024 characters, 100 pages), signing it into a written book,
|
||||
//! reading written books, and lecterns (placing a book, turning its pages
|
||||
//! with a redstone pulse, the comparator output and taking the book).
|
||||
use super::Game;
|
||||
use super::{Game, lan::LanHost};
|
||||
use horizon_client::{
|
||||
containers::{BlockEntity, Lectern},
|
||||
first_person::Hand,
|
||||
hud::{HudBatch, rgba},
|
||||
item_data::WrittenBook,
|
||||
items::ItemStack,
|
||||
@@ -14,6 +15,7 @@ use horizon_client::{
|
||||
ui,
|
||||
};
|
||||
use horizon_core::position::BlockPos;
|
||||
use horizon_protocol::packets::play::{self, Clientbound as C, Serverbound as S};
|
||||
use horizon_world::level::flags as level_flags;
|
||||
use std::time::Instant;
|
||||
use winit::{
|
||||
@@ -48,9 +50,32 @@ impl Game<'_> {
|
||||
fn book_lines(&self, page: &str) -> Vec<Text> {
|
||||
self.gui.atlas.font.split(&Text::literal(page), 114.0)
|
||||
}
|
||||
/// `WritableBookItem.use` / `WrittenBookItem.use`.
|
||||
/// `WritableBookItem.use` / `WrittenBookItem.use` (`Player.openItemGui`).
|
||||
/// A server's player writes in its own client's editor
|
||||
/// (`LocalPlayer.openItemGui`), and reads on its own client once told to
|
||||
/// (`ServerPlayer.openItemGui`'s ClientboundOpenBookPacket).
|
||||
pub(super) fn use_book(&mut self, stack: &ItemStack) -> bool {
|
||||
let screen = match &*stack.item {
|
||||
let written = match &*stack.item {
|
||||
"minecraft:writable_book" => false,
|
||||
"minecraft:written_book" => true,
|
||||
_ => return false,
|
||||
};
|
||||
if self.lan_acting.is_some() {
|
||||
if written {
|
||||
let hand = i32::from(self.hand == Hand::Off);
|
||||
self.guest_packets
|
||||
.push(C::OpenBook(play::OpenBook { hand }));
|
||||
}
|
||||
} else if let Some(screen) = self.book_screen(stack) {
|
||||
self.open_book(screen);
|
||||
}
|
||||
self.swing_arm();
|
||||
true
|
||||
}
|
||||
/// The screen a book in hand opens: `BookEditScreen` for a book and
|
||||
/// quill, `BookViewScreen` for a written book.
|
||||
fn book_screen(&self, stack: &ItemStack) -> Option<BookScreen> {
|
||||
Some(match &*stack.item {
|
||||
"minecraft:writable_book" => {
|
||||
let mut pages = stack
|
||||
.data()
|
||||
@@ -77,12 +102,31 @@ impl Game<'_> {
|
||||
page: 0,
|
||||
lectern: None,
|
||||
},
|
||||
_ => return false,
|
||||
};
|
||||
self.open_book(screen);
|
||||
self.swing_arm();
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
/// `MultiPlayerGameMode.useItem`'s own use of a book and quill on a
|
||||
/// server's client: its editor opens here at once.
|
||||
pub(super) fn remote_use_book(&mut self, stack: &ItemStack) -> bool {
|
||||
if &*stack.item != "minecraft:writable_book" {
|
||||
return false;
|
||||
}
|
||||
if let Some(screen) = self.book_screen(stack) {
|
||||
self.open_book(screen);
|
||||
}
|
||||
true
|
||||
}
|
||||
/// `ClientPacketListener.handleOpenBook`: the written book in that hand.
|
||||
pub(super) fn remote_open_book(&mut self, hand: i32) {
|
||||
let hand = if hand == 1 { Hand::Off } else { Hand::Main };
|
||||
let screen = self.with_hand(hand, |g| {
|
||||
let stack = g.held().filter(|s| &*s.item == "minecraft:written_book")?;
|
||||
g.book_screen(&stack.clone())
|
||||
});
|
||||
if let Some(screen) = screen {
|
||||
self.open_book(screen);
|
||||
}
|
||||
}
|
||||
fn open_book(&mut self, screen: BookScreen) {
|
||||
self.book = Some(screen);
|
||||
self.keys.clear();
|
||||
@@ -104,7 +148,9 @@ impl Game<'_> {
|
||||
self.book = None;
|
||||
self.capture(true);
|
||||
}
|
||||
/// `BookEditScreen.saveChanges`: trailing empty pages dropped.
|
||||
/// `BookEditScreen.saveChanges` and `BookSignScreen`'s Sign and Close:
|
||||
/// trailing empty pages dropped, the book in hand updated here and, on a
|
||||
/// server's client, sent (ServerboundEditBookPacket).
|
||||
fn save_book(&mut self, sign: bool) {
|
||||
let Some(BookScreen::Edit {
|
||||
slot, pages, title, ..
|
||||
@@ -116,29 +162,59 @@ impl Game<'_> {
|
||||
while pages.len() > 1 && pages.last().is_some_and(String::is_empty) {
|
||||
pages.pop();
|
||||
}
|
||||
if let Some(stack) = self.stack_in_mut(slot).as_mut()
|
||||
&& &*stack.item == "minecraft:writable_book"
|
||||
{
|
||||
if sign {
|
||||
let mut written = ItemStack::new("minecraft:written_book", 1);
|
||||
written.data_mut().written = Some(WrittenBook {
|
||||
title: title.trim().to_owned(),
|
||||
author: super::chat::PLAYER_NAME.to_owned(),
|
||||
generation: 0,
|
||||
pages,
|
||||
});
|
||||
*stack = written;
|
||||
} else {
|
||||
stack.data_mut().writable_pages = if pages.iter().all(String::is_empty) {
|
||||
Vec::new()
|
||||
} else {
|
||||
pages
|
||||
};
|
||||
stack.tidy();
|
||||
}
|
||||
let title = sign.then(|| title.trim().to_owned());
|
||||
if self.remote.is_some() {
|
||||
self.send(S::EditBook(play::EditBook {
|
||||
slot: slot as i32,
|
||||
pages: pages.clone(),
|
||||
title: title.clone(),
|
||||
}));
|
||||
}
|
||||
self.write_book(slot, pages, title);
|
||||
self.capture(true);
|
||||
}
|
||||
/// The book and quill at `slot` takes the pages, or with a title becomes
|
||||
/// the written book its writer signed.
|
||||
fn write_book(&mut self, slot: usize, pages: Vec<String>, title: Option<String>) {
|
||||
let author = self
|
||||
.lan_acting_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| super::chat::PLAYER_NAME.to_owned());
|
||||
let Some(stack) = self
|
||||
.stack_in_mut(slot)
|
||||
.as_mut()
|
||||
.filter(|s| &*s.item == "minecraft:writable_book")
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if let Some(title) = title {
|
||||
let mut written = ItemStack::new("minecraft:written_book", 1);
|
||||
written.data_mut().written = Some(WrittenBook {
|
||||
title,
|
||||
author,
|
||||
generation: 0,
|
||||
pages,
|
||||
});
|
||||
*stack = written;
|
||||
} else {
|
||||
stack.data_mut().writable_pages = if pages.iter().all(String::is_empty) {
|
||||
Vec::new()
|
||||
} else {
|
||||
pages
|
||||
};
|
||||
stack.tidy();
|
||||
}
|
||||
}
|
||||
/// `ServerGamePacketListenerImpl.handleEditBook`: a guest's book in a
|
||||
/// hotbar slot or its off hand (`updateBookContents`, `signBook`).
|
||||
pub(super) fn lan_edit_book(&mut self, host: &mut LanHost, conn: u32, p: play::EditBook) {
|
||||
if !((0..9).contains(&p.slot) || p.slot == super::shield::OFFHAND_SLOT as i32) {
|
||||
return;
|
||||
}
|
||||
self.as_guest(host, conn, |g| {
|
||||
g.write_book(p.slot as usize, p.pages, p.title)
|
||||
});
|
||||
}
|
||||
pub(super) fn book_key(&mut self, event: &KeyEvent) {
|
||||
if event.state != ElementState::Pressed {
|
||||
return;
|
||||
@@ -519,7 +595,26 @@ impl Game<'_> {
|
||||
Some(key) => self.lectern_button(key, 3),
|
||||
None => return Err("not a lectern".into()),
|
||||
},
|
||||
Some("done") => self.close_book(),
|
||||
// The book and quill's buttons: Sign, then Sign and Close; Done
|
||||
// (which saves), then Cancel.
|
||||
Some("sign") => match self.book.as_mut() {
|
||||
Some(BookScreen::Edit {
|
||||
signing: true,
|
||||
title,
|
||||
..
|
||||
}) if !title.trim().is_empty() => self.save_book(true),
|
||||
Some(BookScreen::Edit { signing, .. }) => *signing = true,
|
||||
_ => return Err("not a book and quill".into()),
|
||||
},
|
||||
Some("done") => match self.book.as_mut() {
|
||||
Some(BookScreen::Edit { signing: true, .. }) => {
|
||||
if let Some(BookScreen::Edit { signing, .. }) = self.book.as_mut() {
|
||||
*signing = false;
|
||||
}
|
||||
}
|
||||
Some(BookScreen::Edit { .. }) => self.save_book(false),
|
||||
_ => self.close_book(),
|
||||
},
|
||||
Some(other) => return Err(format!("unknown book button {other}").into()),
|
||||
}
|
||||
Ok(match &self.book {
|
||||
|
||||
@@ -232,6 +232,13 @@ impl Game<'_> {
|
||||
"key" => {
|
||||
let key = key_code(arg(1)?).ok_or_else(|| format!("unknown key {}", words[1]))?;
|
||||
self.require_world()?;
|
||||
// The sign editor takes raw presses (Enter, arrows, Backspace, Escape).
|
||||
if self.sign_edit.is_some() {
|
||||
if arg(2)? != "up" {
|
||||
self.sign_input(Some(key), None);
|
||||
}
|
||||
return ok();
|
||||
}
|
||||
// A held edit modifier (`key super down`) and A/C/V/X: the
|
||||
// open book's Select All, Copy, Paste or Cut.
|
||||
if self.book.is_some() && arg(2)? != "up" {
|
||||
@@ -524,6 +531,10 @@ impl Game<'_> {
|
||||
self.book_type(text);
|
||||
return ok();
|
||||
}
|
||||
if self.sign_edit.is_some() {
|
||||
self.sign_input(None, Some(text));
|
||||
return ok();
|
||||
}
|
||||
match self.screen {
|
||||
Screen::Chat => self.chat_type(text),
|
||||
Screen::CommandBlock => self.command_block_type(text),
|
||||
@@ -549,7 +560,7 @@ impl Game<'_> {
|
||||
}
|
||||
_ => {
|
||||
return Err(
|
||||
"type needs the chat screen (key t tap), the language search, the creative search, the anvil or World Options".into(),
|
||||
"type needs the chat screen (key t tap), a sign or book, the language search, the creative search, the anvil or World Options".into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ pub(super) struct Creatures {
|
||||
pub pearl_cooldown: u32,
|
||||
/// The wind charge's use cooldown (ticks).
|
||||
pub wind_cooldown: u32,
|
||||
/// Wind bursts from a guest's turn, waiting for it to end (game/wind.rs).
|
||||
pub wind_bursts: Vec<super::wind::Burst>,
|
||||
/// Ticks left of the elder guardian's apparition (MobAppearanceParticle).
|
||||
pub elder_apparition: i32,
|
||||
/// The wandering trader spawner's clock.
|
||||
@@ -1778,7 +1780,7 @@ impl Game<'_> {
|
||||
_ => self.projectile_hit(&p, hit)?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
self.flush_wind_bursts()
|
||||
}
|
||||
/// `projectile_hit` for a projectile the guest `conn` shot, run as them;
|
||||
/// as the host's when they have left.
|
||||
|
||||
@@ -411,7 +411,12 @@ impl Game<'_> {
|
||||
"game_mode": g.game_mode,
|
||||
// ItemCooldowns: the ticks the shield an axe disabled stays down.
|
||||
"shield_cooldown": h.play.get(&g.conn).map_or(0, |p| p.shield_cooldown),
|
||||
// The item the server has this guest using: its slot and ticks.
|
||||
"using": h.play.get(&g.conn).and_then(|p| p.using())
|
||||
.map(|(slot, ticks)| json!({ "slot": slot, "ticks": ticks })),
|
||||
"sneaking": g.sneaking,
|
||||
// The server's Elytra glide (shared flag 7).
|
||||
"fall_flying": g.fall_flying,
|
||||
"sleeping": g.bed,
|
||||
"health": g.health,
|
||||
// The server's body: its air supply and golden hearts.
|
||||
@@ -1006,6 +1011,8 @@ impl Game<'_> {
|
||||
drop(holding);
|
||||
lan::flush_all(host.hub.guests.iter().map(|g| &g.sender));
|
||||
self.lan = Some(host);
|
||||
// A mace's wind burst from a guest's attack.
|
||||
let _ = self.flush_wind_bursts();
|
||||
}
|
||||
|
||||
fn lan_guest_joined(
|
||||
|
||||
@@ -30,6 +30,8 @@ use winit::keyboard::KeyCode;
|
||||
/// Per-guest state of the gameplay code that lives outside `Guest`.
|
||||
#[derive(Default)]
|
||||
pub(super) struct GuestPlay {
|
||||
/// The sign this guest may write on (`SignBlockEntity.playerWhoMayEdit`).
|
||||
pub(super) sign_editing: Option<[i32; 3]>,
|
||||
mining: Option<Mining>,
|
||||
destroy_delay: u32,
|
||||
using: Option<(usize, u32)>,
|
||||
@@ -259,6 +261,7 @@ impl Game<'_> {
|
||||
motion.pitch = g.pitch;
|
||||
motion.sprinting = g.sprinting;
|
||||
motion.may_fly = g.creative() || g.game_mode == SPECTATOR;
|
||||
motion.flying = g.flying;
|
||||
motion.invulnerable = motion.may_fly;
|
||||
let _ = motion.set_health(g.health, false);
|
||||
motion.health.food.food = g.food;
|
||||
@@ -513,7 +516,8 @@ impl Game<'_> {
|
||||
std::mem::swap(&mut self.mount, &mut play.mount);
|
||||
self.container = container;
|
||||
self.screen = screen;
|
||||
self.sign_edit = host_sign;
|
||||
// A sign the guest's placement opened (SignBlock.openTextEdit).
|
||||
let guest_sign = std::mem::replace(&mut self.sign_edit, host_sign);
|
||||
// A lectern the guest's use opened (LecternBlock.openScreen).
|
||||
let guest_lectern = match std::mem::replace(&mut self.book, host_book) {
|
||||
Some(super::books::BookScreen::View {
|
||||
@@ -652,6 +656,9 @@ impl Game<'_> {
|
||||
if let Some(station) = opened_station {
|
||||
self.lan_station_opened(host, conn, station);
|
||||
}
|
||||
if let Some(edit) = guest_sign {
|
||||
self.lan_open_sign_editor(host, conn, edit);
|
||||
}
|
||||
if let Some(key) = guest_lectern {
|
||||
self.lan_open_lectern(host, conn, key);
|
||||
}
|
||||
@@ -865,14 +872,20 @@ impl Game<'_> {
|
||||
self.as_guest(host, conn, |g| g.drop_held(a.action == 4));
|
||||
}
|
||||
}
|
||||
// RELEASE_USE_ITEM: ServerPlayer.releaseUsingItem, with the
|
||||
// hand of the item in use acting.
|
||||
6 => {
|
||||
self.as_guest(host, conn, |g| {
|
||||
if let Some((slot, ticks)) = g.using.take()
|
||||
&& g.stack_in(slot)
|
||||
.is_some_and(|s| &*s.item == "minecraft:bow")
|
||||
{
|
||||
g.release_bow(ticks);
|
||||
}
|
||||
let Some(hand) = g.used_hand() else {
|
||||
return;
|
||||
};
|
||||
g.with_hand(hand, |g| {
|
||||
if let Some((slot, ticks)) = g.using.take()
|
||||
&& let Some(stack) = g.stack_in(slot).cloned()
|
||||
{
|
||||
g.release_used_item(&stack, ticks, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// SWAP_ITEM_WITH_OFFHAND: the hands swap and the item in use stops
|
||||
@@ -1029,10 +1042,13 @@ impl Game<'_> {
|
||||
g.open_ridden_mount_menu();
|
||||
});
|
||||
}
|
||||
S::PlayerCommand(p) if p.action == 6 => self.lan_start_gliding(host, conn),
|
||||
S::ContainerButtonClick(p) => {
|
||||
self.lan_station_button(host, conn, p.container, p.button);
|
||||
}
|
||||
S::RenameItem(p) => self.lan_station_rename(host, conn, &p.name),
|
||||
S::SignUpdate(p) => self.lan_sign_update(host, conn, p),
|
||||
S::EditBook(p) => self.lan_edit_book(host, conn, p),
|
||||
S::ContainerSlotStateChanged(p) => {
|
||||
self.lan_crafter_slot_state(host, conn, p.container, p.slot, p.enabled);
|
||||
}
|
||||
@@ -1727,6 +1743,9 @@ impl Game<'_> {
|
||||
}
|
||||
.death_reward(self.ui.rules.bool("keep_inventory") || g.game_mode == SPECTATOR);
|
||||
let (name, uuid) = (g.name.clone(), g.uuid);
|
||||
// ServerPlayer.die: dropAllDeathLoot unless a spectator, from the eyes less 0.3.
|
||||
let drops = (g.game_mode != SPECTATOR)
|
||||
.then(|| [at[0], at[1] + g.eye_height() - 0.3, at[2]]);
|
||||
let cause = host.play.get_mut(&conn).and_then(|p| p.death_cause.take());
|
||||
let message = match cause {
|
||||
// ServerPlayer.die: the death message, to everyone unless
|
||||
@@ -1760,6 +1779,9 @@ impl Game<'_> {
|
||||
);
|
||||
// LivingEntity.die: the death animation, the dying player included.
|
||||
host.hub.entity_event(id, 3, at);
|
||||
if let Some(eyes) = drops {
|
||||
self.as_guest(host, conn, |game| game.drop_inventory(eyes));
|
||||
}
|
||||
self.spawn_orbs(at, reward);
|
||||
}
|
||||
}
|
||||
@@ -1846,6 +1868,13 @@ impl Game<'_> {
|
||||
/// sounds are heard at the guest.
|
||||
fn guest_server_tick(&mut self, host: &mut LanHost, conn: u32) {
|
||||
self.guest_vehicle_tick(host, conn);
|
||||
// The last move's fall stands in for the server's delta movement.
|
||||
let fell = host
|
||||
.hub
|
||||
.guest_mut(conn)
|
||||
.and_then(|g| g.moves.last().map(|m| m.0[1]))
|
||||
.unwrap_or(0.0);
|
||||
self.guest_glide_tick(host, conn, fell);
|
||||
let Some(g) = host.hub.guest_mut(conn) else {
|
||||
return;
|
||||
};
|
||||
@@ -1963,6 +1992,87 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// ServerGamePacketListenerImpl.handlePlayerCommand START_FALL_FLYING:
|
||||
/// `Player.tryToStartFallFlying`, or else `stopFallFlying` (a second
|
||||
/// start while gliding ends the glide, as in vanilla).
|
||||
fn lan_start_gliding(&mut self, host: &mut LanHost, conn: u32) {
|
||||
let Some(g) = host.hub.guest_mut(conn) else {
|
||||
return;
|
||||
};
|
||||
let start = !g.fall_flying && self.guest_can_glide(host, conn) == Some(true);
|
||||
let in_liquid = self
|
||||
.as_guest(host, conn, |game| {
|
||||
game.player
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.fluids.was_touching_water || p.is_in_lava())
|
||||
})
|
||||
.unwrap_or(true);
|
||||
if let Some(g) = host.hub.guest_mut(conn) {
|
||||
g.fall_flying = start && !in_liquid;
|
||||
}
|
||||
}
|
||||
|
||||
/// `Player.canGlide`: not flying, airborne, not riding, no levitation, and
|
||||
/// a glider that its next point of wear won't break.
|
||||
fn guest_can_glide(&mut self, host: &mut LanHost, conn: u32) -> Option<bool> {
|
||||
let g = host.hub.guest_mut(conn)?;
|
||||
if g.flying || g.on_ground || g.vehicle.is_some() {
|
||||
return Some(false);
|
||||
}
|
||||
self.as_guest(host, conn, |game| {
|
||||
game.glider_worn() && game.potions.effects.get("levitation").is_none()
|
||||
})
|
||||
}
|
||||
|
||||
/// `LivingEntity.updateFallFlying` on the server, once a tick for a
|
||||
/// gliding guest: the glide ends once `canGlide` fails; otherwise every
|
||||
/// 20th tick wears the glider by one, and every 10th is an
|
||||
/// `elytra_glide` game event. Entity.checkFallDistanceAccumulation runs
|
||||
/// first: a glide that isn't diving keeps the fall distance at 1.
|
||||
fn guest_glide_tick(&mut self, host: &mut LanHost, conn: u32, fell: f64) {
|
||||
let Some(g) = host.hub.guest_mut(conn) else {
|
||||
return;
|
||||
};
|
||||
if !g.fall_flying {
|
||||
if let Some(m) = g.motion.as_mut() {
|
||||
m.fall_fly_ticks = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
let creative = g.creative();
|
||||
if let Some(m) = g.motion.as_mut()
|
||||
&& fell > -0.5
|
||||
&& m.fall_distance > 1.0
|
||||
{
|
||||
m.fall_distance = 1.0;
|
||||
}
|
||||
if self.guest_can_glide(host, conn) != Some(true) {
|
||||
if let Some(g) = host.hub.guest_mut(conn) {
|
||||
g.fall_flying = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.as_guest(host, conn, |game| {
|
||||
let Some(p) = game.player.as_mut() else {
|
||||
return;
|
||||
};
|
||||
p.fall_fly_ticks += 1;
|
||||
let next = p.fall_fly_ticks + 1;
|
||||
let at = [p.position.x, p.position.y, p.position.z];
|
||||
if next % 10 != 0 {
|
||||
return;
|
||||
}
|
||||
if (next / 10) % 2 == 0
|
||||
&& !creative
|
||||
&& let Some(elytra) = game.armor[1].as_mut()
|
||||
{
|
||||
elytra.damage += 1;
|
||||
}
|
||||
let ctx = game.player_context(None);
|
||||
game.post_game_event("minecraft:elytra_glide", at, ctx);
|
||||
});
|
||||
}
|
||||
|
||||
/// `Player.isInWall`: a suffocating block in the 0.48-wide box at the
|
||||
/// eyes. Suffocating is a full collision box that is also a full render
|
||||
/// cube (`BlockBehaviour.isSuffocating`'s default; glass and the like
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! `/particle` uses) and `ClientboundExplodePacket` (`ServerLevel.explode`).
|
||||
use super::Game;
|
||||
use horizon_client::particles::{ParticleOptions, ParticleType};
|
||||
use horizon_core::random::RandomSource;
|
||||
use horizon_core::{geometry::Vec3, random::RandomSource};
|
||||
use horizon_protocol::{
|
||||
entity_data::{Particle, ParticleData},
|
||||
packets::play::{self, Clientbound as C},
|
||||
@@ -154,7 +154,7 @@ impl Game<'_> {
|
||||
}
|
||||
|
||||
/// `ClientPacketListener.handleExplosion`: the blast's particles (and its
|
||||
/// sound, when the packet carries it).
|
||||
/// sound, when the packet carries it), then the player's push.
|
||||
pub(super) fn remote_explode(&mut self, p: &play::Explode) {
|
||||
if p.play_sound {
|
||||
let pitch = (1.0
|
||||
@@ -168,37 +168,88 @@ impl Game<'_> {
|
||||
pitch,
|
||||
);
|
||||
}
|
||||
let (world, facts) = (&self.world, self.res.facts());
|
||||
self.particles
|
||||
.explosion(p.center, p.radius, p.block_count.max(0) as usize, &|b| {
|
||||
facts.is_air(world.block(b[0], b[1], b[2]))
|
||||
});
|
||||
if let Some(options) = self.received_particle(&p.particle, p.center) {
|
||||
self.particles.add(&options, p.center, [1.0, 0.0, 0.0]);
|
||||
}
|
||||
// A wind charge's burst throws none.
|
||||
if !p.block_particles.is_empty() {
|
||||
let (world, facts) = (&self.world, self.res.facts());
|
||||
self.particles.explosion_blocks(
|
||||
p.center,
|
||||
p.radius,
|
||||
p.block_count.max(0) as usize,
|
||||
&|b| facts.is_air(world.block(b[0], b[1], b[2])),
|
||||
);
|
||||
}
|
||||
// Entity.pushFromExplosion.
|
||||
if let (Some(push), Some(player)) = (p.knockback, self.player.as_mut()) {
|
||||
player.velocity = Vec3::new(
|
||||
player.velocity.x + push[0],
|
||||
player.velocity.y + push[1],
|
||||
player.velocity.z + push[2],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `ServerLevel.explode`'s packet to each player within 64 blocks. The
|
||||
/// sound and the push reach them on their own (the level's sounds, the
|
||||
/// players' motion), so the packet carries neither.
|
||||
/// motion a hurt player is sent), so the packet carries neither.
|
||||
pub(super) fn lan_explode(&self, center: [f64; 3], radius: f32, block_count: usize) {
|
||||
let Some(host) = self.lan.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let particle = |name: &str| {
|
||||
Some(Particle {
|
||||
id: host.hub.registry_id("minecraft:particle_type", name)?,
|
||||
data: None,
|
||||
})
|
||||
};
|
||||
let emitter = if radius < 2.0 {
|
||||
"minecraft:explosion"
|
||||
} else {
|
||||
"minecraft:explosion_emitter"
|
||||
};
|
||||
let (Some(at_center), Some(poof), Some(smoke)) = (
|
||||
particle(emitter),
|
||||
particle("minecraft:poof"),
|
||||
particle("minecraft:smoke"),
|
||||
) else {
|
||||
return;
|
||||
let sound = "minecraft:entity.generic.explode";
|
||||
if let Some(packet) =
|
||||
self.explosion_packet(center, radius, block_count, emitter, sound, true)
|
||||
{
|
||||
self.send_explosion(packet, &[]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A wind charge's burst (`AbstractWindCharge.explode`): gust emitters,
|
||||
/// no block particles, and for each player it pushed the push
|
||||
/// (`ServerExplosion.hitPlayers`), which only that client applies, as
|
||||
/// nothing hurt them.
|
||||
pub(super) fn lan_wind_burst(
|
||||
&self,
|
||||
center: [f64; 3],
|
||||
radius: f32,
|
||||
sound: &str,
|
||||
block_count: usize,
|
||||
pushed: &[(u32, [f64; 3])],
|
||||
) {
|
||||
let emitter = if radius < 2.0 {
|
||||
"minecraft:gust_emitter_small"
|
||||
} else {
|
||||
"minecraft:gust_emitter_large"
|
||||
};
|
||||
if let Some(packet) =
|
||||
self.explosion_packet(center, radius, block_count, emitter, sound, false)
|
||||
{
|
||||
self.send_explosion(packet, pushed);
|
||||
}
|
||||
}
|
||||
|
||||
/// `ClientboundExplodePacket` without a push, or None while not serving.
|
||||
/// The sound is the level's, which the players hear already, so it isn't
|
||||
/// played from the packet.
|
||||
fn explosion_packet(
|
||||
&self,
|
||||
center: [f64; 3],
|
||||
radius: f32,
|
||||
block_count: usize,
|
||||
emitter: &str,
|
||||
sound: &str,
|
||||
block_particles: bool,
|
||||
) -> Option<play::Explode> {
|
||||
let hub = &self.lan.as_ref()?.hub;
|
||||
let particle = |name: &str| {
|
||||
Some(Particle {
|
||||
id: hub.registry_id("minecraft:particle_type", name)?,
|
||||
data: None,
|
||||
})
|
||||
};
|
||||
// Level.DEFAULT_EXPLOSION_BLOCK_PARTICLES.
|
||||
let info = |particle, scaling| play::ExplosionParticle {
|
||||
@@ -207,22 +258,49 @@ impl Game<'_> {
|
||||
speed: 1.0,
|
||||
weight: 1,
|
||||
};
|
||||
let packet = C::Explode(play::Explode {
|
||||
let block_particles = if block_particles {
|
||||
vec![
|
||||
info(particle("minecraft:poof")?, 0.5),
|
||||
info(particle("minecraft:smoke")?, 1.0),
|
||||
]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
Some(play::Explode {
|
||||
center,
|
||||
radius,
|
||||
block_count: block_count as i32,
|
||||
knockback: None,
|
||||
particle: at_center,
|
||||
sound: match host
|
||||
.hub
|
||||
.registry_id("minecraft:sound_event", "minecraft:entity.generic.explode")
|
||||
{
|
||||
particle: particle(emitter)?,
|
||||
sound: match hub.registry_id("minecraft:sound_event", sound) {
|
||||
Some(id) => (id, None),
|
||||
None => (-1, Some(("minecraft:entity.generic.explode".into(), None))),
|
||||
None => (-1, Some((sound.into(), None))),
|
||||
},
|
||||
block_particles: vec![info(poof, 0.5), info(smoke, 1.0)],
|
||||
block_particles,
|
||||
play_sound: false,
|
||||
});
|
||||
host.hub.broadcast_near(center, 64.0, &packet, None);
|
||||
})
|
||||
}
|
||||
|
||||
/// `packet` to each player within 64 blocks of its centre, with the push
|
||||
/// `pushed` holds for them.
|
||||
fn send_explosion(&self, mut packet: play::Explode, pushed: &[(u32, [f64; 3])]) {
|
||||
let Some(host) = self.lan.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let center = packet.center;
|
||||
let plain = C::Explode(packet.clone()).encode();
|
||||
for g in host.hub.guests.iter().filter(|g| host.hub.here(g)) {
|
||||
let d: f64 = (0..3).map(|k| (g.position[k] - center[k]).powi(2)).sum();
|
||||
if d >= 4096.0 {
|
||||
continue;
|
||||
}
|
||||
match pushed.iter().find(|(conn, _)| *conn == g.conn) {
|
||||
Some((_, push)) => {
|
||||
packet.knockback = Some(*push);
|
||||
g.send(&C::Explode(packet.clone()));
|
||||
}
|
||||
None => g.sender.send(plain.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -588,17 +588,7 @@ impl Game<'_> {
|
||||
let [x, y, z] = gs.pos;
|
||||
let name = &self.res.facts().get(self.world.block(x, y, z)).name;
|
||||
let block = StationKind::from_block(name) == Some(gs.kind);
|
||||
let eye = [g.position[0], g.position[1] + g.eye_height(), g.position[2]];
|
||||
// The block interaction range (4.5, creative 5) and the buffer.
|
||||
let reach = if g.creative() { 5.0 } else { 4.5 } + 4.0;
|
||||
let near = (0..3)
|
||||
.map(|k| {
|
||||
let lo = f64::from(gs.pos[k]);
|
||||
(lo - eye[k]).max(eye[k] - lo - 1.0).max(0.0).powi(2)
|
||||
})
|
||||
.sum::<f64>()
|
||||
< reach * reach;
|
||||
if !(block && near) {
|
||||
if !(block && within_reach(g, gs.pos)) {
|
||||
let window = gs.window;
|
||||
self.lan_station_closed(host, conn, Some(window));
|
||||
host.hub.send_to(
|
||||
@@ -670,3 +660,18 @@ pub(super) fn station_inputs(gs: &GuestStation) -> Vec<ItemStack> {
|
||||
.map(|(_, _, inputs)| inputs.filter_map(|s| gs.menu.slot(s).cloned()).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// `Player.isWithinBlockInteractionRange(pos, 4)`: the guest's eyes within
|
||||
/// its block interaction range (4.5, creative 5) and a 4-block buffer of the
|
||||
/// block's box.
|
||||
pub(super) fn within_reach(g: &horizon_client::lan::guest::Guest, pos: [i32; 3]) -> bool {
|
||||
let eye = [g.position[0], g.position[1] + g.eye_height(), g.position[2]];
|
||||
let reach = if g.creative() { 5.0 } else { 4.5 } + 4.0;
|
||||
(0..3)
|
||||
.map(|k| {
|
||||
let lo = f64::from(pos[k]);
|
||||
(lo - eye[k]).max(eye[k] - lo - 1.0).max(0.0).powi(2)
|
||||
})
|
||||
.sum::<f64>()
|
||||
< reach * reach
|
||||
}
|
||||
@@ -641,6 +641,22 @@ impl Game<'_> {
|
||||
("mob_factor", Tag::Float(v[5].to_bits())),
|
||||
]),
|
||||
Component::Profile(p) => profile_tag(p),
|
||||
// ItemStackTemplate.CODEC: id, count and components.
|
||||
Component::Stacks(stacks) => Tag::List(
|
||||
stacks
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let mut entries = vec![
|
||||
("id", Tag::String(self.item_name(t.item).as_ref().into())),
|
||||
("count", Tag::Int(t.count)),
|
||||
];
|
||||
if let Some(c) = self.protocol_components(t) {
|
||||
entries.push(("components", c));
|
||||
}
|
||||
Tag::compound(entries)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
Component::CustomModelData {
|
||||
floats,
|
||||
flags,
|
||||
@@ -2557,6 +2573,8 @@ impl Game<'_> {
|
||||
self.drop_remote_chunks_out_of_range();
|
||||
}
|
||||
C::BlockEntityData(p) => self.remote_block_entity_data(p.pos, p.kind, &p.tag),
|
||||
C::OpenSignEditor(p) => self.remote_open_sign_editor(p),
|
||||
C::OpenBook(p) => self.remote_open_book(p.hand),
|
||||
// handleSetSpawn: ClientLevel.setRespawnData, which spawn compasses point at.
|
||||
C::SetDefaultSpawnPosition(p) => {
|
||||
let [x, y, z] = p.pos.pos;
|
||||
@@ -3129,6 +3147,12 @@ impl Game<'_> {
|
||||
.as_ref()
|
||||
.map_or(0, |s| s.enchantment_level("minecraft:respiration") as i32);
|
||||
player.fluids.fast_lava = self.dim == Dim::Nether;
|
||||
let glider = self.glider_worn();
|
||||
let Some(player) = self.player.as_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
player.glider = glider;
|
||||
let was_gliding = player.fall_flying;
|
||||
let dead = self.screen == Screen::Dead;
|
||||
// LivingEntity.isImmobile: a sleeper lies still in the bed.
|
||||
if let Some(head) = self.sleeping {
|
||||
@@ -3153,6 +3177,9 @@ impl Game<'_> {
|
||||
// LocalPlayer.onUpdateAbilities: flight turned on or off (a double
|
||||
// jump, a spectator, landing) is sent to the server.
|
||||
let flying = player.flying;
|
||||
// LocalPlayer.aiStep: a glide this client started is sent to the
|
||||
// server (ServerboundPlayerCommandPacket START_FALL_FLYING).
|
||||
let started_gliding = player.fall_flying && !was_gliding;
|
||||
let events = std::mem::take(&mut player.events);
|
||||
self.previous_eye = self.eye;
|
||||
self.eye += (player.dimensions.eye_height - self.eye) * 0.5;
|
||||
@@ -3192,6 +3219,14 @@ impl Game<'_> {
|
||||
flags: if flying { 2 } else { 0 },
|
||||
}));
|
||||
}
|
||||
if started_gliding {
|
||||
let id = self.remote.as_ref().map_or(0, |r| r.entity_id);
|
||||
self.send(S::PlayerCommand(play::PlayerCommand {
|
||||
id,
|
||||
action: 6,
|
||||
data: 0,
|
||||
}));
|
||||
}
|
||||
self.remote_send_state(input);
|
||||
Ok(())
|
||||
}
|
||||
@@ -4010,21 +4045,60 @@ impl Game<'_> {
|
||||
return false;
|
||||
};
|
||||
// MultiPlayerGameMode.useItem runs the use here too: food starts
|
||||
// being eaten (Consumable.startConsuming).
|
||||
// being eaten (Consumable.startConsuming), a book and quill opens.
|
||||
self.with_hand(hand, |g| g.start_eating(&info));
|
||||
// BowItem.use: startUsingItem here too (with an arrow, or in creative), so that letting
|
||||
// go sends RELEASE_USE_ITEM (Minecraft.handleKeybinds) and the server shoots.
|
||||
if &*held.item == "minecraft:bow" {
|
||||
self.with_hand(hand, |g| {
|
||||
if g.using.is_none() && (g.creative || g.projectile_slot("minecraft:bow").is_some())
|
||||
{
|
||||
g.using = Some((g.held_slot(), 0));
|
||||
}
|
||||
});
|
||||
if self.with_hand(hand, |g| g.remote_use_book(&held)) {
|
||||
return true;
|
||||
}
|
||||
self.with_hand(hand, |g| g.remote_start_held_use(&held));
|
||||
item_has_use(&info)
|
||||
}
|
||||
|
||||
/// `Item.use` runs on the client too (MultiPlayerGameMode.useItem): an
|
||||
/// item held in use until the button lets go starts being used here, so
|
||||
/// that letting go sends RELEASE_USE_ITEM (Minecraft.handleKeybinds) and
|
||||
/// the server ends the use: a bow or a trident is let go, a crossbow's
|
||||
/// charge ends, a shield is lowered, a spyglass stops scoping. Their
|
||||
/// sounds, loaded arrows and thrown tridents come from the server.
|
||||
fn remote_start_held_use(&mut self, held: &ItemStack) {
|
||||
if self.using.is_some() {
|
||||
return;
|
||||
}
|
||||
let start = match &*held.item {
|
||||
// BowItem.use: with an arrow, or in creative.
|
||||
"minecraft:bow" => self.creative || self.projectile_slot("minecraft:bow").is_some(),
|
||||
// CrossbowItem.use: charging when not charged and with ammunition;
|
||||
// a charged one shoots on the server.
|
||||
"minecraft:crossbow" => {
|
||||
held.data().is_none_or(|d| d.charged.is_empty())
|
||||
&& (self.creative || self.projectile_slot("minecraft:crossbow").is_some())
|
||||
}
|
||||
// TridentItem.use: not about to break, Riptide only in water or rain.
|
||||
"minecraft:trident" => {
|
||||
self.use_trident(held);
|
||||
return;
|
||||
}
|
||||
// SpyglassItem.use (its sound is the server's).
|
||||
"minecraft:spyglass" => true,
|
||||
// ShieldItem.use: anything that blocks attacks, off cooldown.
|
||||
_ => {
|
||||
self.raise_shield(self.held_slot());
|
||||
return;
|
||||
}
|
||||
};
|
||||
if start {
|
||||
self.using = Some((self.held_slot(), 0));
|
||||
}
|
||||
}
|
||||
/// Whether `stack` stays in use until the button lets go (bows,
|
||||
/// crossbows, tridents, spyglasses, shields): its use only counts ticks here.
|
||||
fn held_until_released(&self, stack: &ItemStack) -> bool {
|
||||
matches!(
|
||||
&*stack.item,
|
||||
"minecraft:bow" | "minecraft:crossbow" | "minecraft:trident" | "minecraft:spyglass"
|
||||
) || self.blocks_attacks(stack).is_some()
|
||||
}
|
||||
|
||||
/// The rest of Minecraft.startUseItem once the main hand's target passed:
|
||||
/// the main hand's item, then the off hand's turn (its block, its item).
|
||||
/// The server does each packet's part with that packet's hand only.
|
||||
@@ -4043,7 +4117,8 @@ impl Game<'_> {
|
||||
}
|
||||
self.remote_use_item(Hand::Off);
|
||||
}
|
||||
/// `LivingEntity.updatingUsingItem` on a server client: a drawn bow, and a
|
||||
/// `LivingEntity.updatingUsingItem` on a server client: an item held in
|
||||
/// use (`held_until_released`), and a
|
||||
/// consumable's eating animation and its crumbs. The server eats it and
|
||||
/// sends the stack, the hunger and the sounds.
|
||||
fn remote_tick_using(&mut self) {
|
||||
@@ -4051,8 +4126,9 @@ impl Game<'_> {
|
||||
return;
|
||||
};
|
||||
let stack = self.stack_in(slot).cloned();
|
||||
// A drawn bow: held until the button lets go (or another slot is picked).
|
||||
if stack.as_ref().is_some_and(|s| &*s.item == "minecraft:bow") {
|
||||
// A drawn bow, a charging crossbow, a raised shield...: held until
|
||||
// the button lets go (or another slot is picked).
|
||||
if stack.as_ref().is_some_and(|s| self.held_until_released(s)) {
|
||||
self.using = (slot == self.selected || slot == super::shield::OFFHAND_SLOT)
|
||||
.then_some((slot, ticks + 1));
|
||||
return;
|
||||
|
||||
@@ -69,7 +69,7 @@ impl Game<'_> {
|
||||
}
|
||||
};
|
||||
if let Some(r) = self.renderer.as_mut()
|
||||
&& let Err(e) = r.set_shader_pack(pack.as_ref())
|
||||
&& let Err(e) = r.set_shader_pack(pack.as_ref(), &self.pack.render.sprite_layers)
|
||||
{
|
||||
return self.shader_pack_error(&id, &e.to_string());
|
||||
}
|
||||
@@ -79,7 +79,10 @@ impl Game<'_> {
|
||||
/// Give the loaded pack to the renderer (also after it is rebuilt).
|
||||
pub(super) fn apply_shader_pack(&mut self) -> horizon_client::Result<()> {
|
||||
match self.renderer.as_mut() {
|
||||
Some(r) => r.set_shader_pack(self.shader_packs.pack.as_ref()),
|
||||
Some(r) => r.set_shader_pack(
|
||||
self.shader_packs.pack.as_ref(),
|
||||
&self.pack.render.sprite_layers,
|
||||
),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
//! The sign editing screen (`SignEditScreen` / `HangingSignEditScreen`):
|
||||
//! opened when a sign is placed, four lines limited to the sign's line
|
||||
//! width, Up/Down/Enter move between lines, Escape or Done closes.
|
||||
use super::Game;
|
||||
use super::{Game, lan::LanHost};
|
||||
use horizon_client::{
|
||||
containers::BlockEntity,
|
||||
hud::{HudBatch, rgba},
|
||||
signs::wood_type,
|
||||
};
|
||||
use horizon_protocol::packets::play::{self, Clientbound as C, Serverbound as S};
|
||||
use std::time::Instant;
|
||||
use winit::{
|
||||
event::{ElementState, KeyEvent},
|
||||
@@ -33,14 +34,95 @@ impl Game<'_> {
|
||||
self.right = false;
|
||||
self.capture(false);
|
||||
}
|
||||
/// `AbstractSignEditScreen.removed`: a server's client sends what it
|
||||
/// wrote (ServerboundSignUpdatePacket); the window's own world keeps it
|
||||
/// and sends it to its guests (`sendBlockUpdated`).
|
||||
pub(super) fn close_sign_editor(&mut self) {
|
||||
if let Some(edit) = self.sign_edit.take() {
|
||||
self.world
|
||||
.modified
|
||||
.insert((edit.pos.0 >> 4, edit.pos.2 >> 4));
|
||||
if self.remote.is_some() {
|
||||
if let Some(BlockEntity::Sign(sign)) = self.world.block_entities.get(&edit.pos) {
|
||||
let side = if edit.front { &sign.front } else { &sign.back };
|
||||
self.send(S::SignUpdate(play::SignUpdate {
|
||||
pos: [edit.pos.0, edit.pos.1, edit.pos.2],
|
||||
lines: side.lines.clone(),
|
||||
front: edit.front,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
self.mark_block_entity_updated(edit.pos);
|
||||
}
|
||||
}
|
||||
self.capture(true);
|
||||
}
|
||||
/// `ServerPlayer.openTextEdit` for the guest whose action opened a sign:
|
||||
/// the block, then the editor, on its client, and the guest may write on it.
|
||||
pub(super) fn lan_open_sign_editor(&mut self, host: &mut LanHost, conn: u32, edit: SignEditor) {
|
||||
let pos = [edit.pos.0, edit.pos.1, edit.pos.2];
|
||||
let state = self.world.block(pos[0], pos[1], pos[2]).0 as i32;
|
||||
host.hub
|
||||
.send_to(conn, &C::BlockUpdate(play::BlockUpdate { pos, state }));
|
||||
host.hub.send_to(
|
||||
conn,
|
||||
&C::OpenSignEditor(play::OpenSignEditor {
|
||||
pos,
|
||||
front: edit.front,
|
||||
}),
|
||||
);
|
||||
host.play.entry(conn).or_default().sign_editing = Some(pos);
|
||||
}
|
||||
/// `ServerGamePacketListenerImpl.handleSignUpdate`: the lines, formatting
|
||||
/// codes stripped, go on the sign the guest may write on unless it is
|
||||
/// waxed (`SignBlockEntity.updateSignText`); a guest gone out of reach
|
||||
/// may no longer write (`playerIsTooFarAwayToEdit`).
|
||||
pub(super) fn lan_sign_update(&mut self, host: &mut LanHost, conn: u32, p: play::SignUpdate) {
|
||||
let Some(g) = host.hub.guest(conn) else {
|
||||
return;
|
||||
};
|
||||
let near = super::lan_stations::within_reach(g, p.pos);
|
||||
let Some(play) = host.play.get_mut(&conn) else {
|
||||
return;
|
||||
};
|
||||
if !near {
|
||||
play.sign_editing = None;
|
||||
}
|
||||
if play.sign_editing != Some(p.pos) {
|
||||
log::warn!("{} just tried to change non-editable sign", g.name);
|
||||
return;
|
||||
}
|
||||
let key = (p.pos[0], p.pos[1], p.pos[2]);
|
||||
let Some(BlockEntity::Sign(sign)) = self.world.block_entities.get_mut(&key) else {
|
||||
return;
|
||||
};
|
||||
if sign.waxed {
|
||||
return;
|
||||
}
|
||||
let side = if p.front {
|
||||
&mut sign.front
|
||||
} else {
|
||||
&mut sign.back
|
||||
};
|
||||
for (line, new) in side.lines.iter_mut().zip(&p.lines) {
|
||||
*line = horizon_client::clipboard::strip_formatting(new);
|
||||
}
|
||||
play.sign_editing = None;
|
||||
self.mark_block_entity_updated(key);
|
||||
}
|
||||
/// `ClientPacketListener.handleOpenSignEditor`: the editor on a sign this
|
||||
/// client has.
|
||||
pub(super) fn remote_open_sign_editor(&mut self, p: play::OpenSignEditor) {
|
||||
let key = (p.pos[0], p.pos[1], p.pos[2]);
|
||||
if matches!(
|
||||
self.world.block_entities.get(&key),
|
||||
Some(BlockEntity::Sign(_))
|
||||
) {
|
||||
self.open_sign_editor(key, p.front);
|
||||
} else {
|
||||
log::warn!(
|
||||
"ignoring openTextEdit on a block that is no sign at {:?}",
|
||||
p.pos
|
||||
);
|
||||
}
|
||||
}
|
||||
pub(super) fn sign_edit_key(&mut self, event: &KeyEvent) {
|
||||
if event.state != ElementState::Pressed {
|
||||
return;
|
||||
|
||||
@@ -10,7 +10,7 @@ pub(super) const HELP: &[&str] = &[
|
||||
"use X Y Z open the block's menu as if right-clicked",
|
||||
"click-slot ID [right] [shift] click a menu slot (0-35 inventory, 300+ container, 600+ station, 650 result)",
|
||||
"click-gui X Y [right] [shift] click at GUI pixel X Y relative to the menu's top-left corner",
|
||||
"book [next|back|take|done] the open book or lectern: its page, or press one of its buttons",
|
||||
"book [next|back|take|sign|done] the open book or lectern: its page, or press one of its buttons",
|
||||
"type TEXT | type-back type into the anvil name box / delete one character",
|
||||
"enchant ID LEVEL enchant the held item (books store the enchantment)",
|
||||
"potion ITEM POTION give a potion item (potion, splash_potion, lingering_potion, tipped_arrow)",
|
||||
|
||||
@@ -22,6 +22,14 @@ const PLAYER_KNOCKBACK: f32 = 1.22;
|
||||
/// `BreezeWindCharge.RADIUS`.
|
||||
const BREEZE_RADIUS: f32 = 3.0;
|
||||
|
||||
/// A burst `wind_burst` puts off until a guest's turn is over.
|
||||
pub(super) struct Burst {
|
||||
center: [f64; 3],
|
||||
radius: f32,
|
||||
multiplier: f32,
|
||||
breeze: bool,
|
||||
wind_charge: bool,
|
||||
}
|
||||
/// `Projectile.deflect` with `ProjectileDeflection.REVERSE`.
|
||||
pub(super) fn reverse(p: &mut Projectile, jitter: f32) {
|
||||
p.velocity = Vec3::new(
|
||||
@@ -196,7 +204,9 @@ impl Game<'_> {
|
||||
set.into_iter().collect()
|
||||
}
|
||||
/// `ServerExplosion.hurtEntities` for a calculator that damages nothing:
|
||||
/// knockback scaled by distance, exposure and the multiplier.
|
||||
/// knockback scaled by distance, exposure and the multiplier. Returns the
|
||||
/// guests it pushed and by how much (`hitPlayers`), which their clients
|
||||
/// apply from the burst's packet.
|
||||
fn wind_knockback(
|
||||
&mut self,
|
||||
center: [f64; 3],
|
||||
@@ -204,7 +214,7 @@ impl Game<'_> {
|
||||
multiplier: f32,
|
||||
wind_charge: bool,
|
||||
blocklike: bool,
|
||||
) {
|
||||
) -> Vec<(u32, [f64; 3])> {
|
||||
let diameter = f64::from(radius * 2.0);
|
||||
let distance = |p: [f64; 3]| {
|
||||
((p[0] - center[0]).powi(2) + (p[1] - center[1]).powi(2) + (p[2] - center[2]).powi(2))
|
||||
@@ -223,42 +233,30 @@ impl Game<'_> {
|
||||
[d[0] / l, d[1] / l, d[2] / l]
|
||||
}
|
||||
};
|
||||
let multiplier = f64::from(multiplier);
|
||||
// The player (not a spectator; a flying creative player takes none).
|
||||
if let Some(p) = self.player.as_ref()
|
||||
&& !self.spectator()
|
||||
{
|
||||
let feet = [p.position.x, p.position.y, p.position.z];
|
||||
let dist = distance(feet) / diameter;
|
||||
let flying = self.creative && p.flying;
|
||||
if dist <= 1.0 && !flying {
|
||||
let w = f64::from(p.dimensions.width) / 2.0;
|
||||
let h = f64::from(p.dimensions.height);
|
||||
let eye = [
|
||||
feet[0],
|
||||
feet[1] + f64::from(p.dimensions.eye_height),
|
||||
feet[2],
|
||||
];
|
||||
let exposure = self.seen_percent(
|
||||
center,
|
||||
[feet[0] - w, feet[1], feet[2] - w],
|
||||
[feet[0] + w, feet[1] + h, feet[2] + w],
|
||||
);
|
||||
let power = (1.0 - dist) * f64::from(exposure) * multiplier;
|
||||
let dir = direction(eye);
|
||||
if let Some(p) = self.player.as_mut() {
|
||||
p.velocity = Vec3::new(
|
||||
p.velocity.x + dir[0] * power,
|
||||
p.velocity.y + dir[1] * power,
|
||||
p.velocity.z + dir[2] * power,
|
||||
);
|
||||
// ServerPlayer.onExplosionHit: a wind charge's burst cancels the
|
||||
// fall; any other burst ends the grace of the last impulse.
|
||||
let at = p.position;
|
||||
p.impulse.set_ignore_fall_damage(wind_charge, Some(at));
|
||||
// The player, then every other player in the level (LAN guests),
|
||||
// each swapped in as the player. In a guest's turn (a mace's wind
|
||||
// burst) the player is that guest.
|
||||
let own = self.wind_push_player(center, radius, multiplier, wind_charge);
|
||||
let mut pushed: Vec<_> = self.lan_acting.zip(own).into_iter().collect();
|
||||
if let Some(mut host) = self.lan.take() {
|
||||
let guests: Vec<u32> = host
|
||||
.hub
|
||||
.guests
|
||||
.iter()
|
||||
.filter(|g| host.hub.here(g) && g.loaded && !g.dead)
|
||||
.map(|g| g.conn)
|
||||
.collect();
|
||||
for conn in guests {
|
||||
let push = self.as_guest(&mut host, conn, |g| {
|
||||
g.wind_push_player(center, radius, multiplier, wind_charge)
|
||||
});
|
||||
if let Some(Some(push)) = push {
|
||||
pushed.push((conn, push));
|
||||
}
|
||||
}
|
||||
self.lan = Some(host);
|
||||
}
|
||||
let multiplier = f64::from(multiplier);
|
||||
let mut mobs = std::mem::take(&mut self.mobs);
|
||||
for m in mobs.iter_mut().filter(|m| m.alive()) {
|
||||
let feet = [m.position.x, m.position.y, m.position.z];
|
||||
@@ -351,6 +349,67 @@ impl Game<'_> {
|
||||
push(t.position, &mut t.velocity, self);
|
||||
}
|
||||
self.tnts = tnts;
|
||||
pushed
|
||||
}
|
||||
/// `ServerExplosion.hurtEntities` for the player (not a spectator, nor a
|
||||
/// creative player flying): the push, less with explosion knockback
|
||||
/// resistance, and `ServerPlayer.onExplosionHit`. Returns the push.
|
||||
fn wind_push_player(
|
||||
&mut self,
|
||||
center: [f64; 3],
|
||||
radius: f32,
|
||||
multiplier: f32,
|
||||
wind_charge: bool,
|
||||
) -> Option<[f64; 3]> {
|
||||
if self.spectator() {
|
||||
return None;
|
||||
}
|
||||
let resistance = self
|
||||
.player
|
||||
.as_mut()
|
||||
.and_then(|p| p.attribute("minecraft:explosion_knockback_resistance").ok())
|
||||
.unwrap_or(0.0);
|
||||
let creative = self.creative;
|
||||
let p = self.player.as_ref()?;
|
||||
let feet = [p.position.x, p.position.y, p.position.z];
|
||||
let dist = ((feet[0] - center[0]).powi(2)
|
||||
+ (feet[1] - center[1]).powi(2)
|
||||
+ (feet[2] - center[2]).powi(2))
|
||||
.sqrt()
|
||||
/ f64::from(radius * 2.0);
|
||||
if dist > 1.0 || creative && p.flying {
|
||||
return None;
|
||||
}
|
||||
let w = f64::from(p.dimensions.width) / 2.0;
|
||||
let h = f64::from(p.dimensions.height);
|
||||
let eye = [
|
||||
feet[0] - center[0],
|
||||
feet[1] + f64::from(p.dimensions.eye_height) - center[1],
|
||||
feet[2] - center[2],
|
||||
];
|
||||
let exposure = self.seen_percent(
|
||||
center,
|
||||
[feet[0] - w, feet[1], feet[2] - w],
|
||||
[feet[0] + w, feet[1] + h, feet[2] + w],
|
||||
);
|
||||
let power = (1.0 - dist) * f64::from(exposure) * f64::from(multiplier) * (1.0 - resistance);
|
||||
let l = (eye[0] * eye[0] + eye[1] * eye[1] + eye[2] * eye[2]).sqrt();
|
||||
let push = if l < 1.0e-5 {
|
||||
[0.0; 3]
|
||||
} else {
|
||||
eye.map(|d| d / l * power)
|
||||
};
|
||||
let p = self.player.as_mut()?;
|
||||
p.velocity = Vec3::new(
|
||||
p.velocity.x + push[0],
|
||||
p.velocity.y + push[1],
|
||||
p.velocity.z + push[2],
|
||||
);
|
||||
// ServerPlayer.onExplosionHit: a wind charge's burst cancels the
|
||||
// fall; any other burst ends the grace of the last impulse.
|
||||
let at = p.position;
|
||||
p.impulse.set_ignore_fall_damage(wind_charge, Some(at));
|
||||
Some(push)
|
||||
}
|
||||
/// The burst: knockback, the blocks that react, the gust and the sound.
|
||||
pub(super) fn wind_burst(
|
||||
@@ -361,13 +420,27 @@ impl Game<'_> {
|
||||
breeze: bool,
|
||||
wind_charge: bool,
|
||||
) -> Result<()> {
|
||||
// In a guest's turn (their charge landing, their mace's burst) the
|
||||
// host holds the other players (`as_guest`): the burst waits for the
|
||||
// turn to end so that it reaches every player, the guest too.
|
||||
if self.lan_acting.is_some() && self.lan.is_none() {
|
||||
self.creatures.wind_bursts.push(Burst {
|
||||
center,
|
||||
radius,
|
||||
multiplier,
|
||||
breeze,
|
||||
wind_charge,
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
// ServerExplosion.explode: the blast is heard first.
|
||||
let ctx = horizon_client::sculk::Context::default();
|
||||
self.post_game_event("minecraft:explode", center, ctx);
|
||||
let mut targets = self.wind_positions(center, radius);
|
||||
// ServerExplosion.shouldAffectBlocklikeEntities
|
||||
let blocklike = !breeze && !wind_charge && self.ui.rules.bool("mob_griefing");
|
||||
self.wind_knockback(center, radius, multiplier, wind_charge, blocklike);
|
||||
let pushed = self.wind_knockback(center, radius, multiplier, wind_charge, blocklike);
|
||||
let block_count = targets.len();
|
||||
// Explosion.canTriggerBlocks: a breeze's charge needs mob griefing.
|
||||
if !breeze || self.ui.rules.bool("mob_griefing") {
|
||||
// Util.shuffle(list, level.random)
|
||||
@@ -396,6 +469,14 @@ impl Game<'_> {
|
||||
};
|
||||
self.particles
|
||||
.add(&ParticleOptions::simple(ty), center, [1.0, 0.0, 0.0]);
|
||||
self.lan_wind_burst(center, radius, sound, block_count, &pushed);
|
||||
Ok(())
|
||||
}
|
||||
/// The bursts guests' turns put off.
|
||||
pub(super) fn flush_wind_bursts(&mut self) -> Result<()> {
|
||||
for b in std::mem::take(&mut self.creatures.wind_bursts) {
|
||||
self.wind_burst(b.center, b.radius, b.multiplier, b.breeze, b.wind_charge)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// `BlockBehaviour.onExplosionHit` for `TRIGGER_BLOCK`: doors, gates,
|
||||
|
||||
@@ -65,7 +65,8 @@ mod arena;
|
||||
mod cull;
|
||||
mod end_sky;
|
||||
mod flat;
|
||||
mod java_stages;
|
||||
pub(crate) mod java_stages;
|
||||
pub(crate) mod java_terrain;
|
||||
pub mod lightning;
|
||||
mod lod;
|
||||
mod occlusion;
|
||||
@@ -491,6 +492,15 @@ pub struct Renderer {
|
||||
world_post: world_post::Post,
|
||||
/// The selected shader pack's stages (Horizon setting; none by default).
|
||||
pack: Option<Pack>,
|
||||
/// A Java pack's terrain programs (`gfx/java_terrain.rs`): their page
|
||||
/// groups' layout, the glue's table, each arena page's group.
|
||||
java_page_layout: wgpu::BindGroupLayout,
|
||||
/// The device renders to and filters 16-bit normalized formats.
|
||||
java_norm16: bool,
|
||||
java_table: Option<wgpu::Buffer>,
|
||||
java_pages: Vec<Option<(wgpu::Buffer, wgpu::BindGroup)>>,
|
||||
/// The sprite layers and mip levels `animate` wrote this frame.
|
||||
animated: Vec<(u32, u32)>,
|
||||
pub hud: HudRenderer,
|
||||
/// Vanilla's menu background blur, made on first use.
|
||||
menu_blur: Option<crate::menu_blur::MenuBlur>,
|
||||
@@ -839,7 +849,10 @@ async fn open_device(
|
||||
| cull::Cull::features(&adapter)
|
||||
| (adapter.features()
|
||||
& (wgpu::Features::INDIRECT_FIRST_INSTANCE
|
||||
| wgpu::Features::MAPPABLE_PRIMARY_BUFFERS));
|
||||
| wgpu::Features::MAPPABLE_PRIMARY_BUFFERS
|
||||
// Java shader packs' 16-bit colour targets.
|
||||
| wgpu::Features::TEXTURE_FORMAT_16BIT_NORM
|
||||
| wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES));
|
||||
let driver = format!("{} {}", info.driver, info.driver_info);
|
||||
startup_step(format_args!(
|
||||
"requesting the device (driver {}) with features [{features}] and the adapter's \
|
||||
@@ -1369,8 +1382,10 @@ impl Renderer {
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
// COPY_SRC: animated sprites into a Java pack's atlas.
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::COPY_DST
|
||||
| wgpu::TextureUsages::COPY_SRC
|
||||
| wgpu::TextureUsages::STORAGE_BINDING,
|
||||
view_formats: &[],
|
||||
},
|
||||
@@ -2214,6 +2229,8 @@ impl Renderer {
|
||||
std::mem::size_of::<TerrainVertex>() as u64,
|
||||
1 << 20,
|
||||
);
|
||||
let java_page_layout = java_terrain::page_layout(&device);
|
||||
let java_norm16 = java_terrain::norm16_targets(&adapter, &device);
|
||||
// Sections as one index list per frame where multi-draw is emulated.
|
||||
let flat = flat::wanted(&adapter, &device).then(|| {
|
||||
let buffers = [Some(wgpu::VertexBufferLayout {
|
||||
@@ -2414,6 +2431,11 @@ impl Renderer {
|
||||
occlusion: occlusion::Occlusion::new(crate::chunk::MIN_Y, crate::chunk::HEIGHT / 16),
|
||||
world_post,
|
||||
pack: None,
|
||||
java_page_layout,
|
||||
java_norm16,
|
||||
java_table: None,
|
||||
java_pages: Vec::new(),
|
||||
animated: Vec::new(),
|
||||
hud,
|
||||
menu_blur: None,
|
||||
info,
|
||||
@@ -2722,6 +2744,18 @@ impl Renderer {
|
||||
base: u32,
|
||||
first: u32,
|
||||
count: u32,
|
||||
) {
|
||||
self.draw_sections_with(pass, indirect, base, first, count, |_, _| {});
|
||||
}
|
||||
/// [`Self::draw_sections`]; `page` binds what else a page's draws need.
|
||||
fn draw_sections_with(
|
||||
&self,
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
indirect: &wgpu::Buffer,
|
||||
base: u32,
|
||||
first: u32,
|
||||
count: u32,
|
||||
page_group: impl Fn(&mut wgpu::RenderPass<'_>, u32),
|
||||
) {
|
||||
let (mut i, end) = (first as usize, (first + count) as usize);
|
||||
while i < end {
|
||||
@@ -2731,6 +2765,7 @@ impl Renderer {
|
||||
.take_while(|&&p| p == (page, index_page))
|
||||
.count();
|
||||
pass.set_vertex_buffer(0, self.arena.buffer(page).slice(..));
|
||||
page_group(pass, page);
|
||||
let indices = index_page.map_or(&self.indices, |p| self.sorted_indices.buffer(p));
|
||||
pass.set_index_buffer(indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
if self.multi_draw {
|
||||
@@ -2744,6 +2779,88 @@ impl Renderer {
|
||||
i += run;
|
||||
}
|
||||
}
|
||||
/// Horizon's terrain, solid or `translucent`, with a Java pack's
|
||||
/// geometry pipelines (`java_terrain::DrawTerrain`): the frame's flat
|
||||
/// draws or its section draws, each page with its group 3.
|
||||
fn draw_java_terrain(
|
||||
&self,
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
translucent: bool,
|
||||
pipelines: &java_terrain::TerrainPipelines,
|
||||
flat_frame: bool,
|
||||
draws: [u32; 2],
|
||||
) {
|
||||
let page = |pass: &mut wgpu::RenderPass<'_>, page: u32| {
|
||||
if let Some(Some((_, group))) = self.java_pages.get(page as usize) {
|
||||
pass.set_bind_group(3, group, &[]);
|
||||
}
|
||||
};
|
||||
pass.set_bind_group(2, pipelines.group, &[]);
|
||||
match (self.flat.as_ref().filter(|_| flat_frame), pipelines.flat) {
|
||||
(Some(flat), Some(pipeline)) => {
|
||||
pass.set_pipeline(pipeline);
|
||||
let calls = if translucent {
|
||||
&flat.calls[flat.solid_calls..]
|
||||
} else {
|
||||
&flat.calls[..flat.solid_calls]
|
||||
};
|
||||
flat.draw(pass, calls, |p| self.arena.buffer(p), page);
|
||||
}
|
||||
(Some(_), None) => {}
|
||||
(None, _) => {
|
||||
pass.set_pipeline(pipelines.instanced);
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.set_vertex_buffer(1, self.instances.slice(..));
|
||||
let [solid, translucent_draws] = draws;
|
||||
let (first, count) = if translucent {
|
||||
(solid, translucent_draws)
|
||||
} else {
|
||||
(0, solid)
|
||||
};
|
||||
self.draw_sections_with(pass, &self.indirect, 0, first, count, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Each arena page's group for a Java pack's terrain programs: its
|
||||
/// vertices and the glue's table.
|
||||
fn prepare_java_pages(&mut self) {
|
||||
let Some(table) = &self.java_table else {
|
||||
return;
|
||||
};
|
||||
let count = self.arena.page_count() as usize;
|
||||
self.java_pages.resize_with(count, || None);
|
||||
let limit = self.device.limits().max_storage_buffer_binding_size;
|
||||
for page in 0..count {
|
||||
if !self.arena.has_page(page as u32) {
|
||||
self.java_pages[page] = None;
|
||||
continue;
|
||||
}
|
||||
let buffer = self.arena.buffer(page as u32);
|
||||
if matches!(&self.java_pages[page], Some((b, _)) if b == buffer) {
|
||||
continue;
|
||||
}
|
||||
let size = buffer.size().min(limit / 4 * 4);
|
||||
let group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Java shader pack terrain page"),
|
||||
layout: &self.java_page_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||
buffer,
|
||||
offset: 0,
|
||||
size: std::num::NonZeroU64::new(size),
|
||||
}),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: table.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
});
|
||||
self.java_pages[page] = Some((buffer.clone(), group));
|
||||
}
|
||||
}
|
||||
fn release_section(&mut self, key: SectionKey) {
|
||||
if let Some(s) = self.sections.remove(key) {
|
||||
self.release_mesh(s);
|
||||
@@ -2827,9 +2944,29 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
/// Run `pack`'s stages over the world from now on (`None`: no pack, the
|
||||
/// vanilla frame). On error the previous pack stays.
|
||||
pub fn set_shader_pack(&mut self, pack: Option<&crate::shader_pack::ShaderPack>) -> Result<()> {
|
||||
/// vanilla frame). On error the previous pack stays. `sprite_layers`
|
||||
/// names the block sprites, for a Java pack's `block.properties`.
|
||||
pub fn set_shader_pack(
|
||||
&mut self,
|
||||
pack: Option<&crate::shader_pack::ShaderPack>,
|
||||
sprite_layers: &std::collections::BTreeMap<String, u32>,
|
||||
) -> Result<()> {
|
||||
let format = self.config.format.remove_srgb_suffix();
|
||||
let layouts = java_stages::GeometryLayouts {
|
||||
flat: self.flat.as_ref().map(|f| f.draw_layout()),
|
||||
page: &self.java_page_layout,
|
||||
};
|
||||
let table = pack.and_then(|p| p.java.as_ref()).map(|java| {
|
||||
let table = java_terrain::table(sprite_layers, &java.block_ids);
|
||||
wgpu::util::DeviceExt::create_buffer_init(
|
||||
&self.device,
|
||||
&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Java shader pack terrain table"),
|
||||
contents: bytemuck::cast_slice(&table),
|
||||
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
|
||||
},
|
||||
)
|
||||
});
|
||||
self.pack = match pack {
|
||||
Some(pack) => Some(match &pack.java {
|
||||
Some(java) => Pack::Java(java_stages::JavaStages::new(
|
||||
@@ -2838,6 +2975,8 @@ impl Renderer {
|
||||
format,
|
||||
&pack.id,
|
||||
java,
|
||||
&layouts,
|
||||
self.java_norm16,
|
||||
)?),
|
||||
None => Pack::Horizon(pack_stages::PackStages::new(
|
||||
&self.device,
|
||||
@@ -2848,6 +2987,8 @@ impl Renderer {
|
||||
}),
|
||||
None => None,
|
||||
};
|
||||
self.java_table = table;
|
||||
self.java_pages.clear();
|
||||
Ok(())
|
||||
}
|
||||
/// The folder name of the pack `set_shader_pack` gave.
|
||||
@@ -2988,6 +3129,15 @@ impl Renderer {
|
||||
}
|
||||
fn animate(&mut self, encoder: &mut wgpu::CommandEncoder, tick: u64) {
|
||||
self.animations.animate(encoder, &mut self.uploads, tick);
|
||||
// A Java pack's atlas copies what changed, every level.
|
||||
let levels = self.mip_levels;
|
||||
self.animated.clear();
|
||||
self.animated.extend(
|
||||
self.animations
|
||||
.changed
|
||||
.iter()
|
||||
.flat_map(|&layer| (0..levels).map(move |level| (layer, level))),
|
||||
);
|
||||
}
|
||||
pub fn render(&mut self, frame: &Frame) -> Result<bool> {
|
||||
let rendered = self.draw_frame(frame);
|
||||
@@ -3272,6 +3422,12 @@ impl Renderer {
|
||||
let eye = (frame.bob * crate::camera::view_matrix(frame.yaw, frame.pitch))
|
||||
.inverse()
|
||||
.transform_point3(Vec3::ZERO);
|
||||
// A Java pack that draws the terrain (`gfx/java_terrain.rs`): the
|
||||
// world pass leaves it out, and occlusion culling (which tests
|
||||
// against its depth) is off.
|
||||
let java_terrain = !frame.title
|
||||
&& self.java_table.is_some()
|
||||
&& matches!(&self.pack, Some(Pack::Java(j)) if j.draws_terrain(frame.shader.dimension));
|
||||
// Occlusion culling leaves the sections its last result did not see
|
||||
// to phase 2, after the others, with instance counts of 0. Whether
|
||||
// the frame has draws enough goes by the last one's.
|
||||
@@ -3279,7 +3435,11 @@ impl Renderer {
|
||||
.cull
|
||||
.as_mut()
|
||||
.filter(|c| {
|
||||
self.cull_on && !frame.title && self.multi_draw && self.section_draws >= c.min_draws
|
||||
self.cull_on
|
||||
&& !frame.title
|
||||
&& self.multi_draw
|
||||
&& self.section_draws >= c.min_draws
|
||||
&& !java_terrain
|
||||
})
|
||||
.and_then(|cull| cull.begin(&self.device).then_some(cull));
|
||||
self.phase2_draws.clear();
|
||||
@@ -3729,6 +3889,14 @@ impl Renderer {
|
||||
bytemuck::cast_slice(&self.draws),
|
||||
);
|
||||
}
|
||||
if java_terrain {
|
||||
self.prepare_java_pages();
|
||||
if let Some(table) = &self.java_table {
|
||||
let nether = i32::from(frame.shader.dimension == -1);
|
||||
self.queue
|
||||
.write_buffer(table, 4, bytemuck::bytes_of(&nether));
|
||||
}
|
||||
}
|
||||
let view_target = target_texture.create_view(&wgpu::TextureViewDescriptor {
|
||||
format: Some(self.config.format.remove_srgb_suffix()),
|
||||
..Default::default()
|
||||
@@ -3890,6 +4058,7 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
match self.flat.as_ref().filter(|_| flat_frame) {
|
||||
_ if java_terrain => {}
|
||||
Some(flat) => {
|
||||
pass.set_pipeline(&flat.solid);
|
||||
let vertices = |p| self.arena.buffer(p);
|
||||
@@ -3897,7 +4066,12 @@ impl Renderer {
|
||||
// As below: phase 1, the test, then phase 2's draws
|
||||
// that the test saw, copied after it.
|
||||
Some(cull) => {
|
||||
flat.draw(&mut pass, &flat.calls[..flat.phase1_calls], vertices);
|
||||
flat.draw(
|
||||
&mut pass,
|
||||
&flat.calls[..flat.phase1_calls],
|
||||
vertices,
|
||||
|_, _| {},
|
||||
);
|
||||
cull.test(&mut pass);
|
||||
drop(pass);
|
||||
cull.test_pass(&mut encoder);
|
||||
@@ -3912,7 +4086,12 @@ impl Renderer {
|
||||
pass.set_pipeline(&flat.solid);
|
||||
flat.draw_phase2(&mut pass, vertices);
|
||||
}
|
||||
None => flat.draw(&mut pass, &flat.calls[..flat.solid_calls], vertices),
|
||||
None => flat.draw(
|
||||
&mut pass,
|
||||
&flat.calls[..flat.solid_calls],
|
||||
vertices,
|
||||
|_, _| {},
|
||||
),
|
||||
}
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
@@ -4041,11 +4220,15 @@ impl Renderer {
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
}
|
||||
match self.flat.as_ref().filter(|_| flat_frame) {
|
||||
_ if java_terrain => {}
|
||||
Some(flat) => {
|
||||
pass.set_pipeline(&flat.translucent);
|
||||
flat.draw(&mut pass, &flat.calls[flat.solid_calls..], |p| {
|
||||
self.arena.buffer(p)
|
||||
});
|
||||
flat.draw(
|
||||
&mut pass,
|
||||
&flat.calls[flat.solid_calls..],
|
||||
|p| self.arena.buffer(p),
|
||||
|_, _| {},
|
||||
);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
}
|
||||
@@ -4120,10 +4303,26 @@ impl Renderer {
|
||||
frame.shader.rain,
|
||||
);
|
||||
}
|
||||
if let (true, Some((color, depth)), Some(size), Some(stages)) =
|
||||
(pack, &scaled, self.world_post.size(), self.pack.as_mut())
|
||||
if pack
|
||||
&& let (Some((color, depth)), Some(size)) = (&scaled, self.world_post.size())
|
||||
&& let Some(mut stages) = self.pack.take()
|
||||
{
|
||||
let output = match stages {
|
||||
// The pack's terrain programs draw with the frame's draws.
|
||||
let draws = [solid_draws, translucent_draws];
|
||||
let draw = |render: &mut wgpu::RenderPass<'_>,
|
||||
translucent: bool,
|
||||
pipelines: &java_terrain::TerrainPipelines| {
|
||||
self.draw_java_terrain(render, translucent, pipelines, flat_frame, draws);
|
||||
};
|
||||
let world = java_terrain::JavaWorld {
|
||||
sprites: &self.sprite_textures,
|
||||
split: sprite_split(),
|
||||
levels: self.mipmaps,
|
||||
animated: &self.animated,
|
||||
lightmap: &self.lightmap_view,
|
||||
draw: &draw,
|
||||
};
|
||||
let output = match &mut stages {
|
||||
Pack::Horizon(stages) => stages.run(
|
||||
&self.device,
|
||||
&self.queue,
|
||||
@@ -4149,10 +4348,12 @@ impl Renderer {
|
||||
sun_angle: frame.sky.sun_angle,
|
||||
moon_angle: frame.sky.moon_angle,
|
||||
},
|
||||
java_terrain.then_some(&world),
|
||||
),
|
||||
};
|
||||
self.world_post
|
||||
.finish_with(&mut encoder, &view_target, output);
|
||||
self.pack = Some(stages);
|
||||
} else if scaled.is_some() {
|
||||
self.world_post.finish(&mut encoder, &view_target);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@ pub(super) struct SpriteAnimator {
|
||||
pipeline: wgpu::ComputePipeline,
|
||||
/// Edge of the arrays' layers.
|
||||
size: u32,
|
||||
/// The sprite layers (across the arrays) the last `animate` rewrote, at
|
||||
/// every mip level: a Java pack's atlas copies them.
|
||||
pub changed: Vec<u32>,
|
||||
}
|
||||
|
||||
impl SpriteAnimator {
|
||||
@@ -205,6 +208,7 @@ impl SpriteAnimator {
|
||||
jobs,
|
||||
pipeline,
|
||||
size,
|
||||
changed: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +220,7 @@ impl SpriteAnimator {
|
||||
uploads: &mut Uploads,
|
||||
tick: u64,
|
||||
) {
|
||||
self.changed.clear();
|
||||
if self.collect(tick) == 0 {
|
||||
return;
|
||||
}
|
||||
@@ -251,6 +256,7 @@ impl SpriteAnimator {
|
||||
continue;
|
||||
}
|
||||
sprite.shown = Some(blend);
|
||||
self.changed.push(sprite.animation.layer);
|
||||
let job = [
|
||||
sprite.layer,
|
||||
sprite.frames[blend.frame],
|
||||
|
||||
@@ -65,8 +65,11 @@ pub struct Arena {
|
||||
}
|
||||
|
||||
impl Arena {
|
||||
/// Vertices; also storage, which a Java shader pack's terrain programs
|
||||
/// read a quad's corners from (`gfx/java_terrain.rs`).
|
||||
pub fn new(device: &wgpu::Device, stride: u64, capacity: u64) -> Self {
|
||||
Self::with_usage(device, stride, capacity, wgpu::BufferUsages::VERTEX)
|
||||
let usage = wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::STORAGE;
|
||||
Self::with_usage(device, stride, capacity, usage)
|
||||
}
|
||||
/// Sorted translucent indices; `storage` when compute passes read them.
|
||||
pub fn indices(device: &wgpu::Device, capacity: u64, storage: bool) -> Self {
|
||||
@@ -145,6 +148,10 @@ impl Arena {
|
||||
pub fn page_vertices(&self) -> u64 {
|
||||
self.page_limit
|
||||
}
|
||||
/// One past the last page index.
|
||||
pub fn page_count(&self) -> u32 {
|
||||
self.pages.len() as u32
|
||||
}
|
||||
pub fn has_page(&self, page: u32) -> bool {
|
||||
self.pages.get(page as usize).is_some_and(Option::is_some)
|
||||
}
|
||||
|
||||
@@ -640,17 +640,26 @@ impl Flat {
|
||||
pass.set_bind_group(0, group, &[]);
|
||||
pass.dispatch_workgroups((*count).min(GROUP_LIMIT), count.div_ceil(GROUP_LIMIT), 1);
|
||||
}
|
||||
/// The bind group layout of the draws' group 1 (block slots, slot
|
||||
/// origins).
|
||||
pub fn draw_layout(&self) -> &wgpu::BindGroupLayout {
|
||||
&self.draw_layout
|
||||
}
|
||||
|
||||
/// Draws `calls` with a flat pipeline set and `vertices` giving each
|
||||
/// page's vertex buffer.
|
||||
/// page's vertex buffer; `page` binds what else a page's draws need (a
|
||||
/// Java pack's terrain reads the page's vertices).
|
||||
pub fn draw<'a>(
|
||||
&self,
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
calls: &[Call],
|
||||
vertices: impl Fn(u32) -> &'a wgpu::Buffer,
|
||||
page: impl Fn(&mut wgpu::RenderPass<'_>, u32),
|
||||
) {
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
for call in calls {
|
||||
self.bind_page(pass, call.page, &vertices);
|
||||
page(pass, call.page);
|
||||
pass.draw_indexed(call.first..call.first + call.count, 0, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,645 @@
|
||||
//! Horizon's terrain drawn with a Java pack's `gbuffers_terrain` and
|
||||
//! `gbuffers_water` (docs/shader-packs.md). The pack's vertex program runs
|
||||
//! after a glue that rebuilds what Iris would feed it from Horizon's
|
||||
//! `TerrainVertex`: the camera-relative position, the quad's normal and
|
||||
//! tangent (from its four corners, read from the arena page as storage),
|
||||
//! the colour without vanilla's directional shade, the lightmap
|
||||
//! coordinates, the sprite's place in a block atlas laid out from Horizon's
|
||||
//! sprite arrays, and `mc_Entity` from the pack's `block.properties`, by
|
||||
//! sprite name.
|
||||
use horizon_shaderpack::translate::Geometry;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// `TerrainVertex` in 32-bit words.
|
||||
const VERTEX_WORDS: u32 = 6;
|
||||
|
||||
/// The glue's declarations and `hz_vertex`; `HZ_ORIGIN_DECL` and
|
||||
/// `HZ_ORIGIN` are the variant's (instanced or flat draws).
|
||||
const GLUE: &str = r#"
|
||||
layout(location = 0) in vec3 hz_position;
|
||||
layout(location = 1) in uvec2 hz_uv;
|
||||
layout(location = 2) in uint hz_layer;
|
||||
layout(location = 3) in vec4 hz_color;
|
||||
layout(location = 4) in uvec2 hz_light;
|
||||
HZ_ORIGIN_DECL
|
||||
layout(std430, set = 3, binding = 0) readonly buffer HzVertices { uint hz_vertices[]; };
|
||||
// [0]: atlas columns, [1]: 1 in the Nether, then each sprite layer's block id.
|
||||
layout(std430, set = 3, binding = 1) readonly buffer HzTable { int hz_table[]; };
|
||||
vec4 hz_pos;
|
||||
vec3 hz_normal;
|
||||
vec4 hz_tangent;
|
||||
vec4 hz_tint;
|
||||
vec2 hz_atlas;
|
||||
vec2 hz_mid;
|
||||
vec2 hz_lightmap;
|
||||
float hz_block;
|
||||
vec3 hz_mid_block;
|
||||
vec3 hz_corner(uint v) {
|
||||
uint b = v * HZ_WORDSu;
|
||||
return vec3(uintBitsToFloat(hz_vertices[b]), uintBitsToFloat(hz_vertices[b + 1u]),
|
||||
uintBitsToFloat(hz_vertices[b + 2u]));
|
||||
}
|
||||
vec2 hz_corner_uv(uint v) {
|
||||
uint w = hz_vertices[v * HZ_WORDSu + 3u];
|
||||
return vec2(float(w & 65535u), float(w >> 16u));
|
||||
}
|
||||
void hz_vertex() {
|
||||
vec4 origin = HZ_ORIGIN;
|
||||
hz_pos = vec4(hz_position + origin.xyz, 1.0);
|
||||
// The quad's corners: quads are four vertices from a multiple of four.
|
||||
uint q = uint(gl_VertexIndex) & 4294967292u;
|
||||
vec3 p0 = hz_corner(q);
|
||||
vec3 e1 = hz_corner(q + 1u) - p0;
|
||||
vec3 e2 = hz_corner(q + 2u) - p0;
|
||||
vec3 n = cross(e1, e2);
|
||||
if (dot(n, n) < 1e-12) {
|
||||
n = cross(e2, hz_corner(q + 3u) - p0);
|
||||
}
|
||||
hz_normal = dot(n, n) > 0.0 ? normalize(n) : vec3(0.0, 1.0, 0.0);
|
||||
vec2 t0 = hz_corner_uv(q);
|
||||
vec2 d1 = hz_corner_uv(q + 1u) - t0;
|
||||
vec2 d2 = hz_corner_uv(q + 2u) - t0;
|
||||
float s = d1.x * d2.y - d2.x * d1.y < 0.0 ? -1.0 : 1.0;
|
||||
vec3 t = (e1 * d2.y - e2 * d1.y) * s;
|
||||
vec3 b = (e2 * d1.x - e1 * d2.x) * s;
|
||||
t = t - hz_normal * dot(hz_normal, t);
|
||||
t = dot(t, t) > 0.0 ? normalize(t) : vec3(1.0, 0.0, 0.0);
|
||||
hz_tangent = vec4(t, dot(cross(hz_normal, t), b) < 0.0 ? -1.0 : 1.0);
|
||||
// Iris turns vanilla's directional shade off: undo it on axis faces.
|
||||
vec3 a = abs(hz_normal);
|
||||
bool nether = hz_table[1] != 0;
|
||||
float shade = 1.0;
|
||||
if (a.y > 0.999) {
|
||||
shade = nether ? 0.9 : (hz_normal.y > 0.0 ? 1.0 : 0.5);
|
||||
} else if (a.z > 0.999) {
|
||||
shade = 0.8;
|
||||
} else if (a.x > 0.999) {
|
||||
shade = 0.6;
|
||||
}
|
||||
hz_tint = vec4(min(hz_color.rgb / shade, vec3(1.0)), hz_color.a);
|
||||
// The sprite's cell in the atlas, columns of square cells.
|
||||
int layer = int(hz_layer & 16383u);
|
||||
int cols = max(hz_table[0], 1);
|
||||
vec2 cell = vec2(float(layer % cols), float(layer / cols));
|
||||
vec2 uv = clamp(vec2(hz_uv) * origin.w, 0.0, 1.0);
|
||||
hz_atlas = (cell + uv) / float(cols);
|
||||
hz_mid = (cell + 0.5) / float(cols);
|
||||
// Vanilla's UV2: block light, then sky light, 0..240.
|
||||
hz_lightmap = vec2(float(hz_light.y), float(hz_light.x));
|
||||
hz_block = float(hz_table[2 + layer]);
|
||||
hz_mid_block = (floor(hz_position - hz_normal * 0.01) + 0.5 - hz_position) * 64.0;
|
||||
}
|
||||
"#;
|
||||
|
||||
/// The glue of one draw path: instanced section draws (the origin is an
|
||||
/// instance attribute) or flat ones (`gfx/flat.rs`: from the block's slot).
|
||||
pub fn glue(flat: bool, alpha_test: Option<f32>) -> Geometry {
|
||||
let (decl, origin) = if flat {
|
||||
(
|
||||
"layout(std430, set = 1, binding = 0) readonly buffer HzBlockSlots { uint hz_block_slots[]; };\n\
|
||||
layout(std430, set = 1, binding = 1) readonly buffer HzSlotOrigins { vec4 hz_slot_origins[]; };",
|
||||
"hz_slot_origins[hz_block_slots[uint(gl_VertexIndex) >> 8u]]",
|
||||
)
|
||||
} else {
|
||||
("layout(location = 5) in vec4 hz_origin;", "hz_origin")
|
||||
};
|
||||
let vertex_glue = GLUE
|
||||
.replace("HZ_ORIGIN_DECL", decl)
|
||||
.replace("HZ_ORIGIN", origin)
|
||||
.replace("HZ_WORDS", &VERTEX_WORDS.to_string());
|
||||
let lightmap = "vec4(hz_lightmap, 0.0, 1.0)";
|
||||
let attributes: BTreeMap<String, String> = [
|
||||
("iris_Vertex", "hz_pos"),
|
||||
("vaPosition", "hz_pos"),
|
||||
("iris_Normal", "vec4(hz_normal, 0.0)"),
|
||||
("vaNormal", "vec4(hz_normal, 0.0)"),
|
||||
("iris_Color", "hz_tint"),
|
||||
("vaColor", "hz_tint"),
|
||||
("iris_MultiTexCoord0", "vec4(hz_atlas, 0.0, 1.0)"),
|
||||
("vaUV0", "vec4(hz_atlas, 0.0, 1.0)"),
|
||||
("iris_MultiTexCoord1", lightmap),
|
||||
("iris_MultiTexCoord2", lightmap),
|
||||
("vaUV2", lightmap),
|
||||
// OverlayTexture.NO_OVERLAY.
|
||||
("vaUV1", "vec4(0.0, 10.0, 0.0, 0.0)"),
|
||||
("mc_Entity", "vec4(hz_block, -1.0, 0.0, 0.0)"),
|
||||
("mc_midTexCoord", "vec4(hz_mid, 0.0, 1.0)"),
|
||||
("at_tangent", "hz_tangent"),
|
||||
("at_midBlock", "vec4(hz_mid_block, 0.0)"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_owned(), v.to_owned()))
|
||||
.collect();
|
||||
Geometry {
|
||||
vertex_glue,
|
||||
attributes,
|
||||
alpha_test,
|
||||
}
|
||||
}
|
||||
|
||||
/// The glue's table: atlas columns, the Nether flag, then each sprite
|
||||
/// layer's block id from `block.properties` (-1 for blocks it doesn't
|
||||
/// list, as Iris).
|
||||
pub fn table(sprite_layers: &BTreeMap<String, u32>, block_ids: &BTreeMap<String, i32>) -> Vec<i32> {
|
||||
let layers = sprite_layers.values().max().map_or(0, |&m| m as usize + 1);
|
||||
let mut out = vec![-1; layers + 2];
|
||||
out[0] = atlas_columns(layers as u32) as i32;
|
||||
out[1] = 0;
|
||||
for (name, &layer) in sprite_layers {
|
||||
if let Some(id) = horizon_shaderpack::properties::sprite_block_id(name, block_ids) {
|
||||
out[layer as usize + 2] = id;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The atlas's columns (and rows) of sprite cells for `layers` sprites.
|
||||
pub fn atlas_columns(layers: u32) -> u32 {
|
||||
(f64::from(layers.max(1)).sqrt().ceil() as u32).max(1)
|
||||
}
|
||||
|
||||
/// Whether `device` renders to and filters the 16-bit normalized formats
|
||||
/// a Java pack's `RGBA16` targets become (`java_stages::target_format`).
|
||||
pub fn norm16_targets(adapter: &wgpu::Adapter, device: &wgpu::Device) -> bool {
|
||||
use wgpu::TextureFormat as F;
|
||||
let wanted = wgpu::Features::TEXTURE_FORMAT_16BIT_NORM
|
||||
| wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES;
|
||||
device.features().contains(wanted)
|
||||
&& [
|
||||
F::R16Unorm,
|
||||
F::Rg16Unorm,
|
||||
F::Rgba16Unorm,
|
||||
F::R16Snorm,
|
||||
F::Rg16Snorm,
|
||||
F::Rgba16Snorm,
|
||||
]
|
||||
.into_iter()
|
||||
.all(|f| {
|
||||
let features = adapter.get_texture_format_features(f);
|
||||
features
|
||||
.allowed_usages
|
||||
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
|
||||
&& features
|
||||
.flags
|
||||
.contains(wgpu::TextureFormatFeatureFlags::FILTERABLE)
|
||||
})
|
||||
}
|
||||
|
||||
/// The bind group layout of a page group: the page's vertices and the glue's
|
||||
/// table, read by the vertex stage.
|
||||
pub fn page_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
|
||||
let storage = |binding| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
};
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Java shader pack terrain page"),
|
||||
entries: &[storage(0), storage(1)],
|
||||
})
|
||||
}
|
||||
|
||||
/// Draws Horizon's terrain with a pack's pipelines: the solid layer or the
|
||||
/// translucent one. Set by the renderer, which owns the sections.
|
||||
pub type DrawTerrain<'a> = dyn Fn(&mut wgpu::RenderPass<'_>, bool, &TerrainPipelines<'_>) + 'a;
|
||||
|
||||
/// What the renderer binds to draw with a pack's geometry program: the
|
||||
/// pipeline of its draw path and the pack's group (group 2). Group 1 is the
|
||||
/// flat path's, group 3 the page's ([`page_layout`]). `flat` is `None` when
|
||||
/// the renderer has no flat draws.
|
||||
pub struct TerrainPipelines<'a> {
|
||||
pub instanced: &'a wgpu::RenderPipeline,
|
||||
pub flat: Option<&'a wgpu::RenderPipeline>,
|
||||
pub group: &'a wgpu::BindGroup,
|
||||
}
|
||||
|
||||
/// What a Java pack's geometry reads from the renderer each frame.
|
||||
pub struct JavaWorld<'a> {
|
||||
/// The block sprite arrays, [`super::SPRITE_SPLIT`]-layer each.
|
||||
pub sprites: &'a [wgpu::Texture],
|
||||
pub split: u32,
|
||||
/// Mipmap levels the terrain samples.
|
||||
pub levels: u32,
|
||||
/// The sprite layers and mip levels animation rewrote this frame.
|
||||
pub animated: &'a [(u32, u32)],
|
||||
pub lightmap: &'a wgpu::TextureView,
|
||||
pub draw: &'a DrawTerrain<'a>,
|
||||
}
|
||||
|
||||
const ATLAS: &str = r#"
|
||||
struct Atlas { cols: u32, layers: u32, split: u32, pad: u32 }
|
||||
@group(0) @binding(0) var<uniform> atlas: Atlas;
|
||||
@group(0) @binding(1) var s0: texture_2d_array<f32>;
|
||||
@group(0) @binding(2) var s1: texture_2d_array<f32>;
|
||||
@group(0) @binding(3) var s2: texture_2d_array<f32>;
|
||||
@group(0) @binding(4) var s3: texture_2d_array<f32>;
|
||||
@group(0) @binding(5) var s4: texture_2d_array<f32>;
|
||||
@group(0) @binding(6) var s5: texture_2d_array<f32>;
|
||||
@group(0) @binding(7) var s6: texture_2d_array<f32>;
|
||||
@group(0) @binding(8) var s7: texture_2d_array<f32>;
|
||||
|
||||
struct Out {
|
||||
@builtin(position) p: vec4f,
|
||||
@location(0) @interpolate(flat) mip: u32,
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs(@builtin(vertex_index) i: u32, @builtin(instance_index) mip: u32) -> Out {
|
||||
let uv = vec2f(f32((i << 1u) & 2u), f32(i & 2u));
|
||||
return Out(vec4f(uv * 2.0 - 1.0, 0.0, 1.0), mip);
|
||||
}
|
||||
|
||||
// Each sprite layer into its cell, at mip level `mip`.
|
||||
@fragment
|
||||
fn fs(in: Out) -> @location(0) vec4f {
|
||||
let cell = max(textureDimensions(s0, in.mip).x, 1u);
|
||||
let px = vec2u(in.p.xy);
|
||||
let c = px / cell;
|
||||
let layer = c.y * atlas.cols + c.x;
|
||||
if c.x >= atlas.cols || layer >= atlas.layers {
|
||||
return vec4f(0.0);
|
||||
}
|
||||
let at = px % cell;
|
||||
let l = i32(layer % atlas.split);
|
||||
let m = i32(in.mip);
|
||||
switch layer / atlas.split {
|
||||
case 0u: { return textureLoad(s0, at, l, m); }
|
||||
case 1u: { return textureLoad(s1, at, l, m); }
|
||||
case 2u: { return textureLoad(s2, at, l, m); }
|
||||
case 3u: { return textureLoad(s3, at, l, m); }
|
||||
case 4u: { return textureLoad(s4, at, l, m); }
|
||||
case 5u: { return textureLoad(s5, at, l, m); }
|
||||
case 6u: { return textureLoad(s6, at, l, m); }
|
||||
default: { return textureLoad(s7, at, l, m); }
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
/// Horizon's block sprites as one 2D atlas, as Iris binds `gtexture`:
|
||||
/// square cells, [`atlas_columns`] to a row. Drawn from the sprite arrays
|
||||
/// once, then animated sprites are copied in as they change.
|
||||
pub struct Atlas {
|
||||
texture: wgpu::Texture,
|
||||
pub view: wgpu::TextureView,
|
||||
mips: Vec<wgpu::TextureView>,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
layout: wgpu::BindGroupLayout,
|
||||
uniform: wgpu::Buffer,
|
||||
/// Layers, sprite size, mip levels it was made for.
|
||||
key: (u32, u32, u32),
|
||||
/// The first sprite array it was drawn from (resource reloads make
|
||||
/// new ones).
|
||||
source: wgpu::Texture,
|
||||
drawn: bool,
|
||||
}
|
||||
|
||||
impl Atlas {
|
||||
/// The atlas for `world`'s sprites; `None` without any.
|
||||
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, world: &JavaWorld) -> Option<Self> {
|
||||
let key = Self::key(world)?;
|
||||
let (layers, size, levels) = key;
|
||||
let cols = atlas_columns(layers);
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("Java shader pack block atlas"),
|
||||
size: wgpu::Extent3d {
|
||||
width: cols * size,
|
||||
height: cols * size,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: levels,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
});
|
||||
let mips = (0..levels)
|
||||
.map(|level| {
|
||||
texture.create_view(&wgpu::TextureViewDescriptor {
|
||||
base_mip_level: level,
|
||||
mip_level_count: Some(1),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut entries = vec![wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}];
|
||||
entries.extend((1..=8).map(|binding| wgpu::BindGroupLayoutEntry {
|
||||
binding,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
||||
view_dimension: wgpu::TextureViewDimension::D2Array,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
}));
|
||||
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Java shader pack block atlas"),
|
||||
entries: &entries,
|
||||
});
|
||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("Java shader pack block atlas"),
|
||||
source: wgpu::ShaderSource::Wgsl(ATLAS.into()),
|
||||
});
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Java shader pack block atlas"),
|
||||
layout: Some(
|
||||
&device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("Java shader pack block atlas"),
|
||||
bind_group_layouts: &[Some(&layout)],
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &module,
|
||||
entry_point: Some("vs"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some("fs"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::TextureFormat::Rgba8Unorm.into())],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("Java shader pack block atlas"),
|
||||
size: 16,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
queue.write_buffer(
|
||||
&uniform,
|
||||
0,
|
||||
bytemuck::cast_slice(&[cols, layers, world.split.max(1), 0]),
|
||||
);
|
||||
Some(Self {
|
||||
view: texture.create_view(&Default::default()),
|
||||
texture,
|
||||
mips,
|
||||
pipeline,
|
||||
layout,
|
||||
uniform,
|
||||
key,
|
||||
source: world.sprites[0].clone(),
|
||||
drawn: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn key(world: &JavaWorld) -> Option<(u32, u32, u32)> {
|
||||
let first = world.sprites.first()?;
|
||||
let layers = world
|
||||
.sprites
|
||||
.iter()
|
||||
.map(|t| t.depth_or_array_layers())
|
||||
.sum();
|
||||
let levels = (world.levels + 1).min(first.mip_level_count()).max(1);
|
||||
Some((layers, first.width(), levels))
|
||||
}
|
||||
|
||||
/// Whether it was made for `world`'s sprites.
|
||||
pub fn fits(&self, world: &JavaWorld) -> bool {
|
||||
Self::key(world) == Some(self.key) && world.sprites.first() == Some(&self.source)
|
||||
}
|
||||
|
||||
/// Bring it up to date: drawn the first time, then the sprites
|
||||
/// animation rewrote copied into their cells.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
world: &JavaWorld,
|
||||
) {
|
||||
if !self.drawn {
|
||||
self.draw(device, encoder, world);
|
||||
self.drawn = true;
|
||||
return;
|
||||
}
|
||||
let (layers, size, levels) = self.key;
|
||||
let cols = atlas_columns(layers);
|
||||
let split = world.split.max(1);
|
||||
for &(layer, level) in world.animated {
|
||||
let Some(array) = world.sprites.get((layer / split) as usize) else {
|
||||
continue;
|
||||
};
|
||||
if level >= levels || layer >= layers {
|
||||
continue;
|
||||
}
|
||||
let edge = (size >> level).max(1);
|
||||
encoder.copy_texture_to_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: array,
|
||||
mip_level: level,
|
||||
origin: wgpu::Origin3d {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: layer % split,
|
||||
},
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &self.texture,
|
||||
mip_level: level,
|
||||
origin: wgpu::Origin3d {
|
||||
x: (layer % cols) * edge,
|
||||
y: (layer / cols) * edge,
|
||||
z: 0,
|
||||
},
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: edge,
|
||||
height: edge,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The atlas's size in pixels (`atlasSize`).
|
||||
pub fn size(&self) -> [u32; 2] {
|
||||
[self.texture.width(), self.texture.height()]
|
||||
}
|
||||
|
||||
/// Draw the sprites into the atlas, every mip level.
|
||||
fn draw(&self, device: &wgpu::Device, encoder: &mut wgpu::CommandEncoder, world: &JavaWorld) {
|
||||
let views: Vec<wgpu::TextureView> = (0..8)
|
||||
.map(|i| {
|
||||
let t = world.sprites.get(i).unwrap_or(&world.sprites[0]);
|
||||
t.create_view(&wgpu::TextureViewDescriptor {
|
||||
dimension: Some(wgpu::TextureViewDimension::D2Array),
|
||||
mip_level_count: Some(self.key.2),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut entries = vec![wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: self.uniform.as_entire_binding(),
|
||||
}];
|
||||
entries.extend(views.iter().enumerate().map(|(i, v)| wgpu::BindGroupEntry {
|
||||
binding: i as u32 + 1,
|
||||
resource: wgpu::BindingResource::TextureView(v),
|
||||
}));
|
||||
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Java shader pack block atlas"),
|
||||
layout: &self.layout,
|
||||
entries: &entries,
|
||||
});
|
||||
for (mip, view) in self.mips.iter().enumerate() {
|
||||
let attachments = [Some(wgpu::RenderPassColorAttachment {
|
||||
view,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})];
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Java shader pack block atlas"),
|
||||
color_attachments: &attachments,
|
||||
..Default::default()
|
||||
});
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &group, &[]);
|
||||
pass.draw(0..3, mip as u32..mip as u32 + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// `module` validates and translates for Metal, Vulkan and DirectX 12.
|
||||
fn backends(module: &naga::Module, label: &str) {
|
||||
let info = naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
)
|
||||
.validate(module)
|
||||
.unwrap_or_else(|e| panic!("{label}: {e:?}"));
|
||||
naga::back::msl::write_string(
|
||||
module,
|
||||
&info,
|
||||
// wgpu's Metal backend asks for MSL 2.x.
|
||||
&naga::back::msl::Options {
|
||||
lang_version: (2, 4),
|
||||
..Default::default()
|
||||
},
|
||||
&naga::back::msl::PipelineOptions::default(),
|
||||
)
|
||||
.unwrap_or_else(|e| panic!("{label} (MSL): {e}"));
|
||||
naga::back::spv::write_vec(module, &info, &Default::default(), None)
|
||||
.unwrap_or_else(|e| panic!("{label} (SPIR-V): {e}"));
|
||||
for entry in &module.entry_points {
|
||||
let mut hlsl = String::new();
|
||||
naga::back::hlsl::Writer::new(
|
||||
&mut hlsl,
|
||||
&naga::back::hlsl::Options::default(),
|
||||
&naga::back::hlsl::PipelineOptions {
|
||||
entry_point: Some((entry.stage, entry.name.clone())),
|
||||
},
|
||||
)
|
||||
.write(module, &info, None)
|
||||
.unwrap_or_else(|e| panic!("{label} {} (HLSL): {e}", entry.name));
|
||||
}
|
||||
}
|
||||
|
||||
/// A terrain program reading every attribute the glue fills, run after
|
||||
/// each draw path's glue, and the atlas shader, on every backend.
|
||||
#[test]
|
||||
fn the_glue_feeds_a_terrain_program_on_every_backend() {
|
||||
let program = "#version 120
|
||||
attribute vec4 mc_Entity;
|
||||
attribute vec4 mc_midTexCoord;
|
||||
attribute vec4 at_tangent;
|
||||
attribute vec3 at_midBlock;
|
||||
varying vec4 color;
|
||||
varying vec2 uv;
|
||||
varying vec2 lm;
|
||||
varying vec3 normal;
|
||||
varying float id;
|
||||
void main() {
|
||||
vec4 p = gl_ModelViewMatrix * gl_Vertex;
|
||||
p.xyz += at_midBlock / 64.0 * 0.0 + at_tangent.xyz * 0.0;
|
||||
gl_Position = gl_ProjectionMatrix * p;
|
||||
color = gl_Color;
|
||||
uv = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy + mc_midTexCoord.xy * 0.0;
|
||||
lm = (gl_TextureMatrix[1] * gl_MultiTexCoord1).xy;
|
||||
normal = gl_NormalMatrix * gl_Normal;
|
||||
id = mc_Entity.x;
|
||||
}
|
||||
";
|
||||
for flat in [false, true] {
|
||||
let mut link = horizon_shaderpack::translate::Link {
|
||||
set: 2,
|
||||
geometry: Some(glue(flat, Some(0.1))),
|
||||
..Default::default()
|
||||
};
|
||||
let t = horizon_shaderpack::translate::translate(
|
||||
horizon_shaderpack::Stage::Vertex,
|
||||
program,
|
||||
&mut link,
|
||||
&|l| format!("line {l}"),
|
||||
)
|
||||
.unwrap_or_else(|e| panic!("flat {flat}: {e}"));
|
||||
for wanted in ["hz_vertex();", "mc_Entity = vec4(vec4(hz_block"] {
|
||||
assert!(t.glsl.contains(wanted), "{wanted}\n{}", t.glsl);
|
||||
}
|
||||
// The glue's inputs are Horizon's vertex layout.
|
||||
let inputs = t.module.entry_points[0].function.arguments.len();
|
||||
assert_eq!(inputs, if flat { 6 } else { 7 }, "{}", t.wgsl);
|
||||
let reparsed = naga::front::wgsl::parse_str(&t.wgsl).unwrap();
|
||||
backends(&reparsed, &format!("terrain glue (flat {flat})"));
|
||||
}
|
||||
backends(&naga::front::wgsl::parse_str(ATLAS).unwrap(), "atlas");
|
||||
backends(
|
||||
&naga::front::wgsl::parse_str(super::super::java_stages::BLIT).unwrap(),
|
||||
"Java shader pack blits",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sprites_take_their_blocks_ids() {
|
||||
let layers: BTreeMap<String, u32> = [
|
||||
("minecraft:block/oak_leaves", 0),
|
||||
("minecraft:block/water_still", 1),
|
||||
("minecraft:block/stone", 2),
|
||||
("minecraft:block/oak_log_top", 3),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(n, l)| (n.to_owned(), l))
|
||||
.collect();
|
||||
let ids: BTreeMap<String, i32> = [("oak_leaves", 10003), ("water", 8), ("oak_log", 7)]
|
||||
.into_iter()
|
||||
.map(|(n, i)| (n.to_owned(), i))
|
||||
.collect();
|
||||
assert_eq!(table(&layers, &ids), [2, 0, 10003, 8, -1, 7]);
|
||||
assert_eq!(atlas_columns(5), 3);
|
||||
assert_eq!(atlas_columns(0), 1);
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,14 @@
|
||||
//! a folder holding `shaders/`, or a `.zip`. `horizon-shaderpack` translates
|
||||
//! their full-screen programs (`begin`, `prepare`, `deferred`, `composite`,
|
||||
//! `final`) to WGSL, and `gfx/java_stages.rs` runs them over Horizon's world
|
||||
//! as Iris runs them over its own. Geometry programs (`gbuffers_*`,
|
||||
//! `shadow`) don't run yet: the world is drawn by Horizon.
|
||||
//! as Iris runs them over its own. Of the geometry programs, the terrain's
|
||||
//! (`gbuffers_terrain`, `gbuffers_water` and their fallbacks) draw Horizon's
|
||||
//! terrain (`gfx/java_terrain.rs`); the rest of the world is Horizon's.
|
||||
use horizon_shaderpack::{
|
||||
JavaPack,
|
||||
pipeline::{Directives, Program, TargetSettings},
|
||||
pipeline::{Directives, GeometryProgram, Pass, Program, TargetSettings},
|
||||
preprocess::Environment,
|
||||
properties::SkyDirectives,
|
||||
properties::{Blend, SkyDirectives},
|
||||
uniforms::CustomUniforms,
|
||||
};
|
||||
use std::{
|
||||
@@ -36,6 +37,17 @@ pub struct JavaFolder {
|
||||
/// `sunPathRotation`, `eyeBrightnessHalflife`, `wetnessHalflife` and
|
||||
/// `drynessHalflife`.
|
||||
pub directives: Directives,
|
||||
/// The programs Horizon's solid and translucent terrain are drawn with,
|
||||
/// each translated for instanced and flat draws (`variants[0]`).
|
||||
pub terrain: Option<GeometryProgram>,
|
||||
pub water: Option<GeometryProgram>,
|
||||
/// How many of `passes` run before translucent terrain (`begin*`,
|
||||
/// `prepare*`, `deferred*`).
|
||||
pub translucent_at: usize,
|
||||
pub flipped_after_translucent: BTreeSet<u32>,
|
||||
/// Each draw buffer's blending in `terrain` and `water`.
|
||||
pub terrain_blends: Vec<Option<wgpu::BlendState>>,
|
||||
pub water_blends: Vec<Option<wgpu::BlendState>>,
|
||||
}
|
||||
|
||||
/// The key of `texture.noise` (`noisetex`) in [`JavaPasses::custom_textures`].
|
||||
@@ -60,6 +72,8 @@ pub struct JavaPasses {
|
||||
pub uniforms: CustomUniforms,
|
||||
/// What the pack keeps of the vanilla sky (`clouds`, `sun`, `moon`).
|
||||
pub sky: SkyDirectives,
|
||||
/// `block.properties`: block name → `mc_Entity` id.
|
||||
pub block_ids: BTreeMap<String, i32>,
|
||||
/// `texture.<stage>.<sampler>` and `texture.noise` images, by
|
||||
/// `"<stage>.<sampler>"` and [`NOISE_KEY`].
|
||||
pub custom_textures: BTreeMap<String, crate::shader_pack::Rgba>,
|
||||
@@ -144,6 +158,45 @@ pub fn load(
|
||||
if passes.is_empty() && final_pass.is_none() {
|
||||
continue;
|
||||
}
|
||||
// A geometry program that doesn't translate leaves that terrain to
|
||||
// Horizon, with a warning.
|
||||
let geometry = |pass: &Option<horizon_shaderpack::pipeline::Pass>, alpha_test| {
|
||||
let pass = pass.as_ref()?;
|
||||
let glues = [false, true].map(|flat| crate::gfx::java_terrain::glue(flat, alpha_test));
|
||||
match pack.compile_geometry(pass, &plan.targets, &values, &env, &properties, &glues) {
|
||||
Ok(program) => Some(program),
|
||||
Err(e) => {
|
||||
log::warn!("Horizon: shader pack {}: {e}", pass.program);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
// Iris's alpha test of cutout terrain (`AlphaTests.ONE_TENTH_ALPHA`).
|
||||
let terrain = geometry(&plan.terrain, Some(0.1));
|
||||
let water = terrain.as_ref().and_then(|_| geometry(&plan.water, None));
|
||||
let translucent_at = early.clone().count() + plan.deferred.len();
|
||||
let blends = |g: &Option<GeometryProgram>, default| {
|
||||
g.as_ref().map_or_else(Vec::new, |g| {
|
||||
pass_blends(&g.program.pass, &g.program.draw_buffers, default)
|
||||
})
|
||||
};
|
||||
let terrain_blends = blends(&terrain, None);
|
||||
// Vanilla's translucent blending (`BlendFunction.TRANSLUCENT`).
|
||||
let water_blends = blends(
|
||||
&water,
|
||||
Some(wgpu::BlendState {
|
||||
color: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::SrcAlpha,
|
||||
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
alpha: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::One,
|
||||
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
}),
|
||||
);
|
||||
folders.push(JavaFolder {
|
||||
folder: name.to_owned(),
|
||||
targets: plan.targets,
|
||||
@@ -154,6 +207,12 @@ pub fn load(
|
||||
final_pass,
|
||||
flipped_at_end: plan.flipped_at_end,
|
||||
directives: plan.directives,
|
||||
terrain,
|
||||
water,
|
||||
translucent_at,
|
||||
flipped_after_translucent: plan.flipped_after_translucent,
|
||||
terrain_blends,
|
||||
water_blends,
|
||||
});
|
||||
}
|
||||
if folders.is_empty() {
|
||||
@@ -184,11 +243,69 @@ pub fn load(
|
||||
for problem in &uniforms.problems {
|
||||
log::warn!("Horizon: shader pack custom uniform {problem}");
|
||||
}
|
||||
let block_ids = pack.block_ids(&values, &env).unwrap_or_else(|e| {
|
||||
log::warn!("Horizon: shader pack {e}");
|
||||
BTreeMap::new()
|
||||
});
|
||||
Ok(JavaPasses {
|
||||
folders,
|
||||
uniforms,
|
||||
sky: properties.sky(),
|
||||
custom_textures,
|
||||
block_ids,
|
||||
})
|
||||
}
|
||||
|
||||
/// Each draw buffer's blending, as Iris's `BlendModeOverride`:
|
||||
/// `blend.<program>.<buffer>`, else `blend.<program>`, else `default`
|
||||
/// (none for full-screen passes, vanilla's for the geometry programs).
|
||||
pub(crate) fn pass_blends(
|
||||
pass: &Pass,
|
||||
draw_buffers: &[u32],
|
||||
default: Option<wgpu::BlendState>,
|
||||
) -> Vec<Option<wgpu::BlendState>> {
|
||||
let all = pass.blend.as_ref().map_or(default, blend_state);
|
||||
draw_buffers
|
||||
.iter()
|
||||
.map(|i| {
|
||||
pass.buffer_blends
|
||||
.iter()
|
||||
.find(|(b, _)| b == i)
|
||||
.map_or(all, |(_, b)| blend_state(b))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A `blend.*` value as wgpu's blending (`off`: none). The factors are the
|
||||
/// GL ones `ShaderProperties` accepts.
|
||||
pub(crate) fn blend_state(blend: &Blend) -> Option<wgpu::BlendState> {
|
||||
let Blend::Factors(factors) = blend else {
|
||||
return None;
|
||||
};
|
||||
let factor = |name: &str| {
|
||||
use wgpu::BlendFactor as F;
|
||||
match name {
|
||||
"ZERO" => F::Zero,
|
||||
"SRC_COLOR" => F::Src,
|
||||
"ONE_MINUS_SRC_COLOR" => F::OneMinusSrc,
|
||||
"DST_COLOR" => F::Dst,
|
||||
"ONE_MINUS_DST_COLOR" => F::OneMinusDst,
|
||||
"SRC_ALPHA" => F::SrcAlpha,
|
||||
"ONE_MINUS_SRC_ALPHA" => F::OneMinusSrcAlpha,
|
||||
"DST_ALPHA" => F::DstAlpha,
|
||||
"ONE_MINUS_DST_ALPHA" => F::OneMinusDstAlpha,
|
||||
"SRC_ALPHA_SATURATE" => F::SrcAlphaSaturated,
|
||||
_ => F::One,
|
||||
}
|
||||
};
|
||||
let component = |src: &str, dst: &str| wgpu::BlendComponent {
|
||||
src_factor: factor(src),
|
||||
dst_factor: factor(dst),
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
};
|
||||
Some(wgpu::BlendState {
|
||||
color: component(&factors[0], &factors[1]),
|
||||
alpha: component(&factors[2], &factors[3]),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -242,8 +359,18 @@ mod tests {
|
||||
.folders
|
||||
.iter()
|
||||
.flat_map(|f| f.passes.iter().chain(&f.final_pass));
|
||||
let geometry = pack
|
||||
.folders
|
||||
.iter()
|
||||
.flat_map(|f| f.terrain.iter().chain(&f.water));
|
||||
passes
|
||||
.flat_map(|p| [(p, &p.vertex, "vsh"), (p, &p.fragment, "fsh")])
|
||||
.chain(geometry.flat_map(|g| {
|
||||
let p = &g.program;
|
||||
[(p, &p.vertex, "vsh"), (p, &p.fragment, "fsh")]
|
||||
.into_iter()
|
||||
.chain(g.variants.iter().map(move |v| (p, v, "vsh (flat)")))
|
||||
}))
|
||||
.find_map(|(p, t, ext)| {
|
||||
let e = naga::front::wgsl::parse_str(&t.wgsl).err()?;
|
||||
Some(format!(
|
||||
@@ -283,7 +410,13 @@ mod tests {
|
||||
.iter()
|
||||
.map(|f| {
|
||||
let n = f.passes.len() + usize::from(f.final_pass.is_some());
|
||||
format!("{} {n} passes", f.folder)
|
||||
let geometry: Vec<&str> = f
|
||||
.terrain
|
||||
.iter()
|
||||
.chain(&f.water)
|
||||
.map(|g| g.program.pass.program.as_str())
|
||||
.collect();
|
||||
format!("{} {n} passes, terrain {geometry:?}", f.folder)
|
||||
})
|
||||
.collect();
|
||||
println!("{name}: loads ({})", folders.join(", "));
|
||||
|
||||
@@ -432,6 +432,13 @@ pub fn stack_to_network(
|
||||
item_id: impl Fn(&str) -> i32,
|
||||
enchantment_id: impl Fn(&str) -> Option<i32>,
|
||||
s: &Option<ItemStack>,
|
||||
) -> Option<Stack> {
|
||||
network_stack(&item_id, &enchantment_id, s)
|
||||
}
|
||||
fn network_stack(
|
||||
item_id: &dyn Fn(&str) -> i32,
|
||||
enchantment_id: &dyn Fn(&str) -> Option<i32>,
|
||||
s: &Option<ItemStack>,
|
||||
) -> Option<Stack> {
|
||||
let s = s.as_ref()?;
|
||||
let mut stack = Stack::new(item_id(&s.item), s.count as i32);
|
||||
@@ -488,6 +495,18 @@ pub fn stack_to_network(
|
||||
horizon_protocol::item::Component::Int(i32::from(mode)),
|
||||
));
|
||||
}
|
||||
// A loaded crossbow's ammunition.
|
||||
if !d.charged.is_empty() {
|
||||
let charged = d
|
||||
.charged
|
||||
.iter()
|
||||
.filter_map(|c| network_stack(item_id, enchantment_id, &Some(c.clone())))
|
||||
.collect();
|
||||
stack.added.push((
|
||||
horizon_protocol::item::component::CHARGED_PROJECTILES,
|
||||
horizon_protocol::item::Component::Stacks(charged),
|
||||
));
|
||||
}
|
||||
// The books a lectern shows and a guest reads.
|
||||
if !d.writable_pages.is_empty() {
|
||||
stack.added.push((
|
||||
|
||||
@@ -1981,14 +1981,15 @@ impl Hub {
|
||||
self.guests[i].sneaking = p.flags & 32 != 0;
|
||||
self.guests[i].sprinting = p.flags & 64 != 0 && p.flags & 1 != 0;
|
||||
}
|
||||
// OPEN_INVENTORY is the game's: the ridden mount's inventory.
|
||||
S::PlayerCommand(p) if p.action == 5 => {
|
||||
// OPEN_INVENTORY (the ridden mount's inventory) and
|
||||
// START_FALL_FLYING (checked against the guest's chest slot
|
||||
// and effects) are the game's.
|
||||
S::PlayerCommand(p) if matches!(p.action, 5 | 6) => {
|
||||
events.push(HostEvent::Packet(conn, Box::new(S::PlayerCommand(p))));
|
||||
}
|
||||
S::PlayerCommand(p) => match p.action {
|
||||
1 => self.guests[i].sprinting = true,
|
||||
2 => self.guests[i].sprinting = false,
|
||||
6 => self.guests[i].fall_flying = true,
|
||||
// STOP_SLEEPING.
|
||||
0 => self.wake(i),
|
||||
_ => {}
|
||||
|
||||
@@ -2277,7 +2277,7 @@ mod sensor_scan_tests {
|
||||
use horizon_core::geometry::Vec3;
|
||||
use serde_json::json;
|
||||
|
||||
fn kind() -> Arc<MobType> {
|
||||
pub(super) fn kind() -> Arc<MobType> {
|
||||
Arc::new(MobType {
|
||||
id: "minecraft:cow".into(),
|
||||
species: Species::Cow,
|
||||
@@ -2481,56 +2481,9 @@ mod sensor_scan_tests {
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod push_tests {
|
||||
use super::sensor_scan_tests::kind;
|
||||
use super::*;
|
||||
use horizon_core::geometry::Vec3;
|
||||
use serde_json::json;
|
||||
|
||||
fn kind() -> Arc<MobType> {
|
||||
Arc::new(MobType {
|
||||
id: "minecraft:cow".into(),
|
||||
species: Species::Cow,
|
||||
width: 0.9,
|
||||
height: 1.4,
|
||||
eye_height: 1.3,
|
||||
category: "creature".into(),
|
||||
max_health: 10.0,
|
||||
speed: 0.2,
|
||||
flying_speed: 0.4,
|
||||
attack_damage: 0.0,
|
||||
attack_knockback: 0.0,
|
||||
follow_range: 16.0,
|
||||
step_height: 0.6,
|
||||
armor: 0.0,
|
||||
armor_toughness: 0.0,
|
||||
knockback_resistance: 0.0,
|
||||
jump_strength: 0.42,
|
||||
attributes: Arc::default(),
|
||||
gravity: 0.08,
|
||||
safe_fall_distance: 3.0,
|
||||
fall_damage_multiplier: 1.0,
|
||||
water_efficiency: 0.0,
|
||||
water_slowdown: 0.8,
|
||||
air_speed: 0.02,
|
||||
baby_scale: 0.5,
|
||||
head_rot_speed: 10.0,
|
||||
max_head_x_rot: 40.0,
|
||||
max_head_y_rot: 75.0,
|
||||
ambient_interval: 120,
|
||||
flags: Flags {
|
||||
animal: true,
|
||||
..Default::default()
|
||||
},
|
||||
model: Arc::new(Part::parse("root", &json!({}))),
|
||||
layers: HashMap::new(),
|
||||
texture: String::new(),
|
||||
brain: Brain::Passive,
|
||||
anim: Anim::Rabbit,
|
||||
sounds: [""; 3],
|
||||
step_sound: None,
|
||||
food: Arc::default(),
|
||||
tempt: Arc::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The pending pushes as one list, scanned for each mob, as before.
|
||||
fn take_old(events: &mut Vec<MobEvent>, from: usize, pending: &mut Vec<(u32, Vec3)>) {
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
//! own random drives positions, sounds and jumps. `RunOne` shuffles with a
|
||||
//! random of its own, seeded with the brain's `shuffle_seed` when it is
|
||||
//! built.
|
||||
use super::{Mob, MobEvent, MobWorld, Species, Who, path::Path, sniffer};
|
||||
use super::{
|
||||
Mob, MobEvent, MobWorld, Species, Who,
|
||||
brain::{Timed, living_near},
|
||||
path::Path,
|
||||
sniffer,
|
||||
};
|
||||
use horizon_core::{
|
||||
geometry::Vec3,
|
||||
random::{LegacyRandom, RandomSource},
|
||||
@@ -603,22 +608,7 @@ fn tick_sensors(m: &mut Mob, w: &MobWorld) {
|
||||
/// `NearestLivingEntitySensor`: everything alive within the follow range,
|
||||
/// nearest first.
|
||||
fn sense_living(m: &mut Mob, w: &MobWorld) {
|
||||
let r = m.attrs.follow_range;
|
||||
let area = m.bounds().inflate(Vec3::new(r, r, r));
|
||||
let mut found: Vec<(Who, f64)> = Vec::new();
|
||||
for o in w.others_near(area) {
|
||||
if o.id != m.id && o.alive && o.bounds.intersects(area) {
|
||||
found.push((Who::Mob(o.id), m.distance_sqr(o.position)));
|
||||
}
|
||||
}
|
||||
if let Some(p) = &w.player
|
||||
&& p.alive
|
||||
&& p.bounds.intersects(area)
|
||||
{
|
||||
found.push((Who::Player, m.distance_sqr(p.position)));
|
||||
}
|
||||
found.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||||
let list: Vec<Who> = found.into_iter().map(|(w, _)| w).collect();
|
||||
let list = living_near(m, w);
|
||||
let mm = mem(m);
|
||||
mm.mobs.set_list(list.clone());
|
||||
mm.visible_mobs.set(Visible {
|
||||
@@ -794,37 +784,6 @@ fn followable_friendly(s: Species) -> bool {
|
||||
|
||||
// ---- Behaviours ------------------------------------------------------------
|
||||
|
||||
/// `Behavior`'s running state and end time.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct Timed {
|
||||
running: bool,
|
||||
end: i64,
|
||||
min: i32,
|
||||
max: i32,
|
||||
}
|
||||
impl Timed {
|
||||
fn new(min: i32, max: i32) -> Self {
|
||||
Self {
|
||||
running: false,
|
||||
end: 0,
|
||||
min,
|
||||
max,
|
||||
}
|
||||
}
|
||||
/// `Behavior.tryStart`'s duration draw.
|
||||
fn begin(&mut self, cx: &mut Bx) {
|
||||
self.running = true;
|
||||
let bound = self.max.wrapping_add(1).wrapping_sub(self.min);
|
||||
let d = self
|
||||
.min
|
||||
.wrapping_add(cx.lr.next_int_bound(bound).unwrap_or(0));
|
||||
self.end = cx.now + i64::from(d);
|
||||
}
|
||||
fn timed_out(&self, now: i64) -> bool {
|
||||
now > self.end
|
||||
}
|
||||
}
|
||||
|
||||
/// `RunOne`: shuffles its children by weight and starts the first that can.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Gate {
|
||||
@@ -1076,7 +1035,7 @@ impl B {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
mem(m).walk_target.clear();
|
||||
mem(m).look_target.clear();
|
||||
m.add_effect("minecraft:regeneration", 200, 0);
|
||||
@@ -1086,7 +1045,7 @@ impl B {
|
||||
if !swim_condition(m) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::AnimalPanic { t, .. } => {
|
||||
@@ -1094,7 +1053,7 @@ impl B {
|
||||
if !(mm.hurt_by.get() == Some(&true) || mm.is_panicking.present()) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
if brain_mut(m).kind == Kind::Sniffer {
|
||||
reset_sniffing(m, cx);
|
||||
}
|
||||
@@ -1123,7 +1082,7 @@ impl B {
|
||||
let reached = reached(m, cx.w, &walk);
|
||||
if !reached && compute_path(m, cx, &walk, path, speed) {
|
||||
*last_target = walk.target.block(cx.w);
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
mem(m).path.set_opt(path.clone());
|
||||
m.nav_move_to_path(path.clone(), f64::from(*speed));
|
||||
return true;
|
||||
@@ -1139,21 +1098,21 @@ impl B {
|
||||
if !mem(m).temptation_cooldown_ticks.present() {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::CountDownGaze(t) => {
|
||||
if !mem(m).gaze_cooldown_ticks.present() {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::CountDownTransport(t) => {
|
||||
if !mem(m).transport_items_cooldown_ticks.present() {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::Transport(x) => x.try_start(m, cx),
|
||||
@@ -1171,7 +1130,7 @@ impl B {
|
||||
let Some(partner) = breed_partner(m, cx.w) else {
|
||||
return false;
|
||||
};
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
if brain_mut(m).kind == Kind::Sniffer {
|
||||
reset_sniffing(m, cx);
|
||||
}
|
||||
@@ -1200,7 +1159,7 @@ impl B {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
if brain_mut(m).kind == Kind::Sniffer {
|
||||
reset_sniffing(m, cx);
|
||||
}
|
||||
@@ -1210,14 +1169,14 @@ impl B {
|
||||
if !mem(m).look_target.present() {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::FeelingHappy(t) => {
|
||||
if !mem(m).sniffer_happy.present() {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
transition(m, cx, sniffer::FEELING_HAPPY);
|
||||
true
|
||||
}
|
||||
@@ -1233,7 +1192,7 @@ impl B {
|
||||
if !can_sniff(m) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
transition(m, cx, sniffer::DIGGING);
|
||||
true
|
||||
}
|
||||
@@ -1246,7 +1205,7 @@ impl B {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
transition(m, cx, sniffer::RISING);
|
||||
true
|
||||
}
|
||||
@@ -1261,7 +1220,7 @@ impl B {
|
||||
if !can_sniff(m) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
transition(m, cx, sniffer::SEARCHING);
|
||||
true
|
||||
}
|
||||
@@ -1278,7 +1237,7 @@ impl B {
|
||||
if is_tempted(m) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
transition(m, cx, sniffer::SCENTING);
|
||||
true
|
||||
}
|
||||
@@ -1293,12 +1252,12 @@ impl B {
|
||||
if m.is_baby() || !can_sniff(m) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
transition(m, cx, sniffer::SNIFFING);
|
||||
true
|
||||
}
|
||||
B::DoNothing(t) => {
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::AxolotlAction { .. }
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
//! Pieces of vanilla's `Brain` that species implemented as brains share:
|
||||
//! memories that expire, behaviour durations, walk targets and
|
||||
//! `MoveToTargetSink`.
|
||||
use super::{Mob, goals::Cx};
|
||||
use horizon_core::random::RandomSource;
|
||||
use super::{Mob, MobWorld, Who, goals::Cx};
|
||||
use horizon_core::{
|
||||
geometry::Vec3,
|
||||
random::{LegacyRandom, RandomSource},
|
||||
};
|
||||
|
||||
/// A memory with an expiry (`Brain.setMemoryWithExpiry`): present for the
|
||||
/// tick it was set and `ttl` more ticks. `Ttl::default()` is absent.
|
||||
@@ -25,6 +28,57 @@ impl Ttl {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `Behavior`'s bookkeeping in the brains ported behaviour for behaviour
|
||||
/// (villager, animal, monster, warden): running, and until when.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Timed {
|
||||
pub running: bool,
|
||||
pub end: i64,
|
||||
pub min: i32,
|
||||
pub max: i32,
|
||||
}
|
||||
impl Timed {
|
||||
pub fn new(min: i32, max: i32) -> Self {
|
||||
Self {
|
||||
running: false,
|
||||
end: 0,
|
||||
min,
|
||||
max,
|
||||
}
|
||||
}
|
||||
/// `Behavior.tryStart`'s duration draw from the level random, in Java's
|
||||
/// int arithmetic (made even when it cannot vary).
|
||||
pub fn begin(&mut self, lr: &mut LegacyRandom, now: i64) {
|
||||
self.running = true;
|
||||
let bound = self.max.wrapping_add(1).wrapping_sub(self.min);
|
||||
let d = self.min.wrapping_add(lr.next_int_bound(bound).unwrap_or(0));
|
||||
self.end = now + i64::from(d);
|
||||
}
|
||||
/// `Behavior.timedOut`.
|
||||
pub fn timed_out(&self, now: i64) -> bool {
|
||||
now > self.end
|
||||
}
|
||||
}
|
||||
/// `NearestLivingEntitySensor`'s scan: the living entities (the player
|
||||
/// among them) whose boxes meet the follow range around `m`, nearest first.
|
||||
pub fn living_near(m: &Mob, w: &MobWorld) -> Vec<Who> {
|
||||
let r = m.attrs.follow_range;
|
||||
let area = m.bounds().inflate(Vec3::new(r, r, r));
|
||||
let mut found: Vec<(Who, f64)> = Vec::new();
|
||||
for o in w.others_near(area) {
|
||||
if o.id != m.id && o.alive && o.bounds.intersects(area) {
|
||||
found.push((Who::Mob(o.id), m.distance_sqr(o.position)));
|
||||
}
|
||||
}
|
||||
if let Some(p) = &w.player
|
||||
&& p.alive
|
||||
&& p.bounds.intersects(area)
|
||||
{
|
||||
found.push((Who::Player, m.distance_sqr(p.position)));
|
||||
}
|
||||
found.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||||
found.into_iter().map(|(w, _)| w).collect()
|
||||
}
|
||||
/// A random duration in `min..=max` (`Behavior`'s end time).
|
||||
pub fn duration(m: &mut Mob, min: i32, max: i32) -> i32 {
|
||||
if max <= min {
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
//! seeded when the brain is built.
|
||||
use super::{
|
||||
Mob, MobEvent, MobWorld, Species, Who,
|
||||
brain::{Timed, living_near},
|
||||
goals::Cx,
|
||||
path::Path,
|
||||
villager_brain::{NEVER, Slot, Tracker, Visible, WalkTarget},
|
||||
villager_brain::{NEVER, Slot, Tracker, Visible, WalkTarget, compute_path, reached},
|
||||
};
|
||||
use crate::projectiles::{Projectile, ProjectileKind};
|
||||
use horizon_core::{
|
||||
@@ -129,6 +130,10 @@ fn brain_mut(m: &mut Mob) -> &mut Brain {
|
||||
fn mem(m: &mut Mob) -> &mut Memories {
|
||||
&mut brain_mut(m).mem
|
||||
}
|
||||
/// `CANT_REACH_WALK_TARGET_SINCE`, for `compute_path`.
|
||||
fn cant_reach(m: &mut Mob) -> &mut Slot<i64> {
|
||||
&mut mem(m).cant_reach_walk_target_since
|
||||
}
|
||||
fn mem_ref(m: &Mob) -> &Memories {
|
||||
&brain(m).expect("brain").mem
|
||||
}
|
||||
@@ -292,22 +297,7 @@ fn tick_sensors(m: &mut Mob, w: &MobWorld) {
|
||||
/// `NearestLivingEntitySensor`: everything alive within the follow range,
|
||||
/// nearest first.
|
||||
fn sense_living(m: &mut Mob, w: &MobWorld) {
|
||||
let r = m.attrs.follow_range;
|
||||
let area = m.bounds().inflate(Vec3::new(r, r, r));
|
||||
let mut found: Vec<(Who, f64)> = Vec::new();
|
||||
for o in w.others_near(area) {
|
||||
if o.id != m.id && o.alive && o.bounds.intersects(area) {
|
||||
found.push((Who::Mob(o.id), m.distance_sqr(o.position)));
|
||||
}
|
||||
}
|
||||
if let Some(p) = &w.player
|
||||
&& p.alive
|
||||
&& p.bounds.intersects(area)
|
||||
{
|
||||
found.push((Who::Player, m.distance_sqr(p.position)));
|
||||
}
|
||||
found.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||||
let list: Vec<Who> = found.into_iter().map(|(w, _)| w).collect();
|
||||
let list = living_near(m, w);
|
||||
let mm = mem(m);
|
||||
mm.mobs.set_list(list.clone());
|
||||
mm.visible_mobs.set(Visible {
|
||||
@@ -456,31 +446,6 @@ fn sense_breeze_attack(m: &mut Mob, w: &MobWorld) {
|
||||
|
||||
// ---- Behaviours ---------------------------------------------------------
|
||||
|
||||
/// `Behavior`'s bookkeeping: running, and until when.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Timed {
|
||||
running: bool,
|
||||
end: i64,
|
||||
min: i32,
|
||||
max: i32,
|
||||
}
|
||||
impl Timed {
|
||||
fn new(min: i32, max: i32) -> Self {
|
||||
Self {
|
||||
running: false,
|
||||
end: 0,
|
||||
min,
|
||||
max,
|
||||
}
|
||||
}
|
||||
/// `Behavior.tryStart`'s duration draw from the level random.
|
||||
fn begin(&mut self, cx: &mut Bx) {
|
||||
self.running = true;
|
||||
let d = self.min + cx.lr.next_int_bound(self.max + 1 - self.min).unwrap_or(0);
|
||||
self.end = cx.now + i64::from(d);
|
||||
}
|
||||
}
|
||||
|
||||
/// `StartAttacking`'s target finders.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Finder {
|
||||
@@ -607,14 +572,14 @@ impl B {
|
||||
if *creaking && !super::creaking::can_move(m) || !swim_condition(m) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::LookAtTargetSink(t) => {
|
||||
if !mem(m).look_target.present() {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::MoveToTargetSink {
|
||||
@@ -636,9 +601,9 @@ impl B {
|
||||
}
|
||||
let walk = mm.walk_target.value().unwrap();
|
||||
let reached = reached(m, cx.w, &walk);
|
||||
if !reached && compute_path(m, cx, &walk, path, speed) {
|
||||
if !reached && compute_path(m, cx.w, cx.now, &walk, path, speed, cant_reach) {
|
||||
*last_target = walk.target.block(cx.w);
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
// start
|
||||
mem(m).path.set_opt(path.clone());
|
||||
m.nav_move_to_path(path.clone(), f64::from(*speed));
|
||||
@@ -719,7 +684,7 @@ impl B {
|
||||
true
|
||||
}
|
||||
B::DoNothing(t) => {
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::RandomStroll(running, speed) => {
|
||||
@@ -848,7 +813,7 @@ impl B {
|
||||
mem(m).breeze_shoot.clear();
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
// start
|
||||
super::breeze::set_pose(m, super::breeze::SHOOTING);
|
||||
mem(m).breeze_shoot_charging.set_ttl((), 15);
|
||||
@@ -867,7 +832,7 @@ impl B {
|
||||
if !long_jump_can_run(m, cx) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
// start
|
||||
if !mem(m).breeze_jump_inhaling.present() {
|
||||
mem(m).breeze_jump_inhaling.set_ttl((), 10);
|
||||
@@ -905,7 +870,7 @@ impl B {
|
||||
if !stuck {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
mem(m).breeze_shoot.set_ttl((), 60);
|
||||
true
|
||||
}
|
||||
@@ -924,7 +889,7 @@ impl B {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
slide_start(m, cx);
|
||||
true
|
||||
}
|
||||
@@ -989,7 +954,7 @@ impl B {
|
||||
&& let Some(now_target) = walk.target.block(cx.w)
|
||||
{
|
||||
let d = manhattan_sq(now_target, last);
|
||||
if d > 4 && compute_path(m, cx, &walk, path, speed) {
|
||||
if d > 4 && compute_path(m, cx.w, cx.now, &walk, path, speed, cant_reach) {
|
||||
*last_target = Some(now_target);
|
||||
mem(m).path.set_opt(path.clone());
|
||||
m.nav_move_to_path(path.clone(), f64::from(*speed));
|
||||
@@ -1122,53 +1087,6 @@ fn tracker_visible(m: &mut Mob, w: &MobWorld, t: Tracker) -> bool {
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
/// `MoveToTargetSink.reachedTarget`.
|
||||
fn reached(m: &Mob, w: &MobWorld, walk: &WalkTarget) -> bool {
|
||||
walk.target.block(w).is_some_and(|p| {
|
||||
let h = block_of(m.position);
|
||||
(p[0] - h[0]).abs() + (p[1] - h[1]).abs() + (p[2] - h[2]).abs() <= walk.close
|
||||
})
|
||||
}
|
||||
/// `MoveToTargetSink.tryComputePath`.
|
||||
fn compute_path(
|
||||
m: &mut Mob,
|
||||
cx: &mut Bx,
|
||||
walk: &WalkTarget,
|
||||
path: &mut Option<Path>,
|
||||
speed: &mut f32,
|
||||
) -> bool {
|
||||
let Some(target) = walk.target.block(cx.w) else {
|
||||
return false;
|
||||
};
|
||||
*path = m.create_path_to(cx.w, target, 0);
|
||||
*speed = walk.speed;
|
||||
if reached(m, cx.w, walk) {
|
||||
mem(m).cant_reach_walk_target_since.clear();
|
||||
} else {
|
||||
let can_reach = path.as_ref().is_some_and(|p| p.reached);
|
||||
let mm = mem(m);
|
||||
if can_reach {
|
||||
mm.cant_reach_walk_target_since.clear();
|
||||
} else if !mm.cant_reach_walk_target_since.present() {
|
||||
mm.cant_reach_walk_target_since.set(cx.now);
|
||||
}
|
||||
if path.is_some() {
|
||||
return true;
|
||||
}
|
||||
let to = [
|
||||
f64::from(target[0]) + 0.5,
|
||||
f64::from(target[1]),
|
||||
f64::from(target[2]) + 0.5,
|
||||
];
|
||||
if let Some(step) =
|
||||
m.default_random_pos_towards(cx.w, 10, 7, to, f64::from(std::f32::consts::FRAC_PI_2))
|
||||
{
|
||||
*path = m.create_path_to(cx.w, block_of(Vec3::new(step[0], step[1], step[2])), 0);
|
||||
return path.is_some();
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
/// `BehaviorUtils.isWithinAttackRange(body, target, 1)` for a mob without a
|
||||
/// ranged weapon: `Mob.isWithinMeleeAttackRange`.
|
||||
fn within_attack_range(m: &Mob, w: &MobWorld, target: Who) -> bool {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
//! `Brain::shuffle_seed` when the brain is built.
|
||||
use super::{
|
||||
Mob, MobEvent, MobWorld, Species, Who,
|
||||
brain::{Timed, living_near},
|
||||
path::{Path, PathNode},
|
||||
};
|
||||
use horizon_core::{
|
||||
@@ -341,6 +342,10 @@ fn brain_mut(m: &mut Mob) -> &mut Brain {
|
||||
fn mem(m: &mut Mob) -> &mut Memories {
|
||||
&mut brain_mut(m).mem
|
||||
}
|
||||
/// `CANT_REACH_WALK_TARGET_SINCE`, for `compute_path`.
|
||||
fn cant_reach(m: &mut Mob) -> &mut Slot<i64> {
|
||||
&mut mem(m).cant_reach_walk_target_since
|
||||
}
|
||||
fn mem_ref(m: &Mob) -> Option<&Memories> {
|
||||
brain(m).map(|b| &b.mem)
|
||||
}
|
||||
@@ -565,22 +570,7 @@ const FOLLOW_RANGE: f64 = 16.0;
|
||||
/// `NearestLivingEntitySensor`: everything alive within the follow range,
|
||||
/// nearest first.
|
||||
fn sense_living(m: &mut Mob, w: &MobWorld) {
|
||||
let r = m.attrs.follow_range;
|
||||
let area = m.bounds().inflate(Vec3::new(r, r, r));
|
||||
let mut found: Vec<(Who, f64)> = Vec::new();
|
||||
for o in w.others_near(area) {
|
||||
if o.id != m.id && o.alive && o.bounds.intersects(area) {
|
||||
found.push((Who::Mob(o.id), m.distance_sqr(o.position)));
|
||||
}
|
||||
}
|
||||
if let Some(p) = &w.player
|
||||
&& p.alive
|
||||
&& p.bounds.intersects(area)
|
||||
{
|
||||
found.push((Who::Player, m.distance_sqr(p.position)));
|
||||
}
|
||||
found.sort_by(|a, b| a.1.total_cmp(&b.1));
|
||||
let list: Vec<Who> = found.into_iter().map(|(w, _)| w).collect();
|
||||
let list = living_near(m, w);
|
||||
let mm = mem(m);
|
||||
mm.mobs.set_list(list.clone());
|
||||
mm.visible_mobs.set(Visible {
|
||||
@@ -791,34 +781,6 @@ fn sense_golem(m: &mut Mob, w: &MobWorld) {
|
||||
|
||||
// ---- Behaviours ---------------------------------------------------------
|
||||
|
||||
/// `Behavior`'s bookkeeping: running, and until when.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Timed {
|
||||
running: bool,
|
||||
end: i64,
|
||||
min: i32,
|
||||
max: i32,
|
||||
}
|
||||
impl Timed {
|
||||
fn new(min: i32, max: i32) -> Self {
|
||||
Self {
|
||||
running: false,
|
||||
end: 0,
|
||||
min,
|
||||
max,
|
||||
}
|
||||
}
|
||||
/// `Behavior.tryStart`'s duration draw.
|
||||
fn begin(&mut self, cx: &mut Bx) {
|
||||
self.running = true;
|
||||
let bound = self.max.wrapping_add(1).wrapping_sub(self.min);
|
||||
let d = self
|
||||
.min
|
||||
.wrapping_add(cx.lr.next_int_bound(bound).unwrap_or(0));
|
||||
self.end = cx.now + i64::from(d);
|
||||
}
|
||||
}
|
||||
|
||||
/// The POI types villagers use.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Poi {
|
||||
@@ -1498,18 +1460,18 @@ impl B {
|
||||
if !swim_condition(m) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::LookAtTargetSink(t) => {
|
||||
if !mem(m).look_target.present() {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::PanicTrigger(t) => {
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
// VillagerPanicTrigger.start
|
||||
if panic_condition(m) {
|
||||
if !brain_mut(m).is_active(Act::Panic) {
|
||||
@@ -1542,9 +1504,9 @@ impl B {
|
||||
}
|
||||
let walk = mm.walk_target.value().unwrap();
|
||||
let reached = reached(m, cx.w, &walk);
|
||||
if !reached && compute_path(m, cx, &walk, path, speed) {
|
||||
if !reached && compute_path(m, cx.w, cx.now, &walk, path, speed, cant_reach) {
|
||||
*last_target = walk.target.block(cx.w);
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
// start
|
||||
mem(m).path.set_opt(path.clone());
|
||||
m.nav_move_to_path(path.clone(), f64::from(*speed));
|
||||
@@ -1561,7 +1523,7 @@ impl B {
|
||||
if !follow_trading_condition(m, cx.w) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
follow_trading_player(m);
|
||||
true
|
||||
}
|
||||
@@ -1573,7 +1535,7 @@ impl B {
|
||||
if !matches!(a, Act::Idle | Act::Work | Act::Play) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::JumpOnBed {
|
||||
@@ -1590,7 +1552,7 @@ impl B {
|
||||
if mm.walk_target.present() || !m.is_baby() {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
*bed = Some(nearest);
|
||||
*time_left = 100;
|
||||
*jumps = 3 + cx.lr.next_int_bound(4).unwrap_or(0);
|
||||
@@ -1599,7 +1561,7 @@ impl B {
|
||||
true
|
||||
}
|
||||
B::DoNothing(t) => {
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::GiveGiftToHero {
|
||||
@@ -1618,7 +1580,7 @@ impl B {
|
||||
*until_next -= 1;
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
*given = false;
|
||||
*started = now;
|
||||
let mm = mem(m);
|
||||
@@ -1630,7 +1592,7 @@ impl B {
|
||||
if !mem(m).interaction_target.present() || !show_trades_condition(m, cx.w) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
// start
|
||||
if let Some(target) = mem(m).interaction_target.value() {
|
||||
mem(m).look_target.set(Tracker::Entity(target, true));
|
||||
@@ -1646,7 +1608,7 @@ impl B {
|
||||
if !villager_target_valid(m, cx.w, Mem::InteractionTarget) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
let target = mem(m).interaction_target.value().unwrap();
|
||||
lock_gaze(m, target, cx);
|
||||
true
|
||||
@@ -1659,7 +1621,7 @@ impl B {
|
||||
if !breeding_possible(m, cx.w) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
let target = mem(m).breed_target.value().unwrap();
|
||||
lock_gaze(m, target, cx);
|
||||
*birth = now + 275 + i64::from(m.random.next_int_bound(50).unwrap_or(0));
|
||||
@@ -1680,7 +1642,7 @@ impl B {
|
||||
if !closer_to_center(site, m.position, 1.73) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
// start
|
||||
let mm = mem(m);
|
||||
mm.last_worked_at_poi.set(now);
|
||||
@@ -1709,7 +1671,7 @@ impl B {
|
||||
*valid = farmland_around(m, cx.w);
|
||||
*target = pick_farmland(valid, cx);
|
||||
let Some(p) = *target else { return false };
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
if now > *next_ok {
|
||||
let mm = mem(m);
|
||||
mm.look_target.set(Tracker::Block(p));
|
||||
@@ -2288,7 +2250,7 @@ impl B {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
if now > *next_ok {
|
||||
if super::villager::start_sleeping(m, home, w) {
|
||||
mem(m).last_slept.set(now);
|
||||
@@ -2609,7 +2571,7 @@ impl B {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::LocateHidingPlace {
|
||||
@@ -2769,7 +2731,7 @@ impl B {
|
||||
&& let Some(now_target) = walk.target.block(cx.w)
|
||||
{
|
||||
let d = manhattan_sq(now_target, last);
|
||||
if d > 4 && compute_path(m, cx, &walk, path, speed) {
|
||||
if d > 4 && compute_path(m, cx.w, cx.now, &walk, path, speed, cant_reach) {
|
||||
*last_target = Some(now_target);
|
||||
mem(m).path.set_opt(path.clone());
|
||||
m.nav_move_to_path(path.clone(), f64::from(*speed));
|
||||
@@ -3141,33 +3103,37 @@ fn tracker_visible(m: &mut Mob, w: &MobWorld, t: Tracker) -> bool {
|
||||
}
|
||||
}
|
||||
/// `MoveToTargetSink.reachedTarget`.
|
||||
fn reached(m: &Mob, w: &MobWorld, walk: &WalkTarget) -> bool {
|
||||
pub(super) fn reached(m: &Mob, w: &MobWorld, walk: &WalkTarget) -> bool {
|
||||
walk.target
|
||||
.block(w)
|
||||
.is_some_and(|p| manhattan(p, block_of(m.position)) <= walk.close)
|
||||
}
|
||||
/// `MoveToTargetSink.tryComputePath`.
|
||||
fn compute_path(
|
||||
/// `MoveToTargetSink.tryComputePath`, for the brains on these memories
|
||||
/// (villager, monster, warden): `cant_reach` is the brain's
|
||||
/// `CANT_REACH_WALK_TARGET_SINCE`.
|
||||
pub(super) fn compute_path(
|
||||
m: &mut Mob,
|
||||
cx: &mut Bx,
|
||||
w: &MobWorld,
|
||||
now: i64,
|
||||
walk: &WalkTarget,
|
||||
path: &mut Option<Path>,
|
||||
speed: &mut f32,
|
||||
cant_reach: fn(&mut Mob) -> &mut Slot<i64>,
|
||||
) -> bool {
|
||||
let Some(target) = walk.target.block(cx.w) else {
|
||||
let Some(target) = walk.target.block(w) else {
|
||||
return false;
|
||||
};
|
||||
*path = m.create_path_to(cx.w, target, 0);
|
||||
*path = m.create_path_to(w, target, 0);
|
||||
*speed = walk.speed;
|
||||
if reached(m, cx.w, walk) {
|
||||
mem(m).cant_reach_walk_target_since.clear();
|
||||
if reached(m, w, walk) {
|
||||
cant_reach(m).clear();
|
||||
} else {
|
||||
let can_reach = path.as_ref().is_some_and(|p| p.reached);
|
||||
let mm = mem(m);
|
||||
let since = cant_reach(m);
|
||||
if can_reach {
|
||||
mm.cant_reach_walk_target_since.clear();
|
||||
} else if !mm.cant_reach_walk_target_since.present() {
|
||||
mm.cant_reach_walk_target_since.set(cx.now);
|
||||
since.clear();
|
||||
} else if !since.present() {
|
||||
since.set(now);
|
||||
}
|
||||
if path.is_some() {
|
||||
return true;
|
||||
@@ -3178,14 +3144,14 @@ fn compute_path(
|
||||
f64::from(target[2]) + 0.5,
|
||||
];
|
||||
if let Some(step) =
|
||||
m.default_random_pos_towards(cx.w, 10, 7, to, f64::from(std::f32::consts::FRAC_PI_2))
|
||||
m.default_random_pos_towards(w, 10, 7, to, f64::from(std::f32::consts::FRAC_PI_2))
|
||||
{
|
||||
let p = [
|
||||
step[0].floor() as i32,
|
||||
step[1].floor() as i32,
|
||||
step[2].floor() as i32,
|
||||
];
|
||||
*path = m.create_path_to(cx.w, p, 0);
|
||||
*path = m.create_path_to(w, p, 0);
|
||||
return path.is_some();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
//! shuffle seed.
|
||||
use super::{
|
||||
Mob, MobEvent, MobWorld, Who,
|
||||
brain::Timed,
|
||||
goals::Cx,
|
||||
path::Path,
|
||||
villager_brain::{Slot, Tracker, Visible, WalkTarget},
|
||||
villager_brain::{Slot, Tracker, Visible, WalkTarget, compute_path, reached},
|
||||
warden::{self, Pose},
|
||||
};
|
||||
use horizon_core::{
|
||||
@@ -145,31 +146,6 @@ impl Memories {
|
||||
}
|
||||
}
|
||||
|
||||
/// `Behavior`'s bookkeeping: running, and until when.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Timed {
|
||||
running: bool,
|
||||
end: i64,
|
||||
min: i32,
|
||||
max: i32,
|
||||
}
|
||||
impl Timed {
|
||||
fn new(min: i32, max: i32) -> Self {
|
||||
Self {
|
||||
running: false,
|
||||
end: 0,
|
||||
min,
|
||||
max,
|
||||
}
|
||||
}
|
||||
/// `Behavior.tryStart`'s duration draw (made even when it cannot vary).
|
||||
fn begin(&mut self, cx: &mut Bx) {
|
||||
self.running = true;
|
||||
let d = self.min + cx.lr.next_int_bound(self.max + 1 - self.min).unwrap_or(0);
|
||||
self.end = cx.now + i64::from(d);
|
||||
}
|
||||
}
|
||||
|
||||
/// `RunOne`'s `ShufflingList` over a stroll (weight 2) and `DoNothing` (1).
|
||||
#[derive(Clone, Debug)]
|
||||
struct Gate {
|
||||
@@ -383,6 +359,10 @@ fn brain(m: &Mob) -> Option<&Brain> {
|
||||
pub(super) fn mem(m: &mut Mob) -> &mut Memories {
|
||||
&mut m.extra.warden.as_mut().expect("a warden").brain.mem
|
||||
}
|
||||
/// `CANT_REACH_WALK_TARGET_SINCE`, for `compute_path`.
|
||||
fn cant_reach(m: &mut Mob) -> &mut Slot<i64> {
|
||||
&mut mem(m).cant_reach_walk_target_since
|
||||
}
|
||||
pub(super) fn mem_ref(m: &Mob) -> Option<&Memories> {
|
||||
brain(m).map(|b| &b.mem)
|
||||
}
|
||||
@@ -558,9 +538,6 @@ fn find_closest(m: &mut Mob, w: &MobWorld, filter: impl Fn(&Mob, Who) -> bool) -
|
||||
fn block_of(p: Vec3) -> [i32; 3] {
|
||||
[p.x.floor() as i32, p.y.floor() as i32, p.z.floor() as i32]
|
||||
}
|
||||
fn manhattan(a: [i32; 3], b: [i32; 3]) -> i32 {
|
||||
(a[0] - b[0]).abs() + (a[1] - b[1]).abs() + (a[2] - b[2]).abs()
|
||||
}
|
||||
fn dist_sqr(a: [i32; 3], b: [i32; 3]) -> i64 {
|
||||
(0..3).map(|i| i64::from(a[i] - b[i]).pow(2)).sum()
|
||||
}
|
||||
@@ -607,57 +584,6 @@ fn tracker_visible(m: &mut Mob, w: &MobWorld, t: Tracker) -> bool {
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
/// `MoveToTargetSink.reachedTarget`.
|
||||
fn reached(m: &Mob, w: &MobWorld, walk: &WalkTarget) -> bool {
|
||||
walk.target
|
||||
.block(w)
|
||||
.is_some_and(|p| manhattan(p, block_of(m.position)) <= walk.close)
|
||||
}
|
||||
/// `MoveToTargetSink.tryComputePath`.
|
||||
fn compute_path(
|
||||
m: &mut Mob,
|
||||
cx: &mut Bx,
|
||||
walk: &WalkTarget,
|
||||
path: &mut Option<Path>,
|
||||
speed: &mut f32,
|
||||
) -> bool {
|
||||
let Some(target) = walk.target.block(cx.w) else {
|
||||
return false;
|
||||
};
|
||||
*path = m.create_path_to(cx.w, target, 0);
|
||||
*speed = walk.speed;
|
||||
if reached(m, cx.w, walk) {
|
||||
mem(m).cant_reach_walk_target_since.clear();
|
||||
} else {
|
||||
let can_reach = path.as_ref().is_some_and(|p| p.reached);
|
||||
let mm = mem(m);
|
||||
if can_reach {
|
||||
mm.cant_reach_walk_target_since.clear();
|
||||
} else if !mm.cant_reach_walk_target_since.present() {
|
||||
mm.cant_reach_walk_target_since.set(cx.now);
|
||||
}
|
||||
if path.is_some() {
|
||||
return true;
|
||||
}
|
||||
let to = [
|
||||
f64::from(target[0]) + 0.5,
|
||||
f64::from(target[1]),
|
||||
f64::from(target[2]) + 0.5,
|
||||
];
|
||||
if let Some(step) =
|
||||
m.default_random_pos_towards(cx.w, 10, 7, to, f64::from(std::f32::consts::FRAC_PI_2))
|
||||
{
|
||||
let p = [
|
||||
step[0].floor() as i32,
|
||||
step[1].floor() as i32,
|
||||
step[2].floor() as i32,
|
||||
];
|
||||
*path = m.create_path_to(cx.w, p, 0);
|
||||
return path.is_some();
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
fn same_path(a: Option<&Path>, b: Option<&Path>) -> bool {
|
||||
match (a, b) {
|
||||
(None, None) => true,
|
||||
@@ -710,7 +636,7 @@ impl B {
|
||||
if !swim_condition(m) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::SetWardenLookTarget(r) => one(r, || {
|
||||
@@ -732,7 +658,7 @@ impl B {
|
||||
if !mem(m).look_target.present() {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::MoveToTargetSink {
|
||||
@@ -755,9 +681,9 @@ impl B {
|
||||
return false;
|
||||
};
|
||||
let reached = reached(m, cx.w, &walk);
|
||||
if !reached && compute_path(m, cx, &walk, path, speed) {
|
||||
if !reached && compute_path(m, cx.w, cx.now, &walk, path, speed, cant_reach) {
|
||||
*last_target = walk.target.block(cx.w);
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
// start
|
||||
mem(m).path.set_opt(path.clone());
|
||||
m.nav_move_to_path(path.clone(), f64::from(*speed));
|
||||
@@ -775,7 +701,7 @@ impl B {
|
||||
if !mm.is_emerging.present() || mm.walk_target.present() {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
warden::set_pose(m, Pose::Emerging);
|
||||
sound(m, "entity.warden.emerge", 5.0, 1.0, cx.events);
|
||||
true
|
||||
@@ -791,7 +717,7 @@ impl B {
|
||||
if !(m.on_ground || liquid) {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
if m.on_ground {
|
||||
warden::set_pose(m, Pose::Digging);
|
||||
sound(m, "entity.warden.dig", 5.0, 1.0, cx.events);
|
||||
@@ -872,7 +798,7 @@ impl B {
|
||||
true
|
||||
}),
|
||||
B::DoNothing(t) => {
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
true
|
||||
}
|
||||
B::GoToDisturbance(r) => one(r, || {
|
||||
@@ -907,7 +833,7 @@ impl B {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
sound(m, "entity.warden.sniff", 5.0, 1.0, cx.events);
|
||||
true
|
||||
}
|
||||
@@ -919,7 +845,7 @@ impl B {
|
||||
let Some(target) = mm.roar_target.value() else {
|
||||
return false;
|
||||
};
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
let mm = mem(m);
|
||||
mm.roar_sound_delay.set_ttl((), ROAR_SOUND_DELAY);
|
||||
mm.walk_target.clear();
|
||||
@@ -1004,7 +930,7 @@ impl B {
|
||||
if !close {
|
||||
return false;
|
||||
}
|
||||
t.begin(cx);
|
||||
t.begin(cx.lr, cx.now);
|
||||
let mm = mem(m);
|
||||
mm.attack_cooling_down
|
||||
.set_ttl(true, i64::from(SONIC_DURATION));
|
||||
@@ -1092,7 +1018,7 @@ impl B {
|
||||
&& let Some(walk) = mem(m).walk_target.value()
|
||||
&& let Some(now_target) = walk.target.block(cx.w)
|
||||
&& dist_sqr(now_target, last) > 4
|
||||
&& compute_path(m, cx, &walk, path, speed)
|
||||
&& compute_path(m, cx.w, cx.now, &walk, path, speed, cant_reach)
|
||||
{
|
||||
*last_target = Some(now_target);
|
||||
mem(m).path.set_opt(path.clone());
|
||||
|
||||
@@ -1336,6 +1336,16 @@ impl ParticleEngine {
|
||||
ParticleType::ExplosionEmitter
|
||||
};
|
||||
self.add(&ParticleOptions::simple(ty), center, [1.0, 0.0, 0.0]);
|
||||
self.explosion_blocks(center, radius, block_count, is_air);
|
||||
}
|
||||
/// `ClientExplosionTracker` with `Level.DEFAULT_EXPLOSION_BLOCK_PARTICLES`.
|
||||
pub fn explosion_blocks(
|
||||
&mut self,
|
||||
center: [f64; 3],
|
||||
radius: f32,
|
||||
block_count: usize,
|
||||
is_air: &dyn Fn([i32; 3]) -> bool,
|
||||
) {
|
||||
for _ in 0..block_count.min(512) {
|
||||
let r = &mut self.random;
|
||||
let d = Vec3::new(
|
||||
|
||||
@@ -32,6 +32,7 @@ pub mod component {
|
||||
pub const DYED_COLOR: i32 = 47;
|
||||
pub const MAP_ID: i32 = 48;
|
||||
pub const MAP_POST_PROCESSING: i32 = 50;
|
||||
pub const CHARGED_PROJECTILES: i32 = 51;
|
||||
pub const WRITABLE_BOOK_CONTENT: i32 = 56;
|
||||
pub const WRITTEN_BOOK_CONTENT: i32 = 57;
|
||||
}
|
||||
@@ -75,6 +76,8 @@ pub enum Component {
|
||||
/// `WritableBookContent`: the pages (raw text).
|
||||
WritableBook(Vec<String>),
|
||||
WrittenBook(WrittenBook),
|
||||
/// `ChargedProjectiles`: a loaded crossbow's ammunition, as templates.
|
||||
Stacks(Vec<Stack>),
|
||||
}
|
||||
|
||||
/// `Filterable.streamCodec`: the raw value, then an optional filtered one
|
||||
@@ -125,6 +128,10 @@ impl Component {
|
||||
resolved: r.bool()?,
|
||||
})
|
||||
}
|
||||
CHARGED_PROJECTILES => {
|
||||
let n = r.count(1024)?;
|
||||
Self::Stacks((0..n).map(|_| Template::read(r)).collect::<Result<_>>()?)
|
||||
}
|
||||
DYED_COLOR => Self::Int(r.i32()?),
|
||||
UNBREAKABLE | CREATIVE_SLOT_LOCK => Self::Unit,
|
||||
ENCHANTMENT_GLINT_OVERRIDE => Self::Bool(r.bool()?),
|
||||
@@ -283,6 +290,10 @@ impl Component {
|
||||
w.varint(hidden.len() as i32);
|
||||
hidden.iter().for_each(|h| w.varint(*h));
|
||||
}
|
||||
Self::Stacks(stacks) => {
|
||||
w.varint(stacks.len() as i32);
|
||||
stacks.iter().for_each(|s| Template::write(s, w));
|
||||
}
|
||||
Self::WritableBook(pages) => {
|
||||
w.varint(pages.len() as i32);
|
||||
for page in pages {
|
||||
@@ -533,6 +544,21 @@ mod tests {
|
||||
round::<Option<Hashed>, HashedSlot>(None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn charged_projectiles_are_item_stack_templates() {
|
||||
// A crossbow (id 7) loaded with one arrow (id 9): ChargedProjectiles
|
||||
// is a list of ItemStackTemplate, the item before the count.
|
||||
let bytes = [1, 7, 1, 0, 51, 1, 9, 1, 0, 0];
|
||||
let stack = Slot::read(&mut Reader::new(&bytes)).unwrap().unwrap();
|
||||
assert_eq!(
|
||||
stack.component(component::CHARGED_PROJECTILES),
|
||||
Some(&Component::Stacks(vec![Stack::new(9, 1)]))
|
||||
);
|
||||
let mut w = Writer::new();
|
||||
Slot::write(&Some(stack), &mut w);
|
||||
assert_eq!(w.data, bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filtered_book_pages_are_read_and_dropped() {
|
||||
// A writable book with one page "hi" and its filtered "**".
|
||||
|
||||
@@ -461,6 +461,15 @@ packet!(OpenScreen { container: i32 => VarInt, menu: i32 => VarInt, title: Tag =
|
||||
// ClientboundMountScreenOpenPacket: a horse's or llama's inventory, with its
|
||||
// chest's columns (`getInventoryColumns`).
|
||||
packet!(MountScreenOpen { container: i32 => VarInt, columns: i32 => VarInt, entity: i32 => Int });
|
||||
packet!(
|
||||
/// ClientboundOpenSignEditorPacket: the sign whose side the player edits.
|
||||
OpenSignEditor { pos: [i32; 3] => Pos, front: bool => SignSide }
|
||||
);
|
||||
packet!(
|
||||
/// ClientboundOpenBookPacket: the hand (InteractionHand, 0 main, 1 off)
|
||||
/// whose written book the player reads.
|
||||
OpenBook { hand: i32 => VarInt }
|
||||
);
|
||||
packet!(PlayerAbilities { flags: i8 => Byte, flying_speed: f32 => Float, walking_speed: f32 => Float });
|
||||
/// MessageSignature.Packed: an index into the last-seen cache or a full signature.
|
||||
pub struct PackedSignature;
|
||||
@@ -1617,6 +1626,8 @@ packet_enum!(Clientbound, crate::ids::play::clientbound {
|
||||
MerchantOffers(MerchantOffers) = MERCHANT_OFFERS,
|
||||
MountScreenOpen(MountScreenOpen) = MOUNT_SCREEN_OPEN,
|
||||
OpenScreen(OpenScreen) = OPEN_SCREEN,
|
||||
OpenBook(OpenBook) = OPEN_BOOK,
|
||||
OpenSignEditor(OpenSignEditor) = OPEN_SIGN_EDITOR,
|
||||
Ping(Ping) = PING,
|
||||
PongResponse(crate::packets::status::Ping) = PONG_RESPONSE,
|
||||
PlayerAbilities(PlayerAbilities) = PLAYER_ABILITIES,
|
||||
@@ -1917,6 +1928,61 @@ packet!(
|
||||
/// ServerboundRenameItemPacket: `AnvilMenu.setItemName`.
|
||||
RenameItem { name: String => Str }
|
||||
);
|
||||
packet!(
|
||||
/// ServerboundSignUpdatePacket: the four lines the player wrote on a side.
|
||||
SignUpdate { pos: [i32; 3] => Pos, lines: [String; 4] => SignLines, front: bool => SignSide }
|
||||
);
|
||||
packet!(
|
||||
/// ServerboundEditBookPacket: the book's slot (a hotbar slot, or 40 for
|
||||
/// the off hand), its pages and, to sign it, its title.
|
||||
EditBook {
|
||||
slot: i32 => VarInt,
|
||||
pages: Vec<String> => BookPages,
|
||||
title: Option<String> => Opt<StrN<32>>,
|
||||
}
|
||||
);
|
||||
/// `stringUtf8(1024)` in `list(100)`: a book's pages.
|
||||
pub struct BookPages;
|
||||
impl Codec<Vec<String>> for BookPages {
|
||||
fn read(r: &mut Reader) -> Result<Vec<String>> {
|
||||
let n = r.varint()?;
|
||||
if !(0..=100).contains(&n) {
|
||||
return Err(Error::new(format!("{n} book pages, at most 100")));
|
||||
}
|
||||
(0..n).map(|_| r.string(1024)).collect()
|
||||
}
|
||||
fn write(v: &Vec<String>, w: &mut Writer) {
|
||||
List::<StrN<1024>>::write(v, w);
|
||||
}
|
||||
}
|
||||
/// SignTextSlot.STREAM_CODEC: VarInt 0 for the back, 1 for the front (any
|
||||
/// other id reads as the back, ByIdMap's ZERO strategy).
|
||||
pub struct SignSide;
|
||||
impl Codec<bool> for SignSide {
|
||||
fn read(r: &mut Reader) -> Result<bool> {
|
||||
Ok(r.varint()? == 1)
|
||||
}
|
||||
fn write(v: &bool, w: &mut Writer) {
|
||||
w.varint(i32::from(*v));
|
||||
}
|
||||
}
|
||||
/// `stringUtf8(384)` in `fixedSizeList(4)`: four lines, no count.
|
||||
pub struct SignLines;
|
||||
impl Codec<[String; 4]> for SignLines {
|
||||
fn read(r: &mut Reader) -> Result<[String; 4]> {
|
||||
Ok([
|
||||
r.string(384)?,
|
||||
r.string(384)?,
|
||||
r.string(384)?,
|
||||
r.string(384)?,
|
||||
])
|
||||
}
|
||||
fn write(v: &[String; 4], w: &mut Writer) {
|
||||
for line in v {
|
||||
w.string(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
packet!(
|
||||
/// ServerboundSetBeaconPacket: `BeaconMenu.updateEffects`, each power an
|
||||
/// optional `mob_effect` id.
|
||||
@@ -2000,6 +2066,7 @@ packet_enum!(Serverbound, crate::ids::play::serverbound {
|
||||
ContainerClick(ContainerClick) = CONTAINER_CLICK,
|
||||
ContainerClose(ContainerClose) = CONTAINER_CLOSE,
|
||||
CustomPayload(CustomPayload) = CUSTOM_PAYLOAD,
|
||||
EditBook(EditBook) = EDIT_BOOK,
|
||||
Interact(Interact) = INTERACT,
|
||||
KeepAlive(KeepAlive) = KEEP_ALIVE,
|
||||
MovePlayerPos(MovePlayerPos) = MOVE_PLAYER_POS,
|
||||
@@ -2024,6 +2091,7 @@ packet_enum!(Serverbound, crate::ids::play::serverbound {
|
||||
SetBeacon(SetBeacon) = SET_BEACON,
|
||||
SetCarriedItem(SetCarriedItem) = SET_CARRIED_ITEM,
|
||||
SetCreativeModeSlot(SetCreativeModeSlot) = SET_CREATIVE_MODE_SLOT,
|
||||
SignUpdate(SignUpdate) = SIGN_UPDATE,
|
||||
UseItem(UseItem) = USE_ITEM,
|
||||
UseItemOn(UseItemOn) = USE_ITEM_ON,
|
||||
});
|
||||
@@ -2748,4 +2816,48 @@ mod tests {
|
||||
});
|
||||
round_trip(p, Clientbound::encode, Clientbound::decode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_packets_match_vanilla_stream_codecs() {
|
||||
// ClientboundOpenSignEditorPacket: BlockPos, then SignTextSlot as a VarInt (FRONT is 1).
|
||||
let open = Clientbound::OpenSignEditor(OpenSignEditor {
|
||||
pos: [1, 2, 3],
|
||||
front: true,
|
||||
});
|
||||
let body = open.encode();
|
||||
assert_eq!(
|
||||
body[1..9],
|
||||
crate::buf::pack_block_pos([1, 2, 3]).to_be_bytes()
|
||||
);
|
||||
assert_eq!(body[9..], [1]);
|
||||
round_trip(open, Clientbound::encode, Clientbound::decode);
|
||||
// ServerboundSignUpdatePacket: BlockPos, four strings with no count, then the slot.
|
||||
let update = Serverbound::SignUpdate(SignUpdate {
|
||||
pos: [-4, 70, 9],
|
||||
lines: ["Hi".into(), String::new(), "x".into(), String::new()],
|
||||
front: false,
|
||||
});
|
||||
let body = update.encode();
|
||||
assert_eq!(body[9..], [2, b'H', b'i', 0, 1, b'x', 0, 0]);
|
||||
round_trip(update, Serverbound::encode, Serverbound::decode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn book_packets_match_vanilla_stream_codecs() {
|
||||
// ClientboundOpenBookPacket: InteractionHand as a VarInt.
|
||||
let open = Clientbound::OpenBook(OpenBook { hand: 1 });
|
||||
assert_eq!(open.encode()[1..], [1]);
|
||||
round_trip(open, Clientbound::encode, Clientbound::decode);
|
||||
// ServerboundEditBookPacket: the slot, the pages (a counted list), then an optional title.
|
||||
let sign = Serverbound::EditBook(EditBook {
|
||||
slot: 40,
|
||||
pages: vec!["a".into(), String::new()],
|
||||
title: Some("T".into()),
|
||||
});
|
||||
assert_eq!(sign.encode()[1..], [40, 2, 1, b'a', 0, 1, 1, b'T']);
|
||||
round_trip(sign, Serverbound::encode, Serverbound::decode);
|
||||
let mut w = Writer::new();
|
||||
w.varint(101);
|
||||
assert!(BookPages::read(&mut Reader::new(&w.data)).is_err());
|
||||
}
|
||||
}
|
||||
@@ -182,6 +182,22 @@ impl JavaPack {
|
||||
}
|
||||
}
|
||||
|
||||
impl JavaPack {
|
||||
/// The block ids of the pack's `block.properties` (empty without one),
|
||||
/// [`properties::block_ids`].
|
||||
pub fn block_ids(
|
||||
&self,
|
||||
values: &OptionValues,
|
||||
env: &Environment,
|
||||
) -> Result<BTreeMap<String, i32>, String> {
|
||||
match self.files.read("block.properties") {
|
||||
Some(text) => properties::block_ids(&text, env, &self.options, values)
|
||||
.map_err(|e| format!("block.properties: {e}")),
|
||||
None => Ok(BTreeMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A program's name in `shaders.properties` (`world-1/composite1.fsh` is
|
||||
/// `world-1/composite1`).
|
||||
pub fn program_name(program: &str) -> &str {
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
options::OptionValues,
|
||||
preprocess::Environment,
|
||||
properties::{Blend, LEGACY_TARGETS, ShaderProperties},
|
||||
translate::{Link, Translated},
|
||||
translate::{Geometry, Link, Translated},
|
||||
};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
@@ -327,8 +327,33 @@ pub struct Passes {
|
||||
/// next frame starts unflipped.
|
||||
pub flipped_at_end: BTreeSet<u32>,
|
||||
pub directives: Directives,
|
||||
/// The program Horizon's solid terrain is drawn with
|
||||
/// ([`TERRAIN_PROGRAMS`]), before `deferred`.
|
||||
pub terrain: Option<Pass>,
|
||||
/// The program its translucent terrain is drawn with
|
||||
/// ([`WATER_PROGRAMS`]), after `deferred`.
|
||||
pub water: Option<Pass>,
|
||||
}
|
||||
|
||||
/// The programs Iris draws solid terrain with, the first the pack has
|
||||
/// (`ProgramId.Terrain`'s fallbacks).
|
||||
pub const TERRAIN_PROGRAMS: &[&str] = &[
|
||||
"gbuffers_terrain",
|
||||
"gbuffers_textured_lit",
|
||||
"gbuffers_textured",
|
||||
"gbuffers_basic",
|
||||
];
|
||||
|
||||
/// The programs Iris draws translucent terrain with (`ProgramId.Water`'s
|
||||
/// fallbacks).
|
||||
pub const WATER_PROGRAMS: &[&str] = &[
|
||||
"gbuffers_water",
|
||||
"gbuffers_terrain",
|
||||
"gbuffers_textured_lit",
|
||||
"gbuffers_textured",
|
||||
"gbuffers_basic",
|
||||
];
|
||||
|
||||
/// The `const` directives of a program folder that aren't about targets
|
||||
/// (`PackDirectives`), read from the same programs in the same order.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
@@ -554,6 +579,25 @@ impl JavaPack {
|
||||
out.flipped_after_translucent = flipped.clone();
|
||||
out.composite = run(&groups[3], "composite_pre", &mut flipped)?;
|
||||
out.flipped_at_end = flipped;
|
||||
// Geometry writes the side the next passes read.
|
||||
let geometry = |chain: &[&str], flipped: &BTreeSet<u32>| {
|
||||
let Some(name) = chain.iter().find(|n| exists(n)) else {
|
||||
return Ok::<_, String>(None);
|
||||
};
|
||||
let text = fragment(name)?;
|
||||
let (blend, buffer_blends) = resolve_blends(&path(name));
|
||||
Ok(Some(Pass {
|
||||
program: path(name),
|
||||
draw_buffers: draw_buffers(&text).map_err(|e| format!("{}: {e}", path(name)))?,
|
||||
reads_alt: flipped.clone(),
|
||||
mipmapped: BTreeSet::new(),
|
||||
scale: 1.0,
|
||||
blend,
|
||||
buffer_blends,
|
||||
}))
|
||||
};
|
||||
out.terrain = geometry(TERRAIN_PROGRAMS, &out.flipped_after_prepare)?;
|
||||
out.water = geometry(WATER_PROGRAMS, &out.flipped_after_translucent)?;
|
||||
if exists("final") {
|
||||
let prog = path("final");
|
||||
let (blend, buffer_blends) = resolve_blends(&prog);
|
||||
@@ -614,6 +658,70 @@ impl JavaPack {
|
||||
}
|
||||
}
|
||||
|
||||
/// A geometry program translated: the program (its vertex with the first
|
||||
/// glue), and its vertex with each other glue.
|
||||
pub struct GeometryProgram {
|
||||
pub program: Program,
|
||||
pub variants: Vec<Translated>,
|
||||
}
|
||||
|
||||
impl JavaPack {
|
||||
/// Translate geometry program `pass` (`gbuffers_*`) to run over
|
||||
/// Horizon's geometry: its bindings in group 2, its attributes from
|
||||
/// each of `glues` (the vertex program is translated once per glue).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn compile_geometry(
|
||||
&self,
|
||||
pass: &Pass,
|
||||
targets: &[TargetSettings],
|
||||
values: &OptionValues,
|
||||
env: &Environment,
|
||||
properties: &ShaderProperties,
|
||||
glues: &[Geometry],
|
||||
) -> Result<GeometryProgram, String> {
|
||||
let (first, rest) = glues
|
||||
.split_first()
|
||||
.ok_or("a geometry program needs a glue")?;
|
||||
let mut link = Link {
|
||||
image_formats: properties.image_formats(),
|
||||
shared_samplers: shared_samplers(targets),
|
||||
set: 2,
|
||||
geometry: Some(first.clone()),
|
||||
..Link::default()
|
||||
};
|
||||
let vertex = self.translate(&format!("{}.vsh", pass.program), values, env, &mut link)?;
|
||||
let fragment = self.translate(&format!("{}.fsh", pass.program), values, env, &mut link)?;
|
||||
let mut variants = Vec::new();
|
||||
for glue in rest {
|
||||
let mut other = link.clone();
|
||||
other.geometry = Some(glue.clone());
|
||||
variants.push(self.translate(
|
||||
&format!("{}.vsh", pass.program),
|
||||
values,
|
||||
env,
|
||||
&mut other,
|
||||
)?);
|
||||
if other.uniforms != link.uniforms || other.bindings != link.bindings {
|
||||
return Err(format!("{}: its variants bind differently", pass.program));
|
||||
}
|
||||
}
|
||||
let draw_buffers = match &pass.draw_buffers {
|
||||
Some(buffers) => buffers.clone(),
|
||||
None => (0..fragment.interface.outputs.keys().max().map_or(1, |m| m + 1)).collect(),
|
||||
};
|
||||
Ok(GeometryProgram {
|
||||
program: Program {
|
||||
pass: pass.clone(),
|
||||
vertex,
|
||||
fragment,
|
||||
draw_buffers,
|
||||
link,
|
||||
},
|
||||
variants,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Iris's sampler states for the textures it binds: colour targets
|
||||
/// linear (nearest for integer formats, and for 32-bit floats, which wgpu
|
||||
/// can't filter everywhere), depth nearest, noise repeating. For
|
||||
|
||||
@@ -639,6 +639,58 @@ impl ShaderProperties {
|
||||
}
|
||||
}
|
||||
|
||||
/// `block.properties` (`block.<id>=<blocks>`), preprocessed as
|
||||
/// `shaders.properties` is: each block's id by its name without namespace
|
||||
/// (`oak_leaves`). A block listed with a state (`grass_block:snowy=true`)
|
||||
/// takes that id only when no entry names it without one; numeric ids
|
||||
/// (before 1.13) are skipped.
|
||||
pub fn block_ids(
|
||||
text: &str,
|
||||
env: &Environment,
|
||||
options: &OptionSet,
|
||||
values: &OptionValues,
|
||||
) -> Result<BTreeMap<String, i32>, String> {
|
||||
let properties = Properties::parse(&preprocess(text, env, options, values)?);
|
||||
let mut plain = BTreeMap::new();
|
||||
let mut with_state = BTreeMap::new();
|
||||
for (key, value) in properties.iter() {
|
||||
let Some(Ok(id)) = key.strip_prefix("block.").map(str::parse::<i32>) else {
|
||||
continue;
|
||||
};
|
||||
for entry in value.split_whitespace() {
|
||||
let entry = entry.strip_prefix("minecraft:").unwrap_or(entry);
|
||||
if entry.contains(':') && entry.split(':').nth(1).is_some_and(|s| s.contains('=')) {
|
||||
let name = entry.split(':').next().unwrap_or(entry);
|
||||
with_state.entry(name.to_owned()).or_insert(id);
|
||||
} else if !entry.chars().all(|c| c.is_ascii_digit()) && !entry.contains('=') {
|
||||
plain.insert(entry.to_owned(), id);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (name, id) in with_state {
|
||||
plain.entry(name).or_insert(id);
|
||||
}
|
||||
Ok(plain)
|
||||
}
|
||||
|
||||
/// The block a block sprite (`minecraft:block/oak_log_top`) belongs to in
|
||||
/// `ids` ([`block_ids`]): the block named like it, else the longest one its
|
||||
/// name starts with followed by `_` (`oak_log`, `water` for `water_still`).
|
||||
pub fn sprite_block_id(sprite: &str, ids: &BTreeMap<String, i32>) -> Option<i32> {
|
||||
let name = sprite.rsplit(['/', ':']).next()?;
|
||||
if let Some(&id) = ids.get(name) {
|
||||
return Some(id);
|
||||
}
|
||||
let mut end = name.len();
|
||||
while let Some(cut) = name[..end].rfind('_') {
|
||||
if let Some(&id) = ids.get(&name[..cut]) {
|
||||
return Some(id);
|
||||
}
|
||||
end = cut;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Iris's `BooleanParser`: options joined by `!`, `&&`, `||` and brackets,
|
||||
/// `&&` binding tighter. `true`/`1` and `false`/`0` are literals; a name that
|
||||
/// isn't a boolean option is true; a malformed expression is true.
|
||||
@@ -757,6 +809,42 @@ mod tests {
|
||||
set
|
||||
}
|
||||
|
||||
/// `block.properties` as Chocapic13' writes it, and sprites matched to
|
||||
/// its blocks.
|
||||
#[test]
|
||||
fn block_ids_and_the_sprites_of_their_blocks() {
|
||||
let text = "\
|
||||
#if MC_VERSION >= 11300
|
||||
block.10001= minecraft:wheat minecraft:short_grass
|
||||
block.10003 = minecraft:oak_leaves \\
|
||||
minecraft:vine
|
||||
block.10006 = minecraft:grass_block:snowy=true minecraft:snow
|
||||
block.8 = minecraft:water
|
||||
block.2 = minecraft:grass_block
|
||||
#else
|
||||
block.10001= 6 31
|
||||
#endif
|
||||
layer.translucent=minecraft:glass
|
||||
";
|
||||
let env = Environment {
|
||||
defines: vec![("MC_VERSION".into(), "260300".into())],
|
||||
};
|
||||
let ids = block_ids(text, &env, &OptionSet::default(), &OptionValues::default()).unwrap();
|
||||
assert_eq!(ids.get("vine"), Some(&10003));
|
||||
// The stateless entry wins over the snowy one.
|
||||
assert_eq!(ids.get("grass_block"), Some(&2));
|
||||
assert!(!ids.contains_key("6") && !ids.contains_key("glass"));
|
||||
for (sprite, id) in [
|
||||
("minecraft:block/oak_leaves", Some(10003)),
|
||||
("minecraft:block/water_still", Some(8)),
|
||||
("minecraft:block/grass_block_top", Some(2)),
|
||||
("minecraft:block/wheat_stage7", Some(10001)),
|
||||
("minecraft:block/stone", None),
|
||||
] {
|
||||
assert_eq!(sprite_block_id(sprite, &ids), id, "{sprite}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_properties_syntax() {
|
||||
let p = Properties::parse(
|
||||
|
||||
@@ -45,7 +45,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
|
||||
/// Locations, bindings and uniforms shared by the programs of one pass: a
|
||||
/// pass's vertex program is translated first, its fragment program second.
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct Link {
|
||||
/// Varying name → location (split matrices and arrays as `name#i`).
|
||||
pub varyings: BTreeMap<String, u32>,
|
||||
@@ -64,6 +64,27 @@ pub struct Link {
|
||||
/// 16). Once any is named, comparison samplers share `iris_compare` and
|
||||
/// integer ones `iris_nearest`; textures not named keep `{name}_s`.
|
||||
pub shared_samplers: BTreeMap<String, String>,
|
||||
/// The bind group of the pack's bindings (0; 2 for geometry programs,
|
||||
/// whose first groups are Horizon's).
|
||||
pub set: u32,
|
||||
/// Set for `gbuffers_*` programs drawn over Horizon's own geometry.
|
||||
pub geometry: Option<Geometry>,
|
||||
}
|
||||
|
||||
/// How a `gbuffers_*` program runs over Horizon's geometry: its vertex
|
||||
/// attributes aren't inputs but globals the glue fills from Horizon's
|
||||
/// vertices before the program's `main`.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Geometry {
|
||||
/// GLSL put before the vertex program: Horizon's vertex inputs and
|
||||
/// buffers, and a `void hz_vertex()` run first in `main`.
|
||||
pub vertex_glue: String,
|
||||
/// Each attribute's value as a `vec4` expression of the glue
|
||||
/// (`iris_Vertex`, `mc_Entity`, `vaUV0`...). Others are zero.
|
||||
pub attributes: BTreeMap<String, String>,
|
||||
/// Iris's alpha test: the fragment is discarded unless output 0's
|
||||
/// alpha is above it.
|
||||
pub alpha_test: Option<f32>,
|
||||
}
|
||||
|
||||
impl Link {
|
||||
@@ -385,6 +406,7 @@ pub fn translate(
|
||||
.parse(&options, &glsl)
|
||||
.map_err(|e| first_lines(&e.emit_to_string(&glsl)))?;
|
||||
unpad_private_arrays(&mut module);
|
||||
end_returning_cases(&mut module);
|
||||
let info = naga::valid::Validator::new(
|
||||
naga::valid::ValidationFlags::all(),
|
||||
naga::valid::Capabilities::all(),
|
||||
@@ -403,6 +425,49 @@ pub fn translate(
|
||||
})
|
||||
}
|
||||
|
||||
/// A `case` that ends with `return` or `discard` rather than `break`
|
||||
/// (Photon's displacement) is a fall-through one to naga's GLSL front end,
|
||||
/// which its WGSL writer refuses: nothing falls through it.
|
||||
fn end_returning_cases(module: &mut naga::Module) {
|
||||
fn block(b: &mut naga::Block) {
|
||||
for s in b.iter_mut() {
|
||||
match s {
|
||||
naga::Statement::Block(inner) => block(inner),
|
||||
naga::Statement::If { accept, reject, .. } => {
|
||||
block(accept);
|
||||
block(reject);
|
||||
}
|
||||
naga::Statement::Loop {
|
||||
body, continuing, ..
|
||||
} => {
|
||||
block(body);
|
||||
block(continuing);
|
||||
}
|
||||
naga::Statement::Switch { cases, .. } => {
|
||||
for case in cases {
|
||||
block(&mut case.body);
|
||||
if case.fall_through
|
||||
&& matches!(
|
||||
case.body.last(),
|
||||
Some(naga::Statement::Return { .. } | naga::Statement::Kill)
|
||||
)
|
||||
{
|
||||
case.fall_through = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (_, f) in module.functions.iter_mut() {
|
||||
block(&mut f.body);
|
||||
}
|
||||
for e in &mut module.entry_points {
|
||||
block(&mut e.function.body);
|
||||
}
|
||||
}
|
||||
|
||||
/// Built-in functions naga folds in `const` initialisers.
|
||||
const FOLDABLE: &[&str] = &[
|
||||
"radians",
|
||||
@@ -634,6 +699,8 @@ struct Legaliser<'a> {
|
||||
mat2_ctor: bool,
|
||||
/// Constants turned into globals ([`needs_runtime`]).
|
||||
runtime_constants: HashSet<String>,
|
||||
/// Geometry programs: each attribute's type, for the glue.
|
||||
attribute_types: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
fn var(name: &str) -> ast::Expr {
|
||||
@@ -952,6 +1019,7 @@ impl<'a> Legaliser<'a> {
|
||||
gather: false,
|
||||
mat2_ctor: false,
|
||||
runtime_constants: HashSet::new(),
|
||||
attribute_types: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1043,7 +1111,8 @@ impl<'a> Legaliser<'a> {
|
||||
show_declaration(&mut text, d, &mut FormattingState::default())
|
||||
.map_err(|e| e.to_string())?;
|
||||
self.header.push(format!(
|
||||
"layout({kind}, set = 0, binding = {binding}) {}",
|
||||
"layout({kind}, set = {}, binding = {binding}) {}",
|
||||
self.link.set,
|
||||
text.trim()
|
||||
));
|
||||
Ok(false)
|
||||
@@ -1099,7 +1168,8 @@ impl<'a> Legaliser<'a> {
|
||||
for (name, _) in &names {
|
||||
let t = self.link.binding(&format!("{name}_t"));
|
||||
self.header.push(format!(
|
||||
"layout(set = 0, binding = {t}) uniform {texture} {name}_t;"
|
||||
"layout(set = {}, binding = {t}) uniform {texture} {name}_t;",
|
||||
self.link.set
|
||||
));
|
||||
let shared = &self.link.shared_samplers;
|
||||
let var = if shared.is_empty() {
|
||||
@@ -1117,7 +1187,8 @@ impl<'a> Legaliser<'a> {
|
||||
if self.sampler_vars.insert(var.clone()) {
|
||||
let s = self.link.binding(&var);
|
||||
self.header.push(format!(
|
||||
"layout(set = 0, binding = {s}) uniform {sampler} {var};"
|
||||
"layout(set = {}, binding = {s}) uniform {sampler} {var};",
|
||||
self.link.set
|
||||
));
|
||||
}
|
||||
self.interface
|
||||
@@ -1156,7 +1227,8 @@ impl<'a> Legaliser<'a> {
|
||||
.unwrap_or_else(|| "rgba8".to_owned());
|
||||
let binding = self.link.binding(name);
|
||||
layout.push(format!(
|
||||
"layout({f}, set = 0, binding = {binding}) {memory}uniform {ty} {name};"
|
||||
"layout({f}, set = {}, binding = {binding}) {memory}uniform {ty} {name};",
|
||||
self.link.set
|
||||
));
|
||||
self.interface.images.push((name.clone(), f));
|
||||
}
|
||||
@@ -1199,10 +1271,8 @@ impl<'a> Legaliser<'a> {
|
||||
for (i, (name, array)) in names.iter().enumerate() {
|
||||
let full_array = format!("{ty_array}{array}");
|
||||
if input {
|
||||
let at = Link::location(&mut self.link.attributes, name);
|
||||
self.header.push(format!(
|
||||
"layout(location = {at}) in {ty} {name}{full_array};"
|
||||
));
|
||||
let decl = self.attribute(&ty, name, &full_array);
|
||||
self.header.push(decl);
|
||||
self.interface.attributes.insert(name.clone());
|
||||
} else if !varying {
|
||||
let at = explicit
|
||||
@@ -1670,9 +1740,8 @@ impl<'a> Legaliser<'a> {
|
||||
let ty = LEGACY_ATTRIBUTES.iter().find(|(a, _)| *a == n).unwrap().1;
|
||||
let renamed = n.replacen("gl_", "iris_", 1);
|
||||
if self.interface.attributes.insert(renamed.clone()) {
|
||||
let at = Link::location(&mut self.link.attributes, &renamed);
|
||||
self.header
|
||||
.push(format!("layout(location = {at}) in {ty} {renamed};"));
|
||||
let decl = self.attribute(ty, &renamed, "");
|
||||
self.header.push(decl);
|
||||
}
|
||||
renamed
|
||||
}
|
||||
@@ -1694,6 +1763,21 @@ impl<'a> Legaliser<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A vertex attribute's declaration: an input, or for geometry programs
|
||||
/// a global the glue fills ([`Geometry`]).
|
||||
fn attribute(&mut self, ty: &str, name: &str, array: &str) -> String {
|
||||
match &self.link.geometry {
|
||||
Some(_) => {
|
||||
self.attribute_types.insert(name.to_owned(), ty.to_owned());
|
||||
format!("{ty} {name}{array};")
|
||||
}
|
||||
None => {
|
||||
let at = Link::location(&mut self.link.attributes, name);
|
||||
format!("layout(location = {at}) in {ty} {name}{array};")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn frag_data(&mut self, n: u32) -> ast::Expr {
|
||||
let name = format!("iris_FragData{n}");
|
||||
if self.interface.outputs.insert(n, name.clone()).is_none() {
|
||||
@@ -1959,9 +2043,8 @@ impl<'a> Legaliser<'a> {
|
||||
continue;
|
||||
}
|
||||
if attribute && self.stage == Stage::Vertex {
|
||||
let at = Link::location(&mut self.link.attributes, name);
|
||||
self.header
|
||||
.push(format!("layout(location = {at}) in {ty} {name};"));
|
||||
let decl = self.attribute(ty, name, "");
|
||||
self.header.push(decl);
|
||||
self.interface.attributes.insert(name.to_owned());
|
||||
} else if !attribute {
|
||||
self.link.uniform(name, format!("{ty} {name}"))?;
|
||||
@@ -1971,6 +2054,42 @@ impl<'a> Legaliser<'a> {
|
||||
if !self.errors.is_empty() {
|
||||
return Err(self.errors.join("\n"));
|
||||
}
|
||||
if let Some(geometry) = &self.link.geometry {
|
||||
match self.stage {
|
||||
Stage::Vertex => {
|
||||
// The glue first, then the attributes from it, before
|
||||
// the global initialisers (which may read them).
|
||||
let mut first = vec!["hz_vertex();".to_owned()];
|
||||
for (name, ty) in &self.attribute_types {
|
||||
first.push(match geometry.attributes.get(name) {
|
||||
Some(value) => format!("{name} = {ty}({value});"),
|
||||
None => format!("{name} = {ty}(0);"),
|
||||
});
|
||||
}
|
||||
self.prologue.splice(0..0, first);
|
||||
}
|
||||
Stage::Fragment => {
|
||||
if let (Some(reference), Some(name)) =
|
||||
(geometry.alpha_test, self.interface.outputs.get(&0))
|
||||
{
|
||||
let written = format!("iris_out_{name}");
|
||||
let name = if self.declared.contains(&written)
|
||||
|| self
|
||||
.header
|
||||
.iter()
|
||||
.any(|h| h.ends_with(&format!(" {written};")))
|
||||
{
|
||||
written
|
||||
} else {
|
||||
name.clone()
|
||||
};
|
||||
self.epilogue
|
||||
.push(format!("if (!({name}.a > {reference:?})) discard;"));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if self.stage == Stage::Vertex {
|
||||
// GL's clip space to wgpu's: y flipped, so texture row 0 stays
|
||||
// the bottom of the picture as in GL (texture coordinates and
|
||||
@@ -1982,6 +2101,10 @@ impl<'a> Legaliser<'a> {
|
||||
}
|
||||
let mut out = String::from("#version 450 core\n");
|
||||
out.push_str(HELPERS);
|
||||
if let (Some(geometry), Stage::Vertex) = (&self.link.geometry, self.stage) {
|
||||
out.push_str(&geometry.vertex_glue);
|
||||
out.push('\n');
|
||||
}
|
||||
if self.modulo {
|
||||
out.push_str(&modulo_helpers());
|
||||
}
|
||||
@@ -1995,7 +2118,10 @@ impl<'a> Legaliser<'a> {
|
||||
out.push_str(MAT2_HELPERS);
|
||||
}
|
||||
if !self.link.uniforms.is_empty() {
|
||||
out.push_str("layout(std140, set = 0, binding = 0) uniform IrisUniforms {\n");
|
||||
out.push_str(&format!(
|
||||
"layout(std140, set = {}, binding = 0) uniform IrisUniforms {{\n",
|
||||
self.link.set
|
||||
));
|
||||
for (_, decl) in &self.link.uniforms {
|
||||
out.push_str(&format!(" {decl};\n"));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use horizon_shaderpack::{
|
||||
include::Inlined,
|
||||
preprocess::{Environment, preprocess},
|
||||
program_name,
|
||||
translate::{Link, Translated, translate},
|
||||
translate::{Geometry, Link, Translated, translate},
|
||||
};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
@@ -440,6 +440,95 @@ void main() {{
|
||||
);
|
||||
}
|
||||
|
||||
/// A `gbuffers_*` program over Horizon's geometry: its attributes are
|
||||
/// globals the glue fills before `main`, its bindings in group 2, and its
|
||||
/// fragment discards below Iris's alpha test. A `case` ending in `return`
|
||||
/// (Photon's displacement) isn't a fall-through for naga.
|
||||
#[test]
|
||||
fn geometry_programs_take_their_attributes_from_the_glue() {
|
||||
let glue = Geometry {
|
||||
vertex_glue: "layout(location = 0) in vec3 hz_position;
|
||||
layout(std430, set = 3, binding = 0) readonly buffer HzTable { int hz_table[]; };
|
||||
vec4 hz_pos;
|
||||
void hz_vertex() { hz_pos = vec4(hz_position, 1.0) + float(hz_table[0]); }"
|
||||
.into(),
|
||||
attributes: [
|
||||
("iris_Vertex".to_owned(), "hz_pos".to_owned()),
|
||||
(
|
||||
"mc_Entity".to_owned(),
|
||||
"vec4(8.0, -1.0, 0.0, 0.0)".to_owned(),
|
||||
),
|
||||
]
|
||||
.into(),
|
||||
alpha_test: Some(0.1),
|
||||
};
|
||||
let mut link = Link {
|
||||
set: 2,
|
||||
geometry: Some(glue),
|
||||
..Link::default()
|
||||
};
|
||||
let vs = run(
|
||||
Stage::Vertex,
|
||||
"uniform sampler2D noisetex;
|
||||
attribute vec4 mc_Entity;
|
||||
attribute vec2 mc_midTexCoord;
|
||||
varying vec2 uv;
|
||||
vec3 sway(vec3 p, int id) {
|
||||
switch (id) {
|
||||
case 8:
|
||||
return p + 1.0;
|
||||
case 9:
|
||||
case 10:
|
||||
return p - 1.0;
|
||||
default:
|
||||
return p;
|
||||
}
|
||||
}
|
||||
void main() {
|
||||
uv = gl_MultiTexCoord0.xy + mc_midTexCoord;
|
||||
gl_Position = gl_ModelViewProjectionMatrix * vec4(sway(gl_Vertex.xyz, int(mc_Entity.x)), 1.0);
|
||||
}
|
||||
",
|
||||
&mut link,
|
||||
)
|
||||
.unwrap();
|
||||
for wanted in [
|
||||
"hz_vertex();",
|
||||
"mc_Entity = vec4(vec4(8.0, -1.0, 0.0, 0.0));",
|
||||
"iris_Vertex = vec4(hz_pos);",
|
||||
"mc_midTexCoord = vec2(0);",
|
||||
"set = 2, binding = 0) uniform IrisUniforms",
|
||||
] {
|
||||
assert!(vs.glsl.contains(wanted), "{wanted}\n{}", vs.glsl);
|
||||
}
|
||||
// Horizon's vertex input is the only one.
|
||||
assert_eq!(
|
||||
io(&vs, false).into_keys().collect::<Vec<_>>(),
|
||||
["hz_position"]
|
||||
);
|
||||
reparse(&vs.wgsl).unwrap();
|
||||
let fs = run(
|
||||
Stage::Fragment,
|
||||
"uniform sampler2D gtexture;
|
||||
varying vec2 uv;
|
||||
void main() {
|
||||
/* DRAWBUFFERS:02 */
|
||||
gl_FragData[0] = texture2D(gtexture, uv);
|
||||
gl_FragData[1] = vec4(1.0);
|
||||
}
|
||||
",
|
||||
&mut link,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
fs.glsl.contains("if (!(iris_FragData0.a > 0.1)) discard;"),
|
||||
"{}",
|
||||
fs.glsl
|
||||
);
|
||||
assert!(fs.glsl.contains("set = 2, binding = "), "{}", fs.glsl);
|
||||
reparse(&fs.wgsl).unwrap();
|
||||
}
|
||||
|
||||
/// A fragment output narrower than its four-channel target (Steadfast's
|
||||
/// `final` writes `out vec3 color`) is written through a `vec4` with alpha 1:
|
||||
/// GL fills the missing channels, wgpu refuses the pipeline.
|
||||
|
||||
@@ -266,8 +266,8 @@ fn is_virtual(block: usize) -> bool {
|
||||
VIRTUAL.contains(&block)
|
||||
}
|
||||
|
||||
/// `getSideMask`.
|
||||
fn side_mask(west: bool, east: bool, north: bool, south: bool) -> i32 {
|
||||
/// `getSideMask` (LeavesFix has the same copy).
|
||||
pub(super) fn side_mask(west: bool, east: bool, north: bool, south: bool) -> i32 {
|
||||
let mut s = 0;
|
||||
if north {
|
||||
if east {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//! emulated here (`IntOpenHashSet` and the key order of an `Int2ObjectOpenHashMap`).
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use super::chunk_flatten::side_mask;
|
||||
use super::dynamic::{as_int, as_long_stream, as_str, ceillog2, set};
|
||||
use crate::nbt::Tag;
|
||||
|
||||
@@ -355,32 +356,6 @@ fn get_index(x: i32, y: i32, z: i32) -> i32 {
|
||||
y << 8 | z << 4 | x
|
||||
}
|
||||
|
||||
fn side_mask(west: bool, east: bool, north: bool, south: bool) -> i32 {
|
||||
let mut s = 0;
|
||||
if north {
|
||||
if east {
|
||||
s |= 2;
|
||||
} else if west {
|
||||
s |= 128;
|
||||
} else {
|
||||
s |= 1;
|
||||
}
|
||||
} else if south {
|
||||
if west {
|
||||
s |= 32;
|
||||
} else if east {
|
||||
s |= 8;
|
||||
} else {
|
||||
s |= 16;
|
||||
}
|
||||
} else if east {
|
||||
s |= 4;
|
||||
} else if west {
|
||||
s |= 64;
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// `LeavesFix` over a chunk (`{Level: ...}`).
|
||||
pub(super) fn leaves_fix(chunk: &mut Tag) {
|
||||
let Some(level) = chunk.get_mut("Level") else {
|
||||
|
||||
+39
-16
@@ -24,7 +24,8 @@ Both are off by default. With no pack selected the renderer draws exactly what i
|
||||
| Java shader packs: full-screen pass plan (`horizon_shaderpack::pipeline`: `DRAWBUFFERS`/`RENDERTARGETS`, `colortexN` formats and clearing, buffer flips, `blend.*`) | done: planned for the three test packs |
|
||||
| Java shader packs: full-screen passes drawn (`java_pack.rs`, `gfx/java_stages.rs`: `begin`, `prepare`, `deferred*`, `composite*`, `final` over Horizon's world) | first slice: BSL and Complementary run (below); `depthtex1`/`depthtex2` before translucent geometry, `blend.*`, PNG `texture.<stage>.*` and `texture.noise` |
|
||||
| Java shader packs: custom uniforms (`horizon_shaderpack::uniforms`), `sunPathRotation`, `clouds`/`sun`/`moon`, eye light, `wetness` | done for the full-screen passes (below) |
|
||||
| Java shader packs: `gbuffers_*`, `shadow`, `.dat` and resource-location textures, images, compute | planned |
|
||||
| Java shader packs: `gbuffers_terrain`, `gbuffers_water` (`gfx/java_terrain.rs`: Horizon's terrain fed to the pack's programs) | first slice: MakeUp Ultra Fast, Chocapic13' and BSL draw their own terrain and water (below) |
|
||||
| Java shader packs: other `gbuffers_*`, `shadow`, `.dat` and resource-location textures, images, compute | planned |
|
||||
|
||||
## How the renderer draws a frame today
|
||||
|
||||
@@ -396,6 +397,25 @@ Picking one translates every planned pass of each dimension folder (`java_pack.r
|
||||
4. `final` draws into a target that replaces the world's picture, where Horizon packs' `composite` output goes: the
|
||||
hand, menus and HUD are drawn over it.
|
||||
|
||||
When the pack has a `gbuffers_terrain` (or `textured_lit`, `textured`, `basic`, the program Iris falls back to), the
|
||||
pack draws the terrain itself (`gfx/java_terrain.rs`): Horizon's world pass skips its own solid and translucent
|
||||
terrain, then
|
||||
|
||||
- at step 2, after Horizon's sky, entities and the rest enter `colortex0` and depth, the pack's terrain program draws
|
||||
every visible section into its `RENDERTARGETS` (the side the next pass reads), tested against that depth; the
|
||||
depth is then copied into `depthtex0` and `depthtex1`;
|
||||
- before the first `composite`, as Iris does, `gbuffers_water` (or the terrain program) draws the translucent
|
||||
layer with the pack's `blend.*` or vanilla's blending; the depth goes into `depthtex0` only.
|
||||
|
||||
The pack's vertex program reads Horizon's own terrain vertices (the arena pages, instanced or flat draws): a glue
|
||||
function written before `main` decodes them into Iris's attributes (`vaPosition`/`gl_Vertex` relative to the camera,
|
||||
`gl_Color` with vanilla's face shading undone, `gl_MultiTexCoord0/1`, the normal and `at_tangent` from the quad's
|
||||
four corners, `mc_midTexCoord`, `at_midBlock`), and `mc_Entity.x` is the block's id from the pack's
|
||||
`block.properties`, matched to the sprite's block by name. `gtexture`/`texture` is a 2D atlas built from Horizon's
|
||||
sprite arrays (animated sprites copied in each frame), `lightmap` is Horizon's lightmap with Iris's texture matrix,
|
||||
and fragments under alpha 0.1 are discarded (`alphaTestRef`). `RGBA16` targets are 16-bit unorm where the GPU can
|
||||
render them (Chocapic13' packs its G-buffer into them; half floats broke it).
|
||||
|
||||
What makes Horizon look like OpenGL to the pack:
|
||||
|
||||
- **Layout.** The translator ends every vertex shader by flipping `gl_Position.y` and moving depth from `[-w, w]` to
|
||||
@@ -426,12 +446,15 @@ What makes Horizon look like OpenGL to the pack:
|
||||
|
||||
Measured on the Mac mini (M4 Pro) with the packs' default options: BSL v10.1.8 and Complementary Reimagined r5.9.3
|
||||
grade and light Horizon's world as their post-processing would. Sildur's Vibrant v2.02 draws only its sky: its
|
||||
`deferred` reads the albedo `gbuffers_*` write to `colortex4`. Without `gbuffers_*` and `shadow`, no pack has its
|
||||
shadows, water or sky look yet; Distant Horizons' and Voxy's depth read as far. Horizon's world goes into the target
|
||||
the pack's `gbuffers_terrain` (or the program Iris falls back to) draws to first: `colortex0` for most packs,
|
||||
`colortex1` for MakeUp. Not supported yet: images and compute (`.csh`; a pack that needs them doesn't load),
|
||||
mipmapped targets (`colortexNMipmapEnabled` is read but ignored), `.dat` and resource-location custom textures (PNG
|
||||
files in the pack only); shadow matrices are identity.
|
||||
`deferred` reads the albedo `gbuffers_*` write to `colortex4` (not checked again since the terrain programs run).
|
||||
Without the other `gbuffers_*` and `shadow`, no pack has its shadows or sky look yet; Distant Horizons' and Voxy's
|
||||
depth read as far. Horizon's world goes into the target the pack's `gbuffers_terrain` (or the program Iris falls back
|
||||
to) draws to first: `colortex0` for most packs, `colortex1` for MakeUp. Not supported yet: images and compute (`.csh`;
|
||||
a pack that needs them doesn't load), mipmapped targets (`colortexNMipmapEnabled` is read but ignored), `.dat` and
|
||||
resource-location custom textures (PNG files in the pack only); shadow matrices are identity. With the pack's terrain:
|
||||
entities, the sky, the hand, weather and clouds are still Horizon's (in that first target only), GPU occlusion culling
|
||||
is off, block ids come from sprite names rather than block states, distant (LOD) terrain doesn't repeat its textures,
|
||||
and only the overworld has been checked.
|
||||
|
||||
### Compatibility
|
||||
|
||||
@@ -439,19 +462,19 @@ Tested on 2026-10-22 on the Mac mini (M4 Pro, Metal) with each pack's default op
|
||||
the authors' pages (dev-only test inputs, kept outside the repository). "Loads" means every pass the pack enables for
|
||||
the overworld, Nether and End is preprocessed, translated and validated, as the game does when the pack is picked
|
||||
(`java_pack::tests::real_packs_load` with `HORIZON_TEST_SHADERPACKS`); "drawn" means it was also picked in the game
|
||||
and screenshotted. Only full-screen passes run (above), so no pack has its own terrain, water, shadows or sky yet.
|
||||
The MakeUp, Chocapic13' and Photon rows are from 2026-10-23, on Metal and on Vulkan (RTX 4080).
|
||||
and screenshotted. Full-screen passes, `gbuffers_terrain` and `gbuffers_water` run (above); no pack has its own
|
||||
shadows, entities or sky yet.
|
||||
|
||||
| Pack | Version | Result | First thing that stops it |
|
||||
| --- | --- | --- | --- |
|
||||
| BSL | v10.1.8 | drawn: 26 passes | |
|
||||
| BSL | v10.1.8 | drawn: 26 passes and its terrain and water programs | |
|
||||
| Complementary Reimagined | r5.9.3 | drawn: 27 passes | |
|
||||
| Complementary Unbound | r5.8 | loads: 27 passes | |
|
||||
| Sildur's Vibrant Extreme | 2.02 | drawn: 18 passes, only its sky (above) | |
|
||||
| Steadfast | 0.8.0 | drawn: 3 passes | |
|
||||
| MakeUp Ultra Fast | 9.6 | drawn: 6 passes, its fog, light shafts and grade over Horizon's world | |
|
||||
| Chocapic13' | V9 High | drawn: 16 passes; its default TAA upscaling shrinks the world in its `gbuffers_*` vertex programs, which don't run, so the picture is magnified 1/0.7: turn TAA Upscaling off in its settings | |
|
||||
| Photon | v1.4 | loads: 27 passes; draws black: it lights the scene from the G-buffer its `gbuffers_*` write | |
|
||||
| Chocapic13' | V9 High | drawn: 16 passes and its terrain and water programs | |
|
||||
| MakeUp Ultra Fast | 9.6 | drawn: 6 passes and its terrain and water programs | |
|
||||
| Photon | v1.4 | loads: 27 passes and its terrain and water programs; draws black | its sky and light come from a compute pass and `.dat` look-up tables, not loaded yet |
|
||||
| Bliss | 2.1.2 | fails | `world0/composite1.fsh`: a vector component passed to an `inout` parameter (WGSL can't point at one) |
|
||||
| Bloop | 1.8.0-Alpha-3 | fails | `world0/prepare1.fsh`: `mat2(vec4)` and other constructors naga's GLSL front end refuses |
|
||||
| Noble | v1.9.9 | fails (Apple and NVIDIA) | `world0/deferred4.vsh`: a `vec3[9][9]` varying (nested arrays between stages) |
|
||||
@@ -492,9 +515,9 @@ What the translator does for these packs, as Iris (JCPP and its GLSL transformer
|
||||
3. **Full-screen programs**: `deferred*`, `composite*`, `final` with `colortex0..15` (formats from the pack),
|
||||
`depthtex0..2`, `noisetex` and the uniforms (most are already computed for the world shaders). This alone runs
|
||||
Sildur's and BSL post-processing over Horizon's own world.
|
||||
4. **`gbuffers_terrain`, `gbuffers_water`, `gbuffers_sky*`**: Horizon's terrain vertices extended with Iris's
|
||||
attributes (`mc_Entity` from `block.properties`, `mc_midTexCoord`, `at_tangent`, lightmap), a second set of
|
||||
terrain pipelines writing the pack's render targets.
|
||||
4. **`gbuffers_terrain`, `gbuffers_water`** (done, above), **`gbuffers_sky*`**: Horizon's terrain vertices decoded
|
||||
into Iris's attributes (`mc_Entity` from `block.properties`, `mc_midTexCoord`, `at_tangent`, lightmap), a second
|
||||
set of terrain pipelines writing the pack's render targets.
|
||||
5. **Entities, block entities, hand, weather, clouds, particles** `gbuffers_*`.
|
||||
6. **Shadow pass**: terrain and entities from the sun into `shadowtex0/1` and `shadowcolor0/1`, culled from the
|
||||
light's view, with the pack's distortion.
|
||||
|
||||
+360
-2
@@ -189,6 +189,27 @@ address. The script then checks, from both sides, what a guest's game used to ge
|
||||
- totem: the host and the guest each take a lethal hit holding a totem of undying in the off
|
||||
hand, then the main hand: the server uses it up and keeps them alive at 1 health with its
|
||||
effects, and each side hears the totem once (entity event 35) (#1292).
|
||||
- keepinv: the host and the guest each die holding diamonds and wearing a helmet: with keep_inventory
|
||||
off both drop where they died and the player respawns without them; with it on nothing
|
||||
drops and the player keeps them (#1284).
|
||||
- signs: the host and the guest each place an oak sign: the editor opens on their own screen
|
||||
(ClientboundOpenSignEditorPacket), the two lines they write reach the server
|
||||
(ServerboundSignUpdatePacket) and both sides show them (#1380).
|
||||
- books: the host and the guest each write in a book and quill on their own screen, sign it
|
||||
(ServerboundEditBookPacket), read the written book the server gave them
|
||||
(ClientboundOpenBookPacket), then drop it: the other side sees its title, author and
|
||||
page (#1366).
|
||||
- elytra: the host and the guest, high up with an Elytra on, jump in the air and glide
|
||||
(ServerboundPlayerCommandPacket START_FALL_FLYING): both their client and the server
|
||||
have them gliding, they go forward far more than down, and the glide ends on both once
|
||||
they land (#1288).
|
||||
- helduse: the host and the guest raise a shield, charge and fire a crossbow, throw a trident
|
||||
and scope with a spyglass: each use starts on their own client and on the server, and
|
||||
letting go ends it on both (RELEASE_USE_ITEM), so the shield lowers, the trident flies
|
||||
and the loaded crossbow fires on the next use (#1300, #1281, #1282, #1301).
|
||||
- wind: the host and the guest each throw a wind charge at their feet: the server's burst
|
||||
launches them about six blocks up (the push in ClientboundExplodePacket) and they land
|
||||
unhurt (#1386).
|
||||
|
||||
Each check is reported on its own (a failing one doesn't stop the others) and the screenshots of
|
||||
both sides go to --out (host-*.png, guest-*.png).
|
||||
@@ -4517,8 +4538,345 @@ def check_totem(host, guest, out):
|
||||
return seen
|
||||
|
||||
|
||||
CHECKS += ['totem']
|
||||
CHECK_FNS.update(totem=check_totem)
|
||||
def check_keep_inventory(host, guest, out):
|
||||
"""ServerPlayer.die's dropAllDeathLoot for each player (Player.dropEquipment): with keep_inventory
|
||||
off, the diamonds and the helmet a player dies with lie where it died and it respawns with
|
||||
neither; with it on, nothing drops and it respawns with both (#1284)."""
|
||||
hx, hy, hz = (int(v // 1) for v in pos_of(host))
|
||||
seen = {}
|
||||
host('command', command=f'setworldspawn {hx} {hy} {hz}')
|
||||
host('command', command='gamerule respawn_radius 0')
|
||||
try:
|
||||
for keep in ('false', 'true'):
|
||||
host('command', command=f'gamerule keep_inventory {keep}')
|
||||
for game, dz in ((host, -3), (guest, 3)):
|
||||
# 14 blocks from the world spawn: nothing dropped is picked up on respawning.
|
||||
x, z = hx + 14, hz + dz
|
||||
host('command', command=f'fill {x - 2} {hy - 1} {z - 1} {x + 2} {hy - 1} {z + 1} minecraft:stone')
|
||||
host('command', command=f'fill {x - 2} {hy} {z - 1} {x + 2} {hy + 2} {z + 1} minecraft:air')
|
||||
host('command', command='kill @e[type=minecraft:item]')
|
||||
game('command', command='gamemode survival')
|
||||
game('command', command='clear @s')
|
||||
game('command', command='give @s minecraft:diamond 7')
|
||||
game('command', command='item replace entity @s armor.head with minecraft:iron_helmet')
|
||||
game('command', command=f'tp @s {x + 0.5} {hy} {z + 0.5}')
|
||||
game.wait(20)
|
||||
game('command', command='kill @s')
|
||||
game.wait(30)
|
||||
dropped = sorted((e['item']['item'], e['item']['count'])
|
||||
for e in game.raw('entities', 8, 'minecraft:item')['entities'])
|
||||
game.shot(out, f'keepinv-{keep}')
|
||||
respawn_when_ready(game)
|
||||
game.wait(40)
|
||||
back_to_game(game)
|
||||
seen[f'{game.name}-keep-{keep}'] = {
|
||||
'dropped': dropped,
|
||||
'kept': {item: server_count(game, item) for item in ('diamond', 'iron_helmet')}}
|
||||
finally:
|
||||
host('command', command='gamerule keep_inventory false')
|
||||
host('command', command='gamerule respawn_radius 10')
|
||||
host('command', command='kill @e[type=minecraft:item]')
|
||||
for game in (host, guest):
|
||||
game('command', command='clear @s')
|
||||
face_each_other(host, guest)
|
||||
for case, s in seen.items():
|
||||
want = ({'dropped': [], 'kept': {'diamond': 7, 'iron_helmet': 1}} if case.endswith('true') else
|
||||
{'dropped': [('minecraft:diamond', 7), ('minecraft:iron_helmet', 1)],
|
||||
'kept': {'diamond': 0, 'iron_helmet': 0}})
|
||||
assert s == want, f'{case}: {seen}'
|
||||
return seen
|
||||
|
||||
|
||||
def check_signs(host, guest, out):
|
||||
"""SignBlock.openTextEdit on the placing player's client, for the host and the guest: placing
|
||||
an oak sign opens its editor there and nowhere else, the lines typed into it go to the server
|
||||
when it closes, and both sides then show them (#1380)."""
|
||||
hx, hy, hz = (int(v // 1) for v in pos_of(host))
|
||||
seen = {}
|
||||
for game, other, dz in ((host, guest, -2), (guest, host, 2)):
|
||||
spot = (hx + 2, hy, hz + dz)
|
||||
game('command', command=f'setblock {spot[0]} {spot[1] - 1} {spot[2]} minecraft:stone')
|
||||
game('command', command=f'setblock {spot[0]} {spot[1]} {spot[2]} minecraft:air')
|
||||
game('command', command='gamemode survival')
|
||||
game('command', command='clear @s')
|
||||
game('command', command='give @s minecraft:oak_sign 1')
|
||||
game.wait(10)
|
||||
game.raw('place', *spot, '--item', 'minecraft:oak_sign')
|
||||
game.wait(10)
|
||||
opened = {g.name: g('state').get('screen') for g in (game, other)}
|
||||
game.shot(out, 'signs-editor')
|
||||
lines = [f'{game.name} wrote', 'line two']
|
||||
game.raw('type', lines[0])
|
||||
game.raw('key', 'enter', 'tap')
|
||||
game.raw('type', lines[1])
|
||||
game.raw('key', 'esc', 'tap')
|
||||
game.wait(20)
|
||||
back_to_game(other)
|
||||
seen[game.name] = {'opened': opened,
|
||||
'shown': {g.name: (g.raw('block', *spot).get('sign') or {}).get('front')
|
||||
for g in (game, other)}}
|
||||
other.shot(out, 'signs-shown')
|
||||
for name, s in seen.items():
|
||||
other = 'guest' if name == 'host' else 'host'
|
||||
want = [f'{name} wrote', 'line two', '', '']
|
||||
assert s['opened'] == {name: 'sign', other: 'playing'}, f'{name}: {seen}'
|
||||
assert s['shown'] == {name: want, other: want}, f'{name}: {seen}'
|
||||
return seen
|
||||
|
||||
|
||||
def book_screen(game):
|
||||
"""The open book (`book`'s page and page count), once `game` shows it."""
|
||||
game.raw('wait-for', 'screen', 'book', '--timeout', 10)
|
||||
return game.raw('book')
|
||||
|
||||
|
||||
def check_books(host, guest, out):
|
||||
"""Books in hand for the host and the guest: a book and quill opens its editor on the player's
|
||||
own client (LocalPlayer.openItemGui), what it writes and signs reaches the server
|
||||
(ServerboundEditBookPacket), and the written book opens on the player's client when the server
|
||||
says so (ServerPlayer.openItemGui's ClientboundOpenBookPacket); dropped, the other side sees the
|
||||
server's book (#1366)."""
|
||||
seen = {}
|
||||
for game, other in ((host, guest), (guest, host)):
|
||||
game('command', command='gamemode survival')
|
||||
game('command', command='clear @s')
|
||||
game('command', command='item replace entity @s weapon.mainhand with minecraft:writable_book')
|
||||
x, y, z = pos_of(game)
|
||||
game.raw('look-at', f'{x + 0.5:.2f}', f'{y + 30:.2f}', f'{z:.2f}')
|
||||
game.wait(10)
|
||||
case = seen[game.name] = {}
|
||||
game.raw('use-item')
|
||||
case['edit'] = book_screen(game)
|
||||
case['other_screen'] = other('state').get('screen')
|
||||
game.raw('type', f'{game.name} page one')
|
||||
game.shot(out, 'books-edit')
|
||||
game.raw('book', 'sign')
|
||||
game.raw('type', f'{game.name} book')
|
||||
game.raw('book', 'sign')
|
||||
game.wait(20)
|
||||
case['signed'] = {item: server_count(game, item) for item in ('writable_book', 'written_book')}
|
||||
game.raw('use-item')
|
||||
case['view'] = book_screen(game)
|
||||
game.shot(out, 'books-view')
|
||||
game.raw('book', 'done')
|
||||
game.wait(5)
|
||||
game.raw('key', 'q', 'tap')
|
||||
game.wait(30)
|
||||
books = [e['item'].get('components', '') for e in other.raw('entities', 16, 'minecraft:item')['entities']
|
||||
if e['item']['item'] == 'minecraft:written_book']
|
||||
case['dropped'] = [all(f'{game.name} {t}' in c for t in ('page one', 'book')) for c in books]
|
||||
host('command', command='kill @e[type=minecraft:item]')
|
||||
for name, case in seen.items():
|
||||
assert case['edit'] == {'page': 0, 'pages': 1, 'lectern': None}, f'{name}: {seen}'
|
||||
assert case['other_screen'] == 'playing', f'{name}: {seen}'
|
||||
assert case['signed'] == {'writable_book': 0, 'written_book': 1}, f'{name}: {seen}'
|
||||
assert case['view'] == {'page': 0, 'pages': 1, 'lectern': None}, f'{name}: {seen}'
|
||||
assert case['dropped'] == [True], f'{name}: {seen}'
|
||||
return seen
|
||||
|
||||
|
||||
CHECKS += ['totem', 'keepinv', 'signs', 'books']
|
||||
CHECK_FNS.update(totem=check_totem, keepinv=check_keep_inventory, signs=check_signs, books=check_books)
|
||||
|
||||
|
||||
def check_elytra(host, guest, out):
|
||||
"""LivingEntity.updateFallFlying for each player: a jump while falling with an Elytra on starts a
|
||||
glide that the server takes too (Player.tryToStartFallFlying), the glide carries the player
|
||||
forward rather than down, and landing ends it on both sides (canGlide fails on the ground)."""
|
||||
hx, hy, hz = (int(v // 1) for v in pos_of(host))
|
||||
seen = {}
|
||||
try:
|
||||
for game, dx in ((host, -2), (guest, 2)):
|
||||
x, z = hx + dx, hz
|
||||
host('command', command=f'fill {x - 1} {hy - 1} {z - 1} {x + 1} {hy - 1} {z + 1} minecraft:stone')
|
||||
host('command', command=f'fill {x - 1} {hy} {z - 1} {x + 1} {hy + 3} {z + 1} minecraft:air')
|
||||
game('command', command='gamemode survival')
|
||||
game('command', command='clear @s')
|
||||
game('command', command='effect give @s minecraft:resistance 60 4 true')
|
||||
game('command', command='item replace entity @s armor.chest with minecraft:elytra')
|
||||
game('command', command=f'tp @s {x + 0.5} {hy + 60} {z + 0.5} 0 0')
|
||||
game.wait(6)
|
||||
start = pos_of(game)
|
||||
game.raw('key', 'space', 'down')
|
||||
game.wait(1)
|
||||
game.raw('key', 'space', 'up')
|
||||
game.wait(30)
|
||||
end = pos_of(game)
|
||||
glide = {'client': own(game).get('fall_flying'),
|
||||
'server': server_player(host, game).get('fall_flying'),
|
||||
'forward': round(math.hypot(end[0] - start[0], end[2] - start[2]), 1),
|
||||
'down': round(start[1] - end[1], 1)}
|
||||
game.shot(out, 'elytra')
|
||||
game('command', command=f'tp @s {x + 0.5} {hy + 1} {z + 0.5}')
|
||||
game.wait(30)
|
||||
landed = {'on_ground': own(game).get('on_ground'),
|
||||
'client': own(game).get('fall_flying'),
|
||||
'server': server_player(host, game).get('fall_flying')}
|
||||
seen[game.name] = {'glide': glide, 'landed': landed}
|
||||
finally:
|
||||
for game in (host, guest):
|
||||
game('command', command='clear @s')
|
||||
game('command', command='effect clear @s')
|
||||
face_each_other(host, guest)
|
||||
for name, s in seen.items():
|
||||
g, l = s['glide'], s['landed']
|
||||
# Falling alone covers about 25 blocks in those 30 ticks and goes nowhere.
|
||||
assert g['client'] and g['server'] and g['forward'] > 8 and g['down'] < 20, f'{name} glide: {seen}'
|
||||
assert l['on_ground'] and not l['client'] and not l['server'], f'{name} landed: {seen}'
|
||||
return seen
|
||||
|
||||
|
||||
CHECKS += ['elytra']
|
||||
CHECK_FNS.update(elytra=check_elytra)
|
||||
|
||||
|
||||
def server_using(host, game):
|
||||
"""The item the server has the player using ({slot, ticks}), or None."""
|
||||
if game is host and not SERVER_THREAD:
|
||||
return host('state').get('using')
|
||||
return server_player(host, game).get('using')
|
||||
|
||||
|
||||
def flying(game, kind):
|
||||
"""How many of the projectiles this side draws are of `kind` (a trident, an arrow)."""
|
||||
return sum(kind in q['kind'].lower() for q in game('status')['projectiles'])
|
||||
|
||||
|
||||
def check_held_use(host, guest, out):
|
||||
"""Items used until the button lets go, for each player: the client starts the use too
|
||||
(Item.use runs on both sides), so letting go sends RELEASE_USE_ITEM and the server ends it."""
|
||||
hx, hy, hz = (int(v // 1) for v in pos_of(host))
|
||||
seen = {}
|
||||
try:
|
||||
for game, dz in ((host, -3), (guest, 3)):
|
||||
x, z = hx, hz + dz
|
||||
host('command', command=f'fill {x - 1} {hy - 1} {z - 1} {x + 12} {hy - 1} {z + 1} minecraft:stone')
|
||||
host('command', command=f'fill {x - 1} {hy} {z - 1} {x + 12} {hy + 3} {z + 1} minecraft:air')
|
||||
host('command', command=f'fill {x + 12} {hy} {z - 1} {x + 12} {hy + 3} {z + 1} minecraft:stone')
|
||||
game('command', command='gamemode survival')
|
||||
game('command', command='clear @s')
|
||||
game('command', command=f'tp @s {x + 0.5} {hy} {z + 0.5} -90 0')
|
||||
game.wait(10)
|
||||
s = {}
|
||||
# ShieldItem: raised while held, lowered once let go.
|
||||
game('command', command='item replace entity @s weapon.offhand with minecraft:shield')
|
||||
game.wait(5)
|
||||
game.raw('mouse', 'right', 'down')
|
||||
game.wait(10)
|
||||
s['shield'] = {'held': (bool(game('state').get('using')), bool(server_using(host, game)))}
|
||||
game.raw('mouse', 'right', 'up')
|
||||
game.wait(10)
|
||||
s['shield']['let_go'] = (bool(game('state').get('using')), bool(server_using(host, game)))
|
||||
game('command', command='item replace entity @s weapon.offhand with minecraft:air')
|
||||
# SpyglassItem: scoping while held.
|
||||
game('command', command='item replace entity @s weapon.mainhand with minecraft:spyglass')
|
||||
game.wait(5)
|
||||
game.raw('mouse', 'right', 'down')
|
||||
game.wait(10)
|
||||
s['spyglass'] = {'held': (bool(game('state').get('using')), bool(server_using(host, game)))}
|
||||
game.raw('mouse', 'right', 'up')
|
||||
game.wait(10)
|
||||
s['spyglass']['let_go'] = (bool(game('state').get('using')), bool(server_using(host, game)))
|
||||
# TridentItem.releaseUsing after 10 ticks: thrown.
|
||||
host('command', command='kill @e[type=minecraft:trident]')
|
||||
game('command', command='item replace entity @s weapon.mainhand with minecraft:trident')
|
||||
game.wait(5)
|
||||
# /kill finds no thrown trident (Horizon's selectors miss them): count what's new.
|
||||
before = flying(game, 'trident')
|
||||
game.raw('use-item', '--hold', 1.2)
|
||||
game.wait(10)
|
||||
s['trident'] = {'kept': server_count(game, 'trident'),
|
||||
'thrown': flying(game, 'trident') - before}
|
||||
host('command', command='kill @e[type=minecraft:trident]')
|
||||
# CrossbowItem: charged by holding (25 ticks), fired by the next use.
|
||||
host('command', command='kill @e[type=minecraft:arrow]')
|
||||
game('command', command='clear @s')
|
||||
game('command', command='item replace entity @s weapon.mainhand with minecraft:crossbow')
|
||||
game('command', command='give @s minecraft:arrow 16')
|
||||
game.wait(5)
|
||||
before = flying(game, 'arrow')
|
||||
game.raw('use-item', '--hold', 1.6)
|
||||
game.wait(10)
|
||||
held = game('state').get('held') or {}
|
||||
s['crossbow'] = {'loaded': 'charged_projectiles' in str(held.get('components', '')),
|
||||
'arrows': server_count(game, 'arrow')}
|
||||
game.raw('use-item')
|
||||
game.wait(10)
|
||||
held = game('state').get('held') or {}
|
||||
s['crossbow'].update(
|
||||
fired=flying(game, 'arrow') - before,
|
||||
emptied='charged_projectiles' not in str(held.get('components', '')))
|
||||
game.shot(out, 'helduse')
|
||||
seen[game.name] = s
|
||||
finally:
|
||||
host('command', command='kill @e[type=minecraft:arrow]')
|
||||
host('command', command='kill @e[type=minecraft:trident]')
|
||||
for game in (host, guest):
|
||||
back_to_game(game)
|
||||
game.raw('mouse', 'right', 'up')
|
||||
game('command', command='clear @s')
|
||||
face_each_other(host, guest)
|
||||
for name, s in seen.items():
|
||||
for item in ('shield', 'spyglass'):
|
||||
assert s[item] == {'held': (True, True), 'let_go': (False, False)}, f'{name} {item}: {seen}'
|
||||
assert s['trident'] == {'kept': 0, 'thrown': 1}, f'{name} trident: {seen}'
|
||||
assert s['crossbow'] == {'loaded': True, 'arrows': 15, 'fired': 1, 'emptied': True}, \
|
||||
f'{name} crossbow: {seen}'
|
||||
return seen
|
||||
|
||||
|
||||
CHECKS += ['helduse']
|
||||
CHECK_FNS.update(helduse=check_held_use)
|
||||
|
||||
|
||||
def check_wind(host, guest, out):
|
||||
"""A wind charge each player throws at its own feet (ServerExplosion with TRIGGER_BLOCK): the
|
||||
server's burst pushes the thrower through its ClientboundExplodePacket (hitPlayers), which
|
||||
launches it about six blocks up, and onExplosionHit spares it the fall damage where it lands
|
||||
(#1386)."""
|
||||
hx, hy, hz = (int(v // 1) for v in pos_of(host))
|
||||
host('command', command='gamerule natural_health_regeneration false')
|
||||
seen = {}
|
||||
try:
|
||||
for game, dz in ((host, -4), (guest, 4)):
|
||||
x, z = hx, hz + dz
|
||||
host('command', command=f'fill {x - 2} {hy - 1} {z - 2} {x + 2} {hy - 1} {z + 2} minecraft:stone')
|
||||
host('command', command=f'fill {x - 2} {hy} {z - 2} {x + 2} {hy + 10} {z + 2} minecraft:air')
|
||||
game('command', command='gamemode survival')
|
||||
game('command', command='clear @s')
|
||||
game('command', command='effect clear @s')
|
||||
game('command', command='effect give @s minecraft:instant_health 1 5 true')
|
||||
game('command', command='give @s minecraft:wind_charge 2')
|
||||
game('command', command=f'tp @s {x + 0.5} {hy} {z + 0.5} 0 90')
|
||||
game.wait(20)
|
||||
start = pos_of(game)[1]
|
||||
game.raw('use-item')
|
||||
peak = start
|
||||
for _ in range(30):
|
||||
game.wait(2)
|
||||
peak = max(peak, pos_of(game)[1])
|
||||
game.wait(40)
|
||||
game.shot(out, 'wind')
|
||||
me = own(game)
|
||||
seen[game.name] = {'rose': round(peak - start, 1),
|
||||
'landed': round(me['position'][1] - start, 1),
|
||||
'health': me['health'],
|
||||
'server_health': server_player(host, game)['health'],
|
||||
'charges': server_count(game, 'wind_charge')}
|
||||
finally:
|
||||
host('command', command='gamerule natural_health_regeneration true')
|
||||
for game in (host, guest):
|
||||
game('command', command='clear @s')
|
||||
face_each_other(host, guest)
|
||||
for name, s in seen.items():
|
||||
# Vanilla rises about 6 blocks from a charge at the feet.
|
||||
assert 4 < s['rose'] < 8 and s['landed'] == 0, f'{name}: {seen}'
|
||||
assert s['health'] == 20 and s['server_health'] == 20 and s['charges'] == 1, f'{name}: {seen}'
|
||||
return seen
|
||||
|
||||
|
||||
CHECKS += ['wind']
|
||||
CHECK_FNS.update(wind=check_wind)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in new issue
Block a user