mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-10 17:18:25 -10:00
Merge remote-tracking branch 'origin/main' into refactor/entity-walk-portals
# Conflicts: # crates/horizon-client/src/game/entity.rs
This commit is contained in:
commit
c2f28b5ddb
6 files changed
+935
-47
No files matched your search
@@ -115,3 +115,35 @@ licensed under the MIT license:
|
||||
> NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
> DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## BlueMap
|
||||
|
||||
`crates/horizon-client-render/src/bluemap.rs` reads the lowres tiles of a server's
|
||||
[BlueMap](https://github.com/BlueMap-Minecraft/BlueMap) web map. It follows three of BlueMap's files:
|
||||
`core/src/main/java/de/bluecolored/bluemap/core/map/lowres/LowresLayer.java` (`saveTile`) for the
|
||||
tiles' pixel layout, `common/webapp/src/js/util/Utils.js` (`pathFromCoords`) for their paths, and
|
||||
`core/src/main/java/de/bluecolored/bluemap/core/map/MapSettingsSerializer.java` for a map's
|
||||
`settings.json`, at commit
|
||||
[96ae320d1d39](https://github.com/BlueMap-Minecraft/BlueMap/tree/96ae320d1d39581c10058d8222f2290f2fcb5333).
|
||||
BlueMap is licensed under the MIT license:
|
||||
|
||||
> Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
|
||||
> Copyright (c) contributors
|
||||
>
|
||||
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
> of this software and associated documentation files (the "Software"), to deal
|
||||
> in the Software without restriction, including without limitation the rights
|
||||
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
> copies of the Software, and to permit persons to whom the Software is
|
||||
> furnished to do so, subject to the following conditions:
|
||||
>
|
||||
> The above copyright notice and this permission notice shall be included in
|
||||
> all copies or substantial portions of the Software.
|
||||
>
|
||||
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
> THE SOFTWARE.
|
||||
@@ -0,0 +1,835 @@
|
||||
//! Far terrain from a server's BlueMap: the
|
||||
//! lowres tiles of the server's web map, fetched over HTTP around the player,
|
||||
//! fill the far terrain where no real chunk was ever seen
|
||||
//! (`server_terrain`'s store always comes first). Off unless the
|
||||
//! player names the server's map (`Settings`).
|
||||
//!
|
||||
//! A lowres tile (`LowresTile`) is a PNG of `(size + 1)` × `2 (size + 1)`
|
||||
//! pixels: the top half each column's colour, the bottom half its height
|
||||
//! (green and blue, a signed 16-bit number) and block light (red). Level 1
|
||||
//! has a pixel a block; each next level averages `lodFactor` × `lodFactor`
|
||||
//! pixels into one (`LowresLayer.saveTile`).
|
||||
//!
|
||||
//! The tile layout and paths follow BlueMap's own (MIT, Copyright (c) Blue
|
||||
//! (Lukas Rieger) and contributors; THIRD_PARTY_NOTICES.md): `LowresLayer`,
|
||||
//! `MapSettingsSerializer` and the web app's `pathFromCoords`, at `96ae320d1d39`.
|
||||
use crate::{
|
||||
biome::Biomes,
|
||||
chunk::MIN_Y,
|
||||
fast_hash::{FxHashMap, FxHashSet},
|
||||
lod::{Column, Span},
|
||||
};
|
||||
use horizon_world::registry::{BlockRegistry, BlockStateId};
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Where the player names the servers' maps (`Settings::servers`), beside
|
||||
/// `horizon-graphics.txt`: kept out of the graphics settings, which shared
|
||||
/// instances and modpacks carry, as a map the client asks for tiles around
|
||||
/// the player (where they stand, which server) is the player's to name, not
|
||||
/// a pack's.
|
||||
pub const SERVERS_FILE: &str = "horizon-bluemap.txt";
|
||||
/// The player's BlueMap settings: `blueMapRadius:<chunks>` (or `<blocks>b`)
|
||||
/// in `horizon-graphics.txt`, and
|
||||
/// `blueMap:<server address> <map URL> [<level>=<map id> ...]` per server in
|
||||
/// `SERVERS_FILE`.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Settings {
|
||||
/// How far around the player tiles are downloaded, in chunks; 0 uses
|
||||
/// `DEFAULT_RADIUS`.
|
||||
pub radius: u32,
|
||||
pub servers: Vec<Server>,
|
||||
}
|
||||
pub const DEFAULT_RADIUS: u32 = 64;
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Server {
|
||||
pub address: String,
|
||||
pub url: String,
|
||||
/// `(level, map id)`: which of the server's maps draws which level;
|
||||
/// unnamed levels are guessed from the map ids (`pick_map`).
|
||||
pub maps: Vec<(String, String)>,
|
||||
}
|
||||
impl Settings {
|
||||
/// `blueMapRadius:`'s value: chunks, or blocks with a `b` (`2048b`).
|
||||
pub fn parse_radius(value: &str) -> Option<u32> {
|
||||
let value = value.trim().to_ascii_lowercase();
|
||||
let blocks = value
|
||||
.strip_suffix("blocks")
|
||||
.or_else(|| value.strip_suffix('b'));
|
||||
let chunks = match blocks {
|
||||
Some(blocks) => blocks.trim().parse::<u32>().ok()?.div_ceil(16),
|
||||
None => value
|
||||
.strip_suffix("chunks")
|
||||
.unwrap_or(&value)
|
||||
.trim()
|
||||
.parse()
|
||||
.ok()?,
|
||||
};
|
||||
Some(chunks.min(4096))
|
||||
}
|
||||
/// A `blueMap:` line's value.
|
||||
pub fn parse_server(value: &str) -> Option<Server> {
|
||||
let mut words = value.split_whitespace();
|
||||
let address = words.next()?.to_owned();
|
||||
let url = words.next()?.to_owned();
|
||||
let maps = words
|
||||
.filter_map(|w| w.split_once('='))
|
||||
.map(|(level, map)| (level_key(level), map.to_owned()))
|
||||
.collect();
|
||||
Some(Server { address, url, maps })
|
||||
}
|
||||
/// The `blueMap:` lines of `SERVERS_FILE`.
|
||||
pub fn parse_servers(text: &str) -> Vec<Server> {
|
||||
// Notepad's "UTF-8 with BOM" would hide the first line.
|
||||
text.trim_start_matches('\u{feff}')
|
||||
.lines()
|
||||
.filter_map(|line| line.split_once(':'))
|
||||
.filter(|(key, _)| key.trim() == "blueMap")
|
||||
.filter_map(|(_, value)| Self::parse_server(value))
|
||||
.collect()
|
||||
}
|
||||
/// What goes in `horizon-graphics.txt` (the servers don't).
|
||||
pub fn serialize(&self) -> String {
|
||||
format!("blueMapRadius:{}\n", self.radius)
|
||||
}
|
||||
/// The settings of the server at `address`, as typed to join it.
|
||||
pub fn server(&self, address: &str) -> Option<&Server> {
|
||||
let folder = crate::server_terrain::server_folder(address);
|
||||
self.servers
|
||||
.iter()
|
||||
.find(|s| crate::server_terrain::server_folder(&s.address) == folder)
|
||||
}
|
||||
pub fn radius_blocks(&self) -> i32 {
|
||||
let chunks = if self.radius == 0 {
|
||||
DEFAULT_RADIUS
|
||||
} else {
|
||||
self.radius
|
||||
};
|
||||
chunks as i32 * 16
|
||||
}
|
||||
}
|
||||
|
||||
/// `nether` → `minecraft:the_nether`, `overworld` → `minecraft:overworld`.
|
||||
fn level_key(level: &str) -> String {
|
||||
match level {
|
||||
"nether" => "minecraft:the_nether".into(),
|
||||
"end" => "minecraft:the_end".into(),
|
||||
_ if level.contains(':') => level.into(),
|
||||
_ => format!("minecraft:{level}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The map drawing `level` among the server's map ids: the player's choice,
|
||||
/// else the first id naming the level's dimension (`world_nether`,
|
||||
/// `end`...), the overworld's being the first that names neither.
|
||||
pub fn pick_map(maps: &[String], level: &str, chosen: &[(String, String)]) -> Option<String> {
|
||||
if let Some((_, map)) = chosen.iter().find(|(l, _)| *l == level) {
|
||||
return Some(map.clone());
|
||||
}
|
||||
let named = |m: &&String, word: &str| m.to_ascii_lowercase().contains(word);
|
||||
match level {
|
||||
"minecraft:the_nether" => maps.iter().find(|m| named(m, "nether")),
|
||||
"minecraft:the_end" => maps.iter().find(|m| named(m, "end")),
|
||||
"minecraft:overworld" => maps
|
||||
.iter()
|
||||
.find(|m| !named(m, "nether") && !named(m, "end")),
|
||||
_ => None,
|
||||
}
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// The web app's `settings.json`: the map ids and where their data lies.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Root {
|
||||
pub maps: Vec<String>,
|
||||
pub data_root: String,
|
||||
}
|
||||
impl Root {
|
||||
pub fn parse(json: &[u8]) -> Option<Self> {
|
||||
let v: serde_json::Value = serde_json::from_slice(json).ok()?;
|
||||
let maps = v
|
||||
.get("maps")?
|
||||
.as_array()?
|
||||
.iter()
|
||||
.filter_map(|m| m.as_str().map(str::to_owned))
|
||||
.collect();
|
||||
let data_root = v
|
||||
.get("mapDataRoot")
|
||||
.and_then(|r| r.as_str())
|
||||
.unwrap_or("maps")
|
||||
.trim_matches('/')
|
||||
.to_owned();
|
||||
Some(Self { maps, data_root })
|
||||
}
|
||||
}
|
||||
|
||||
/// A map's `settings.json`, its lowres part (`MapSettingsSerializer`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Lowres {
|
||||
pub tile: [i32; 2],
|
||||
pub factor: i32,
|
||||
pub levels: u8,
|
||||
}
|
||||
impl Lowres {
|
||||
pub fn parse(json: &[u8]) -> Option<Self> {
|
||||
let v: serde_json::Value = serde_json::from_slice(json).ok()?;
|
||||
let lowres = v.get("lowres")?;
|
||||
let size = lowres.get("tileSize").and_then(|s| s.as_array());
|
||||
let axis = |i: usize| {
|
||||
size.and_then(|s| s.get(i)?.as_i64()).map_or(Some(32), |n| {
|
||||
i32::try_from(n).ok().filter(|n| (1..=1024).contains(n))
|
||||
})
|
||||
};
|
||||
let factor = lowres
|
||||
.get("lodFactor")
|
||||
.and_then(|f| f.as_i64())
|
||||
.unwrap_or(5);
|
||||
let levels = lowres.get("lodCount").and_then(|c| c.as_i64()).unwrap_or(3);
|
||||
let lowres = Self {
|
||||
tile: [axis(0)?, axis(1)?],
|
||||
factor: i32::try_from(factor)
|
||||
.ok()
|
||||
.filter(|f| (2..=64).contains(f))?,
|
||||
levels: u8::try_from(levels).ok().filter(|c| (1..=8).contains(c))?,
|
||||
};
|
||||
// A tile packs `factor` of the level below on each axis: smaller ones
|
||||
// have no pixel for each, and the coarsest must span the world, no
|
||||
// more (block coordinates stay far from overflowing).
|
||||
let coarsest = lowres.factor.checked_pow(u32::from(lowres.levels) - 1)?;
|
||||
lowres
|
||||
.tile
|
||||
.iter()
|
||||
.all(|&size| {
|
||||
size >= lowres.factor && size.checked_mul(coarsest).is_some_and(|b| b <= 1 << 26)
|
||||
})
|
||||
.then_some(lowres)
|
||||
}
|
||||
/// Blocks a pixel of `level` (1 = finest) spans, about.
|
||||
pub fn scale(&self, level: u8) -> i32 {
|
||||
self.factor.saturating_pow(u32::from(level) - 1)
|
||||
}
|
||||
/// The coarsest level whose pixels are no wider than `width` blocks.
|
||||
pub fn level_for(&self, width: i32) -> u8 {
|
||||
(1..=self.levels)
|
||||
.rev()
|
||||
.find(|&l| self.scale(l) <= width)
|
||||
.unwrap_or(1)
|
||||
}
|
||||
/// The tile of `level` holding block coordinate `block` on one axis, and
|
||||
/// its pixel. Each level packs `factor` tiles of the one below into one,
|
||||
/// `size / factor` pixels a tile (`LowresLayer.saveTile`): a pixel past
|
||||
/// those (`size % factor` of them) is drawn with the last.
|
||||
pub fn locate(&self, axis: usize, level: u8, block: i32) -> (i32, usize) {
|
||||
let size = self.tile[axis];
|
||||
let groups = (size / self.factor).max(1);
|
||||
let (mut tile, mut pixel) = (block.div_euclid(size), block.rem_euclid(size));
|
||||
for _ in 1..level {
|
||||
let group = (pixel / self.factor).min(groups - 1);
|
||||
pixel = tile.rem_euclid(self.factor) * groups + group;
|
||||
tile = tile.div_euclid(self.factor);
|
||||
}
|
||||
(tile, pixel as usize)
|
||||
}
|
||||
/// The blocks of one axis a tile of `level` covers, `[min, max)`.
|
||||
pub fn span(&self, axis: usize, level: u8, tile: i32) -> (i32, i32) {
|
||||
let blocks = self.tile[axis].saturating_mul(self.scale(level));
|
||||
(
|
||||
tile.saturating_mul(blocks),
|
||||
(tile + 1).saturating_mul(blocks),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// `pathFromCoords`: `(12, -3)` → `x1/2/z-3`.
|
||||
pub fn tile_path(x: i32, z: i32) -> String {
|
||||
let digits = |n: i32| {
|
||||
let mut out = String::new();
|
||||
if n < 0 {
|
||||
out.push('-');
|
||||
}
|
||||
for c in n.unsigned_abs().to_string().chars() {
|
||||
out.push(c);
|
||||
out.push('/');
|
||||
}
|
||||
out
|
||||
};
|
||||
let mut path = format!("x{}z{}", digits(x), digits(z));
|
||||
path.pop();
|
||||
path
|
||||
}
|
||||
|
||||
/// One column of a lowres tile.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Pixel {
|
||||
/// ARGB, straight alpha; 0 alpha: nothing was rendered there.
|
||||
pub color: u32,
|
||||
/// The highest block BlueMap drew.
|
||||
pub height: i16,
|
||||
pub block_light: u8,
|
||||
}
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Tile {
|
||||
width: usize,
|
||||
pixels: Vec<Pixel>,
|
||||
}
|
||||
impl Tile {
|
||||
pub fn pixel(&self, x: usize, z: usize) -> Pixel {
|
||||
self.pixels[z * self.width + x]
|
||||
}
|
||||
}
|
||||
|
||||
/// A lowres tile's PNG (gzipped too, as a misconfigured web server may send
|
||||
/// it), `None` if it isn't one of `lowres`'s size.
|
||||
pub fn decode_tile(bytes: &[u8], lowres: &Lowres) -> Option<Tile> {
|
||||
let unzipped;
|
||||
let bytes = if bytes.starts_with(&[0x1f, 0x8b]) {
|
||||
unzipped = horizon_world::compression::Compression::Gzip
|
||||
.decode(bytes, 8 << 20)
|
||||
.ok()?;
|
||||
&unzipped[..]
|
||||
} else {
|
||||
bytes
|
||||
};
|
||||
let mut decoder = png::Decoder::new(std::io::Cursor::new(bytes));
|
||||
decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
|
||||
let mut reader = decoder.read_info().ok()?;
|
||||
let (width, height) = (lowres.tile[0] as usize + 1, lowres.tile[1] as usize + 1);
|
||||
let info = reader.info();
|
||||
if info.width as usize != width || info.height as usize != height * 2 {
|
||||
return None;
|
||||
}
|
||||
let mut buf = vec![0; reader.output_buffer_size()?];
|
||||
let frame = reader.next_frame(&mut buf).ok()?;
|
||||
let channels = frame.color_type.samples();
|
||||
let rgba = |i: usize| -> [u8; 4] {
|
||||
let p = &buf[i * channels..(i + 1) * channels];
|
||||
match channels {
|
||||
4 => [p[0], p[1], p[2], p[3]],
|
||||
3 => [p[0], p[1], p[2], 255],
|
||||
2 => [p[0], p[0], p[0], p[1]],
|
||||
_ => [p[0], p[0], p[0], 255],
|
||||
}
|
||||
};
|
||||
let pixels = (0..width * height)
|
||||
.map(|i| {
|
||||
let [r, g, b, a] = rgba(i);
|
||||
let [light, hi, lo, _] = rgba(i + width * height);
|
||||
Pixel {
|
||||
color: u32::from_be_bytes([a, r, g, b]),
|
||||
// No world reaches past ±2048 blocks (`DimensionType`).
|
||||
height: i16::from_be_bytes([hi, lo]).clamp(-2048, 2047),
|
||||
block_light: light,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Some(Tile { width, pixels })
|
||||
}
|
||||
|
||||
/// What a lookup found: data, data on its way, or nothing to wait for.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Found<T> {
|
||||
Ready(T),
|
||||
Waiting,
|
||||
Absent,
|
||||
}
|
||||
|
||||
/// Block states a map colour is drawn with: the nearest of a few natural
|
||||
/// blocks' colours as BlueMap renders them (their texture's mean, tinted with
|
||||
/// each biome's grass, foliage and water colours).
|
||||
pub struct Palette {
|
||||
candidates: Vec<Candidate>,
|
||||
under: BlockStateId,
|
||||
deep: BlockStateId,
|
||||
matches: Mutex<FxHashMap<u32, usize>>,
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
struct Candidate {
|
||||
rgb: [f32; 3],
|
||||
top: BlockStateId,
|
||||
/// The few blocks under the top one (dirt under grass).
|
||||
under: Option<BlockStateId>,
|
||||
biome: u16,
|
||||
water: bool,
|
||||
}
|
||||
/// A biome's tints, for `Palette::new`.
|
||||
pub struct Tints {
|
||||
pub id: u16,
|
||||
pub grass: u32,
|
||||
pub foliage: u32,
|
||||
pub water: u32,
|
||||
}
|
||||
/// Natural and common building blocks, with BlueMap's colour of each, about.
|
||||
const BLOCKS: &[(&str, u32)] = &[
|
||||
("stone", 0x7E7E7E),
|
||||
("deepslate", 0x505053),
|
||||
("dirt", 0x866043),
|
||||
("podzol", 0x5C3F18),
|
||||
("mycelium", 0x6F6369),
|
||||
("sand", 0xDBCFA3),
|
||||
("red_sand", 0xBE6621),
|
||||
("gravel", 0x847F7F),
|
||||
("clay", 0xA0A6B3),
|
||||
("snow_block", 0xF5FAFA),
|
||||
("ice", 0x91B7FD),
|
||||
("terracotta", 0x985E44),
|
||||
("orange_terracotta", 0xA25426),
|
||||
("yellow_terracotta", 0xBA8523),
|
||||
("white_terracotta", 0xD2B2A1),
|
||||
("brown_terracotta", 0x4D3324),
|
||||
("red_terracotta", 0x8F3D2F),
|
||||
("light_gray_terracotta", 0x876B62),
|
||||
("moss_block", 0x596E2D),
|
||||
("mud", 0x3C3A3D),
|
||||
("netherrack", 0x622626),
|
||||
("crimson_nylium", 0x831F1F),
|
||||
("warped_nylium", 0x2B7265),
|
||||
("soul_sand", 0x513E33),
|
||||
("basalt", 0x505155),
|
||||
("blackstone", 0x2A2328),
|
||||
("magma_block", 0x8E3F1F),
|
||||
("lava", 0xCF5B13),
|
||||
("end_stone", 0xDBDEA1),
|
||||
("obsidian", 0x0F0A18),
|
||||
("oak_planks", 0xA2834F),
|
||||
("spruce_planks", 0x735531),
|
||||
("dark_oak_planks", 0x432B14),
|
||||
("bricks", 0x976253),
|
||||
("smooth_stone", 0x9F9F9F),
|
||||
("quartz_block", 0xEBE5DE),
|
||||
("white_concrete", 0xCFD5D6),
|
||||
("light_gray_concrete", 0x7D7D73),
|
||||
("gray_concrete", 0x373A3E),
|
||||
("black_concrete", 0x080A0F),
|
||||
("red_concrete", 0x8E2121),
|
||||
("blue_concrete", 0x2D2F8F),
|
||||
("spruce_leaves", 0x304C30),
|
||||
("birch_leaves", 0x40532A),
|
||||
];
|
||||
/// The mean grey of grass_block_top, the leaves and water_still, which the
|
||||
/// biome colours tint.
|
||||
const GRASS_GREY: f32 = 0.6;
|
||||
const LEAVES_GREY: f32 = 0.5;
|
||||
const WATER_GREY: f32 = 0.7;
|
||||
|
||||
fn rgb(color: u32) -> [f32; 3] {
|
||||
[16, 8, 0].map(|s| ((color >> s) & 255) as f32)
|
||||
}
|
||||
impl Palette {
|
||||
pub fn new(
|
||||
state: impl Fn(&str) -> Option<BlockStateId>,
|
||||
biomes: &[Tints],
|
||||
plains: u16,
|
||||
) -> Self {
|
||||
let block = |name: &str| state(&format!("minecraft:{name}"));
|
||||
let mut candidates = Vec::new();
|
||||
let dirt = block("dirt");
|
||||
for &(name, color) in BLOCKS {
|
||||
if let Some(top) = block(name) {
|
||||
let under = matches!(name, "podzol" | "mycelium" | "snow_block").then_some(dirt);
|
||||
candidates.push(Candidate {
|
||||
rgb: rgb(color),
|
||||
top,
|
||||
under: under.flatten(),
|
||||
biome: plains,
|
||||
water: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
let tinted = |tint: u32, grey: f32| rgb(tint).map(|c| c * grey);
|
||||
let mut seen = FxHashSet::default();
|
||||
for b in biomes {
|
||||
for (state, color, under, water) in [
|
||||
(
|
||||
block("grass_block"),
|
||||
tinted(b.grass, GRASS_GREY),
|
||||
dirt,
|
||||
false,
|
||||
),
|
||||
(
|
||||
block("oak_leaves"),
|
||||
tinted(b.foliage, LEAVES_GREY),
|
||||
None,
|
||||
false,
|
||||
),
|
||||
(block("water"), tinted(b.water, WATER_GREY), None, true),
|
||||
] {
|
||||
let key = (color.map(|c| c as u32), water, under.is_some());
|
||||
if let Some(top) = state
|
||||
&& seen.insert(key)
|
||||
{
|
||||
candidates.push(Candidate {
|
||||
rgb: color,
|
||||
top,
|
||||
under,
|
||||
biome: b.id,
|
||||
water,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Self {
|
||||
candidates,
|
||||
under: block("sand").unwrap_or(BlockStateId(0)),
|
||||
deep: block("stone").unwrap_or(BlockStateId(0)),
|
||||
matches: Mutex::default(),
|
||||
}
|
||||
}
|
||||
pub fn from_game(registry: &BlockRegistry, biomes: &Biomes) -> Self {
|
||||
let tints: Vec<_> = biomes
|
||||
.list
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, b)| Tints {
|
||||
id: id as u16,
|
||||
grass: b.grass,
|
||||
foliage: b.foliage,
|
||||
water: b.water,
|
||||
})
|
||||
.collect();
|
||||
Self::new(
|
||||
|name| registry.default_state(name),
|
||||
&tints,
|
||||
biomes.id("minecraft:plains"),
|
||||
)
|
||||
}
|
||||
fn nearest(&self, color: u32) -> Option<Candidate> {
|
||||
let key = color & 0xFF_FFFF;
|
||||
let mut matches = self.matches.lock().unwrap();
|
||||
if matches.len() > 1 << 16 {
|
||||
matches.clear();
|
||||
}
|
||||
let index = *matches.entry(key).or_insert_with(|| {
|
||||
let want = rgb(key);
|
||||
let distance = |c: &Candidate| {
|
||||
// Weighted towards green, as the eye is (a "redmean"-ish norm).
|
||||
let d = [0, 1, 2].map(|i| c.rgb[i] - want[i]);
|
||||
2.0 * d[0] * d[0] + 4.0 * d[1] * d[1] + 3.0 * d[2] * d[2]
|
||||
};
|
||||
(0..self.candidates.len())
|
||||
.min_by(|&a, &b| {
|
||||
distance(&self.candidates[a]).total_cmp(&distance(&self.candidates[b]))
|
||||
})
|
||||
.unwrap_or(usize::MAX)
|
||||
});
|
||||
self.candidates.get(index).copied()
|
||||
}
|
||||
/// A pixel as a far-terrain column: its top block at its height, a few
|
||||
/// blocks under it, then stone; water over sand.
|
||||
pub fn column(&self, pixel: Pixel) -> Column {
|
||||
if pixel.color >> 24 == 0 {
|
||||
return Column::default();
|
||||
}
|
||||
let Some(c) = self.nearest(pixel.color) else {
|
||||
return Column::default();
|
||||
};
|
||||
let top = i32::from(pixel.height) + 1;
|
||||
let light = 240 | u16::from(pixel.block_light.min(15)) << 12;
|
||||
let mut column = Column::default();
|
||||
let mut push = |min: i32, max: i32, state: BlockStateId| {
|
||||
let (min, max) = (min.max(MIN_Y), max.max(MIN_Y));
|
||||
if min < max {
|
||||
column.push(Span {
|
||||
min: min as i16,
|
||||
max: max as i16,
|
||||
state,
|
||||
biome: c.biome,
|
||||
light,
|
||||
});
|
||||
}
|
||||
};
|
||||
if c.water {
|
||||
let floor = top - 3;
|
||||
push(MIN_Y, floor - 3, self.deep);
|
||||
push(floor - 3, floor, self.under);
|
||||
push(floor, top, c.top);
|
||||
column.surface = Some(crate::lod::Surface {
|
||||
height: floor,
|
||||
state: self.under,
|
||||
water: Some((top, c.top)),
|
||||
biome: c.biome,
|
||||
});
|
||||
} else {
|
||||
let under = c.under.unwrap_or(c.top);
|
||||
push(MIN_Y, top - 4, self.deep);
|
||||
push(top - 4, top - 1, under);
|
||||
push(top - 1, top, c.top);
|
||||
column.surface = Some(crate::lod::Surface {
|
||||
height: top,
|
||||
state: c.top,
|
||||
water: None,
|
||||
biome: c.biome,
|
||||
});
|
||||
}
|
||||
column
|
||||
}
|
||||
}
|
||||
|
||||
/// `https://map.example.net/#world:0:64:0...` → `https://map.example.net`.
|
||||
pub fn base_url(url: &str) -> String {
|
||||
let url = url.trim();
|
||||
let url = url.split(['#', '?']).next().unwrap_or(url);
|
||||
let url = url.strip_suffix("index.html").unwrap_or(url);
|
||||
url.trim_end_matches('/').to_owned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const LOWRES: Lowres = Lowres {
|
||||
tile: [32, 32],
|
||||
factor: 5,
|
||||
levels: 3,
|
||||
};
|
||||
|
||||
/// A lowres tile as BlueMap writes it: `pixel` gives each column.
|
||||
fn tile_png(lowres: &Lowres, pixel: impl Fn(usize, usize) -> Pixel) -> Vec<u8> {
|
||||
let (w, h) = (lowres.tile[0] as usize + 1, lowres.tile[1] as usize + 1);
|
||||
let mut data = vec![0u8; w * h * 2 * 4];
|
||||
for z in 0..h {
|
||||
for x in 0..w {
|
||||
let p = pixel(x, z);
|
||||
let [a, r, g, b] = p.color.to_be_bytes();
|
||||
let [hi, lo] = p.height.to_be_bytes();
|
||||
data[(z * w + x) * 4..][..4].copy_from_slice(&[r, g, b, a]);
|
||||
data[((z + h) * w + x) * 4..][..4].copy_from_slice(&[p.block_light, hi, lo, 255]);
|
||||
}
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
let mut encoder = png::Encoder::new(&mut out, w as u32, (h * 2) as u32);
|
||||
encoder.set_color(png::ColorType::Rgba);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
let mut writer = encoder.write_header().unwrap();
|
||||
writer.write_image_data(&data).unwrap();
|
||||
writer.finish().unwrap();
|
||||
out
|
||||
}
|
||||
fn sample(x: usize, z: usize) -> Pixel {
|
||||
Pixel {
|
||||
color: 0xFF00_0000 | (x as u32) << 16 | (z as u32) << 8 | 0x40,
|
||||
height: x as i16 * 3 - 60,
|
||||
block_light: (z % 16) as u8,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiles_decode_colour_height_and_light() {
|
||||
let png = tile_png(&LOWRES, sample);
|
||||
let tile = decode_tile(&png, &LOWRES).expect("tile");
|
||||
for (x, z) in [(0, 0), (5, 31), (32, 32), (20, 3)] {
|
||||
assert_eq!(tile.pixel(x, z), sample(x, z));
|
||||
}
|
||||
assert_eq!(tile.pixel(0, 0).height, -60);
|
||||
// Gzipped by a web server, still the same tile.
|
||||
let mut gz = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
|
||||
std::io::Write::write_all(&mut gz, &png).unwrap();
|
||||
assert_eq!(decode_tile(&gz.finish().unwrap(), &LOWRES), Some(tile));
|
||||
// Another tile size is refused.
|
||||
let small = Lowres {
|
||||
tile: [16, 16],
|
||||
..LOWRES
|
||||
};
|
||||
assert_eq!(decode_tile(&png, &small), None);
|
||||
assert_eq!(decode_tile(b"not a png", &LOWRES), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tile_paths_split_digits_as_the_web_app() {
|
||||
assert_eq!(tile_path(0, 0), "x0/z0");
|
||||
assert_eq!(tile_path(12, -3), "x1/2/z-3");
|
||||
assert_eq!(tile_path(-120, 45), "x-1/2/0/z4/5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocks_find_their_tile_and_pixel_at_every_level() {
|
||||
// Level 1: a pixel a block.
|
||||
assert_eq!(LOWRES.locate(0, 1, 0), (0, 0));
|
||||
assert_eq!(LOWRES.locate(0, 1, 33), (1, 1));
|
||||
assert_eq!(LOWRES.locate(0, 1, -1), (-1, 31));
|
||||
// Level 2: five level-1 tiles a tile, six pixels of five blocks each.
|
||||
assert_eq!(LOWRES.locate(0, 2, 0), (0, 0));
|
||||
assert_eq!(LOWRES.locate(0, 2, 4), (0, 0));
|
||||
assert_eq!(LOWRES.locate(0, 2, 5), (0, 1));
|
||||
assert_eq!(LOWRES.locate(0, 2, 32 + 7), (0, 7));
|
||||
assert_eq!(
|
||||
LOWRES.locate(0, 2, 31),
|
||||
(0, 5),
|
||||
"the leftover blocks join the last pixel"
|
||||
);
|
||||
assert_eq!(LOWRES.locate(0, 2, -1), (-1, 29));
|
||||
assert_eq!(LOWRES.locate(0, 2, 5 * 32), (1, 0));
|
||||
// Level 3: level 2's tiles packed the same way.
|
||||
assert_eq!(LOWRES.locate(0, 3, 5 * 32 * 2 + 10), (0, 12));
|
||||
assert_eq!(LOWRES.locate(0, 3, -1), (-1, 29));
|
||||
// Every block of a level-3 tile's span lands in that tile.
|
||||
let (min, max) = LOWRES.span(0, 3, -2);
|
||||
assert_eq!((min, max), (-1600, -800));
|
||||
assert!(
|
||||
(min..max)
|
||||
.step_by(7)
|
||||
.all(|b| LOWRES.locate(0, 3, b).0 == -2)
|
||||
);
|
||||
assert_eq!(LOWRES.level_for(1), 1);
|
||||
assert_eq!(LOWRES.level_for(16), 2);
|
||||
assert_eq!(LOWRES.level_for(1024), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_files_parse() {
|
||||
let root = Root::parse(br#"{"version":"5.4","maps":["world","world_nether","world_the_end"],"mapDataRoot":"maps/","liveDataRoot":"maps"}"#).unwrap();
|
||||
assert_eq!(root.data_root, "maps");
|
||||
let map = br#"{"name":"World","hires":{"tileSize":[32,32]},"lowres":{"tileSize":[50,40],"lodFactor":4,"lodCount":2}}"#;
|
||||
assert_eq!(
|
||||
Lowres::parse(map),
|
||||
Some(Lowres {
|
||||
tile: [50, 40],
|
||||
factor: 4,
|
||||
levels: 2
|
||||
})
|
||||
);
|
||||
assert_eq!(Lowres::parse(br#"{"lowres":{}}"#), Some(LOWRES));
|
||||
assert_eq!(Lowres::parse(br#"{"lowres":{"lodFactor":0}}"#), None);
|
||||
assert_eq!(
|
||||
pick_map(&root.maps, "minecraft:overworld", &[]).as_deref(),
|
||||
Some("world")
|
||||
);
|
||||
assert_eq!(
|
||||
pick_map(&root.maps, "minecraft:the_nether", &[]).as_deref(),
|
||||
Some("world_nether")
|
||||
);
|
||||
assert_eq!(
|
||||
pick_map(&root.maps, "minecraft:the_end", &[]).as_deref(),
|
||||
Some("world_the_end")
|
||||
);
|
||||
let chosen = [("minecraft:overworld".to_owned(), "survival".to_owned())];
|
||||
assert_eq!(
|
||||
pick_map(&root.maps, "minecraft:overworld", &chosen).as_deref(),
|
||||
Some("survival")
|
||||
);
|
||||
assert_eq!(pick_map(&root.maps, "mod:moon", &[]), None);
|
||||
assert_eq!(
|
||||
base_url(" https://map.example.net/#world:1:2:3 "),
|
||||
"https://map.example.net"
|
||||
);
|
||||
assert_eq!(
|
||||
base_url("http://example.net:8100/index.html"),
|
||||
"http://example.net:8100"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_settings_round_trip() {
|
||||
let server = Settings::parse_server(
|
||||
"play.example.net:25566 https://map.example.net nether=world_nether mod:moon=moon",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
server.maps,
|
||||
[
|
||||
("minecraft:the_nether".to_owned(), "world_nether".to_owned()),
|
||||
("mod:moon".to_owned(), "moon".to_owned())
|
||||
]
|
||||
);
|
||||
let settings = Settings {
|
||||
radius: 32,
|
||||
servers: vec![server.clone()],
|
||||
};
|
||||
let text = settings.serialize();
|
||||
let mut parsed = Settings::default();
|
||||
for line in text.lines() {
|
||||
match line.split_once(':') {
|
||||
Some(("blueMapRadius", v)) => parsed.radius = Settings::parse_radius(v).unwrap(),
|
||||
_ => panic!("{line}: no map in the graphics settings"),
|
||||
}
|
||||
}
|
||||
parsed.servers = Settings::parse_servers(
|
||||
"\u{feff}# a comment\r\nblueMap: play.example.net:25566 https://map.example.net nether=world_nether mod:moon=moon\nlodDistance:64\n",
|
||||
);
|
||||
assert_eq!(parsed, settings);
|
||||
assert!(settings.server("Play.Example.net:25566").is_some());
|
||||
assert!(settings.server("play.example.net").is_none());
|
||||
assert_eq!(Settings::parse_radius("48"), Some(48));
|
||||
assert_eq!(Settings::parse_radius("2000b"), Some(125));
|
||||
assert_eq!(Settings::parse_radius("1024 blocks"), Some(64));
|
||||
assert_eq!(Settings::parse_radius("lots"), None);
|
||||
assert_eq!(
|
||||
Settings::default().radius_blocks(),
|
||||
DEFAULT_RADIUS as i32 * 16
|
||||
);
|
||||
}
|
||||
|
||||
fn palette() -> Palette {
|
||||
let names = [
|
||||
"stone",
|
||||
"dirt",
|
||||
"sand",
|
||||
"grass_block",
|
||||
"oak_leaves",
|
||||
"water",
|
||||
"snow_block",
|
||||
];
|
||||
Palette::new(
|
||||
|name| {
|
||||
let name = name.strip_prefix("minecraft:")?;
|
||||
names
|
||||
.iter()
|
||||
.position(|n| *n == name)
|
||||
.map(|i| BlockStateId(i as u32 + 1))
|
||||
},
|
||||
&[
|
||||
Tints {
|
||||
id: 0,
|
||||
grass: 0x91BD59,
|
||||
foliage: 0x77AB2F,
|
||||
water: 0x3F76E4,
|
||||
},
|
||||
Tints {
|
||||
id: 1,
|
||||
grass: 0xBFB755,
|
||||
foliage: 0xAEA42A,
|
||||
water: 0x3F76E4,
|
||||
},
|
||||
],
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colours_become_the_nearest_block() {
|
||||
let palette = palette();
|
||||
let at = |color: u32, height: i16| {
|
||||
palette.column(Pixel {
|
||||
color: 0xFF00_0000 | color,
|
||||
height,
|
||||
block_light: 0,
|
||||
})
|
||||
};
|
||||
let grass = at(0x577133, 70);
|
||||
let surface = grass.surface.unwrap();
|
||||
assert_eq!(
|
||||
(surface.height, surface.state, surface.biome),
|
||||
(71, BlockStateId(4), 0)
|
||||
);
|
||||
assert_eq!(grass.spans.last().unwrap().max, 71);
|
||||
assert_eq!(
|
||||
grass.spans[grass.spans.len() - 2].state,
|
||||
BlockStateId(2),
|
||||
"dirt under grass"
|
||||
);
|
||||
// A savanna-ish green picks that biome's tint.
|
||||
assert_eq!(at(0x736E33, 70).surface.unwrap().biome, 1);
|
||||
let sea = at(0x2C52A0, 62).surface.unwrap();
|
||||
assert_eq!(sea.water, Some((63, BlockStateId(6))));
|
||||
assert_eq!(sea.state, BlockStateId(3));
|
||||
assert_eq!(at(0xF0F5F5, 90).surface.unwrap().state, BlockStateId(7));
|
||||
assert_eq!(at(0x7A7A7A, -10).surface.unwrap().state, BlockStateId(1));
|
||||
// Nothing rendered there: no column.
|
||||
assert_eq!(
|
||||
palette.column(Pixel {
|
||||
color: 0x00FF_FFFF,
|
||||
height: 64,
|
||||
block_light: 0
|
||||
}),
|
||||
Column::default()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ pub mod block_entities;
|
||||
pub mod block_entity_anim;
|
||||
pub mod block_entity_cem;
|
||||
pub mod block_entity_render;
|
||||
pub mod bluemap;
|
||||
pub mod border_render;
|
||||
pub mod breaking;
|
||||
pub mod brewing;
|
||||
|
||||
@@ -9,19 +9,19 @@ pub mod arrows;
|
||||
pub mod block_contact;
|
||||
pub use horizon_client_render::{
|
||||
assets, atlas_sources, audio, auth, ban_notice, bidi, biome, block_edit, block_entities,
|
||||
block_entity_anim, block_entity_render, border_render, breaking, brewing, camera, chat, chunk,
|
||||
chunk_map, cit, cit_armor, cit_assets, cit_glint, clouds, collisions, command_block,
|
||||
containers, crash, datapack, dynamic_light, effects, environment, extras, fast_hash, fog, font,
|
||||
gfx, graphics, graphics_style, graphics_ui, hdr_calibration, hud, hud_layout, hud_overlays,
|
||||
inventory_effects, java_text, jigsaw_block, json_folder, level_loading, light, lod, logging,
|
||||
menu_blur, mesher, minimap, mipmap, mod_list, model_library, models, multiline_edit,
|
||||
native_emissive, options_file, options_fixes, options_ui, overlay, pack_screen, packs, par,
|
||||
particles, poi_sections, post_chain, potent_sulfur, preedit, providers, screen_draw,
|
||||
screen_effects, sculk, section_mask, server_terrain, settings, shader_options,
|
||||
shader_pack_list, signs, skins, sound_engine, sound_events, sound_preview, spans, split_layout,
|
||||
split_map, split_profiles, sprites, streaming, structure_block, tab_list, telemetry,
|
||||
test_block, test_instance_block, testgen, title_edition, trims, ui, vault, weather, widgets,
|
||||
world, worldborder, zip,
|
||||
block_entity_anim, block_entity_render, bluemap, border_render, breaking, brewing, camera,
|
||||
chat, chunk, chunk_map, cit, cit_armor, cit_assets, cit_glint, clouds, collisions,
|
||||
command_block, containers, crash, datapack, dynamic_light, effects, environment, extras,
|
||||
fast_hash, fog, font, gfx, graphics, graphics_style, graphics_ui, hdr_calibration, hud,
|
||||
hud_layout, hud_overlays, inventory_effects, java_text, jigsaw_block, json_folder,
|
||||
level_loading, light, lod, logging, menu_blur, mesher, minimap, mipmap, mod_list,
|
||||
model_library, models, multiline_edit, native_emissive, options_file, options_fixes,
|
||||
options_ui, overlay, pack_screen, packs, par, particles, poi_sections, post_chain,
|
||||
potent_sulfur, preedit, providers, screen_draw, screen_effects, sculk, section_mask,
|
||||
server_terrain, settings, shader_options, shader_pack_list, signs, skins, sound_engine,
|
||||
sound_events, sound_preview, spans, split_layout, split_map, split_profiles, sprites,
|
||||
streaming, structure_block, tab_list, telemetry, test_block, test_instance_block, testgen,
|
||||
title_edition, trims, ui, vault, weather, widgets, world, worldborder, zip,
|
||||
};
|
||||
pub mod attack_strength;
|
||||
pub mod attribute_updates;
|
||||
|
||||
@@ -850,41 +850,18 @@ impl Game<'_> {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
/// The entities of the level in place a selector can find.
|
||||
/// The entities of the level in place a selector can find: one walk over
|
||||
/// the entity lists (a kind added there joins `@e` once it names its
|
||||
/// `Target`), then the kinds that are not on the walk. An
|
||||
/// unsorted selector lists them in that order.
|
||||
fn level_candidates(&self) -> Vec<Target> {
|
||||
use horizon_client::projectiles::ProjectileKind as K;
|
||||
use super::entity::{Entity as _, entity_lists_of, for_each_entity};
|
||||
let mut candidates = Vec::new();
|
||||
candidates.extend(
|
||||
self.mobs
|
||||
.iter()
|
||||
.filter(|m| m.death_time == 0)
|
||||
.map(|m| Target::Mob(m.id)),
|
||||
);
|
||||
candidates.extend(self.dropped.iter().map(|i| Target::Item(i.id)));
|
||||
candidates.extend(self.orbs.iter().map(|o| Target::Orb(o.id)));
|
||||
candidates.extend(
|
||||
self.creatures
|
||||
.projectiles
|
||||
.iter()
|
||||
// The kinds /data can write (entity_nbt).
|
||||
.filter(|p| {
|
||||
p.is_arrow()
|
||||
|| horizon_client::entity_nbt::saved_with_chunk(p)
|
||||
|| matches!(
|
||||
p.kind,
|
||||
K::LargeFireball { .. }
|
||||
| K::SmallFireball
|
||||
| K::WindCharge
|
||||
| K::BreezeWindCharge
|
||||
)
|
||||
})
|
||||
.map(|p| Target::Projectile(p.id)),
|
||||
);
|
||||
candidates.extend(self.armor_stands.iter().map(|a| Target::Stand(a.id)));
|
||||
for_each_entity!(entity_lists_of!(self), entity => {
|
||||
candidates.extend(entity.selector_target());
|
||||
});
|
||||
candidates.extend(self.item_frames.iter().map(|f| Target::Frame(f.id)));
|
||||
candidates.extend(self.paintings.iter().map(|p| Target::Painting(p.id)));
|
||||
candidates.extend(self.boats.iter().map(|b| Target::Boat(b.id)));
|
||||
candidates.extend(self.carts.iter().map(|c| Target::Cart(c.id)));
|
||||
candidates.extend(self.others.iter().map(|o| Target::Other(o.id)));
|
||||
// The End crystals and the dragon (EndCrystal and EnderDragon
|
||||
// are entities of their own).
|
||||
@@ -892,9 +869,6 @@ impl Game<'_> {
|
||||
if let Some(d) = &self.dragon {
|
||||
candidates.push(Target::Dragon(d.id));
|
||||
}
|
||||
candidates.extend(self.tnts.iter().map(|t| Target::Tnt(t.id)));
|
||||
candidates.extend(self.falling.iter().map(|f| Target::Falling(f.id)));
|
||||
candidates.extend(self.rockets.iter().map(|r| Target::Rocket(r.id)));
|
||||
candidates.extend(self.ominous_spawners.iter().map(|s| Target::Spawner(s.id)));
|
||||
candidates
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//! `Game` end. The walk allocates nothing and dispatch is static: hot paths pay
|
||||
//! no vtable. A new kind is one line in `entity_lists!` and one `impl Entity`.
|
||||
|
||||
use super::commands::Target;
|
||||
use super::{FallingBlock, Game};
|
||||
use super::{
|
||||
boats::Boat, fireworks::Rocket, minecarts::Minecart, tnt::PrimedTnt, trident::ThrownTrident,
|
||||
@@ -53,6 +54,9 @@ pub(super) trait Entity {
|
||||
fn position_mut(&mut self) -> &mut Vec3;
|
||||
fn velocity_mut(&mut self) -> &mut Vec3;
|
||||
fn bounds(&self) -> Aabb;
|
||||
/// What a selector finds this entity as (`@e`), or `None` for one it cannot
|
||||
/// name yet. Required, so a new kind states which.
|
||||
fn selector_target(&self) -> Option<Target>;
|
||||
|
||||
fn on_ground(&self) -> bool {
|
||||
false
|
||||
@@ -360,6 +364,9 @@ impl Entity for Mob {
|
||||
fn can_use_portal(&self, game: &Game<'_>) -> bool {
|
||||
game.mob_can_use_portal(self)
|
||||
}
|
||||
fn selector_target(&self) -> Option<Target> {
|
||||
(self.death_time == 0).then_some(Target::Mob(self.id))
|
||||
}
|
||||
fn bounds(&self) -> Aabb {
|
||||
Mob::bounds(self)
|
||||
}
|
||||
@@ -434,6 +441,9 @@ impl Entity for ItemEntity {
|
||||
fn portal_size(&self) -> Option<(f32, f32)> {
|
||||
Some((0.25, 0.25))
|
||||
}
|
||||
fn selector_target(&self) -> Option<Target> {
|
||||
Some(Target::Item(self.id))
|
||||
}
|
||||
fn thunder_hit(&self) -> ThunderHit {
|
||||
ThunderHit::Item
|
||||
}
|
||||
@@ -466,6 +476,9 @@ impl Entity for ExperienceOrb {
|
||||
fn portal_size(&self) -> Option<(f32, f32)> {
|
||||
Some((0.5, 0.5))
|
||||
}
|
||||
fn selector_target(&self) -> Option<Target> {
|
||||
Some(Target::Orb(self.id))
|
||||
}
|
||||
fn thunder_hit(&self) -> ThunderHit {
|
||||
ThunderHit::Orb
|
||||
}
|
||||
@@ -498,6 +511,9 @@ impl Entity for Boat {
|
||||
fn can_use_portal(&self, game: &Game<'_>) -> bool {
|
||||
game.vehicle_can_use_portal(self.id)
|
||||
}
|
||||
fn selector_target(&self) -> Option<Target> {
|
||||
Some(Target::Boat(self.id))
|
||||
}
|
||||
fn thunder_hit(&self) -> ThunderHit {
|
||||
ThunderHit::Vehicle
|
||||
}
|
||||
@@ -527,6 +543,9 @@ impl Entity for Minecart {
|
||||
fn can_use_portal(&self, game: &Game<'_>) -> bool {
|
||||
game.vehicle_can_use_portal(self.id)
|
||||
}
|
||||
fn selector_target(&self) -> Option<Target> {
|
||||
Some(Target::Cart(self.id))
|
||||
}
|
||||
fn thunder_hit(&self) -> ThunderHit {
|
||||
ThunderHit::Vehicle
|
||||
}
|
||||
@@ -558,6 +577,9 @@ fn armor_stand_size(stand: &ArmorStand) -> (f64, f64) {
|
||||
|
||||
impl Entity for ArmorStand {
|
||||
stored_motion!();
|
||||
fn selector_target(&self) -> Option<Target> {
|
||||
Some(Target::Stand(self.id))
|
||||
}
|
||||
/// `ArmorStand.ignoreExplosion`: a visible stand feels only a burst that
|
||||
/// affects block-like entities (`items_ignore` is that flag, negated).
|
||||
fn ignores_explosion(&self, blast: &Blast) -> bool {
|
||||
@@ -621,6 +643,9 @@ impl Entity for PrimedTnt {
|
||||
fn can_use_portal(&self, game: &Game<'_>) -> bool {
|
||||
game.tnt_can_use_portal(self.id)
|
||||
}
|
||||
fn selector_target(&self) -> Option<Target> {
|
||||
Some(Target::Tnt(self.id))
|
||||
}
|
||||
/// Pushed from its position, not its eyes.
|
||||
fn explosion_eye_height(&self) -> f64 {
|
||||
0.0
|
||||
@@ -641,6 +666,9 @@ impl Entity for PrimedTnt {
|
||||
|
||||
impl Entity for FallingBlock {
|
||||
stored_motion!();
|
||||
fn selector_target(&self) -> Option<Target> {
|
||||
Some(Target::Falling(self.id))
|
||||
}
|
||||
fn bounds(&self) -> Aabb {
|
||||
horizon_client::falling_block::bounds(self.position)
|
||||
}
|
||||
@@ -659,6 +687,17 @@ impl Entity for Projectile {
|
||||
fn portal_sweeps(&self) -> bool {
|
||||
!super::nether_portal::is_arrow(self) || !self.stuck || self.previous != self.position
|
||||
}
|
||||
fn selector_target(&self) -> Option<Target> {
|
||||
use horizon_client::projectiles::ProjectileKind as K;
|
||||
// The kinds /data can write (entity_nbt).
|
||||
let selectable = self.is_arrow()
|
||||
|| horizon_client::entity_nbt::saved_with_chunk(self)
|
||||
|| matches!(
|
||||
self.kind,
|
||||
K::LargeFireball { .. } | K::SmallFireball | K::WindCharge | K::BreezeWindCharge
|
||||
);
|
||||
selectable.then_some(Target::Projectile(self.id))
|
||||
}
|
||||
/// `AbstractArrow.push` does nothing in the ground; a wind charge's motion
|
||||
/// stays 0.
|
||||
fn pushed_by_explosion(&self) -> bool {
|
||||
@@ -697,6 +736,9 @@ impl Entity for Rocket {
|
||||
fn portal_from(&self) -> Option<Vec3> {
|
||||
Some(self.previous)
|
||||
}
|
||||
fn selector_target(&self) -> Option<Target> {
|
||||
Some(Target::Rocket(self.id))
|
||||
}
|
||||
/// 0.25 wide, so 0.2125 to the eyes.
|
||||
fn explosion_eye_height(&self) -> f64 {
|
||||
0.2125
|
||||
@@ -721,6 +763,10 @@ impl Entity for ThrownTrident {
|
||||
fn portal_sweeps(&self) -> bool {
|
||||
!self.in_ground || self.previous != self.position
|
||||
}
|
||||
fn selector_target(&self) -> Option<Target> {
|
||||
// No `Target` variant for it yet: `@e` does not find tridents.
|
||||
None
|
||||
}
|
||||
/// Like any arrow (`AbstractArrow.push`) not pushed while in the ground.
|
||||
fn pushed_by_explosion(&self) -> bool {
|
||||
!self.in_ground
|
||||
|
||||
Reference in new issue
Block a user