Compare commits

...
Author SHA1 Message Date
Ludovic Marie 1d161337d0 fix for #3355: audio and sound_engine into the render crate 2026-10-28 23:59:49 +01:00
Ludovic Marie 52011a52f0 union: #3417 2026-10-28 23:45:38 +01:00
Ludovic Marie 66fa994370 union: #3416 2026-10-28 23:45:38 +01:00
Ludovic Marie e3791b7931 union: #3355 2026-10-28 23:45:38 +01:00
Ludovic Marie ca7692cf15 union: #3203 2026-10-28 23:45:38 +01:00
Ludovic MarieandClaude Opus 5.5 78e449714a Merge refactor/spawner-hive-traits (B8 with main) into refactor/horizon-client-render
crash.rs: main's separator-neutral manifest path, with the render crate's suffix.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 23:43:22 +01:00
Ludovic MarieandClaude Opus 5.5 ab0f591673 Merge origin/main into refactor/spawner-hive-traits
#3064's TrialConfig::encode and TrialSpawner::override_entity become
methods of TrialConfigExt and TrialSpawnerExt; game/spawner.rs (#3064)
and lan/chunks.rs (#2952) import the extension traits they now need.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 23:42:52 +01:00
Ludovic Marie 142388e1c1 Merge pull request #3361 from horizon-rust/launcher-mixins-0d6b103
🤖 Agent: MIXINS_REV bump to 0d6b103; launcher builds green on Linux, macOS and Windows at this head.
2026-10-28 23:31:50 +01:00
Hector EcclesandOpenCode a1542cb263 Give a single-player world its icon from the first frame
GameRenderer.tryTakeScreenshotIfNeeded and takeAutoScreenshot: once a
second, a world with no icon.png takes the centre square of its first
fully meshed frame, resampled to 64x64. Only the window's own
single-player world qualifies; joining a server clears it.

The snapshot runs after the hand and screen effects and before the
entity outline and post chain, as vanilla does, so the icon is the
scene rather than the GUI.

Not bit-identical to vanilla: the resize is a block average rather than
STB's filtered one, the PNG is written by the png crate, and the alpha
channel is forced opaque. The crop, size, timing and "only if missing"
rule match, so the icons look the same.

Co-Authored-By: OpenCode (DeepSeek V4.1 Flash) <[email protected]>
2026-10-28 18:23:21 -04:00
Ludovic Marie 93b7ddd09e Merge branch 'main' into launcher-mixins-0d6b103 2026-10-28 23:21:42 +01:00
d3grandoandClaude Opus 5.5 8b94f011c6 Name tags: draw text as Font.prepareText does (bold, tall glyphs)
Name tags had their own glyph loop: every glyph an 8x8 quad at the line
top and no style. accented.png's 9x12 glyphs (й, Й, Ё...) were squeezed
into the line, bold, italic, underline, strikethrough, obfuscated and the
font style were ignored, the line was centred at -trunc(width / 2) and the
background stopped 8 pixels below the line top.

Vanilla (SubmitNodeCollection.submitNameTag -> TextFeatureRenderer ->
Font.prepareText) draws them like any text, centred at -Font.width / 2,
on a background from y - 1 to y + 9 (PreparedTextBuilder.markBackground).
HudBatch::name_tag lays the line out with text_styled, and names.rs maps
its quads into the world. Fixes #3084.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:03:36 +07:00
Ludovic Marie b276d65b5f Merge branch 'main' into launcher-mixins-0d6b103 2026-10-28 22:13:02 +01:00
Ludovic MarieandClaude Opus 5.5 5a2d71d2ea launcher: build native mods with horizon-mixins 0d6b103
levzzz5154's update (optional public visibility for support modules); bundles that
pin their own mixins revision keep it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 21:09:24 +01:00
Gitlab Main 4564f7f0e6 options: choose the audio output device in Music & Sounds (#1035)
Adds audio output device selection matching vanilla options.txt format:
- Bind soundDevice option in Settings with serialization roundtrip
- Enumerate output devices and support live stream switching in Audio
- Add SoundDevice option button to Music & Sounds page cycling devices
- Apply audio device changes dynamically on options save and startup
- Unit tests for settings parsing and options UI button cycling
2026-10-29 00:42:10 +05:30
Ludovic MarieandClaude Opus 5.5 d2e7a54082 rustfmt: wrap horizon-client's re-export of the render modules
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 14:32:21 +01:00
SenjinandClaude Opus 5.5 380d5ebe9f HDR: its settings get a screen of their own
Paper White and Peak Brightness sat among the display settings, where players
without an HDR display took them for settings of their own display. Display now
has only HDR Settings..., greyed where the display path offers no HDR output (the
renderer tells the settings when it opens its surface, `graphics::offer_hdr`).
It opens Graphics: HDR: the HDR switch, then its sliders, greyed while it is off.

Where HDR isn't offered, `horizon-graphics.txt`'s HDR settings are left as saved
and not used: the renderer already keeps HDR off there, and the screen no longer
opens, so a game folder copied from another computer, or a display swapped for
an SDR one, shows nothing that does nothing; an HDR display brings them back.

The screen leaves room for a calibration step and an in-game shortcut to it
later. horizon-ctl opens it as `options graphics-hdr`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:58:55 +01:00
Ludovic MarieandClaude Opus 5.5 a7efe597ea refactor: the client's world data, rendering and screens become horizon-client-render
80 modules of horizon-client's lib (chunks, meshing, light, streaming, gfx, the HUD,
menus, settings, resource packs, particles, block entity data) and its 8 WGSL shaders move
to crates/horizon-client-render with git mv. Nothing in them reaches gameplay. horizon-client
re-exports each under its old path, so horizon_client::gfx and the rest are unchanged.

Beyond the moves: ParsedModels, lod's Column::reduced/push and the spawner's Next,
Spawner.next and Selector.current become pub; Properties::for_tests sits behind a
test-support feature; crash.rs strips the new crate's path; tools and docs point at the
new paths, and three harnesses that hash every client source hash the new crate too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 13:23:12 +01:00
Ludovic Marie f70501aa42 Merge branch 'refactor/cut-biome-spawn-parsers' into refactor/horizon-client-render 2026-10-28 13:13:50 +01:00
Ludovic Marie d0a29a0f66 Merge branch 'refactor/furnace-cooking' into refactor/spawner-hive-traits 2026-10-28 13:13:03 +01:00
Ludovic MarieandClaude Opus 5.5 f6807fe7cd refactor: spawner, trial spawner and beehive logic as extension traits
Move-only step B8 of the horizon-client crate split. The spawner, trial
spawner and beehive data live in chunk::block_entity since the
BlockEntity cut; their behaviour needs mobs, so it stays above. Once the
data is in its own crate, Rust allows no inherent impl outside it, so
each impl block becomes an extension trait in the same module, with the
same method bodies:

- beehive: OccupantExt, BeehiveExt
- mobs::spawner: SpawnDataExt, SpawnerExt
- mobs::trial_spawner: TrialConfigExt, TrialSpawnerExt

The Default impls of Spawner, TrialConfig and TrialSpawner (plain field
values) move next to their structs, since a foreign trait impl must sit
with its type. Callers add one `use` for the trait.

No behaviour change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 20:00:59 +01:00
206 changed files with 1268 additions and 463 deletions

No files matched your search

+4 -2
View File
@@ -35,11 +35,12 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| Crate | Owns |
|---|---|
| `horizon-client` | The game: a lib (`src/lib.rs`, ~170 flat modules) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `horizon-client` | The game: a lib (`src/lib.rs`, ~100 flat modules: gameplay, networking, title screens) and the bin (`src/main.rs` → `game.rs` + `game/`), plus `horizon-ctl` (`src/bin/horizon-ctl/`). Also the dedicated server (`--dedicated`) |
| `horizon-items` | Blocks and items below gameplay: `blocks.rs` (block facts), `items.rs`, `item_data.rs` (components), `enchant.rs`, `cooldowns.rs`, `bundle.rs`, `item_nbt.rs`. Re-exported as `horizon_client::items`... |
| `horizon-command` | Command parsing (Brigadier): the vanilla tree, argument types, selectors, suggestions, the tree's network form. Re-exported as `horizon_client::command` |
| `horizon-client-core` | The client lib's bottom layer: `text.rs`, `lang.rs`, `snbt.rs`, `nbt_path.rs`, `component.rs`, `scoreboard.rs`, `bossbar.rs`, `gamerules.rs`, `block_entity_nbt.rs`, registry/particle/sound tables. Re-exported under the old paths (`horizon_client::text`...) |
| `horizon-client-input` | Key bindings and key names, toggle keys, mouse options, controller settings and glyphs, the clipboard, window options (`keybinds.rs`, `gamepad.rs`, `mouse.rs`, `window.rs`...). Re-exported under the old paths |
| `horizon-client-render` | The client's world data, rendering and screens (chunks, meshing, light, streaming, `gfx`, HUD, menus, settings): ~80 modules that horizon-client re-exports under the same paths. Nothing in it reaches gameplay |
| `horizon-world` | Vanilla world data with no gameplay loop: NBT, regions, chunks, `datafix/`, `worldgen/`, `level/` (server-side block behaviour, ticks, redstone), entity physics, items, effects, food, damage |
| `horizon-core` | Simulation primitives: Java `Random`s, `jmath` (HotSpot-exact `Math`), positions, noise, density, paletted storage, `version.rs` (the pinned version) |
| `horizon-protocol` | Network protocol 777: codecs, framing, encryption, `packets/{handshake,status,login,configuration,play}.rs`, `entity_data.rs` |
@@ -110,7 +111,8 @@ python3 tools/agent_control_e2e.py # horizon-ctl / MCP end to end
## horizon-client: where things live
`L:` = lib (`crates/horizon-client/src/`), `G:` = bin (`crates/horizon-client/src/game/`). The lib can't see
`L:` = lib (`crates/horizon-client/src/`, or `crates/horizon-client-render/src/` for the modules that crate holds, under
the same `horizon_client::<module>` paths), `G:` = bin (`crates/horizon-client/src/game/`). The lib can't see
`Game`; bin code reaches the lib as `horizon_client::<module>`.
| Subsystem | Files |
Generated
+43 -2
View File
@@ -1378,7 +1378,6 @@ dependencies = [
"ab_glyph",
"bytemuck",
"chrono",
"cpal",
"flate2",
"gilrs",
"glam",
@@ -1386,6 +1385,7 @@ dependencies = [
"horizon-bench",
"horizon-client-core",
"horizon-client-input",
"horizon-client-render",
"horizon-command",
"horizon-core",
"horizon-dualsense",
@@ -1412,7 +1412,6 @@ dependencies = [
"serde_json",
"sha1",
"sha2",
"symphonia",
"unicode-bidi",
"ureq",
"wgpu",
@@ -1441,6 +1440,48 @@ dependencies = [
"winit",
]
[[package]]
name = "horizon-client-render"
version = "0.1.0"
dependencies = [
"ab_glyph",
"bytemuck",
"chrono",
"cpal",
"flate2",
"glam",
"horizon-assets",
"horizon-bench",
"horizon-client-core",
"horizon-client-input",
"horizon-command",
"horizon-core",
"horizon-hdr",
"horizon-items",
"horizon-protocol",
"horizon-shader-api",
"horizon-shaders",
"horizon-telemetry",
"horizon-theme",
"horizon-threads",
"horizon-world",
"log",
"miniz_oxide 0.8.9",
"naga",
"png",
"pollster",
"rustix 1.1.5",
"serde",
"serde_json",
"sha2",
"symphonia",
"unicode-bidi",
"ureq",
"wgpu",
"winit",
"zip",
]
[[package]]
name = "horizon-command"
version = "0.1.0"
+1
View File
@@ -9,6 +9,7 @@ members = [
"crates/horizon-client",
"crates/horizon-client-core",
"crates/horizon-client-input",
"crates/horizon-client-render",
"crates/horizon-command",
"crates/horizon-items",
"crates/horizon-server-files",
+67
View File
@@ -0,0 +1,67 @@
[package]
name = "horizon-client-render"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version = "1.90"
description = "The client's world data, rendering and screens: chunks, meshing, light, streaming, the renderer, the HUD, menus and settings; no gameplay"
[dependencies]
# Sound (ALSA on Linux); the `audio` feature.
cpal = { version = "0.16", optional = true }
symphonia = { version = "0.5", default-features = false, features = ["ogg", "vorbis"] }
horizon-assets = { path = "../horizon-assets" }
horizon-bench = { path = "../horizon-bench" }
horizon-client-core = { path = "../horizon-client-core" }
horizon-client-input = { path = "../horizon-client-input" }
horizon-command = { path = "../horizon-command" }
horizon-core = { path = "../horizon-core" }
horizon-hdr = { path = "../horizon-hdr" }
horizon-items = { path = "../horizon-items" }
horizon-protocol = { path = "../horizon-protocol" }
horizon-shader-api = { path = "../horizon-shader-api" }
horizon-shaders = { path = "../horizon-shaders" }
horizon-telemetry = { path = "../horizon-telemetry", default-features = false, features = ["live"] }
horizon-theme = { path = "../horizon-theme" }
horizon-threads = { path = "../horizon-threads" }
horizon-world = { path = "../horizon-world" }
ab_glyph = "0.2.32"
bytemuck = { version = "1.24", features = ["derive"] }
chrono = { version = "0.4.42", default-features = false, features = ["clock", "std"] }
flate2 = { version = "1.1.10", default-features = false, features = ["zlib-rs", "runtime_detection"] }
glam = "0.30"
log = "0.4"
miniz_oxide = "0.8"
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out"] }
png = "0.18"
pollster = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0.151", features = ["raw_value"] }
sha2 = "0.10.9"
unicode-bidi = "0.3.18"
ureq = { version = "2", default-features = false, features = ["tls"] }
wgpu = { version = "30.0.1", default-features = false, features = ["std", "parking_lot", "vulkan", "metal", "wgsl", "naga-ir"] }
winit = "0.30.13"
zip = { version = "2", default-features = false, features = ["deflate"] }
[target.'cfg(unix)'.dependencies]
rustix = { version = "1", default-features = false, features = ["std", "time"] }
[target.'cfg(windows)'.dependencies]
wgpu = { version = "30.0.1", default-features = false, features = ["dx12"] }
[target.'cfg(not(target_os = "macos"))'.dependencies]
wgpu = { version = "30.0.1", default-features = false, features = ["gles"] }
[features]
# Sound output (horizon-client's `audio` feature).
audio = ["dep:cpal"]
# Helpers horizon-client's tests build their fixtures with.
test-support = []
[lints]
workspace = true
[dev-dependencies]
horizon-items = { path = "../horizon-items", features = ["test-support"] }
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out", "msl-out", "spv-out"] }
@@ -42,11 +42,11 @@ impl HeightmapState for StateFacts {
}
#[derive(Clone)]
pub struct Properties(Arc<Vec<StateFacts>>);
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
impl Properties {
/// Heightmap facts for tests, per state: (air, has a fluid, blocks
/// motion, blocks motion but is not leaves). State 0 is minecraft:air.
pub(crate) fn for_tests(states: &[(bool, bool, bool, bool)]) -> Self {
pub fn for_tests(states: &[(bool, bool, bool, bool)]) -> Self {
Self(Arc::new(
states
.iter()
@@ -144,6 +144,7 @@ type Stream = cpal::Stream;
type Stream = std::convert::Infallible;
pub struct Audio {
_stream: Option<Stream>,
device_name: String,
pub engine: SoundEngine,
backend: MixerBackend,
/// Record `Heard` sounds (the subtitle overlay is listening).
@@ -543,15 +544,47 @@ fn build_stream<T: cpal::SizedSample + cpal::FromSample<f32>>(
)?;
Ok(stream)
}
/// Available audio output device names.
#[cfg(feature = "audio")]
pub fn output_devices() -> Vec<String> {
if output_disabled(|name| std::env::var_os(name)).is_some() {
return Vec::new();
}
let host = cpal::default_host();
let mut names = Vec::new();
if let Ok(devices) = host.output_devices() {
for device in devices {
if let Ok(name) = device.name()
&& !names.contains(&name)
{
names.push(name);
}
}
}
names
}
#[cfg(not(feature = "audio"))]
pub fn output_devices() -> Vec<String> {
Vec::new()
}
impl Audio {
pub fn new(root: &Path) -> Self {
Self::with_output(root, true)
Self::with_device(root, "")
}
pub fn with_device(root: &Path, device_name: &str) -> Self {
Self::with_output(root, true, device_name)
}
/// No output device: the dedicated server.
pub fn silent(root: &Path) -> Self {
Self::with_output(root, false)
Self::with_output(root, false, "")
}
fn with_output(root: &Path, output: bool) -> Self {
/// Active audio device name, or empty for system default.
pub fn device(&self) -> &str {
&self.device_name
}
fn with_output(root: &Path, output: bool, target_device: &str) -> Self {
let registry = SoundRegistry::load(root);
if output && registry.is_empty() {
// The game then plays nothing on a working output (#1035, #1076).
@@ -564,23 +597,24 @@ impl Audio {
}
let mixer = Arc::new(Mutex::new(Mixer::default()));
let opened = if output {
Self::open(mixer.clone())
Self::open_named(mixer.clone(), target_device)
} else {
Err("no output on a dedicated server".into())
};
let (stream, live) = match opened {
Ok(v) => (Some(v), true),
let (stream, live, actual_device) = match opened {
Ok((v, dev)) => (Some(v), true, dev),
Err(e) => {
if output {
log::warn!("Horizon: no sound: {e}");
}
(None, false)
(None, false, String::new())
}
};
let mut engine = SoundEngine::new(registry);
engine.loaded = live;
Self {
_stream: stream,
device_name: actual_device,
engine,
backend: MixerBackend {
mixer,
@@ -602,6 +636,63 @@ impl Audio {
}
}
#[cfg(not(feature = "audio"))]
fn open_named(_: Arc<Mutex<Mixer>>, _: &str) -> Result<(Stream, String)> {
Err("this build has no sound output (built without the `audio` feature)".into())
}
#[cfg(feature = "audio")]
fn open_named(mixer: Arc<Mutex<Mixer>>, target: &str) -> Result<(Stream, String)> {
if let Some(reason) = output_disabled(|name| std::env::var_os(name)) {
return Err(reason.into());
}
let host = cpal::default_host();
if !target.is_empty()
&& let Ok(devices) = host.output_devices()
{
for device in devices {
let name = device.name().unwrap_or_else(|_| "unnamed device".into());
if name == target {
match open_device(&device, mixer.clone()) {
Ok((stream, config)) => {
log::info!(
"Horizon: audio output {name} ({} Hz, {} channels, {})",
config.sample_rate().0,
config.channels(),
config.sample_format()
);
return Ok((stream, target.to_owned()));
}
Err(e) => {
log::warn!(
"Horizon: could not open audio device '{target}': {e}; falling back to default"
);
}
}
break;
}
}
}
let stream = Self::open(mixer)?;
Ok((stream, String::new()))
}
#[cfg(feature = "audio")]
pub fn set_device(&mut self, name: &str) -> Result<()> {
if !self.backend.live && self._stream.is_none() && !self.engine.loaded {
self.device_name = name.to_owned();
return Ok(());
}
let (stream, actual) = Self::open_named(self.backend.mixer.clone(), name)?;
self._stream = Some(stream);
self.device_name = actual;
self.backend.live = true;
self.engine.loaded = true;
Ok(())
}
#[cfg(not(feature = "audio"))]
pub fn set_device(&mut self, name: &str) -> Result<()> {
self.device_name = name.to_owned();
Ok(())
}
#[cfg(not(feature = "audio"))]
fn open(_: Arc<Mutex<Mixer>>) -> Result<Stream> {
Err("this build has no sound output (built without the `audio` feature)".into())
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -230,7 +230,7 @@ pub struct Info {
/// `VibrationSelector`: the closest, then highest-frequency candidate of a tick.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Selector {
pub(crate) current: Option<(Info, i64)>,
pub current: Option<(Info, i64)>,
}
/// `VibrationSystem.Data`.
#[derive(Clone, Debug, Default, PartialEq)]
@@ -278,7 +278,7 @@ pub struct SpawnData {
/// Which `SpawnData` is next: one of the potentials (the same object in
/// vanilla, so changing it changes the potential) or its own.
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum Next {
pub enum Next {
Potential(usize),
Own(SpawnData),
}
@@ -288,7 +288,7 @@ pub struct Spawner {
pub spawn_delay: i32,
/// `SpawnPotentials`: data and weight.
pub potentials: Vec<(SpawnData, i32)>,
pub(crate) next: Option<Next>,
pub next: Option<Next>,
pub min_spawn_delay: i32,
pub max_spawn_delay: i32,
pub spawn_count: i32,
@@ -301,6 +301,24 @@ pub struct Spawner {
pub spin: f64,
pub o_spin: f64,
}
impl Default for Spawner {
fn default() -> Self {
Self {
spawn_delay: 20,
potentials: Vec::new(),
next: None,
min_spawn_delay: 200,
max_spawn_delay: 800,
spawn_count: 4,
max_nearby_entities: 6,
required_player_range: 16,
spawn_range: 4,
client_delay: 20,
spin: 0.0,
o_spin: 0.0,
}
}
}
/// `TrialSpawnerConfig`.
#[derive(Clone, Debug, PartialEq)]
pub struct TrialConfig {
@@ -314,6 +332,26 @@ pub struct TrialConfig {
pub loot_tables_to_eject: Vec<(String, i32)>,
pub items_to_drop_when_ominous: String,
}
impl Default for TrialConfig {
/// `TrialSpawnerConfig.Builder`'s defaults.
fn default() -> Self {
Self {
spawn_range: 4,
total_mobs: 6.0,
simultaneous_mobs: 2.0,
total_mobs_added_per_player: 2.0,
simultaneous_mobs_added_per_player: 1.0,
ticks_between_spawn: 40,
spawn_potentials: Vec::new(),
loot_tables_to_eject: vec![
("minecraft:spawners/trial_chamber/consumables".into(), 1),
("minecraft:spawners/trial_chamber/key".into(), 1),
],
items_to_drop_when_ominous:
"minecraft:spawners/trial_chamber/items_to_drop_when_ominous".into(),
}
}
}
/// A `Holder<TrialSpawnerConfig>` as saved: a registry id
/// (`minecraft:trial_chamber/breeze/normal`) or an inline config.
#[derive(Clone, Debug, Default, PartialEq)]
@@ -347,6 +385,28 @@ pub struct TrialSpawner {
/// and their weights (the rolled counts).
pub dispensing: Option<Vec<ItemStack>>,
}
impl Default for TrialSpawner {
fn default() -> Self {
Self {
normal: ConfigSlot::default(),
ominous: ConfigSlot::default(),
target_cooldown_length: 36000,
required_player_range: 14,
detected_players: Vec::new(),
current_mobs: Vec::new(),
cooldown_ends_at: 0,
next_mob_spawns_at: 0,
total_mobs_spawned: 0,
next_spawn_data: None,
ejecting_loot_table: None,
is_ominous: false,
has_display: false,
spin: 0.0,
o_spin: 0.0,
dispensing: None,
}
}
}
/// `VaultConfig`.
#[derive(Clone, Debug, PartialEq)]
pub struct VaultConfig {
@@ -330,7 +330,7 @@ pub fn stack_lines(backtrace: &str, from_panic: bool) -> String {
// separator the build machine used.
let manifest = env!("CARGO_MANIFEST_DIR").replace('\\', "/");
let root = manifest
.strip_suffix("crates/horizon-client")
.strip_suffix("crates/horizon-client-render")
.unwrap_or(&manifest);
let mut frames: Vec<(String, Option<String>)> = Vec::new();
for line in backtrace.lines() {
@@ -605,7 +605,7 @@ mod tests {
);
assert_eq!(
stack_lines(&bt, true),
"horizon_client::crash::debug_crash_tick (crates/horizon-client/src/crash.rs:359:9)\nwinit::dispatch (winit-0.30.13/src/event_loop.rs:642:56)\nmain\n"
"horizon_client::crash::debug_crash_tick (crates/horizon-client-render/src/crash.rs:359:9)\nwinit::dispatch (winit-0.30.13/src/event_loop.rs:642:56)\nmain\n"
);
}
File renamed without changes.
File renamed without changes.
@@ -574,6 +574,9 @@ pub struct Renderer {
pub screenshot: Option<PathBuf>,
/// Outcome of the last screenshot: frame size, or the error.
pub screenshot_result: Option<std::result::Result<[u32; 2], String>>,
/// Write the next frame's centre square as the world's 64x64 `icon.png`
/// (GameRenderer.takeAutoScreenshot).
pub world_icon: Option<PathBuf>,
/// Brightness option (lightmap gamma), 0..1.
pub gamma: f32,
/// Optional full-light lightmap (Horizon Extras).
@@ -1128,7 +1131,9 @@ impl Renderer {
));
}
surface.configure(&device, &config);
(config, caps.present_modes.clone(), hdr::output(&caps))
let output = hdr::output(&caps);
crate::graphics::offer_hdr(output.is_some());
(config, caps.present_modes.clone(), output)
} else {
let format = wgpu::TextureFormat::Bgra8Unorm;
let config = wgpu::SurfaceConfiguration {
@@ -2669,6 +2674,7 @@ impl Renderer {
started: Instant::now(),
screenshot: None,
screenshot_result: None,
world_icon: None,
gamma: 0.5,
fullbright: false,
animated_textures: true,
@@ -5113,6 +5119,18 @@ impl Renderer {
pass.draw_indexed(0..6, (i * 4) as i32, 0..1);
}
}
// GameRenderer.tryTakeScreenshotIfNeeded runs after the level (hand and
// screen effects included) and before the entity outline and the post
// chain: the icon is the plain scene, with no GUI either.
let eight_bit = self.config.format.block_copy_size(None) == Some(4);
let icon = self.world_icon.take().and_then(|path| {
if eight_bit {
Some((path, self.copy_frame(&mut encoder, &target_texture)))
} else {
log::warn!("Couldn't save auto screenshot: no 32-bit surface format");
None
}
});
// GameRenderer.render3dHud: the debug crosshair after the hand and
// the screen effects.
if crosshair > 0 {
@@ -5192,7 +5210,7 @@ impl Renderer {
let readback = self.screenshot.take().and_then(|path| {
// The copy and PNG writer both assume 8-bit pixels; a float surface would make
// the row stride too small for the copy and abort in validation.
if self.config.format.block_copy_size(None) == Some(4) {
if eight_bit {
Some((path, self.copy_frame(&mut encoder, &target_texture)))
} else {
self.screenshot_result = Some(Err(format!(
@@ -5233,6 +5251,11 @@ impl Renderer {
.map_err(|e| e.to_string()),
);
}
if let Some((path, buffer)) = icon
&& let Err(e) = self.write_world_icon(&path, &buffer)
{
log::warn!("Couldn't save auto screenshot: {e}");
}
let present = Instant::now();
if let Some(surface_frame) = surface_frame {
// On Wayland this ties RedrawRequested to the compositor's frame
@@ -5313,6 +5336,16 @@ impl Renderer {
buffer
}
fn write_screenshot(&self, path: &std::path::Path, buffer: &wgpu::Buffer) -> Result<[u32; 2]> {
let (width, height, pixels) = self.read_frame(buffer)?;
write_png(path, width, height, &pixels)?;
Ok([width, height])
}
/// GameRenderer.takeAutoScreenshot: the centre square, scaled to 64x64.
fn write_world_icon(&self, path: &std::path::Path, buffer: &wgpu::Buffer) -> Result<()> {
let (width, height, pixels) = self.read_frame(buffer)?;
write_png(path, 64, 64, &world_icon_pixels(&pixels, width, height))
}
fn read_frame(&self, buffer: &wgpu::Buffer) -> Result<(u32, u32, Vec<u8>)> {
if !self.config.usage.contains(wgpu::TextureUsages::COPY_SRC) {
return Err("this surface does not support frame readback".into());
}
@@ -5350,17 +5383,51 @@ impl Renderer {
}
}
buffer.unmap();
let mut encoder = png::Encoder::new(
std::io::BufWriter::new(std::fs::File::create(path)?),
width,
height,
);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
encoder.write_header()?.write_image_data(&pixels)?;
Ok([width, height])
Ok((width, height, pixels))
}
}
fn write_png(path: &std::path::Path, width: u32, height: u32, pixels: &[u8]) -> Result<()> {
let mut encoder = png::Encoder::new(
std::io::BufWriter::new(std::fs::File::create(path)?),
width,
height,
);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
encoder.write_header()?.write_image_data(pixels)?;
Ok(())
}
/// `NativeImage.resizeSubRectTo` from the screen's centre to 64x64. Vanilla
/// resamples with STB; this averages each destination texel's source block,
/// which differs by a texel where a block does not divide evenly.
fn world_icon_pixels(pixels: &[u8], width: u32, height: u32) -> Vec<u8> {
let side = width.min(height) as usize;
if side == 0 {
return vec![0; 64 * 64 * 4];
}
let (left, top) = ((width as usize - side) / 2, (height as usize - side) / 2);
let mut out = Vec::with_capacity(64 * 64 * 4);
for y in 0..64 {
let y0 = y * side / 64;
let y1 = ((y + 1) * side).div_ceil(64).max(y0 + 1).min(side);
for x in 0..64 {
let x0 = x * side / 64;
let x1 = ((x + 1) * side).div_ceil(64).max(x0 + 1).min(side);
let mut sum = [0u32; 4];
for sy in y0..y1 {
for sx in x0..x1 {
let i = ((top + sy) * width as usize + left + sx) * 4;
for c in 0..4 {
sum[c] += u32::from(pixels[i + c]);
}
}
}
let n = ((y1 - y0) * (x1 - x0)) as u32;
out.extend(sum.map(|c| (c / n) as u8));
}
}
out
}
/// Camera-relative clip planes (`Frustum`).
#[derive(Clone, Copy, Debug)]
pub struct Frustum {
@@ -5452,6 +5519,32 @@ fn frustum(m: Mat4) -> [Vec4; 6] {
#[cfg(test)]
mod tests {
#[test]
fn world_icon_crops_the_centre_and_scales_to_64() {
use super::world_icon_pixels;
// 128x64, red left half and blue right half: the centre square is
// 64x64 at x=32, so it keeps half of each colour.
let mut pixels = Vec::with_capacity(128 * 64 * 4);
for _ in 0..128 * 64 {
pixels.extend_from_slice(&[255, 0, 0, 255]);
}
for y in 0..64 {
for x in 64..128 {
let i = (y * 128 + x) * 4;
pixels[i..i + 4].copy_from_slice(&[0, 0, 255, 255]);
}
}
let icon = world_icon_pixels(&pixels, 128, 64);
assert_eq!(icon.len(), 64 * 64 * 4);
assert_eq!(&icon[..4], [255, 0, 0, 255]);
let last = (63 * 64 + 63) * 4;
assert_eq!(&icon[last..last + 4], [0, 0, 255, 255]);
// A source smaller than the icon repeats its texels rather than
// indexing past them.
let tiny = world_icon_pixels(&[1, 2, 3, 255], 1, 1);
assert!(tiny.as_chunks::<4>().0.iter().all(|p| p == &[1, 2, 3, 255]));
}
#[test]
fn surface_is_8_bit_and_not_srgb() {
use super::surface_format;
@@ -210,6 +210,16 @@ pub fn reduced_far_detail_suggested() -> bool {
REDUCED_SUGGESTED.load(std::sync::atomic::Ordering::Relaxed)
}
static HDR_OFFERED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
/// Whether the display path offers HDR output (HDR10 or scRGB): set when the renderer
/// opens its surface; the settings open HDR's screen only then.
pub fn offer_hdr(on: bool) {
HDR_OFFERED.store(on, std::sync::atomic::Ordering::Relaxed);
}
pub fn hdr_offered() -> bool {
HDR_OFFERED.load(std::sync::atomic::Ordering::Relaxed)
}
/// The farthest LOD Distance, in chunks, on integrated GPUs other than
/// Apple's: they share the CPU's memory bandwidth, and past it far terrain
/// costs them more frame time than it is worth.
@@ -130,8 +130,9 @@ impl Style {
label: &Text,
value: f32,
hover: bool,
active: bool,
) {
ui::slider(batch, atlas, x, y, w, label, value, hover, true);
ui::slider(batch, atlas, x, y, w, label, value, hover && active, active);
}
/// The cost tag and the description beside a control, wrapped to two lines.
pub fn description(
@@ -1,4 +1,4 @@
//! Horizon's graphics settings screens, Simple and Advanced: which settings
//! Horizon's graphics settings screens, Simple, Advanced and HDR: which settings
//! they show, in what groups, with what description and cost, and their
//! input. How they look comes from `graphics_style`.
use crate::{
@@ -14,6 +14,8 @@ use crate::{
pub enum Level {
Simple,
Advanced,
/// HDR output's own settings, opened from Advanced while HDR is on.
Hdr,
}
/// Horizon's own settings (`graphics::Graphics`).
@@ -37,6 +39,8 @@ pub enum H {
ResolutionScale,
Antialiasing,
Hdr,
/// Opens `Level::Hdr`.
HdrSettings,
HdrPaperWhite,
HdrPeak,
BatteryMode,
@@ -119,6 +123,24 @@ pub fn rows(level: Level) -> Vec<Row> {
);
let mut rows = match level {
Level::Simple => vec![preset, render, lod, far, fov, fps, vsync, gui],
Level::Hdr => vec![
Row::Header("HDR"),
h(
H::Hdr,
None,
"Lava, fire and sunlight shine brighter than white. Needs an HDR display.",
),
h(
H::HdrPaperWhite,
None,
"How bright white paper and menus look. Match your desktop.",
),
h(
H::HdrPeak,
None,
"How bright the brightest highlights get. Your display's peak.",
),
],
Level::Advanced => vec![
Row::Header("Rendering"),
render,
@@ -224,20 +246,10 @@ pub fn rows(level: Level) -> Vec<Row> {
"FXAA smooths jagged edges of the world; slightly softer image.",
),
h(
H::Hdr,
H::HdrSettings,
None,
"HDR output on an HDR display: lava, fire and sunlight shine brighter.",
),
h(
H::HdrPaperWhite,
None,
"HDR: how bright white paper and menus look. Match your desktop.",
),
h(
H::HdrPeak,
None,
"HDR: how bright the brightest highlights get. Your display's peak.",
),
h(
H::PresentMode,
None,
@@ -385,6 +397,7 @@ impl H {
H::ResolutionScale => labelled("Resolution Scale", format!("{}%", g.resolution_scale)),
H::Antialiasing => labelled("Anti-aliasing", if g.fxaa { "FXAA" } else { "Off" }),
H::Hdr => labelled("HDR", on_off(g.hdr)),
H::HdrSettings => Text::literal("HDR Settings..."),
H::HdrPaperWhite => labelled("Paper White", format!("{} nits", g.hdr_paper_white)),
H::HdrPeak => labelled("Peak Brightness", format!("{} nits", g.hdr_peak)),
H::BatteryMode => labelled("Battery Mode", on_off(g.battery)),
@@ -471,6 +484,25 @@ impl Item {
fn available(self) -> bool {
!matches!(self, Item::Vanilla(o) if !o.available())
}
/// Why a control can't be used now, when it can't: HDR's screen opens only where
/// the display offers HDR output, whatever `horizon-graphics.txt` says (a copied game
/// folder, a display swapped for an SDR one: the renderer leaves HDR off there, and
/// the saved values wait for an HDR display), and its sliders do something only
/// while it is on.
fn blocked(self, s: &Settings) -> Option<&'static str> {
if !self.available() {
return Some("This feature is not yet available in Horizon.");
}
match self {
Item::Horizon(H::HdrSettings | H::Hdr) if !crate::graphics::hdr_offered() => {
Some("Your display or system doesn't offer HDR output.")
}
Item::Horizon(H::HdrPaperWhite | H::HdrPeak) if !s.horizon.hdr => {
Some("Turn HDR on to use it.")
}
_ => None,
}
}
/// On/off settings and their value.
pub fn switch(self, s: &Settings) -> Option<bool> {
let g = &s.horizon;
@@ -572,6 +604,7 @@ pub enum Action {
Changed,
OpenAdvanced,
OpenSimple,
OpenHdr,
Done,
}
@@ -649,6 +682,17 @@ impl GraphicsUi {
let other = match level {
Level::Simple => Action::OpenAdvanced,
Level::Advanced => Action::OpenSimple,
// One Done, back to Advanced.
Level::Hdr => {
widgets.push(Widget {
x: cx - 100.0,
y: by,
w: 200.0,
target: Target::Button(Action::Done),
list: false,
});
return (widgets, headers, content);
}
};
widgets.push(Widget {
x: cx - 155.0,
@@ -710,12 +754,10 @@ impl GraphicsUi {
continue;
}
let label = item.label(s);
let live = item.available();
if hover && !live {
tooltip = Some(atlas.font.split(
&Text::literal("This feature is not yet available in Horizon."),
170.0,
));
let blocked = item.blocked(s);
let live = blocked.is_none();
if hover && let Some(why) = blocked {
tooltip = Some(atlas.font.split(&Text::literal(why), 170.0));
}
match item.slider(s) {
Some(value) => STYLE.slider(
@@ -725,6 +767,7 @@ impl GraphicsUi {
&label,
value,
hover || self.dragging == Some(item),
live,
),
None => STYLE.button(
batch,
@@ -774,6 +817,7 @@ impl GraphicsUi {
let title = match level {
Level::Simple => "Graphics",
Level::Advanced => "Graphics: Advanced",
Level::Hdr => "Graphics: HDR",
};
STYLE.title(batch, atlas, &Text::literal(title));
if let Some(lines) = tooltip {
@@ -817,7 +861,10 @@ impl GraphicsUi {
return match wd.target {
Target::Button(action) => vec![Action::ButtonClick, action],
// A disabled control does nothing: no sound, no preset change.
Target::Item(item) if !item.available() => Vec::new(),
Target::Item(item) if item.blocked(s).is_some() => Vec::new(),
Target::Item(Item::Horizon(H::HdrSettings)) => {
vec![Action::ButtonClick, Action::OpenHdr]
}
Target::Item(item) => {
if item.slider(s).is_some() {
self.dragging = Some(item);
@@ -847,6 +894,47 @@ impl GraphicsUi {
mod tests {
use super::*;
#[test]
fn hdr_settings_open_where_hdr_is_offered() {
let mut s = Settings::default();
let mut ui = GraphicsUi::default();
let size = (480.0, 2000.0);
let at = |ui: &mut GraphicsUi, level, item| {
let (widgets, _, _) = ui.layout(level, size.0, size.1);
let w = widgets
.iter()
.find(|w| matches!(w.target, Target::Item(i) if i == item))
.unwrap();
(w.x + 1.0, w.y + 1.0)
};
let button = at(&mut ui, Level::Advanced, Item::Horizon(H::HdrSettings));
let click = |ui: &mut GraphicsUi, s: &mut Settings, level, point| {
ui.click(level, s, point, size, [480, 2000], false, false)
};
crate::graphics::offer_hdr(false);
assert!(click(&mut ui, &mut s, Level::Advanced, button).is_empty());
// Not even with HDR saved on, from another computer.
s.horizon.hdr = true;
assert!(click(&mut ui, &mut s, Level::Advanced, button).is_empty());
s.horizon.hdr = false;
crate::graphics::offer_hdr(true);
assert_eq!(
click(&mut ui, &mut s, Level::Advanced, button),
[Action::ButtonClick, Action::OpenHdr]
);
// On HDR's screen, the sliders wait for the switch above them.
let peak = at(&mut ui, Level::Hdr, Item::Horizon(H::HdrPeak));
assert!(click(&mut ui, &mut s, Level::Hdr, peak).is_empty());
s.horizon.hdr = true;
assert_eq!(click(&mut ui, &mut s, Level::Hdr, peak), [Action::Changed]);
let on = |level, item| {
extra_rows(level)
.iter()
.any(|r| matches!(r, Row::Setting(x) if x.item == Item::Horizon(item)))
};
assert!(on(Level::Hdr, H::Hdr) && !on(Level::Advanced, H::Hdr));
assert!(on(Level::Hdr, H::HdrPeak) && !on(Level::Advanced, H::HdrPeak));
}
#[test]
fn graphics_buttons_sound_on_press_and_sliders_on_release() {
let mut s = Settings::default();
File renamed without changes.
@@ -1107,6 +1107,33 @@ impl HudBatch {
}
cursor - x
}
/// SubmitNodeCollection.submitNameTag's text in its own space (x right,
/// y down, in GUI pixels): centred at `-Font.width / 2`, drawn as
/// Font.prepareText draws any text (styles, fonts and glyph boxes; no
/// shadow), over `background` (RGBA) unless its alpha is 0. Returns how
/// many vertices at the start of the batch are the background.
pub fn name_tag(
&mut self,
atlas: &HudAtlas,
text: &crate::text::Text,
y: f32,
color: u32,
background: u32,
) -> usize {
let x = -atlas.font.width_text(text) / 2.0;
let start = self.vertices.len();
let advance = self.text_styled(atlas, text, x, y, color, false);
if background >> 24 == 0 {
return 0;
}
// PreparedTextBuilder.markBackground: one pixel round the advances,
// from a pixel above the line to 9 below its top.
let glyphs = self.vertices.split_off(start);
self.fill(atlas, x - 1.0, y - 1.0, advance + 1.0, 10.0, background);
let behind = self.vertices.len() - start;
self.vertices.extend(glyphs);
behind
}
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
@@ -1822,6 +1849,65 @@ mod tests {
assert!(missing.rect.is_none() && missing.right == 5.0);
}
#[test]
fn name_tags_draw_bold_text_and_tall_glyphs_like_any_text() {
// `ascii.png`'s 8x8 'a' (4 pixels wide) and `accented.png`'s 9x12
// 'й' (height 12, ascent 10: the breve above the line), 7 wide.
let root = std::env::temp_dir().join(format!("horizon-name-tag-{}", std::process::id()));
let textures = root.join("assets/minecraft/textures/font");
fs::create_dir_all(root.join("assets/minecraft/font")).unwrap();
fs::create_dir_all(&textures).unwrap();
fs::write(
root.join("assets/minecraft/font/default.json"),
r#"{"providers":[
{"type":"bitmap","file":"minecraft:font/a.png","ascent":7,"chars":["a"]},
{"type":"bitmap","file":"minecraft:font/tall.png","height":12,"ascent":10,"chars":["й"]}]}"#,
)
.unwrap();
for (name, (w, h), used) in [("a", (8, 8), 4), ("tall", (9, 12), 7)] {
let pixels: Vec<u8> = (0..w * h)
.flat_map(|i| [255, 255, 255, if i % w < used { 255 } else { 0 }])
.collect();
let file = fs::File::create(textures.join(format!("{name}.png"))).unwrap();
let mut encoder = png::Encoder::new(file, w, h);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().unwrap();
writer.write_image_data(&pixels).unwrap();
}
let atlas = HudAtlas::build(&root).unwrap();
let _ = fs::remove_dir_all(&root);
let text = crate::text::Text::literal("aй").bold();
let mut batch = HudBatch::new([1, 1]);
let behind = batch.name_tag(&atlas, &text, 0.0, u32::MAX, 0x3F00_0000);
// Each quad's box: x0, y0, x1, y1.
let boxes: Vec<[f32; 4]> = batch
.vertices
.as_chunks::<6>()
.0
.iter()
.map(|q| [q[0].pos[0], q[0].pos[1], q[2].pos[0], q[2].pos[1]])
.collect();
// Advances 5 and 8, plus 1 each for bold: Font.width 15, centred.
let x = -7.5;
// Each glyph twice (bold), the copy a pixel right; 'й' in its 9x12
// box from 3 pixels above the line, each bold quad 0.1 larger.
let near = |a: [f32; 4], b: [f32; 4]| a.iter().zip(b).all(|(a, b)| (a - b).abs() < 1e-4);
let expected = [
[x - 0.1, -0.1, x + 8.1, 8.1],
[x + 0.9, -0.1, x + 9.1, 8.1],
[x + 5.9, -3.1, x + 15.1, 9.1],
[x + 6.9, -3.1, x + 16.1, 9.1],
];
for (got, want) in boxes[1..].iter().zip(expected) {
assert!(near(*got, want), "{got:?} != {want:?}");
}
assert_eq!(boxes.len(), 1 + expected.len());
// The background first: a pixel round the advances, 10 pixels tall.
assert_eq!(behind, 6);
assert_eq!(boxes[0], [x - 1.0, -1.0, x + 15.0, 9.0]);
}
#[test]
fn effect_icons_are_painted_from_the_mob_effect_directory() {
use crate::{effects::EffectInstance, hud_layout, hud_overlays, inventory_effects};
File renamed without changes.
+113
View File
@@ -0,0 +1,113 @@
//! The client's world data, rendering and screens: chunks, meshing, light, streaming, the renderer,
//! the HUD, menus and settings. Gameplay (horizon-client) builds on it; nothing here reaches gameplay.
#![forbid(unsafe_code)]
pub mod assets;
pub mod atlas_sources;
pub mod audio;
pub mod auth;
pub mod bidi;
pub mod biome;
pub mod block_entities;
pub mod block_entity_anim;
pub mod block_entity_render;
pub mod border_render;
pub mod breaking;
pub mod brewing;
pub mod camera;
pub mod chat;
pub mod chunk;
pub mod chunk_map;
pub mod clouds;
pub mod collisions;
pub mod command_block;
pub mod containers;
pub mod crash;
pub mod dynamic_light;
pub mod effects;
pub mod environment;
pub mod extras;
pub mod fast_hash;
pub mod fog;
pub mod font;
pub mod gfx;
pub mod graphics;
pub mod graphics_style;
pub mod graphics_ui;
pub mod hud;
pub mod hud_layout;
pub mod hud_overlays;
pub mod inventory_effects;
pub mod java_text;
pub mod level_loading;
pub mod light;
pub mod lod;
pub mod logging;
pub mod menu_blur;
pub mod mesher;
pub mod minimap;
pub mod mipmap;
pub mod mod_list;
pub mod model_library;
pub mod models;
pub mod options_file;
pub mod options_fixes;
pub mod options_ui;
pub mod overlay;
pub mod pack_screen;
pub mod packs;
pub mod par;
pub mod particles;
pub mod poi_sections;
pub mod providers;
pub mod screen_draw;
pub mod screen_effects;
pub mod sculk;
pub mod settings;
pub mod shader_pack_list;
pub mod signs;
pub mod skins;
pub mod sound_engine;
pub mod sound_events;
pub mod spans;
pub mod sprites;
pub mod streaming;
pub mod tab_list;
pub mod telemetry;
pub mod testgen;
pub mod title_edition;
pub mod trims;
pub mod ui;
pub mod vault;
pub mod weather;
pub mod widgets;
pub mod world;
pub mod worldborder;
pub mod zip;
pub use horizon_client_core::Result;
pub use horizon_client_core::block_entity_nbt;
pub use horizon_client_core::bossbar;
pub use horizon_client_core::component;
pub use horizon_client_core::fastutil_order;
pub use horizon_client_core::gamerules;
pub use horizon_client_core::lang;
pub use horizon_client_core::nbt_path;
pub use horizon_client_core::scoreboard;
pub use horizon_client_core::snbt;
pub use horizon_client_core::text;
pub use horizon_client_input::clipboard;
pub use horizon_client_input::gamepad;
pub use horizon_client_input::input;
pub use horizon_client_input::interaction_input;
pub use horizon_client_input::key_names;
pub use horizon_client_input::keybinds;
pub use horizon_client_input::keymapping;
pub use horizon_client_input::mouse;
pub use horizon_client_input::window;
pub use horizon_command as command;
pub use horizon_items::blocks;
pub use horizon_items::bundle;
pub use horizon_items::cooldowns;
pub use horizon_items::enchant;
pub use horizon_items::item_data;
pub use horizon_items::items;
pub use horizon_items::random_sequences;
File renamed without changes.
@@ -56,7 +56,7 @@ pub struct Column {
impl Column {
/// Reduce an entire horizontal footprint, rather than inflating its corner
/// column (a single leaf used to become a whole distant tree-sized slab).
pub(crate) fn reduced(columns: &[Column; 256], x: usize, z: usize, width: usize) -> Self {
pub fn reduced(columns: &[Column; 256], x: usize, z: usize, width: usize) -> Self {
if width == 1 {
return columns[z * 16 + x].clone();
}
@@ -139,7 +139,7 @@ impl Column {
}
column
}
pub(crate) fn push(&mut self, span: Span) {
pub fn push(&mut self, span: Span) {
if let Some(last) = self.spans.last_mut()
&& last.max == span.min
&& last.state == span.state
Loaded 100 of 206 files, more files were not shown because too many files have changed in this diff. Show more