mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Merge pull request #10194 from IanWorley/native-api/block-appearance
Native mods: block state facts and the client's resource files, for maps and radars
This commit is contained in:
8 files changed
+490
-19
No files matched your search
@@ -0,0 +1,249 @@
|
||||
//! Every block state's facts and looks by its id, for a client mod drawing blocks or a map off the main thread
|
||||
//! (`native_block_states`; docs/native-mods.md): what the game knows of each state, what its model draws on each side
|
||||
//! and the texture behind each of those sprites. A copy, so the game's own tables can change shape under it.
|
||||
use crate::{
|
||||
block_sprites::{BlockSprites, Lookup, SpriteRef},
|
||||
streaming::WorkerContext,
|
||||
};
|
||||
use horizon_items::blocks::{BlockFacts, MAP_COLORS, RenderShape};
|
||||
use horizon_world::{
|
||||
block_collision::BlockCollisionRegistry, fluid::FluidState, registry::BlockStateId,
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
/// What one block state is, as a renderer or a map reads it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub struct BlockStateFacts {
|
||||
/// Its block, without properties (`minecraft:grass_block`).
|
||||
pub name: Arc<str>,
|
||||
/// `BlockState.isAir`: air, cave air or void air.
|
||||
pub air: bool,
|
||||
/// No model draws it (render shape `INVISIBLE`): air, barriers, light, and water and lava, drawn as fluids.
|
||||
pub invisible: bool,
|
||||
/// `BlockState.getFluidState`: the fluid in it, a waterlogged block's and kelp's included; empty for none.
|
||||
pub fluid: FluidState,
|
||||
/// Its `waterlogged` property is true.
|
||||
pub waterlogged: bool,
|
||||
/// The light it gives, 0 to 15.
|
||||
pub emission: u8,
|
||||
/// `BlockState.getShape`, the outline the cursor picks, as block-local boxes
|
||||
/// `[min_x, min_y, min_z, max_x, max_y, max_z]`: `[0, 0, 0, 1, 1, 1]` for a whole block, none for air. Without
|
||||
/// the random offset of flowers and grass.
|
||||
pub outline: Arc<[[f64; 6]]>,
|
||||
/// `MapColor.col` (`0xRRGGBB`), its colour on a vanilla map; none for `MapColor.NONE`, which maps leave out.
|
||||
pub map_color: Option<u32>,
|
||||
/// It can collide at all: false for a state whose collision shape is always empty (air, flowers, water).
|
||||
pub collides: bool,
|
||||
}
|
||||
|
||||
// A mod builds its tables from these on a worker thread.
|
||||
const _: () = {
|
||||
const fn shareable<T: Send + Sync + 'static>() {}
|
||||
shareable::<BlockStates>()
|
||||
};
|
||||
|
||||
/// Every block state by its id: its facts, and what its model draws with the resources loaded when it was made.
|
||||
#[derive(Clone)]
|
||||
pub struct BlockStates {
|
||||
states: Arc<[BlockStateFacts]>,
|
||||
context: Arc<WorkerContext>,
|
||||
/// Texture names by sprite layer, across every array.
|
||||
textures: Arc<HashMap<u32, String>>,
|
||||
split: u32,
|
||||
}
|
||||
|
||||
impl BlockStates {
|
||||
/// The states of `context`'s registry, with their `facts` and `collision`, and the sprites of `context`'s
|
||||
/// resources at `split` layers to an array (`Renderer::sprite_split`).
|
||||
pub fn new(
|
||||
facts: &BlockFacts,
|
||||
collision: &BlockCollisionRegistry<'_>,
|
||||
context: Arc<WorkerContext>,
|
||||
split: u32,
|
||||
) -> Self {
|
||||
let registry = context.assets.registry;
|
||||
let states = facts
|
||||
.states
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| {
|
||||
let id = BlockStateId(i as u32);
|
||||
BlockStateFacts {
|
||||
name: f.name.clone(),
|
||||
air: facts.is_air(id),
|
||||
invisible: f.render_shape == RenderShape::Invisible,
|
||||
fluid: registry
|
||||
.state(id)
|
||||
.and_then(|state| FluidState::from_block(state).ok())
|
||||
.unwrap_or(FluidState::EMPTY),
|
||||
waterlogged: f.waterlogged,
|
||||
emission: f.emission,
|
||||
outline: f.outline.clone(),
|
||||
map_color: MAP_COLORS
|
||||
.get(usize::from(f.map_color))
|
||||
.copied()
|
||||
.filter(|_| f.map_color != 0),
|
||||
collides: collision.may_collide(id),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
// Vanilla's names go without their namespace, as `native_draw_cube` takes them.
|
||||
let textures = context
|
||||
.render
|
||||
.sprite_layers
|
||||
.iter()
|
||||
.map(|(name, layer)| (*layer, name.trim_start_matches("minecraft:").to_owned()))
|
||||
.collect();
|
||||
Self {
|
||||
states,
|
||||
context,
|
||||
textures: Arc::new(textures),
|
||||
split: split.max(1),
|
||||
}
|
||||
}
|
||||
|
||||
/// How many states there are: their ids run from 0 to one less.
|
||||
pub fn len(&self) -> usize {
|
||||
self.states.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.states.is_empty()
|
||||
}
|
||||
|
||||
/// `state`'s facts; none for an id past the last state.
|
||||
pub fn get(&self, state: BlockStateId) -> Option<&BlockStateFacts> {
|
||||
self.states.get(state.0 as usize)
|
||||
}
|
||||
|
||||
/// Every state with its id, in id order.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (BlockStateId, &BlockStateFacts)> {
|
||||
self.states
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, facts)| (BlockStateId(i as u32), facts))
|
||||
}
|
||||
|
||||
/// What `state` draws on each side, as `native_block_sprites` says for a block by name; none for an id past the
|
||||
/// last state.
|
||||
pub fn sprites(&self, state: BlockStateId) -> Option<BlockSprites> {
|
||||
self.get(state)?;
|
||||
Some(Lookup::new(&self.context, self.split).state(state))
|
||||
}
|
||||
|
||||
/// The texture `sprite` shows, by the name `native_texture_sprites` takes (`block/grass_block_top`; a mod's with
|
||||
/// its namespace, `mymod:block/gear`).
|
||||
pub fn texture(&self, sprite: SpriteRef) -> Option<&str> {
|
||||
let layer = sprite
|
||||
.array
|
||||
.checked_mul(self.split)?
|
||||
.checked_add(sprite.layer)?;
|
||||
self.textures.get(&layer).map(String::as_str)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
assets::Assets, biome::Biomes, block_sprites::parse_state, light::LightTables,
|
||||
mesher::MeshTables,
|
||||
};
|
||||
use horizon_assets::model::Direction;
|
||||
use horizon_world::fluid::FluidKind;
|
||||
use std::{path::PathBuf, sync::OnceLock};
|
||||
|
||||
/// Far fewer layers to an array than a GPU takes, so most sprites lie past array 0.
|
||||
const SMALL_SPLIT: u32 = 64;
|
||||
|
||||
/// The game's own states from real prepared resources, loaded once for every test here.
|
||||
fn states() -> &'static BlockStates {
|
||||
static STATES: OnceLock<BlockStates> = OnceLock::new();
|
||||
STATES.get_or_init(|| {
|
||||
let root = PathBuf::from(
|
||||
std::env::var_os("HORIZON_TEST_RESOURCES").expect("HORIZON_TEST_RESOURCES"),
|
||||
);
|
||||
let assets = Arc::new(Assets::load(&root).unwrap());
|
||||
let render = Arc::new(assets.reload(&root).unwrap());
|
||||
let facts = BlockFacts::load(&root, assets.registry).unwrap();
|
||||
let collision = crate::collisions::load(&root, assets.registry).unwrap();
|
||||
let visibility = crate::models::load_visibility(&root, assets.registry).unwrap();
|
||||
let context = Arc::new(WorkerContext {
|
||||
light: Arc::new(LightTables::load(&root, assets.registry, &facts).unwrap()),
|
||||
tables: Arc::new(MeshTables::new(&assets, &render, &facts, visibility).unwrap()),
|
||||
assets: assets.clone(),
|
||||
render,
|
||||
biomes: Arc::new(Biomes::load(&root).unwrap()),
|
||||
});
|
||||
BlockStates::new(&facts, &collision, context, SMALL_SPLIT)
|
||||
})
|
||||
}
|
||||
|
||||
fn facts(text: &str) -> &'static BlockStateFacts {
|
||||
let states = states();
|
||||
let id = parse_state(states.context.assets.registry, text).expect(text);
|
||||
states.get(id).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn a_grass_block_is_a_whole_solid_grass_coloured_block() {
|
||||
let grass = facts("grass_block");
|
||||
assert_eq!(&*grass.name, "minecraft:grass_block");
|
||||
assert!(!grass.air && !grass.invisible && !grass.waterlogged);
|
||||
assert_eq!(grass.fluid.kind(), FluidKind::Empty);
|
||||
assert_eq!(grass.emission, 0);
|
||||
assert_eq!(&*grass.outline, [[0.0, 0.0, 0.0, 1.0, 1.0, 1.0]]);
|
||||
assert_eq!(grass.map_color, Some(0x7F_B238), "MapColor.GRASS");
|
||||
assert!(grass.collides);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn air_and_light_are_not_drawn() {
|
||||
let air = facts("air");
|
||||
assert!(air.air && air.invisible && !air.collides);
|
||||
assert!(air.outline.is_empty());
|
||||
assert_eq!(air.map_color, None);
|
||||
let light = facts("light[level=15]");
|
||||
assert!(!light.air && light.invisible);
|
||||
assert_eq!(light.emission, 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn a_slabs_outline_is_its_half() {
|
||||
assert_eq!(
|
||||
&*facts("stone_slab[type=bottom]").outline,
|
||||
[[0.0, 0.0, 0.0, 1.0, 0.5, 1.0]]
|
||||
);
|
||||
assert_eq!(
|
||||
&*facts("stone_slab[type=top]").outline,
|
||||
[[0.0, 0.5, 0.0, 1.0, 1.0, 1.0]]
|
||||
);
|
||||
let wet = facts("stone_slab[type=bottom,waterlogged=true]");
|
||||
assert!(wet.waterlogged && wet.collides);
|
||||
assert_eq!(wet.fluid.kind(), FluidKind::Water);
|
||||
assert!(wet.fluid.is_source());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires prepared resources in HORIZON_TEST_RESOURCES"]
|
||||
fn a_face_names_its_texture() {
|
||||
let states = states();
|
||||
let id = parse_state(states.context.assets.registry, "grass_block").unwrap();
|
||||
let grass = states.sprites(id).unwrap();
|
||||
let top = grass.sides[Direction::Up as usize][0].sprite;
|
||||
assert_eq!(states.texture(top), Some("block/grass_block_top"));
|
||||
let north = &grass.sides[Direction::North as usize];
|
||||
assert_eq!(
|
||||
states.texture(north[0].sprite),
|
||||
Some("block/grass_block_side")
|
||||
);
|
||||
assert_eq!(
|
||||
states.texture(north[1].sprite),
|
||||
Some("block/grass_block_side_overlay")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
//! The files of the resources the game loaded, resource packs' over vanilla's, for a client mod reading textures and
|
||||
//! other resources on any thread (`native_client_resources`; docs/native-mods.md).
|
||||
use horizon_core::prepared_files;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// A texture's pixels: `width` by `height` RGBA, rows top first, as its PNG stores them (gamma-encoded, alpha not
|
||||
/// premultiplied). An animated texture is its frames one under another, as in its file.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TexturePixels {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub rgba: Vec<u8>,
|
||||
}
|
||||
|
||||
// A mod reads textures on a worker thread.
|
||||
const _: () = {
|
||||
const fn shareable<T: Send + Sync + 'static>() {}
|
||||
shareable::<ClientResources>()
|
||||
};
|
||||
|
||||
/// The resources the game loaded, as one tree: vanilla's, with the resource packs turned on (a higher pack over a
|
||||
/// lower one) and the content mods' files over it. A pack turned on or off makes a new tree: ask for it again then.
|
||||
/// It reads the files on disk at each call, not a copy: while a reload rebuilds them, a read can miss a file or get
|
||||
/// the new pack's before `native_block_sprites_version()` changes.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ClientResources {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl ClientResources {
|
||||
/// The tree at `root` (the game's `options.resources`).
|
||||
pub fn new(root: PathBuf) -> Self {
|
||||
Self { root }
|
||||
}
|
||||
|
||||
/// The bytes of the resource at `path` in the tree (`assets/minecraft/textures/entity/cow/cow_temperate.png`).
|
||||
/// None when there is no such file, or for a path that isn't plain names under `assets/`.
|
||||
pub fn read(&self, path: &str) -> Option<Vec<u8>> {
|
||||
let plain = |p: &str| !p.is_empty() && p != "." && p != ".." && !p.contains('\\');
|
||||
if !path.starts_with("assets/") || !path.split('/').all(plain) {
|
||||
return None;
|
||||
}
|
||||
prepared_files::read(&self.root.join(path)).ok()
|
||||
}
|
||||
|
||||
/// A texture's pixels by name (`block/stone`, `entity/cow/cow_temperate`, a mod's `mymod:item/gear`): the PNG
|
||||
/// under `assets/<namespace>/textures/`, decoded. None when there is no such PNG or it doesn't decode.
|
||||
pub fn texture(&self, name: &str) -> Option<TexturePixels> {
|
||||
let path = crate::atlas_sources::texture_path(&self.root, name, "textures");
|
||||
let (width, height, rgba) =
|
||||
crate::hud::decode_bytes(&prepared_files::read(&path).ok()?).ok()?;
|
||||
Some(TexturePixels {
|
||||
width,
|
||||
height,
|
||||
rgba,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::packs::{build_overlay, discover};
|
||||
use std::{fs, path::Path};
|
||||
|
||||
const COW: &str = "assets/minecraft/textures/entity/cow/cow_temperate.png";
|
||||
const RED: [u8; 4] = [255, 0, 0, 255];
|
||||
const BLUE: [u8; 4] = [0, 0, 255, 255];
|
||||
|
||||
fn one_pixel_png(rgba: [u8; 4]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
let mut encoder = png::Encoder::new(&mut out, 1, 1);
|
||||
encoder.set_color(png::ColorType::Rgba);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
encoder
|
||||
.write_header()
|
||||
.unwrap()
|
||||
.write_image_data(&rgba)
|
||||
.unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
fn write(path: &Path, bytes: &[u8]) {
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
fs::write(path, bytes).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_resource_pack_overrides_a_texture() {
|
||||
let base =
|
||||
std::env::temp_dir().join(format!("horizon-client-resources-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&base);
|
||||
let vanilla = base.join("vanilla");
|
||||
write(&vanilla.join(COW), &one_pixel_png(RED));
|
||||
let pack = base.join("packs/Blue cows");
|
||||
write(
|
||||
&pack.join("pack.mcmeta"),
|
||||
br#"{"pack":{"description":"blue cows","pack_format":97}}"#,
|
||||
);
|
||||
write(&pack.join(COW), &one_pixel_png(BLUE));
|
||||
let overlay = base.join("overlay");
|
||||
build_overlay(&vanilla, &discover(&vanilla, &base.join("packs")), &overlay).unwrap();
|
||||
|
||||
let (plain, packed) = (ClientResources::new(vanilla), ClientResources::new(overlay));
|
||||
assert_eq!(plain.read(COW), Some(one_pixel_png(RED)));
|
||||
assert_eq!(packed.read(COW), Some(one_pixel_png(BLUE)));
|
||||
assert_eq!(
|
||||
packed.texture("entity/cow/cow_temperate"),
|
||||
Some(TexturePixels {
|
||||
width: 1,
|
||||
height: 1,
|
||||
rgba: BLUE.to_vec(),
|
||||
})
|
||||
);
|
||||
let _ = fs::remove_dir_all(base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_plain_paths_under_assets_are_read() {
|
||||
let base = std::env::temp_dir().join(format!(
|
||||
"horizon-client-resources-paths-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&base);
|
||||
let root = base.join("root");
|
||||
write(&root.join(COW), &one_pixel_png(RED));
|
||||
write(&root.join("horizon-resources.json"), b"{}");
|
||||
write(&base.join("secret.txt"), b"secret");
|
||||
|
||||
let resources = ClientResources::new(root);
|
||||
assert!(resources.read(COW).is_some());
|
||||
for refused in [
|
||||
"horizon-resources.json",
|
||||
"assets/../horizon-resources.json",
|
||||
"assets/../../secret.txt",
|
||||
"assets//minecraft/textures/entity/cow/cow_temperate.png",
|
||||
"/assets/minecraft/textures/entity/cow/cow_temperate.png",
|
||||
] {
|
||||
assert_eq!(resources.read(refused), None, "{refused}");
|
||||
}
|
||||
let _ = fs::remove_dir_all(base);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ pub mod block_entity_anim;
|
||||
pub mod block_entity_cem;
|
||||
pub mod block_entity_render;
|
||||
pub mod block_sprites;
|
||||
pub mod block_states;
|
||||
pub mod bluemap;
|
||||
pub mod bluemap_detect;
|
||||
pub mod border_render;
|
||||
@@ -27,6 +28,7 @@ pub mod cit;
|
||||
pub mod cit_armor;
|
||||
pub mod cit_assets;
|
||||
pub mod cit_glint;
|
||||
pub mod client_resources;
|
||||
pub mod clouds;
|
||||
pub mod collisions;
|
||||
pub mod command_block;
|
||||
|
||||
@@ -10,20 +10,20 @@ pub mod block_contact;
|
||||
pub mod powder_snow;
|
||||
pub use horizon_client_render::{
|
||||
assets, atlas_sources, audio, auth, ban_notice, bidi, biome, block_edit, block_entities,
|
||||
block_entity_anim, block_entity_render, block_sprites, bluemap, bluemap_detect, border_render,
|
||||
breaking, brewing, camera, chat, chunk, chunk_map, cit, cit_armor, cit_assets, cit_glint,
|
||||
clouds, collisions, command_block, containers, controller_ui, 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_browser, mod_list, model_library, models, multiline_edit, native_emissive, options_file,
|
||||
options_fixes, options_ui, overlay, pack_browser, pack_screen, packs, pad_cursor, pad_glyph,
|
||||
par, particles, poi_sections, post_chain, potent_sulfur, preedit, providers, pvp, pvp_hud,
|
||||
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, block_sprites, block_states, bluemap, bluemap_detect,
|
||||
border_render, breaking, brewing, camera, chat, chunk, chunk_map, cit, cit_armor, cit_assets,
|
||||
cit_glint, client_resources, clouds, collisions, command_block, containers, controller_ui,
|
||||
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_browser, mod_list, model_library, models,
|
||||
multiline_edit, native_emissive, options_file, options_fixes, options_ui, overlay,
|
||||
pack_browser, pack_screen, packs, pad_cursor, pad_glyph, par, particles, poi_sections,
|
||||
post_chain, potent_sulfur, preedit, providers, pvp, pvp_hud, 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;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
//! Where blocks' textures are in the block sprite arrays, for a client mod drawing blocks with the player's own
|
||||
//! textures (resource packs included) in its own pass: with `WorldDrawn::block_sprites`, the arrays themselves.
|
||||
//! Also every block state's facts and looks, and the resources' files, for a mod working on them off the main thread.
|
||||
use super::Game;
|
||||
use horizon_client::block_sprites::{BlockSprites, Lookup, SpriteRef};
|
||||
use horizon_client::block_states::BlockStates;
|
||||
use horizon_client::client_resources::ClientResources;
|
||||
use horizon_core::geometry::Direction;
|
||||
use horizon_world::registry::BlockStateId;
|
||||
|
||||
@@ -56,12 +59,30 @@ impl Game<'_> {
|
||||
pub(super) fn native_block_sprites_version(&self) -> u64 {
|
||||
self.pack.context.render.generation
|
||||
}
|
||||
/// For a client mod: every block state by its id, with its facts, what its model draws on each side and the
|
||||
/// texture behind each sprite, as the resources loaded now; a copy to keep and use on any thread. Ask again when
|
||||
/// `native_block_sprites_version` changes. API @API:block_appearance@.
|
||||
pub(super) fn native_block_states(&self) -> BlockStates {
|
||||
BlockStates::new(
|
||||
self.res.facts(),
|
||||
self.res.collision(),
|
||||
self.pack.context.clone(),
|
||||
self.native_sprite_split(),
|
||||
)
|
||||
}
|
||||
/// For a client mod: the files of the resources loaded now, resource packs' over vanilla's, to read textures
|
||||
/// and other resources on any thread. Ask again when `native_block_sprites_version` changes. API
|
||||
/// @API:block_appearance@.
|
||||
pub(super) fn native_client_resources(&self) -> ClientResources {
|
||||
ClientResources::new(self.options.resources.clone())
|
||||
}
|
||||
fn native_sprite_lookup(&self) -> Lookup<'_> {
|
||||
Lookup::new(&self.pack.context, self.native_sprite_split())
|
||||
}
|
||||
fn native_sprite_split(&self) -> u32 {
|
||||
// (no renderer, no arrays: every layer counts as array 0's)
|
||||
let split = self
|
||||
.renderer
|
||||
self.renderer
|
||||
.as_ref()
|
||||
.map_or(u32::MAX, |r| r.sprite_split());
|
||||
Lookup::new(&self.pack.context, split)
|
||||
.map_or(u32::MAX, |r| r.sprite_split())
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,9 @@ use std::sync::{
|
||||
|
||||
pub use crate::biome::{BiomeColors, SpawnEntry};
|
||||
pub use crate::block_sprites::{BlockSprites, FaceSprite, SpriteRef};
|
||||
pub use crate::block_states::{BlockStateFacts, BlockStates};
|
||||
pub use crate::camera::{CameraEntity, FovPlayer};
|
||||
pub use crate::client_resources::{ClientResources, TexturePixels};
|
||||
pub use crate::command::Suggestion;
|
||||
pub use crate::containers::{ModBlockEntity, ModContainer, ModEnergyBuffer, ModFluidTanks};
|
||||
pub use crate::crafting::{CustomRecipe, Ingredient};
|
||||
@@ -70,7 +72,7 @@ pub use horizon_world::{
|
||||
attachments::{
|
||||
AttachmentCodec, AttachmentError, AttachmentHost, AttachmentType, Attachments, Holder,
|
||||
},
|
||||
fluid::FluidState,
|
||||
fluid::{FluidKind, FluidState},
|
||||
level::{BlockId, Dimension as LevelDimension, PlacementContext},
|
||||
loot::{LootStack, NativeLootContext},
|
||||
nbt::{Compound, Tag},
|
||||
|
||||
@@ -3446,6 +3446,48 @@ as the terrain samples them. API 380.
|
||||
|
||||
Look blocks up once and keep what you need (a few words a block for your own shader), not every frame.
|
||||
|
||||
### Block states and resource files
|
||||
|
||||
`crates/horizon-client/src/game/native_block_sprites.rs`, `horizon_client::block_states` and
|
||||
`horizon_client::client_resources`: for a client mod working out what every block looks like (a map's colours, a
|
||||
voxel renderer's table) or reading textures, on a thread of its own. `native_block_states()` returns a copy to keep,
|
||||
and `native_client_resources()` a handle that reads the files on disk at each call. Both are cheap to clone, `Send` and
|
||||
`Sync`: fetch them on the game's thread, then use them anywhere. Both follow the resources loaded at the time, so ask
|
||||
again when `native_block_sprites_version()` changes (`native_client_resources_reloaded` hears the same reloads, but not
|
||||
the first load). A reload rebuilds the tree's files in place, so while it runs a read can miss a file or get the new
|
||||
pack's before the version changes: read again after it does.
|
||||
|
||||
- `native_block_states()` (`BlockStates`) holds every block state by its id (`BlockStateId`, as the world stores
|
||||
blocks): `len()`, `get(id)` and `iter()` give each state's `BlockStateFacts`:
|
||||
- `name`: its block, without properties (`minecraft:grass_block`);
|
||||
- `air`: air, cave air or void air (`BlockState.isAir`);
|
||||
- `invisible`: no model draws it (render shape `INVISIBLE`: air, barriers, light, and water and lava, which are drawn
|
||||
as fluids);
|
||||
- `fluid`: `BlockState.getFluidState` (`FluidState`, whose `kind()` is a `FluidKind`), a waterlogged block's water
|
||||
and kelp's included;
|
||||
- `waterlogged`: its `waterlogged` property is true;
|
||||
- `emission`: the light it gives, 0 to 15;
|
||||
- `outline`: `BlockState.getShape`, the outline the cursor picks, as block-local `[min_x, min_y, min_z, max_x,
|
||||
max_y, max_z]` boxes (`[0, 0, 0, 1, 0.5, 1]` for a bottom slab, none for air), without flowers' random offset;
|
||||
- `map_color`: `MapColor.col` as `0xRRGGBB`, its colour on a vanilla map, none for `MapColor.NONE`;
|
||||
- `collides`: whether it can collide at all (false for a state whose collision shape is always empty: flowers,
|
||||
water). `native_block_collision(pos)` gives a placed block's boxes.
|
||||
|
||||
`sprites(id)` says what the state's model draws on each side, as `native_block_sprites` does for a block by name,
|
||||
and `texture(sprite)` names the texture behind any of its sprites (`block/grass_block_top`: vanilla's without their
|
||||
namespace, a mod's with it), the name `native_texture_sprites` and `native_draw_cube` take.
|
||||
- `native_client_resources()` (`ClientResources`) reads the resources the game loaded as one tree: vanilla's, with the
|
||||
resource packs turned on (a higher pack over a lower one) and the content mods' files over it. `read(path)` gives a
|
||||
file's bytes by its path in the tree (`assets/minecraft/textures/entity/cow/cow_temperate.png`, a texture's
|
||||
`.png.mcmeta`), only for plain names under `assets/`; `texture(name)` the decoded pixels of a texture by name
|
||||
(`block/stone`, `entity/cow/cow_temperate`, `mymod:item/gear`), as `TexturePixels`: `width` by `height` RGBA, rows
|
||||
top first, as its PNG stores them. An animated texture is its frames one under another. Both are none for a file
|
||||
that isn't there.
|
||||
|
||||
A face's colour on a map, say: `states.sprites(id)` for its top, `states.texture(face.sprite)` for its name, and
|
||||
`resources.texture(name)` for its pixels, averaged as the mod likes. Many states share a texture: decode each one
|
||||
once.
|
||||
|
||||
### Menu extra data
|
||||
|
||||
A content mod's menu type opened by its block can carry bytes of the mod's own to the screen, as NeoForge's
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"hooks": [],
|
||||
"helpers": [
|
||||
{"name": "block_states", "target": "horizon_client::game::native_block_sprites::Game<'_>::native_block_states"},
|
||||
{"name": "client_resources", "target": "horizon_client::game::native_block_sprites::Game<'_>::native_client_resources"}
|
||||
],
|
||||
"rust_summary": "every block state's facts and looks by its id, and the resources' files, for client mods working off the main thread (`native_block_states`, `native_client_resources`).",
|
||||
"doc_summary": "[block states and resource files](#block-states-and-resource-files) for client mods, to keep and use on any thread: `native_block_states()` gives every block state by its id with its facts (name, air, fluid, light, outline, map colour, waterlogged, whether it collides), what its model draws on each side and the texture behind each sprite; `native_client_resources()` reads the resources' files, resource packs' over vanilla's, as bytes or a texture's decoded pixels. Bundles need \"hooks\": @API:block_appearance@.",
|
||||
"comments": [
|
||||
"crates/horizon-client/src/game/native_block_sprites.rs"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user